approval-md 0.0.1 → 0.2.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 +940 -4
- package/SPEC.md +476 -34
- package/cli.js +29 -3
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +1200 -0
- package/dist/src/adapters/agentmail.js.map +1 -0
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/conformance.js +461 -0
- package/dist/src/adapters/conformance.js.map +1 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +1035 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/email.js +749 -0
- package/dist/src/adapters/email.js.map +1 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/env-passphrase.js +132 -0
- package/dist/src/adapters/env-passphrase.js.map +1 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +77 -0
- package/dist/src/adapters/registry.js.map +1 -0
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/smtp.js +499 -0
- package/dist/src/adapters/smtp.js.map +1 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +161 -0
- package/dist/src/adapters/vault-provider.js.map +1 -0
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/batch.js +121 -0
- package/dist/src/channels/batch.js.map +1 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/cli.js +468 -0
- package/dist/src/channels/cli.js.map +1 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/conformance.js +445 -0
- package/dist/src/channels/conformance.js.map +1 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/contract.js +494 -0
- package/dist/src/channels/contract.js.map +1 -0
- package/dist/src/channels/payload-view.d.ts +35 -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.d.ts +149 -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.d.ts +196 -0
- package/dist/src/channels/tagging.js +723 -0
- package/dist/src/channels/tagging.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/telegram.js +3190 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/channels/web.js +903 -0
- package/dist/src/channels/web.js.map +1 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +288 -0
- package/dist/src/cli/adapter.js.map +1 -0
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +2171 -0
- package/dist/src/cli/amend.js.map +1 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/args.js +86 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/attest.js +307 -0
- package/dist/src/cli/attest.js.map +1 -0
- package/dist/src/cli/audit-card.d.ts +62 -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.d.ts +59 -0
- package/dist/src/cli/audit.js +460 -0
- package/dist/src/cli/audit.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -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.d.ts +131 -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.d.ts +71 -0
- package/dist/src/cli/channel.js +438 -0
- package/dist/src/cli/channel.js.map +1 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/checkpoint-tap.js +238 -0
- package/dist/src/cli/checkpoint-tap.js.map +1 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/coverage.js +343 -0
- package/dist/src/cli/coverage.js.map +1 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +631 -0
- package/dist/src/cli/daemon.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +2762 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/env.js +302 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +1682 -0
- package/dist/src/cli/execute.js.map +1 -0
- package/dist/src/cli/exit-codes.d.ts +73 -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.d.ts +60 -0
- package/dist/src/cli/feedback.js +205 -0
- package/dist/src/cli/feedback.js.map +1 -0
- package/dist/src/cli/gate-window.d.ts +40 -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.d.ts +68 -0
- package/dist/src/cli/gate.js +557 -0
- package/dist/src/cli/gate.js.map +1 -0
- package/dist/src/cli/git-scope.d.ts +190 -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.d.ts +85 -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.d.ts +9 -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.d.ts +24 -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.d.ts +42 -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.d.ts +265 -0
- package/dist/src/cli/gloss.js +362 -0
- package/dist/src/cli/gloss.js.map +1 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +2339 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +2849 -0
- package/dist/src/cli/hook.js.map +1 -0
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +175 -0
- package/dist/src/cli/import.js.map +1 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +336 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/instructions.js +262 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/journal.js +238 -0
- package/dist/src/cli/journal.js.map +1 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +840 -0
- package/dist/src/cli/log-advance.js.map +1 -0
- package/dist/src/cli/log-anchor.d.ts +176 -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.d.ts +22 -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.d.ts +243 -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.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +360 -0
- package/dist/src/cli/log-verbs.js.map +1 -0
- package/dist/src/cli/long-help.d.ts +70 -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.d.ts +77 -0
- package/dist/src/cli/main.js +1206 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/mcp.js +306 -0
- package/dist/src/cli/mcp.js.map +1 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/paths.js +79 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/payload.js +253 -0
- package/dist/src/cli/payload.js.map +1 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/policy.js +229 -0
- package/dist/src/cli/policy.js.map +1 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +1175 -0
- package/dist/src/cli/preflight.js.map +1 -0
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/progress.js +112 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/prompt.js +312 -0
- package/dist/src/cli/prompt.js.map +1 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/records.js +66 -0
- package/dist/src/cli/records.js.map +1 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/render.js +132 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +150 -0
- package/dist/src/cli/sandbox.js.map +1 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +137 -0
- package/dist/src/cli/scaffold.js.map +1 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +509 -0
- package/dist/src/cli/setup-adapter.js.map +1 -0
- package/dist/src/cli/setup-channel.d.ts +117 -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.d.ts +57 -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.d.ts +275 -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.d.ts +287 -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.d.ts +96 -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.d.ts +202 -0
- package/dist/src/cli/setup.js +473 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/style.js +469 -0
- package/dist/src/cli/style.js.map +1 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/token.js +274 -0
- package/dist/src/cli/token.js.map +1 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +849 -0
- package/dist/src/cli/up.js.map +1 -0
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/usage.js +91 -0
- package/dist/src/cli/usage.js.map +1 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +189 -0
- package/dist/src/cli/values.js.map +1 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +362 -0
- package/dist/src/cli/vault.js.map +1 -0
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +2341 -0
- package/dist/src/cli/verb-registry.js.map +1 -0
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +52 -0
- package/dist/src/cli/wordmark.js.map +1 -0
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -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.d.ts +276 -0
- package/dist/src/core/agents-md.js +747 -0
- package/dist/src/core/agents-md.js.map +1 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +589 -0
- package/dist/src/core/attest.js.map +1 -0
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/audit.js +882 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/budgets.js +449 -0
- package/dist/src/core/budgets.js.map +1 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/checkpoint.js +738 -0
- package/dist/src/core/checkpoint.js.map +1 -0
- package/dist/src/core/child-env.d.ts +88 -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.d.ts +52 -0
- package/dist/src/core/clock.js +43 -0
- package/dist/src/core/clock.js.map +1 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +2356 -0
- package/dist/src/core/command-class.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -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.d.ts +48 -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.d.ts +101 -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.d.ts +217 -0
- package/dist/src/core/coverage.js +337 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/credential-spec.d.ts +72 -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.d.ts +331 -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.d.ts +185 -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.d.ts +450 -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.d.ts +858 -0
- package/dist/src/core/execute.js +1271 -0
- package/dist/src/core/execute.js.map +1 -0
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/frontmatter.js +100 -0
- package/dist/src/core/frontmatter.js.map +1 -0
- package/dist/src/core/gate-window.d.ts +312 -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.d.ts +1364 -0
- package/dist/src/core/gate.js +3002 -0
- package/dist/src/core/gate.js.map +1 -0
- package/dist/src/core/git-run.d.ts +73 -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.d.ts +157 -0
- package/dist/src/core/harness-version.js +211 -0
- package/dist/src/core/harness-version.js.map +1 -0
- package/dist/src/core/harness-wait.d.ts +55 -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.d.ts +107 -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.d.ts +253 -0
- package/dist/src/core/instance.js +319 -0
- package/dist/src/core/instance.js.map +1 -0
- package/dist/src/core/intake-limits.d.ts +247 -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.d.ts +52 -0
- package/dist/src/core/jcs.js +132 -0
- package/dist/src/core/jcs.js.map +1 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/journal.js +200 -0
- package/dist/src/core/journal.js.map +1 -0
- package/dist/src/core/live-draw.d.ts +436 -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.d.ts +89 -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-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/log.js +546 -0
- package/dist/src/core/log.js.map +1 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +487 -0
- package/dist/src/core/loop.js.map +1 -0
- package/dist/src/core/md-fence.d.ts +41 -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.d.ts +147 -0
- package/dist/src/core/money.js +195 -0
- package/dist/src/core/money.js.map +1 -0
- package/dist/src/core/payload-census.d.ts +74 -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.d.ts +175 -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.d.ts +71 -0
- package/dist/src/core/payload.js +80 -0
- package/dist/src/core/payload.js.map +1 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +588 -0
- package/dist/src/core/policy-diff.js.map +1 -0
- package/dist/src/core/policy-expectations.d.ts +199 -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.d.ts +150 -0
- package/dist/src/core/policy-explain.js +258 -0
- package/dist/src/core/policy-explain.js.map +1 -0
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +536 -0
- package/dist/src/core/policy-load.js.map +1 -0
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +478 -0
- package/dist/src/core/policy-match.js.map +1 -0
- package/dist/src/core/policy-proposal.d.ts +265 -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.d.ts +221 -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.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +1566 -0
- package/dist/src/core/protected-path-guard.js.map +1 -0
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/registration.js +39 -0
- package/dist/src/core/registration.js.map +1 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/reindex.js +336 -0
- package/dist/src/core/reindex.js.map +1 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sampler.js +388 -0
- package/dist/src/core/sampler.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/sandbox.js +424 -0
- package/dist/src/core/sandbox.js.map +1 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/seal.js +290 -0
- package/dist/src/core/seal.js.map +1 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/state.js +1009 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/task-file.d.ts +185 -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.d.ts +93 -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.d.ts +409 -0
- package/dist/src/core/token.js +561 -0
- package/dist/src/core/token.js.map +1 -0
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/values.js +153 -0
- package/dist/src/core/values.js.map +1 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/vault.js +612 -0
- package/dist/src/core/vault.js.map +1 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -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.d.ts +336 -0
- package/dist/src/core/verify.js +549 -0
- package/dist/src/core/verify.js.map +1 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/version.js +9 -0
- package/dist/src/core/version.js.map +1 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/core/wysiwys.js +728 -0
- package/dist/src/core/wysiwys.js.map +1 -0
- package/dist/src/daemon/advance-child.d.ts +39 -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.d.ts +466 -0
- package/dist/src/daemon/advance.js +849 -0
- package/dist/src/daemon/advance.js.map +1 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/audit.js +90 -0
- package/dist/src/daemon/audit.js.map +1 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +1988 -0
- package/dist/src/daemon/daemon.js.map +1 -0
- package/dist/src/daemon/dark-session.d.ts +64 -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.d.ts +36 -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.d.ts +154 -0
- package/dist/src/daemon/draw.js +458 -0
- package/dist/src/daemon/draw.js.map +1 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -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.d.ts +180 -0
- package/dist/src/daemon/projection.js +233 -0
- package/dist/src/daemon/projection.js.map +1 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/daemon/prune.js +376 -0
- package/dist/src/daemon/prune.js.map +1 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/http.js +343 -0
- package/dist/src/mcp/http.js.map +1 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +602 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +5716 -0
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +53 -4
- package/schema/.gitkeep +0 -0
- package/schema/LICENSE +117 -0
- package/schema/codex-instance.schema.json +82 -0
- package/schema/envelope.schema.json +137 -0
- package/schema/event.schema.json +1811 -0
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/fixtures/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 +501 -0
- package/schema/sample-record.schema.json +26 -0
- package/schema/values.schema.json +55 -0
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,1682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution verbs of SPEC.md §10.1: `approval run`, `approval wait`,
|
|
3
|
+
* `approval status`, and `approval queue`.
|
|
4
|
+
*
|
|
5
|
+
* As everywhere else in this CLI, **no logic lives here.** Authorization,
|
|
6
|
+
* budgets, loop safety, and the appends are `core/execute.ts`; state derivation
|
|
7
|
+
* is `core/gate.ts`; chain verification is `core/verify.ts`. This file splits
|
|
8
|
+
* argv, resolves paths and identity, spawns the child, chooses an exit code, and
|
|
9
|
+
* formats output.
|
|
10
|
+
*
|
|
11
|
+
* Four choices are load-bearing enough to state plainly.
|
|
12
|
+
*
|
|
13
|
+
* **`approval run` is transparent.** Its own exit code is the CHILD's exit code
|
|
14
|
+
* — a wrapper that swallowed it would break every `make`, every CI step, and
|
|
15
|
+
* every `&&` that ever wrapped a command. run's own refusals are the only thing
|
|
16
|
+
* that can produce a different code, and they all happen *before* the child is
|
|
17
|
+
* spawned: 5 when no valid token was presented, 2 for a usage error, 4 for a
|
|
18
|
+
* filesystem fact, 3 for a torn tail, 1 for any other gate refusal.
|
|
19
|
+
*
|
|
20
|
+
* **Exit 5 is new, and it is only here.** The human-settled design (2026-08-06)
|
|
21
|
+
* asked for a distinct code when `run` refuses for want of a token, and
|
|
22
|
+
* `exit-codes.ts` defines it as an addition to the frozen table rather than a
|
|
23
|
+
* redefinition of anything in it. `approval wait` likewise adds 6 for timeout.
|
|
24
|
+
* No other command emits either.
|
|
25
|
+
*
|
|
26
|
+
* **The child owns stdout.** `run` inherits stdio, so the child's output is not
|
|
27
|
+
* captured, buffered, or interleaved with ours. That leaves `--json` nowhere
|
|
28
|
+
* safe to print on stdout, so run's own JSON summary goes to **stderr** — the
|
|
29
|
+
* one place we can write without corrupting a stream the child is entitled to.
|
|
30
|
+
* This is stated in `--help` because it is the single place this CLI departs
|
|
31
|
+
* from "one JSON object on stdout".
|
|
32
|
+
*
|
|
33
|
+
* **`run` and the adapter contract are two callers of one core path.** A command
|
|
34
|
+
* is an adapter whose `act` is a spawn, whose payload is SPEC.md §6.2's `{argv,
|
|
35
|
+
* cwd}`, and whose credentials are the ambient environment.
|
|
36
|
+
* `src/adapters/contract.ts` wraps the same `startExecution` / `finishExecution`
|
|
37
|
+
* pair for adapters that are objects rather than processes; `run` calls the core
|
|
38
|
+
* verbs directly because its stdio inheritance, exit-code transparency, and `--`
|
|
39
|
+
* argv split are CLI concerns with nothing to do with adapters. What both must
|
|
40
|
+
* obey belongs in `core/execute.ts`, where both already read it; a rule added to
|
|
41
|
+
* the adapter contract alone protects adapters and not this verb.
|
|
42
|
+
*
|
|
43
|
+
* **`status` and `queue` answer different questions.** `queue` is the pending
|
|
44
|
+
* decision inbox and nothing else: requests awaiting a human, inside their TTL.
|
|
45
|
+
* `status` is system health: attestation, dangling executions, budget headroom,
|
|
46
|
+
* the latest verification, loop escalations. A dangling execution appears in
|
|
47
|
+
* `status` and never in `queue` — nobody is being asked to decide it, and
|
|
48
|
+
* putting operational debris in a human's approval inbox is how inboxes get
|
|
49
|
+
* ignored.
|
|
50
|
+
*/
|
|
51
|
+
import { spawnSync } from "node:child_process";
|
|
52
|
+
import { readdirSync, rmSync } from "node:fs";
|
|
53
|
+
import { constants as osConstants } from "node:os";
|
|
54
|
+
import { dirname, isAbsolute, resolve as resolvePathSegments } from "node:path";
|
|
55
|
+
import { HUMAN_ACTOR_ENV, checkAttestation, resolveHumanActor } from "../core/attest.js";
|
|
56
|
+
import { RESOLVE_DANGLING_COMMAND, proveDanglingAdvances, } from "../core/advance-cycle.js";
|
|
57
|
+
import { openObligations } from "../core/audit.js";
|
|
58
|
+
import { evaluateBudgets } from "../core/budgets.js";
|
|
59
|
+
import { declaredCredentialsForClass } from "../adapters/registry.js";
|
|
60
|
+
import { childEnvironment } from "../core/child-env.js";
|
|
61
|
+
import { coverageReport } from "../core/coverage.js";
|
|
62
|
+
import { credentialPathsFor, detectSandbox, resolveExecutable, sandboxPosture, sandboxRequired, wrapForSandbox, } from "../core/sandbox.js";
|
|
63
|
+
import { DEFAULT_TRUNK_REF, defaultRange, observeGit, } from "../core/coverage-sources/git.js";
|
|
64
|
+
import { danglingExecutions, findDeclaration, finishExecution, indexDeclarations, indeterminateExecutions, isReconcileResolution, loopEscalation, reconcileExecution, resolveExecution, startExecution, } from "../core/execute.js";
|
|
65
|
+
import { harnessLoopEscalation, harnessOutcomeCoverage, loopClearance } from "../core/loop.js";
|
|
66
|
+
import { isPayloadHash, runPayloadHash } from "../core/payload.js";
|
|
67
|
+
import { payloadStoreCensus } from "../core/payload-census.js";
|
|
68
|
+
import { payloadStoreDirFor } from "../core/payload-store.js";
|
|
69
|
+
import { withdraw } from "../core/gate.js";
|
|
70
|
+
import { openGateWindow } from "../core/gate-window.js";
|
|
71
|
+
import { keyStoreDirFor } from "../core/seal.js";
|
|
72
|
+
import { readVerifiedRecords, requestState } from "../core/state.js";
|
|
73
|
+
import { deliveredToken } from "../core/token.js";
|
|
74
|
+
import { passphraseEnvFor } from "../core/vault.js";
|
|
75
|
+
import { loadPolicy, parseDuration, POLICY_FILENAMES } from "../core/policy-load.js";
|
|
76
|
+
import { verify } from "../core/verify.js";
|
|
77
|
+
import { boolFlag, parseFlags, stringFlag } from "./args.js";
|
|
78
|
+
import { EXIT_INTEGRITY, EXIT_IO, EXIT_NO_TOKEN, EXIT_OK, EXIT_TIMEOUT, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
|
|
79
|
+
import { repoRoot } from "./git-scope.js";
|
|
80
|
+
import { publishedState } from "./log-advance.js";
|
|
81
|
+
import { confirmUntil, createPrompter } from "./prompt.js";
|
|
82
|
+
import { EXECUTION_HELP, QUEUE_HELP, RECONCILE_HELP, RESOLVE_HELP, RUN_HELP, STATUS_HELP, WAIT_HELP, } from "./help.js";
|
|
83
|
+
import { DEFAULT_LOG_PATH, preflightLog, resolvePath } from "./paths.js";
|
|
84
|
+
import { refusal as renderRefusal, relPath, style, table, } from "./style.js";
|
|
85
|
+
import { usageErrorText } from "./usage.js";
|
|
86
|
+
/** Identity accepted by `run`: a person or an agent, never the runtime. */
|
|
87
|
+
const PRINCIPAL_ACTOR = /^(human|agent):.+/u;
|
|
88
|
+
/** Poll interval for `approval wait`, in milliseconds. */
|
|
89
|
+
const DEFAULT_WAIT_INTERVAL_MS = 500;
|
|
90
|
+
/** Exit code recorded when the command itself could not be spawned. */
|
|
91
|
+
const EXIT_COMMAND_NOT_RUN = 127;
|
|
92
|
+
const COMMON_FLAGS = {
|
|
93
|
+
"--log": "string",
|
|
94
|
+
"--json": "boolean",
|
|
95
|
+
"--help": "boolean",
|
|
96
|
+
"-h": "boolean",
|
|
97
|
+
};
|
|
98
|
+
const POLICY_FLAGS = {
|
|
99
|
+
"--policy": "string",
|
|
100
|
+
"--dir": "string",
|
|
101
|
+
};
|
|
102
|
+
function absolute(value, cwd) {
|
|
103
|
+
return isAbsolute(value) ? value : resolvePathSegments(cwd, value);
|
|
104
|
+
}
|
|
105
|
+
function usageError(streams, json, message, helpText) {
|
|
106
|
+
if (json)
|
|
107
|
+
streams.err(`${JSON.stringify({ error: { code: "usage", message } })}\n`);
|
|
108
|
+
else
|
|
109
|
+
streams.err(usageErrorText(message, helpText));
|
|
110
|
+
return EXIT_USAGE;
|
|
111
|
+
}
|
|
112
|
+
function ioError(streams, json, message) {
|
|
113
|
+
if (json)
|
|
114
|
+
streams.err(`${JSON.stringify({ error: { code: "io", message } })}\n`);
|
|
115
|
+
else
|
|
116
|
+
streams.err(`approval: ${message}\n`);
|
|
117
|
+
return EXIT_IO;
|
|
118
|
+
}
|
|
119
|
+
function emitJson(streams, value) {
|
|
120
|
+
streams.out(`${JSON.stringify(value)}\n`);
|
|
121
|
+
}
|
|
122
|
+
/** The clock is read here, at the edge, and handed to core. */
|
|
123
|
+
function now() {
|
|
124
|
+
return new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Map an execution refusal onto the exit table.
|
|
128
|
+
*
|
|
129
|
+
* `token-required` is the one addition: exit 5, and only `run` can produce it.
|
|
130
|
+
* Everything else follows the split the gate and token verbs already draw —
|
|
131
|
+
* filesystem facts are 4, a crashed write is 3, and every decision the runtime
|
|
132
|
+
* itself made is 1.
|
|
133
|
+
*
|
|
134
|
+
* Exported because `approval adapter` (APRV-69) is the second caller of the core
|
|
135
|
+
* execution path and must map its refusals identically. Two copies of this
|
|
136
|
+
* switch would drift the first time a code was added, and an agent's retry logic
|
|
137
|
+
* keys on the difference between 5 and 1.
|
|
138
|
+
*/
|
|
139
|
+
export function executeRefusalExitCode(refusal) {
|
|
140
|
+
switch (refusal.code) {
|
|
141
|
+
case "token-required":
|
|
142
|
+
return EXIT_NO_TOKEN;
|
|
143
|
+
case "log-unreadable":
|
|
144
|
+
return EXIT_IO;
|
|
145
|
+
case "log-torn-tail":
|
|
146
|
+
return EXIT_TORN_TAIL;
|
|
147
|
+
case "append-failed":
|
|
148
|
+
switch (refusal.append?.code) {
|
|
149
|
+
case "corrupt-tail":
|
|
150
|
+
return EXIT_TORN_TAIL;
|
|
151
|
+
case "io":
|
|
152
|
+
case "lock-timeout":
|
|
153
|
+
return EXIT_IO;
|
|
154
|
+
default:
|
|
155
|
+
return EXIT_INTEGRITY;
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
return EXIT_INTEGRITY;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function emitRefusal(streams, json, refusal) {
|
|
162
|
+
if (json) {
|
|
163
|
+
const error = { code: refusal.code, message: refusal.message };
|
|
164
|
+
if (refusal.detail !== undefined)
|
|
165
|
+
error["detail"] = refusal.detail;
|
|
166
|
+
if (refusal.verdicts !== undefined)
|
|
167
|
+
error["verdicts"] = refusal.verdicts;
|
|
168
|
+
if (refusal.seq !== undefined)
|
|
169
|
+
error["seq"] = refusal.seq;
|
|
170
|
+
if (refusal.record !== undefined)
|
|
171
|
+
error["event_seq"] = refusal.record.seq;
|
|
172
|
+
streams.err(`${JSON.stringify({ ok: false, error })}\n`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// APRV-102: the one refusal shape — glyph, machine-readable code, message,
|
|
176
|
+
// and never a help page after it. No `fix:` line is invented: an execution
|
|
177
|
+
// refusal names a STATE (no token, already started, budget exhausted) and
|
|
178
|
+
// the repair depends on which, so a guessed command would be wrong more
|
|
179
|
+
// often than right. Argument and payload refusals, which DO have one
|
|
180
|
+
// command each, keep theirs.
|
|
181
|
+
streams.err(`${renderRefusal(style({ json }), refusal.code, refusal.message)}\n`);
|
|
182
|
+
}
|
|
183
|
+
return executeRefusalExitCode(refusal);
|
|
184
|
+
}
|
|
185
|
+
/** Where policy lives, from `--policy` / `--dir`, with the CLI's cwd default. */
|
|
186
|
+
function policyLocation(flags, cwd) {
|
|
187
|
+
const policyFlag = stringFlag(flags, "--policy");
|
|
188
|
+
const dirFlag = stringFlag(flags, "--dir");
|
|
189
|
+
if (policyFlag !== null)
|
|
190
|
+
return { file: absolute(policyFlag, cwd) };
|
|
191
|
+
return { dir: dirFlag === null ? cwd : absolute(dirFlag, cwd) };
|
|
192
|
+
}
|
|
193
|
+
function executeOptions(flags, cwd, token) {
|
|
194
|
+
return {
|
|
195
|
+
policy: policyLocation(flags, cwd),
|
|
196
|
+
...(token === null ? {} : { token }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The environment the granted child gets, and the count of what was withheld
|
|
201
|
+
* (APRV-205).
|
|
202
|
+
*
|
|
203
|
+
* Three inputs, none of them a flag. The policy names the passphrase variable
|
|
204
|
+
* (`vault.passphrase_env`); the credential-bearing prefixes and their allowlist
|
|
205
|
+
* come from the classifier's own list (APRV-194, exported for this); and the
|
|
206
|
+
* pass-through set is whatever adapter serves the DECLARED class of this action
|
|
207
|
+
* named in its `requiredCredentials` (APRV-169). The declaration is read from
|
|
208
|
+
* verified records — SPEC.md §11.1's first invariant, and the reason this is a
|
|
209
|
+
* second read of the log rather than a peek at the task file.
|
|
210
|
+
*
|
|
211
|
+
* A log this cannot read yields the empty pass-through set and a scrub that
|
|
212
|
+
* removes more, which is the fail-closed direction: `startExecution` is about to
|
|
213
|
+
* refuse the same read anyway, and if it somehow does not, the child is starved
|
|
214
|
+
* rather than fed.
|
|
215
|
+
*/
|
|
216
|
+
function childEnvFor(logPath, actionKey, flags, cwd) {
|
|
217
|
+
const location = policyLocation(flags, cwd);
|
|
218
|
+
const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
|
|
219
|
+
const read = readVerifiedRecords(logPath);
|
|
220
|
+
const declared = read.ok ? findDeclaration(read.records, actionKey) : null;
|
|
221
|
+
return childEnvironment({
|
|
222
|
+
passphraseEnv: passphraseEnvFor(load),
|
|
223
|
+
declaredCredentials: declared === null ? [] : declaredCredentialsForClass(declared.class),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The wrapped spawn for a child that must not reach the network (APRV-193), or
|
|
228
|
+
* `null` when there is nothing to wrap.
|
|
229
|
+
*
|
|
230
|
+
* The allowance is the runtime's, not the caller's: outbound network denied,
|
|
231
|
+
* loopback with it (the gate's IPC is a file, so there is no socket to except),
|
|
232
|
+
* and the credential material beside the log unreadable. No flag widens it.
|
|
233
|
+
* `--no-sandbox` is all or nothing, and it is recorded.
|
|
234
|
+
*/
|
|
235
|
+
function wrapExecutable(mechanism, command, args, env, logPath) {
|
|
236
|
+
const resolved = resolveExecutable(command, env);
|
|
237
|
+
if (resolved === null)
|
|
238
|
+
return null;
|
|
239
|
+
return wrapForSandbox(mechanism, resolved, args, {
|
|
240
|
+
loopback: false,
|
|
241
|
+
denyRead: credentialPathsFor(logPath),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/** `defaults.approval_ttl` in force, or `null` when the policy declares none. */
|
|
245
|
+
function ttlOf(flags, cwd) {
|
|
246
|
+
const location = policyLocation(flags, cwd);
|
|
247
|
+
const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
|
|
248
|
+
return load.ok ? load.durations.approvalTtlMs : null;
|
|
249
|
+
}
|
|
250
|
+
function front(argv, spec, helpText, streams, cwd) {
|
|
251
|
+
const json = argv.includes("--json");
|
|
252
|
+
const parsed = parseFlags(argv, spec);
|
|
253
|
+
if (!parsed.ok) {
|
|
254
|
+
return { kind: "handled", code: usageError(streams, json, parsed.message, helpText) };
|
|
255
|
+
}
|
|
256
|
+
if (boolFlag(parsed.flags, "--help") || boolFlag(parsed.flags, "-h")) {
|
|
257
|
+
streams.out(`${helpText}\n`);
|
|
258
|
+
return { kind: "handled", code: EXIT_OK };
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
kind: "run",
|
|
262
|
+
flags: parsed.flags,
|
|
263
|
+
positionals: parsed.positionals,
|
|
264
|
+
json,
|
|
265
|
+
logPath: resolvePath(stringFlag(parsed.flags, "--log"), DEFAULT_LOG_PATH, cwd),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ===========================================================================
|
|
269
|
+
// approval run
|
|
270
|
+
// ===========================================================================
|
|
271
|
+
/**
|
|
272
|
+
* The exit code a child's outcome reduces to.
|
|
273
|
+
*
|
|
274
|
+
* A child killed by a signal has no exit code, so the shell convention is used:
|
|
275
|
+
* `128 + signal number` (SIGKILL → 137, SIGTERM → 143). It is recorded in the
|
|
276
|
+
* `execution.failed` payload as that number, and `run` exits with it, so a
|
|
277
|
+
* killed execution reads identically from the log and from the shell.
|
|
278
|
+
*/
|
|
279
|
+
function childExitCode(status, signal) {
|
|
280
|
+
if (signal !== null) {
|
|
281
|
+
const numbers = osConstants.signals;
|
|
282
|
+
const number = numbers[signal];
|
|
283
|
+
return 128 + (number ?? 0);
|
|
284
|
+
}
|
|
285
|
+
return status ?? EXIT_COMMAND_NOT_RUN;
|
|
286
|
+
}
|
|
287
|
+
const INHERIT_CHILD_IO = { stdio: "inherit" };
|
|
288
|
+
export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
|
|
289
|
+
// `--` separates our flags from the child's argv, and the child's argv may
|
|
290
|
+
// legitimately contain anything at all — including flags this CLI knows. So
|
|
291
|
+
// the split happens on the RAW argv, before any parsing, and everything to the
|
|
292
|
+
// right of the first `--` is handed to the child untouched.
|
|
293
|
+
const separator = argv.indexOf("--");
|
|
294
|
+
const ours = separator === -1 ? argv : argv.slice(0, separator);
|
|
295
|
+
const childArgv = separator === -1 ? [] : argv.slice(separator + 1);
|
|
296
|
+
const outcome = front(ours, {
|
|
297
|
+
...COMMON_FLAGS,
|
|
298
|
+
...POLICY_FLAGS,
|
|
299
|
+
"--token": "string",
|
|
300
|
+
"--as": "string",
|
|
301
|
+
"--payload-hash": "string",
|
|
302
|
+
"--no-sandbox": "boolean",
|
|
303
|
+
}, RUN_HELP, streams, cwd);
|
|
304
|
+
if (outcome.kind === "handled")
|
|
305
|
+
return outcome.code;
|
|
306
|
+
const { flags, positionals, json, logPath } = outcome;
|
|
307
|
+
const actionKey = positionals[0];
|
|
308
|
+
if (actionKey === undefined) {
|
|
309
|
+
return usageError(streams, json, "missing <action-key> argument", RUN_HELP);
|
|
310
|
+
}
|
|
311
|
+
const extra = positionals[1];
|
|
312
|
+
if (extra !== undefined) {
|
|
313
|
+
return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}; the command to run goes after \`--\``, RUN_HELP);
|
|
314
|
+
}
|
|
315
|
+
const command = childArgv[0];
|
|
316
|
+
if (command === undefined) {
|
|
317
|
+
return usageError(streams, json, "missing command: `approval run <action-key> [--token <t>] -- <cmd…>`", RUN_HELP);
|
|
318
|
+
}
|
|
319
|
+
const asFlag = stringFlag(flags, "--as");
|
|
320
|
+
const actor = asFlag === null ? resolveHumanActor() : asFlag;
|
|
321
|
+
if (actor === null || !PRINCIPAL_ACTOR.test(actor)) {
|
|
322
|
+
if (asFlag !== null) {
|
|
323
|
+
return usageError(streams, json, `--as expects human:<id> or agent:<id>, got ${JSON.stringify(asFlag)}`, RUN_HELP);
|
|
324
|
+
}
|
|
325
|
+
return usageError(streams, json, `no identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id> | agent:<id>`, RUN_HELP);
|
|
326
|
+
}
|
|
327
|
+
// Content binding (amended SPEC.md §6.2, §10.4). §6.2 defines `approval run`'s
|
|
328
|
+
// payload as "the argv array and cwd", so run computes the hash itself from
|
|
329
|
+
// the command it is about to spawn — an executor that had to be *told* what it
|
|
330
|
+
// was running could be told wrong.
|
|
331
|
+
//
|
|
332
|
+
// APRV-140 (red-team F3) closes the door that used to be here. `--payload-hash`
|
|
333
|
+
// was an OVERRIDE: when it was present the computation was skipped entirely,
|
|
334
|
+
// so presenting the grant's own hash while spawning arbitrary argv spent the
|
|
335
|
+
// token and ran something nobody approved. Adapters whose real payload is
|
|
336
|
+
// something else (a message body, a proposed record) were the reason, but they
|
|
337
|
+
// do not need this door: `src/adapters/contract.ts` hashes the bytes and calls
|
|
338
|
+
// core directly, and `approval consume` spends a token for a payload this
|
|
339
|
+
// process is not spawning. Neither is a plain `approval run` invocation, which
|
|
340
|
+
// is what an agent has.
|
|
341
|
+
//
|
|
342
|
+
// So the flag survives as a CHECK, never a substitute: run always recomputes,
|
|
343
|
+
// and a supplied value must equal what will actually spawn. The refusal is
|
|
344
|
+
// `payload-mismatch` — the same code the manual path already emits for the
|
|
345
|
+
// same fact, because an agent's response to it is the same either way — and it
|
|
346
|
+
// happens before `startExecution`, so nothing is appended and no token moves.
|
|
347
|
+
const hashFlag = stringFlag(flags, "--payload-hash");
|
|
348
|
+
if (hashFlag !== null && !isPayloadHash(hashFlag)) {
|
|
349
|
+
return usageError(streams, json, `--payload-hash expects 64 lowercase hex characters (SHA-256 over the RFC 8785 canonical serialization of the payload), got ${JSON.stringify(hashFlag)}`, RUN_HELP);
|
|
350
|
+
}
|
|
351
|
+
const payloadHash = runPayloadHash(childArgv, cwd);
|
|
352
|
+
if (hashFlag !== null && hashFlag !== payloadHash) {
|
|
353
|
+
return emitRefusal(streams, json, {
|
|
354
|
+
ok: false,
|
|
355
|
+
code: "payload-mismatch",
|
|
356
|
+
message: `--payload-hash ${hashFlag} is not the hash of the command this would spawn: ${JSON.stringify(childArgv[0])} and ${childArgv.length - 1} argument(s) in ${cwd} hash to ${payloadHash}. \`approval run\` recomputes the binding from the argv and cwd it is about to spawn and never accepts a caller's substitute for it (amended SPEC.md §10.4, APRV-140); the flag states what you believe you are running, and this is a refusal to run something else. Nothing was appended.`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// APRV-205. The child's environment is built BEFORE `execution.started`,
|
|
360
|
+
// because the count of what was withheld is recorded on that event and a
|
|
361
|
+
// number written after the fact would be a number nobody measured. The child
|
|
362
|
+
// gets everything the session holds except the credential-bearing names: see
|
|
363
|
+
// `core/child-env.ts` for the three rules and for what this deliberately does
|
|
364
|
+
// NOT do (it is a scrub, not the sandbox APRV-193 designs).
|
|
365
|
+
const childEnv = childEnvFor(logPath, actionKey, flags, cwd);
|
|
366
|
+
// APRV-193. The room the child runs in, decided BEFORE `execution.started`
|
|
367
|
+
// for the same reason the count above is: the record says what happened, and
|
|
368
|
+
// a value written after the fact would be a value nobody measured.
|
|
369
|
+
//
|
|
370
|
+
// `granted` is the presence of a token, and it is the whole class test this
|
|
371
|
+
// verb needs. The manual path is a human's grant over these exact bytes, and
|
|
372
|
+
// `approval run` on a grant is the one door to the world the design leaves
|
|
373
|
+
// open (the registry for `deps.add`, the API host for `network.call`). Every
|
|
374
|
+
// other path — autonomous, and supervised-sampled — is one nobody was asked
|
|
375
|
+
// about, and that is where the child is starved. Reading the token rather
|
|
376
|
+
// than re-resolving the class keeps this decision on THIS side of the append,
|
|
377
|
+
// and it widens nothing an agent can reach on its own: a token that does not
|
|
378
|
+
// verify runs no command at all, so the loosening needs something a human
|
|
379
|
+
// minted (SPEC.md §11.1 invariant 4).
|
|
380
|
+
const posture = sandboxPosture({
|
|
381
|
+
optedOut: boolFlag(flags, "--no-sandbox"),
|
|
382
|
+
granted: stringFlag(flags, "--token") !== null,
|
|
383
|
+
detection: detectSandbox(),
|
|
384
|
+
...(sandboxRequired() ? { requireSupported: true } : {}),
|
|
385
|
+
});
|
|
386
|
+
if (posture.kind === "refuse") {
|
|
387
|
+
// Fail closed, and BEFORE anything is appended: a machine that cannot
|
|
388
|
+
// protect an execution costs no authority, so the same token still spends
|
|
389
|
+
// once the mechanism works. Not one of `EXECUTE_REFUSAL_CODES` — adding
|
|
390
|
+
// `sandbox-unavailable` to that union widens frozen public API (§11.1
|
|
391
|
+
// invariant 6), which is a human's decision and is drafted for sign-off in
|
|
392
|
+
// `docs/proposals/aprv-193-amendments.md`. Until then this is what it is:
|
|
393
|
+
// the executor declining to run something it cannot put in the room it
|
|
394
|
+
// promised, with the exit code that already means "the command did not run".
|
|
395
|
+
streams.err(`approval: the egress sandbox is unavailable (${posture.reason}); the command was NOT run and nothing was appended. Fix the sandbox, or take the recorded opt-out with \`--no-sandbox\` (docs/sandboxed-exec.md).\n`);
|
|
396
|
+
return EXIT_COMMAND_NOT_RUN;
|
|
397
|
+
}
|
|
398
|
+
// execution.started is appended HERE, before the child exists. A crash from
|
|
399
|
+
// this line until the finish below leaves a dangling execution, which
|
|
400
|
+
// `approval status` reports and nothing repairs on its own.
|
|
401
|
+
const started = startExecution(logPath, actionKey, {
|
|
402
|
+
...executeOptions(flags, cwd, stringFlag(flags, "--token")),
|
|
403
|
+
presentedPayloadHash: payloadHash,
|
|
404
|
+
envStripped: childEnv.stripped,
|
|
405
|
+
sandbox: posture.state,
|
|
406
|
+
}, actor);
|
|
407
|
+
if (!started.ok)
|
|
408
|
+
return emitRefusal(streams, json, started);
|
|
409
|
+
// APRV-193. The wrapper is built here and not inside `core/sandbox.ts`'s own
|
|
410
|
+
// spawn, because this verb's spawn is the one place the payload binding, the
|
|
411
|
+
// starved environment and the stdio contract already meet: a second spawn
|
|
412
|
+
// site would be a second place for them to drift.
|
|
413
|
+
//
|
|
414
|
+
// The command is resolved to an absolute path first. `sandbox-exec` execs
|
|
415
|
+
// through `execvp`, so the lookup would still happen — but a lookup that
|
|
416
|
+
// FAILS exits 71, and 71 recorded as the child's exit code is a lie about a
|
|
417
|
+
// command that never ran. A command that does not resolve is left unwrapped
|
|
418
|
+
// and fails as the ENOENT it is.
|
|
419
|
+
const wrapped = posture.kind === "apply"
|
|
420
|
+
? wrapExecutable(posture.mechanism, command, childArgv.slice(1), childEnv.env, logPath)
|
|
421
|
+
: null;
|
|
422
|
+
const child = spawnSync(wrapped?.command ?? command, wrapped?.args ?? childArgv.slice(1), {
|
|
423
|
+
cwd,
|
|
424
|
+
stdio: childIo.stdio,
|
|
425
|
+
encoding: "utf8",
|
|
426
|
+
env: childEnv.env,
|
|
427
|
+
});
|
|
428
|
+
if (wrapped !== null)
|
|
429
|
+
rmSync(wrapped.cleanup, { recursive: true, force: true });
|
|
430
|
+
if (childIo.onOutput !== undefined) {
|
|
431
|
+
childIo.onOutput({ stdout: child.stdout ?? "", stderr: child.stderr ?? "" });
|
|
432
|
+
}
|
|
433
|
+
const exitCode = child.error === undefined
|
|
434
|
+
? childExitCode(child.status, child.signal)
|
|
435
|
+
: EXIT_COMMAND_NOT_RUN;
|
|
436
|
+
if (child.error !== undefined) {
|
|
437
|
+
streams.err(`approval: the command could not be run (${child.error.message}); recording execution.failed with exit_code ${EXIT_COMMAND_NOT_RUN}\n`);
|
|
438
|
+
}
|
|
439
|
+
const finished = finishExecution(logPath, actionKey, exitCode, actor);
|
|
440
|
+
if (!finished.ok) {
|
|
441
|
+
const code = emitRefusal(streams, json, finished);
|
|
442
|
+
// The child's code is the more important fact when the child itself failed;
|
|
443
|
+
// when it succeeded, a failure to RECORD that success must not read as one.
|
|
444
|
+
return exitCode === 0 ? code : exitCode;
|
|
445
|
+
}
|
|
446
|
+
if (json) {
|
|
447
|
+
// stderr, not stdout: stdout belongs to the child. See the module header.
|
|
448
|
+
streams.err(`${JSON.stringify({
|
|
449
|
+
ok: true,
|
|
450
|
+
action_key: actionKey,
|
|
451
|
+
task: started.task,
|
|
452
|
+
class: started.class,
|
|
453
|
+
autonomy: started.autonomy,
|
|
454
|
+
started_seq: started.record.seq,
|
|
455
|
+
outcome: finished.event,
|
|
456
|
+
outcome_seq: finished.record.seq,
|
|
457
|
+
exit_code: exitCode,
|
|
458
|
+
payload_hash: payloadHash,
|
|
459
|
+
})}\n`);
|
|
460
|
+
}
|
|
461
|
+
return exitCode;
|
|
462
|
+
}
|
|
463
|
+
// ===========================================================================
|
|
464
|
+
// approval wait
|
|
465
|
+
// ===========================================================================
|
|
466
|
+
/** Synchronous sleep with no dependency and no busy-spin. */
|
|
467
|
+
function sleepSync(ms) {
|
|
468
|
+
if (ms <= 0)
|
|
469
|
+
return;
|
|
470
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* The role a decision state wears. Colour is redundant here: the word is
|
|
474
|
+
* printed beside the glyph, so a pipe (or a colour-blind reader) loses nothing.
|
|
475
|
+
*/
|
|
476
|
+
function stateRole(state) {
|
|
477
|
+
if (state === "granted")
|
|
478
|
+
return "ok";
|
|
479
|
+
if (state === "expired" || state === "requested" || state === "withdrawn")
|
|
480
|
+
return "warn";
|
|
481
|
+
return "fail";
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* `approval wait`'s human answer: the verdict, then one aligned row per action.
|
|
485
|
+
*
|
|
486
|
+
* Action keys are copyable, so the left column opts out of `key` styling
|
|
487
|
+
* (`plainLeft`) and the state beside it carries the colour instead.
|
|
488
|
+
*/
|
|
489
|
+
export function renderWaitHuman(task, status, actions, st = style()) {
|
|
490
|
+
const glyph = status === "granted" ? "ok" : status === "expired" || status === "withdrawn" ? "skip" : "fail";
|
|
491
|
+
const head = `${st.glyph(glyph)} ${st.key(task)} ${st.paint(stateRole(status), status)}`;
|
|
492
|
+
if (actions.length === 0)
|
|
493
|
+
return `${head}\n`;
|
|
494
|
+
const rows = actions.map((action) => ({
|
|
495
|
+
left: action.action_key,
|
|
496
|
+
right: st.paint(stateRole(action.state), action.state),
|
|
497
|
+
plainLeft: true,
|
|
498
|
+
}));
|
|
499
|
+
return `${head}\n${st.table(rows, { indent: 2 })}\n`;
|
|
500
|
+
}
|
|
501
|
+
/** Every action key of `task` that ever carried an `approval.requested`. */
|
|
502
|
+
function requestedKeysOf(records, task) {
|
|
503
|
+
const keys = [];
|
|
504
|
+
for (const record of records) {
|
|
505
|
+
if (record.event !== "approval.requested")
|
|
506
|
+
continue;
|
|
507
|
+
if (record.task !== task)
|
|
508
|
+
continue;
|
|
509
|
+
const key = record.action_key;
|
|
510
|
+
if (typeof key !== "string" || key.length === 0)
|
|
511
|
+
continue;
|
|
512
|
+
if (!keys.includes(key))
|
|
513
|
+
keys.push(key);
|
|
514
|
+
}
|
|
515
|
+
return keys;
|
|
516
|
+
}
|
|
517
|
+
export function commandWait(argv, streams, cwd) {
|
|
518
|
+
const outcome = front(argv, {
|
|
519
|
+
...COMMON_FLAGS,
|
|
520
|
+
...POLICY_FLAGS,
|
|
521
|
+
"--timeout": "string",
|
|
522
|
+
"--interval": "string",
|
|
523
|
+
"--withdraw-on-timeout": "boolean",
|
|
524
|
+
"--as": "string",
|
|
525
|
+
}, WAIT_HELP, streams, cwd);
|
|
526
|
+
if (outcome.kind === "handled")
|
|
527
|
+
return outcome.code;
|
|
528
|
+
const { flags, positionals, json, logPath } = outcome;
|
|
529
|
+
const task = positionals[0];
|
|
530
|
+
if (task === undefined)
|
|
531
|
+
return usageError(streams, json, "missing <task> argument", WAIT_HELP);
|
|
532
|
+
const extra = positionals[1];
|
|
533
|
+
if (extra !== undefined) {
|
|
534
|
+
return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, WAIT_HELP);
|
|
535
|
+
}
|
|
536
|
+
const timeoutText = stringFlag(flags, "--timeout");
|
|
537
|
+
if (timeoutText === null) {
|
|
538
|
+
return usageError(streams, json, "missing --timeout <duration>", WAIT_HELP);
|
|
539
|
+
}
|
|
540
|
+
const timeoutMs = parseDuration(timeoutText);
|
|
541
|
+
if (timeoutMs === null) {
|
|
542
|
+
return usageError(streams, json, `--timeout expects a duration like 30s, 10m, 6h, got ${JSON.stringify(timeoutText)}`, WAIT_HELP);
|
|
543
|
+
}
|
|
544
|
+
const intervalText = stringFlag(flags, "--interval");
|
|
545
|
+
const intervalMs = intervalText === null ? DEFAULT_WAIT_INTERVAL_MS : parseDuration(intervalText);
|
|
546
|
+
if (intervalMs === null) {
|
|
547
|
+
return usageError(streams, json, `--interval expects a duration like 500ms, 2s, got ${JSON.stringify(intervalText)}`, WAIT_HELP);
|
|
548
|
+
}
|
|
549
|
+
// APRV-106. OFF by default, and it is the only thing that makes `wait` a
|
|
550
|
+
// writer: a caller that merely stopped waiting has not necessarily stopped
|
|
551
|
+
// wanting an answer (a supervisor may wait again), so the retraction is
|
|
552
|
+
// opt-in. When it is on, the actor is required up front — resolving identity
|
|
553
|
+
// after a nine-minute wait, and failing then, would leave exactly the stale
|
|
554
|
+
// request the flag exists to prevent.
|
|
555
|
+
const withdrawOnTimeout = boolFlag(flags, "--withdraw-on-timeout");
|
|
556
|
+
const asFlag = stringFlag(flags, "--as");
|
|
557
|
+
const withdrawActor = asFlag ?? resolveHumanActor();
|
|
558
|
+
if (withdrawOnTimeout && (withdrawActor === null || !PRINCIPAL_ACTOR.test(withdrawActor))) {
|
|
559
|
+
return usageError(streams, json, `--withdraw-on-timeout needs an identity: pass --as human:<id> | agent:<id>, or set ${HUMAN_ACTOR_ENV}. Only the actor that opened a request may withdraw it, so there is no default.`, WAIT_HELP);
|
|
560
|
+
}
|
|
561
|
+
const ttlMs = ttlOf(flags, cwd);
|
|
562
|
+
const deadline = Date.now() + timeoutMs;
|
|
563
|
+
for (;;) {
|
|
564
|
+
const read = readVerifiedRecords(logPath);
|
|
565
|
+
if (!read.ok) {
|
|
566
|
+
return emitRefusal(streams, json, {
|
|
567
|
+
ok: false,
|
|
568
|
+
code: read.code === "log-torn-tail" ? "log-torn-tail" : "log-unreadable",
|
|
569
|
+
message: read.message,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const ts = now();
|
|
573
|
+
const actions = [];
|
|
574
|
+
let pending = false;
|
|
575
|
+
for (const key of requestedKeysOf(read.records, task)) {
|
|
576
|
+
const derivation = requestState(read.records, key, ts, ttlMs);
|
|
577
|
+
// APRV-105. The token, when this machine can open it: the grant sealed it
|
|
578
|
+
// to the ephemeral public key this action's request published, and the
|
|
579
|
+
// private half is in the key store beside the log. Attached only to a
|
|
580
|
+
// GRANTED action, and only in `--json` below — a `null` on every other
|
|
581
|
+
// state would be a field consumers have to ignore, and a token on a
|
|
582
|
+
// rejected action would be a value with nothing behind it.
|
|
583
|
+
const token = json && derivation.state === "granted"
|
|
584
|
+
? deliveredToken(read.records, key, keyStoreDirFor(logPath))
|
|
585
|
+
: null;
|
|
586
|
+
actions.push({
|
|
587
|
+
action_key: key,
|
|
588
|
+
state: derivation.state,
|
|
589
|
+
seq: derivation.decisionSeq ?? derivation.requestSeq,
|
|
590
|
+
...(token === null ? {} : { token }),
|
|
591
|
+
});
|
|
592
|
+
if (derivation.state === "requested")
|
|
593
|
+
pending = true;
|
|
594
|
+
}
|
|
595
|
+
if (!pending) {
|
|
596
|
+
// Precedence, documented in --help: a human's "no" outranks a lapse, and
|
|
597
|
+
// both outrank "everything was granted". A task with no requests at all
|
|
598
|
+
// has nothing to wait for and is granted vacuously.
|
|
599
|
+
//
|
|
600
|
+
// APRV-106 puts `withdrawn` between the two, and REUSES exit 1 rather
|
|
601
|
+
// than adding a code. The table in `cli/exit-codes.ts` is frozen public
|
|
602
|
+
// API — adding a number is a spec change, and agents already branch on
|
|
603
|
+
// these seven — while the fact an agent needs is the one exit 1 already
|
|
604
|
+
// carries: this action is NOT authorized and no retry of the same request
|
|
605
|
+
// will change that. The distinction lives where a distinction can be
|
|
606
|
+
// added without breaking anyone, in `status`, which is `"withdrawn"` in
|
|
607
|
+
// the JSON and printed beside the action in the human render.
|
|
608
|
+
const rejected = actions.some((action) => action.state === "rejected" || action.state === "revoked");
|
|
609
|
+
const withdrawn = actions.some((action) => action.state === "withdrawn");
|
|
610
|
+
const expired = actions.some((action) => action.state === "expired");
|
|
611
|
+
const status = rejected
|
|
612
|
+
? "rejected"
|
|
613
|
+
: withdrawn
|
|
614
|
+
? "withdrawn"
|
|
615
|
+
: expired
|
|
616
|
+
? "expired"
|
|
617
|
+
: "granted";
|
|
618
|
+
const code = rejected || withdrawn ? EXIT_INTEGRITY : expired ? EXIT_TORN_TAIL : EXIT_OK;
|
|
619
|
+
if (json)
|
|
620
|
+
emitJson(streams, { ok: true, task, status, actions });
|
|
621
|
+
else
|
|
622
|
+
streams.out(renderWaitHuman(task, status, actions, style({ json })));
|
|
623
|
+
return code;
|
|
624
|
+
}
|
|
625
|
+
if (Date.now() >= deadline) {
|
|
626
|
+
// APRV-106. Best effort, and never fatal: the exit code is still 6, which
|
|
627
|
+
// is what the caller branches on. A withdrawal that itself fails leaves
|
|
628
|
+
// the request live — the pre-APRV-106 behaviour — and says so on stderr
|
|
629
|
+
// rather than converting a timeout into a different outcome.
|
|
630
|
+
const withdrawn = [];
|
|
631
|
+
if (withdrawOnTimeout && withdrawActor !== null) {
|
|
632
|
+
for (const action of actions) {
|
|
633
|
+
if (action.state !== "requested")
|
|
634
|
+
continue;
|
|
635
|
+
const result = withdraw(logPath, action.action_key, withdrawActor, {
|
|
636
|
+
policy: policyLocation(flags, cwd),
|
|
637
|
+
reason: "timeout",
|
|
638
|
+
note: `the waiting process stopped waiting after ${timeoutText}; a decision on this request can no longer be consumed`,
|
|
639
|
+
});
|
|
640
|
+
if (result.ok)
|
|
641
|
+
withdrawn.push(action.action_key);
|
|
642
|
+
else {
|
|
643
|
+
streams.err(`approval: could not withdraw ${action.action_key} on timeout (${result.code}): ${result.message}\n`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (json) {
|
|
648
|
+
// `withdrawn` appears only when the flag asked for it. The default
|
|
649
|
+
// timeout object is the shape callers already parse, and adding an
|
|
650
|
+
// always-empty array to it would be a breaking change bought for
|
|
651
|
+
// nothing.
|
|
652
|
+
streams.err(`${JSON.stringify({
|
|
653
|
+
ok: false,
|
|
654
|
+
task,
|
|
655
|
+
status: "timeout",
|
|
656
|
+
actions,
|
|
657
|
+
...(withdrawOnTimeout ? { withdrawn } : {}),
|
|
658
|
+
})}\n`);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
streams.err(withdrawn.length === 0
|
|
662
|
+
? `approval: timeout: ${task} still has undecided request(s) after ${timeoutText}; nothing was appended and the request(s) remain live\n`
|
|
663
|
+
: `approval: timeout: ${task} was undecided after ${timeoutText}; withdrew ${withdrawn.length} request(s) so no one is asked a question this process can no longer answer to: ${withdrawn.join(", ")}\n`);
|
|
664
|
+
}
|
|
665
|
+
return EXIT_TIMEOUT;
|
|
666
|
+
}
|
|
667
|
+
sleepSync(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/** The live inbox: requests inside their TTL, awaiting a human decision. */
|
|
671
|
+
function pendingRequests(records, ts, ttlMs) {
|
|
672
|
+
const keys = [];
|
|
673
|
+
for (const record of records) {
|
|
674
|
+
if (record.event !== "approval.requested")
|
|
675
|
+
continue;
|
|
676
|
+
const key = record.action_key;
|
|
677
|
+
if (typeof key !== "string" || key.length === 0)
|
|
678
|
+
continue;
|
|
679
|
+
if (!keys.includes(key))
|
|
680
|
+
keys.push(key);
|
|
681
|
+
}
|
|
682
|
+
const entries = [];
|
|
683
|
+
for (const key of keys) {
|
|
684
|
+
const derivation = requestState(records, key, ts, ttlMs);
|
|
685
|
+
if (derivation.state !== "requested")
|
|
686
|
+
continue;
|
|
687
|
+
const requestedAt = Date.parse(derivation.requestTs ?? "");
|
|
688
|
+
const asked = Date.parse(ts);
|
|
689
|
+
const remaining = ttlMs === null || Number.isNaN(requestedAt) || Number.isNaN(asked)
|
|
690
|
+
? null
|
|
691
|
+
: requestedAt + ttlMs - asked;
|
|
692
|
+
entries.push({
|
|
693
|
+
action_key: key,
|
|
694
|
+
task: derivation.task,
|
|
695
|
+
class: derivation.declared.class,
|
|
696
|
+
est_cost_usd: derivation.declared.est_cost_usd,
|
|
697
|
+
requested_ts: derivation.requestTs,
|
|
698
|
+
seq: derivation.requestSeq,
|
|
699
|
+
ttl_remaining_ms: remaining,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return entries.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
703
|
+
}
|
|
704
|
+
/** `23h 58m left`, `45s left`, `lapsed` — the TTL column's text. */
|
|
705
|
+
function ttlText(remainingMs) {
|
|
706
|
+
if (remainingMs === null)
|
|
707
|
+
return "no TTL";
|
|
708
|
+
const seconds = Math.max(0, Math.round(remainingMs / 1000));
|
|
709
|
+
if (seconds === 0)
|
|
710
|
+
return "lapsed";
|
|
711
|
+
if (seconds < 60)
|
|
712
|
+
return `${seconds}s left`;
|
|
713
|
+
const minutes = Math.floor(seconds / 60);
|
|
714
|
+
if (minutes < 60)
|
|
715
|
+
return `${minutes}m left`;
|
|
716
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m left`;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The TTL column's role, by the fraction of the window still standing:
|
|
720
|
+
* comfortable above half, worth noticing above a tenth, urgent below it.
|
|
721
|
+
*/
|
|
722
|
+
function ttlRole(remainingMs, ttlMs) {
|
|
723
|
+
if (remainingMs === null || ttlMs === null || ttlMs <= 0)
|
|
724
|
+
return "muted";
|
|
725
|
+
const fraction = remainingMs / ttlMs;
|
|
726
|
+
if (fraction > 0.5)
|
|
727
|
+
return "ok";
|
|
728
|
+
if (fraction > 0.1)
|
|
729
|
+
return "warn";
|
|
730
|
+
return "fail";
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* The live inbox as an aligned table (APRV-91 #9).
|
|
734
|
+
*
|
|
735
|
+
* The old shape was tab-separated, which lines up only when every field happens
|
|
736
|
+
* to be the same width, and a queue's fields never are. The alignment is
|
|
737
|
+
* `style.table`'s (APRV-102 replaced a hand-rolled copy of it here), so widths
|
|
738
|
+
* are measured on the UNDRESSED cells and the coloured render is the plain one
|
|
739
|
+
* with escapes inserted.
|
|
740
|
+
*
|
|
741
|
+
* The TTL is the only dressed cell. The request timestamp was `muted` until
|
|
742
|
+
* APRV-102 and is not any more: a timestamp is a value an operator copies into
|
|
743
|
+
* a `grep` or a bug report, and rule 3 of `style.ts` does not have a "but this
|
|
744
|
+
* one is only dim" exception.
|
|
745
|
+
*/
|
|
746
|
+
export function renderQueueHuman(pending, ttlMs, st = style()) {
|
|
747
|
+
const rows = pending.map((entry) => [
|
|
748
|
+
entry.action_key,
|
|
749
|
+
entry.task ?? "-",
|
|
750
|
+
entry.class ?? "-",
|
|
751
|
+
`$${String(entry.est_cost_usd ?? 0)}`,
|
|
752
|
+
entry.requested_ts ?? "-",
|
|
753
|
+
{ text: ttlText(entry.ttl_remaining_ms), role: ttlRole(entry.ttl_remaining_ms, ttlMs) },
|
|
754
|
+
]);
|
|
755
|
+
return `${table(st, rows, {
|
|
756
|
+
header: ["action", "task", "class", "cost", "requested", "ttl"],
|
|
757
|
+
})}\n`;
|
|
758
|
+
}
|
|
759
|
+
export function commandQueue(argv, streams, cwd) {
|
|
760
|
+
const outcome = front(argv, { ...COMMON_FLAGS, ...POLICY_FLAGS }, QUEUE_HELP, streams, cwd);
|
|
761
|
+
if (outcome.kind === "handled")
|
|
762
|
+
return outcome.code;
|
|
763
|
+
const { flags, positionals, json, logPath } = outcome;
|
|
764
|
+
const extra = positionals[0];
|
|
765
|
+
if (extra !== undefined) {
|
|
766
|
+
return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, QUEUE_HELP);
|
|
767
|
+
}
|
|
768
|
+
const check = preflightLog(logPath);
|
|
769
|
+
if (!check.ok)
|
|
770
|
+
return ioError(streams, json, check.message);
|
|
771
|
+
const read = readVerifiedRecords(logPath);
|
|
772
|
+
if (!read.ok) {
|
|
773
|
+
return emitRefusal(streams, json, {
|
|
774
|
+
ok: false,
|
|
775
|
+
// The read refusal's code is already one of this command's codes
|
|
776
|
+
// (`log-unreadable`, `log-torn-tail`, `log-corrupt`); it is surfaced
|
|
777
|
+
// unchanged so a corrupt log is reported as corruption, not as I/O.
|
|
778
|
+
code: read.code,
|
|
779
|
+
message: read.message,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// ONE read of the policy: `ttlOf` loads and parses it, and calling it twice
|
|
783
|
+
// (as this did before APRV-102) risks the two halves of one render disagreeing
|
|
784
|
+
// about the TTL if the file changes underneath, on top of the wasted work.
|
|
785
|
+
const ttlMs = ttlOf(flags, cwd);
|
|
786
|
+
const pending = pendingRequests(read.records, now(), ttlMs);
|
|
787
|
+
if (json) {
|
|
788
|
+
emitJson(streams, { ok: true, pending });
|
|
789
|
+
}
|
|
790
|
+
else if (pending.length === 0) {
|
|
791
|
+
streams.out("queue: empty — no requests awaiting a decision\n");
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
streams.out(renderQueueHuman(pending, ttlMs, style({ json })));
|
|
795
|
+
}
|
|
796
|
+
// An empty inbox is a healthy inbox: queue never exits non-zero for having
|
|
797
|
+
// nothing (or something) in it. Only the filesystem and a torn log can.
|
|
798
|
+
return EXIT_OK;
|
|
799
|
+
}
|
|
800
|
+
// ===========================================================================
|
|
801
|
+
// approval status
|
|
802
|
+
// ===========================================================================
|
|
803
|
+
/** The policy file whose bytes attestation is judged against. */
|
|
804
|
+
function policyPathFor(flags, cwd) {
|
|
805
|
+
const location = policyLocation(flags, cwd);
|
|
806
|
+
if (location.file !== undefined)
|
|
807
|
+
return location.file;
|
|
808
|
+
const dir = location.dir ?? cwd;
|
|
809
|
+
return resolvePathSegments(dir, POLICY_FILENAMES[0] ?? "APPROVAL.md");
|
|
810
|
+
}
|
|
811
|
+
function budgetHeadroom(records, flags, cwd, ts) {
|
|
812
|
+
const location = policyLocation(flags, cwd);
|
|
813
|
+
const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
|
|
814
|
+
// A ZERO-COST PROBE: the hypothetical next action, declaring $0 and no class.
|
|
815
|
+
// Only the global budgets are evaluated (class limits need a matched rule and
|
|
816
|
+
// therefore a specific action, which status does not have). `remaining` is
|
|
817
|
+
// consequently headroom AFTER that probe — which for daily_actions means one
|
|
818
|
+
// action is already subtracted, because every authorization counts as one.
|
|
819
|
+
// Stated here and in --help rather than quietly adjusted: the number a reader
|
|
820
|
+
// sees is the number the evaluator would produce for the next action.
|
|
821
|
+
return evaluateBudgets(records, {
|
|
822
|
+
classLimits: null,
|
|
823
|
+
classPattern: null,
|
|
824
|
+
globalBudgets: load.ok ? load.policy.budgets ?? null : null,
|
|
825
|
+
}, { class: "", est_cost_usd: "0" }, ts).verdicts;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* The one warning `status` exists to keep in front of an operator: the payload
|
|
829
|
+
* store is the only thing under `.approval/` that a rebuild cannot recreate.
|
|
830
|
+
*
|
|
831
|
+
* QUEUE.md regenerates and `index.sqlite` reindexes, both from the log. The
|
|
832
|
+
* store does not: the log records the *hash* a request bound to, never the
|
|
833
|
+
* bytes, so bytes deleted from `.approval/payloads/` are gone. What survives is
|
|
834
|
+
* the binding, which is why the loss is visible rather than silent: every
|
|
835
|
+
* manual request whose material went with it renders `payload-unavailable`
|
|
836
|
+
* (`channels/tagging.ts`) instead of showing an approver something no hash ever
|
|
837
|
+
* bound.
|
|
838
|
+
*/
|
|
839
|
+
const PAYLOAD_STORE_NOTE = "the payload store holds the bytes approvals bind to, keyed by their hash; " +
|
|
840
|
+
"it is the one cache that cannot be rebuilt from the log, and losing it leaves " +
|
|
841
|
+
"manual requests rendering as payload-unavailable rather than showing bytes no hash bound";
|
|
842
|
+
/**
|
|
843
|
+
* How many payloads are stored, whether the store exists, and what the log says
|
|
844
|
+
* about the ones that are gone (APRV-41).
|
|
845
|
+
*
|
|
846
|
+
* `pruned` counts distinct hashes named by a `payload.pruned` event: retention
|
|
847
|
+
* removes bytes and leaves that record behind on purpose, so a reader can tell
|
|
848
|
+
* "this store never held it" from "this store held it and the daemon let it go".
|
|
849
|
+
* `orphans` counts files no record binds — head-moved residue, which the daemon
|
|
850
|
+
* removes once `payload_retention` is set and which nothing removes while it is
|
|
851
|
+
* absent. Both are facts, never health inputs.
|
|
852
|
+
*/
|
|
853
|
+
function payloadStoreSummary(logPath, records) {
|
|
854
|
+
let files = 0;
|
|
855
|
+
let present = true;
|
|
856
|
+
try {
|
|
857
|
+
for (const entry of readdirSync(payloadStoreDirFor(logPath), { withFileTypes: true })) {
|
|
858
|
+
// `<hash>.json` and nothing else. Temp files from an interrupted atomic
|
|
859
|
+
// write start with a dot and are not payloads anybody can read.
|
|
860
|
+
if (entry.isFile() && entry.name.endsWith(".json") && !entry.name.startsWith(".")) {
|
|
861
|
+
files += 1;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
// Unreadable and absent are reported the same way on purpose: `status` is
|
|
867
|
+
// not the environment diagnostic. `approval doctor` distinguishes them, and
|
|
868
|
+
// an unwritable store is a failure there.
|
|
869
|
+
present = false;
|
|
870
|
+
files = 0;
|
|
871
|
+
}
|
|
872
|
+
const census = payloadStoreCensus(records, payloadStoreDirFor(logPath));
|
|
873
|
+
return {
|
|
874
|
+
present,
|
|
875
|
+
files,
|
|
876
|
+
pruned: census.pruned,
|
|
877
|
+
orphans: census.orphans,
|
|
878
|
+
note: PAYLOAD_STORE_NOTE,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The informational git-coverage line of `approval status` (APRV-245).
|
|
883
|
+
*
|
|
884
|
+
* The range is the CURRENT BRANCH's own commits: `defaultRange` takes the merge
|
|
885
|
+
* base with `origin/main`, so what this counts is what this branch added and not
|
|
886
|
+
* the whole history. Two states are reported instead of a count, because in
|
|
887
|
+
* neither of them would a count mean anything: a directory that is not a git
|
|
888
|
+
* checkout, and a checkout where `origin/main` does not resolve. The second is
|
|
889
|
+
* NOT quietly swapped for the last twenty commits here — `approval coverage`
|
|
890
|
+
* announces that fallback in its own output where there is room to say so, and
|
|
891
|
+
* a one-line summary that silently changed what it measured would be worse than
|
|
892
|
+
* one that says it cannot measure.
|
|
893
|
+
*
|
|
894
|
+
* Informational, exactly as `harness outcomes` beside it is (APRV-145): it is a
|
|
895
|
+
* coverage measurement rather than an integrity verdict, so it is deliberately
|
|
896
|
+
* outside `healthy` and outside the exit code. A gap here is a question for a
|
|
897
|
+
* person ("was that commit ever declared?"), and questions with legitimate
|
|
898
|
+
* answers must not turn a `status` run red.
|
|
899
|
+
*/
|
|
900
|
+
function gitCoverageSummary(records, flags, cwd) {
|
|
901
|
+
const empty = (reason) => ({
|
|
902
|
+
available: false,
|
|
903
|
+
reason,
|
|
904
|
+
observed: 0,
|
|
905
|
+
covered: 0,
|
|
906
|
+
});
|
|
907
|
+
const root = repoRoot(cwd);
|
|
908
|
+
if (root === null)
|
|
909
|
+
return empty("not a git checkout");
|
|
910
|
+
const range = defaultRange(root, DEFAULT_TRUNK_REF);
|
|
911
|
+
if (range.note !== undefined)
|
|
912
|
+
return empty(`${DEFAULT_TRUNK_REF} absent`);
|
|
913
|
+
const location = policyLocation(flags, cwd);
|
|
914
|
+
const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
|
|
915
|
+
const seen = observeGit(root, {
|
|
916
|
+
base: range.base,
|
|
917
|
+
head: range.head,
|
|
918
|
+
policyProtectedPaths: load.ok ? (load.policy.protected_paths ?? []) : [],
|
|
919
|
+
});
|
|
920
|
+
if (!seen.available)
|
|
921
|
+
return empty(seen.reason ?? "git could not be asked");
|
|
922
|
+
const report = coverageReport(seen.effects, records);
|
|
923
|
+
return {
|
|
924
|
+
available: true,
|
|
925
|
+
reason: seen.reason ?? null,
|
|
926
|
+
observed: report.observed,
|
|
927
|
+
covered: report.covered,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
export function commandStatus(argv, streams, cwd) {
|
|
931
|
+
const outcome = front(argv, { ...COMMON_FLAGS, ...POLICY_FLAGS, "--verbose": "boolean" }, STATUS_HELP, streams, cwd);
|
|
932
|
+
if (outcome.kind === "handled")
|
|
933
|
+
return outcome.code;
|
|
934
|
+
const { flags, positionals, json, logPath } = outcome;
|
|
935
|
+
const extra = positionals[0];
|
|
936
|
+
if (extra !== undefined) {
|
|
937
|
+
return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, STATUS_HELP);
|
|
938
|
+
}
|
|
939
|
+
const check = preflightLog(logPath);
|
|
940
|
+
if (!check.ok)
|
|
941
|
+
return ioError(streams, json, check.message);
|
|
942
|
+
// The policy is read here for one number, the skew tolerance of amended
|
|
943
|
+
// SPEC.md §8 (APRV-58), and it reaches only which anomalies are reported. The
|
|
944
|
+
// verdict, the health line and the exit code below are unmoved by it.
|
|
945
|
+
const verification = verify(logPath, { policy: policyLocation(flags, cwd) });
|
|
946
|
+
const read = readVerifiedRecords(logPath);
|
|
947
|
+
// A log that cannot be read at all is an I/O fact, not a health report.
|
|
948
|
+
if (!read.ok && read.code === "log-unreadable") {
|
|
949
|
+
return ioError(streams, json, read.message);
|
|
950
|
+
}
|
|
951
|
+
// A torn or corrupt log still produces a health report — that *is* the report,
|
|
952
|
+
// and `verification` below names the damage. Projections over an unverifiable
|
|
953
|
+
// log are simply empty: `status` describes the log, it never authorizes
|
|
954
|
+
// anything from one.
|
|
955
|
+
const records = read.ok ? read.records : [];
|
|
956
|
+
const attestation = checkAttestation(records, policyPathFor(flags, cwd));
|
|
957
|
+
// APRV-120. `dangling` is now the `open` custody state only: a harness
|
|
958
|
+
// execution is terminal by design and never gains an outcome, so listing it
|
|
959
|
+
// as debris trained operators to ignore this list (the reference repository's
|
|
960
|
+
// own log carried dozens). Indeterminate outcomes are reported beside it
|
|
961
|
+
// rather than inside it, because the two ask a person for different things:
|
|
962
|
+
// look at what our runtime did, against establish whether the far side
|
|
963
|
+
// committed.
|
|
964
|
+
const dangling = danglingExecutions(records).map((entry) => ({
|
|
965
|
+
action_key: entry.actionKey,
|
|
966
|
+
task: entry.task,
|
|
967
|
+
ts: entry.ts,
|
|
968
|
+
seq: entry.seq,
|
|
969
|
+
}));
|
|
970
|
+
const indeterminate = indeterminateExecutions(records).map((entry) => ({
|
|
971
|
+
action_key: entry.actionKey,
|
|
972
|
+
task: entry.task,
|
|
973
|
+
ts: entry.ts,
|
|
974
|
+
seq: entry.indeterminateSeq,
|
|
975
|
+
reason: entry.reason,
|
|
976
|
+
}));
|
|
977
|
+
// APRV-145. Three streaks now, one shape. The per-task streak of SPEC.md
|
|
978
|
+
// §10.2 keeps its `task` field and its meaning; the two harness scopes of the
|
|
979
|
+
// amended §10.2 report their scope KEY in that same field, so it stays a
|
|
980
|
+
// non-empty string an operator can grep the log for and every pre-existing
|
|
981
|
+
// consumer reads the three fields it always read. `scope` is the additive
|
|
982
|
+
// field that says which derivation produced the key.
|
|
983
|
+
//
|
|
984
|
+
// APRV-280 adds `clears`, additively: an escalated scope is a repository state
|
|
985
|
+
// an operator has to get OUT of, and a row that names the scope without naming
|
|
986
|
+
// the exit is a row that sends them to the source. The sentence is
|
|
987
|
+
// `core/loop.ts`'s own, so `status`, the gate's refusals and the hook's denies
|
|
988
|
+
// cannot come to disagree about what recovery is.
|
|
989
|
+
const escalations = [
|
|
990
|
+
...loopEscalation(records)
|
|
991
|
+
.filter((state) => state.escalated)
|
|
992
|
+
.map((state) => ({
|
|
993
|
+
task: state.task,
|
|
994
|
+
scope: "task",
|
|
995
|
+
consecutive_failures: state.consecutiveFailures,
|
|
996
|
+
escalated: true,
|
|
997
|
+
clears: loopClearance("task", state.task),
|
|
998
|
+
})),
|
|
999
|
+
...harnessLoopEscalation(records)
|
|
1000
|
+
.filter((state) => state.escalated)
|
|
1001
|
+
.map((state) => ({
|
|
1002
|
+
task: state.key,
|
|
1003
|
+
scope: state.scope,
|
|
1004
|
+
consecutive_failures: state.consecutiveFailures,
|
|
1005
|
+
escalated: true,
|
|
1006
|
+
clears: loopClearance(state.scope, state.key),
|
|
1007
|
+
})),
|
|
1008
|
+
];
|
|
1009
|
+
// Informational, and deliberately outside `healthy` and the exit code, for the
|
|
1010
|
+
// reason the timestamp anomalies below are: this is a coverage measurement,
|
|
1011
|
+
// not an integrity verdict. A persistently high `unreported` is how an
|
|
1012
|
+
// operator learns the post-execution hook is not installed or not firing.
|
|
1013
|
+
const harnessOutcomes = harnessOutcomeCoverage(records);
|
|
1014
|
+
// APRV-245, and informational for the reason directly above: the same rule,
|
|
1015
|
+
// applied to a witness this project does not write. `harness outcomes` counts
|
|
1016
|
+
// the tool calls the runtime was told about; this counts the commits git saw
|
|
1017
|
+
// whether or not anybody told the runtime anything.
|
|
1018
|
+
const coverage = gitCoverageSummary(records, flags, cwd);
|
|
1019
|
+
// APRV-127. The reconciliation backlog: obligations opened by a retrospective
|
|
1020
|
+
// DENIAL and not yet discharged by a person. It counts toward `healthy` for
|
|
1021
|
+
// the same reason a dangling execution does — an unreconciled denial is a "no"
|
|
1022
|
+
// that has so far changed nothing, and a "no" nobody can see is the failure
|
|
1023
|
+
// the whole retrospective path exists to prevent. Quiet here would mean a
|
|
1024
|
+
// human said an action should not have happened and the system moved on.
|
|
1025
|
+
const obligations = openObligations(records).map((item) => ({
|
|
1026
|
+
seq: item.seq,
|
|
1027
|
+
ts: item.ts,
|
|
1028
|
+
action_key: item.actionKey,
|
|
1029
|
+
task: item.task,
|
|
1030
|
+
class: item.class,
|
|
1031
|
+
obligation: item.obligation,
|
|
1032
|
+
review_seq: item.reviewSeq,
|
|
1033
|
+
}));
|
|
1034
|
+
// APRV-214, amended SPEC.md §5.2. An open window is a suspension of the
|
|
1035
|
+
// policy for every gated tool call under this root, so it belongs in the one
|
|
1036
|
+
// report an operator reads to answer "is this repository in a normal state".
|
|
1037
|
+
// It counts toward `healthy` DELIBERATELY: a CI check or a `doctor` run keyed
|
|
1038
|
+
// on `healthy` should go red while a bypass stands, and a window nobody
|
|
1039
|
+
// noticed was left open is the failure mode of the whole feature.
|
|
1040
|
+
const gateWindow = openGateWindow(records);
|
|
1041
|
+
const budgets = budgetHeadroom(records, flags, cwd, now());
|
|
1042
|
+
// Informational: the store's state never moves `healthy` or the exit code.
|
|
1043
|
+
// A repo that has never made a `--payload` request has no store, and an
|
|
1044
|
+
// operator is being told what it is, not that anything is wrong.
|
|
1045
|
+
const payloadStore = payloadStoreSummary(logPath, records);
|
|
1046
|
+
const verificationSummary = {
|
|
1047
|
+
status: verification.status,
|
|
1048
|
+
records: verification.status === "corrupt" ? null : verification.records,
|
|
1049
|
+
};
|
|
1050
|
+
// APRV-40. Timestamp anomalies (SPEC.md §8) are informational and deliberately
|
|
1051
|
+
// outside `healthy`: they are a judgment, not an integrity verdict. `verify`
|
|
1052
|
+
// already declined to refuse on them, and `status` does not get to overrule it
|
|
1053
|
+
// by flipping a health bit an operator reads as "something is broken". The
|
|
1054
|
+
// field is present only when there is something to report, so every existing
|
|
1055
|
+
// `--json` consumer sees a byte-identical object on a log with no anomaly.
|
|
1056
|
+
//
|
|
1057
|
+
// The sampler's own state is NOT reported here: `status --json` is a frozen
|
|
1058
|
+
// shape, and the configuration fact belongs to `approval audit list`, which
|
|
1059
|
+
// reports it beside the backlog it explains.
|
|
1060
|
+
const anomalies = verification.anomalies.map((anomaly) => ({
|
|
1061
|
+
kind: anomaly.kind,
|
|
1062
|
+
seq: anomaly.seq,
|
|
1063
|
+
previous_seq: anomaly.previousSeq,
|
|
1064
|
+
skew_ms: anomaly.skewMs,
|
|
1065
|
+
message: anomaly.message,
|
|
1066
|
+
}));
|
|
1067
|
+
const healthy = attestation.status === "attested" &&
|
|
1068
|
+
verification.status === "clean" &&
|
|
1069
|
+
dangling.length === 0 &&
|
|
1070
|
+
// An unreconciled indeterminate outcome is a side effect nobody has
|
|
1071
|
+
// established the fate of, and a repo carrying one is not healthy; an open
|
|
1072
|
+
// reconciliation obligation is a denial nobody has answered for yet.
|
|
1073
|
+
indeterminate.length === 0 &&
|
|
1074
|
+
escalations.length === 0 &&
|
|
1075
|
+
obligations.length === 0 &&
|
|
1076
|
+
gateWindow === null;
|
|
1077
|
+
if (json) {
|
|
1078
|
+
emitJson(streams, {
|
|
1079
|
+
ok: true,
|
|
1080
|
+
healthy,
|
|
1081
|
+
attestation: {
|
|
1082
|
+
state: attestation.status,
|
|
1083
|
+
seq: attestation.status === "attested" || attestation.status === "hash-mismatch"
|
|
1084
|
+
? attestation.seq
|
|
1085
|
+
: null,
|
|
1086
|
+
},
|
|
1087
|
+
verification: verificationSummary,
|
|
1088
|
+
dangling,
|
|
1089
|
+
// Present only when there is something to report, exactly as `anomalies`
|
|
1090
|
+
// is: every existing consumer sees a byte-identical object on a log that
|
|
1091
|
+
// carries no indeterminate outcome.
|
|
1092
|
+
...(indeterminate.length === 0 ? {} : { indeterminate }),
|
|
1093
|
+
budgets,
|
|
1094
|
+
loop_escalations: escalations,
|
|
1095
|
+
harness_outcomes: harnessOutcomes,
|
|
1096
|
+
// APRV-245. Always present, so every consumer sees the same four keys
|
|
1097
|
+
// whether or not this directory is a checkout, and outside `healthy` and
|
|
1098
|
+
// the exit code for the APRV-145 reason stated where it is computed.
|
|
1099
|
+
coverage: {
|
|
1100
|
+
available: coverage.available,
|
|
1101
|
+
reason: coverage.reason,
|
|
1102
|
+
observed: coverage.observed,
|
|
1103
|
+
covered: coverage.covered,
|
|
1104
|
+
},
|
|
1105
|
+
reconciliation: obligations,
|
|
1106
|
+
payload_store: payloadStore,
|
|
1107
|
+
...(anomalies.length === 0 ? {} : { anomalies }),
|
|
1108
|
+
// Present only while a window stands, exactly as `anomalies` and
|
|
1109
|
+
// `indeterminate` are: a repository with no window emits the object it
|
|
1110
|
+
// has always emitted, byte for byte.
|
|
1111
|
+
...(gateWindow === null
|
|
1112
|
+
? {}
|
|
1113
|
+
: {
|
|
1114
|
+
gate_window: {
|
|
1115
|
+
seq: gateWindow.seq,
|
|
1116
|
+
opened_at: gateWindow.openedAt,
|
|
1117
|
+
opened_by: gateWindow.openedBy,
|
|
1118
|
+
reason: gateWindow.reason,
|
|
1119
|
+
expires_at: gateWindow.expiresAt,
|
|
1120
|
+
bypassed: gateWindow.bypassCount,
|
|
1121
|
+
},
|
|
1122
|
+
}),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
const st = style({ json });
|
|
1127
|
+
const verbose = boolFlag(flags, "--verbose");
|
|
1128
|
+
const rows = [
|
|
1129
|
+
{
|
|
1130
|
+
left: "health",
|
|
1131
|
+
right: healthy ? st.ok("ok") : st.warn("attention"),
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
left: "attestation",
|
|
1135
|
+
// The seq is a value: an operator pastes it into `approval log tail` or
|
|
1136
|
+
// a bug report, so APRV-102 took the `muted` dressing off it. The state
|
|
1137
|
+
// beside it still carries the colour.
|
|
1138
|
+
right: `${st.paint(attestation.status === "attested" ? "ok" : "warn", attestation.status)}${attestation.status === "attested" || attestation.status === "hash-mismatch"
|
|
1139
|
+
? ` (seq ${attestation.seq})`
|
|
1140
|
+
: ""}`,
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
left: "verification",
|
|
1144
|
+
right: `${st.paint(verificationSummary.status === "clean" ? "ok" : "fail", verificationSummary.status)}${verificationSummary.records === null
|
|
1145
|
+
? ""
|
|
1146
|
+
: ` ${st.muted(`(${verificationSummary.records} record(s))`)}`}`,
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
left: "timestamp anomalies",
|
|
1150
|
+
right: anomalies.length === 0
|
|
1151
|
+
? st.muted("none")
|
|
1152
|
+
: st.warn(`${anomalies.length} (reported, NOT refused — the chain verifies and health is unaffected)`),
|
|
1153
|
+
...(anomalies.length === 0
|
|
1154
|
+
? {}
|
|
1155
|
+
: {
|
|
1156
|
+
under: anomalies.map((anomaly) => `${anomaly.kind} seq ${anomaly.seq} ${anomaly.skew_ms}ms before seq ${anomaly.previous_seq}`),
|
|
1157
|
+
}),
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
left: "dangling executions",
|
|
1161
|
+
right: dangling.length === 0 ? st.muted("none") : st.fail(String(dangling.length)),
|
|
1162
|
+
...(dangling.length === 0
|
|
1163
|
+
? {}
|
|
1164
|
+
: {
|
|
1165
|
+
under: dangling.map((entry) => `${entry.action_key} started ${entry.ts} seq ${entry.seq}`),
|
|
1166
|
+
}),
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
// Its own row, never folded into the one above: the repair is a
|
|
1170
|
+
// different verb answering a different question (APRV-120).
|
|
1171
|
+
left: "indeterminate executions",
|
|
1172
|
+
right: indeterminate.length === 0 ? st.muted("none") : st.fail(String(indeterminate.length)),
|
|
1173
|
+
...(indeterminate.length === 0
|
|
1174
|
+
? {}
|
|
1175
|
+
: {
|
|
1176
|
+
under: indeterminate.map((entry) => `${entry.action_key} ${entry.reason ?? "unknown reason"} seq ${String(entry.seq)} — outcome unknown; \`approval execution reconcile\``),
|
|
1177
|
+
}),
|
|
1178
|
+
},
|
|
1179
|
+
...(budgets.length === 0
|
|
1180
|
+
? [{ left: "budgets", right: st.muted("none configured") }]
|
|
1181
|
+
: budgets.map((verdict) => ({
|
|
1182
|
+
left: `budget ${verdict.limit}`,
|
|
1183
|
+
right: `consumed ${verdict.consumed}, remaining ${verdict.remaining}`,
|
|
1184
|
+
}))),
|
|
1185
|
+
{
|
|
1186
|
+
// APRV-91: the two-line sentence that used to live here is the store's
|
|
1187
|
+
// rationale, not its state. The state is three numbers, and the
|
|
1188
|
+
// rationale is one `--json` field (`payload_store.note`) and a
|
|
1189
|
+
// paragraph in `docs/` away.
|
|
1190
|
+
left: "payload store",
|
|
1191
|
+
right: `${payloadStore.present ? `${payloadStore.files} file(s)` : "not created yet"}, ${payloadStore.pruned} pruned, ${payloadStore.orphans} unbound`,
|
|
1192
|
+
// …and `--verbose` puts it back (APRV-102). The sentence is the one
|
|
1193
|
+
// thing here a first-time reader cannot reconstruct from the numbers,
|
|
1194
|
+
// so it is one flag away rather than gone.
|
|
1195
|
+
...(verbose ? { under: [st.muted(payloadStore.note)] } : {}),
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
left: "loop escalations",
|
|
1199
|
+
right: escalations.length === 0 ? st.muted("none") : st.fail(String(escalations.length)),
|
|
1200
|
+
...(escalations.length === 0
|
|
1201
|
+
? {}
|
|
1202
|
+
: {
|
|
1203
|
+
// APRV-280: the scope, then the way out. The clearing sentence is
|
|
1204
|
+
// long and it earns its line — an operator reading this row is
|
|
1205
|
+
// looking at a repository where a floor is routing everything to a
|
|
1206
|
+
// phone, and the next thing they need is what ends that.
|
|
1207
|
+
under: escalations.flatMap((entry) => [
|
|
1208
|
+
entry.scope === "task"
|
|
1209
|
+
? `${entry.task} (${entry.consecutive_failures} consecutive failed side-effecting executions, task) — escalated to manual`
|
|
1210
|
+
: `${entry.task} (${entry.consecutive_failures} consecutive failed side-effecting tool calls, ${entry.scope}) — escalated to manual`,
|
|
1211
|
+
st.muted(` clears: ${entry.clears}`),
|
|
1212
|
+
]),
|
|
1213
|
+
}),
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
// APRV-145. Its own INFORMATIONAL row, because the append-nothing rule
|
|
1217
|
+
// of the counterpart is otherwise invisible: a harness start with no
|
|
1218
|
+
// outcome is not debris, it is a tool call nobody reported on, and the
|
|
1219
|
+
// number is how an operator sees that the post-execution hook is dark.
|
|
1220
|
+
left: "harness outcomes",
|
|
1221
|
+
right: st.muted(`${harnessOutcomes.started} started, ${harnessOutcomes.reported} reported, ${harnessOutcomes.unreported} unreported`),
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
// APRV-245, and INFORMATIONAL for the APRV-145 reason the row above is:
|
|
1225
|
+
// a coverage measurement is not an integrity verdict, so it moves
|
|
1226
|
+
// neither `healthy` nor the exit code. What it counts is this branch's
|
|
1227
|
+
// own commits, as git recorded them, against the verified log. The full
|
|
1228
|
+
// report, `gh` and the adapters included, is `approval coverage`.
|
|
1229
|
+
left: "git coverage",
|
|
1230
|
+
right: st.muted(coverage.available
|
|
1231
|
+
? `${String(coverage.covered)} of ${String(coverage.observed)} effects carry evidence`
|
|
1232
|
+
: (coverage.reason ?? "unavailable")),
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
// APRV-214. Its own row and not a footnote: while this says OPEN, the
|
|
1236
|
+
// policy is deciding nothing for the harness, and the person reading
|
|
1237
|
+
// this report is the person who can end that.
|
|
1238
|
+
left: "gate window",
|
|
1239
|
+
right: gateWindow === null
|
|
1240
|
+
? st.muted("closed")
|
|
1241
|
+
: st.warn(`OPEN until ${gateWindow.expiresAt}, opened by ${gateWindow.openedBy}, ${String(gateWindow.bypassCount)} call(s) bypassed`),
|
|
1242
|
+
...(gateWindow === null
|
|
1243
|
+
? {}
|
|
1244
|
+
: {
|
|
1245
|
+
under: [
|
|
1246
|
+
`seq ${String(gateWindow.seq)} reason: ${gateWindow.reason}`,
|
|
1247
|
+
"every gated tool call under this root is allowed without approval — `approval gate close`",
|
|
1248
|
+
],
|
|
1249
|
+
}),
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
left: "reconciliation",
|
|
1253
|
+
right: obligations.length === 0
|
|
1254
|
+
? st.muted("none open")
|
|
1255
|
+
: st.fail(`${obligations.length} UNRECONCILED DENIAL(S)`),
|
|
1256
|
+
...(obligations.length === 0
|
|
1257
|
+
? {}
|
|
1258
|
+
: {
|
|
1259
|
+
under: obligations.map((item) => `seq ${item.seq} ${item.action_key} ${item.class} ${item.obligation} — close with \`approval audit reconcile ${item.seq}\``),
|
|
1260
|
+
}),
|
|
1261
|
+
},
|
|
1262
|
+
{ left: "log", right: relPath(logPath, cwd) },
|
|
1263
|
+
];
|
|
1264
|
+
streams.out(`${st.table(rows)}\n`);
|
|
1265
|
+
}
|
|
1266
|
+
return healthy ? EXIT_OK : EXIT_INTEGRITY;
|
|
1267
|
+
}
|
|
1268
|
+
/** The manual command for a key nothing can prove, spelled once. */
|
|
1269
|
+
function manualResolveCommand(actionKey) {
|
|
1270
|
+
return `approval execution resolve ${actionKey} --outcome completed|failed --note "<what you observed>"`;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* The note a bulk resolution writes, which is the evidence and not a summary.
|
|
1274
|
+
*
|
|
1275
|
+
* It names the ref, the seq that ref carries, and the fact that the operator
|
|
1276
|
+
* confirmed it after being shown exactly that. A human-attested record whose
|
|
1277
|
+
* note said only "closed in bulk" would be the unexplained attestation the
|
|
1278
|
+
* single form refuses, arriving five at a time.
|
|
1279
|
+
*/
|
|
1280
|
+
function sweptNote(entry) {
|
|
1281
|
+
return `${entry.actionKey} named seq ${String(entry.toSeq)}, which ${String(entry.provenBy)} carries in this checkout, so the action it was authorized for completed. Confirmed against that evidence and closed with \`${RESOLVE_DANGLING_COMMAND}\`.`;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* `approval execution resolve --dangling [--class <class>] [--yes] [--json]`
|
|
1285
|
+
*
|
|
1286
|
+
* The bulk form, and the manual step it removes (APRV-264). On 2026-09-05
|
|
1287
|
+
* `approval status` listed five dangling daemon advance executions left by the
|
|
1288
|
+
* 2026-09-02 loop; the daemon refused one advance per tick naming one key each,
|
|
1289
|
+
* and Carter closed all five by hand with five near-identical commands in a
|
|
1290
|
+
* second terminal window. The cadence exists to remove taps, and this was five
|
|
1291
|
+
* of them for one fact.
|
|
1292
|
+
*
|
|
1293
|
+
* What it does NOT do is decide anything the single form would not. Every rule
|
|
1294
|
+
* of `resolve` is intact: human-only, one `execution.completed` per key through
|
|
1295
|
+
* {@link resolveExecution}'s own compare-and-append, `exit_code: null`,
|
|
1296
|
+
* `attested_by_human: true`, and a mandatory non-empty note — generated here
|
|
1297
|
+
* rather than typed, because what it has to say is the evidence the runtime
|
|
1298
|
+
* showed the operator and the operator agreed with, which is a sentence a
|
|
1299
|
+
* person retyping it five times would only ever get less exact.
|
|
1300
|
+
*
|
|
1301
|
+
* The evidence is the trunk. A key is provable when it is one of the daemon's
|
|
1302
|
+
* own advance keys and a ref in this checkout carries the seq that key names
|
|
1303
|
+
* (`core/advance-cycle.ts`'s rule, read through the same `publishedState` the
|
|
1304
|
+
* daemon and the doctor row read). Everything else is UNPROVABLE and is left
|
|
1305
|
+
* exactly alone, listed with the one-line manual command: an outcome nobody can
|
|
1306
|
+
* demonstrate is a person's to go and look at, and a bulk verb that guessed at
|
|
1307
|
+
* one would be writing five guesses instead of one.
|
|
1308
|
+
*
|
|
1309
|
+
* One confirmation, on a terminal. Without a terminal it refuses
|
|
1310
|
+
* (`dangling-stdin-not-tty`) unless `--yes` is passed, which is the flag a
|
|
1311
|
+
* runbook uses after it has read the same list with `--json`. `--json` on its
|
|
1312
|
+
* own still asks, because the list and the question are the whole of what makes
|
|
1313
|
+
* this safe.
|
|
1314
|
+
*/
|
|
1315
|
+
function resolveDangling(streams, cwd, front_, actor, deps) {
|
|
1316
|
+
const { flags, json, logPath } = front_;
|
|
1317
|
+
const check = preflightLog(logPath);
|
|
1318
|
+
if (!check.ok)
|
|
1319
|
+
return ioError(streams, json, check.message);
|
|
1320
|
+
const read = readVerifiedRecords(logPath);
|
|
1321
|
+
if (!read.ok)
|
|
1322
|
+
return emitRefusal(streams, json, read);
|
|
1323
|
+
const classFilter = stringFlag(flags, "--class");
|
|
1324
|
+
const index = indexDeclarations(read.records);
|
|
1325
|
+
// The LOG's repository, exactly as the `log-advance-cadence` doctor row asks
|
|
1326
|
+
// it: the refs that can prove anything about an advance are the ones in the
|
|
1327
|
+
// checkout the log lives in, which is not necessarily where the operator is
|
|
1328
|
+
// standing when they run this.
|
|
1329
|
+
const root = repoRoot(dirname(logPath)) ?? repoRoot(cwd);
|
|
1330
|
+
// The git read, once, for the whole list. `null` when this is not a git
|
|
1331
|
+
// checkout at all, in which case nothing is provable and every key is listed
|
|
1332
|
+
// as a person's — the fail-closed direction, and the honest one.
|
|
1333
|
+
const published = root === null
|
|
1334
|
+
? { publishedSeq: 0, publishedRev: null }
|
|
1335
|
+
: publishedState(root, logPath, read.records, { remote: "origin", base: null }, now());
|
|
1336
|
+
const proved = new Map(proveDanglingAdvances(read.records, published).map((entry) => [entry.actionKey, entry]));
|
|
1337
|
+
const candidates = danglingExecutions([...read.records])
|
|
1338
|
+
.map((entry) => {
|
|
1339
|
+
const advance = proved.get(entry.actionKey);
|
|
1340
|
+
return {
|
|
1341
|
+
actionKey: entry.actionKey,
|
|
1342
|
+
task: entry.task,
|
|
1343
|
+
ts: entry.ts,
|
|
1344
|
+
seq: entry.seq,
|
|
1345
|
+
cls: index.declarations.get(entry.actionKey)?.class ?? null,
|
|
1346
|
+
provenBy: advance?.provenBy ?? null,
|
|
1347
|
+
toSeq: advance?.toSeq ?? null,
|
|
1348
|
+
};
|
|
1349
|
+
})
|
|
1350
|
+
.filter((entry) => classFilter === null || entry.cls === classFilter);
|
|
1351
|
+
const provable = candidates.filter((entry) => entry.provenBy !== null);
|
|
1352
|
+
const unprovable = candidates.filter((entry) => entry.provenBy === null);
|
|
1353
|
+
const listed = candidates.map((entry) => ({
|
|
1354
|
+
action_key: entry.actionKey,
|
|
1355
|
+
task: entry.task,
|
|
1356
|
+
class: entry.cls,
|
|
1357
|
+
seq: entry.seq,
|
|
1358
|
+
ts: entry.ts,
|
|
1359
|
+
provable: entry.provenBy !== null,
|
|
1360
|
+
proven_by: entry.provenBy,
|
|
1361
|
+
proven_seq: entry.provenBy === null ? null : entry.toSeq,
|
|
1362
|
+
...(entry.provenBy === null ? { fix: manualResolveCommand(entry.actionKey) } : {}),
|
|
1363
|
+
}));
|
|
1364
|
+
// Nothing to do is exit 0 and says so: an empty list is a healthy log, and a
|
|
1365
|
+
// repair verb that failed when there was nothing to repair would be a repair
|
|
1366
|
+
// verb nobody could put in a runbook.
|
|
1367
|
+
if (candidates.length === 0) {
|
|
1368
|
+
if (json)
|
|
1369
|
+
emitJson(streams, { ok: true, dangling: [], resolved: [], unresolved: [], actor });
|
|
1370
|
+
else {
|
|
1371
|
+
streams.out(classFilter === null
|
|
1372
|
+
? "no dangling executions: every execution in this log has an outcome\n"
|
|
1373
|
+
: `no dangling executions in class ${classFilter}\n`);
|
|
1374
|
+
}
|
|
1375
|
+
return EXIT_OK;
|
|
1376
|
+
}
|
|
1377
|
+
const st = style({ json });
|
|
1378
|
+
if (!json)
|
|
1379
|
+
streams.out(renderDanglingList(st, candidates));
|
|
1380
|
+
// The confirmation. `--yes` is the runbook's answer to it and the ONLY way
|
|
1381
|
+
// past it without a terminal: a prompter that fell back to a pipe would let
|
|
1382
|
+
// anything that can write bytes attest, on a record whose whole content is
|
|
1383
|
+
// that a person looked.
|
|
1384
|
+
if (!boolFlag(flags, "--yes")) {
|
|
1385
|
+
const prompter = deps.prompter === undefined ? createPrompter(streams) : deps.prompter;
|
|
1386
|
+
if (prompter === null) {
|
|
1387
|
+
return emitRefusal(streams, json, {
|
|
1388
|
+
ok: false,
|
|
1389
|
+
code: "dangling-stdin-not-tty",
|
|
1390
|
+
message: `stdin is not a terminal, so nobody can be asked to attest. ${String(provable.length)} execution(s) would be closed as completed on this checkout's own evidence, and a human-attested outcome nobody was asked about is not an attestation. Re-run it from a terminal, or pass --yes after reading the list (\`${RESOLVE_DANGLING_COMMAND} --json\`)`,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
if (provable.length === 0) {
|
|
1394
|
+
// There is nothing to confirm: the whole list is a person's to go and
|
|
1395
|
+
// look at, and asking a yes/no question about zero records would train an
|
|
1396
|
+
// operator to say yes to this prompt.
|
|
1397
|
+
if (json)
|
|
1398
|
+
emitJson(streams, { ok: true, dangling: listed, resolved: [], unresolved: listed.map((entry) => entry.action_key), actor });
|
|
1399
|
+
return EXIT_OK;
|
|
1400
|
+
}
|
|
1401
|
+
const agreed = confirmUntil(streams, prompter, `Close ${String(provable.length)} execution(s) as completed, attested by ${actor}?`, false);
|
|
1402
|
+
if (!agreed) {
|
|
1403
|
+
return emitRefusal(streams, json, {
|
|
1404
|
+
ok: false,
|
|
1405
|
+
code: "dangling-declined",
|
|
1406
|
+
message: "nothing was appended: the confirmation was declined",
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const resolved = [];
|
|
1411
|
+
const failed = [];
|
|
1412
|
+
for (const entry of provable) {
|
|
1413
|
+
const result = resolveExecution(logPath, entry.actionKey, "completed", sweptNote(entry), actor, {
|
|
1414
|
+
policy: policyLocation(flags, cwd),
|
|
1415
|
+
});
|
|
1416
|
+
if (result.ok) {
|
|
1417
|
+
resolved.push({
|
|
1418
|
+
action_key: entry.actionKey,
|
|
1419
|
+
seq: result.record.seq,
|
|
1420
|
+
proven_by: entry.provenBy,
|
|
1421
|
+
});
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
// One refusal does not stop the sweep: the keys are independent, and a
|
|
1425
|
+
// fourth that cannot be closed is no reason to leave the fifth open. Every
|
|
1426
|
+
// refusal is reported by code, and the exit code below says some of them
|
|
1427
|
+
// were refused.
|
|
1428
|
+
failed.push({ action_key: entry.actionKey, code: result.code, message: result.message });
|
|
1429
|
+
}
|
|
1430
|
+
if (json) {
|
|
1431
|
+
emitJson(streams, {
|
|
1432
|
+
ok: failed.length === 0,
|
|
1433
|
+
dangling: listed,
|
|
1434
|
+
resolved,
|
|
1435
|
+
unresolved: [
|
|
1436
|
+
...unprovable.map((entry) => entry.actionKey),
|
|
1437
|
+
...failed.map((entry) => entry.action_key),
|
|
1438
|
+
],
|
|
1439
|
+
...(failed.length === 0 ? {} : { failed }),
|
|
1440
|
+
attested_by_human: true,
|
|
1441
|
+
actor,
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
for (const entry of resolved) {
|
|
1446
|
+
streams.out(`resolved ${entry.action_key} as completed at seq ${String(entry.seq)} by ${actor} (human-attested, no exit code; ${String(entry.proven_by)})\n`);
|
|
1447
|
+
}
|
|
1448
|
+
for (const entry of failed) {
|
|
1449
|
+
streams.err(`${renderRefusal(st, entry.code, `${entry.action_key}: ${entry.message}`)}\n`);
|
|
1450
|
+
}
|
|
1451
|
+
if (unprovable.length > 0) {
|
|
1452
|
+
streams.out(`${String(unprovable.length)} execution(s) were left alone: nothing in this checkout can prove how they ended, and only a person who goes and looks may say. Close each with its own command, listed above.\n`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return failed.length === 0 ? EXIT_OK : EXIT_INTEGRITY;
|
|
1456
|
+
}
|
|
1457
|
+
/** The list, as a person reads it: what is provable, by what, and what is not. */
|
|
1458
|
+
function renderDanglingList(st, candidates) {
|
|
1459
|
+
const rows = candidates.map((entry) => ({
|
|
1460
|
+
left: entry.actionKey,
|
|
1461
|
+
right: entry.provenBy === null
|
|
1462
|
+
? st.fail("nothing proves how it ended")
|
|
1463
|
+
: st.ok(`${entry.provenBy} carries seq ${String(entry.toSeq)}`),
|
|
1464
|
+
under: [
|
|
1465
|
+
`seq ${String(entry.seq)} ${entry.ts} ${entry.cls ?? "class undeclared"}${entry.task === null ? "" : ` ${entry.task}`}`,
|
|
1466
|
+
...(entry.provenBy === null ? [manualResolveCommand(entry.actionKey)] : []),
|
|
1467
|
+
],
|
|
1468
|
+
}));
|
|
1469
|
+
return `${st.table(rows)}\n`;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* `approval execution resolve <action-key> --outcome completed|failed --note …`
|
|
1473
|
+
*
|
|
1474
|
+
* The human recovery verb for a dangling execution: the runtime died between
|
|
1475
|
+
* `execution.started` and its outcome, `approval status` has been reporting the
|
|
1476
|
+
* gap ever since, and a person went and looked. This records what they saw.
|
|
1477
|
+
*
|
|
1478
|
+
* Three rules, enforced here as usage errors before core is called at all, so
|
|
1479
|
+
* the log is untouched by a malformed invocation:
|
|
1480
|
+
*
|
|
1481
|
+
* - `--outcome` is `completed` or `failed`. Nothing is inferred.
|
|
1482
|
+
* - `--note` is MANDATORY and non-empty. The event's whole value is the
|
|
1483
|
+
* observation behind it.
|
|
1484
|
+
* - The actor must be a human (`--as human:<id>` or `APPROVAL_HUMAN`). An agent
|
|
1485
|
+
* closing its own dangling execution is the executing party reporting on
|
|
1486
|
+
* itself.
|
|
1487
|
+
*
|
|
1488
|
+
* No attestation is required, and the help text says why: resolve records a
|
|
1489
|
+
* fact a human observed; it exercises no policy authority, so it does not
|
|
1490
|
+
* require an attested policy.
|
|
1491
|
+
*
|
|
1492
|
+
* `--dangling` is the bulk form of the same verb ({@link resolveDangling}): the
|
|
1493
|
+
* whole list, one confirmation, one record per key the checkout can prove.
|
|
1494
|
+
*/
|
|
1495
|
+
export function commandResolve(argv, streams, cwd, deps = {}) {
|
|
1496
|
+
const outcomeFront = front(argv, {
|
|
1497
|
+
...COMMON_FLAGS,
|
|
1498
|
+
"--outcome": "string",
|
|
1499
|
+
"--note": "string",
|
|
1500
|
+
"--as": "string",
|
|
1501
|
+
"--dangling": "boolean",
|
|
1502
|
+
"--class": "string",
|
|
1503
|
+
"--yes": "boolean",
|
|
1504
|
+
}, RESOLVE_HELP, streams, cwd);
|
|
1505
|
+
if (outcomeFront.kind === "handled")
|
|
1506
|
+
return outcomeFront.code;
|
|
1507
|
+
const { flags, positionals, json, logPath } = outcomeFront;
|
|
1508
|
+
// The actor is settled before the two forms diverge: both write a
|
|
1509
|
+
// human-attested record and neither may be performed by an agent.
|
|
1510
|
+
const asFlag0 = stringFlag(flags, "--as");
|
|
1511
|
+
const actor0 = resolveHumanActor(asFlag0 === null ? {} : { actor: asFlag0 });
|
|
1512
|
+
const bulk = boolFlag(flags, "--dangling");
|
|
1513
|
+
const actionKey = positionals[0];
|
|
1514
|
+
if (bulk) {
|
|
1515
|
+
if (actionKey !== undefined) {
|
|
1516
|
+
return usageError(streams, json, `--dangling takes no <action-key>: it acts on every dangling execution the log holds, and naming one is the single form (drop --dangling). Got ${JSON.stringify(actionKey)}`, RESOLVE_HELP);
|
|
1517
|
+
}
|
|
1518
|
+
if (stringFlag(flags, "--outcome") !== null || stringFlag(flags, "--note") !== null) {
|
|
1519
|
+
return usageError(streams, json, "--dangling takes neither --outcome nor --note: it records `completed` for exactly the keys this checkout can prove completed, and writes each note from that evidence. Nothing is inferred for a key nothing proves — those are listed and left alone", RESOLVE_HELP);
|
|
1520
|
+
}
|
|
1521
|
+
if (actor0 === null) {
|
|
1522
|
+
return usageError(streams, json, asFlag0 === null
|
|
1523
|
+
? `no human identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id>`
|
|
1524
|
+
: `--as expects a human identity matching human:<id>, got ${JSON.stringify(asFlag0)}; resolve records what a person observed and an agent: or system: actor cannot perform it`, RESOLVE_HELP);
|
|
1525
|
+
}
|
|
1526
|
+
return resolveDangling(streams, cwd, outcomeFront, actor0, deps);
|
|
1527
|
+
}
|
|
1528
|
+
if (stringFlag(flags, "--class") !== null || boolFlag(flags, "--yes")) {
|
|
1529
|
+
return usageError(streams, json, "--class and --yes belong to the bulk form: they select and confirm a LIST, and the single form already names its one key and takes its one note. Add --dangling, or drop them", RESOLVE_HELP);
|
|
1530
|
+
}
|
|
1531
|
+
if (actionKey === undefined) {
|
|
1532
|
+
return usageError(streams, json, "missing <action-key> argument (or --dangling for every dangling execution at once)", RESOLVE_HELP);
|
|
1533
|
+
}
|
|
1534
|
+
const extra = positionals[1];
|
|
1535
|
+
if (extra !== undefined) {
|
|
1536
|
+
return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, RESOLVE_HELP);
|
|
1537
|
+
}
|
|
1538
|
+
const outcomeFlag = stringFlag(flags, "--outcome");
|
|
1539
|
+
if (outcomeFlag === null) {
|
|
1540
|
+
return usageError(streams, json, "missing --outcome completed|failed", RESOLVE_HELP);
|
|
1541
|
+
}
|
|
1542
|
+
if (outcomeFlag !== "completed" && outcomeFlag !== "failed") {
|
|
1543
|
+
return usageError(streams, json, `--outcome expects completed or failed, got ${JSON.stringify(outcomeFlag)}; nothing is inferred from a dangling execution`, RESOLVE_HELP);
|
|
1544
|
+
}
|
|
1545
|
+
const outcome = outcomeFlag;
|
|
1546
|
+
const note = stringFlag(flags, "--note");
|
|
1547
|
+
if (note === null || note.trim().length === 0) {
|
|
1548
|
+
return usageError(streams, json, note === null
|
|
1549
|
+
? "missing --note \"<what you observed>\": resolve records a human observation, and an unexplained attested outcome cannot be told apart from a guess"
|
|
1550
|
+
: "--note must not be empty: resolve records a human observation, and an unexplained attested outcome cannot be told apart from a guess", RESOLVE_HELP);
|
|
1551
|
+
}
|
|
1552
|
+
const asFlag = stringFlag(flags, "--as");
|
|
1553
|
+
const actor = resolveHumanActor(asFlag === null ? {} : { actor: asFlag });
|
|
1554
|
+
if (actor === null) {
|
|
1555
|
+
if (asFlag !== null) {
|
|
1556
|
+
return usageError(streams, json, `--as expects a human identity matching human:<id>, got ${JSON.stringify(asFlag)}; resolve records what a person observed and an agent: or system: actor cannot perform it`, RESOLVE_HELP);
|
|
1557
|
+
}
|
|
1558
|
+
return usageError(streams, json, `no human identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id>`, RESOLVE_HELP);
|
|
1559
|
+
}
|
|
1560
|
+
const result = resolveExecution(logPath, actionKey, outcome, note, actor, {
|
|
1561
|
+
policy: policyLocation(flags, cwd),
|
|
1562
|
+
});
|
|
1563
|
+
if (!result.ok)
|
|
1564
|
+
return emitRefusal(streams, json, result);
|
|
1565
|
+
if (json) {
|
|
1566
|
+
emitJson(streams, {
|
|
1567
|
+
ok: true,
|
|
1568
|
+
action_key: actionKey,
|
|
1569
|
+
task: result.task,
|
|
1570
|
+
event: result.event,
|
|
1571
|
+
outcome: result.outcome,
|
|
1572
|
+
seq: result.record.seq,
|
|
1573
|
+
attested_by_human: true,
|
|
1574
|
+
actor,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
streams.out(`resolved ${actionKey} as ${result.outcome} at seq ${result.record.seq} by ${actor} (human-attested, no exit code)\n`);
|
|
1579
|
+
}
|
|
1580
|
+
return EXIT_OK;
|
|
1581
|
+
}
|
|
1582
|
+
// ===========================================================================
|
|
1583
|
+
// approval execution reconcile
|
|
1584
|
+
// ===========================================================================
|
|
1585
|
+
/**
|
|
1586
|
+
* `approval execution reconcile <action-key> --resolution executed|not-executed
|
|
1587
|
+
* --note …`
|
|
1588
|
+
*
|
|
1589
|
+
* The human resolution of an INDETERMINATE execution (APRV-120): the side
|
|
1590
|
+
* effect was attempted, the runtime could not tell whether it committed, and a
|
|
1591
|
+
* person went and looked at the far side.
|
|
1592
|
+
*
|
|
1593
|
+
* It is a separate verb from `resolve` rather than a third `--outcome` value,
|
|
1594
|
+
* because the two answer different questions from different evidence. `resolve`
|
|
1595
|
+
* asks "what did our runtime do?" and is answered from this machine. This asks
|
|
1596
|
+
* "did the provider commit?" and is answered from the provider's own console,
|
|
1597
|
+
* inbox or ledger. An operator who reached for the wrong one is told so
|
|
1598
|
+
* (`not-indeterminate`, `already-finished`) rather than quietly writing the
|
|
1599
|
+
* wrong record into an append-only log.
|
|
1600
|
+
*
|
|
1601
|
+
* The same three rules `resolve` enforces, enforced here before core is called:
|
|
1602
|
+
* the resolution is one of two closed values and nothing is inferred, the note
|
|
1603
|
+
* is mandatory and non-empty because it is the evidence, and the actor must be
|
|
1604
|
+
* a human — the daemon never auto-resolves, and an agent reconciling its own
|
|
1605
|
+
* unknown outcome is the executing party reporting on itself.
|
|
1606
|
+
*/
|
|
1607
|
+
export function commandReconcile(argv, streams, cwd) {
|
|
1608
|
+
const outcomeFront = front(argv, { ...COMMON_FLAGS, "--resolution": "string", "--note": "string", "--as": "string" }, RECONCILE_HELP, streams, cwd);
|
|
1609
|
+
if (outcomeFront.kind === "handled")
|
|
1610
|
+
return outcomeFront.code;
|
|
1611
|
+
const { flags, positionals, json, logPath } = outcomeFront;
|
|
1612
|
+
const actionKey = positionals[0];
|
|
1613
|
+
if (actionKey === undefined) {
|
|
1614
|
+
return usageError(streams, json, "missing <action-key> argument", RECONCILE_HELP);
|
|
1615
|
+
}
|
|
1616
|
+
const extra = positionals[1];
|
|
1617
|
+
if (extra !== undefined) {
|
|
1618
|
+
return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, RECONCILE_HELP);
|
|
1619
|
+
}
|
|
1620
|
+
const resolutionFlag = stringFlag(flags, "--resolution");
|
|
1621
|
+
if (resolutionFlag === null) {
|
|
1622
|
+
return usageError(streams, json, "missing --resolution executed|not-executed", RECONCILE_HELP);
|
|
1623
|
+
}
|
|
1624
|
+
if (!isReconcileResolution(resolutionFlag)) {
|
|
1625
|
+
return usageError(streams, json, `--resolution expects executed or not-executed, got ${JSON.stringify(resolutionFlag)}; nothing is inferred about an outcome the runtime could not observe`, RECONCILE_HELP);
|
|
1626
|
+
}
|
|
1627
|
+
const note = stringFlag(flags, "--note");
|
|
1628
|
+
if (note === null || note.trim().length === 0) {
|
|
1629
|
+
return usageError(streams, json, note === null
|
|
1630
|
+
? 'missing --note "<the evidence>": reconcile records what a person established about an unknown outcome, and an unexplained resolution cannot be told apart from a guess'
|
|
1631
|
+
: "--note must not be empty: reconcile records what a person established about an unknown outcome, and an unexplained resolution cannot be told apart from a guess", RECONCILE_HELP);
|
|
1632
|
+
}
|
|
1633
|
+
const asFlag = stringFlag(flags, "--as");
|
|
1634
|
+
const actor = resolveHumanActor(asFlag === null ? {} : { actor: asFlag });
|
|
1635
|
+
if (actor === null) {
|
|
1636
|
+
if (asFlag !== null) {
|
|
1637
|
+
return usageError(streams, json, `--as expects a human identity matching human:<id>, got ${JSON.stringify(asFlag)}; reconcile records what a person established and an agent: or system: actor cannot perform it`, RECONCILE_HELP);
|
|
1638
|
+
}
|
|
1639
|
+
return usageError(streams, json, `no human identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id>`, RECONCILE_HELP);
|
|
1640
|
+
}
|
|
1641
|
+
const result = reconcileExecution(logPath, actionKey, resolutionFlag, note, actor, {
|
|
1642
|
+
policy: policyLocation(flags, cwd),
|
|
1643
|
+
});
|
|
1644
|
+
if (!result.ok)
|
|
1645
|
+
return emitRefusal(streams, json, result);
|
|
1646
|
+
if (json) {
|
|
1647
|
+
emitJson(streams, {
|
|
1648
|
+
ok: true,
|
|
1649
|
+
action_key: actionKey,
|
|
1650
|
+
task: result.task,
|
|
1651
|
+
event: "execution.reconciled",
|
|
1652
|
+
resolution: result.resolution,
|
|
1653
|
+
indeterminate_seq: result.indeterminateSeq,
|
|
1654
|
+
seq: result.record.seq,
|
|
1655
|
+
attested_by_human: true,
|
|
1656
|
+
actor,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
streams.out(`reconciled ${actionKey} as ${result.resolution} at seq ${result.record.seq} by ${actor}, resolving the indeterminate outcome at seq ${result.indeterminateSeq} (human-attested; the idempotency key stays spent)\n`);
|
|
1661
|
+
}
|
|
1662
|
+
return EXIT_OK;
|
|
1663
|
+
}
|
|
1664
|
+
/** `approval execution <subcommand>`: `resolve` and `reconcile`. */
|
|
1665
|
+
export function commandExecution(argv, streams, cwd, deps = {}) {
|
|
1666
|
+
const sub = argv[0];
|
|
1667
|
+
const rest = argv.slice(1);
|
|
1668
|
+
const json = argv.includes("--json");
|
|
1669
|
+
if (sub === undefined) {
|
|
1670
|
+
return usageError(streams, json, "missing subcommand for `approval execution`", EXECUTION_HELP);
|
|
1671
|
+
}
|
|
1672
|
+
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1673
|
+
streams.out(`${EXECUTION_HELP}\n`);
|
|
1674
|
+
return EXIT_OK;
|
|
1675
|
+
}
|
|
1676
|
+
if (sub === "resolve")
|
|
1677
|
+
return commandResolve(rest, streams, cwd, deps);
|
|
1678
|
+
if (sub === "reconcile")
|
|
1679
|
+
return commandReconcile(rest, streams, cwd);
|
|
1680
|
+
return usageError(streams, json, `unknown subcommand ${JSON.stringify(sub)} for \`approval execution\``, EXECUTION_HELP);
|
|
1681
|
+
}
|
|
1682
|
+
//# sourceMappingURL=execute.js.map
|