approval-md 0.0.1 → 0.1.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/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +909 -4
- package/SPEC.md +445 -32
- package/cli.js +29 -3
- package/dist/src/adapters/agentmail.js +1200 -0
- package/dist/src/adapters/agentmail.js.map +1 -0
- package/dist/src/adapters/conformance.js +461 -0
- package/dist/src/adapters/conformance.js.map +1 -0
- package/dist/src/adapters/contract.js +941 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/email.js +749 -0
- package/dist/src/adapters/email.js.map +1 -0
- package/dist/src/adapters/env-passphrase.js +132 -0
- package/dist/src/adapters/env-passphrase.js.map +1 -0
- package/dist/src/adapters/registry.js +76 -0
- package/dist/src/adapters/registry.js.map +1 -0
- package/dist/src/adapters/smtp.js +499 -0
- package/dist/src/adapters/smtp.js.map +1 -0
- package/dist/src/adapters/vault-provider.js +161 -0
- package/dist/src/adapters/vault-provider.js.map +1 -0
- package/dist/src/channels/batch.js +121 -0
- package/dist/src/channels/batch.js.map +1 -0
- package/dist/src/channels/cli.js +468 -0
- package/dist/src/channels/cli.js.map +1 -0
- package/dist/src/channels/conformance.js +445 -0
- package/dist/src/channels/conformance.js.map +1 -0
- package/dist/src/channels/contract.js +494 -0
- package/dist/src/channels/contract.js.map +1 -0
- package/dist/src/channels/payload-view.js +43 -0
- package/dist/src/channels/payload-view.js.map +1 -0
- package/dist/src/channels/render-queue.js +564 -0
- package/dist/src/channels/render-queue.js.map +1 -0
- package/dist/src/channels/tagging.js +723 -0
- package/dist/src/channels/tagging.js.map +1 -0
- package/dist/src/channels/telegram.js +3190 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/web.js +903 -0
- package/dist/src/channels/web.js.map +1 -0
- package/dist/src/cli/adapter.js +278 -0
- package/dist/src/cli/adapter.js.map +1 -0
- package/dist/src/cli/amend.js +2171 -0
- package/dist/src/cli/amend.js.map +1 -0
- package/dist/src/cli/args.js +86 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/attest.js +307 -0
- package/dist/src/cli/attest.js.map +1 -0
- package/dist/src/cli/audit-card.js +201 -0
- package/dist/src/cli/audit-card.js.map +1 -0
- package/dist/src/cli/audit.js +460 -0
- package/dist/src/cli/audit.js.map +1 -0
- package/dist/src/cli/channel-telegram.js +2063 -0
- package/dist/src/cli/channel-telegram.js.map +1 -0
- package/dist/src/cli/channel-web.js +357 -0
- package/dist/src/cli/channel-web.js.map +1 -0
- package/dist/src/cli/channel.js +438 -0
- package/dist/src/cli/channel.js.map +1 -0
- package/dist/src/cli/checkpoint-tap.js +238 -0
- package/dist/src/cli/checkpoint-tap.js.map +1 -0
- package/dist/src/cli/coverage.js +343 -0
- package/dist/src/cli/coverage.js.map +1 -0
- package/dist/src/cli/daemon.js +631 -0
- package/dist/src/cli/daemon.js.map +1 -0
- package/dist/src/cli/doctor.js +2648 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/env.js +302 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/execute.js +1682 -0
- package/dist/src/cli/execute.js.map +1 -0
- package/dist/src/cli/exit-codes.js +82 -0
- package/dist/src/cli/exit-codes.js.map +1 -0
- package/dist/src/cli/feedback.js +205 -0
- package/dist/src/cli/feedback.js.map +1 -0
- package/dist/src/cli/gate-window.js +294 -0
- package/dist/src/cli/gate-window.js.map +1 -0
- package/dist/src/cli/gate.js +557 -0
- package/dist/src/cli/gate.js.map +1 -0
- package/dist/src/cli/git-scope.js +295 -0
- package/dist/src/cli/git-scope.js.map +1 -0
- package/dist/src/cli/gloss-attach.js +107 -0
- package/dist/src/cli/gloss-attach.js.map +1 -0
- package/dist/src/cli/gloss-codex-child.js +149 -0
- package/dist/src/cli/gloss-codex-child.js.map +1 -0
- package/dist/src/cli/gloss-codex.js +255 -0
- package/dist/src/cli/gloss-codex.js.map +1 -0
- package/dist/src/cli/gloss-options.js +79 -0
- package/dist/src/cli/gloss-options.js.map +1 -0
- package/dist/src/cli/gloss.js +362 -0
- package/dist/src/cli/gloss.js.map +1 -0
- package/dist/src/cli/help.js +2217 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hook.js +2743 -0
- package/dist/src/cli/hook.js.map +1 -0
- package/dist/src/cli/import.js +175 -0
- package/dist/src/cli/import.js.map +1 -0
- package/dist/src/cli/init.js +336 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.js +262 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/journal.js +238 -0
- package/dist/src/cli/journal.js.map +1 -0
- package/dist/src/cli/log-advance.js +749 -0
- package/dist/src/cli/log-advance.js.map +1 -0
- package/dist/src/cli/log-anchor.js +387 -0
- package/dist/src/cli/log-anchor.js.map +1 -0
- package/dist/src/cli/log-checkpoint.js +128 -0
- package/dist/src/cli/log-checkpoint.js.map +1 -0
- package/dist/src/cli/log-sync.js +849 -0
- package/dist/src/cli/log-sync.js.map +1 -0
- package/dist/src/cli/log-verbs.js +354 -0
- package/dist/src/cli/log-verbs.js.map +1 -0
- package/dist/src/cli/long-help.js +148 -0
- package/dist/src/cli/long-help.js.map +1 -0
- package/dist/src/cli/main.js +1056 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/mcp.js +306 -0
- package/dist/src/cli/mcp.js.map +1 -0
- package/dist/src/cli/paths.js +79 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/payload.js +253 -0
- package/dist/src/cli/payload.js.map +1 -0
- package/dist/src/cli/policy.js +229 -0
- package/dist/src/cli/policy.js.map +1 -0
- package/dist/src/cli/preflight.js +888 -0
- package/dist/src/cli/preflight.js.map +1 -0
- package/dist/src/cli/progress.js +112 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/prompt.js +312 -0
- package/dist/src/cli/prompt.js.map +1 -0
- package/dist/src/cli/records.js +66 -0
- package/dist/src/cli/records.js.map +1 -0
- package/dist/src/cli/render.js +132 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/sandbox.js +150 -0
- package/dist/src/cli/sandbox.js.map +1 -0
- package/dist/src/cli/scaffold.js +137 -0
- package/dist/src/cli/scaffold.js.map +1 -0
- package/dist/src/cli/setup-adapter.js +475 -0
- package/dist/src/cli/setup-adapter.js.map +1 -0
- package/dist/src/cli/setup-channel.js +635 -0
- package/dist/src/cli/setup-channel.js.map +1 -0
- package/dist/src/cli/setup-checkpoint.js +196 -0
- package/dist/src/cli/setup-checkpoint.js.map +1 -0
- package/dist/src/cli/setup-common.js +376 -0
- package/dist/src/cli/setup-common.js.map +1 -0
- package/dist/src/cli/setup-flow.js +476 -0
- package/dist/src/cli/setup-flow.js.map +1 -0
- package/dist/src/cli/setup-service.js +308 -0
- package/dist/src/cli/setup-service.js.map +1 -0
- package/dist/src/cli/setup.js +473 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/style.js +469 -0
- package/dist/src/cli/style.js.map +1 -0
- package/dist/src/cli/token.js +274 -0
- package/dist/src/cli/token.js.map +1 -0
- package/dist/src/cli/up.js +847 -0
- package/dist/src/cli/up.js.map +1 -0
- package/dist/src/cli/usage.js +91 -0
- package/dist/src/cli/usage.js.map +1 -0
- package/dist/src/cli/values.js +189 -0
- package/dist/src/cli/values.js.map +1 -0
- package/dist/src/cli/vault.js +362 -0
- package/dist/src/cli/vault.js.map +1 -0
- package/dist/src/cli/verb-registry.js +2173 -0
- package/dist/src/cli/verb-registry.js.map +1 -0
- package/dist/src/cli/wordmark.js +52 -0
- package/dist/src/cli/wordmark.js.map +1 -0
- package/dist/src/core/advance-cycle.js +200 -0
- package/dist/src/core/advance-cycle.js.map +1 -0
- package/dist/src/core/agents-md.js +747 -0
- package/dist/src/core/agents-md.js.map +1 -0
- package/dist/src/core/attest.js +577 -0
- package/dist/src/core/attest.js.map +1 -0
- package/dist/src/core/audit.js +882 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/budgets.js +449 -0
- package/dist/src/core/budgets.js.map +1 -0
- package/dist/src/core/checkpoint.js +738 -0
- package/dist/src/core/checkpoint.js.map +1 -0
- package/dist/src/core/child-env.js +86 -0
- package/dist/src/core/child-env.js.map +1 -0
- package/dist/src/core/clock.js +43 -0
- package/dist/src/core/clock.js.map +1 -0
- package/dist/src/core/command-class.js +2321 -0
- package/dist/src/core/command-class.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.js +71 -0
- package/dist/src/core/coverage-sources/adapter.js.map +1 -0
- package/dist/src/core/coverage-sources/gh.js +136 -0
- package/dist/src/core/coverage-sources/gh.js.map +1 -0
- package/dist/src/core/coverage-sources/git.js +269 -0
- package/dist/src/core/coverage-sources/git.js.map +1 -0
- package/dist/src/core/coverage.js +337 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/credential-spec.js +23 -0
- package/dist/src/core/credential-spec.js.map +1 -0
- package/dist/src/core/dark-session.js +714 -0
- package/dist/src/core/dark-session.js.map +1 -0
- package/dist/src/core/decision-refusal.js +265 -0
- package/dist/src/core/decision-refusal.js.map +1 -0
- package/dist/src/core/env-file.js +837 -0
- package/dist/src/core/env-file.js.map +1 -0
- package/dist/src/core/execute.js +1233 -0
- package/dist/src/core/execute.js.map +1 -0
- package/dist/src/core/frontmatter.js +100 -0
- package/dist/src/core/frontmatter.js.map +1 -0
- package/dist/src/core/gate-window.js +506 -0
- package/dist/src/core/gate-window.js.map +1 -0
- package/dist/src/core/gate.js +2947 -0
- package/dist/src/core/gate.js.map +1 -0
- package/dist/src/core/git-run.js +93 -0
- package/dist/src/core/git-run.js.map +1 -0
- package/dist/src/core/harness-version.js +210 -0
- package/dist/src/core/harness-version.js.map +1 -0
- package/dist/src/core/harness-wait.js +58 -0
- package/dist/src/core/harness-wait.js.map +1 -0
- package/dist/src/core/head-retry.js +121 -0
- package/dist/src/core/head-retry.js.map +1 -0
- package/dist/src/core/instance.js +319 -0
- package/dist/src/core/instance.js.map +1 -0
- package/dist/src/core/intake-limits.js +350 -0
- package/dist/src/core/intake-limits.js.map +1 -0
- package/dist/src/core/jcs.js +132 -0
- package/dist/src/core/jcs.js.map +1 -0
- package/dist/src/core/journal.js +200 -0
- package/dist/src/core/journal.js.map +1 -0
- package/dist/src/core/live-draw.js +703 -0
- package/dist/src/core/live-draw.js.map +1 -0
- package/dist/src/core/log-reconcile.js +136 -0
- package/dist/src/core/log-reconcile.js.map +1 -0
- package/dist/src/core/log.js +546 -0
- package/dist/src/core/log.js.map +1 -0
- package/dist/src/core/loop.js +476 -0
- package/dist/src/core/loop.js.map +1 -0
- package/dist/src/core/md-fence.js +74 -0
- package/dist/src/core/md-fence.js.map +1 -0
- package/dist/src/core/money.js +195 -0
- package/dist/src/core/money.js.map +1 -0
- package/dist/src/core/payload-census.js +146 -0
- package/dist/src/core/payload-census.js.map +1 -0
- package/dist/src/core/payload-store.js +340 -0
- package/dist/src/core/payload-store.js.map +1 -0
- package/dist/src/core/payload.js +80 -0
- package/dist/src/core/payload.js.map +1 -0
- package/dist/src/core/policy-diff.js +565 -0
- package/dist/src/core/policy-diff.js.map +1 -0
- package/dist/src/core/policy-expectations.js +394 -0
- package/dist/src/core/policy-expectations.js.map +1 -0
- package/dist/src/core/policy-explain.js +230 -0
- package/dist/src/core/policy-explain.js.map +1 -0
- package/dist/src/core/policy-load.js +524 -0
- package/dist/src/core/policy-load.js.map +1 -0
- package/dist/src/core/policy-match.js +467 -0
- package/dist/src/core/policy-match.js.map +1 -0
- package/dist/src/core/policy-proposal.js +458 -0
- package/dist/src/core/policy-proposal.js.map +1 -0
- package/dist/src/core/prompt-layout.js +422 -0
- package/dist/src/core/prompt-layout.js.map +1 -0
- package/dist/src/core/protected-path-guard.js +1087 -0
- package/dist/src/core/protected-path-guard.js.map +1 -0
- package/dist/src/core/registration.js +39 -0
- package/dist/src/core/registration.js.map +1 -0
- package/dist/src/core/reindex.js +336 -0
- package/dist/src/core/reindex.js.map +1 -0
- package/dist/src/core/sampler.js +388 -0
- package/dist/src/core/sampler.js.map +1 -0
- package/dist/src/core/sandbox.js +424 -0
- package/dist/src/core/sandbox.js.map +1 -0
- package/dist/src/core/seal.js +290 -0
- package/dist/src/core/seal.js.map +1 -0
- package/dist/src/core/state.js +1009 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/task-file.js +464 -0
- package/dist/src/core/task-file.js.map +1 -0
- package/dist/src/core/telegram-config.js +114 -0
- package/dist/src/core/telegram-config.js.map +1 -0
- package/dist/src/core/token.js +578 -0
- package/dist/src/core/token.js.map +1 -0
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -0
- package/dist/src/core/values.js +153 -0
- package/dist/src/core/values.js.map +1 -0
- package/dist/src/core/vault.js +612 -0
- package/dist/src/core/vault.js.map +1 -0
- package/dist/src/core/verified-snapshot.js +506 -0
- package/dist/src/core/verified-snapshot.js.map +1 -0
- package/dist/src/core/verify.js +549 -0
- package/dist/src/core/verify.js.map +1 -0
- package/dist/src/core/version.js +9 -0
- package/dist/src/core/version.js.map +1 -0
- package/dist/src/core/wysiwys.js +728 -0
- package/dist/src/core/wysiwys.js.map +1 -0
- package/dist/src/daemon/advance-child.js +78 -0
- package/dist/src/daemon/advance-child.js.map +1 -0
- package/dist/src/daemon/advance.js +849 -0
- package/dist/src/daemon/advance.js.map +1 -0
- package/dist/src/daemon/audit.js +90 -0
- package/dist/src/daemon/audit.js.map +1 -0
- package/dist/src/daemon/daemon.js +1988 -0
- package/dist/src/daemon/daemon.js.map +1 -0
- package/dist/src/daemon/dark-session.js +119 -0
- package/dist/src/daemon/dark-session.js.map +1 -0
- package/dist/src/daemon/draw-child.js +132 -0
- package/dist/src/daemon/draw-child.js.map +1 -0
- package/dist/src/daemon/draw.js +458 -0
- package/dist/src/daemon/draw.js.map +1 -0
- package/dist/src/daemon/git-evidence.js +345 -0
- package/dist/src/daemon/git-evidence.js.map +1 -0
- package/dist/src/daemon/projection.js +233 -0
- package/dist/src/daemon/projection.js.map +1 -0
- package/dist/src/daemon/prune.js +376 -0
- package/dist/src/daemon/prune.js.map +1 -0
- package/dist/src/mcp/http.js +343 -0
- package/dist/src/mcp/http.js.map +1 -0
- package/dist/src/mcp/server.js +594 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/docs/cli-reference.md +5363 -0
- package/package.json +43 -4
- package/schema/.gitkeep +0 -0
- package/schema/LICENSE +117 -0
- package/schema/envelope.schema.json +137 -0
- package/schema/event.schema.json +1810 -0
- package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
- package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
- package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
- package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
- package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
- package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
- package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
- package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
- package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
- package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
- package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
- package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
- package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
- package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
- package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
- package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
- package/schema/fixtures/envelope/valid/canonical.json +25 -0
- package/schema/fixtures/envelope/valid/minimal.json +7 -0
- package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
- package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
- package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
- package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
- package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
- package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
- package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
- package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
- package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
- package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
- package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
- package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
- package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
- package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
- package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
- package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
- package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
- package/schema/fixtures/event/invalid/missing-alg.json +14 -0
- package/schema/fixtures/event/invalid/missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
- package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
- package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
- package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
- package/schema/fixtures/event/invalid/short-hash.json +15 -0
- package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
- package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
- package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
- package/schema/fixtures/event/valid/approval-expired.json +15 -0
- package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
- package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
- package/schema/fixtures/event/valid/approval-granted.json +15 -0
- package/schema/fixtures/event/valid/approval-rejected.json +15 -0
- package/schema/fixtures/event/valid/approval-requested.json +19 -0
- package/schema/fixtures/event/valid/approval-revoked.json +15 -0
- package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
- package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
- package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
- package/schema/fixtures/event/valid/audit-sampled.json +14 -0
- package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
- package/schema/fixtures/event/valid/envelope-drift.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
- package/schema/fixtures/event/valid/execution-completed.json +15 -0
- package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-failed.json +15 -0
- package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
- package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
- package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
- package/schema/fixtures/event/valid/execution-started.json +14 -0
- package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
- package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
- package/schema/fixtures/event/valid/gate-closed.json +14 -0
- package/schema/fixtures/event/valid/gate-opened.json +16 -0
- package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
- package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
- package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
- package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
- package/schema/fixtures/event/valid/payload-pruned.json +17 -0
- package/schema/fixtures/event/valid/policy-declined.json +16 -0
- package/schema/fixtures/event/valid/policy-proposed.json +35 -0
- package/schema/fixtures/event/valid/policy-updated.json +14 -0
- package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
- package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
- package/schema/fixtures/event/valid/route-accepted.json +15 -0
- package/schema/fixtures/event/valid/route-proposed.json +16 -0
- package/schema/fixtures/event/valid/spec-example.json +15 -0
- package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
- package/schema/fixtures/event/valid/task-registered.json +14 -0
- package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
- package/schema/fixtures/hash/known-answer.json +74 -0
- package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
- package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
- package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
- package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
- package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/missing-version.json +8 -0
- package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
- package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
- package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
- package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
- package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
- package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
- package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
- package/schema/fixtures/policy/valid/canonical.json +47 -0
- package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
- package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
- package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
- package/schema/fixtures/policy/valid/global-budgets.json +19 -0
- package/schema/fixtures/policy/valid/human-only.json +9 -0
- package/schema/fixtures/policy/valid/minimal.json +6 -0
- package/schema/fixtures/policy/valid/protected-paths.json +10 -0
- package/schema/fixtures/policy/valid/record-namespace.json +13 -0
- package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
- package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
- package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
- package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
- package/schema/fixtures/policy/valid/wildcards.json +15 -0
- package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
- package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
- package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
- package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
- package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
- package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
- package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/valid/canonical.md +50 -0
- package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
- package/schema/fixtures/policy-md/valid/minimal.md +3 -0
- package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
- package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
- package/schema/fixtures/policy-md/valid/with-values.md +79 -0
- package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
- package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
- package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
- package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
- package/schema/fixtures/sample-record/valid/minimal.json +4 -0
- package/schema/fixtures/sample-record/valid/with-note.json +5 -0
- package/schema/fixtures/values/invalid/class-shaped.json +9 -0
- package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
- package/schema/fixtures/values/invalid/non-string-item.json +4 -0
- package/schema/fixtures/values/invalid/over-cap.json +26 -0
- package/schema/fixtures/values/invalid/unknown-key.json +5 -0
- package/schema/fixtures/values/invalid/version-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +7 -0
- package/schema/fixtures/values/valid/full.json +20 -0
- package/schema/fixtures/values/valid/minimal.json +1 -0
- package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
- package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
- package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
- package/schema/fixtures/values-md/valid/absent.md +50 -0
- package/schema/fixtures/values-md/valid/with-values.md +79 -0
- package/schema/policy.schema.json +481 -0
- package/schema/sample-record.schema.json +26 -0
- package/schema/values.schema.json +55 -0
package/README.md
CHANGED
|
@@ -1,6 +1,911 @@
|
|
|
1
|
-
# approval
|
|
1
|
+
# approval.md
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
approval.md runtime. The full specification is in SPEC.md.
|
|
3
|
+
[](https://github.com/approval-md/approval.md/actions/workflows/ci.yml)
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
**Human approval for agent actions.**
|
|
6
|
+
|
|
7
|
+
Your agent is about to send the email, spend the money, delete the folder, or
|
|
8
|
+
publish the post. A bad diff is revertible, so coding agents have a safety net.
|
|
9
|
+
Once an agent leaves the repository that net disappears: a sent message has no
|
|
10
|
+
revert, and the action carries your name.
|
|
11
|
+
|
|
12
|
+
The permissions section in an AGENTS.md file is prose: two lists, one headed
|
|
13
|
+
"allowed without prompting" and one headed "require approval first", written for
|
|
14
|
+
an agent trusted to obey them. Nothing checks. approval.md is the layer that
|
|
15
|
+
checks:
|
|
16
|
+
|
|
17
|
+
- **A policy file you wrote.** `APPROVAL.md` is human-authored markdown at the
|
|
18
|
+
root of your project, declaring which classes of side effect an agent may take
|
|
19
|
+
on its own, which need you, and under what budgets.
|
|
20
|
+
- **The approve button on your phone.** A request arrives over Telegram (the
|
|
21
|
+
reference channel) carrying what the runtime computed, what the agent claimed,
|
|
22
|
+
and the exact bytes about to leave. You tap Approve or Reject.
|
|
23
|
+
- **A single-use execution token**, minted at one site in the codebase, only as a
|
|
24
|
+
human decision is recorded, spent once, stored nowhere. Adapters holding real
|
|
25
|
+
credentials answer to nothing else.
|
|
26
|
+
- **A log that cannot be quietly rewritten.** Every proposal, decision, and
|
|
27
|
+
execution is an append-only, hash-chained JSONL record, and `approval log
|
|
28
|
+
verify` answers for the chain.
|
|
29
|
+
|
|
30
|
+
Not everything is worth a tap: a class declared `supervised` runs immediately,
|
|
31
|
+
and a policy-declared fraction of those runs is sampled for your retrospective
|
|
32
|
+
review, using a secret the agent cannot read.
|
|
33
|
+
|
|
34
|
+
Spec site: https://approval.md · Specification: [SPEC.md](SPEC.md)
|
|
35
|
+
|
|
36
|
+
## How the gate holds
|
|
37
|
+
|
|
38
|
+
- **Credentials live in an encrypted vault**, never in the policy file and never
|
|
39
|
+
in the agent's environment. `APPROVAL.md` carries the *name* of an environment
|
|
40
|
+
variable, and there is no `approval vault get`.
|
|
41
|
+
- **Adapters answer only to tokens.** The email adapter opens the vault inside a
|
|
42
|
+
verified token window, sends, closes it. An agent without a token reaches no
|
|
43
|
+
credential.
|
|
44
|
+
- **Tokens are minted at one site**, in the path that records a human decision,
|
|
45
|
+
and the log holds only their SHA-256. A second spend is refused
|
|
46
|
+
`token-consumed`.
|
|
47
|
+
- **The log makes tampering evident.** Each record chains to the previous one,
|
|
48
|
+
and projections rebuild from it and never write back.
|
|
49
|
+
- **The harness hook covers the direct-shell path.** `approval hook claude-code`
|
|
50
|
+
classifies the commands a coding agent runs on its own (`git push`, `npm
|
|
51
|
+
install`, `curl`) and answers allow or deny, fail-closed.
|
|
52
|
+
- **The escape hatch is a recorded ceremony.** When the gate itself is broken
|
|
53
|
+
and every command dies, a human opens a time-boxed window with `approval gate
|
|
54
|
+
open`: a terminal, a required `--reason`, and the word `understood`. Every
|
|
55
|
+
call it lets through is logged as `gate.bypassed`, human-only classes stay
|
|
56
|
+
refused, and `approval status` reports unhealthy until it closes. The
|
|
57
|
+
synopsis and a worked example are in
|
|
58
|
+
[docs/cli-reference.md#gate](docs/cli-reference.md#gate).
|
|
59
|
+
|
|
60
|
+
The honest posture, from [SPEC.md](SPEC.md) section 11: this is an oversight
|
|
61
|
+
layer for broadly cooperative agents, with hard enforcement at the adapter
|
|
62
|
+
boundaries that hold the credentials. Identity in v0.1 is config-declared, so
|
|
63
|
+
the trust boundary is the machine rather than cryptography.
|
|
64
|
+
["Can't the agent just go around it?"](#cant-the-agent-just-go-around-it) works
|
|
65
|
+
through each evasion and says where the boundary actually is.
|
|
66
|
+
|
|
67
|
+
The design mantra is **files are the interface, the log is the truth, the
|
|
68
|
+
database is a cache**. Routing, gating, budget math, and chain verification are
|
|
69
|
+
deterministic code. Models propose, and the runtime decides.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npm install -g approval-md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
(Publishing is imminent. Until it lands, `git clone`, `npm ci`, `npm run build`,
|
|
78
|
+
`npm link` in the checkout gives you the same `approval` binary.)
|
|
79
|
+
|
|
80
|
+
Six commands take an empty directory to a machine that will tell you what it is
|
|
81
|
+
missing. `init` authorizes nothing, `policy attest` is what makes a policy
|
|
82
|
+
operative, and `doctor` reports and repairs nothing.
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
mkdir -p /tmp/approval-demo && cd /tmp/approval-demo
|
|
86
|
+
approval init # APPROVAL.md, .approval/log/, QUEUE.md, .gitignore
|
|
87
|
+
approval setup identity # writes where APPROVAL_HUMAN comes from
|
|
88
|
+
eval "$(approval env)" # put the resolved variables in this shell
|
|
89
|
+
approval policy attest # a human signs for these exact policy bytes
|
|
90
|
+
approval doctor # can this machine run the system at all?
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
attested /tmp/approval-demo/APPROVAL.md at seq 1: sha256 cff55216c7be9bfbf35a7d980b6a0c75d250ebc039d7584cb9b3aa3bf25b2f91
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`doctor` prints one line per check and a tally. Three of the 27 lines from a
|
|
98
|
+
fresh directory, plus that tally:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
✓ identity APPROVAL_HUMAN=human:alice (config-declared: the trust boundary is this machine, not cryptography)
|
|
102
|
+
✓ log /tmp/approval-demo/.approval/log/events.jsonl verifies: 1 record(s), head seq 1 0f3c4a19187a…
|
|
103
|
+
✗ audit-sampling disabled (secret-env-unnamed): APPROVAL.md sets audit.supervised_sample_rate to 0.1 but names no audit.sampling_secret_env. …
|
|
104
|
+
fix: approval policy attest --as human:<id> — after setting audit.supervised_sample_rate and audit.sampling_secret_env in the policy; then export the named variable where the daemon runs
|
|
105
|
+
9 ok · 17 not applicable · 1 failed
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The checks run in the order their failures cascade, from build freshness through
|
|
109
|
+
identity, attestation, the log chain, the channels, the payload store, audit
|
|
110
|
+
sampling, envelope integrity, the vault, and the environment source map behind
|
|
111
|
+
`approval env`, then the rows that ask git and the harness what happened. The
|
|
112
|
+
full roster and what a fresh directory skips are under [Running the
|
|
113
|
+
checks](#running-the-checks). Each carries a `fix:` line you run yourself, and
|
|
114
|
+
that one failure is real and intended: the scaffolded policy samples supervised
|
|
115
|
+
actions for audit, sampling needs an operator-held secret the policy only names,
|
|
116
|
+
and a control that looks like it is running while the party under oversight can
|
|
117
|
+
steer it is worse than one that is visibly off. What `init` scaffolds is the
|
|
118
|
+
canonical example policy of SPEC.md section 5.1, which names an approver you are
|
|
119
|
+
probably not. Read every class before you sign for it, then attest again.
|
|
120
|
+
|
|
121
|
+
## Gate your coding agent
|
|
122
|
+
|
|
123
|
+
`approval run` gates the commands an agent hands to the runtime. It cannot gate
|
|
124
|
+
the ones the harness runs directly, and those are most of them. Two surfaces
|
|
125
|
+
close that gap, a PreToolUse hook for Claude Code and an MCP server for any
|
|
126
|
+
harness that speaks MCP, both resolving against the same policy and appending to
|
|
127
|
+
the same log as the CLI.
|
|
128
|
+
|
|
129
|
+
**1. See how a command classifies.** This touches nothing, and it is the fastest
|
|
130
|
+
way to understand a verdict.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
$ approval hook classify -- npm install left-pad
|
|
134
|
+
class rule command
|
|
135
|
+
deps.add npm-install-package npm install left-pad
|
|
136
|
+
|
|
137
|
+
classes: deps.add
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every segment of a command line is classified and the command takes the union, so
|
|
141
|
+
`git status && curl -d … ` is gated as `network.call`.
|
|
142
|
+
|
|
143
|
+
The taxonomy grows where the log shows a class asking for a decision nobody was
|
|
144
|
+
making. `files.delete.scratch` (APRV-267) is the sibling of
|
|
145
|
+
`files.delete.out_of_scope` for a delete whose every target sits strictly under a
|
|
146
|
+
scratch root the agent made itself; everything not provably scratch keeps the old
|
|
147
|
+
class. `vcs.remote.meta` (APRV-268) is exactly three `gh` forms against the
|
|
148
|
+
checkout's own origin, `gh api graphql`, `gh pr update-branch` and `gh run
|
|
149
|
+
rerun`, split out of `network.call` because asking a forge about the repository
|
|
150
|
+
it already tracks is not the send that `network.call` exists for. Any flag
|
|
151
|
+
pointing `gh` at another repository or another host falls back to today's class,
|
|
152
|
+
since the classifier is pure and cannot resolve `origin`.
|
|
153
|
+
|
|
154
|
+
**2. Install the hook.** It lives in `.claude/settings.json`, and a human commits
|
|
155
|
+
that file: an agent that could write its own hook entry could write itself out
|
|
156
|
+
of it.
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "hooks": { "PreToolUse": [ {
|
|
160
|
+
"matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",
|
|
161
|
+
"hooks": [ { "type": "command", "timeout": 600,
|
|
162
|
+
"command": "approval hook claude-code --dir <primary checkout> --as agent:claude-code --timeout 9m" } ]
|
|
163
|
+
} ] } }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`--dir` resolves the policy and the log together, so a session inside a linked
|
|
167
|
+
worktree still writes to the one log. Keep `--timeout` (how long the hook waits
|
|
168
|
+
for a human) comfortably below `timeout` (Claude Code's cap on the process). The
|
|
169
|
+
harness now asks before it acts.
|
|
170
|
+
|
|
171
|
+
**3. Watch a verdict.** An `autonomous` class allows and logs nothing, a
|
|
172
|
+
`supervised` class allows and records `task.registered`, a `manual` class waits
|
|
173
|
+
for your decision, and anything the classifier cannot read denies. There is no
|
|
174
|
+
"ask" answer by design: a decision taken outside the log is a decision nothing
|
|
175
|
+
can audit. The deny reason is `<code>: <detail>`, the codes frozen
|
|
176
|
+
(`hook-unclassified`, `hook-opaque`, `hook-rejected`, `hook-timeout`, and kin).
|
|
177
|
+
|
|
178
|
+
**4. Know the three sharp edges.** The hook never creates a log: pointed at a
|
|
179
|
+
path with no log it denies `hook-log-unreachable` rather than forking a second
|
|
180
|
+
chain, because hash chains do not survive a merge. A wait that runs out withdraws
|
|
181
|
+
its request, so nobody is pinged about a question whose asker has left. And a
|
|
182
|
+
hook grant mints no token: the harness runs the command, `approval token` reports
|
|
183
|
+
`none minted: harness-executed`, and `approval run` refuses with the same code.
|
|
184
|
+
Full account: [docs/claude-code-hook.md](docs/claude-code-hook.md).
|
|
185
|
+
|
|
186
|
+
**5. Or connect the MCP server instead.** `approval mcp serve` is a foreground
|
|
187
|
+
stdio server publishing the agent's verbs as tools, built from the same registry
|
|
188
|
+
`approval instructions --schemas` prints.
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
claude mcp add approval -- \
|
|
192
|
+
node /path/to/approval-md/dist/src/cli/main.js mcp serve \
|
|
193
|
+
--as agent:claude-code \
|
|
194
|
+
--dir /path/to/project
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Ask the client for its tool list. `register`, `request`, `wait`, `run`, `queue`,
|
|
198
|
+
`status`, `log_verify` and the rest of the agent's surface are there; `grant`,
|
|
199
|
+
`reject`, `revoke`, `policy attest` and `vault set` are not, and their absence is
|
|
200
|
+
the design. SPEC.md section 11 makes the agent the untrusted policy and the human
|
|
201
|
+
the trusted overseer, an MCP client is the agent's harness, and a `grant` tool on
|
|
202
|
+
it would hand the untrusted policy the overseer's pen. **Grant never travels over
|
|
203
|
+
MCP**, and neither does the token it mints. The identity is fixed at startup and
|
|
204
|
+
`--as` is deleted from every published input schema, so a tool call cannot name
|
|
205
|
+
an actor. Provoke `unknown tool "grant"` once, deliberately, so you have seen it.
|
|
206
|
+
Walkthrough: [examples/mcp-demo.md](examples/mcp-demo.md).
|
|
207
|
+
|
|
208
|
+
A harness that can simply run commands needs neither surface: `request`, `wait`,
|
|
209
|
+
`run` is how sessions in this repository take manual-class actions
|
|
210
|
+
([docs/dogfood-cutover.md](docs/dogfood-cutover.md)). The task-file side of that
|
|
211
|
+
flow, on a Backlog.md board with a policy of its own, is the worked example in
|
|
212
|
+
[examples/backlog-md-project/README.md](examples/backlog-md-project/README.md):
|
|
213
|
+
one envelope on one task file, then `register`, `request`, `wait`, `run`, with
|
|
214
|
+
what each prints. There is no Backlog.md adapter, and the example says why.
|
|
215
|
+
|
|
216
|
+
## Put approvals on your phone
|
|
217
|
+
|
|
218
|
+
**1. Create a bot and let setup do the rest.** Message **@BotFather** with
|
|
219
|
+
`/newbot`, then:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
approval setup identity # APPROVAL_HUMAN, validated
|
|
223
|
+
approval setup channel telegram # token into the keystore, getMe, chat discovery
|
|
224
|
+
eval "$(approval env)" # put them in this shell
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`setup` writes `.approval/env`, the environment source map: the secret goes into
|
|
228
|
+
the OS keystore (macOS Keychain, or `secret-tool` on Linux) and the file records
|
|
229
|
+
only where it lives. It is interactive by refusal (a pipe or `--json` exits 2 and
|
|
230
|
+
prints the non-interactive commands), because a setup a CI job could drive would
|
|
231
|
+
be a way for a CI job to declare a human identity. `approval env` is the only
|
|
232
|
+
command that reads that file, and evaluating it is a step a human takes. Full
|
|
233
|
+
walkthrough: [examples/telegram-demo.md](examples/telegram-demo.md).
|
|
234
|
+
|
|
235
|
+
**2. Bind a request to exact bytes.** The payload lives in a file, the envelope
|
|
236
|
+
declares its `payload_hash`, and `--payload` supplies the bytes at request time:
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
approval payload hash payload.json # the binding the envelope declares
|
|
240
|
+
approval register task-demo.md --as agent:drafter
|
|
241
|
+
approval request task-demo --action task-demo:chaser --payload payload.json --as agent:drafter
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
registered task-demo at seq 2: 1 action(s)
|
|
246
|
+
requested task-demo task-demo:chaser at seq 3 (manual)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Material that hashes to something else is refused `payload-mismatch`, and nothing
|
|
250
|
+
is stored and nothing is appended. Class, cost, and reversibility come from the
|
|
251
|
+
registered envelope rather than from flags, so an agent cannot rename its own
|
|
252
|
+
class between registering and asking. An approval is about specific bytes, never
|
|
253
|
+
about a description of them.
|
|
254
|
+
|
|
255
|
+
**3. Start the runtime and read the message.** `approval up` prints
|
|
256
|
+
`notified task-demo:chaser (message 501)` and your phone has it. That one
|
|
257
|
+
foreground process is the whole gate: the daemon loop that records envelope
|
|
258
|
+
drift, expires what lapsed and regenerates the queue, plus every channel the
|
|
259
|
+
policy configures. A channel whose credential variable is unset is not started,
|
|
260
|
+
says so in the words `approval doctor` uses, and the daemon runs anyway; a
|
|
261
|
+
channel that falls over is restarted with a doubling backoff while the loop keeps
|
|
262
|
+
ticking. `approval daemon run` and `approval channel telegram listen` still run
|
|
263
|
+
the halves separately and behave identically, and `approval setup service` writes
|
|
264
|
+
the launchd or systemd user unit that starts the runtime at login (printing the
|
|
265
|
+
whole unit for you to read first, naming variables and never copying a value).
|
|
266
|
+
|
|
267
|
+
To use the signed-in Codex CLI for the optional unverified gloss, name both the
|
|
268
|
+
provider and model:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
approval up --gloss-provider codex --gloss-model gpt-5.4-mini
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Claude with model `haiku` remains the default. The runtime never falls back
|
|
275
|
+
between providers; a failed gloss is omitted and approval delivery continues.
|
|
276
|
+
The message shows the action key, a **COMPUTED** block the runtime derived (class,
|
|
277
|
+
task, state, binding, budget verdicts, chain head), a **CLAIMED** block naming the
|
|
278
|
+
agent and marked unverified, the **FULL PAYLOAD**, and two buttons. It also says
|
|
279
|
+
how long an answer still has: `waiting: requested 4 min ago · expires 13:07 UTC
|
|
280
|
+
(clock)`, or, for a request some process is blocked on, `requester waits until
|
|
281
|
+
13:07 UTC`, the deadline that actually applies to you.
|
|
282
|
+
|
|
283
|
+
A payload the runtime recognises by shape is laid out so a human can read it: an
|
|
284
|
+
email-shaped payload (recipients, subject, body) is rendered field by field with
|
|
285
|
+
real line breaks, and the canonical JSON and its bound hash sit underneath
|
|
286
|
+
unchanged. Detection is structural and never reads a self-declared type, because
|
|
287
|
+
a field the requesting agent authored must not choose its own presentation.
|
|
288
|
+
Agent-authored text is HTML-escaped, so markup stays inert.
|
|
289
|
+
|
|
290
|
+
**4. Tap Approve.** The prompt rewrites itself in place. The buttons go away and
|
|
291
|
+
the text becomes the outcome:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
✓ APPROVED
|
|
295
|
+
task-demo:chaser
|
|
296
|
+
|
|
297
|
+
by human:alice at 10:20 UTC (seq 4)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
One edit call carries the annotation and the disarming together, so there is no
|
|
301
|
+
window in which the message reads "approved" and still offers a tap. Rejections,
|
|
302
|
+
revocations, expiries and withdrawals settle the same way with their own
|
|
303
|
+
headline, a decision taken at another surface annotates the prompt on the next
|
|
304
|
+
poll cycle, and a tap on a stale button is answered with a toast and records
|
|
305
|
+
nothing.
|
|
306
|
+
|
|
307
|
+
**5. Take the token from the terminal, not the chat.** The grant mints a
|
|
308
|
+
single-use execution token, printed once, in a panel, at whichever surface
|
|
309
|
+
recorded the decision:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
granted task-demo:chaser at seq 4 by human:alice
|
|
313
|
+
─────────────────────────────────────────────────────────────
|
|
314
|
+
execution token task-demo:chaser
|
|
315
|
+
516670320878e97dede99cf84bc48025fc80b7cf14bd9e9782bb1cfd0d92a787
|
|
316
|
+
single-use · stored nowhere · copy it now
|
|
317
|
+
─────────────────────────────────────────────────────────────
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
For a tap on your phone the same panel appears on the terminal running the
|
|
321
|
+
runtime, and its last line reads `not sent to Telegram`. Delivery differs per channel on
|
|
322
|
+
purpose: a chat transcript lives on servers you do not control and is readable by
|
|
323
|
+
anyone later added to that chat, so a credential does not go there, while the
|
|
324
|
+
local **web** channel shows the raw token once in the response page for the grant
|
|
325
|
+
that minted it, served over loopback, generated per request, persisted nowhere,
|
|
326
|
+
and gone on reload: there the browser is already the surface the human is looking
|
|
327
|
+
at. In both cases the log holds only the token's SHA-256, it never appears in a
|
|
328
|
+
URL, and nothing can recover it. Lose it, revoke the grant, and request again.
|
|
329
|
+
|
|
330
|
+
**6. Spend it.** `approval run <action> --token "$TOKEN" -- <command>` appends
|
|
331
|
+
`execution.started` before spawning the child and
|
|
332
|
+
`execution.completed` after, and exits with the child's own exit code, so it
|
|
333
|
+
composes with `make`, CI, and `&&` as an unwrapped command would. Run it before
|
|
334
|
+
the approval and it refuses `token-required` at exit 5, writing nothing. Run it
|
|
335
|
+
twice and it refuses:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
✗ token-consumed action task-demo:chaser already executed: execution.started at seq 5 spent this token. A token is single-use and the log is the proof.
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
A request is not owed an answer forever, either. `approval withdraw` lets the
|
|
342
|
+
party that opened one take it back while it is pending, and `approval wait
|
|
343
|
+
--withdraw-on-timeout` does it for you when your own wait elapsed.
|
|
344
|
+
|
|
345
|
+
**7. Read the whole story.** Two actors, one clean chain:
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
1 2026-08-19T19:03:58.381Z policy.updated human:alice -
|
|
349
|
+
2 2026-08-19T19:03:58.585Z task.registered agent:drafter task-demo
|
|
350
|
+
3 2026-08-19T19:03:58.767Z approval.requested agent:drafter task-demo
|
|
351
|
+
4 2026-08-19T19:04:31.192Z approval.granted human:alice task-demo
|
|
352
|
+
5 2026-08-19T19:04:41.371Z execution.started agent:drafter task-demo
|
|
353
|
+
6 2026-08-19T19:04:41.499Z execution.completed agent:drafter task-demo
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
That is `approval log tail` piped, fields tab-separated for `cut` and its kin; on
|
|
357
|
+
a terminal it aligns and colours its columns. `approval log verify` answers for
|
|
358
|
+
the chain: `clean: 6 record(s), head seq 6 843705c6bbea…`.
|
|
359
|
+
|
|
360
|
+
## The other half of the word
|
|
361
|
+
|
|
362
|
+
Everything above is control: what an agent may do, who decides, what is
|
|
363
|
+
sampled. From 0.1.0 the file carries the human's voice too. Below the policy
|
|
364
|
+
block, `APPROVAL.md` may hold one optional `yaml approval-values` block:
|
|
365
|
+
what you love, like and dislike in the work, what you want from an agent as
|
|
366
|
+
behaviour, and how you read and answer.
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
approval values # the operator's block, or "the operator has declared no values here."
|
|
370
|
+
approval feedback # the reactions and notes humans left on this log's actions
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
A retrospective review or a grant can carry a graded reaction (`disliked`,
|
|
374
|
+
`indifferent`, `liked`, `loved`; the two extremes need a note), and
|
|
375
|
+
`approval feedback` reads them back to the agent whose work they were about.
|
|
376
|
+
Both verbs print human-authored guidance behind a banner that says so, and
|
|
377
|
+
neither reaches enforcement: no verdict, sample, budget or token is moved by
|
|
378
|
+
anything in them (SPEC.md section 11.1, invariant 10). They are the mirror of
|
|
379
|
+
`approval journal write`, the agent's outlet the gate does not stand in front
|
|
380
|
+
of. The importer drafts the block too: `approval import agents-md` turns a
|
|
381
|
+
"What I value" heading into a `wants` list for you to grade.
|
|
382
|
+
|
|
383
|
+
## Define what needs approval
|
|
384
|
+
|
|
385
|
+
A policy is a fenced `yaml approval-policy` block inside a markdown file named
|
|
386
|
+
`APPROVAL.md`. The prose around the block is for you; the runtime parses the
|
|
387
|
+
block and ignores the rest. That is the point of the format: the thing you sign
|
|
388
|
+
for is text you read.
|
|
389
|
+
|
|
390
|
+
**1. Name the classes.** A class is a dotted path from the side-effect taxonomy
|
|
391
|
+
of SPEC.md section 7 (`communicate.email.external`, `financial.spend`,
|
|
392
|
+
`public.post`, `data.delete`, `read.*`). Matching is most-specific-first, `*` is
|
|
393
|
+
a single-segment wildcard, a trailing `.*` matches any depth, and at equal
|
|
394
|
+
specificity the strictest rule wins.
|
|
395
|
+
|
|
396
|
+
**2. Pick an autonomy for each.** Six values, strictest first: `human-only` (a
|
|
397
|
+
person performs the action outside agent execution, and every gate verb refuses
|
|
398
|
+
an agent with `class-human-only`), `manual` (a human decides before execution),
|
|
399
|
+
`supervised-live` (a policy-declared fraction blocks on the gate exactly as
|
|
400
|
+
`manual` does, and the rest proceed, so the rule carries a `live_rate`),
|
|
401
|
+
`supervised-retro` (executes immediately, a sampled fraction escalated for
|
|
402
|
+
retrospective review), `supervised` (the pre-split spelling, an alias of
|
|
403
|
+
`supervised-retro`, and the runtime records a load-time note naming the alias),
|
|
404
|
+
`autonomous` (executes freely). An email is `reversible: false`, which engages
|
|
405
|
+
section 7's irreversibility floor: the class resolves to `manual` even where the
|
|
406
|
+
policy says `supervised`, because retrospective sampling cannot un-send a
|
|
407
|
+
message.
|
|
408
|
+
|
|
409
|
+
**3. Set the budgets.** Class `limits` and the `budgets` scopes are conjunctive,
|
|
410
|
+
so an action must pass both, and consumption is computed from the log over
|
|
411
|
+
rolling windows rather than from a mutable counter. An action whose class matches
|
|
412
|
+
no rule takes `defaults.autonomy`, and a policy that does not parse resolves every
|
|
413
|
+
class to `manual`: unattested and unparseable are both strict, never permissive.
|
|
414
|
+
|
|
415
|
+
**4. Widen the protected paths.** `APPROVAL.md`, the agent instruction files,
|
|
416
|
+
`.approval/`, the harness settings and the release configuration are protected by
|
|
417
|
+
the runtime whatever a policy says. `protected_paths` adds repo-relative literals
|
|
418
|
+
(an exact file, `SPEC.md`, or a directory prefix, `design/`), so a project can put
|
|
419
|
+
its own governing documents behind the gate that already stands in front of its
|
|
420
|
+
policy. The key can only widen, and globs are a schema violation.
|
|
421
|
+
|
|
422
|
+
An entry can also be an object, `{path, class}`, which routes that path family to
|
|
423
|
+
a named `policy.edit` sub-class so it carries its own autonomy and its own live
|
|
424
|
+
rate. Four names are reserved with fixed meanings, so two policies mean the same
|
|
425
|
+
thing by them: `policy.edit.spec` (the governing specification),
|
|
426
|
+
`policy.edit.harness` (agent instruction files and harness configuration that is
|
|
427
|
+
not the hook itself), `policy.edit.ci` (continuous-integration and release
|
|
428
|
+
configuration), `policy.edit.design` (design documents and decision records). Any
|
|
429
|
+
other lowercase word may be minted beside them, and nothing outside `policy.edit`
|
|
430
|
+
may be named: a route to `policy.core` or `log.mutate` is refused, since a policy
|
|
431
|
+
that could widen its own protected surface mints no authority over the gate's own
|
|
432
|
+
organs. A route aimed at a built-in protected path must land at least as strictly
|
|
433
|
+
as the `policy.edit` line itself, and a policy that breaks that floor is refused
|
|
434
|
+
at load with `protected-route-floor`.
|
|
435
|
+
|
|
436
|
+
**5. Attest it.** `approval policy attest` is what makes a policy operative. An
|
|
437
|
+
attestation records that a human saw these exact bytes, and it records their
|
|
438
|
+
SHA-256 rather than their text. Edit `APPROVAL.md` afterwards and every gated
|
|
439
|
+
operation refuses `hash-mismatch` until you attest again. Attestation is
|
|
440
|
+
human-only, and identity in v0.1 is config-declared, so what one proves is that
|
|
441
|
+
*someone with local control* signed off.
|
|
442
|
+
|
|
443
|
+
**6. Amend it with the verb, not by hand.** Changing a policy is two facts that
|
|
444
|
+
have to land together, the new bytes and a human's attestation of them, and
|
|
445
|
+
`approval policy amend` owns the whole ceremony (`--dry-run` reports only,
|
|
446
|
+
`--require-load` refuses to attest a policy that does not load, `--commit` lands
|
|
447
|
+
the two files as one commit). It prints a **semantic diff** (class resolutions,
|
|
448
|
+
approver changes, defaults, limits) rather than a text diff, so you see what
|
|
449
|
+
changed in meaning; the baseline comes from `HEAD:<policy>` and is used only when
|
|
450
|
+
its SHA-256 equals the attested hash, and otherwise the verb drops loudly to
|
|
451
|
+
hash-only mode. Then it prints a **load advisory**: whether the edited policy
|
|
452
|
+
actually parses. Attesting one that does not is still allowed, since attestation
|
|
453
|
+
records bytes and not correctness, but such a policy fails closed to all-manual.
|
|
454
|
+
|
|
455
|
+
### Why this verb exists: seq 2
|
|
456
|
+
|
|
457
|
+
Read this repository's own log. At **seq 2** a policy amendment was attested at
|
|
458
|
+
11:56:07. It was **superseded** seven minutes later, at seq 3 at 12:03:35,
|
|
459
|
+
because the edit broke a pinned assertion and nobody found out until the test
|
|
460
|
+
suite ran against it. The operator attested bytes whose consequences had never
|
|
461
|
+
been shown to them.
|
|
462
|
+
|
|
463
|
+
This account originally said eleven minutes. The log says seven, and the log
|
|
464
|
+
won: the figure was corrected against the chain after being misremembered, which
|
|
465
|
+
is the whole thesis of keeping one.
|
|
466
|
+
|
|
467
|
+
That is the failure the load advisory is for. Had `approval policy amend` existed
|
|
468
|
+
that morning, the load failure would have been on screen while the human was
|
|
469
|
+
deciding, and `--require-load` would have refused to attest at all. The incident
|
|
470
|
+
is cited by number on purpose: it is in the log, it is checkable, and the log is
|
|
471
|
+
the truth.
|
|
472
|
+
|
|
473
|
+
## Hand a grant to a real credential
|
|
474
|
+
|
|
475
|
+
`echo sent` is a demo. The point of the gate is the send that cannot be undone,
|
|
476
|
+
so the runtime holds a credential the agent never sees. Four commands carry the
|
|
477
|
+
ceremony; the walkthrough against real Telegram and a real mail provider is
|
|
478
|
+
[examples/email-demo.md](examples/email-demo.md).
|
|
479
|
+
|
|
480
|
+
```sh
|
|
481
|
+
approval setup vault # mint the passphrase, store it, record where
|
|
482
|
+
approval setup adapter email # the five SMTP settings, into the vault
|
|
483
|
+
eval "$(approval env)" # the variable the policy names, in this shell
|
|
484
|
+
approval adapter email task-042:chaser --token "$TOKEN" \
|
|
485
|
+
--payload message.json --as agent:claude-admin
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**1. The two stores divide cleanly.** `.approval/env` says where the values that
|
|
489
|
+
unlock the machine come from, and `approval setup vault` writes the passphrase
|
|
490
|
+
line under whatever name `vault.passphrase_env` declares. The SMTP password is an
|
|
491
|
+
adapter credential, so it goes in the vault instead, where a gated adapter spends
|
|
492
|
+
it inside a verified token window.
|
|
493
|
+
|
|
494
|
+
**2. Setup fills the vault and proves it.** `approval setup adapter email` reads
|
|
495
|
+
the credential manifest the adapter declares, then probes the server without
|
|
496
|
+
sending anything; a partial re-run probes the **merged** configuration.
|
|
497
|
+
|
|
498
|
+
**3. A credential's only journey is into an adapter.** `approval vault set`
|
|
499
|
+
stores one credential in `.approval/vault.enc`, encrypted under a passphrase the
|
|
500
|
+
policy names and never carries. The value comes from stdin or `--value-env
|
|
501
|
+
<VAR>`; there is no `--value` flag, because a secret on a command line is a
|
|
502
|
+
secret in the shell history and in `ps` output. There is no `approval vault get`
|
|
503
|
+
and will not be; `approval vault list` shows the names.
|
|
504
|
+
|
|
505
|
+
**4. The send happens inside the token window.** `approval adapter email` verifies
|
|
506
|
+
the token, re-hashes `message.json` against the binding the grant recorded,
|
|
507
|
+
appends `execution.started`, opens the vault, reads the five SMTP settings inside
|
|
508
|
+
the window, sends over STARTTLS, closes the window, and appends
|
|
509
|
+
`execution.completed`. The credential exists for one send and appears in no
|
|
510
|
+
event, no output, no error message. Nothing about the vault is ever a log entry:
|
|
511
|
+
a list of the credentials an operator holds is a map of the machine's reach.
|
|
512
|
+
|
|
513
|
+
**5. Check two properties in your own mailbox.** The bytes that left are the
|
|
514
|
+
bytes you approved, since the hash the token spend verified is the hash of the
|
|
515
|
+
payload your phone displayed. And the `Message-ID` is derived from the action
|
|
516
|
+
key, the payload hash and the sender, so the header in a mailbox and the binding
|
|
517
|
+
in the chain identify each other months later.
|
|
518
|
+
|
|
519
|
+
### The same grant over AgentMail
|
|
520
|
+
|
|
521
|
+
`communicate.email.external` has a second adapter. Where the email adapter opens
|
|
522
|
+
an SMTP session, `approval adapter agentmail` calls the AgentMail API, and the
|
|
523
|
+
mail an agent has already composed as a Draft leaves only when a grant says so.
|
|
524
|
+
The walkthrough is [examples/agentmail-demo.md](examples/agentmail-demo.md).
|
|
525
|
+
|
|
526
|
+
```sh
|
|
527
|
+
approval setup adapter agentmail # inbox id + sending key, into the vault
|
|
528
|
+
approval payload agentmail-draft "$INBOX" "$DRAFT" > payload.json
|
|
529
|
+
approval adapter agentmail task-042:chaser --token "$TOKEN" \
|
|
530
|
+
--payload payload.json --as agent:claude-admin
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Two keys, and the split is the enforcement.** AgentMail API keys carry
|
|
534
|
+
per-permission booleans, and `draft_create`, `draft_update` and `draft_read` are
|
|
535
|
+
separate from `draft_send` and `message_send`. Give the agent a key holding the
|
|
536
|
+
first three and none of the last two, and put a key holding the send permissions
|
|
537
|
+
in the vault, where the adapter reads it inside the verified token window. The
|
|
538
|
+
agent then composes all day and cannot send at all: an ungated send attempt is
|
|
539
|
+
refused by AgentMail itself, `agentmail-unauthorized`, before this runtime is
|
|
540
|
+
involved. Without that split, an AgentMail key sitting in the agent's
|
|
541
|
+
environment is a full bypass of the gate, which is why `AGENTMAIL_` is withheld
|
|
542
|
+
from every child `approval run` spawns.
|
|
543
|
+
|
|
544
|
+
**A draft is mutable, so the grant binds its bytes.** `approval payload
|
|
545
|
+
agentmail-draft` snapshots the draft's recipients, subject and text at request
|
|
546
|
+
time, and that snapshot is what the payload hash binds and what your phone
|
|
547
|
+
displays. Before it sends, the adapter re-fetches the draft and compares; a
|
|
548
|
+
draft edited after the grant refuses `agentmail-draft-drifted`, sends nothing,
|
|
549
|
+
and names which fields differ without quoting text nobody approved. That
|
|
550
|
+
comparison runs before the token is spent, so the refusal costs no authority:
|
|
551
|
+
restore the approved text and the same token still sends. Approving a draft id
|
|
552
|
+
alone would be approving whatever the agent wrote into it last.
|
|
553
|
+
|
|
554
|
+
## The APPROVAL.md dictionary
|
|
555
|
+
|
|
556
|
+
Every key that can appear in the policy block. The schema is closed at every
|
|
557
|
+
level: an unrecognised key fails validation, which fails the policy closed to
|
|
558
|
+
all-manual, because a key the runtime did not understand is a rule its author
|
|
559
|
+
believed was in force. Full semantics: SPEC.md section 5.
|
|
560
|
+
|
|
561
|
+
| key | what it says |
|
|
562
|
+
| --- | --- |
|
|
563
|
+
| `version` | Policy format version, quoted (`"0.1"`). The only required key (§5.1). |
|
|
564
|
+
| `defaults.autonomy` | Autonomy for an action matching no class rule. Five of the six levels are admitted: `supervised-live` is not, since it needs a `live_rate` that `defaults` has nowhere to hold. `human-only` is, and reserves every unnamed class to human hands. No default of its own, and `manual` is the fail-closed choice (§5.2, APRV-185). |
|
|
565
|
+
| `defaults.channel` | Channel name requests surface on by default; expected to name a key of `channels`, which is a runtime cross-check rather than a schema one. No default (§5.1, §10.3). |
|
|
566
|
+
| `defaults.approval_ttl` | How long a pending request stays actionable. Duration string, `24h`. No default; the scaffolded policy writes one (§5.1). |
|
|
567
|
+
| `defaults.token_delivery` | How a minted token reaches the process that will spend it. `manual` (the default, and what an absent key means): printed once on the granting surface and carried by a human. `sealed`: sealed to a per-request X25519 key so `approval wait` can hand it back, which addresses the token and never authorizes it (§10.4, APRV-105). |
|
|
568
|
+
| `defaults.on_expiry` | What happens when the TTL lapses. `reject` is the only value, and absent means `reject` (§5.1). |
|
|
569
|
+
| `payload_retention` | How long payload bytes are kept after their action is terminal. Absent means nothing is ever pruned (§5.2). |
|
|
570
|
+
| `protected_paths` | Repo-relative files and directory prefixes whose edit is classified `policy.edit`. A bare string is the whole entry. Additive only, no globs, and absent means the built-in protected set alone (§5.2, APRV-107). |
|
|
571
|
+
| `protected_paths[].path` | The path half of the object form: the same grammar as the bare string, an exact file (`SPEC.md`) or a directory prefix (`design/`) (§5.2, APRV-266). |
|
|
572
|
+
| `protected_paths[].class` | The class half: one lowercase segment under `policy.edit`. Four reserved names, `policy.edit.spec`, `policy.edit.harness`, `policy.edit.ci` and `policy.edit.design`, plus any word an author mints beside them. Nothing outside `policy.edit` may be named, and a route below the `policy.edit` line is refused `protected-route-floor`. No default: an entry that wants a sub-class states it (§5.2, APRV-266). |
|
|
573
|
+
| `approvers.<name>.channels` | The channels one approver can decide on. At least one: an approver reachable nowhere can never grant. No default (§5.1). |
|
|
574
|
+
| `classes.<pattern>.autonomy` | Required on every class rule, so it has no default. Six levels, strictest first: `human-only`, `manual`, `supervised-live`, `supervised-retro`, `autonomous`, and `supervised`, which is the pre-split spelling and an alias of `supervised-retro` (§5.2, APRV-127, APRV-185). |
|
|
575
|
+
| `classes.<pattern>.live_rate` | The fraction of a `supervised-live` class that blocks on the gate, in (0, 1]. Required there and refused everywhere else, so it has no default: a live mode with no fraction declares a control without saying how much of it runs. Selection is HMAC-SHA-256 over the payload hash under the operator's secret (§5.2, APRV-127). |
|
|
576
|
+
| `classes.<pattern>.retro_rate` | This class's retrospective sampling rate, in (0, 1], overriding `audit.supervised_sample_rate` for it alone. Optional on `supervised`, `supervised-retro` and `supervised-live`, refused on the rest. Absent means the global rate (§5.2, APRV-183). |
|
|
577
|
+
| `classes.<pattern>.approvers` | Approver ids permitted to decide this class. Absent restricts nobody, since the list is a narrowing and a narrowing nobody wrote narrows nothing; a named list refuses everyone else with `actor-not-approver` (§5.1). |
|
|
578
|
+
| `classes.<pattern>.limits` | Per-class ceilings, every value a positive number: `per_action_usd`, `daily_usd`, and the request-volume counts `max_pending` and `requests_per_hour`. Absent means this class carries no ceiling of its own (§5.1, §5.2). |
|
|
579
|
+
| `budgets.global.daily_usd` | Repo-wide spend ceiling per rolling day, computed from the log. Absent means no spend ceiling (§5.1). |
|
|
580
|
+
| `budgets.global.daily_actions` | Repo-wide count of side-effecting actions per rolling day. Absent means no count ceiling (§5.1). |
|
|
581
|
+
| `budgets.global.max_pending` | Simultaneously pending requests across the scope; excess is refused `queue-full`. Absent means no ceiling (§5.2). |
|
|
582
|
+
| `budgets.<scope>` | Any other named scope, same three keys. Budgets are conjunctive with class limits (§5.2). |
|
|
583
|
+
| `audit.supervised_sample_rate` | The FALLBACK fraction of supervised actions escalated for retrospective review, in [0, 1], for classes declaring no `retro_rate`. Absent means no fallback rate is configured (§5.2, APRV-183). |
|
|
584
|
+
| `audit.sampling_secret_env` | Name of the variable holding the operator's HMAC sampling secret. Unnamed means sampling is off and says so (§5.2, §11). |
|
|
585
|
+
| `audit.skew_tolerance` | How far a gate-typed event's timestamp may step back before verification reports an anomaly. Report-only; default 2 seconds (§8). |
|
|
586
|
+
| `audit.checkpoint_keys` | Public halves of the Ed25519 keys permitted to sign a `log.checkpoint`, base64 DER SPKI. The private halves live in the vault and never in this file. A list, so a retired key stays listed: a checkpoint signed by a key the list does not carry is refused. Absent, empty or unreadable means verification skips the checkpoint check with a reason and never reports it as a pass (§9, APRV-220). |
|
|
587
|
+
| `audit.checkpoint_every` | How long the log may go without a human-signed checkpoint before verification says one is due, and before the listener puts one `CHECKPOINT DUE` prompt on the approver's channel (`approval setup checkpoint` mints the key). Report-only at every layer: a due checkpoint is a warning and never a refusal. Absent means the cadence is off and nothing is ever reported as due (§9, APRV-220, APRV-257). |
|
|
588
|
+
| `daemon.read_proof` | Which prefix proof a long-lived reader runs before reusing a cached prefix: `full` (the default, re-hash the whole prefix on every read) or `incremental` (hash only the appended bytes, re-proving in full on a cadence). One-shot processes, the Claude Code hook and `approval log verify` prove in full regardless (§5.2, APRV-217). |
|
|
589
|
+
| `daemon.full_reproof_every` | Reads one full re-proof may cover under `incremental`, the anchoring read included. Default 50 (§5.2). |
|
|
590
|
+
| `daemon.full_reproof_after` | Wall clock one full re-proof may cover under `incremental`. Duration string, default `60s` (§5.2). |
|
|
591
|
+
| `vault.passphrase_env` | Name of the variable holding the vault passphrase. Absent means `APPROVAL_VAULT_PASSPHRASE` (§5.2, §10.4). |
|
|
592
|
+
| `channels.telegram.token_env` | Name of the variable holding the bot token. Default `APPROVAL_TG_TOKEN` (§5.1). |
|
|
593
|
+
| `channels.telegram.chat_id_env` | Name of the variable holding the approver chat id. Default `APPROVAL_TG_CHAT` (§5.1). |
|
|
594
|
+
| `channels.telegram.delivery` | `paced` (the default) shows one summary line and the oldest pending request, then the next one after a decision, `/skip` or `/next`; `burst` sends every pending request the listener has not sent yet. Neither mode changes what is pending: that is re-derived from the verified log on every cycle (§10.3, APRV-216). |
|
|
595
|
+
| `channels.web.port` | TCP port for the local approval UI, bound on loopback only. No default in the schema; the scaffolded policy names `4680`, and 0 is excluded because the policy must name a port a human can navigate to (§5.1). |
|
|
596
|
+
| `channels.<name>.prompt.rows` | Order only, for `telegram`, `web` and `cli`: the rows named here render in this order ahead of the rest, which keep their default relative order behind them. Never a whitelist, so a field added later cannot be lost to a list written before it existed. Absent means the layout the channel ships (§5.2, §10.3, APRV-218). |
|
|
597
|
+
| `channels.<name>.prompt.always` | Rows this channel renders only when abnormal, or not at all, render on every prompt instead. The anomaly mark stays a statement about the value, so a forced-on row shouts only when the value is in fact the reason to look. Absent means the channel's own visibility rules (§5.2, §10.3, APRV-218). |
|
|
598
|
+
| `channels.<name>.prompt.hide` | Rows this channel never renders. Refused for the rows required for a decision (`action_key`, `class`, `command_breakdown`, `protected_path`, `policy_diff`, `policy_load`) with `prompt-row-required`, and refused for a row `always` also names. Absent means nothing is hidden, and the canonical payload block is out of reach either way (§5.2, §10.3, APRV-218). |
|
|
599
|
+
| `channels.<other>` | An unknown channel name is accepted as an object, so a third-party transport does not fail the whole policy closed (§10.3). A `prompt` block written under such a name is still validated: a layout is checked wherever it appears. |
|
|
600
|
+
|
|
601
|
+
Every key ending in `_env` carries a variable's *name* and never its value:
|
|
602
|
+
agents may read `APPROVAL.md`, so a secret it carried would be a secret they
|
|
603
|
+
hold. Where those values live is recorded in `.approval/env`, which a single verb
|
|
604
|
+
reads, `approval env`, whose output is an export block a human evaluates.
|
|
605
|
+
|
|
606
|
+
## How this compares
|
|
607
|
+
|
|
608
|
+
Three kinds of thing already exist in this space, and each solves a different
|
|
609
|
+
part of the problem. (A hosted daemon and reviewer layer is operated by
|
|
610
|
+
Bountify.ai; it is optional, and nothing in the format depends on it. See
|
|
611
|
+
[GOVERNANCE.md](GOVERNANCE.md).)
|
|
612
|
+
|
|
613
|
+
**Harness-native permission prompts** (Claude Code permission rules and hooks,
|
|
614
|
+
Cursor auto-run, Codex CLI approval modes) enforce inside the one harness they
|
|
615
|
+
ship with. That enforcement is real: a Claude Code PreToolUse deny holds even
|
|
616
|
+
under its bypass mode, and Codex backs its gate with an OS-level sandbox, a
|
|
617
|
+
defense layer this project does not attempt. What they lack is a durable record
|
|
618
|
+
and portability. None writes an append-only log of what was asked, who decided,
|
|
619
|
+
and what ran; the decision reaches a human only as a synchronous terminal
|
|
620
|
+
prompt; and the mechanism does not travel to any other harness. approval.md's
|
|
621
|
+
own Claude Code hook is built on top of that PreToolUse mechanism and adds the
|
|
622
|
+
two missing pieces: the decision comes from an attested policy file rather than
|
|
623
|
+
the session, and it lands in a verifiable log.
|
|
624
|
+
|
|
625
|
+
**AGENTS.md permissions prose** states the policy in English and trusts the
|
|
626
|
+
agent to obey. Nothing parses it, nothing blocks a call against it, and no
|
|
627
|
+
record exists when it is violated. approval.md is the enforcement layer that
|
|
628
|
+
convention is missing, and treats it as an input: the permissions section of
|
|
629
|
+
this repository's own CLAUDE.md is the first import fixture.
|
|
630
|
+
|
|
631
|
+
**Framework interrupts** (LangGraph `interrupt()`, CrewAI human input, AutoGen
|
|
632
|
+
`UserProxyAgent`, the OpenAI Agents SDK's `needsApproval`, Temporal signal
|
|
633
|
+
approvals) give a developer a pause-and-resume primitive and leave policy,
|
|
634
|
+
audit format, the human channel, and the credential boundary entirely to them.
|
|
635
|
+
They also require adopting the framework. Temporal deserves its credit: its
|
|
636
|
+
event history is a genuine append-only execution record with crash recovery
|
|
637
|
+
this project does not claim, though it lives in Temporal's storage as a replay
|
|
638
|
+
log rather than as policy-attested files in your repo.
|
|
639
|
+
|
|
640
|
+
**Hosted approval platforms** (HumanLayer, gotoHuman, Permit.io's access
|
|
641
|
+
requests) are the closest relatives: multi-channel human routing, review UIs,
|
|
642
|
+
and in Permit.io's case a real authorization engine richer than autonomy
|
|
643
|
+
classes. Their model is a third-party service in the decision path, with the
|
|
644
|
+
audit trail in the platform's backend, and the agent's own process still
|
|
645
|
+
choosing to honor the returned verdict. They bring things a file convention
|
|
646
|
+
cannot: hosted infrastructure, escalation and team routing, compliance
|
|
647
|
+
certifications.
|
|
648
|
+
|
|
649
|
+
The differentiation is the combination rather than any single feature: policy
|
|
650
|
+
as a hash-attested markdown file in your repo; an append-only, hash-chained log
|
|
651
|
+
you can verify locally with one command; and an execution boundary where the
|
|
652
|
+
credential is inert until a single-use token is minted at the moment a human
|
|
653
|
+
decides. Every framework primitive and every hosted API above ultimately relies
|
|
654
|
+
on the agent's process honoring a returned decision. Here the thing the agent
|
|
655
|
+
needs (the credential) answers only to the thing it cannot make (the token).
|
|
656
|
+
The tradeoffs are equally plain: you run the daemon and listener yourself,
|
|
657
|
+
there is no OS-level sandbox, no compliance certification, and the reference
|
|
658
|
+
phone channel is one app, Telegram.
|
|
659
|
+
|
|
660
|
+
## Can't the agent just go around it?
|
|
661
|
+
|
|
662
|
+
**Edit the policy?** An attestation records the SHA-256 of the policy's bytes,
|
|
663
|
+
and every gated operation refuses `hash-mismatch` when the live file disagrees
|
|
664
|
+
with it. An unattested policy refuses too, and attesting is human-only. Under the
|
|
665
|
+
harness hook the edit itself is classified `policy.edit` before it happens,
|
|
666
|
+
because `APPROVAL.md` is in the built-in protected set no policy can narrow. A
|
|
667
|
+
`protected_paths` entry may route a path family to a `policy.edit` sub-class
|
|
668
|
+
(`policy.edit.spec`, `policy.edit.harness`, `policy.edit.ci`,
|
|
669
|
+
`policy.edit.design`) so each carries its own autonomy, and the routing floor
|
|
670
|
+
keeps a built-in path from landing anywhere looser than `policy.edit` itself.
|
|
671
|
+
|
|
672
|
+
**Fabricate or rewrite the log?** Each record chains to the previous one's hash,
|
|
673
|
+
so an edited or reordered record breaks the chain and `approval log verify` says
|
|
674
|
+
so. Appends go through compare-and-append against the head, and projections
|
|
675
|
+
(`QUEUE.md`, the SQLite index) rebuild from the log and never write back to it.
|
|
676
|
+
Tampering is made evident, which is what an audit trail is for.
|
|
677
|
+
|
|
678
|
+
**Mint its own token, or reuse one?** Tokens are minted at one site, inside the
|
|
679
|
+
path that records a human decision, and the log stores only the hash. No verb and
|
|
680
|
+
no tool returns a token for a grant it did not just record, and a hook grant
|
|
681
|
+
mints none at all. A token is single-use: the second spend is refused
|
|
682
|
+
`token-consumed`, naming the seq of the `execution.started` that spent it, and no
|
|
683
|
+
second record is appended.
|
|
684
|
+
|
|
685
|
+
**Call the adapter or the credential directly?** Credentials live in
|
|
686
|
+
`.approval/vault.enc` under a passphrase the policy names and never carries, and
|
|
687
|
+
an adapter opens the vault only inside a verified token window. There is no
|
|
688
|
+
`approval vault get`. An agent that never held a token never reaches a
|
|
689
|
+
credential, which is where the hard enforcement in this system lives.
|
|
690
|
+
|
|
691
|
+
**Run the side effect from its own shell?** That is what the PreToolUse hook is
|
|
692
|
+
for: it classifies the command text, resolves the class, and answers allow or
|
|
693
|
+
deny before the harness runs anything, fail-closed and with no "ask". The
|
|
694
|
+
classifier reads shell text without being a shell, so parameter expansion,
|
|
695
|
+
aliases and wrapper binaries are gaps, and each resolves toward denial or the
|
|
696
|
+
stricter class. `bash -c`, `eval`, `sudo`, `xargs` and `node -e` deny as
|
|
697
|
+
`hook-opaque`.
|
|
698
|
+
|
|
699
|
+
**POST straight at the web queue?** It can. `approval channel web` binds
|
|
700
|
+
`127.0.0.1` with the host hard-coded and no `--host` flag, because with no
|
|
701
|
+
authentication the loopback interface is the entire access control, and every
|
|
702
|
+
decision the page collects is recorded against the human the runtime was started
|
|
703
|
+
with: it proves "someone with access to this machine approved" and never "that
|
|
704
|
+
specific person approved". The page says so on itself. There is no CSRF token in
|
|
705
|
+
v0.1, deliberately: a CSRF token defends a session, and with no session and
|
|
706
|
+
nothing to authenticate against, anything that can open a socket to
|
|
707
|
+
`127.0.0.1:4680` can POST with or without one. The same-origin check that rejects
|
|
708
|
+
a POST naming a non-loopback `Origin` allows one carrying neither header, since
|
|
709
|
+
`curl` and older browsers send neither: treat it as a speed bump rather than a
|
|
710
|
+
control. A session, or a wider bind address, would need a real anti-CSRF token.
|
|
711
|
+
|
|
712
|
+
**Lie in its self-report?** Self-reported fields never reduce scrutiny (SPEC.md
|
|
713
|
+
section 11.1, invariant 4). They may raise it, inform display, or feed
|
|
714
|
+
retrospective analysis, and they can never downgrade a resolved autonomy level or
|
|
715
|
+
shortcut a refusal path. The approval prompt keeps them visually apart: a
|
|
716
|
+
COMPUTED block the runtime derived, and a CLAIMED block marked unverified.
|
|
717
|
+
|
|
718
|
+
**So what is left?** A fully adversarial agent on a machine it controls. Identity
|
|
719
|
+
in v0.1 is config-declared, so anyone who can set that variable and write to the
|
|
720
|
+
log is inside the trust boundary, and cryptographic identity is future work
|
|
721
|
+
rather than a v0.1 claim. What holds regardless of what the harness believes it
|
|
722
|
+
is running: the vault, the adapter boundary, and the single-use token. Keep
|
|
723
|
+
`manual` floors on irreversible classes, which SPEC.md section 7 does for you.
|
|
724
|
+
|
|
725
|
+
## Running the checks
|
|
726
|
+
|
|
727
|
+
```
|
|
728
|
+
npm run ci:local # run the CI tier this diff would get, before pushing
|
|
729
|
+
npm run check:changed # classify the working tree, then run that tier
|
|
730
|
+
npm run check:tier -- <path> # classify the given paths and print the tier
|
|
731
|
+
approval doctor # the other check: this machine, not the code
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
`approval doctor` prints **27 rows** and a tally, in the order their failures
|
|
735
|
+
cascade: build freshness, identity, attestation, the log chain, the channels
|
|
736
|
+
(`telegram`, `web-port`), the payload store, audit sampling, envelope integrity,
|
|
737
|
+
the vault, the environment source map, then the rows that ask git and the harness
|
|
738
|
+
what happened (`log-drift`, `reconciliation`, `harness-hook-outcomes`,
|
|
739
|
+
`harness-hook-wiring`, `keychain-scope`, `log-advance-cadence`, `dark-sessions`,
|
|
740
|
+
`verified-snapshot`, `read-proof`, `main-behind-origin`,
|
|
741
|
+
`harness-version-unverified`, `live-draw`, `values-block`, `checkpoint`,
|
|
742
|
+
`gate-organs`, `sealed-keys`).
|
|
743
|
+
|
|
744
|
+
**17 of the 27 report `not applicable` in a fresh directory**, and each names the
|
|
745
|
+
absence it skipped on rather than passing quietly: `telegram` (no bot variables),
|
|
746
|
+
`envelope-integrity` (no task folder), `vault` (no vault file), `environment` (no
|
|
747
|
+
`.approval/env`), `read-proof` (no `daemon` block), `live-draw` (no
|
|
748
|
+
`supervised-live` class), `checkpoint` (no `audit.checkpoint_keys`),
|
|
749
|
+
`harness-hook-outcomes`, `harness-hook-wiring`, `harness-version-unverified` and
|
|
750
|
+
`gate-organs` (no harness settings file), `verified-snapshot` (no daemon has
|
|
751
|
+
run), and `log-drift`, `log-advance-cadence`, `dark-sessions`,
|
|
752
|
+
`main-behind-origin` and `sealed-keys` (not a git checkout). `sealed-keys` is
|
|
753
|
+
the one that asks git what it TRACKS: `.approval/payloads/` is tracked on
|
|
754
|
+
purpose, so `.approval/` is a directory people `git add` from, and a
|
|
755
|
+
sealed-delivery private key swept in by one of those adds opens that action's
|
|
756
|
+
token for everyone holding the log. `gate-organs` is informational
|
|
757
|
+
wherever it lands: it lists the harness files whose current bytes carry no
|
|
758
|
+
`approval policy attest --organ` record, and it never moves the exit code, since
|
|
759
|
+
the enforcement for one of those is the protected-path guard in CI. Doctor
|
|
760
|
+
appends nothing, sends nothing and repairs nothing, and no credential value
|
|
761
|
+
appears in its output.
|
|
762
|
+
|
|
763
|
+
Checks come in three tiers.
|
|
764
|
+
|
|
765
|
+
| Tier | Chosen when every changed path is | What runs |
|
|
766
|
+
| --- | --- | --- |
|
|
767
|
+
| light | `README.md`, `docs/**/*.md`, `examples/**/*.md` | the documentation guard (`tests/docs-guard.test.ts`) |
|
|
768
|
+
| records | `backlog/**`, `MILESTONES.md` | the tests that read records (`milestones-guard`, `backlog-fixtures`, `docs-guard`), on Node 20 |
|
|
769
|
+
| full | anything else, or a mix of the above | the whole suite in three shards plus `npm run lint`, on Node 22; the Node 20 floor runs the same three shards on the merge queue and on pushes to `main` |
|
|
770
|
+
|
|
771
|
+
A denylist forces the full tier regardless of file extension: `APPROVAL.md`,
|
|
772
|
+
`CLAUDE.md`, `.claude/**`, `SPEC.md`, `schema/**`, `**/fixtures/**`,
|
|
773
|
+
`backlog/**`, `scripts/**`, `.github/**`, the packaging files, and `cli.js`.
|
|
774
|
+
|
|
775
|
+
`backlog/**` sits on both that denylist and the records list, which is what
|
|
776
|
+
makes the records tier all-or-nothing: a task file mixed with any other path
|
|
777
|
+
takes the full tier. Task files are markdown by extension and behavior by
|
|
778
|
+
effect, since their acceptance criteria are instructions to future agents. That
|
|
779
|
+
earns them every check which can observe a task file, and the records tier is
|
|
780
|
+
exactly those; it does not earn them a matrix of ~1800 tests on two Node
|
|
781
|
+
majors, none of which reads one. `MILESTONES.md` rides along because the
|
|
782
|
+
milestones guard checks the two against each other.
|
|
783
|
+
|
|
784
|
+
Classification is computed from the changed paths by
|
|
785
|
+
`scripts/classify-tier.mjs`, never asserted by the author of the change. Every
|
|
786
|
+
merge to `main` runs the full suite unconditionally, and anything ambiguous, an
|
|
787
|
+
empty path set included, resolves to full.
|
|
788
|
+
|
|
789
|
+
### Before the push: `npm run ci:local`
|
|
790
|
+
|
|
791
|
+
The merge queue is serial, so every red run there costs a slot, a re-merge and
|
|
792
|
+
another wait. `npm run ci:local` (APRV-275) is where that red gets found
|
|
793
|
+
instead. It asks the same classifier the workflow's `classify` job asks, by
|
|
794
|
+
spawning the same command with the same arguments, and then runs the jobs
|
|
795
|
+
`.github/workflows/ci.yml` declares for the tier that comes back: the docs
|
|
796
|
+
guard for light, the record-reading tests for records, the three shards plus
|
|
797
|
+
lint for full, and the protected-path grant cross-check on every tier whenever
|
|
798
|
+
a merge base is computable. `--base <ref>` picks the base (default
|
|
799
|
+
`origin/main`, three-dot, as CI classifies), `--working-tree` and explicit paths
|
|
800
|
+
are the other two path sources, `--dry-run` prints the plan and runs nothing,
|
|
801
|
+
`--json` prints it as data, and `--parallel` runs the tier's jobs concurrently
|
|
802
|
+
the way the matrix does.
|
|
803
|
+
|
|
804
|
+
`npm run check:changed` predates it and answers a different question: it
|
|
805
|
+
classifies the working tree and runs the tier in its own shape, which for full
|
|
806
|
+
is `npm test`, `npm run lint` and `npm run typecheck`. Use it while working, and
|
|
807
|
+
`ci:local` before pushing, when the question is what the workflow will say.
|
|
808
|
+
|
|
809
|
+
What it cannot reproduce it says, rather than passing over. The Node 20 floor
|
|
810
|
+
legs need Node 20, and this host runs whatever it runs. CI's runner is
|
|
811
|
+
`ubuntu-latest`, so on any other platform the report names the suites whose
|
|
812
|
+
meaning differs here, the temp root's shape and the symlink cases among them. A
|
|
813
|
+
cross-check with no reachable merge base, or with the records branches
|
|
814
|
+
unfetched, is reported unresolved and kept out of the verdict. A red step exits
|
|
815
|
+
non-zero and names the files that failed. Nothing in CI consults any of this: a
|
|
816
|
+
green run locally is a prediction, and the workflow remains the verdict.
|
|
817
|
+
|
|
818
|
+
A full-tier CI job compiles once. It builds, then runs `node
|
|
819
|
+
scripts/run-tests.mjs` over what it built, because `npm test` and `npm run
|
|
820
|
+
typecheck` would each recompile the same tree and neither pass can fail where
|
|
821
|
+
the build passed. `npm test` keeps its build-then-run shape for anyone running
|
|
822
|
+
it by hand. `scripts/run-tests.mjs --shard <k>/<n>` takes shard `k` of the
|
|
823
|
+
sorted file list, where the file at position `i` belongs to shard `(i mod n) +
|
|
824
|
+
1`, so the shards of a matrix are a partition of the suite: every file in
|
|
825
|
+
exactly one shard, and the matrix covers all of them. An out-of-range index, an
|
|
826
|
+
empty shard, and `--shard` combined with `--only` are refused rather than run.
|
|
827
|
+
The Node 20 floor moved to the merge queue and to pushes to `main` because the
|
|
828
|
+
queue candidate is what stands between a change and the branch, and a pull
|
|
829
|
+
request now gets its verdict from the shards alone. The floor leg is sharded
|
|
830
|
+
three ways too, so it proves the same whole suite in roughly a third of the
|
|
831
|
+
wall clock it took as one run.
|
|
832
|
+
|
|
833
|
+
## Exit codes
|
|
834
|
+
|
|
835
|
+
An agent branches on the exit code before it ever reads stdout, so these numbers
|
|
836
|
+
are frozen. Adding one is a spec change; changing a meaning is breaking.
|
|
837
|
+
|
|
838
|
+
| Code | Meaning |
|
|
839
|
+
| --- | --- |
|
|
840
|
+
| 0 | success |
|
|
841
|
+
| 1 | integrity failure (corrupt log) |
|
|
842
|
+
| 2 | usage error |
|
|
843
|
+
| 3 | torn tail |
|
|
844
|
+
| 4 | I/O error |
|
|
845
|
+
| 5 | no valid execution token (approval run only) |
|
|
846
|
+
| 6 | timeout (approval wait only) |
|
|
847
|
+
|
|
848
|
+
Code 1 and code 4 are kept apart deliberately. "I could not read the file" and
|
|
849
|
+
"the file has been tampered with" are different facts about the world, and
|
|
850
|
+
conflating them either cries wolf over a permission bit or lets real tampering
|
|
851
|
+
read as a filesystem hiccup. Code 3, a torn tail, is the signature of a crashed
|
|
852
|
+
write rather than of tampering, and nothing is ever repaired automatically:
|
|
853
|
+
truncating a torn line is a human decision. A gate refusal is exit 1 and never 2,
|
|
854
|
+
since the command was well-formed and the answer is no, so branch on
|
|
855
|
+
`error.code` under `--json` rather than retrying with different flags.
|
|
856
|
+
|
|
857
|
+
## Where to look next
|
|
858
|
+
|
|
859
|
+
[SPEC.md](SPEC.md) is the source of truth for every design decision, and this
|
|
860
|
+
README defers to it wherever the two could be read differently.
|
|
861
|
+
[CLAUDE.md](CLAUDE.md) describes how this repository builds itself, including
|
|
862
|
+
where it starts running behind its own gate.
|
|
863
|
+
|
|
864
|
+
Every command carries its own instructions, so this README shows no verb
|
|
865
|
+
inventory. `approval --help` lists them grouped by what they are for. `approval
|
|
866
|
+
<command> --help` gives one command's flags, refusal codes, and JSON shape, and
|
|
867
|
+
`--help --long` appends that verb's reasoning from
|
|
868
|
+
[docs/cli-reference.md](docs/cli-reference.md). `approval instructions` is the
|
|
869
|
+
agent-facing guide, and `--schemas` prints the verb registry as JSON.
|
|
870
|
+
|
|
871
|
+
Every external adapter, harness, updater or gateway this project has weighed
|
|
872
|
+
for integration has an entry in
|
|
873
|
+
[docs/integrations-considered.md](docs/integrations-considered.md): what it
|
|
874
|
+
exposes, how it fits, the verdict, and the next step, so the question is
|
|
875
|
+
answered once.
|
|
876
|
+
|
|
877
|
+
One of those entries has a runbook of its own.
|
|
878
|
+
[examples/grok-bot-connector/runbook.md](examples/grok-bot-connector/runbook.md)
|
|
879
|
+
puts a Grok Bot agent on the far end of `approval mcp serve --http --guest`,
|
|
880
|
+
behind a tunnel that is itself gated, and rehearses both halves of the story: the
|
|
881
|
+
agent asking for a branch push and an email and a human deciding them on a phone,
|
|
882
|
+
then the agent skipping the gate entirely. What holds when it does is the point.
|
|
883
|
+
Credentials answer only to single-use tokens, so the send it was never granted
|
|
884
|
+
stays impossible, and `approval coverage` reports every observed effect with its
|
|
885
|
+
evidence seq or `none`.
|
|
886
|
+
|
|
887
|
+
Designs that are proposed and not yet built live under `docs/proposals/`.
|
|
888
|
+
[docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)
|
|
889
|
+
is the longest of them: what a grant in this log can and cannot prove to a
|
|
890
|
+
service that does not trust the operator, and what an optional stronger tier
|
|
891
|
+
would have to be. Identity in v0.1 is config-declared, so the honest ceiling
|
|
892
|
+
today is "a party with write access to this log recorded a decision", and the
|
|
893
|
+
proposal works through device-bound keys, WebAuthn on a separately controlled
|
|
894
|
+
surface, per-decision signatures over the existing checkpoint machinery, and
|
|
895
|
+
third-party witnesses, with the phasing, the receipt format, and the negative
|
|
896
|
+
tests each would need. Nothing in it is implemented, and nothing in it amends
|
|
897
|
+
SPEC.md. Two shorter ones,
|
|
898
|
+
[docs/proposals/solo-dev-quickstart.md](docs/proposals/solo-dev-quickstart.md)
|
|
899
|
+
and [docs/proposals/no-daemon-mode.md](docs/proposals/no-daemon-mode.md),
|
|
900
|
+
design the path for one person gating their own app: a three-question setup,
|
|
901
|
+
one `guard` verb, and a runtime that lives inside the waiting command instead
|
|
902
|
+
of a daemon.
|
|
903
|
+
|
|
904
|
+
## License and governance
|
|
905
|
+
|
|
906
|
+
Code: Apache 2.0, see [LICENSE](LICENSE) and [NOTICE](NOTICE). Specification
|
|
907
|
+
and schemas: CC0 1.0, so any language can implement the format without asking.
|
|
908
|
+
Who holds the specification and the name, the relationship to Bountify.ai's
|
|
909
|
+
hosted offering, and the plan for neutral governance:
|
|
910
|
+
[GOVERNANCE.md](GOVERNANCE.md). How to contribute, including the DCO sign-off:
|
|
911
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|