diff --git a/docs/repository-security.md b/docs/repository-security.md index 50c4ac26..5f3b492a 100644 --- a/docs/repository-security.md +++ b/docs/repository-security.md @@ -32,7 +32,7 @@ Enable merge commits and disable squash and rebase merges so stacked PR ancestry ## Mutation authority boundary -The broker's legacy `mutationMode`, `explicitInstruction`, and `exactTextConfirmed` request fields remain caller-provided policy assertions. They help a compliant agent avoid accidental writes, but they are not independently authenticated user consent. `authorityMode=off` does not change that: actions that require explicit lifecycle instruction or exact-text confirmation still need a verified host grant at execution. +The broker's legacy `mutationMode`, `explicitInstruction`, and `exactTextConfirmed` request fields remain caller-provided policy assertions. They help a compliant agent avoid accidental writes, but they are not independently authenticated user consent. Legacy Ed25519 `gd1` verification remains available through `GITHUB_DELIVERY_AUTHORITY_PUBLIC_KEY`. New issuers may instead provide a public-only algorithm-agile trust store through `GITHUB_DELIVERY_AUTHORITY_TRUST_STORE`. Invalid supplied grants fail closed instead of falling back to caller assertions. diff --git a/references/mutation-modes.md b/references/mutation-modes.md index 3baf54df..78176de6 100644 --- a/references/mutation-modes.md +++ b/references/mutation-modes.md @@ -37,13 +37,13 @@ The global user setting `authorityMode` has exactly three values: | authorityMode | Extra trusted-authority requirement at execution | |---|---| -| `off` | independent intent (explicit lifecycle instruction and exact-text consent) still required; other high-assurance writes skip Hello | +| `off` | none; explicit compatibility opt-out | | `high-assurance` | autonomous execution and registry actions marked `highAssurance` | | `all` | every executed GitHub mutation | The persistent user config defaults to `high-assurance`. It lives outside the installed skill directory so upgrading the skill cannot overwrite an explicit user choice. `GITHUB_DELIVERY_AUTHORITY_MODE=off|high-assurance|all` may override the persistent value for automation or diagnosis. The legacy `GITHUB_DELIVERY_REQUIRE_TRUSTED_AUTHORITY=1` switch remains supported and maps to the stricter `all` mode. -`off` is an explicit opt-out that skips Windows Hello for high-assurance writes that do **not** require independently authenticated lifecycle intent or exact-text consent. It does not mean “the agent can do anything.” Direct merge instruction, exact-text confirmation for human replies, expected-head checks, ownership checks, idempotency, workflow routing, ship gates, and all other mutation-policy rules remain mandatory. Caller-supplied `explicitInstruction` and `exactTextConfirmed` are never themselves that independent intent. +`off` is an explicit opt-out that means **no Windows Hello / trusted-authority prompt**. It does not mean “the agent can do anything.” Direct merge instruction, exact-text confirmation for human replies, expected-head checks, ownership checks, idempotency, workflow routing, ship gates, and all other mutation-policy rules remain mandatory. Dry-run planning never requires trusted authority. When the selected mode requires authority at `--execute`, the trusted grant must contain `scopeSha256`; a legacy resource-only signature is not enough. @@ -78,7 +78,7 @@ The canonical enabled high-assurance action set is listed below. CI verifies exa - `update_pr_body` -This keeps hostile repository text and model-selected mode inside the request layer. In the default `high-assurance` mode and in `all`, a protected write additionally needs an independently verified grant for the exact effect. An explicit `off` configuration skips that additional trusted-authority requirement for writes that are not independent-intent actions. Independent intent still requires a verified host grant at `--execute`. +This keeps hostile repository text and model-selected mode inside the request layer. In the default `high-assurance` mode and in `all`, a protected write additionally needs an independently verified grant for the exact effect. Only an explicit `off` configuration skips that additional trusted-authority requirement. A human reply always needs exact-text confirmation. When authority protection requires a grant, the grant additionally binds `exactTextSha256` to the exact outgoing body. Caller-supplied `exactTextConfirmed` is never itself trusted provenance. diff --git a/scripts/lib/mutation-execution-context.mjs b/scripts/lib/mutation-execution-context.mjs index 72ccf30a..0140af76 100644 --- a/scripts/lib/mutation-execution-context.mjs +++ b/scripts/lib/mutation-execution-context.mjs @@ -13,7 +13,6 @@ import { import { classifyMergeOutcome, readMergeState } from "./merge-outcome.mjs"; import { verifyMergeStackEligibility } from "./merge-stack-policy.mjs"; import { actionDefinition } from "./mutation-action-registry.mjs"; -import { mutationRequiresIndependentIntent } from "./mutation-policy.mjs"; import { boundedSpawnSync } from "./subprocess-policy.mjs"; import { readUserConfig, resolveAuthorityMode } from "./user-config.mjs"; @@ -109,7 +108,6 @@ export function mutationAuthorityOptions({ const modeRequiresAuthority = enforceHighAssurance === true && (authorityMode === "all" || - mutationRequiresIndependentIntent(request) || (authorityMode === "high-assurance" && mutationRequiresTrustedAuthority(request))); @@ -146,7 +144,6 @@ export function mutationAuthorityRequired( }); return ( authorityMode === "all" || - mutationRequiresIndependentIntent(request) || (authorityMode === "high-assurance" && mutationRequiresTrustedAuthority(request)) ); diff --git a/tests/unit/authority-mode-enforcement.test.mjs b/tests/unit/authority-mode-enforcement.test.mjs index 70be6d3b..4db75441 100644 --- a/tests/unit/authority-mode-enforcement.test.mjs +++ b/tests/unit/authority-mode-enforcement.test.mjs @@ -2,11 +2,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { - executeMutationWithAuthority, mutationAuthorityOptions, mutationAuthorityRequired, } from "../../scripts/lib/mutation-execution-context.mjs"; -import { mutationRequiresIndependentIntent } from "../../scripts/lib/mutation-policy.mjs"; import { executeMutationDocument } from "../../scripts/lib/mutation-document-execution.mjs"; function request(action = "merge_pr", extra = {}) { @@ -22,13 +20,9 @@ function request(action = "merge_pr", extra = {}) { }; } -test("off mode removes the trusted-authority requirement from high-assurance execution that is not independent intent", () => { - const comment = request("post_comment", { - body: "status update", - idempotencyKey: "comment-1", - }); +test("off mode removes the trusted-authority requirement from high-assurance execution", () => { const options = mutationAuthorityOptions({ - request: comment, + request: request(), enforceHighAssurance: true, env: {}, config: { schemaVersion: 1, authorityMode: "off" }, @@ -36,7 +30,7 @@ test("off mode removes the trusted-authority requirement from high-assurance exe assert.equal(options.authorityMode, "off"); assert.equal(options.requireTrustedAuthority, false); assert.equal( - mutationAuthorityRequired(comment, { + mutationAuthorityRequired(request(), { execute: true, env: {}, config: { schemaVersion: 1, authorityMode: "off" }, @@ -45,87 +39,6 @@ test("off mode removes the trusted-authority requirement from high-assurance exe ); }); -test("off mode still requires a trusted grant for explicit lifecycle intent and exact-text consent", () => { - const off = { schemaVersion: 1, authorityMode: "off" }; - const merge = request("merge_pr", { - expectedBase: "main", - expectedBaseOid: "b".repeat(40), - mergeMethod: "merge", - }); - const reply = request("reply_human_thread", { - mutationMode: "review", - exactTextConfirmed: true, - exactTextSha256: "a".repeat(64), - commentId: 77, - body: "Thanks, this is fixed.", - idempotencyKey: "reply-1", - explicitInstruction: false, - }); - - assert.equal(mutationRequiresIndependentIntent(merge), true); - assert.equal(mutationRequiresIndependentIntent(reply), true); - assert.equal(mutationRequiresIndependentIntent(request("post_comment")), false); - - for (const actionRequest of [merge, reply]) { - assert.equal( - mutationAuthorityRequired(actionRequest, { - execute: true, - env: {}, - config: off, - }), - true, - actionRequest.action, - ); - assert.equal( - mutationAuthorityOptions({ - request: actionRequest, - enforceHighAssurance: true, - env: {}, - config: off, - }).requireTrustedAuthority, - true, - actionRequest.action, - ); - } -}); - -test("off-mode execution cannot satisfy independent intent from caller-asserted booleans", () => { - const offEnv = { GITHUB_DELIVERY_AUTHORITY_MODE: "off" }; - for (const actionRequest of [ - request("merge_pr", { - expectedBase: "main", - expectedBaseOid: "b".repeat(40), - mergeMethod: "merge", - }), - request("reply_human_thread", { - mutationMode: "review", - exactTextConfirmed: true, - commentId: 77, - body: "Thanks, this is fixed.", - idempotencyKey: "reply-1", - explicitInstruction: false, - }), - ]) { - let calls = 0; - assert.throws( - () => - executeMutationWithAuthority({ - request: actionRequest, - execute: true, - env: offEnv, - config: { schemaVersion: 1, authorityMode: "off" }, - runner() { - calls += 1; - return { status: 0, stdout: "", stderr: "" }; - }, - }), - /trusted_authority_required/, - actionRequest.action, - ); - assert.equal(calls, 0, actionRequest.action); - } -}); - test("high-assurance mode preserves the intrinsic high-assurance and autonomous boundary", () => { assert.equal( mutationAuthorityRequired(request(), { @@ -195,58 +108,6 @@ test("legacy strict env still forces all mode", () => { assert.equal(options.requireTrustedAuthority, true); }); -test("mutation document prompts in off mode for exact-text human replies", () => { - let authorized = 0; - const result = executeMutationDocument({ - document: request("reply_human_thread", { - mutationMode: "review", - exactTextConfirmed: true, - commentId: 77, - body: "Thanks, this is fixed.", - idempotencyKey: "reply-1", - explicitInstruction: false, - }), - execute: true, - env: { GITHUB_DELIVERY_AUTHORITY_MODE: "off" }, - dependencies: { - planMutationWithAuthority() { - return { kind: "validated" }; - }, - refreshExpectedHeads({ requests }) { - return { requests, refreshed: [] }; - }, - authorizeBatchSync(requests) { - authorized += 1; - return { - batchId: "batch-1", - grants: requests.map((_, operation) => ({ - operation, - token: `gd1.token${operation}.signature`, - })), - }; - }, - attachAuthorityGrants(requests, authorization) { - return { - batchId: authorization.batchId, - requests: requests.map((entry, index) => ({ - ...entry, - authorityGrant: authorization.grants[index].token, - })), - }; - }, - stampAuthorizedReviewVerdicts(batch) { - return batch; - }, - executeMutationWithAuthority({ request: executedRequest }) { - return { action: executedRequest.action, grant: executedRequest.authorityGrant }; - }, - }, - }); - assert.equal(authorized, 1); - assert.equal(result.action, "reply_human_thread"); - assert.equal(result.grant, "gd1.token0.signature"); -}); - test("mutation document does not prompt in off mode even when the action is intrinsically high assurance", () => { let authorized = false; let executed = false; diff --git a/tests/unit/mutation-modes-docs.test.mjs b/tests/unit/mutation-modes-docs.test.mjs index 59adad89..9520eda4 100644 --- a/tests/unit/mutation-modes-docs.test.mjs +++ b/tests/unit/mutation-modes-docs.test.mjs @@ -62,10 +62,6 @@ test("bare full review selects review mode and follows configured authority prot assert.match(reference, /full review PR #32[\s\S]*→ `review`/); assert.match(reference, /full-review workflow[\s\S]*trusted authority is required by the default protection mode/i); assert.match(reference, /When `authorityMode` is explicitly `off`[\s\S]*does not require OS-backed provenance/i); - assert.match( - reference, - /`off`[\s\S]*independent(?:ly authenticated)?(?: lifecycle)? intent[\s\S]*exact-text/i, - ); assert.match(reference, /reports `trusted:false`/); });