approval-md 0.1.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 +629 -559
- package/SPEC.md +99 -24
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +656 -0
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1944 -0
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +350 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +50 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +879 -0
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +80 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- 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 +2 -0
- package/dist/src/cli/codex.js +469 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +586 -17
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +107 -0
- package/dist/src/cli/help.js +320 -93
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +126 -0
- package/dist/src/cli/hook-codex.js +226 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +787 -0
- package/dist/src/cli/hook.js +1235 -181
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +159 -7
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- 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.d.ts +43 -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 +501 -0
- package/dist/src/cli/preflight.js +689 -45
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +126 -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-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +277 -0
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +204 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +119 -53
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +344 -11
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- 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/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.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/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.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/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +221 -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 +278 -0
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +635 -0
- package/dist/src/core/attest.js +326 -4
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +510 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/budgets.d.ts +238 -0
- 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/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +697 -0
- package/dist/src/core/command-class.js +713 -25
- 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/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -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 +432 -0
- 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 +206 -0
- 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 +455 -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 +871 -0
- package/dist/src/core/execute.js +59 -8
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1449 -0
- package/dist/src/core/gate.js +149 -14
- 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/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +4 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +310 -0
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +316 -0
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +160 -0
- package/dist/src/core/policy-explain.js +63 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +567 -0
- package/dist/src/core/policy-load.js +36 -6
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +324 -0
- package/dist/src/core/policy-match.js +72 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +317 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +566 -0
- package/dist/src/core/protected-path-guard.js +848 -55
- 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/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +371 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/seal.d.ts +165 -0
- 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/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +147 -0
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +476 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +17 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +1316 -63
- package/docs/codex-enforced-session.md +103 -0
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +14 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +539 -9
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- 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 +75 -3
- package/schema/values.schema.json +7 -11
- package/templates/codex/README.md +9 -0
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/docs/cli-reference.md
CHANGED
|
@@ -14,6 +14,10 @@ a gate refusal is exit 1 and not 2, approval events are exclusive to the manual
|
|
|
14
14
|
path, the raw token is shown once, a channel is transport — are stated once at
|
|
15
15
|
the top of `approval --help` and are not repeated here.
|
|
16
16
|
|
|
17
|
+
`approval --version`, `approval -v`, and `approval version` print the package
|
|
18
|
+
version and exit 0. These aliases apply only at the top level; a version-looking
|
|
19
|
+
flag after a verb remains that verb's argument.
|
|
20
|
+
|
|
17
21
|
Each section below is what the corresponding `--help` points at with its
|
|
18
22
|
`why: docs/cli-reference.md#…` footer.
|
|
19
23
|
|
|
@@ -35,11 +39,12 @@ resolved, and nothing is written.
|
|
|
35
39
|
|
|
36
40
|
## log
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
Four subcommands open the log for reading only. `verify` walks the hash chain
|
|
39
43
|
end to end and reports clean | torn-tail | corrupt, `tail` prints the last N
|
|
40
|
-
records (default 10),
|
|
41
|
-
byte
|
|
42
|
-
|
|
44
|
+
records (default 10), `export` streams every stored line to stdout byte for
|
|
45
|
+
byte, and `follow` emits verified records after an exclusive cursor before
|
|
46
|
+
waiting for appends. The default log path is `.approval/log/events.jsonl`,
|
|
47
|
+
relative to the working directory.
|
|
43
48
|
|
|
44
49
|
## log verify
|
|
45
50
|
|
|
@@ -297,6 +302,47 @@ that is due to a refusal of anything: it is a warning on `log verify`, a
|
|
|
297
302
|
doctor`'s `checkpoint` row. A gate that held up an action for want of a tap is a
|
|
298
303
|
gate whose operator turns the check off.
|
|
299
304
|
|
|
305
|
+
## log follow
|
|
306
|
+
|
|
307
|
+
`approval log follow --from <seq> --json` is the channel-independent decision
|
|
308
|
+
listener. `--from` is exclusive and defaults to zero, so a new consumer first
|
|
309
|
+
receives the entire verified log. Output is JSON Lines: one complete stored
|
|
310
|
+
event object per line. It emits every event type in chain order; a refund or
|
|
311
|
+
queue consumer selects the decisions relevant to its own work. The command then
|
|
312
|
+
remains in the foreground and exits 0 on SIGINT, SIGTERM, or a closed downstream
|
|
313
|
+
pipe. Signal cancellation may interrupt the final native stdout write. Consumers
|
|
314
|
+
must process only newline-terminated JSON records, discard any incomplete final
|
|
315
|
+
fragment, and reconnect from the cursor of the last complete record they
|
|
316
|
+
processed.
|
|
317
|
+
|
|
318
|
+
Filesystem notifications only prompt another read. On every notification wake
|
|
319
|
+
and every bounded 500ms fallback poll, the runtime rereads and verifies the
|
|
320
|
+
complete chain from genesis through the observed head, even when the log has not
|
|
321
|
+
changed. It emits nothing from a corrupt, torn, unreadable, truncated, or
|
|
322
|
+
cursor-mismatched snapshot. Pull backpressure means a slow consumer holds one
|
|
323
|
+
verified snapshot and no growing notification or event queue. Each check costs
|
|
324
|
+
O(N) verification time and O(N) snapshot memory for a log of N records. This
|
|
325
|
+
bounded initial implementation is not an incremental, low-overhead tail.
|
|
326
|
+
|
|
327
|
+
`--cursor-hash <64hex>` supplies the hash of record `--from`. Store both the
|
|
328
|
+
sequence and hash outside the log and pass both on reconnect. That binding
|
|
329
|
+
detects a truncated or replaced prefix. A sequence alone is a weaker bootstrap:
|
|
330
|
+
an internally valid rewritten prefix with the same sequence numbers cannot be
|
|
331
|
+
distinguished from the original, for the same reason an unanchored hash chain
|
|
332
|
+
cannot detect a fully recomputed forgery.
|
|
333
|
+
|
|
334
|
+
Delivery across reconnects is at least once. Apply the external effect first,
|
|
335
|
+
then persist the emitted event's `seq` and `hash`; a crash between those steps
|
|
336
|
+
replays the event. Consumers that require exactly-once external effects must
|
|
337
|
+
make their own effect idempotent or transact it with cursor storage. Persisting
|
|
338
|
+
the cursor before the effect instead risks silently losing that effect.
|
|
339
|
+
|
|
340
|
+
The stream uses the existing exit classes: 1 for corrupt or mismatched cursor,
|
|
341
|
+
2 for usage, 3 for a torn tail, and 4 for I/O. Its error object is written to
|
|
342
|
+
stderr and no event from the refused batch is written to stdout. `log follow`
|
|
343
|
+
is deliberately absent from MCP because one unbounded call would occupy the
|
|
344
|
+
finite MCP call queue; run it as a separate CLI process.
|
|
345
|
+
|
|
300
346
|
## log tail
|
|
301
347
|
|
|
302
348
|
The chain is verified first. On a torn tail the intact records are printed and the
|
|
@@ -344,6 +390,13 @@ did left conflict markers inside the log mid-ceremony.
|
|
|
344
390
|
So the ritual became deterministic code, on the `policy amend` precedent: when a
|
|
345
391
|
hand-ritual proves dangerous, it becomes a verb the gate can read.
|
|
346
392
|
|
|
393
|
+
**You rarely type it any more (APRV-346).** `approval up`'s preflight calls this
|
|
394
|
+
function itself whenever the working log is a byte-for-byte extension of the
|
|
395
|
+
committed one, which is the state every records advance leaves behind. What is
|
|
396
|
+
left for a hand-run `approval log sync` is the fork: two chains that share a
|
|
397
|
+
prefix and then carry different records at the same `seq`. See
|
|
398
|
+
[up](#up) for what the preflight checks before it delegates.
|
|
399
|
+
|
|
347
400
|
Everything runs inside ONE hold of the append lockfile. The lock is normally
|
|
348
401
|
taken per append; here it spans the whole operation, because an append landing
|
|
349
402
|
between the snapshot and the restore is exactly the interleaving that forks a
|
|
@@ -439,6 +492,26 @@ range they cover, and pushed to a short-lived records branch that exists for
|
|
|
439
492
|
exactly that commit. Main is protected here, so the commit reaches it through a
|
|
440
493
|
pull request; `--pr` opens that pull request through the ordinary `gh` path.
|
|
441
494
|
|
|
495
|
+
This verb carries the class `log.advance` for everyone who runs it, in a
|
|
496
|
+
session, in an orchestrator, or at a human terminal. The daemon's cadence
|
|
497
|
+
advance is the one that may carry `log.advance.daemon` instead, and only from
|
|
498
|
+
inside the daemon process: see the `--advance` paragraph under `daemon run`
|
|
499
|
+
(APRV-382).
|
|
500
|
+
|
|
501
|
+
`--co-author "Name <email>"` adds one validated `Co-authored-by` trailer to the
|
|
502
|
+
generated records commit and to the pull request body. When the day's pull
|
|
503
|
+
request already exists, the verb preserves its body and adds the trailer once.
|
|
504
|
+
This is display credit only. It does not set an event actor, name an approver,
|
|
505
|
+
grant authority, or derive an identity from the log. Omitting the flag preserves
|
|
506
|
+
the existing commit message, pull request body, and merge command byte for byte.
|
|
507
|
+
|
|
508
|
+
The merge queue ignored the custom auto-merge commit body observed on PR 378.
|
|
509
|
+
For a queued merge commit to retain this credit, the repository must use GitHub's
|
|
510
|
+
PR-body merge-message setting (`merge_commit_message=PR_BODY`); the package does
|
|
511
|
+
not change repository settings. The PR body is therefore the durable source the
|
|
512
|
+
queue can copy, rather than a claim that `gh pr merge --body` controls the final
|
|
513
|
+
queued merge.
|
|
514
|
+
|
|
442
515
|
**You do not fetch or reset first (APRV-203).** The verb owns its own git
|
|
443
516
|
preconditions: it fetches the base branch (the one you are standing on, or
|
|
444
517
|
`--base <name>`), builds the commit on `origin/<base>` in a scratch index rather
|
|
@@ -536,7 +609,8 @@ is not manual:
|
|
|
536
609
|
policy was read and understood, and it says ask.
|
|
537
610
|
- `irreversibility-floor` — policy granted autonomous or supervised and SPEC §7's
|
|
538
611
|
floor overrode it because `--reversible false` was given. `overridden` records
|
|
539
|
-
what policy actually said.
|
|
612
|
+
what policy actually said. The floor remains the default when a class rule
|
|
613
|
+
omits `allow_irreversible` or writes `false`.
|
|
540
614
|
- `load-failure` — the policy could not be loaded at all, so every class is
|
|
541
615
|
manual. `loadFailure` carries a code and a message.
|
|
542
616
|
|
|
@@ -561,14 +635,26 @@ The exit codes, at length. `policy check|test` uses only 0, 2 and 4:
|
|
|
561
635
|
key may contain, never something an agent can do.
|
|
562
636
|
|
|
563
637
|
`--reversible` takes an explicit value because "unstated", "reversible" and
|
|
564
|
-
"irreversible" are three different questions.
|
|
565
|
-
|
|
638
|
+
"irreversible" are three different questions. Explicit `false` asks for the
|
|
639
|
+
effective irreversible answer. It resolves to `manual` by default. A nonmanual
|
|
640
|
+
class retains its autonomy only when every equally most-specific matching rule
|
|
641
|
+
sets `allow_irreversible: true`. Lower-specificity rules do not vote, defaults
|
|
642
|
+
cannot opt in, and neither this flag nor any other action metadata can create
|
|
643
|
+
the permission. Manual remains manual and human-only remains human-only.
|
|
644
|
+
|
|
645
|
+
For an allowed `supervised-live` class, the existing live sampler decides
|
|
646
|
+
whether this action prompts before execution. Allowed `supervised` and
|
|
647
|
+
`supervised-retro` actions execute first and remain eligible for retrospective
|
|
648
|
+
review. Allowed autonomous actions proceed without either review path. Telegram
|
|
649
|
+
appears only when the effective path actually requests approval through that
|
|
650
|
+
channel; policy-authorized execution is never represented as a human grant.
|
|
566
651
|
|
|
567
652
|
**`--json`** (one object on stdout):
|
|
568
653
|
|
|
569
654
|
```
|
|
570
655
|
{"class":"vcs.push.main","reversible":null,
|
|
571
|
-
"outcome":{"autonomy":"supervised","approvers":null,"limits":null
|
|
656
|
+
"outcome":{"autonomy":"supervised","approvers":null,"limits":null,
|
|
657
|
+
"allowIrreversible":false},
|
|
572
658
|
"provenance":"rule"|"default"|"inherited"|"fail-closed"|"floor",
|
|
573
659
|
"manualBecause":null|"matched-rule"|"irreversibility-floor"|"load-failure",
|
|
574
660
|
"loadFailure":null|{"code":"file-missing"|"no-block"|"multiple-blocks"|
|
|
@@ -576,6 +662,9 @@ SPEC §7's irreversibility floor.
|
|
|
576
662
|
"message":"..."},
|
|
577
663
|
"matched":null|{"pattern":"vcs.push.main","rule":{"autonomy":"supervised"}},
|
|
578
664
|
"overridden":null|{"pattern":"read.web"|null,"autonomy":"autonomous"},
|
|
665
|
+
"irreversibility":"not-applicable"|"policy-allowed"|"floor-applied"|
|
|
666
|
+
"already-manual"|"human-only",
|
|
667
|
+
"irreversiblePatterns":["read.*"],
|
|
579
668
|
"candidates":[{"pattern":"read.*","specificity":[1,1,2],
|
|
580
669
|
"autonomy":"autonomous","winner":true,
|
|
581
670
|
"tieBreak":"specificity"|"strictest-autonomy"|
|
|
@@ -636,7 +725,21 @@ refusal {"ok":false,"error":{"code":"...","message":"..."}} on stderr
|
|
|
636
725
|
|
|
637
726
|
`path` is the file that was hashed; the logged payload carries its basename only,
|
|
638
727
|
so an exported log leaks no home directory. The event's payload is
|
|
639
|
-
`{"policy_path":"APPROVAL.md","sha256":"<64 hex>"}`.
|
|
728
|
+
`{"policy_path":"APPROVAL.md","sha256":"<64 hex>","payload_hash":"<64 hex>"}`.
|
|
729
|
+
|
|
730
|
+
**`payload_hash`: the attested bytes, recoverable (APRV-356).** The verb also
|
|
731
|
+
writes the attested text to the payload store beside the log and binds that
|
|
732
|
+
file's hash on the record, so the policy IN FORCE can be produced rather than
|
|
733
|
+
only named. A digest cannot produce the file it names, and the privileged-gesture
|
|
734
|
+
rule (`policy.core` gestures from a channel, SPEC.md §10.3) has to be decided
|
|
735
|
+
against the policy in force rather than against the one being proposed. A reader
|
|
736
|
+
re-hashes the recovered text against `sha256` from the verified log, so the store
|
|
737
|
+
is checked rather than trusted, and a store that cannot be written REFUSES the
|
|
738
|
+
attestation: nothing is appended, because a record claiming a binding whose bytes
|
|
739
|
+
are absent is a worse artifact than no record. Records written before this
|
|
740
|
+
existed still validate and verify; a reader treats the absent field as the
|
|
741
|
+
pre-amendment state, where the in-force bytes are unrecoverable and the
|
|
742
|
+
fail-closed fallback applies.
|
|
640
743
|
|
|
641
744
|
### `--organ <path>`: the gate's organs (APRV-272)
|
|
642
745
|
|
|
@@ -693,6 +796,92 @@ current bytes carry no attestation. It never moves doctor's exit code: an
|
|
|
693
796
|
unattested organ breaks nothing on this machine, and the enforcement for one is
|
|
694
797
|
the guard in CI.
|
|
695
798
|
|
|
799
|
+
### `--path <path>`: signing off a protected file (APRV-338)
|
|
800
|
+
|
|
801
|
+
SPEC.md's amendment-provenance rule says text that reached a protected file
|
|
802
|
+
without a grant carries `(Amended APRV-n, pending sign-off.)` and holds no more
|
|
803
|
+
authority than a proposal until a human ratifies it. Nothing recorded the
|
|
804
|
+
ratification: no event, no verb, and no way for CI or doctor to tell a ratified
|
|
805
|
+
amendment from a pending one. This flag is that record.
|
|
806
|
+
|
|
807
|
+
```
|
|
808
|
+
approval policy attest --path SPEC.md --as human:carter
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
One path per call, repository-relative (an absolute path under `--dir` is
|
|
812
|
+
accepted and recorded relative). The runtime hashes the bytes on disk; there is
|
|
813
|
+
no flag for the digest. The record is a **`gate.path.signed_off`** event:
|
|
814
|
+
|
|
815
|
+
```
|
|
816
|
+
{"event":"gate.path.signed_off","actor":"human:<id>",
|
|
817
|
+
"payload":{"path":"SPEC.md","sha256":"<64 hex>"}}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
**Which paths.** Exactly those whose edits classify `policy.edit` or a
|
|
821
|
+
`policy.edit.*` sub-class: the built-in prose set (`CLAUDE.md`, `AGENTS.md`,
|
|
822
|
+
`.npmrc`, `.github/workflows/`) plus everything the live policy's
|
|
823
|
+
`protected_paths` widens to, including a path routed to a sub-class such as
|
|
824
|
+
`policy.edit.design`. The policy is loaded to answer that, and a policy that
|
|
825
|
+
does not load contributes nothing, which narrows what may be signed rather than
|
|
826
|
+
widening it.
|
|
827
|
+
|
|
828
|
+
Three refusals are specific to this flag, all exit 2:
|
|
829
|
+
|
|
830
|
+
```
|
|
831
|
+
path-is-policy the policy file: use `approval policy attest` with no flag
|
|
832
|
+
path-is-core a gate organ (use --organ), the approval home, or the log
|
|
833
|
+
directory, which no verb ratifies
|
|
834
|
+
path-not-protected an ordinary file, or a path that is not repository-relative
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
`--organ` and `--path` together are a usage error, as are `--policy` and
|
|
838
|
+
`--path`: each pair names two different claims and guessing which one was meant
|
|
839
|
+
is how the wrong record gets written. `--json` adds `signed_path`:
|
|
840
|
+
|
|
841
|
+
```
|
|
842
|
+
success {"ok":true,"seq":7,"sha256":"<64 hex>","path":"/abs/SPEC.md",
|
|
843
|
+
"signed_path":"SPEC.md"}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**Weaker than a grant, and read last.** A grant binds the exact hunk a human saw
|
|
847
|
+
in a prompt; a sign-off stands for the whole file. So the protected-path guard
|
|
848
|
+
asks about a sign-off only after its search for a grant covering the change has
|
|
849
|
+
come up empty, and the finding it prints says so in words. A change that a grant
|
|
850
|
+
does cover still passes on the grant and still names it. Signing off is for the
|
|
851
|
+
case the pending-sign-off suffix was invented for: text a human has read at that
|
|
852
|
+
commit and agrees with, for which no grant was ever taken.
|
|
853
|
+
|
|
854
|
+
**Signing off bytes that are on a branch.** The digest the guard checks is the
|
|
855
|
+
file's blob at the commit under review, and that is usually a pull request's
|
|
856
|
+
head rather than anything on disk in the primary checkout. `--dir` and `--log`
|
|
857
|
+
are separate flags for exactly this: `--dir` is the checkout whose bytes are
|
|
858
|
+
hashed and which the recorded path is relative to, `--log` is the log the record
|
|
859
|
+
is appended to. So a human ratifying an open pull request reads the change,
|
|
860
|
+
puts a checkout at that commit somewhere (a `git worktree`, or the branch
|
|
861
|
+
checked out in a scratch clone), and runs:
|
|
862
|
+
|
|
863
|
+
```
|
|
864
|
+
approval policy attest --path SPEC.md \
|
|
865
|
+
--dir /path/to/checkout-at-that-commit \
|
|
866
|
+
--log /path/to/primary/.approval/log/events.jsonl \
|
|
867
|
+
--as human:<id>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
The record lands in the primary checkout's log, where a log advance carries it
|
|
871
|
+
to a records branch, and the guard then finds it for that path at that digest.
|
|
872
|
+
The verb never writes a log inside the worktree it hashed. If the digest it
|
|
873
|
+
prints is not the one the guard's failure named, the checkout is at the wrong
|
|
874
|
+
commit or the file has been edited since: the two must agree exactly, and a
|
|
875
|
+
mismatch is a sign-off on bytes nobody reviewed.
|
|
876
|
+
|
|
877
|
+
The verb classifies `policy.core` when `--path` is present (without it the verb
|
|
878
|
+
attests the gate's own configuration and stays pass-through), so under this
|
|
879
|
+
repository's policy the harness hook denies it to an agent with
|
|
880
|
+
`hook-class-human-only` before the verb's own `actor-not-human` refusal is
|
|
881
|
+
reached. `approval doctor`'s `pending-sign-off` row lists the protected files
|
|
882
|
+
that still carry the marker with no record over their current bytes; like
|
|
883
|
+
`gate-organs` it never moves doctor's exit code.
|
|
884
|
+
|
|
696
885
|
## policy amend
|
|
697
886
|
|
|
698
887
|
**Progress, on stderr.** The verb re-verifies the whole chain and recovers the
|
|
@@ -756,6 +945,16 @@ resolution still prints in the semantic diff below for the human who attests it.
|
|
|
756
945
|
Until APRV-296 every declared class had to be pinned, in both directions, and a
|
|
757
946
|
one-line TTL amendment on 2026-09-07 took three runs to land because of it.
|
|
758
947
|
|
|
948
|
+
The one check every declared class still faces is REACHABILITY: a class nothing
|
|
949
|
+
can emit is a line that will never fire, and the ceremony refuses it rather than
|
|
950
|
+
leaving an operator believing in it for a year. Three ways to be reachable: the
|
|
951
|
+
command classifier's fixed table, a `protected_paths` entry routing a path
|
|
952
|
+
family to a `policy.edit` sub-class, and `RUNTIME_CLASSES` in
|
|
953
|
+
`src/core/command-class.ts`, which names the classes a runtime cycle asks the
|
|
954
|
+
gate for directly and no command spells (`log.advance.daemon` is the first,
|
|
955
|
+
APRV-382). A new class of that third kind needs its line there in the SAME build
|
|
956
|
+
the ceremony runs, or the amendment refuses `policy-suite-failed`.
|
|
957
|
+
|
|
759
958
|
**And then the whole dogfood suite, still before the attestation.** The pin check
|
|
760
959
|
is a subset of `tests/dogfood.test.ts`, and the seq 23351 ceremony passed the pins
|
|
761
960
|
and went red on CI over a dogfood test about the values block. So `--commit` also
|
|
@@ -836,15 +1035,62 @@ base carries it, and a pins edit somebody else landed is not reverted by this
|
|
|
836
1035
|
ceremony. The pin deltas print in the semantic diff beside the class deltas, ride
|
|
837
1036
|
in the commit subject, and appear in `--json` as `pins`.
|
|
838
1037
|
|
|
839
|
-
It refuses outside a git repository, and refuses
|
|
840
|
-
|
|
841
|
-
staged edit would make "this commit is
|
|
1038
|
+
It refuses outside a git repository (`commit-preconditions`), and refuses
|
|
1039
|
+
`staged-unrelated` when the index holds staged changes to anything beyond those
|
|
1040
|
+
three: a commit that swept in an unrelated staged edit would make "this commit is
|
|
1041
|
+
the amendment" false. It refuses `dirty-tree` when one of those three is staged
|
|
1042
|
+
in one state and modified again in the working tree, because the commit is
|
|
1043
|
+
assembled from the WORKING TREE and would otherwise carry bytes the operator's
|
|
1044
|
+
`git diff --cached` never showed. An unrelated *unstaged* path is deliberately
|
|
1045
|
+
not a refusal: the scratch index lays exactly the ceremony's paths over the
|
|
1046
|
+
remote's tree, so nothing else can reach the commit, and refusing over one would
|
|
1047
|
+
stop the ceremony in the checkout it is written for, where the daemon's envelope
|
|
1048
|
+
write-backs leave task files modified as a matter of course. On the branch flow
|
|
842
1049
|
it also refuses when there is no `origin` remote, and when a `--branch` name
|
|
843
1050
|
already exists. Every one of those refusals happens BEFORE the attestation, so a
|
|
844
1051
|
refused `--commit` never leaves an attested policy without its commit. The same
|
|
845
1052
|
holds for the fetch, the two base checks, the policy suite and the dogfood suite
|
|
846
1053
|
above.
|
|
847
1054
|
|
|
1055
|
+
**`--pr`: the ceremony finishes its own job (APRV-341).** `--pr` is `--commit`
|
|
1056
|
+
plus "and publish it": it forces the BRANCH flow whatever the protection probe
|
|
1057
|
+
answered, so the amendment is committed on `origin/<default branch>` in a scratch
|
|
1058
|
+
index, pushed to `policy-amend-<seq>`, carried by a pull request, and armed with
|
|
1059
|
+
`gh pr merge <branch> --merge --auto`. The merge queue picks the strategy from
|
|
1060
|
+
there. `--pr --direct` and `--pr --no-publish` are usage errors: each pair asks
|
|
1061
|
+
for opposite ceremonies.
|
|
1062
|
+
|
|
1063
|
+
The pull request is OPENED or UPDATED. `gh pr list --head <branch> --state open`
|
|
1064
|
+
is asked first, and when one is already standing its title and body are edited
|
|
1065
|
+
rather than a second being opened, so a re-run of a ceremony that stopped
|
|
1066
|
+
half-way finishes rather than failing at `gh pr create`. A `gh` that cannot
|
|
1067
|
+
answer the question falls through to `create`, which is the path that was there
|
|
1068
|
+
before.
|
|
1069
|
+
|
|
1070
|
+
**The verb never switches branches.** On 2026-09-16 an agent-written runbook for
|
|
1071
|
+
the primary checkout ended with `git checkout main` after the amend commit, which
|
|
1072
|
+
rewound `APPROVAL.md` and `events.jsonl` under a live appender; the hook then
|
|
1073
|
+
appended 204 records on the stale chain and the log forked. `--pr` exists so that
|
|
1074
|
+
runbook does not: the commit is assembled with `git read-tree` into a scratch
|
|
1075
|
+
index and pushed by sha, the checkout ends the verb on the branch it started on,
|
|
1076
|
+
with the same HEAD, the same index and the same working tree, and the only file
|
|
1077
|
+
that moved is the log, which gained the attestation. A test compares all four
|
|
1078
|
+
before and after.
|
|
1079
|
+
|
|
1080
|
+
Without `--pr` (and on a box with no `gh`) the printed runbook does the same
|
|
1081
|
+
thing by hand, and it does not switch branches either (APRV-360): `approval log
|
|
1082
|
+
sync`, `git add`, `git commit`, `git push origin HEAD:refs/heads/policy-amend-<seq>`,
|
|
1083
|
+
`gh pr create --head`, `gh pr merge --auto --merge`. A fixture test pins the six
|
|
1084
|
+
commands and asserts that no printed line contains a `git checkout`, because a
|
|
1085
|
+
fallback nobody checks is where the branch switch came back.
|
|
1086
|
+
|
|
1087
|
+
The form printed before APRV-360 opened with `git checkout -b policy-amend-<seq>
|
|
1088
|
+
origin/main`. On 2026-09-18 the primary's main was fourteen commits behind, the
|
|
1089
|
+
switch refused rather than overwrite `QUEUE.md`, the working log and six
|
|
1090
|
+
payloads, and the ceremony stalled with an edited, attested, unpublished policy.
|
|
1091
|
+
`approval log sync` is the first line now for that reason: it brings the checkout
|
|
1092
|
+
current, and it refuses `log-diverged` rather than fast-forwarding over a fork.
|
|
1093
|
+
|
|
848
1094
|
`--commit` also pushes, on both flows. When there is no `origin` to push to, the
|
|
849
1095
|
direct flow reports the push as still to run rather than listing it among the
|
|
850
1096
|
commands it ran. `--no-publish` stops the ceremony at the commit: nothing is
|
|
@@ -886,9 +1132,9 @@ beneath it.
|
|
|
886
1132
|
sha256 8acbd01cda98
|
|
887
1133
|
|
|
888
1134
|
Committed
|
|
889
|
-
✓ committed the policy and the
|
|
1135
|
+
✓ committed the policy, the log and the attested policy text together:
|
|
890
1136
|
|
|
891
|
-
git add APPROVAL.md .approval/log/events.jsonl
|
|
1137
|
+
git add APPROVAL.md .approval/log/events.jsonl .approval/payloads/8acbd01cda98….json
|
|
892
1138
|
git commit -m "Policy: amend APPROVAL.md: 1 class resolution(s) (attested seq 2)"
|
|
893
1139
|
|
|
894
1140
|
Publishing
|
|
@@ -981,10 +1227,16 @@ is the human rendering only.
|
|
|
981
1227
|
prints the SEMANTIC diff, computed by the real engine on both versions;
|
|
982
1228
|
4. runs the load advisory;
|
|
983
1229
|
5. asks for confirmation (skipped by `--yes` and `--dry-run`);
|
|
984
|
-
6. attests: one `policy.updated` event, identical to `approval policy attest
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1230
|
+
6. attests: one `policy.updated` event, identical to `approval policy attest`,
|
|
1231
|
+
which since APRV-356 also stores the attested bytes in the payload store and
|
|
1232
|
+
binds their hash on the record;
|
|
1233
|
+
7. prints, or with `--commit` runs, the git ceremony — `git add <policy> <log>
|
|
1234
|
+
<attested bytes>` (plus the pins when they moved), a `git commit` citing the
|
|
1235
|
+
attestation seq, and the push (and, on the branch flow, the branch and the
|
|
1236
|
+
pull request). The store file rides in the same commit because a committed
|
|
1237
|
+
log carrying a binding whose bytes were never committed leaves the policy in
|
|
1238
|
+
force unrecoverable for everyone reading that copy, the CI protected-path
|
|
1239
|
+
guard included;
|
|
988
1240
|
8. publishes, unless `--no-publish`: a push the remote refuses is answered by
|
|
989
1241
|
the branch, push, pull request and auto-merge above, each reported as it
|
|
990
1242
|
lands, and a step that fails drops to the runbook from there.
|
|
@@ -1041,7 +1293,7 @@ attestation may still proceed.
|
|
|
1041
1293
|
"publishing":null|{"attempted":true,"complete":true,
|
|
1042
1294
|
"via":"direct"|"branch"|"recovery"|"none",
|
|
1043
1295
|
"branch":null|"policy-amend-2","pushed":true,
|
|
1044
|
-
"prUrl":null|"https://...",
|
|
1296
|
+
"prUrl":null|"https://...","prUpdated":false,
|
|
1045
1297
|
"autoMerge":"armed"|"refused"|"not-attempted",
|
|
1046
1298
|
"steps":[{"command":"git push origin main","ok":false}],
|
|
1047
1299
|
"stoppedAt":null|"git push -u origin policy-amend-2",
|
|
@@ -1073,10 +1325,19 @@ the message.
|
|
|
1073
1325
|
- `io` — the policy file or the log could not be read or written.
|
|
1074
1326
|
- `load-failed` — `--require-load` and the policy does not load. Nothing was
|
|
1075
1327
|
appended.
|
|
1076
|
-
- `commit-preconditions` — `--commit` outside a git repository, with
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1328
|
+
- `commit-preconditions` — `--commit` outside a git repository, with the policy
|
|
1329
|
+
and the log in different repositories, or (branch flow) with no origin remote
|
|
1330
|
+
or a `--branch` name already taken. Checked before the attestation; nothing
|
|
1331
|
+
was appended.
|
|
1332
|
+
- `staged-unrelated` — the index carries staged changes beyond the policy, the
|
|
1333
|
+
log and the pins (APRV-341). Its own code because its repair is its own: one
|
|
1334
|
+
`git restore --staged <path>`. Checked before the attestation; nothing was
|
|
1335
|
+
appended.
|
|
1336
|
+
- `dirty-tree` — one of those three is staged in one state and modified again in
|
|
1337
|
+
the working tree (APRV-341). The commit is assembled from the working tree, so
|
|
1338
|
+
it would carry bytes `git diff --cached` does not show. An unrelated *unstaged*
|
|
1339
|
+
path is not this refusal and never was. Checked before the attestation;
|
|
1340
|
+
nothing was appended.
|
|
1080
1341
|
- `fetch-failed` / `base-policy-diverged` / `base-log-diverged` — the remote the
|
|
1081
1342
|
amendment would be based on could not be fetched, carries a policy this edit
|
|
1082
1343
|
was not written against, or carries a log this working log does not contain.
|
|
@@ -1105,6 +1366,114 @@ the message.
|
|
|
1105
1366
|
- `log-unreadable` / `log-torn-tail` / `log-corrupt` — nothing is amended from a
|
|
1106
1367
|
log that does not verify.
|
|
1107
1368
|
|
|
1369
|
+
## policy apply
|
|
1370
|
+
|
|
1371
|
+
**The hand-paste this replaces (APRV-343).** Agents may not write `APPROVAL.md`:
|
|
1372
|
+
it is `policy.core`, and this project's policy holds that class human-only. So a
|
|
1373
|
+
policy change an agent proposes travels as a document under `docs/proposals/`
|
|
1374
|
+
that quotes each current line byte for byte beside its replacement, and the
|
|
1375
|
+
human pastes. Two things go wrong with a paste, and both have:
|
|
1376
|
+
|
|
1377
|
+
- **a whole-file copy reverts what it did not know about.** The prepared file was
|
|
1378
|
+
written against the policy as it stood when the proposal was drafted, so
|
|
1379
|
+
anything landed in between is silently undone. That is the failure
|
|
1380
|
+
`base-policy-diverged` catches for the commit, one step too late to help;
|
|
1381
|
+
- **a paste carries its wrapper.** One paste of a proposal page took the page's
|
|
1382
|
+
own fence with it, which hid a block from the loader (APRV-273).
|
|
1383
|
+
|
|
1384
|
+
`approval policy apply <proposal.md>` answers both by construction. It writes no
|
|
1385
|
+
byte that is not anchored to a byte it proved present in the live file, and it
|
|
1386
|
+
reads fences by their backtick run, so a four-backtick wrapper around a
|
|
1387
|
+
three-backtick block is the wrapper it is rather than part of the content.
|
|
1388
|
+
|
|
1389
|
+
### The proposal format
|
|
1390
|
+
|
|
1391
|
+
A proposal is ordinary markdown. Anywhere in it, a fenced block **with a
|
|
1392
|
+
declared language** whose immediately preceding non-blank line is a label is a
|
|
1393
|
+
member of a pair:
|
|
1394
|
+
|
|
1395
|
+
| label | means |
|
|
1396
|
+
|---|---|
|
|
1397
|
+
| `Current:` | the bytes as they stand in the live policy |
|
|
1398
|
+
| `Replace with:` | what they become |
|
|
1399
|
+
| `Supersedes:` | optional: an earlier section's RESULT, to match instead |
|
|
1400
|
+
|
|
1401
|
+
A fence with no info string is skipped, which is APRV-273's hazard turned into a
|
|
1402
|
+
rule. A block whose label names no role is skipped too, so a proposal page can
|
|
1403
|
+
carry a `bash` block of commands to run afterwards without the applier mistaking
|
|
1404
|
+
it for policy text. Pairs apply in document order.
|
|
1405
|
+
|
|
1406
|
+
**Supersession is declared, never inferred from position.** A later section that
|
|
1407
|
+
rewrites a line an earlier section already rewrote quotes the earlier section's
|
|
1408
|
+
*result* under a `Supersedes:` label; the applier looks for that text when the
|
|
1409
|
+
section's own `Current` block is no longer in the file, which is exactly the
|
|
1410
|
+
state the earlier section left behind. Position could not carry this: two
|
|
1411
|
+
sections that touch one line are not in general in the order the file needs, and
|
|
1412
|
+
a rule inferred from order is a rule nobody can read off the page. A pair whose
|
|
1413
|
+
`Current` **and** `Supersedes` blocks both occur in the file is
|
|
1414
|
+
`proposal-ambiguous`: two spellings of one line is a question about which the
|
|
1415
|
+
file means, and no verb here will pick.
|
|
1416
|
+
|
|
1417
|
+
**Whole-file replacement is not accepted**, and that is the decision rather than
|
|
1418
|
+
an omission. The verb's whole value is that every byte it writes is anchored to
|
|
1419
|
+
a byte it proved present, which is what makes a stale proposal a refusal instead
|
|
1420
|
+
of a silent revert. A whole-file blob has no anchor. `approval policy amend`
|
|
1421
|
+
over a hand-edited file is already the supported way to replace the file
|
|
1422
|
+
deliberately.
|
|
1423
|
+
|
|
1424
|
+
**The values block is treated exactly as the policy block is**, by knowing
|
|
1425
|
+
nothing about either. The applier is a byte-level replacement over the whole
|
|
1426
|
+
file: it does not parse the policy, does not locate blocks, and does not care
|
|
1427
|
+
which fence a pair lands in. The values block is inert (SPEC §11.1 invariant
|
|
1428
|
+
10), so applying one changes no verdict, and the attestation the amendment
|
|
1429
|
+
appends covers the whole file's bytes either way (SPEC §5.2, §5.3).
|
|
1430
|
+
|
|
1431
|
+
`docs/proposals/README.md` is the contract as a page for proposal authors, and
|
|
1432
|
+
`docs/proposals/approval-md-2026-09.md` is a worked example of every part of it.
|
|
1433
|
+
|
|
1434
|
+
### What it does, in order
|
|
1435
|
+
|
|
1436
|
+
1. Refuses an agent identity (`apply-agent-actor`) before reading anything.
|
|
1437
|
+
2. Parses the proposal into ordered pairs.
|
|
1438
|
+
3. Resolves every pair against an **in-memory** copy of the policy. A proposal
|
|
1439
|
+
whose third pair is stale writes nothing at all, so "a stale proposal cannot
|
|
1440
|
+
half-apply" is a property of the code rather than of the order somebody wrote
|
|
1441
|
+
the sections in.
|
|
1442
|
+
4. Prints the replacements, each as its matched block and its replacement.
|
|
1443
|
+
5. Asks for confirmation (`--yes` skips it, `--dry-run` stops here).
|
|
1444
|
+
6. Writes the policy, then runs `approval policy amend --pr` in this process —
|
|
1445
|
+
which asks its OWN question about the semantic diff, because "are these the
|
|
1446
|
+
bytes" and "is this the policy" are different questions.
|
|
1447
|
+
|
|
1448
|
+
**It publishes by default (APRV-360).** The amendment runs with `--pr`, so the
|
|
1449
|
+
branch is created on the remote by refspec, the pull request is opened, the
|
|
1450
|
+
merge is armed, and the checkout ends where it started. Before this the amend
|
|
1451
|
+
ran with no flag: the policy was written and attested, and the operator was
|
|
1452
|
+
handed a procedure that began with a branch switch. On 2026-09-18 the primary
|
|
1453
|
+
refused that switch and the ceremony stalled with an edited, attested,
|
|
1454
|
+
unpublished policy, which is the one state in which every gated operation on the
|
|
1455
|
+
box refuses. `--no-publish` stops at the commit and is passed through to the
|
|
1456
|
+
amend; `--pr` is accepted and does nothing, because runbooks already say it, and
|
|
1457
|
+
`--pr` with `--no-publish` is a usage error.
|
|
1458
|
+
|
|
1459
|
+
`--no-amend` writes and stops, and says loudly that the policy is now edited and
|
|
1460
|
+
unattested. A run where every pair resolves and no byte moves is a success and a
|
|
1461
|
+
no-op: the proposal has already been applied.
|
|
1462
|
+
|
|
1463
|
+
**Refusal codes** (`error.code` with `--json`; frozen public API): `usage`,
|
|
1464
|
+
`io`, `apply-agent-actor`, `proposal-empty`, `proposal-malformed` (a `Current`
|
|
1465
|
+
with no `Replace with`, or the reverse), `proposal-stale` (a quoted current text
|
|
1466
|
+
is not in the file), `proposal-ambiguous` (it occurs more than once, or both it
|
|
1467
|
+
and its superseded text occur). Every one of them writes nothing.
|
|
1468
|
+
|
|
1469
|
+
Two outcomes are deliberately not in that union. Answering no at the
|
|
1470
|
+
confirmation is exit 0 with `aborted:` on stdout, exactly as `policy amend`
|
|
1471
|
+
answers it: nothing failed, and an error object at exit 0 would be a
|
|
1472
|
+
contradiction the caller has to resolve. An amendment that refuses has already
|
|
1473
|
+
printed its own code from its own frozen union, so this verb adds a sentence
|
|
1474
|
+
naming the state that leaves behind — the replacements written, the policy
|
|
1475
|
+
unattested — and returns the amendment's exit code unchanged.
|
|
1476
|
+
|
|
1108
1477
|
## register
|
|
1109
1478
|
|
|
1110
1479
|
The task file is read only. Nothing is rewritten, so unknown frontmatter keys
|
|
@@ -1155,6 +1524,13 @@ hash must equal the declared `payload_hash` and it is filed in
|
|
|
1155
1524
|
bytes from. Supply it here once and no channel needs `--payload-dir` or
|
|
1156
1525
|
`--payloads` at all.
|
|
1157
1526
|
|
|
1527
|
+
When the policy permits an unattended action, supplied material is still checked
|
|
1528
|
+
against the registered action's task, class and payload hash and retained before
|
|
1529
|
+
`proceed:true` is returned. This creates no approval event and does not require
|
|
1530
|
+
an extra human grant. The retained bytes let later audits match the execution
|
|
1531
|
+
record to the actual edit. Missing bindings, mismatched material or a storage
|
|
1532
|
+
failure refuse the request; an existing valid payload is preserved.
|
|
1533
|
+
|
|
1158
1534
|
**`--json`** (one object on stdout):
|
|
1159
1535
|
|
|
1160
1536
|
```
|
|
@@ -1715,7 +2091,7 @@ refusal {"ok":false,"error":{"code":"...","message":"...","detail"?:"...",
|
|
|
1715
2091
|
## sandbox
|
|
1716
2092
|
|
|
1717
2093
|
```
|
|
1718
|
-
approval sandbox [--allow-loopback] [--log <path>] -- <cmd> [args…]
|
|
2094
|
+
approval sandbox [--allow-loopback] [--read-jail] [--log <path>] -- <cmd> [args…]
|
|
1719
2095
|
```
|
|
1720
2096
|
|
|
1721
2097
|
Runs a command with outbound network denied by the operating system. It appends
|
|
@@ -1753,6 +2129,15 @@ is a sandbox somebody turns off.
|
|
|
1753
2129
|
server. It is a real widening: a port is a port, and anything listening on one is
|
|
1754
2130
|
reachable from inside.
|
|
1755
2131
|
|
|
2132
|
+
**`--read-jail`** (APRV-347) goes the other way and makes the room smaller: file
|
|
2133
|
+
reads become deny-default, with the gate root, the scratch roots and a fixed
|
|
2134
|
+
runtime set opened by `subpath`. It is already on, without the flag, whenever
|
|
2135
|
+
the policy declares a `read_scope` block, which is the spelling an operator
|
|
2136
|
+
commits and attests; the flag is for trying it on one command first. There is no
|
|
2137
|
+
flag that turns the jail OFF where a policy asked for it, because a flag an agent
|
|
2138
|
+
can pass must only ever narrow what it can do. See
|
|
2139
|
+
[docs/sandboxed-exec.md](./sandboxed-exec.md) for the profile and its limits.
|
|
2140
|
+
|
|
1756
2141
|
**Exit 127** means the command was NOT run: this machine has no working sandbox
|
|
1757
2142
|
primitive, or the command is not on `PATH`. Unlike `approval run`, this verb
|
|
1758
2143
|
fails closed on both: it makes one promise and has nothing else to offer.
|
|
@@ -2447,6 +2832,23 @@ The checks, at length:
|
|
|
2447
2832
|
log sync` for a diverged log, `approval up` otherwise — never a `git` command:
|
|
2448
2833
|
a repair line telling an operator to reset a branch would be doctor making the
|
|
2449
2834
|
decision this project keeps human.
|
|
2835
|
+
- **attested-policy-on-main** — whether the policy the log vouches for is the
|
|
2836
|
+
policy `origin/<branch>` carries (APRV-342). `attestation` above asks whether
|
|
2837
|
+
the LOCAL file is attested; between a `policy amend` and its pull request
|
|
2838
|
+
merging that answer is yes while a fresh checkout of main carries the old
|
|
2839
|
+
policy with no attestation covering it, so every gate operation there refuses
|
|
2840
|
+
`policy-not-attested`. Nothing said so until this row: on 2026-09-16 `approval
|
|
2841
|
+
up` ran in exactly that state and reported "already at the remote tip". PASS
|
|
2842
|
+
when the attested hash equals the SHA-256 of `APPROVAL.md` at the remote tip.
|
|
2843
|
+
FAIL with `attested at seq N, not yet on main`, naming
|
|
2844
|
+
`policy-amend-<seq>` when this checkout has already seen that branch on the
|
|
2845
|
+
remote, and fixing with `approval policy amend --pr` — which opens the pull
|
|
2846
|
+
request or updates the open one, so the same command is right either way.
|
|
2847
|
+
SKIP with no attestation, outside a git checkout, and where there is no
|
|
2848
|
+
remote-tracking ref. **It fetches nothing**, for the reason
|
|
2849
|
+
`main-behind-origin` fetches nothing, and it looks for the amend branch among
|
|
2850
|
+
the remote-tracking refs rather than asking GitHub. `approval up`'s preflight
|
|
2851
|
+
prints the same sentence on stderr and never refuses on it.
|
|
2450
2852
|
- **harness-version-unverified** — whether the harness binary hosting the
|
|
2451
2853
|
PreToolUse hook changed since the log last saw a record from it (APRV-227).
|
|
2452
2854
|
The only row that asks anything about a program outside this repository, and
|
|
@@ -2490,7 +2892,14 @@ The checks, at length:
|
|
|
2490
2892
|
words SPEC.md §5.3 fixes: a file with no block is an operator who has declared
|
|
2491
2893
|
no values, which is a state and not a fault. The only FAIL is a block that is
|
|
2492
2894
|
present and unreadable, and its fix names the code rather than proposing a
|
|
2493
|
-
repair, because what the block should say is the human's to write.
|
|
2895
|
+
repair, because what the block should say is the human's to write. The one
|
|
2896
|
+
exception is the block APRV-336 replaced (`version: 1` with a `wants` list):
|
|
2897
|
+
that is a correct document of the wrong vintage rather than a broken one, so
|
|
2898
|
+
the row's detail carries the loader's migration message and its fix names the
|
|
2899
|
+
three edits (fold `wants:` into `like:`, rename `responds:` to
|
|
2900
|
+
`communication:`, quote `version: "0.2"`) plus the re-attestation that the
|
|
2901
|
+
whole-file digest requires. The pass detail lists what the block declares,
|
|
2902
|
+
which are `love`, `like`, `dislike` and `communication` and nothing else.
|
|
2494
2903
|
- **checkpoint** — how this log stands against its own human-signed checkpoints
|
|
2495
2904
|
(APRV-257), running the same check as `approval log verify --checkpoints`, so
|
|
2496
2905
|
two implementations of "does this log's own signature contradict it" cannot
|
|
@@ -2529,6 +2938,67 @@ The checks, at length:
|
|
|
2529
2938
|
where there is nothing to commit a key to. Neither fix deletes nor commits:
|
|
2530
2939
|
`git rm --cached` for a key already in the index is named in the prose and
|
|
2531
2940
|
left to you, along with revoking every action whose token is still unspent.
|
|
2941
|
+
- **codex-hook-wiring** — whether this checkout's `.codex/hooks.json` carries
|
|
2942
|
+
the reviewed approval.md profile for both `PreToolUse` and `PostToolUse`: the
|
|
2943
|
+
exact `Bash|apply_patch` matcher, a direct synchronous `approval hook codex`
|
|
2944
|
+
command, and a `600` second outer timeout. A PASS establishes only those JSON
|
|
2945
|
+
bytes on disk. Codex trust, loading, and observed execution remain separate
|
|
2946
|
+
facts checked through `/hooks` and the bounded smoke test. TOML-only hook
|
|
2947
|
+
configuration, or JSON combined with `.codex/config.toml`, SKIPS because
|
|
2948
|
+
doctor does not interpret or merge the TOML hook tables. Malformed JSON
|
|
2949
|
+
FAILS; a different valid Codex hook profile SKIPS as undetermined rather than
|
|
2950
|
+
being called broken.
|
|
2951
|
+
- **autonomy-alias** — which rules of this policy still write the deprecated
|
|
2952
|
+
bare `supervised` (APRV-335). The spelling parses as `supervised-retro` and
|
|
2953
|
+
every gate enforces it as one, so the row is never a FAIL and never moves the
|
|
2954
|
+
exit code: a red line over a spelling would be doctor going red over prose.
|
|
2955
|
+
The two PASS shapes are the whole row. Where some rule uses it, the detail
|
|
2956
|
+
names each one, in the order the loader's own notes name them, and the `fix`
|
|
2957
|
+
is `approval policy amend`, which owns the edit and the re-attestation that
|
|
2958
|
+
edit costs. Where none does, the detail says so plainly, which is the answer
|
|
2959
|
+
an operator wants before a future schema version drops the alias. A policy
|
|
2960
|
+
that did not load is a SKIP: it names no level at all, and its own failure is
|
|
2961
|
+
reported by the attestation row and by `approval policy check`.
|
|
2962
|
+
- **pending-sign-off** — which protected files carry SPEC.md's
|
|
2963
|
+
`(Amended APRV-n, pending sign-off.)` marker with no `gate.path.signed_off`
|
|
2964
|
+
record over their current bytes (APRV-338). Informational and never a FAIL,
|
|
2965
|
+
for the two reasons `gate-organs` is: nothing on this machine is broken by
|
|
2966
|
+
unratified prose, and the enforcement that does bite is the CI-side
|
|
2967
|
+
protected-path guard. What the row buys is that the debt is visible at the
|
|
2968
|
+
terminal rather than discovered when a pull request fails. It reads the
|
|
2969
|
+
enumerated directories (`.`, `.github/workflows/`, `docs/`, `design/`) plus
|
|
2970
|
+
every path the policy's `protected_paths` names, keeps only what classifies
|
|
2971
|
+
`policy.edit` or a `policy.edit.*` sub-class, and reports a file whose current
|
|
2972
|
+
bytes ARE signed off as ratified rather than pending. The `fix` is
|
|
2973
|
+
`approval policy attest --path <p> --as human:<id>`, to be run after reading
|
|
2974
|
+
the file; a marker whose text was later granted through the gate should lose
|
|
2975
|
+
the suffix instead.
|
|
2976
|
+
- **sender-mapping** — which approvers a channel whose senders the policy maps
|
|
2977
|
+
can still recognize (APRV-324). A SKIP where no approver declares a `senders`
|
|
2978
|
+
block, which is every installation before the key existed: decisions are
|
|
2979
|
+
recorded against the identity the deciding process was launched with, no
|
|
2980
|
+
channel enforces a mapping, and nothing is wrong. A FAIL where the policy
|
|
2981
|
+
maps senders for a channel and lists an approver on that channel with no id
|
|
2982
|
+
of their own there — that person's next tap is refused `sender-unmapped` and
|
|
2983
|
+
nothing is recorded, so the file says they may decide on a surface where they
|
|
2984
|
+
cannot. A PASS where every approver a mapped channel reaches carries an id
|
|
2985
|
+
there. The `fix` is `approval policy amend`, which owns the edit and the
|
|
2986
|
+
re-attestation it costs. The row reads the policy and nothing else: no log,
|
|
2987
|
+
no network, no credential, and it prints nobody's account id — the mapping is
|
|
2988
|
+
in a file the operator can open, and a health row is read over shoulders.
|
|
2989
|
+
- **codex-auto-reviewer** — has anything other than this gate answered a
|
|
2990
|
+
question this gate exists to ask (APRV-378)? It reads
|
|
2991
|
+
`audit.question_preempted` and nothing else. A FAIL where one landed in the
|
|
2992
|
+
last 24 hours, naming the source, the question in the other party's terms and
|
|
2993
|
+
their verdict; a PASS where none did, mentioning any older ones; a SKIP where
|
|
2994
|
+
the chain did not verify. A PASS says the log holds no such record and NOT
|
|
2995
|
+
that a harness auto-reviewer is off: whether it runs is configuration this
|
|
2996
|
+
runtime cannot read, and a row written against a guessed configuration key
|
|
2997
|
+
would find nothing and report green, which is the worst direction a health
|
|
2998
|
+
check can fail in. The `fix` points at the harness's own configuration,
|
|
2999
|
+
because nothing here can turn another system's reviewer off.
|
|
3000
|
+
|
|
3001
|
+
|
|
2532
3002
|
|
|
2533
3003
|
**`--json`** (one object on stdout):
|
|
2534
3004
|
|
|
@@ -2557,8 +3027,9 @@ reaches this backlog. A `supervised-live` class puts a declared `live_rate`
|
|
|
2557
3027
|
fraction of its actions through the human gate BEFORE they run; those are
|
|
2558
3028
|
ordinary manual requests with ordinary grants and tokens, a person has already
|
|
2559
3029
|
answered them, and they are not drawn a second time for retrospective review. A
|
|
2560
|
-
`supervised-retro` class — and the bare `supervised`, which is now
|
|
2561
|
-
|
|
3030
|
+
`supervised-retro` class — and the bare `supervised`, which is now a deprecated
|
|
3031
|
+
alias for it, still parsed and removed in a future schema version (APRV-335) —
|
|
3032
|
+
is what this page is about. `approval policy check` names the mode in its
|
|
2562
3033
|
final line and in `outcome.supervision`.
|
|
2563
3034
|
|
|
2564
3035
|
Supervised actions execute immediately and are audited afterwards. The daemon
|
|
@@ -2684,9 +3155,10 @@ and cannot do. It selects the SHAPE of an obligation that exists either way; it
|
|
|
2684
3155
|
cannot remove one, delay one, or decide whether the denial happened. The only
|
|
2685
3156
|
thing a false `reversible: true` buys is the shape whose discharge this runtime
|
|
2686
3157
|
checks against the chain, which makes the claimant's own exit harder rather than
|
|
2687
|
-
easier. The same reading applies to the irreversibility floor
|
|
2688
|
-
`reversible: false` action out of `supervised-retro
|
|
2689
|
-
|
|
3158
|
+
easier. The same reading applies to the irreversibility floor. By default it
|
|
3159
|
+
keeps a `reversible: false` action out of `supervised-retro`; an attested class
|
|
3160
|
+
rule may explicitly accept that consequence with `allow_irreversible: true`.
|
|
3161
|
+
The field acts on the acting party's own claim, so it catches the honest
|
|
2690
3162
|
declaration and never the lie. What answers the lie is writing `manual` for the
|
|
2691
3163
|
class, which no declaration can loosen.
|
|
2692
3164
|
|
|
@@ -3192,6 +3664,11 @@ would put a bot token into a shell history or a process listing.
|
|
|
3192
3664
|
|
|
3193
3665
|
## channel telegram listen
|
|
3194
3666
|
|
|
3667
|
+
This starts the standalone Telegram component. For normal operation, use
|
|
3668
|
+
[`approval up`](#up), which also runs the daemon. Do not run this listener beside
|
|
3669
|
+
`up` or another listener polling the same bot, even for a different policy
|
|
3670
|
+
project: competing `getUpdates` calls produce Telegram HTTP 409.
|
|
3671
|
+
|
|
3195
3672
|
**Delivery is per cycle, not only at startup.** Before every `getUpdates` the
|
|
3196
3673
|
listener re-derives the pending queue from the verified log and sends whatever
|
|
3197
3674
|
it has not already sent, so a request appended while this listener is running
|
|
@@ -3402,6 +3879,64 @@ leaves the sample exactly where it was: open, listed by `approval audit list`,
|
|
|
3402
3879
|
and reviewable with `approval audit review <seq>`. Nothing in this channel can
|
|
3403
3880
|
empty the backlog, which is the property a sampled-audit backlog exists to have.
|
|
3404
3881
|
|
|
3882
|
+
**A refused gesture leaves a record (APRV-355).** When the policy maps senders,
|
|
3883
|
+
a checkpoint signature or a review from an account the attested policy names
|
|
3884
|
+
nobody for is refused before any verb runs, and since this change the attempt is
|
|
3885
|
+
recorded: one **`audit.gesture_refused`**, with a `system:gate` actor, the
|
|
3886
|
+
channel, and a payload carrying `gesture`
|
|
3887
|
+
(`checkpoint-signature`, `review`, `review-note`), `code`
|
|
3888
|
+
(`sender-unmapped`, `sender-ambiguous`, `sender-key-unavailable`,
|
|
3889
|
+
`policy-not-attested`), the refusal
|
|
3890
|
+
`message`, the observed `sender`, and `actor` only where the runtime could name
|
|
3891
|
+
a person. Under a keyed mapping (APRV-370) the `sender.id` is the digest rather
|
|
3892
|
+
than the account, and `sender.hashed` is `true` beside it.
|
|
3893
|
+
|
|
3894
|
+
```
|
|
3895
|
+
{"event":"audit.gesture_refused","actor":"system:gate","channel":"telegram",
|
|
3896
|
+
"payload":{"gesture":"checkpoint-signature","code":"sender-unmapped",
|
|
3897
|
+
"sender":{"channel":"telegram","id":"5551234567"},"message":"…"}}
|
|
3898
|
+
```
|
|
3899
|
+
|
|
3900
|
+
It exists because the only refusal record before it,
|
|
3901
|
+
`audit.decision_refused`, requires an `action_key` and a `decision` of grant,
|
|
3902
|
+
reject or revoke, and a signature or a review has neither; writing one there
|
|
3903
|
+
would mean inventing both. The record is audit tier in the strict sense: it
|
|
3904
|
+
authorizes nothing, settles no request, charges no budget, is not sampled, and
|
|
3905
|
+
no enforcement path reads it. `approval log tail` and `approval log export`
|
|
3906
|
+
show it like any other record. Nothing about the refusal itself changed — no
|
|
3907
|
+
signature is appended and no review is recorded — and a listener whose policy
|
|
3908
|
+
maps no senders never reaches this path at all.
|
|
3909
|
+
|
|
3910
|
+
**A question answered by something other than the gate leaves a record
|
|
3911
|
+
(APRV-378).** Codex's app-server can resolve an approval with a model call
|
|
3912
|
+
before `approval codex bridge` is asked, and disclose it afterwards through an
|
|
3913
|
+
`item/autoApprovalReview` notification. The bridge stops the session on one
|
|
3914
|
+
(`bridge-auto-reviewer-active`), and it appends exactly one
|
|
3915
|
+
**`audit.question_preempted`** first, through the same append path as every
|
|
3916
|
+
other record:
|
|
3917
|
+
|
|
3918
|
+
```
|
|
3919
|
+
{"event":"audit.question_preempted","actor":"system:gate",
|
|
3920
|
+
"payload":{"source":"codex-auto-reviewer","verdict":"accept",
|
|
3921
|
+
"question":{"id":"item_01H9","method":"item/autoApprovalReview/completed",
|
|
3922
|
+
"thread":"thread_7f2","turn":"turn_3"}}}
|
|
3923
|
+
```
|
|
3924
|
+
|
|
3925
|
+
`source` is a closed set naming who answered, so the next system that does this
|
|
3926
|
+
gains a member rather than a type. `question` is the other party's own
|
|
3927
|
+
identifiers, because a record about their decision has to name it in their terms
|
|
3928
|
+
or a reader cannot go and find it on their side. `verdict` is their word
|
|
3929
|
+
verbatim and is ABSENT when the disclosure stated none: a record that said
|
|
3930
|
+
`accept` by default would be this runtime inventing somebody else's decision.
|
|
3931
|
+
|
|
3932
|
+
The record is audit tier in the strict sense: it authorizes nothing, settles no
|
|
3933
|
+
request, charges no budget, is not sampled, and no enforcement path reads it.
|
|
3934
|
+
`approval log tail` and `approval log export` show it like any other record, and
|
|
3935
|
+
`approval doctor`'s `codex-auto-reviewer` row reads it: fail when one landed in
|
|
3936
|
+
the last 24 hours, pass otherwise, and a pass says the log holds no such record
|
|
3937
|
+
rather than that any auto-reviewer is off. Nothing on this machine can say the
|
|
3938
|
+
latter, which is why the bridge probes (APRV-364) instead of reading a setting.
|
|
3939
|
+
|
|
3405
3940
|
**A settled request stops looking live.** Every terminal state the listener
|
|
3406
3941
|
observes for a message it sent edits that message: the text becomes the outcome
|
|
3407
3942
|
(`✓ APPROVED`, `✗ REJECTED`, `✗ REVOKED`, `✗ EXPIRED`, `WITHDRAWN`) with the
|
|
@@ -3456,6 +3991,43 @@ belong to a RUNNING listener: they are on its stderr as they happen, in its
|
|
|
3456
3991
|
Which variables are read comes from the policy, so a renamed variable reads back
|
|
3457
3992
|
as the name you set.
|
|
3458
3993
|
|
|
3994
|
+
## quickstart
|
|
3995
|
+
|
|
3996
|
+
`approval quickstart [--dir <path>] [--api-base <url>]` is the human-only solo setup ceremony. It
|
|
3997
|
+
asks three decisions: the human identifier, terminal or Telegram, and which of
|
|
3998
|
+
five class families always ask. The default checklist selects `communicate.*`,
|
|
3999
|
+
`financial.*`, `files.delete.*`, `public.*`, and `vcs.push.main`.
|
|
4000
|
+
|
|
4001
|
+
The command validates the complete generated policy before writing it. It
|
|
4002
|
+
creates the same log directory, queue projection, and gitignore entries as
|
|
4003
|
+
`init`, writes `APPROVAL_HUMAN` through the existing `.approval/env` writer,
|
|
4004
|
+
and uses the existing Telegram setup path when selected. A token therefore
|
|
4005
|
+
follows the OS-keystore or no-echo path already documented under [setup channel
|
|
4006
|
+
telegram](#setup-channel-telegram). It refuses before prompting when a policy or
|
|
4007
|
+
`.approval` instance state already exists, so it cannot silently reuse a log,
|
|
4008
|
+
queue, environment map, vault, or channel setup from another ceremony.
|
|
4009
|
+
|
|
4010
|
+
Quickstart resolves this new instance's environment map explicitly, without
|
|
4011
|
+
borrowing ambient approval credentials, and runs a bounded doctor preflight.
|
|
4012
|
+
When `--api-base` is present, the same endpoint is used for Telegram setup and
|
|
4013
|
+
that preflight; a local or private Bot API selection never falls through to the
|
|
4014
|
+
public endpoint.
|
|
4015
|
+
The expected `attestation` failure is the only failed row accepted before the
|
|
4016
|
+
ceremony; any other failed row is printed and stops before attestation. It then
|
|
4017
|
+
prints the exact policy bytes and requires the operator to type `understood`.
|
|
4018
|
+
The append rechecks the live digest and refuses if the file changed after it was
|
|
4019
|
+
shown. An abort or failed step therefore leaves the generated policy unattested.
|
|
4020
|
+
The final `activate:` line includes `approval env --dir` with the absolute,
|
|
4021
|
+
shell-quoted target directory. It is required because no ordinary runtime
|
|
4022
|
+
command loads `.approval/env` implicitly, and it still names the right instance
|
|
4023
|
+
when the operator starts the next shell elsewhere.
|
|
4024
|
+
|
|
4025
|
+
This verb classifies `policy.core` and is omitted from MCP. Piped stdin and
|
|
4026
|
+
`--json` exit 2 before any write and print the manual sequence. The generated
|
|
4027
|
+
`defaults.autonomy: autonomous` applies to other classified reversible actions.
|
|
4028
|
+
Protected controls, fail-closed policy loading, the irreversibility floor, and
|
|
4029
|
+
unclassified-command refusal continue to apply.
|
|
4030
|
+
|
|
3459
4031
|
## init
|
|
3460
4032
|
|
|
3461
4033
|
`init` holds no authority: the policy it writes authorizes nothing until a human
|
|
@@ -3511,6 +4083,20 @@ A HUMAN commits those files: they are `policy.edit`. `docs/agent-sdk-hook.md`
|
|
|
3511
4083
|
is the third caller: a Python Agent SDK application has no settings file, so it
|
|
3512
4084
|
spawns this same verb from a hook callback (APRV-242).
|
|
3513
4085
|
|
|
4086
|
+
**`hook grok` is the one exception to the exit codes above (APRV-243).** Grok
|
|
4087
|
+
Build reads exit 2 as the deny and exit 0 as the allow, whatever stdout said,
|
|
4088
|
+
so on that harness alone a deny is exit 2 with `{"decision":"deny","reason":…}`
|
|
4089
|
+
on stdout, and the post-execution event exits 0 in every case rather than
|
|
4090
|
+
using 2 for visibility. Its envelope is camelCase (`toolName`, `toolInput`,
|
|
4091
|
+
`sessionId`, `hookEventName`, plus a `workspaceRoot` this runtime ignores in
|
|
4092
|
+
favour of `cwd`); snake_case is still read and wins when both spellings are
|
|
4093
|
+
present. `approval hook grok --help` prints the `.grok/hooks/pre-tool-use.json`
|
|
4094
|
+
the human commits, and that entry's own `timeout` must exceed `--timeout`.
|
|
4095
|
+
Grok Build FAILS OPEN on hook timeout, crash and malformed output, with no
|
|
4096
|
+
setting to change it, which the fail-closed invariant does not survive;
|
|
4097
|
+
`docs/grok-hook.md` states which cases the adapter cannot cover and is worth
|
|
4098
|
+
reading before the file is committed.
|
|
4099
|
+
|
|
3514
4100
|
**Register the same command for the post-execution event too (APRV-145).** One
|
|
3515
4101
|
binary answers two events, dispatched on `hook_event_name`. A `PostToolUse` or
|
|
3516
4102
|
`PostToolUseFailure` run closes the delegated `execution.started` the
|
|
@@ -3541,7 +4127,14 @@ gate.self the "approval" CLI itself is pass-through
|
|
|
3541
4127
|
```
|
|
3542
4128
|
|
|
3543
4129
|
Bash (Claude Code) and Shell (Cursor) commands are classified into SPEC.md §7
|
|
3544
|
-
action classes.
|
|
4130
|
+
action classes. A `git push` that names `refs/tags/*`, a bare `v`-prefixed
|
|
4131
|
+
semantic-version-shaped tag, `tag <name>`, `--tags`, or `--follow-tags` is
|
|
4132
|
+
`release.publish`; force and mirror pushes remain `vcs.history.rewrite`; a push
|
|
4133
|
+
that DELETES a remote ref — `--delete`, `-d`, or a colon refspec such as
|
|
4134
|
+
`:refs/heads/x` — is `vcs.ref.delete` with the ref names bound (APRV-352), and a
|
|
4135
|
+
tag deletion keeps `release.publish`; ordinary branch pushes retain their branch
|
|
4136
|
+
or trunk class. Claude file tools
|
|
4137
|
+
(Edit, Write, MultiEdit, NotebookEdit) and
|
|
3545
4138
|
Cursor Write/Delete are gated only when the file is policy-protected
|
|
3546
4139
|
(`APPROVAL.md`, `.approval/`, `CLAUDE.md`, `AGENTS.md`, `.claude/settings*`,
|
|
3547
4140
|
`.cursor/hooks.json`, `.cursor/hooks/`, `.cursor/agents/`,
|
|
@@ -3558,8 +4151,21 @@ its own flags are not parsed as this verb's.
|
|
|
3558
4151
|
no human is asked. This union's spelling of the gate's `class-human-only`,
|
|
3559
4152
|
which the detail names in full. The opposite repair to `hook-unclassified`:
|
|
3560
4153
|
that one says declare a class, this one says a person runs the command.
|
|
4154
|
+
- `hook-harness-launch-unruled` — some class of the command is in the
|
|
4155
|
+
`harness.launch.*` family and this policy names no rule for it (APRV-354).
|
|
4156
|
+
The family resolves only under an explicit rule, `harness.launch.*` or
|
|
4157
|
+
`harness.launch.NAME`, and never under `defaults.autonomy`, because a grant
|
|
4158
|
+
of the class covers the launch and nothing the launched session then does.
|
|
4159
|
+
Distinct from `hook-unclassified` (the classifier had nothing to say; here it
|
|
4160
|
+
was clear and the policy is silent) and from `hook-class-human-only` (the
|
|
4161
|
+
policy has spoken and reserved the class; the repair there is for a person to
|
|
4162
|
+
run the command, and here it is to write a line).
|
|
3561
4163
|
- `hook-opaque` — a construct whose effect cannot be read from the text
|
|
3562
|
-
(`
|
|
4164
|
+
(`eval`, `xargs`, backticks, a non-read substitution). A login shell around
|
|
4165
|
+
ONE inline script is classified by that script since APRV-380, so
|
|
4166
|
+
`zsh -lc 'git push origin main'` is `vcs.push.main`; a script file, an extra
|
|
4167
|
+
word, a redirection on the wrapper, an assignment prefix and a nested shell
|
|
4168
|
+
all stay opaque.
|
|
3563
4169
|
- `hook-unparseable` — the command line could not be tokenized.
|
|
3564
4170
|
- `hook-rejected` — a human said no.
|
|
3565
4171
|
- `hook-revoked` — a granted approval was withdrawn.
|
|
@@ -3638,8 +4244,10 @@ importer collects the bullets under those headings into a second draft fence,
|
|
|
3638
4244
|
` ```yaml approval-values ` (SPEC.md §5.3), printed after the policy draft on
|
|
3639
4245
|
stdout and written after it with `--out`; `--json` carries it as
|
|
3640
4246
|
`values_draft`, or `null` when no such heading exists. Every bullet lands in
|
|
3641
|
-
`
|
|
3642
|
-
|
|
4247
|
+
`like`, the middle grade, and none in `love` or `dislike`: how strongly a line
|
|
4248
|
+
is meant is the human's to say, and an importer that reached for the strongest
|
|
4249
|
+
grade would be putting words in their mouth. (`like` is where what an operator
|
|
4250
|
+
asks for lives since APRV-336 folded `wants` into it.) A
|
|
3643
4251
|
bullet over the schema's 200 characters is truncated with a warning rather than
|
|
3644
4252
|
dropped, and bullets past the twentieth are kept as comments inside the fence,
|
|
3645
4253
|
which is the same stance the permissions half takes on unmapped bullets. The
|
|
@@ -3771,6 +4379,19 @@ in the file said what the operator wanted the work to be like. The optional
|
|
|
3771
4379
|
` ```yaml approval-values ` block (SPEC.md §5.3) is that, and this verb prints
|
|
3772
4380
|
it.
|
|
3773
4381
|
|
|
4382
|
+
**The keys.** `version`, the quoted string `"0.2"` and the only required one;
|
|
4383
|
+
the standing grades `love`, `like` and `dislike`, printed as `loves:`, `likes:`
|
|
4384
|
+
and `dislikes:`; and `communication`, one sentence or two on how the operator
|
|
4385
|
+
reads and answers, printed under `communication:`. APRV-336 settled that shape:
|
|
4386
|
+
a `wants` list for what the operator asks of an agent folded into `like`, since
|
|
4387
|
+
a request about behaviour and a preference about the output are graded by the
|
|
4388
|
+
same person in the same way and one list is easier to keep true, and `responds`
|
|
4389
|
+
became `communication`, which no longer reads as a sibling of `approval
|
|
4390
|
+
feedback`. A block written to the earlier format (`version: 1`, a `wants` list)
|
|
4391
|
+
is refused with the code `version-unsupported` and a message naming both edits,
|
|
4392
|
+
rather than with a schema violation a reader has to decode. The same code
|
|
4393
|
+
answers `version: 0.2` written without quotes, which YAML reads as a float.
|
|
4394
|
+
|
|
3774
4395
|
**It is guidance, and it is never policy.** Every output form opens with the
|
|
3775
4396
|
banner saying so, `--json` carries the same sentence in `note`, and the reason
|
|
3776
4397
|
is the same discipline `journal read` applies in the opposite direction: a
|
|
@@ -4051,7 +4672,22 @@ the clock starts when the daemon starts), and at a clean shutdown when records
|
|
|
4051
4672
|
are still owed. Every attempt goes through the gate as `agent:daemon`: the cycle
|
|
4052
4673
|
registers, requests, and proceeds only where the policy lets it, so a
|
|
4053
4674
|
`supervised-live` draw that selects the advance, or a class that resolves
|
|
4054
|
-
`manual`, stops it with nothing committed and the question in the queue.
|
|
4675
|
+
`manual`, stops it with nothing committed and the question in the queue.
|
|
4676
|
+
|
|
4677
|
+
**Which class it asks under, and which actor may advance autonomously
|
|
4678
|
+
(APRV-382).** Two classes, and the running process picks between them.
|
|
4679
|
+
`log.advance.daemon` is the daemon's own, asked only by the cadence advance
|
|
4680
|
+
inside `approval daemon run` and `approval up`; `log.advance` is what every
|
|
4681
|
+
other actor asks under, a session in a worktree and a human terminal alike. A
|
|
4682
|
+
policy may hold the daemon's class `autonomous` (this repository's does, since
|
|
4683
|
+
the advance publishes records the log already holds, appends nothing and decides
|
|
4684
|
+
nothing) while leaving the base class where it was. Nothing an agent can type
|
|
4685
|
+
reaches the looser line: `approval log advance` classifies `log.advance`
|
|
4686
|
+
whoever runs it. The choice is read from the process rather than from any
|
|
4687
|
+
argument, a policy that declares no rule for the daemon's class leaves the
|
|
4688
|
+
cadence gated exactly as it was, and a cycle that is NOT the daemon's whose
|
|
4689
|
+
class resolves `autonomous` is refused `advance-actor-not-daemon` before
|
|
4690
|
+
anything is appended. A gated
|
|
4055
4691
|
or failed attempt is an `advance` line plus an `advance-refused` warning, and the
|
|
4056
4692
|
next tick tries again — the cadence interval is the retry bound, so a refusal
|
|
4057
4693
|
never loops. One records branch and ONE PULL REQUEST PER DAY: the first advance
|
|
@@ -4302,6 +4938,87 @@ when the log does not.
|
|
|
4302
4938
|
|
|
4303
4939
|
## up
|
|
4304
4940
|
|
|
4941
|
+
**Normal startup after setup and human attestation.** Run from the intended
|
|
4942
|
+
policy project's directory, load its environment explicitly, then leave this
|
|
4943
|
+
foreground process running:
|
|
4944
|
+
|
|
4945
|
+
```sh
|
|
4946
|
+
cd /path/to/your/project
|
|
4947
|
+
eval "$(approval env)"
|
|
4948
|
+
approval up
|
|
4949
|
+
```
|
|
4950
|
+
|
|
4951
|
+
For a source build, replace `approval` in both commands with
|
|
4952
|
+
`node /path/to/approval.md/cli.js`. `--as human:<id>` may name the configured
|
|
4953
|
+
approver explicitly; it does not perform identity setup or policy attestation.
|
|
4954
|
+
`up` reads credentials from its launch environment and does not load
|
|
4955
|
+
`.approval/env`. Existing exported approval variables take precedence over that
|
|
4956
|
+
map, so use a clean shell or unset another instance's variables first. Changing
|
|
4957
|
+
only `--dir` selects the policy; it does not relocate every log, environment or
|
|
4958
|
+
task path. Starting in the intended project keeps the default paths together.
|
|
4959
|
+
|
|
4960
|
+
**Startup messages describe separate parts.** The default task directory is
|
|
4961
|
+
`backlog/tasks/`. If envelopes live elsewhere, pass
|
|
4962
|
+
`--tasks /path/to/existing/task-folder`. The daemon scans regular `.md` files
|
|
4963
|
+
immediately inside that directory, without recursion; an empty default folder
|
|
4964
|
+
does not cover envelopes in nested bundle directories. A missing default folder
|
|
4965
|
+
warns about absent drift coverage while TTL sweeping and queue rendering still
|
|
4966
|
+
run. An explicitly supplied missing directory is an error. `watch-unavailable`
|
|
4967
|
+
reports a failed watcher; the daemon still scans on its interval. Neither
|
|
4968
|
+
warning proves Telegram failed: check the channel's own startup line.
|
|
4969
|
+
|
|
4970
|
+
A `live-draw` doctor failure needs a daemon serving this instance's draw socket.
|
|
4971
|
+
`up` starts that daemon, but serving draws also requires a `supervised-live`
|
|
4972
|
+
policy class and the configured sampling secret resolved in this process's
|
|
4973
|
+
launch environment, with `--no-draw` absent. Starting `up` alone cannot supply a
|
|
4974
|
+
missing secret. Without usable draws, every supervised-live action gates to a
|
|
4975
|
+
human. A policy with no `channels.web.port` and no explicit `--port` serves no
|
|
4976
|
+
web queue; that informational message is legitimate configuration.
|
|
4977
|
+
|
|
4978
|
+
Stop `up` before running Telegram setup or a standalone listener for its bot.
|
|
4979
|
+
One bot must have one polling runtime. After setup changes, reload the instance
|
|
4980
|
+
environment before starting `up` again.
|
|
4981
|
+
|
|
4982
|
+
**Two refusals keep one bot to one instance (APRV-390).** Both are made before
|
|
4983
|
+
the daemon's first tick and before the first poll, and both exit 1 with a
|
|
4984
|
+
machine-readable code on stderr:
|
|
4985
|
+
|
|
4986
|
+
| code | what it means |
|
|
4987
|
+
| --- | --- |
|
|
4988
|
+
| `cross-instance-credential` | A credential variable holds a value this instance did not configure: an `.approval/env` line naming another instance's keystore item, or an export whose value is not what this instance's file resolves to today. The message carries the `unset` line that fixes it. |
|
|
4989
|
+
| `bot-owned-elsewhere` | One `getMe`, before any `getUpdates`, named a bot another instance on this machine has already claimed. The message names that instance's directory. |
|
|
4990
|
+
|
|
4991
|
+
`--allow-cross-instance` overrides both, starts, and prints on every run what it
|
|
4992
|
+
is overriding. Until APRV-390 the first of these was a warning printed on the
|
|
4993
|
+
way past a runtime that had already started, which is how a demo gate spent an
|
|
4994
|
+
evening polling the primary's bot.
|
|
4995
|
+
|
|
4996
|
+
The first refusal COMPARES VALUES rather than only reading names, because it is
|
|
4997
|
+
the one check that is about to use the value. That closes the case names cannot
|
|
4998
|
+
see: `approval env` never overrides a variable this shell has already exported,
|
|
4999
|
+
so a token re-stored in the keystore does not reach a terminal holding the old
|
|
5000
|
+
one, and the daemon answers 401 while the same line read by hand passes `getMe`.
|
|
5001
|
+
It also removes a false positive — an export whose value IS what the file
|
|
5002
|
+
resolves to is correct however it got there, so the documented `eval "$(approval
|
|
5003
|
+
env)"` is not a finding. `approval doctor` keeps the name-only rule; a
|
|
5004
|
+
diagnostic may not block on a keystore-unlock dialog. No value is printed on any
|
|
5005
|
+
path.
|
|
5006
|
+
|
|
5007
|
+
The `getMe` result is recorded against the instance in
|
|
5008
|
+
`.approval/channel-owner.json` (gitignored) and in a per-machine registry under
|
|
5009
|
+
the platform's user state directory, `approval/bots.json`. `APPROVAL_STATE_DIR`
|
|
5010
|
+
relocates that registry. A bot is identified by its id AND its Bot API base,
|
|
5011
|
+
because an id is unique within one deployment and nothing more: two gates
|
|
5012
|
+
pointed at two different `--api-base` values hold two different bots and neither
|
|
5013
|
+
refuses the other. Neither file is evidence, nothing reads them to widen a
|
|
5014
|
+
permission, and losing them costs one `getMe`. An unreachable Bot API is not a
|
|
5015
|
+
refusal: it says so and starts, because a captive portal is also a `getUpdates`
|
|
5016
|
+
that cannot conflict with anything.
|
|
5017
|
+
|
|
5018
|
+
A 409 that still happens at runtime — another machine, or a poller started
|
|
5019
|
+
outside this runtime — is reported once, with the instances the registry knows
|
|
5020
|
+
about, and its repeats are counted rather than reprinted.
|
|
5021
|
+
|
|
4305
5022
|
**The ambient runtime: the daemon loop and every configured channel in one
|
|
4306
5023
|
supervised foreground process.** `approval daemon run --with-channels` is the
|
|
4307
5024
|
same verb spelled from the other side, and it reaches the same function before
|
|
@@ -4324,11 +5041,15 @@ because `git status` does not say what the upstream range changed. So the verb
|
|
|
4324
5041
|
does all four, and `approval daemon run` runs the identical preflight from the
|
|
4325
5042
|
identical module, printing the identical two lines.
|
|
4326
5043
|
|
|
4327
|
-
It is allowed exactly
|
|
5044
|
+
It is allowed exactly four writes: a `--ff-only` merge, `npm run build`,
|
|
5045
|
+
clearing an untracked `backlog/tasks/` file the incoming commit already contains
|
|
5046
|
+
out of the merge's way (APRV-300, described below), and the reconcile `approval
|
|
5047
|
+
log sync` performs on its behalf (APRV-346, described below). It
|
|
4328
5048
|
never resets, never stashes, never checks anything out, and never touches the
|
|
4329
|
-
working log. That list is not caution for its own sake: a working
|
|
4330
|
-
rewound through git underneath a live appender is fork 2 of
|
|
4331
|
-
incident `approval log sync` exists to prevent
|
|
5049
|
+
working log itself. That list is not caution for its own sake: a working
|
|
5050
|
+
`events.jsonl` rewound through git underneath a live appender is fork 2 of
|
|
5051
|
+
2026-08-20, the incident `approval log sync` exists to prevent, which is why the
|
|
5052
|
+
one write that does move that file is made by that verb and by nothing here.
|
|
4332
5053
|
|
|
4333
5054
|
**Safe** means both of: this checkout is not AHEAD of the remote, and no path the
|
|
4334
5055
|
upstream range changes is locally modified. When it is safe, the preflight
|
|
@@ -4338,10 +5059,78 @@ running. When it is not, it refuses, and changes nothing:
|
|
|
4338
5059
|
| code | fires when | next |
|
|
4339
5060
|
|---|---|---|
|
|
4340
5061
|
| `up-preflight-behind-ahead` | `origin/<branch>..HEAD` is non-empty: this checkout carries commits the remote has never seen. A fast-forward is not the operation for that state, and choosing a side is a decision. | look at them (`git log --oneline origin/main..HEAD`), then push them or `git reset --keep` |
|
|
4341
|
-
| `up-preflight-log-diverged` | the upstream range rewrites `.approval/log/events.jsonl` or `.approval/QUEUE.md`,
|
|
5062
|
+
| `up-preflight-log-diverged` | the upstream range rewrites `.approval/log/events.jsonl` or `.approval/QUEUE.md`, this working copy has uncommitted changes to one of them, and the two chains are **not** in a prefix relationship (or cannot be compared at all). The judgment a human could not make by eye. A working log that merely extends the committed one is reconciled instead, see below. | `approval log sync` |
|
|
4342
5063
|
| `up-preflight-dirty-protected` | some other path the upstream range changes is locally modified, so `git merge --ff-only` would refuse rather than overwrite it. | look at the diff, or `approval up --no-preflight` |
|
|
5064
|
+
| `up-preflight-task-file-conflict` | an untracked file under `backlog/tasks/` stopped the fast-forward and it holds lines the incoming copy does not. Which version is wanted is a question, and no verb here will pick. | read the two copies, move yours aside, run `approval up` again |
|
|
4343
5065
|
| `up-preflight-failed` | a write the preflight attempted did not complete: the fast-forward, or the rebuild. Not a judgment, so it is not in the union above; the message names the step, and for a build it names the exit code `npm run build` came back with. | `npm run build` to see the whole error, or `approval up --no-build` if you mean to run the stale one |
|
|
4344
5066
|
|
|
5067
|
+
**A working log that merely extends the committed one is reconciled, not
|
|
5068
|
+
refused (APRV-346).** Every records advance moves `origin/main`'s
|
|
5069
|
+
`.approval/log/events.jsonl` while the hook keeps appending locally, so
|
|
5070
|
+
"upstream changed the log and so did this working copy" is the normal state of
|
|
5071
|
+
the primary checkout after a merge. Refusing it sent the operator to `approval
|
|
5072
|
+
log sync` and then back to `approval up`, every time. So the collision is a
|
|
5073
|
+
question now: `core/log-reconcile.ts` — the same comparison `log sync` and
|
|
5074
|
+
doctor's `log-drift` row use — is asked how the working chain stands to the
|
|
5075
|
+
committed chain at the fetched tip, and:
|
|
5076
|
+
|
|
5077
|
+
- **`ahead`, `behind` or `equal`** — one chain contains the other whole, so
|
|
5078
|
+
adopting the longer one extends and rewinds nothing. The preflight calls
|
|
5079
|
+
`approval log sync` itself, which holds the append lock for its whole ceremony
|
|
5080
|
+
(snapshot, baseline, fast-forward, reconcile, rebuild the projections,
|
|
5081
|
+
post-verify), and prints one line: `synced: fast-forwarded to origin/main
|
|
5082
|
+
<sha>, kept K local records`. The `--json` stream carries it as a
|
|
5083
|
+
`preflight_sync` event and the `preflight` line's `log_synced` reads true.
|
|
5084
|
+
Nothing here reimplements any of that ceremony; it supplies a caller for it;
|
|
5085
|
+
- **`diverged`** — two appenders built different records on one predecessor.
|
|
5086
|
+
Hash chains do not merge, so this is the `up-preflight-log-diverged` refusal
|
|
5087
|
+
above, unchanged, and it is now the **only** case that needs a hand-run
|
|
5088
|
+
`approval log sync` (which will tell you the same thing, at more length).
|
|
5089
|
+
|
|
5090
|
+
Four conditions have to hold before the reconcile is even offered, and each of
|
|
5091
|
+
them answers "refuse" rather than "probably fine": the log is the repository's
|
|
5092
|
+
own `.approval/log/events.jsonl` (not some other file named with `--log`), no
|
|
5093
|
+
*other* path the upstream range touches is locally modified, both chains verify
|
|
5094
|
+
clean, and the relation is a prefix one. A `log sync` that refuses anyway — an
|
|
5095
|
+
appender that took the lock first (`log-sync-locked`), a fork that landed
|
|
5096
|
+
between the read and the ceremony, a git failure — comes back as the same
|
|
5097
|
+
`up-preflight-log-diverged` refusal with the sync's own code and sentence in
|
|
5098
|
+
YOUR STATE. Nothing starts, and the working log is exactly as `log sync` found
|
|
5099
|
+
it.
|
|
5100
|
+
|
|
5101
|
+
**An untracked task file no longer stops it (APRV-300).** A lane files
|
|
5102
|
+
`backlog/tasks/aprv-299` on its branch and its pull request merges, while the
|
|
5103
|
+
primary checkout holds the same path untracked from its own `backlog task
|
|
5104
|
+
create`. `git merge --ff-only` will not write over an untracked file, so on
|
|
5105
|
+
2026-09-07 the preflight refused, and its next-steps text pointed at `git
|
|
5106
|
+
status`, which cannot say whether the local copy holds anything the incoming one
|
|
5107
|
+
does not. That question is answerable, so it is answered. When the merge fails
|
|
5108
|
+
over untracked files and every path git names sits under `backlog/tasks/`, each
|
|
5109
|
+
one is read alongside `git show <target>:<path>` and given one of three
|
|
5110
|
+
verdicts:
|
|
5111
|
+
|
|
5112
|
+
- **byte-identical** — the local copy says nothing the incoming copy does not,
|
|
5113
|
+
so it is removed and the merge is retried once;
|
|
5114
|
+
- **every line also in the incoming copy** — the ordinary shape, a hand-filed
|
|
5115
|
+
stub against a lane copy that added a plan and criteria. Nothing is lost by
|
|
5116
|
+
letting the incoming copy land, but that is a judgment about an operator's
|
|
5117
|
+
file, so the bytes are moved to a sibling of the checkout named
|
|
5118
|
+
`<repo>-preflight-aside-<YYYY-MM-DD>` (outside the repository, so the next
|
|
5119
|
+
fast-forward cannot collide with it again), the destination is printed on the
|
|
5120
|
+
`preflight_warning` line, and the merge is retried once;
|
|
5121
|
+
- **anything else** — `up-preflight-task-file-conflict`, naming your path, the
|
|
5122
|
+
incoming spelling, and how many lines only yours has.
|
|
5123
|
+
|
|
5124
|
+
Every file is judged before any file is touched, the same two-pass shape as
|
|
5125
|
+
`approval log sync`'s payload reconciliation, so a refusal over the last
|
|
5126
|
+
collision cannot have already removed the first. One path outside
|
|
5127
|
+
`backlog/tasks/` and the whole set is declined: the merge keeps its old
|
|
5128
|
+
`up-preflight-failed` refusal and nothing is cleared, because clearing what was
|
|
5129
|
+
understood and then refusing anyway would have moved files for a merge that was
|
|
5130
|
+
never going to run. A path git chose to quote (`core.quotePath`) is declined for
|
|
5131
|
+
the same reason: guessing the spelling of a file about to be moved is the
|
|
5132
|
+
mistake the whole check exists to avoid.
|
|
5133
|
+
|
|
4345
5134
|
**`git reset --hard` is printed on no path, ever**, and a test asserts it. The
|
|
4346
5135
|
one reset that appears is `--keep`, which refuses rather than discarding
|
|
4347
5136
|
uncommitted work, and it is the third step of a runbook whose first step is to
|
|
@@ -4498,9 +5287,11 @@ is the verb that hands them the unit.
|
|
|
4498
5287
|
**There is no `approval vault get`**, and it is not an oversight. A verb that
|
|
4499
5288
|
printed a credential would put it in a terminal, a scrollback buffer, a CI log
|
|
4500
5289
|
and — through the shell that ran it — a history file. A credential's only
|
|
4501
|
-
sanctioned journey is from the vault into an adapter, inside the verified
|
|
4502
|
-
window the adapter contract holds open
|
|
4503
|
-
|
|
5290
|
+
sanctioned journey is from the vault into an adapter, inside the verified
|
|
5291
|
+
execution window the adapter contract holds open. Manual and selected-live
|
|
5292
|
+
paths verify and consume a token; an attested class rule may explicitly
|
|
5293
|
+
authorize an irreversible supervised or autonomous path. Names are visible;
|
|
5294
|
+
values are not.
|
|
4504
5295
|
|
|
4505
5296
|
**What the vault DEFENDS:** credentials at rest, and casual reads by an agent
|
|
4506
5297
|
that can read files in the working tree — the ciphertext hides the NAMES as well
|
|
@@ -4607,26 +5398,47 @@ the check would require this verb to know every adapter a machine might run.
|
|
|
4607
5398
|
|
|
4608
5399
|
## adapter
|
|
4609
5400
|
|
|
4610
|
-
An adapter is the hard boundary of SPEC.md §10.4: it holds the credentials
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
5401
|
+
An adapter is the hard boundary of SPEC.md §10.4: it holds the credentials while
|
|
5402
|
+
the runtime recomputes the payload hash and applies attested policy. Manual and
|
|
5403
|
+
selected-live paths require a valid, unexpired, single-use execution token bound
|
|
5404
|
+
to the action's `idempotency_key` and `payload_hash`. An explicitly opted-in
|
|
5405
|
+
supervised or autonomous path has no grant and mints no token, so `--token` is
|
|
5406
|
+
optional at the command boundary.
|
|
4614
5407
|
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
5408
|
+
Credential custody does not become implicit on the no-token path. The vault
|
|
5409
|
+
passphrase must already be present in the adapter process environment. The
|
|
5410
|
+
`.approval/env` source-map fallback remains available only inside a real token
|
|
5411
|
+
window: it rejects a null grant and a consumed nonmanual execution carrying no
|
|
5412
|
+
token digest. This lets an operator authorize nonmanual execution without
|
|
5413
|
+
giving an agent a new way to load credentials.
|
|
4621
5414
|
|
|
4622
|
-
The
|
|
5415
|
+
The runtime, not the adapter, owns the sequence: recompute the payload hash,
|
|
5416
|
+
read the declared class from the verified log, check policy eligibility or the
|
|
5417
|
+
manual token without appending a start, resolve the credentials the adapter says
|
|
5418
|
+
it cannot act without, run the adapter's own pre-token check, then recheck policy
|
|
5419
|
+
and, on a manual path, verify and consume the token, append
|
|
5420
|
+
`execution.started`, call the adapter, append the outcome. The adapter implements
|
|
5421
|
+
one method and cannot skip a step, because it never holds the sequence.
|
|
5422
|
+
|
|
5423
|
+
`supervised-live` selection still belongs to `approval request`. On a direct
|
|
5424
|
+
no-token invocation with no earlier approval cycle, the adapter contract runs
|
|
5425
|
+
that existing intake path from the verified declaration before it touches a
|
|
5426
|
+
credential. A selected or unavailable draw records the ordinary pending cycle
|
|
5427
|
+
and stops for its token. The request retains the exact already-hashed payload
|
|
5428
|
+
through the normal payload store, so the selected human sees the bound bytes.
|
|
5429
|
+
An unselected draw appends no approval event and continues, bound to the same
|
|
5430
|
+
attested policy digest through eligibility and start. Once any cycle exists,
|
|
5431
|
+
including a rejected or expired one, the contract never redraws it.
|
|
5432
|
+
|
|
5433
|
+
The two steps that sit before authorization starts the execution are there for one reason: a
|
|
4623
5434
|
condition that makes the side effect impossible, and that the runtime can
|
|
4624
5435
|
establish without attempting it, must not cost a human's single-use grant to
|
|
4625
5436
|
discover. A credential nobody stored refuses `credential-unavailable`; whatever
|
|
4626
5437
|
the adapter's own check refuses arrives as `adapter-precheck-refused` with the
|
|
4627
5438
|
adapter's reason in `adapter_code`. Both leave the log exactly as they found it,
|
|
4628
5439
|
`acted` is `false`, there is no `started_seq` and no `outcome`, and the same
|
|
4629
|
-
token executes once the condition is repaired.
|
|
5440
|
+
token executes once the condition is repaired. On an admitted nonmanual path,
|
|
5441
|
+
there is no token to preserve and the same preflight still appends nothing.
|
|
4630
5442
|
|
|
4631
5443
|
The pre-token check is offered only bytes the log binds to the action: the
|
|
4632
5444
|
grant's `payload_hash` on the manual path, the registered declaration's off it.
|
|
@@ -4739,8 +5551,10 @@ The enforcement model it assumes is a split pair of keys. AgentMail keys carry
|
|
|
4739
5551
|
`draft_create`, `draft_update`, `draft_read`, `draft_send` and `message_send`
|
|
4740
5552
|
separately. The agent gets a key WITHOUT the two send permissions, so it can
|
|
4741
5553
|
compose all day and cannot send; the key WITH them goes in the vault under
|
|
4742
|
-
`agentmail.api_key`, readable only inside the
|
|
4743
|
-
|
|
5554
|
+
`agentmail.api_key`, readable only inside the contract's execution window. On a
|
|
5555
|
+
nonmanual path, opening the encrypted vault requires its passphrase in the
|
|
5556
|
+
adapter process environment; the token-scoped source-map fallback does not run.
|
|
5557
|
+
`approval setup adapter agentmail` stores that pair, and
|
|
4744
5558
|
`approval payload agentmail-draft` is the composing side's own verb.
|
|
4745
5559
|
|
|
4746
5560
|
Two payload modes, told apart by the keys they carry, and a payload carrying
|
|
@@ -4811,6 +5625,82 @@ string as `message_id`, under the one key the adapter contract lifts onto
|
|
|
4811
5625
|
as `"provider_ref":{"adapter":"agentmail","id":…}` beside the detail. A send
|
|
4812
5626
|
whose answer names no id carries neither.
|
|
4813
5627
|
|
|
5628
|
+
## adapter zzz
|
|
5629
|
+
|
|
5630
|
+
Creates a zzz.bot thread or reply for `communicate.zzz.external`. The only
|
|
5631
|
+
credential is `zzz.agent_token`, an invited principal token with write scope.
|
|
5632
|
+
It is read from the vault inside the shared execution window and sent only as an
|
|
5633
|
+
Authorization Bearer header. `--token` is required for manual or selected-live
|
|
5634
|
+
execution and omitted for an explicitly policy-authorized supervised or
|
|
5635
|
+
autonomous execution. On the no-token path, the vault passphrase must already be
|
|
5636
|
+
present in the adapter process environment.
|
|
5637
|
+
|
|
5638
|
+
This verb is available from a source checkout containing APRV-320 until the
|
|
5639
|
+
next approval.md package release. npm `approval-md@0.1.0` predates the adapter;
|
|
5640
|
+
this change does not publish a package.
|
|
5641
|
+
|
|
5642
|
+
The contract implemented here is zzz.bot API v0.1.0: [quickstart](https://zzz.bot/quickstart),
|
|
5643
|
+
[API guide](https://zzz.bot/api), [approval semantics](https://zzz.bot/approval),
|
|
5644
|
+
and [OpenAPI](https://zzz.bot/openapi.json).
|
|
5645
|
+
|
|
5646
|
+
The payload is a strict tagged union:
|
|
5647
|
+
|
|
5648
|
+
```json
|
|
5649
|
+
{"environment":"production","operation":"create_thread",
|
|
5650
|
+
"room_id":"<room-id-from-GET-api-v1-rooms>","title":"…","body":"…",
|
|
5651
|
+
"metadata":{},"tags":["…"],
|
|
5652
|
+
"references":[{"kind":"external","target":"https://example.com/source",
|
|
5653
|
+
"label":"Source","relationship":"source"}]}
|
|
5654
|
+
{"environment":"preview","operation":"create_reply",
|
|
5655
|
+
"thread_id":"…","body":"…","metadata":{},"tags":["…"],"references":[]}
|
|
5656
|
+
```
|
|
5657
|
+
|
|
5658
|
+
`environment` is exactly `production` or `preview`; operation is exactly
|
|
5659
|
+
`create_thread` or `create_reply`. Reference `kind` and `relationship` use
|
|
5660
|
+
the alternatives shown above. Unknown keys are refused. Bodies are 1 to 65,536
|
|
5661
|
+
characters, titles 1 to 200, tags at most 10 strings of 1 to 40 characters, and
|
|
5662
|
+
references at most 20. The entire canonical message JSON must fit 65,536 UTF-8
|
|
5663
|
+
bytes, so a maximum-length body can exceed the request limit once its other
|
|
5664
|
+
fields and JSON encoding are included.
|
|
5665
|
+
External reference targets must be HTTP or HTTPS URLs. All optional values that
|
|
5666
|
+
are present remain inside the bound payload and are sent unchanged.
|
|
5667
|
+
|
|
5668
|
+
The message fields are deliberately flat beside the operation tag and target.
|
|
5669
|
+
This keeps the payload a person reviews close to ZZZ's request body. The adapter
|
|
5670
|
+
removes only `environment`, `operation` and the target id when building the
|
|
5671
|
+
POST body; every content field remains byte-for-byte represented in the
|
|
5672
|
+
canonical JSON sent to ZZZ.
|
|
5673
|
+
|
|
5674
|
+
`production` routes to `https://zzz.bot` and `preview` to the fixed preview
|
|
5675
|
+
service. There is no API-base flag. Thread creation posts to
|
|
5676
|
+
`/api/v1/rooms/{room}/threads`; replies post to
|
|
5677
|
+
`/api/v1/threads/{thread}/posts`. Redirects are rejected. The
|
|
5678
|
+
`Idempotency-Key` is deterministic SHA-256 over the RFC 8785 form of the
|
|
5679
|
+
approval action key and payload hash, so the provider's retry identity binds the
|
|
5680
|
+
same action and exact bytes.
|
|
5681
|
+
|
|
5682
|
+
HTTP 201 is accepted only with `{"id":"thr_…"|"pst_…","replayed":false}`,
|
|
5683
|
+
and HTTP 200 only with the same operation-appropriate id and `replayed:true`.
|
|
5684
|
+
The validated service id becomes `provider_ref`. A malformed or inconsistent
|
|
5685
|
+
success, transport error, redirect, or 5xx is `execution.indeterminate` because
|
|
5686
|
+
the POST may have committed. No response text is recorded. Definite refusals
|
|
5687
|
+
map to `zzz-invalid-request` (400), `zzz-unauthorized` (401),
|
|
5688
|
+
`zzz-forbidden` (403), `zzz-not-found` (404),
|
|
5689
|
+
`zzz-idempotency-conflict` (409), `zzz-payload-too-large` (413),
|
|
5690
|
+
`zzz-rejected` (422), or `zzz-rate-limited` (429).
|
|
5691
|
+
|
|
5692
|
+
Public writes require an invited credential with write scope. Private rooms
|
|
5693
|
+
also require current membership carrying write and accepted, unexpired
|
|
5694
|
+
approval.md workflow evidence. zzz.bot intentionally returns 404 when private
|
|
5695
|
+
access is absent, so the adapter does not guess which prerequisite failed.
|
|
5696
|
+
`approval setup adapter zzz` verifies only credential acceptance through one
|
|
5697
|
+
read-only `GET /api/v1/rooms` and posts nothing.
|
|
5698
|
+
|
|
5699
|
+
The local non-guest MCP server publishes this same verb from the registry. MCP
|
|
5700
|
+
invocation remains voluntary. Mechanical enforcement comes from keeping the
|
|
5701
|
+
write credential solely in the vault; an agent that also holds the credential
|
|
5702
|
+
can bypass the adapter.
|
|
5703
|
+
|
|
4814
5704
|
## env
|
|
4815
5705
|
|
|
4816
5706
|
This command is the only thing that reads `.approval/env`, and its default output
|
|
@@ -4909,7 +5799,7 @@ channel surfaces requests and collects decisions and holds no state, so its setu
|
|
|
4909
5799
|
fills the OS keystore and `.approval/env` — the map of where the values that
|
|
4910
5800
|
unlock the machine live. An adapter executes side effects and holds credentials,
|
|
4911
5801
|
so its setup fills `.approval/vault.enc`, which holds the values a gated adapter
|
|
4912
|
-
SPENDS, read inside the verified
|
|
5802
|
+
SPENDS, read inside the verified execution window and by nothing else. There is no
|
|
4913
5803
|
verb that prints one back. (An older build spelled the Telegram one without the
|
|
4914
5804
|
`channel` noun. That form exits 2 and names this one; there is no alias, because
|
|
4915
5805
|
two spellings of a distinction the SPEC draws on purpose is how the distinction
|
|
@@ -5024,6 +5914,51 @@ disables it whenever the policy names no variable, and this verb does not edit a
|
|
|
5024
5914
|
attested policy file. It prints the block to add and the `approval policy amend`
|
|
5025
5915
|
ceremony that attests it.
|
|
5026
5916
|
|
|
5917
|
+
## setup sender-key
|
|
5918
|
+
|
|
5919
|
+
The operator-held key that turns a channel account id into the value an
|
|
5920
|
+
`approvers[id].senders` mapping carries (APRV-370). It exists for the deployment
|
|
5921
|
+
that PUBLISHES its policy and its log, where the raw account id is disclosed
|
|
5922
|
+
once in the file and then on every decision.
|
|
5923
|
+
|
|
5924
|
+
A plain unkeyed digest would not fix that. A Telegram account id is a ten-digit
|
|
5925
|
+
decimal number, the whole space is enumerable on a laptop, and a digest anybody
|
|
5926
|
+
can reverse states a privacy property it does not have. So the mapping value is
|
|
5927
|
+
`hmac-sha256:<hex>`, computed under this key, which is in the environment and
|
|
5928
|
+
never in the policy file.
|
|
5929
|
+
|
|
5930
|
+
Bare, the verb mints, stores and records the key, exactly as `setup sampling`
|
|
5931
|
+
does with its secret, and edits no policy file. The value is not printed and
|
|
5932
|
+
there is no verb that prints it.
|
|
5933
|
+
|
|
5934
|
+
With `--id <account-id>` it mints nothing and stores nothing. It reads the key
|
|
5935
|
+
from the environment, prints the `hmac-sha256:<hex>` for that account, and
|
|
5936
|
+
prints the `senders` line and a paste-ready proposal pair around it. That is the
|
|
5937
|
+
one thing an agent writing a policy proposal cannot do — the digest depends on a
|
|
5938
|
+
secret only the operator's machine holds — so the proposal page says to run it
|
|
5939
|
+
rather than carrying a placeholder:
|
|
5940
|
+
|
|
5941
|
+
```sh
|
|
5942
|
+
approval setup sender-key # once, interactive, human-only
|
|
5943
|
+
eval "$(approval env)"
|
|
5944
|
+
approval setup sender-key --id 7345216485
|
|
5945
|
+
```
|
|
5946
|
+
|
|
5947
|
+
Because it stores nothing and prints a value designed to be published, `--id` is
|
|
5948
|
+
the one `setup` path that runs without a terminal and accepts `--json`.
|
|
5949
|
+
|
|
5950
|
+
**The key is not an authenticator.** Nothing about the gate's safety rests on
|
|
5951
|
+
its secrecy: somebody who learns it learns which account ids a policy names,
|
|
5952
|
+
which is what the raw form told everybody. What it buys is that the published
|
|
5953
|
+
policy and the published log stop carrying the account.
|
|
5954
|
+
|
|
5955
|
+
**A listener that holds no key under a keyed mapping decides nothing** on that
|
|
5956
|
+
channel. Every tap is refused `sender-key-unavailable`, and there is no fallback
|
|
5957
|
+
to comparing the observed id against the raw entries: without the key the
|
|
5958
|
+
runtime cannot tell whether the account is also claimed by a keyed approver, so
|
|
5959
|
+
it cannot run the ambiguity check the mapping rests on. `approval doctor`'s
|
|
5960
|
+
`sender-mapping` row names the form in use and says whether the key resolves.
|
|
5961
|
+
|
|
5027
5962
|
## setup checkpoint
|
|
5028
5963
|
|
|
5029
5964
|
Mints the Ed25519 keypair a human signs the log's head with (APRV-220's record,
|
|
@@ -5080,7 +6015,7 @@ unset, nothing is stored and no vault is created.
|
|
|
5080
6015
|
|
|
5081
6016
|
The values go into the vault, not into the OS keystore and not into
|
|
5082
6017
|
`.approval/env`: what this verb stores is what a gated adapter spends inside a
|
|
5083
|
-
verified
|
|
6018
|
+
verified execution window.
|
|
5084
6019
|
|
|
5085
6020
|
What it reports: the path, the count, the names written and the names left alone.
|
|
5086
6021
|
Never a value, on any path, including a failed probe. Exit 1 means the service
|
|
@@ -5157,7 +6092,7 @@ agentmail.api_key the key that carries draft_send and message_send
|
|
|
5157
6092
|
Store the SENDING key here and give the agent a different one. An AgentMail key
|
|
5158
6093
|
is a mailbox in one string, so a deployment that hands the agent the sending key
|
|
5159
6094
|
has an agent that can send without asking anybody, and the gate in front of it is
|
|
5160
|
-
decoration. The key in the vault is read only inside the verified
|
|
6095
|
+
decoration. The key in the vault is read only inside the verified execution window
|
|
5161
6096
|
the adapter contract opens.
|
|
5162
6097
|
|
|
5163
6098
|
The probe sends nothing. It is `GET /v0/inboxes/{inbox_id}`, the same read a
|
|
@@ -5179,6 +6114,20 @@ adapter's does. A re-run that replaced only one name is offered the same probe
|
|
|
5179
6114
|
over the stored pair, read through `readAgentmailConfig` over the vault: the
|
|
5180
6115
|
exact path `approval adapter agentmail` takes at send time, printed by nothing.
|
|
5181
6116
|
|
|
6117
|
+
## setup adapter zzz
|
|
6118
|
+
|
|
6119
|
+
The manifest contains one secret, `zzz.agent_token`. It must be an invited
|
|
6120
|
+
zzz.bot principal credential with write scope. Store the write-capable token in
|
|
6121
|
+
the vault and keep it out of the agent environment; otherwise the agent can post
|
|
6122
|
+
without passing through the adapter.
|
|
6123
|
+
|
|
6124
|
+
The optional probe sends nothing. It makes one authenticated
|
|
6125
|
+
`GET /api/v1/rooms` against production and reports success only when zzz.bot
|
|
6126
|
+
accepts the credential. That endpoint does not disclose the principal's write
|
|
6127
|
+
scope, room memberships, or accepted private-room workflow evidence, so setup
|
|
6128
|
+
states those limits instead of claiming the token can publish. The actual
|
|
6129
|
+
approved POST remains the first proof of all write prerequisites.
|
|
6130
|
+
|
|
5182
6131
|
## setup channel
|
|
5183
6132
|
|
|
5184
6133
|
A channel is not an adapter, and the two setup verbs fill different stores.
|
|
@@ -5189,9 +6138,11 @@ credentials, so `approval setup adapter <name>` fills the vault instead.
|
|
|
5189
6138
|
|
|
5190
6139
|
## setup channel telegram
|
|
5191
6140
|
|
|
5192
|
-
Stop `approval channel telegram listen`
|
|
5193
|
-
|
|
5194
|
-
|
|
6141
|
+
Stop any `approval up` process or `approval channel telegram listen` polling
|
|
6142
|
+
this bot first. Setup also uses `getUpdates` to discover the approver chat.
|
|
6143
|
+
Competing polls produce HTTP 409 from the Bot API. This is a configuration verb;
|
|
6144
|
+
after it finishes, reload the instance environment and use `approval up` for
|
|
6145
|
+
normal operation.
|
|
5195
6146
|
|
|
5196
6147
|
The token is never typed into this process on a machine with a keystore: the
|
|
5197
6148
|
helper's own no-echo prompt collects it, and this runtime reads it back on stdout
|
|
@@ -5217,6 +6168,28 @@ a `human:<id>` and an `agent:` or `system:` actor is refused at exit 2. Exit 1
|
|
|
5217
6168
|
means the far end refused: an invalid token, a 409 from a running listener, or no
|
|
5218
6169
|
message reaching the bot before the deadline.
|
|
5219
6170
|
|
|
6171
|
+
**One bot per instance (APRV-390), and both names are derived.** The keystore
|
|
6172
|
+
item this verb creates is `approval-tg-token-<instance id>`, where the instance
|
|
6173
|
+
id is the eight hex digits `approval doctor` prints in its `keychain-scope` row,
|
|
6174
|
+
so two gates on one machine store two items. An operator who prefers a readable
|
|
6175
|
+
suffix may write their own name into `.approval/env` instead; nothing is
|
|
6176
|
+
migrated and `approval env --check` reports which item each variable resolves
|
|
6177
|
+
through. The variable is whatever the policy's
|
|
6178
|
+
`channels.telegram.token_env` declares; a policy that declares nothing gets
|
|
6179
|
+
`APPROVAL_TG_TOKEN`, which every other silent policy on the machine also gets,
|
|
6180
|
+
so a second gate on one machine declares a pair of its own (the packaged demo
|
|
6181
|
+
policy declares `APPROVAL_DEMO_TG_TOKEN` and `APPROVAL_DEMO_TG_CHAT`).
|
|
6182
|
+
|
|
6183
|
+
The `getMe` that proves the token also says which bot it is, and this verb
|
|
6184
|
+
records that against the instance in `.approval/channel-owner.json` (gitignored)
|
|
6185
|
+
and in a per-machine registry under the platform's user state directory,
|
|
6186
|
+
`approval/bots.json`. A bot another local instance has already claimed is
|
|
6187
|
+
refused before anything is written, naming that instance's directory; the values
|
|
6188
|
+
are open names and never a token. `--allow-cross-instance` records this instance
|
|
6189
|
+
as an owner anyway and says what it is doing. An instance set up before this
|
|
6190
|
+
existed keeps working: nothing is migrated, and the first `approval up` or
|
|
6191
|
+
listener start writes the record from its own `getMe`.
|
|
6192
|
+
|
|
5220
6193
|
## setup service
|
|
5221
6194
|
|
|
5222
6195
|
**It writes one file: the launchd user agent or the systemd user unit that runs
|
|
@@ -5361,3 +6334,283 @@ queue, so an unbounded guest wait is one stranger stalling every other session.
|
|
|
5361
6334
|
The guest instructions string says so, tells the caller to poll `status`, and
|
|
5362
6335
|
states plainly that a granted request executes nowhere: the demo is the approval
|
|
5363
6336
|
flow itself.
|
|
6337
|
+
|
|
6338
|
+
## Constrained Codex preparation
|
|
6339
|
+
|
|
6340
|
+
approval codex prepare is an artifact generator. It writes one fresh review
|
|
6341
|
+
directory and has no activation path. Its requirements, managed config,
|
|
6342
|
+
launchers and launchd files are text for a human or MDM workflow to inspect.
|
|
6343
|
+
|
|
6344
|
+
approval codex setup --check proves only that those artifacts match their
|
|
6345
|
+
closed manifest and hashes. approval codex doctor --strict asks the separate
|
|
6346
|
+
host question: are the package and its ancestors root-owned and immutable, are
|
|
6347
|
+
the three principals distinct, are the roots disjoint and canonical, and are
|
|
6348
|
+
the broker and runner present? POSIX ownership does not establish ACL custody,
|
|
6349
|
+
so this slice executes no manifest-selected binary and reports runtime versions
|
|
6350
|
+
unchecked. Unknown evidence is a refusal.
|
|
6351
|
+
|
|
6352
|
+
The first slice deliberately made start and serve return codex-not-ready. An
|
|
6353
|
+
npm install, generated config, or passing bundle check does not create a
|
|
6354
|
+
mandatory boundary. The whole approval codex family is operator-only and absent
|
|
6355
|
+
from the ordinary broad MCP catalog.
|
|
6356
|
+
|
|
6357
|
+
### The workspace broker (APRV-325.2)
|
|
6358
|
+
|
|
6359
|
+
approval codex apply is the executable half. It takes an instance manifest and a
|
|
6360
|
+
proposal file, and the split between them is the design: the manifest supplies
|
|
6361
|
+
the acting identity (agent:codex-<instance_id>), the workspace root, the policy
|
|
6362
|
+
file and the log, and the proposal supplies operations and the SHA-256 of the
|
|
6363
|
+
policy it was built against. A proposal naming anything else, an actor, a root,
|
|
6364
|
+
a class, a token, a sandbox posture, is refused input-invalid rather than having
|
|
6365
|
+
the extra key ignored, because a caller that wrote one meant something by it.
|
|
6366
|
+
|
|
6367
|
+
The order of one apply, and why each step is where it is:
|
|
6368
|
+
|
|
6369
|
+
1. Read the log under chain verification, read the policy once, hash those exact
|
|
6370
|
+
bytes, and check them against the latest attestation. An unattested or
|
|
6371
|
+
drifted policy refuses here, before a plan exists.
|
|
6372
|
+
2. Compare the caller's expected digest. A mismatch is attestation-drift: the
|
|
6373
|
+
proposal was built against a policy nobody is enforcing.
|
|
6374
|
+
3. Plan through src/codex/workspace-plan.ts, which does the path work: traversal,
|
|
6375
|
+
symlinks, hardlinks, case aliases, missing sources, existing destinations,
|
|
6376
|
+
preimage digests, and human-only classes refused before any preimage is read.
|
|
6377
|
+
4. Register one action per distinct path class under a task id derived from the
|
|
6378
|
+
payload hash. Classes are never collapsed: the class is what the operator's
|
|
6379
|
+
roster, budget and autonomy are keyed to.
|
|
6380
|
+
5. Authorize every leg. A leg the policy sends to a human refuses
|
|
6381
|
+
approval-required and names the action keys to grant; nothing is written, and
|
|
6382
|
+
the same proposal applies once a person has decided.
|
|
6383
|
+
6. Start every leg before any byte moves. A later leg refusing to start closes
|
|
6384
|
+
the earlier ones execution.failed and leaves the workspace untouched by
|
|
6385
|
+
construction rather than by cleanup.
|
|
6386
|
+
7. Take the workspace lock, then revalidate the plan under it. A revalidation
|
|
6387
|
+
that precedes the lock proves only what was true before another writer could
|
|
6388
|
+
act.
|
|
6389
|
+
8. Stage the new bytes and the preimages on the same filesystem, fsync them,
|
|
6390
|
+
write a journal naming the before-state and the after-state, fsync that, and
|
|
6391
|
+
only then apply.
|
|
6392
|
+
9. Read the workspace back. All-after completes every leg, all-before fails every
|
|
6393
|
+
leg, and anything else, a partial apply, a failed rollback, an endpoint that
|
|
6394
|
+
cannot be read, records execution.indeterminate with the reason
|
|
6395
|
+
workspace-commit-unknown on every leg and retains the journal and the lock.
|
|
6396
|
+
|
|
6397
|
+
approval codex recover reads that retained journal and reports before, after or
|
|
6398
|
+
mixed. It repairs nothing, and the restraint is the point: rolling a mixed
|
|
6399
|
+
workspace forward would guess which half the human approved, and rolling it back
|
|
6400
|
+
would delete the half that committed. Exit 1 means mixed. The resolution is a
|
|
6401
|
+
person's, through approval execution reconcile.
|
|
6402
|
+
|
|
6403
|
+
Custody is claimed only as far as the platform proves it. The broker takes an
|
|
6404
|
+
O_CREAT|O_EXCL lock, which excludes other cooperating brokers and nothing else,
|
|
6405
|
+
and then asks POSIX ownership and mode whether any other principal can write the
|
|
6406
|
+
directories it is about to touch. It reports os-exclusive only when both hold and
|
|
6407
|
+
advisory otherwise, and it always reports acl-unproven, because ownership and
|
|
6408
|
+
mode say nothing about ACLs. --require-exclusive-custody turns the weak answer
|
|
6409
|
+
into a refusal rather than a footnote.
|
|
6410
|
+
|
|
6411
|
+
approval codex serve publishes that broker over stdio as exactly one MCP tool,
|
|
6412
|
+
codex_workspace_apply, checked at call time as well as at list time. It is a
|
|
6413
|
+
different server from approval mcp serve, whose catalog is the whole verb
|
|
6414
|
+
registry and therefore grows: a constrained session has to reach one door, and
|
|
6415
|
+
the same door next month.
|
|
6416
|
+
|
|
6417
|
+
### The confined session (APRV-325.3)
|
|
6418
|
+
|
|
6419
|
+
approval codex start prepares the room a session's shell runs in. With no
|
|
6420
|
+
-- <command> it reports the room and runs nothing, which is what an operator
|
|
6421
|
+
checking a host should not have to start Codex to learn. With one, it runs that
|
|
6422
|
+
command inside the room and exits with the child's own code.
|
|
6423
|
+
|
|
6424
|
+
The shell gets a disposable workspace under the system temporary directory, and
|
|
6425
|
+
that workspace is the only path it may write. The canonical workspace is
|
|
6426
|
+
readable and never writable. Reads are jailed to exactly those two roots
|
|
6427
|
+
(APRV-347), so the gate home, other repositories and everything else the
|
|
6428
|
+
operator's home holds are unreadable whether or not anyone thought to name them,
|
|
6429
|
+
and the credential denials are emitted after the jail's allows so they remain
|
|
6430
|
+
the last word on the vault, the environment map and the sealing keys. The
|
|
6431
|
+
environment is an allow-list rather than a filtered copy of the operator's, so a
|
|
6432
|
+
provider key nobody taught this runtime about is absent rather than forgotten.
|
|
6433
|
+
Outbound network is denied, loopback included. Descendants inherit all of it,
|
|
6434
|
+
which is the property that matters: a session does not write files by calling
|
|
6435
|
+
into this runtime, it spawns shells that do.
|
|
6436
|
+
|
|
6437
|
+
There is no opt-out flag and no unwrapped fallback. approval run has
|
|
6438
|
+
--no-sandbox because a human's grant over exact bytes is authority to reach the
|
|
6439
|
+
world; a confined session has no such authority to present, so a host with no
|
|
6440
|
+
sandbox mechanism refuses (sandbox-unsupported) where approval run would record
|
|
6441
|
+
unsupported and proceed. A command that cannot be resolved on the session PATH is
|
|
6442
|
+
refused rather than spawned outside the room.
|
|
6443
|
+
|
|
6444
|
+
The disposable workspace is removed when the session ends, so a replay of the
|
|
6445
|
+
same shell work starts from an empty room. Only a brokered change survives it,
|
|
6446
|
+
which is the whole arrangement: the shell cannot reach the canonical workspace,
|
|
6447
|
+
and approval codex apply is how a change that a policy admitted does.
|
|
6448
|
+
|
|
6449
|
+
### The app-server bridge (APRV-361)
|
|
6450
|
+
|
|
6451
|
+
```
|
|
6452
|
+
approval codex bridge --prompt <text> [--workspace <dir>] [-- <server command>]
|
|
6453
|
+
```
|
|
6454
|
+
|
|
6455
|
+
approval codex bridge starts `codex app-server` and answers every approval
|
|
6456
|
+
request it raises through the policy and the log. Each
|
|
6457
|
+
`item/commandExecution/requestApproval` carries `command` and `cwd` on the same
|
|
6458
|
+
frame, minted by the harness runtime, and those two fields are exactly the pair
|
|
6459
|
+
the native hook lacks: a Codex `Bash` pre-event names only the command, so
|
|
6460
|
+
`approval hook codex` refuses every shell call as
|
|
6461
|
+
`hook-unsupported-execution-context` rather than bind bytes whose directory it
|
|
6462
|
+
does not know. Here the directory arrives with the question.
|
|
6463
|
+
|
|
6464
|
+
It reuses the hook's decision path rather than a copy of it. The request becomes
|
|
6465
|
+
the hook's own input (tool `Bash`, `tool_input.command` the string the server
|
|
6466
|
+
sent, `cwd` the directory it named) and goes through the same classifier, the
|
|
6467
|
+
same human-only refusal, the same sandbox requirement, the same loop floor and
|
|
6468
|
+
unattended guard, and the same register, request and wait against the verified
|
|
6469
|
+
view. What differs is where the answer goes: `{id, result: {decision}}` on the
|
|
6470
|
+
connection instead of a decision object on stdout.
|
|
6471
|
+
|
|
6472
|
+
The deadline is the policy's `approval_ttl`, not a harness ceiling. Every hook
|
|
6473
|
+
adapter answers inside a timeout its harness sets, and the retry grace exists so
|
|
6474
|
+
a denial-by-deadline is recoverable; this transport has no timeout at all, so a
|
|
6475
|
+
human who answers in eleven minutes is answering rather than arriving too late.
|
|
6476
|
+
`--wait` overrides it.
|
|
6477
|
+
|
|
6478
|
+
It answers accept or decline only, in the vocabulary the request advertised
|
|
6479
|
+
through `availableDecisions`, matched exactly and never by prefix, so
|
|
6480
|
+
`acceptWithExecpolicyAmendment` is not read as an accept. It never sends
|
|
6481
|
+
`acceptForSession` (standing authority for a whole session is a grant shape this
|
|
6482
|
+
project does not have), `cancel` or `abort` (those mean "stop the turn", and
|
|
6483
|
+
sending one would record an interruption as a denial). A request advertising
|
|
6484
|
+
nothing gets `accept` or `decline` and the report says the word was this
|
|
6485
|
+
runtime's own.
|
|
6486
|
+
|
|
6487
|
+
The vocabulary is eight spellings of those two words (`accept`, `approved`,
|
|
6488
|
+
`approve`, `allow`; `decline`, `denied`, `deny`, `reject`), and since APRV-367
|
|
6489
|
+
it is a type rather than a convention: the reply value cannot be constructed
|
|
6490
|
+
outside the list, one function turns a decision into bytes and re-checks
|
|
6491
|
+
membership there, and a word that somehow failed that check would be sent as a
|
|
6492
|
+
decline, since the only safe substitute for a word you cannot name is no. The
|
|
6493
|
+
match is case-insensitive, and what goes on the wire is this runtime's own
|
|
6494
|
+
spelling of the matched word. The `bridge-decisions` conformance suite pins the
|
|
6495
|
+
behaviour for a second implementation.
|
|
6496
|
+
|
|
6497
|
+
Its own refusals, beside the gate's:
|
|
6498
|
+
|
|
6499
|
+
```
|
|
6500
|
+
bridge-request-unbound no command, no cwd, or no call identity on the request
|
|
6501
|
+
bridge-command-unbound a command string that names no argv this client can bind (APRV-362)
|
|
6502
|
+
bridge-file-change-unbound an item-based file change whose content this client cannot
|
|
6503
|
+
produce from the item/started frame its item id names: no such
|
|
6504
|
+
frame, an item that is not a fileChange, an empty change set, or
|
|
6505
|
+
a frame belonging to another thread or turn (APRV-379)
|
|
6506
|
+
bridge-file-change-already-completed
|
|
6507
|
+
item/completed for that item arrived before the question, so the
|
|
6508
|
+
change had finished before this client was asked (APRV-379)
|
|
6509
|
+
bridge-unknown-request a server request this client has no reading for
|
|
6510
|
+
```
|
|
6511
|
+
|
|
6512
|
+
The **item-based file change is correlated, not guessed** (APRV-379). That API
|
|
6513
|
+
puts the content on an earlier `item/started` notification and the approval
|
|
6514
|
+
request refers to it by `itemId`, so the bridge keeps every item the thread
|
|
6515
|
+
announces and decides the request against the frame that id names: the paths
|
|
6516
|
+
take their classes, the payload binds the change set verbatim with
|
|
6517
|
+
`content_sha256` over it as received, and nothing parses the `diff`. The request
|
|
6518
|
+
carries no directory, so the paths resolve against the workspace the bridge
|
|
6519
|
+
named on `thread/start`, and one landing outside it is refused `hook-io`.
|
|
6520
|
+
|
|
6521
|
+
The **exec request binds words, not a rendering** (APRV-362). The item-based API
|
|
6522
|
+
delivers the command as one string, joined from the argv Codex will run, so the
|
|
6523
|
+
bridge un-joins it and the registered payload carries the string that arrived
|
|
6524
|
+
and the argv beside it. A string that is not readable as a join (an unterminated
|
|
6525
|
+
quote, a double quote outside a quoted run, a trailing backslash, or separation
|
|
6526
|
+
no join produces) is `bridge-command-unbound`, because approving it would
|
|
6527
|
+
approve this client's own re-parse. Byte equality with a re-rendering is
|
|
6528
|
+
deliberately not required: a join written for shell safety quotes more than this
|
|
6529
|
+
one does, and demanding equality would refuse ordinary traffic over a quoting
|
|
6530
|
+
rule nothing here records. A legacy argv array is rendered word by word rather
|
|
6531
|
+
than concatenated, so `["bash", "-lc", "rm -rf build"]` reaches the classifier
|
|
6532
|
+
as `bash -lc 'rm -rf build'` and not as five separate words.
|
|
6533
|
+
|
|
6534
|
+
A **legacy `applyPatchApproval` is decided rather than declined** (APRV-363).
|
|
6535
|
+
Its `fileChanges` map rides on the request, so there is nothing to correlate and
|
|
6536
|
+
nothing is re-rendered: the change is classified by the paths it names, through
|
|
6537
|
+
the same protected-path rules every other file tool uses, and the registered
|
|
6538
|
+
payload carries those paths, the change verbatim and `content_sha256` over the
|
|
6539
|
+
map as it arrived. A path that is absolute, or that resolves outside the
|
|
6540
|
+
directory the server named, is refused `hook-io`; a request with a map and no
|
|
6541
|
+
directory (`cwd` or `grantRoot`) is `bridge-request-unbound`. The item-based
|
|
6542
|
+
`item/fileChange/requestApproval`, which carries an identifier and no content,
|
|
6543
|
+
stays declined.
|
|
6544
|
+
|
|
6545
|
+
The thread is started with `approvalPolicy: untrusted` and `sandbox: read-only`.
|
|
6546
|
+
`untrusted` is the wire spelling of the source's `UnlessTrusted`, the only
|
|
6547
|
+
variant under which every command asks, and the server refuses the source name.
|
|
6548
|
+
There is no flag for it: a session gating an unknown fraction of itself is what
|
|
6549
|
+
the pin exists to prevent (APRV-366).
|
|
6550
|
+
|
|
6551
|
+
The pin is checked as well as sent, and a failure ends the run rather than
|
|
6552
|
+
declining one request:
|
|
6553
|
+
|
|
6554
|
+
```
|
|
6555
|
+
bridge-thread-start-refused the server refused thread/start, so no thread
|
|
6556
|
+
exists and no policy was established; its own
|
|
6557
|
+
error is carried verbatim
|
|
6558
|
+
bridge-approval-policy-mismatch the server reported an effective approval
|
|
6559
|
+
policy that is not untrusted
|
|
6560
|
+
```
|
|
6561
|
+
|
|
6562
|
+
A server that reports no policy at all is run against, because the observed
|
|
6563
|
+
0.155.0 server echoes none and a client demanding an echo could not start. The
|
|
6564
|
+
report says which case it was: `thread.requested` is what went on the wire,
|
|
6565
|
+
`thread.effective` is what the server said, and `thread.confirmed` names where
|
|
6566
|
+
the claim comes from: `unconfirmed`, `reported` (a frame echoed the pin back)
|
|
6567
|
+
or `observed` (a probe command produced an approval request that reached this
|
|
6568
|
+
client). Both stops exit 4, as every other protocol stop in this verb does; the
|
|
6569
|
+
code in the report is the part to branch on.
|
|
6570
|
+
|
|
6571
|
+
**A preflight probe runs before every turn** (APRV-364). Codex's auto-reviewer
|
|
6572
|
+
can resolve an approval with a model call before this client is asked, and
|
|
6573
|
+
nothing in the protocol reports whether it is running, so the bridge watches one
|
|
6574
|
+
command instead of reading a setting. Each start asks a preflight turn for
|
|
6575
|
+
`true` and nothing else, before the operator's prompt, with no flag to skip it;
|
|
6576
|
+
it costs one turn per start. The probe's own request is declined immediately as
|
|
6577
|
+
an observation and never reaches the gate, so nothing is registered for it and
|
|
6578
|
+
no approver is asked about it.
|
|
6579
|
+
|
|
6580
|
+
```
|
|
6581
|
+
bridge-preflight-void the preflight turn ran no command at all, so
|
|
6582
|
+
nothing was established; the report carries
|
|
6583
|
+
the turn's frames verbatim and nothing is
|
|
6584
|
+
retried. Run the verb again
|
|
6585
|
+
bridge-auto-reviewer-active an item/autoApprovalReview notification
|
|
6586
|
+
arrived in either turn: something other than
|
|
6587
|
+
this client answered a question
|
|
6588
|
+
```
|
|
6589
|
+
|
|
6590
|
+
A probe that RAN without asking is `bridge-approval-policy-mismatch`, because a
|
|
6591
|
+
policy under which one command did not ask is not `untrusted` whatever the
|
|
6592
|
+
server reports about itself. A probe that was asked about lets the real turn
|
|
6593
|
+
run, and `thread.confirmed` becomes `observed`. That word is narrow on purpose:
|
|
6594
|
+
it says one question reached this client unanswered by anything else, and it is
|
|
6595
|
+
not a claim that the auto-reviewer is off. The report's `preflight` block
|
|
6596
|
+
carries the turn id, the command, the outcome (`asked`, `executed`, `void` or
|
|
6597
|
+
`pending`), the word sent, and, on a void, the frames.
|
|
6598
|
+
|
|
6599
|
+
**It is an advisory checkpoint and not a boundary**, for reasons
|
|
6600
|
+
docs/codex-app-server-bridge.md states in full: Codex's auto-reviewer can
|
|
6601
|
+
resolve a question before this client sees it, and the approval policy and
|
|
6602
|
+
sandbox posture decide how many questions exist. An open gate window is not
|
|
6603
|
+
honoured here either, which is the strict direction. The claim it supports is
|
|
6604
|
+
"this client decided every question this app-server child asked in this
|
|
6605
|
+
session", and nothing wider.
|
|
6606
|
+
|
|
6607
|
+
**Custody is the operating system's** (APRV-365). The server is started by this
|
|
6608
|
+
verb as its own child over stdio pipes: there is no socket, nothing binds a
|
|
6609
|
+
path, and no other process holds a descriptor to speak on, so the replay of a
|
|
6610
|
+
pending request to whatever connects next cannot arise inside one run. The
|
|
6611
|
+
scope of the claim is that child and that session; a Codex started outside this
|
|
6612
|
+
arrangement is a different process and nothing here observes it. The verb does
|
|
6613
|
+
not inspect the command after `--` for a shape that would attach to something
|
|
6614
|
+
already running instead: that would be a guess at another program's command
|
|
6615
|
+
line, and a check written against a guessed shape finds nothing while reporting
|
|
6616
|
+
that it looked.
|