Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/repository-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions references/mutation-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -78,7 +78,7 @@ The canonical enabled high-assurance action set is listed below. CI verifies exa
- `update_pr_body`
<!-- high-assurance-actions:end -->

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.

Expand Down
3 changes: 0 additions & 3 deletions scripts/lib/mutation-execution-context.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -109,7 +108,6 @@ export function mutationAuthorityOptions({
const modeRequiresAuthority =
enforceHighAssurance === true &&
(authorityMode === "all" ||
mutationRequiresIndependentIntent(request) ||
(authorityMode === "high-assurance" &&
mutationRequiresTrustedAuthority(request)));

Expand Down Expand Up @@ -146,7 +144,6 @@ export function mutationAuthorityRequired(
});
return (
authorityMode === "all" ||
mutationRequiresIndependentIntent(request) ||
(authorityMode === "high-assurance" &&
mutationRequiresTrustedAuthority(request))
);
Expand Down
145 changes: 3 additions & 142 deletions tests/unit/authority-mode-enforcement.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Expand All @@ -22,21 +20,17 @@ 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" },
});
assert.equal(options.authorityMode, "off");
assert.equal(options.requireTrustedAuthority, false);
assert.equal(
mutationAuthorityRequired(comment, {
mutationAuthorityRequired(request(), {
execute: true,
env: {},
config: { schemaVersion: 1, authorityMode: "off" },
Expand All @@ -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(), {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 0 additions & 4 deletions tests/unit/mutation-modes-docs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`/);
});

Expand Down
Loading