approval-md 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/dist/src/cli/amend.js
CHANGED
|
@@ -59,12 +59,14 @@ import { createHash } from "node:crypto";
|
|
|
59
59
|
import { accessSync, constants, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
60
60
|
import { tmpdir } from "node:os";
|
|
61
61
|
import { basename, dirname, isAbsolute, join, relative, resolve as resolvePathSegments, sep } from "node:path";
|
|
62
|
-
import { HUMAN_ACTOR_ENV, appendAttestation, checkAttestation,
|
|
62
|
+
import { HUMAN_ACTOR_ENV, appendAttestation, attestedPolicyPayloadHash, checkAttestation, policyBytesHash, resolveHumanActor, } from "../core/attest.js";
|
|
63
63
|
import { compareChains } from "../core/log-reconcile.js";
|
|
64
64
|
import { diffPolicies, renderDiff, SPEC_NAMESPACES } from "../core/policy-diff.js";
|
|
65
65
|
import { checkPolicyExpectations, describeFailure, describePinChange, diffPinSources, expectationsFor, DOGFOOD_SUITE_BUILT, DOGFOOD_SUITE_SOURCE, EXPECTATIONS_MODULE, } from "../core/policy-expectations.js";
|
|
66
66
|
import { loadPolicy, parseDuration, POLICY_FILENAMES, } from "../core/policy-load.js";
|
|
67
|
-
import {
|
|
67
|
+
import { payloadHash } from "../core/payload.js";
|
|
68
|
+
import { payloadPath, payloadStoreDirFor } from "../core/payload-store.js";
|
|
69
|
+
import { proposalPayloadValue, proposalState, proposeAttestation, } from "../core/policy-proposal.js";
|
|
68
70
|
import { readVerifiedRecords } from "../core/state.js";
|
|
69
71
|
import { boolFlag, parseFlags, stringFlag } from "./args.js";
|
|
70
72
|
import { EXIT_INTEGRITY, EXIT_IO, EXIT_OK, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
|
|
@@ -83,6 +85,9 @@ const FLAGS = {
|
|
|
83
85
|
"--require-load": "boolean",
|
|
84
86
|
"--dry-run": "boolean",
|
|
85
87
|
"--commit": "boolean",
|
|
88
|
+
// APRV-341: the whole ceremony in one word. Implies `--commit` and forces the
|
|
89
|
+
// branch flow, so the amendment reaches a protected trunk the only way it can.
|
|
90
|
+
"--pr": "boolean",
|
|
86
91
|
"--no-publish": "boolean",
|
|
87
92
|
"--branch": "string",
|
|
88
93
|
"--direct": "boolean",
|
|
@@ -946,7 +951,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
946
951
|
}
|
|
947
952
|
const dryRun = boolFlag(parsed.flags, "--dry-run");
|
|
948
953
|
const requireLoad = boolFlag(parsed.flags, "--require-load");
|
|
949
|
-
|
|
954
|
+
// APRV-341. `--pr` is `--commit` plus "and finish the job": the branch flow,
|
|
955
|
+
// whatever the protection probe thinks, because the operator has said which
|
|
956
|
+
// ceremony they want. `--commit` alone keeps letting the probe decide.
|
|
957
|
+
const wantPr = boolFlag(parsed.flags, "--pr");
|
|
958
|
+
const wantCommit = boolFlag(parsed.flags, "--commit") || wantPr;
|
|
950
959
|
// APRV-130: the ceremony publishes by default (push, and on a protected main
|
|
951
960
|
// branch + push + PR). `--no-publish` is the operator who wants it to stop at
|
|
952
961
|
// the commit, which is what `--commit` did before the publishing half existed.
|
|
@@ -960,6 +969,12 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
960
969
|
if (branchFlag !== null && branchFlag.trim().length === 0) {
|
|
961
970
|
return usageError(streams, json, "--branch expects a branch name");
|
|
962
971
|
}
|
|
972
|
+
if (wantPr && forceDirect) {
|
|
973
|
+
return usageError(streams, json, "--pr and --direct ask for opposite ceremonies: --pr publishes through a branch and a pull request, --direct commits on the branch you are standing on. Pass one of them");
|
|
974
|
+
}
|
|
975
|
+
if (wantPr && noPublish) {
|
|
976
|
+
return usageError(streams, json, "--pr and --no-publish ask for opposite ceremonies: --pr opens the pull request, --no-publish stops at the commit. Pass one of them");
|
|
977
|
+
}
|
|
963
978
|
// Identity first, before a byte is read. Asking a human to read a diff and
|
|
964
979
|
// only then telling them their sign-off cannot be attributed wastes the one
|
|
965
980
|
// resource this system spends.
|
|
@@ -1006,13 +1021,54 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1006
1021
|
return refuse(streams, json, "io", policy.message, EXIT_IO);
|
|
1007
1022
|
const policyPath = policy.path;
|
|
1008
1023
|
let liveSha256;
|
|
1024
|
+
/**
|
|
1025
|
+
* The live bytes as text, for the payload file this ceremony will commit
|
|
1026
|
+
* (APRV-356).
|
|
1027
|
+
*
|
|
1028
|
+
* One read for both, on the rule `core/attest.ts` states for the same pair:
|
|
1029
|
+
* two reads of a file an operator may still be editing can disagree, and the
|
|
1030
|
+
* digest reported in the report has to be the digest of the bytes whose store
|
|
1031
|
+
* file the `git add` names.
|
|
1032
|
+
*/
|
|
1033
|
+
let liveText;
|
|
1009
1034
|
try {
|
|
1010
|
-
|
|
1035
|
+
const liveBytes = readFileSync(policyPath);
|
|
1036
|
+
liveSha256 = policyBytesHash(liveBytes);
|
|
1037
|
+
liveText = liveBytes.toString("utf8");
|
|
1011
1038
|
}
|
|
1012
1039
|
catch (cause) {
|
|
1013
1040
|
return refuse(streams, json, "io", `policy ${policyPath} could not be read: ${detail(cause)}`, EXIT_IO);
|
|
1014
1041
|
}
|
|
1015
1042
|
const logPath = resolvePath(stringFlag(parsed.flags, "--log"), DEFAULT_LOG_PATH, cwd);
|
|
1043
|
+
/**
|
|
1044
|
+
* The store file holding the attested policy text, absolute (APRV-356).
|
|
1045
|
+
*
|
|
1046
|
+
* The ceremony commit carries it beside the policy and the log, because a
|
|
1047
|
+
* committed log carrying a binding whose payload file was never committed is
|
|
1048
|
+
* a chain whose in-force bytes are unrecoverable to every reader of the
|
|
1049
|
+
* committed copy, which is the state this task exists to end. The CI
|
|
1050
|
+
* protected-path guard resolves payloads out of committed trees, so that copy
|
|
1051
|
+
* is the one it reads.
|
|
1052
|
+
*
|
|
1053
|
+
* Which file it is depends on which door the attestation comes through, and
|
|
1054
|
+
* both are addressable from the bytes on disk before anything is appended:
|
|
1055
|
+
*
|
|
1056
|
+
* - The HUMAN path appends the attestation here, and since APRV-356 that
|
|
1057
|
+
* record binds `{ text }` (`core/attest.ts`).
|
|
1058
|
+
* - The AGENT path proposes and waits for a tap. The phone's attestation
|
|
1059
|
+
* stores nothing, which APRV-356 left unchanged; what carries the text on
|
|
1060
|
+
* that chain is the `policy.proposed` this ceremony appends, binding
|
|
1061
|
+
* `{ policy_path, text }` (`core/policy-proposal.ts`).
|
|
1062
|
+
*
|
|
1063
|
+
* Addressed from `liveText` rather than read back from the appended record,
|
|
1064
|
+
* so it is a ceremony file from the start: `--dry-run` names it in the `git
|
|
1065
|
+
* add` it would run, and `--commit` counts it as its own rather than as a
|
|
1066
|
+
* stray in the index. An edit landing between here and the append moves the
|
|
1067
|
+
* hash, and the append refuses on the digest before this path is used.
|
|
1068
|
+
*/
|
|
1069
|
+
const payloadFile = payloadPath(payloadStoreDirFor(logPath), agentActor === null
|
|
1070
|
+
? attestedPolicyPayloadHash(liveText)
|
|
1071
|
+
: payloadHash(proposalPayloadValue(policyPath, liveText)));
|
|
1016
1072
|
// (a-pre) The thirty-three seconds of silence, ended (APRV-167).
|
|
1017
1073
|
//
|
|
1018
1074
|
// Everything from here to the `Policy` block below is work the operator could
|
|
@@ -1108,7 +1164,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1108
1164
|
const onProtectedDefault = probe.protection === "protected" &&
|
|
1109
1165
|
probe.currentBranch !== null &&
|
|
1110
1166
|
probe.currentBranch === probe.defaultBranch;
|
|
1111
|
-
|
|
1167
|
+
// APRV-341: `--pr` is a third way into the branch flow, and it is the
|
|
1168
|
+
// operator's own instruction rather than an inference from a probe. A probe
|
|
1169
|
+
// that cannot reach GitHub answers `unknown`, which used to mean "commit in
|
|
1170
|
+
// place"; an operator who typed `--pr` has said what they want either way.
|
|
1171
|
+
const useBranch = branchFlag !== null || wantPr || (!forceDirect && onProtectedDefault);
|
|
1112
1172
|
const branchName = (seq) => branchFlag ?? `policy-amend-${seq}`;
|
|
1113
1173
|
// The direct flow's push is about to hit a protected branch. Say so before
|
|
1114
1174
|
// the human types it, rather than after GitHub says it.
|
|
@@ -1133,9 +1193,9 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1133
1193
|
*/
|
|
1134
1194
|
let pinsChange = null;
|
|
1135
1195
|
if (wantCommit && !dryRun) {
|
|
1136
|
-
const plan = planCommit(policyPath, logPath, useBranch ? { branch: branchFlag } : null);
|
|
1196
|
+
const plan = planCommit(policyPath, logPath, payloadFile, useBranch ? { branch: branchFlag } : null);
|
|
1137
1197
|
if (!plan.ok) {
|
|
1138
|
-
return refuse(streams, json,
|
|
1198
|
+
return refuse(streams, json, plan.code, plan.message, EXIT_USAGE);
|
|
1139
1199
|
}
|
|
1140
1200
|
commitPlan = plan.plan;
|
|
1141
1201
|
const prepared = prepareBase({ root: commitPlan.root, policyArg: commitPlan.policyArg, logPath, progress }, probe, attested?.sha256 ?? null);
|
|
@@ -1211,7 +1271,7 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1211
1271
|
pinsChange = pinsChangeIn(amendRoot, policyPath, "HEAD");
|
|
1212
1272
|
}
|
|
1213
1273
|
/**
|
|
1214
|
-
* The same command, with the
|
|
1274
|
+
* The same command, with the long absolute paths written the way the
|
|
1215
1275
|
* operator would type them.
|
|
1216
1276
|
*
|
|
1217
1277
|
* This is a HUMAN transform and nothing else: `--json`'s `git.commands` keeps
|
|
@@ -1224,7 +1284,9 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1224
1284
|
.split(policyPath)
|
|
1225
1285
|
.join(relPath(policyPath, cwd))
|
|
1226
1286
|
.split(logPath)
|
|
1227
|
-
.join(relPath(logPath, cwd))
|
|
1287
|
+
.join(relPath(logPath, cwd))
|
|
1288
|
+
.split(payloadFile)
|
|
1289
|
+
.join(relPath(payloadFile, cwd));
|
|
1228
1290
|
return pinsChange === null
|
|
1229
1291
|
? shortened
|
|
1230
1292
|
: shortened.split(pinsChange.path).join(relPath(pinsChange.path, cwd));
|
|
@@ -1242,8 +1304,13 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1242
1304
|
};
|
|
1243
1305
|
const summary = summarize(policyPath, diff, pinsChange);
|
|
1244
1306
|
// APRV-274: the pins file is a member of the `git add` exactly when it moved,
|
|
1245
|
-
// so a copied command lands the same three (or
|
|
1246
|
-
const ceremonyFiles = [
|
|
1307
|
+
// so a copied command lands the same three (or four) files the verb would.
|
|
1308
|
+
const ceremonyFiles = [
|
|
1309
|
+
policyPath,
|
|
1310
|
+
logPath,
|
|
1311
|
+
...(pinsChange === null ? [] : [pinsChange.path]),
|
|
1312
|
+
payloadFile,
|
|
1313
|
+
];
|
|
1247
1314
|
const commitCommands = (seq) => [
|
|
1248
1315
|
`git add ${ceremonyFiles.join(" ")}`,
|
|
1249
1316
|
`git commit -m ${JSON.stringify(`Policy: ${summary} (attested seq ${seq})`)}`,
|
|
@@ -1251,15 +1318,31 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1251
1318
|
const gitCommands = (seq) => {
|
|
1252
1319
|
// APRV-203: `--commit` runs none of these; it assembles the commit on the
|
|
1253
1320
|
// remote's tip without a checkout. These are the HAND procedure, and they
|
|
1254
|
-
// start where `--commit` starts:
|
|
1255
|
-
// on a local trunk that has fallen behind.
|
|
1321
|
+
// start where `--commit` starts: with the checkout brought current, so the
|
|
1322
|
+
// branch is not built on a local trunk that has fallen behind.
|
|
1323
|
+
//
|
|
1324
|
+
// APRV-360: and they never switch branches. The form printed before this
|
|
1325
|
+
// opened with `git checkout -b policy-amend-<seq> origin/main`, which is
|
|
1326
|
+
// the shape that forked the log on 2026-09-16 and that refused outright on
|
|
1327
|
+
// 2026-09-18, when the primary's main was fourteen commits behind and the
|
|
1328
|
+
// switch would have overwritten QUEUE.md, the working log and six
|
|
1329
|
+
// payloads. A branch switch in the primary is never the right answer: the
|
|
1330
|
+
// daemon is appending to the working log while the operator reads this.
|
|
1331
|
+
//
|
|
1332
|
+
// What replaces it is what the 2026-09-18 recovery actually did. `approval
|
|
1333
|
+
// log sync` brings the checkout current (and refuses `log-diverged` rather
|
|
1334
|
+
// than fast-forwarding over a fork), the commit is made where the operator
|
|
1335
|
+
// is standing, and the branch is created on the REMOTE by refspec, so it
|
|
1336
|
+
// exists without any local ref ever being checked out.
|
|
1256
1337
|
if (useBranch) {
|
|
1338
|
+
const branch = branchName(seq);
|
|
1339
|
+
const base = probe.defaultBranch === null ? "" : ` --base ${probe.defaultBranch}`;
|
|
1257
1340
|
return [
|
|
1258
|
-
"
|
|
1259
|
-
`git checkout -b ${branchName(seq)} origin/${probe.defaultBranch ?? "main"}`,
|
|
1341
|
+
"approval log sync",
|
|
1260
1342
|
...commitCommands(seq),
|
|
1261
|
-
`git push
|
|
1262
|
-
`gh pr create --title ${JSON.stringify(prTitle(summary, seq))} --body ${JSON.stringify(prBody(seq))}`,
|
|
1343
|
+
`git push origin HEAD:refs/heads/${branch}`,
|
|
1344
|
+
`gh pr create --title ${JSON.stringify(prTitle(summary, seq))} --body ${JSON.stringify(prBody(seq))} --head ${branch}${base}`,
|
|
1345
|
+
`gh pr merge ${branch} --auto --merge`,
|
|
1263
1346
|
];
|
|
1264
1347
|
}
|
|
1265
1348
|
return amendRoot === null
|
|
@@ -1506,6 +1589,7 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1506
1589
|
branch: null,
|
|
1507
1590
|
pushed: false,
|
|
1508
1591
|
prUrl: null,
|
|
1592
|
+
prUpdated: false,
|
|
1509
1593
|
autoMerge: "not-attempted",
|
|
1510
1594
|
steps: [],
|
|
1511
1595
|
stoppedAt: null,
|
|
@@ -1615,6 +1699,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1615
1699
|
commitPlan.policyArg,
|
|
1616
1700
|
commitPlan.logArg,
|
|
1617
1701
|
...(pinsChange === null ? [] : [pinsChange.arg]),
|
|
1702
|
+
// APRV-356: the attested policy text, so the commit carrying the
|
|
1703
|
+
// binding carries the bytes it binds. Without it the in-force policy is
|
|
1704
|
+
// unrecoverable for every reader of the committed copy, the CI guard
|
|
1705
|
+
// included.
|
|
1706
|
+
commitPlan.payloadArg,
|
|
1618
1707
|
],
|
|
1619
1708
|
message,
|
|
1620
1709
|
});
|
|
@@ -1669,6 +1758,47 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1669
1758
|
: `${commitSha.slice(0, 12)} on ${baseLabel} (held at ${anchor}; your checkout was not moved)`;
|
|
1670
1759
|
/** `gh pr create …` as the operator would type it, for both flows. */
|
|
1671
1760
|
const prCreateCommand = (head) => `gh pr create --title ${JSON.stringify(prTitle(summary, String(seq)))} --body ${JSON.stringify(prBody(String(seq)))} --head ${head}${probe.defaultBranch === null ? "" : ` --base ${probe.defaultBranch}`}`;
|
|
1761
|
+
/**
|
|
1762
|
+
* The open pull request for `head`, or `null` when there is none (APRV-341).
|
|
1763
|
+
*
|
|
1764
|
+
* Asked before `gh pr create`, because a second run of the ceremony onto a
|
|
1765
|
+
* branch that already carries one is the ordinary shape of "the ceremony
|
|
1766
|
+
* stopped half-way and I ran it again": `gh pr create` answers that with a
|
|
1767
|
+
* failure, and a failure there used to end the verb holding an attestation
|
|
1768
|
+
* it had already appended. A `gh` that cannot answer is `null`, which falls
|
|
1769
|
+
* through to `create` — the path that was there before this existed.
|
|
1770
|
+
*/
|
|
1771
|
+
const openPrFor = (head) => {
|
|
1772
|
+
const listed = gh(["pr", "list", "--head", head, "--state", "open", "--json", "url"], commitPlan.root);
|
|
1773
|
+
if (!listed.ok)
|
|
1774
|
+
return null;
|
|
1775
|
+
const text = listed.stdout.trim();
|
|
1776
|
+
if (text.length === 0)
|
|
1777
|
+
return null;
|
|
1778
|
+
let rows;
|
|
1779
|
+
try {
|
|
1780
|
+
rows = JSON.parse(text);
|
|
1781
|
+
}
|
|
1782
|
+
catch {
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
if (!Array.isArray(rows))
|
|
1786
|
+
return null;
|
|
1787
|
+
const first = rows[0];
|
|
1788
|
+
if (typeof first !== "object" || first === null)
|
|
1789
|
+
return null;
|
|
1790
|
+
const url = first.url;
|
|
1791
|
+
return typeof url === "string" && url.length > 0 ? url : null;
|
|
1792
|
+
};
|
|
1793
|
+
const prEditArgs = (head) => [
|
|
1794
|
+
"pr",
|
|
1795
|
+
"edit",
|
|
1796
|
+
head,
|
|
1797
|
+
"--title",
|
|
1798
|
+
prTitle(summary, String(seq)),
|
|
1799
|
+
"--body",
|
|
1800
|
+
prBody(String(seq)),
|
|
1801
|
+
];
|
|
1672
1802
|
const prCreateArgs = (head) => [
|
|
1673
1803
|
"pr",
|
|
1674
1804
|
"create",
|
|
@@ -1714,11 +1844,22 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1714
1844
|
publishing.pushed = true;
|
|
1715
1845
|
output = `${output}\n${`${push.stdout}${push.stderr}`.trim()}`.trim();
|
|
1716
1846
|
if (ghAvailable(commitPlan.root)) {
|
|
1717
|
-
|
|
1718
|
-
|
|
1847
|
+
// APRV-341: open, or update the one that is already open for this
|
|
1848
|
+
// branch. A ceremony re-run onto a branch a pull request already stands
|
|
1849
|
+
// on is not an error to report, it is the state to bring up to date.
|
|
1850
|
+
const standing = openPrFor(branch);
|
|
1851
|
+
const pr = standing === null
|
|
1852
|
+
? gh(prCreateArgs(branch), commitPlan.root)
|
|
1853
|
+
: gh(prEditArgs(branch), commitPlan.root);
|
|
1854
|
+
publishing.steps.push({
|
|
1855
|
+
command: standing === null ? prCreateCommand(branch) : `gh pr edit ${branch} --title … --body …`,
|
|
1856
|
+
ok: pr.ok,
|
|
1857
|
+
});
|
|
1719
1858
|
if (!pr.ok) {
|
|
1720
1859
|
const ghFailure = pr.stderr.trim() || pr.stdout.trim() || "gh did not run";
|
|
1721
|
-
return stalled("pr-failed",
|
|
1860
|
+
return stalled("pr-failed", standing === null
|
|
1861
|
+
? "`gh pr create` failed; the branch is already on origin"
|
|
1862
|
+
: "`gh pr edit` failed; the branch and its pull request are already on origin", pr, `the attestation was appended at seq ${seq}, committed on ${branch} and pushed, but \`gh pr create\` failed: ${ghFailure}; open the pull request by hand and merge it with a merge commit`, [
|
|
1722
1863
|
`attestation appended at seq ${seq}: it is in the log, on disk`,
|
|
1723
1864
|
`committed on ${branch} and PUSHED: origin has the branch`,
|
|
1724
1865
|
"no pull request: origin's default branch still carries the previous policy",
|
|
@@ -1727,8 +1868,12 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1727
1868
|
{ command: `gh pr merge ${branch} --merge`, note: "or merge it in the web UI" },
|
|
1728
1869
|
], 0, [MERGE_COMMIT_LINE]);
|
|
1729
1870
|
}
|
|
1730
|
-
|
|
1871
|
+
// `gh pr edit` prints the pull request's URL too, but the one this
|
|
1872
|
+
// ceremony already knows is the one it asked for; `create` has nothing
|
|
1873
|
+
// but its own output. Either way `null` is an honest answer.
|
|
1874
|
+
prUrl = standing ?? lastUrl(pr.stdout);
|
|
1731
1875
|
publishing.prUrl = prUrl;
|
|
1876
|
+
publishing.prUpdated = standing !== null;
|
|
1732
1877
|
publishing.complete = true;
|
|
1733
1878
|
// APRV-130: the ceremony offers to finish the last step too.
|
|
1734
1879
|
armAutoMerge(commitPlan.root, branch);
|
|
@@ -1865,7 +2010,10 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1865
2010
|
// APRV-274: the headline names what the commit actually carries. A reader
|
|
1866
2011
|
// who is told "the policy and the log" and finds a third file in the diff
|
|
1867
2012
|
// has been told something false about the one commit that must not lie.
|
|
1868
|
-
|
|
2013
|
+
// APRV-356 added the fourth: the store file holding the attested bytes.
|
|
2014
|
+
const carried = pinsChange === null
|
|
2015
|
+
? "the policy, the log and the attested policy text"
|
|
2016
|
+
: "the policy, the log, the pins and the attested policy text";
|
|
1869
2017
|
const done = [
|
|
1870
2018
|
branch === null
|
|
1871
2019
|
? `${st.glyph("ok")} committed ${carried} together:`
|
|
@@ -1958,6 +2106,8 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1958
2106
|
* across two commits and a hand-run cherry-pick.
|
|
1959
2107
|
*/
|
|
1960
2108
|
function planCommit(policyPath, logPath,
|
|
2109
|
+
/** The store file holding the attested policy text, absolute (APRV-356). */
|
|
2110
|
+
payloadFile,
|
|
1961
2111
|
/**
|
|
1962
2112
|
* The branch flow's preconditions, checked here for the same reason: an
|
|
1963
2113
|
* `origin` that does not exist, or a branch name already taken, would fail
|
|
@@ -1971,11 +2121,16 @@ branchFlow) {
|
|
|
1971
2121
|
if (root === null) {
|
|
1972
2122
|
return {
|
|
1973
2123
|
ok: false,
|
|
2124
|
+
code: "commit-preconditions",
|
|
1974
2125
|
message: `--commit needs a git repository and ${policyPath} is not inside one; nothing was attested`,
|
|
1975
2126
|
};
|
|
1976
2127
|
}
|
|
1977
2128
|
const policyArg = repoPath(root, policyPath);
|
|
1978
2129
|
const logArg = repoPath(root, logPath);
|
|
2130
|
+
// APRV-356. The payload store is a sibling of the log directory, so a log
|
|
2131
|
+
// inside the repository puts this inside it too; the check below covers both
|
|
2132
|
+
// with one sentence rather than naming a path no operator chose.
|
|
2133
|
+
const payloadArg = repoPath(root, payloadFile);
|
|
1979
2134
|
// APRV-274. `null` where these pins do not govern this policy, and where the
|
|
1980
2135
|
// repository simply has no pins module: in both cases the ceremony's file set
|
|
1981
2136
|
// is the two it always was.
|
|
@@ -1985,32 +2140,59 @@ branchFlow) {
|
|
|
1985
2140
|
if (policyArg.startsWith("../") || logArg.startsWith("../")) {
|
|
1986
2141
|
return {
|
|
1987
2142
|
ok: false,
|
|
2143
|
+
code: "commit-preconditions",
|
|
1988
2144
|
message: `--commit needs the policy (${policyPath}) and the log (${logPath}) inside the same repository (${root}); nothing was attested`,
|
|
1989
2145
|
};
|
|
1990
2146
|
}
|
|
1991
2147
|
const status = git(["status", "--porcelain"], root);
|
|
1992
2148
|
if (!status.ok) {
|
|
1993
|
-
return {
|
|
2149
|
+
return {
|
|
2150
|
+
ok: false,
|
|
2151
|
+
code: "commit-preconditions",
|
|
2152
|
+
message: `--commit could not read git status: ${status.stderr.trim()}`,
|
|
2153
|
+
};
|
|
1994
2154
|
}
|
|
2155
|
+
const ceremonyFiles = [policyArg, logArg, payloadArg, ...(pinsArg === null ? [] : [pinsArg])];
|
|
2156
|
+
const carried = pinsArg === null
|
|
2157
|
+
? "the policy, the log and the attested policy text"
|
|
2158
|
+
: `the policy, the log, the attested policy text and ${pinsArg}`;
|
|
1995
2159
|
const strays = [];
|
|
2160
|
+
/** Ceremony files staged in one state and left in another (APRV-341). */
|
|
2161
|
+
const split = [];
|
|
1996
2162
|
for (const line of status.stdout.split("\n")) {
|
|
1997
2163
|
if (line.trim().length === 0)
|
|
1998
2164
|
continue;
|
|
1999
2165
|
const index = line[0] ?? " ";
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2166
|
+
const worktree = line[1] ?? " ";
|
|
2167
|
+
const path = line.slice(3).trim();
|
|
2168
|
+
const ceremony = ceremonyFiles.includes(path);
|
|
2169
|
+
// A ceremony file the operator staged and then edited again. The commit is
|
|
2170
|
+
// assembled from the working tree, so the bytes it would carry are not the
|
|
2171
|
+
// bytes `git diff --cached` shows, and an amendment is the one commit that
|
|
2172
|
+
// may not be a surprise to the person who signed it.
|
|
2173
|
+
if (ceremony && index !== " " && index !== "?" && worktree === "M")
|
|
2174
|
+
split.push(path);
|
|
2175
|
+
// Only the INDEX column matters for everything else: an unstaged or
|
|
2176
|
+
// untracked file elsewhere is not going into this commit (the scratch index
|
|
2177
|
+
// lays exactly the ceremony paths over the remote's tree), and refusing
|
|
2178
|
+
// over one would make the verb unusable in any working repository.
|
|
2003
2179
|
if (index === " " || index === "?")
|
|
2004
2180
|
continue;
|
|
2005
|
-
|
|
2006
|
-
if (path === policyArg || path === logArg || path === pinsArg)
|
|
2181
|
+
if (ceremony)
|
|
2007
2182
|
continue;
|
|
2008
2183
|
strays.push(path);
|
|
2009
2184
|
}
|
|
2185
|
+
if (split.length > 0) {
|
|
2186
|
+
return {
|
|
2187
|
+
ok: false,
|
|
2188
|
+
code: "dirty-tree",
|
|
2189
|
+
message: `--commit refuses: ${split.join(", ")} ${split.length === 1 ? "is" : "are"} staged in one state and modified again in the working tree. The amendment commit is assembled from the WORKING TREE, so it would carry bytes your \`git diff --cached\` does not show, and the one commit that may not surprise the person who signed it is this one. Stage the file as it stands (\`git add ${split.join(" ")}\`) or unstage it, then run this again. Nothing was attested`,
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2010
2192
|
if (strays.length > 0) {
|
|
2011
|
-
const carried = pinsArg === null ? "the policy and the log" : `the policy, the log and ${pinsArg}`;
|
|
2012
2193
|
return {
|
|
2013
2194
|
ok: false,
|
|
2195
|
+
code: "staged-unrelated",
|
|
2014
2196
|
message: `--commit refuses: the index carries ${strays.length} staged change(s) beyond ${carried} (${strays.join(", ")}). The amendment commit carries EXACTLY those files, so that "this commit is the amendment" stays true. Unstage them, or drop --commit and run the printed commands yourself. Nothing was attested`,
|
|
2015
2197
|
};
|
|
2016
2198
|
}
|
|
@@ -2019,6 +2201,7 @@ branchFlow) {
|
|
|
2019
2201
|
if (!remote.ok) {
|
|
2020
2202
|
return {
|
|
2021
2203
|
ok: false,
|
|
2204
|
+
code: "commit-preconditions",
|
|
2022
2205
|
message: `--commit on a branch needs an "origin" remote to push to, and ${root} has none (${remote.stderr.trim()}); pass --direct to commit in place, or add the remote. Nothing was attested`,
|
|
2023
2206
|
};
|
|
2024
2207
|
}
|
|
@@ -2027,12 +2210,13 @@ branchFlow) {
|
|
|
2027
2210
|
if (exists.ok) {
|
|
2028
2211
|
return {
|
|
2029
2212
|
ok: false,
|
|
2213
|
+
code: "commit-preconditions",
|
|
2030
2214
|
message: `--branch ${branchFlow.branch} already exists in ${root}; the amendment branch is created fresh so it carries exactly one commit. Pick another name. Nothing was attested`,
|
|
2031
2215
|
};
|
|
2032
2216
|
}
|
|
2033
2217
|
}
|
|
2034
2218
|
}
|
|
2035
|
-
return { ok: true, plan: { root, policyArg, logArg, pinsArg } };
|
|
2219
|
+
return { ok: true, plan: { root, policyArg, logArg, pinsArg, payloadArg } };
|
|
2036
2220
|
}
|
|
2037
2221
|
/**
|
|
2038
2222
|
* Fetch the remote and establish that this ceremony may be based on it (APRV-203).
|