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,1009 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived state: the one place the runtime turns the log into answers
|
|
3
|
+
* (SPEC.md §6.3, §7, §8).
|
|
4
|
+
*
|
|
5
|
+
* Everything the gate, the token module, and the executor believe about an
|
|
6
|
+
* action comes from here. The module exists for two reasons, both structural,
|
|
7
|
+
* both from the APRV-20 review:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Verified reads (finding S1).** {@link readVerifiedRecords} is the only
|
|
10
|
+
* sanctioned way for a decision-making module to read the log. It runs the
|
|
11
|
+
* *full* chain verification — hash recompute, schema validation, `prev`/`seq`
|
|
12
|
+
* walk — by calling `core/verify.ts`'s {@link verifyWithRecords}, and refuses
|
|
13
|
+
* a corrupt log outright. Before this, the gate parsed lines as JSON and
|
|
14
|
+
* trusted them: a forged or spliced record could authorize an action, and the
|
|
15
|
+
* corruption would surface only when someone ran `approval log verify`. A
|
|
16
|
+
* permission system that reads its own evidence without checking it is not a
|
|
17
|
+
* permission system.
|
|
18
|
+
*
|
|
19
|
+
* APRV-43 lifted the linear cost that S1 accepted; see "The verified-read
|
|
20
|
+
* cache" below. What it did not lift is the rule: a decision is still made
|
|
21
|
+
* only from records this process verified in full, against bytes it proved
|
|
22
|
+
* unchanged.
|
|
23
|
+
*
|
|
24
|
+
* 2. **One derivation, no cycle (finding S4).** {@link requestState} used to
|
|
25
|
+
* live in `core/gate.ts`, which `core/token.ts` imported — while
|
|
26
|
+
* `core/gate.ts` imported `core/token.ts` to mint at grant. That import cycle
|
|
27
|
+
* made "which module owns approval state?" unanswerable. The derivation lives
|
|
28
|
+
* here now; `gate.ts`, `token.ts`, and `execute.ts` all import it, and the
|
|
29
|
+
* only remaining edge between them is the intended one, gate → token, at the
|
|
30
|
+
* mint seam. `gate.ts` re-exports the moved names so existing importers (the
|
|
31
|
+
* CLI, the tests) are unaffected.
|
|
32
|
+
*
|
|
33
|
+
* ## The verified-read cache (APRV-43), and why it is not a bypass
|
|
34
|
+
*
|
|
35
|
+
* A daemon re-reads the log on every watch event. Re-verifying from genesis
|
|
36
|
+
* every time makes a session quadratic in the log it is watching, so
|
|
37
|
+
* {@link readVerifiedRecords} keeps a process-lifetime, memory-only cache of the
|
|
38
|
+
* last log it verified clean: the prefix bytes' length, a SHA-256 over those
|
|
39
|
+
* bytes, the head line's bytes at their offset, the chain head, and the records
|
|
40
|
+
* the walk produced. On the next read of the same path, a prefix proved
|
|
41
|
+
* byte-identical is not re-walked; only the appended suffix is verified, chained
|
|
42
|
+
* onto the cached head.
|
|
43
|
+
*
|
|
44
|
+
* **Global invariant 1 ("enforcement paths read only verified records") is what
|
|
45
|
+
* this touches, so the argument is written out rather than assumed.**
|
|
46
|
+
*
|
|
47
|
+
* The tempting design is the cheap one: remember the head line and its offset,
|
|
48
|
+
* and on a re-read accept the prefix if the head line is byte-identical where it
|
|
49
|
+
* was. That design is unsound, and the specific attack says why. Append-only
|
|
50
|
+
* growth is a convention the *writer* honors; it is not a property of the file.
|
|
51
|
+
* An attacker with write access can mutate a record strictly before the head
|
|
52
|
+
* without changing its length — swap two characters of a `summary`, flip a digit
|
|
53
|
+
* of `est_cost_usd` — leaving the file size, the head line, and the head line's
|
|
54
|
+
* offset all identical. Nothing in the suffix walk touches those bytes. The
|
|
55
|
+
* forged record would be handed to the gate as verified, and a hash chain would
|
|
56
|
+
* have been defeated by a cache. "Byte-identical head at the same offset" does
|
|
57
|
+
* not imply "unchanged prefix", and no amount of `stat` makes it imply that.
|
|
58
|
+
*
|
|
59
|
+
* So the cache pays for what it claims: it stores a SHA-256 over the entire
|
|
60
|
+
* verified prefix and **re-hashes those bytes on every cached read**. A match
|
|
61
|
+
* proves the prefix on disk is bit-for-bit the prefix this process verified in
|
|
62
|
+
* full, in this process lifetime. Verification is a pure function of (bytes,
|
|
63
|
+
* schema files, options) — `core/verify.ts` says so and holds no state — so
|
|
64
|
+
* identical bytes verify identically, and replaying the walk over them could
|
|
65
|
+
* only reproduce the records already held. The cache therefore never *admits* a
|
|
66
|
+
* record: it declines to recompute a conclusion it has already computed from
|
|
67
|
+
* bytes it has just re-proved. Every record the caller receives was walked
|
|
68
|
+
* through the full check ladder (parse, `alg`, schema, hash recompute, `seq`
|
|
69
|
+
* succession, `prev` link) by this process, over exactly these bytes.
|
|
70
|
+
*
|
|
71
|
+
* **APRV-217 makes the last sentence conditional, and only under a policy that
|
|
72
|
+
* says so.** Under the default proof (`full`) the paragraph above is exactly
|
|
73
|
+
* what happens on every cached read. Under `incremental`, an operator's policy
|
|
74
|
+
* trades "re-hash the whole prefix on every read" for "hash the appended bytes,
|
|
75
|
+
* and re-hash the whole prefix on a cadence" — same guards, same walk, same
|
|
76
|
+
* verdicts, a bounded window in which an in-place rewrite of the prefix would be
|
|
77
|
+
* served from cache. The argument for that trade, and what it costs, is in the
|
|
78
|
+
* "incremental prefix proof" section below and in
|
|
79
|
+
* `docs/proposals/incremental-prefix-proof.md`.
|
|
80
|
+
*
|
|
81
|
+
* That is still a large win, because the two costs are not comparable: hashing
|
|
82
|
+
* bytes is a single linear pass at memory bandwidth, while the walk it replaces
|
|
83
|
+
* is a JSON parse, an Ajv schema validation, a JCS canonicalization, and a
|
|
84
|
+
* SHA-256 *per record*. The cache trades a full re-verification for a hash of
|
|
85
|
+
* the same bytes and a real verification of the new tail.
|
|
86
|
+
*
|
|
87
|
+
* Everything else the cache records is a discard trigger and never a licence:
|
|
88
|
+
*
|
|
89
|
+
* - **Size** shrinking below the cached prefix discards the entry. A shorter
|
|
90
|
+
* file cannot contain the prefix, and a truncated log must be re-read cold so
|
|
91
|
+
* that the head reported is the file's head, never the remembered one.
|
|
92
|
+
* - **mtime** is a staleness hint with no evidentiary weight. It can only cause
|
|
93
|
+
* a discard (a same-size file whose mtime moved is suspicious), never skip a
|
|
94
|
+
* check. Correctness does not depend on its granularity, on the clock being
|
|
95
|
+
* monotonic, or on the filesystem storing it at all: delete the mtime
|
|
96
|
+
* comparison and the cache is exactly as sound.
|
|
97
|
+
* - **The head line's bytes at their recorded offset** are compared before the
|
|
98
|
+
* prefix hash. This is a fast rejection of the ordinary tamper, not the proof;
|
|
99
|
+
* the prefix hash covers those same bytes and is what the soundness argument
|
|
100
|
+
* rests on.
|
|
101
|
+
* - **The schema directory** is part of the key. Records verified against one
|
|
102
|
+
* schema set are not evidence under another.
|
|
103
|
+
*
|
|
104
|
+
* Only a `clean` verdict populates the cache. A torn tail or a corrupt log
|
|
105
|
+
* leaves the previous entry in place unused (it is discarded on the mismatch
|
|
106
|
+
* that revealed the damage), so nothing derived from a broken read can ever be
|
|
107
|
+
* resumed from. And the cache is memory-only and process-lifetime: no file, no
|
|
108
|
+
* shared state between processes, nothing an attacker can pre-seed. A CLI
|
|
109
|
+
* invocation is a fresh process with an empty cache and behaves exactly as it
|
|
110
|
+
* did before this existed.
|
|
111
|
+
*
|
|
112
|
+
* Records handed out from the cache are deep-frozen, because a caller that
|
|
113
|
+
* mutated a returned record would otherwise corrupt the next reader's evidence.
|
|
114
|
+
* Freezing makes the aliasing safe instead of merely unlikely.
|
|
115
|
+
*
|
|
116
|
+
* Determinism: `requestState` and everything downstream of it read no clock, no
|
|
117
|
+
* network, and no cache; `ts` is a parameter everywhere, so a derivation can be
|
|
118
|
+
* replayed from the log exactly as it was made. The cache sits strictly above
|
|
119
|
+
* that line, in the read path, and is observationally invisible: for the same
|
|
120
|
+
* bytes it returns the same records and the same verdict as a cold read, which
|
|
121
|
+
* `tests/state-cache.test.ts` asserts scenario by scenario.
|
|
122
|
+
*/
|
|
123
|
+
import { createHash } from "node:crypto";
|
|
124
|
+
import { closeSync, fstatSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
125
|
+
import { resolve } from "node:path";
|
|
126
|
+
import { isPolicySha256, POLICY_HASH_FIELD } from "./attest.js";
|
|
127
|
+
import { onLogAppended } from "./log.js";
|
|
128
|
+
import { normalizeUsd } from "./money.js";
|
|
129
|
+
import { isPayloadHash } from "./payload.js";
|
|
130
|
+
import { publishVerifiedPrefix, snapshotPrefix } from "./verified-snapshot.js";
|
|
131
|
+
import { verifyText, verifyWithRecords, } from "./verify.js";
|
|
132
|
+
/**
|
|
133
|
+
* Whether this process may resume a read behind a published snapshot
|
|
134
|
+
* (APRV-188).
|
|
135
|
+
*
|
|
136
|
+
* Off by default, so the daemon, every CLI verb, the channels and
|
|
137
|
+
* `approval log verify` read exactly as they did before this existed. It is
|
|
138
|
+
* turned on by one caller, `approval hook`, which is the short-lived process
|
|
139
|
+
* whose empty cache pays for the walk.
|
|
140
|
+
*
|
|
141
|
+
* A process-wide switch rather than a per-call option on purpose. A hook's
|
|
142
|
+
* first verified read is followed by several more from inside `core/gate.ts`,
|
|
143
|
+
* which threads no options of its own; if the switch were an option only the
|
|
144
|
+
* first read could carry it, and the value is in the FIRST read of a process,
|
|
145
|
+
* with the rest served from the cache the first one seeded.
|
|
146
|
+
*/
|
|
147
|
+
let snapshotReads = false;
|
|
148
|
+
/**
|
|
149
|
+
* The proof this process's cached reads run when a call names none (APRV-217).
|
|
150
|
+
*
|
|
151
|
+
* `null` means {@link FULL_READ_PROOF}, and that is what every process starts
|
|
152
|
+
* with: a CLI verb, a hook, a test, and a daemon before its own startup line.
|
|
153
|
+
*
|
|
154
|
+
* It is a process-wide switch for the reason `snapshotReads` above is one. The
|
|
155
|
+
* daemon's tick reads are only some of the reads its process makes: the queue
|
|
156
|
+
* renderer and the pending-queue builder read the same log through call paths
|
|
157
|
+
* that thread no options of their own, and a per-call option could not reach
|
|
158
|
+
* them. What sets this is `approval daemon run` / `approval up`, once, from the
|
|
159
|
+
* mode they printed on the `started` line. Nothing an agent runs sets it.
|
|
160
|
+
*/
|
|
161
|
+
let processReadProof = null;
|
|
162
|
+
/**
|
|
163
|
+
* Set (or clear, with `null`) the default proof for this process's reads.
|
|
164
|
+
*
|
|
165
|
+
* Called by exactly one kind of caller: the two long-lived operator verbs, at
|
|
166
|
+
* startup, after they have resolved the flag against the policy.
|
|
167
|
+
*/
|
|
168
|
+
export function useReadProof(proof) {
|
|
169
|
+
processReadProof = proof;
|
|
170
|
+
}
|
|
171
|
+
/** The default proof in force here. Diagnostics and tests. */
|
|
172
|
+
export function readProofInForce() {
|
|
173
|
+
return processReadProof ?? FULL_READ_PROOF;
|
|
174
|
+
}
|
|
175
|
+
/** Opt this process into (or out of) snapshot-resumed reads. */
|
|
176
|
+
export function useVerifiedSnapshots(enabled) {
|
|
177
|
+
snapshotReads = enabled;
|
|
178
|
+
}
|
|
179
|
+
/** Whether snapshot-resumed reads are enabled here. Diagnostics and tests. */
|
|
180
|
+
export function verifiedSnapshotsEnabled() {
|
|
181
|
+
return snapshotReads;
|
|
182
|
+
}
|
|
183
|
+
function refuseRead(code, message) {
|
|
184
|
+
return { ok: false, code, message };
|
|
185
|
+
}
|
|
186
|
+
function errorMessage(cause) {
|
|
187
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
188
|
+
}
|
|
189
|
+
/** The head of a record list: the last record's `(seq, hash)`, or `null`. */
|
|
190
|
+
export function headOf(records) {
|
|
191
|
+
const last = records[records.length - 1];
|
|
192
|
+
return last === undefined ? null : { seq: last.seq, hash: last.hash };
|
|
193
|
+
}
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// The verified-read cache
|
|
196
|
+
//
|
|
197
|
+
// The soundness argument lives in the module header. The code below is the
|
|
198
|
+
// mechanical part: what is remembered, what discards it, and what a surviving
|
|
199
|
+
// entry is allowed to save.
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
const NEWLINE = 0x0a;
|
|
202
|
+
/** Reads between full re-proofs under `incremental` (SPEC-signed default). */
|
|
203
|
+
export const DEFAULT_FULL_REPROOF_EVERY = 50;
|
|
204
|
+
/** Milliseconds between full re-proofs under `incremental`. */
|
|
205
|
+
export const DEFAULT_FULL_REPROOF_AFTER_MS = 60_000;
|
|
206
|
+
/**
|
|
207
|
+
* What a reader gets when it asks for nothing: today's proof on every read.
|
|
208
|
+
*
|
|
209
|
+
* The default is `full` because it is the behaviour this repository has today
|
|
210
|
+
* and the one an operator has attested to. `incremental` is reached only by a
|
|
211
|
+
* caller that was handed a policy (or a flag) saying so.
|
|
212
|
+
*/
|
|
213
|
+
export const FULL_READ_PROOF = Object.freeze({
|
|
214
|
+
mode: "full",
|
|
215
|
+
everyReads: DEFAULT_FULL_REPROOF_EVERY,
|
|
216
|
+
afterMs: DEFAULT_FULL_REPROOF_AFTER_MS,
|
|
217
|
+
});
|
|
218
|
+
/**
|
|
219
|
+
* Bytes fed to SHA-256 by this module since the last reset.
|
|
220
|
+
*
|
|
221
|
+
* The one seam the APRV-217 tests need: "the incremental path hashes only the
|
|
222
|
+
* appended bytes" is a claim about work, and work is invisible in a result by
|
|
223
|
+
* design. A wall-clock assertion would measure the machine instead of the code,
|
|
224
|
+
* so the tests count bytes here. Nothing in the runtime reads it.
|
|
225
|
+
*/
|
|
226
|
+
let hashedBytes = 0;
|
|
227
|
+
/** Bytes hashed by the verified-read cache so far. Diagnostics and tests. */
|
|
228
|
+
export function hashedByteCount() {
|
|
229
|
+
return hashedBytes;
|
|
230
|
+
}
|
|
231
|
+
/** Reset {@link hashedByteCount}. Tests only. */
|
|
232
|
+
export function resetHashedByteCount() {
|
|
233
|
+
hashedBytes = 0;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* How many distinct logs one process remembers. A process reads one log; a test
|
|
237
|
+
* process reads many, and an unbounded map would hold every record of every
|
|
238
|
+
* scratch log for the life of the run. Eviction is by insertion order and costs
|
|
239
|
+
* only a cold read.
|
|
240
|
+
*/
|
|
241
|
+
const MAX_CACHED_LOGS = 8;
|
|
242
|
+
function sha256(bytes) {
|
|
243
|
+
hashedBytes += bytes.length;
|
|
244
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
245
|
+
}
|
|
246
|
+
/** A SHA-256 state over `bytes`, counted for {@link hashedByteCount}. */
|
|
247
|
+
function hashState(bytes) {
|
|
248
|
+
hashedBytes += bytes.length;
|
|
249
|
+
return createHash("sha256").update(bytes);
|
|
250
|
+
}
|
|
251
|
+
/** Feed `bytes` to `state`, counted. Mutates and returns the state given. */
|
|
252
|
+
function feedHash(state, bytes) {
|
|
253
|
+
hashedBytes += bytes.length;
|
|
254
|
+
state.update(bytes);
|
|
255
|
+
return state;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Is a full re-proof owed for `entry`?
|
|
259
|
+
*
|
|
260
|
+
* Every answer here is "prove more", never "prove less": the `true` branches
|
|
261
|
+
* send the read down the whole-file path, which is today's behaviour exactly.
|
|
262
|
+
*/
|
|
263
|
+
function fullReproofDue(entry, proof, now) {
|
|
264
|
+
if (entry.forceFullReproof)
|
|
265
|
+
return true;
|
|
266
|
+
if (entry.hashState === null)
|
|
267
|
+
return true;
|
|
268
|
+
// `everyReads` counts the reads a single full pass may cover, the anchoring
|
|
269
|
+
// read included: at 1 every read re-proves in full, at 50 one pass anchors
|
|
270
|
+
// itself and the 49 reads that follow it.
|
|
271
|
+
if (entry.readsSinceFullReproof + 1 >= proof.everyReads)
|
|
272
|
+
return true;
|
|
273
|
+
return now - entry.lastFullReproofAt >= proof.afterMs;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Read exactly `into.length` bytes at `position`. Returns false for a short
|
|
277
|
+
* read, which is a file that changed under us and therefore a guard failure.
|
|
278
|
+
*/
|
|
279
|
+
function readExactly(fd, into, position) {
|
|
280
|
+
let done = 0;
|
|
281
|
+
while (done < into.length) {
|
|
282
|
+
let got;
|
|
283
|
+
try {
|
|
284
|
+
got = readSync(fd, into, done, into.length - done, position + done);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (got <= 0)
|
|
290
|
+
return false;
|
|
291
|
+
done += got;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Freeze a record and everything reachable from it.
|
|
297
|
+
*
|
|
298
|
+
* Cached records are handed to every subsequent reader. A caller that mutated
|
|
299
|
+
* one would be rewriting another reader's evidence in place, which is the one
|
|
300
|
+
* way a memory cache could forge a record that no log ever contained. Freezing
|
|
301
|
+
* turns that from a convention into a `TypeError`.
|
|
302
|
+
*/
|
|
303
|
+
function deepFreeze(value) {
|
|
304
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
305
|
+
return;
|
|
306
|
+
Object.freeze(value);
|
|
307
|
+
for (const inner of Object.values(value))
|
|
308
|
+
deepFreeze(inner);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Process-lifetime, memory-only store of last-verified log state.
|
|
312
|
+
*
|
|
313
|
+
* Nothing here is written to disk and nothing is shared between processes. An
|
|
314
|
+
* instance is safe to construct per caller (the daemon may want its own); the
|
|
315
|
+
* default is {@link processReadCache}, which is why a repeat reader gets the
|
|
316
|
+
* acceleration without asking for it and a one-shot CLI process cannot notice
|
|
317
|
+
* it exists.
|
|
318
|
+
*/
|
|
319
|
+
export class VerifiedReadCache {
|
|
320
|
+
#entries = new Map();
|
|
321
|
+
#hits = 0;
|
|
322
|
+
#misses = 0;
|
|
323
|
+
/**
|
|
324
|
+
* Misses that were served from a published snapshot instead of a cold walk
|
|
325
|
+
* (APRV-188). Counted alongside the miss it followed rather than instead of
|
|
326
|
+
* it: the process cache genuinely had nothing, and the walk was skipped only
|
|
327
|
+
* because another process's verification was re-proved over these bytes.
|
|
328
|
+
*/
|
|
329
|
+
#resumed = 0;
|
|
330
|
+
/**
|
|
331
|
+
* Reads that hashed the whole prefix — a full digest compare, or a cold walk
|
|
332
|
+
* (APRV-217). Equal to `hits + misses` under `full`, which is the point: it
|
|
333
|
+
* is how a tick line says which path its reads took.
|
|
334
|
+
*/
|
|
335
|
+
#fullReproofs = 0;
|
|
336
|
+
/** Forget everything. Tests use this to force a genuinely cold read. */
|
|
337
|
+
clear() {
|
|
338
|
+
this.#entries.clear();
|
|
339
|
+
this.#hits = 0;
|
|
340
|
+
this.#misses = 0;
|
|
341
|
+
this.#resumed = 0;
|
|
342
|
+
this.#fullReproofs = 0;
|
|
343
|
+
}
|
|
344
|
+
/** How many logs are remembered. Diagnostics and tests only. */
|
|
345
|
+
get size() {
|
|
346
|
+
return this.#entries.size;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Reads that reused a proved prefix, and reads that verified from genesis.
|
|
350
|
+
*
|
|
351
|
+
* Diagnostics, and the one way a test can tell the two paths apart: reusing a
|
|
352
|
+
* prefix is *designed* to be invisible in the result, so a test that wants to
|
|
353
|
+
* assert "this tamper discarded the cache" has nothing else to look at.
|
|
354
|
+
*/
|
|
355
|
+
get stats() {
|
|
356
|
+
return {
|
|
357
|
+
hits: this.#hits,
|
|
358
|
+
misses: this.#misses,
|
|
359
|
+
resumed: this.#resumed,
|
|
360
|
+
fullReproofs: this.#fullReproofs,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Require a full re-proof of `logPath` on this cache's next read (APRV-217).
|
|
365
|
+
*
|
|
366
|
+
* Called by `core/log.ts` after a successful append, through the listener it
|
|
367
|
+
* registers below. Can only add work: the next read hashes the whole prefix
|
|
368
|
+
* exactly as a `full` read does.
|
|
369
|
+
*/
|
|
370
|
+
requireFullReproof(logPath) {
|
|
371
|
+
const entry = this.#entries.get(resolve(logPath));
|
|
372
|
+
if (entry !== undefined)
|
|
373
|
+
entry.forceFullReproof = true;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Verify `logPath`, reusing a proved-identical prefix when there is one.
|
|
377
|
+
*
|
|
378
|
+
* The whole file is read once, and every decision is made from that single
|
|
379
|
+
* snapshot: nothing is re-`stat`ed and re-read behind its own conclusion.
|
|
380
|
+
*/
|
|
381
|
+
read(logPath, options, snapshot = {}, proof = FULL_READ_PROOF) {
|
|
382
|
+
const key = resolve(logPath);
|
|
383
|
+
const entry = this.#entries.get(key);
|
|
384
|
+
// The incremental path (APRV-217), taken only when every precondition holds
|
|
385
|
+
// and the cadence has not come due. It reads the head line and the appended
|
|
386
|
+
// bytes and nothing else; on ANY guard failure it returns `null` and this
|
|
387
|
+
// read falls back to the whole-file path below, which is a cold walk.
|
|
388
|
+
if (proof.mode === "incremental" &&
|
|
389
|
+
entry !== undefined &&
|
|
390
|
+
entry.schemaKey === (options.schemaDir === undefined ? "" : resolve(options.schemaDir)) &&
|
|
391
|
+
!fullReproofDue(entry, proof, Date.now())) {
|
|
392
|
+
const incremental = this.#readTail(logPath, key, entry, options, snapshot);
|
|
393
|
+
if (incremental !== null)
|
|
394
|
+
return incremental;
|
|
395
|
+
}
|
|
396
|
+
return this.#readWhole(logPath, key, options, snapshot, proof.mode);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Today's read, unchanged: the whole file, the whole prefix hash, the walk.
|
|
400
|
+
*
|
|
401
|
+
* Every `full` read lands here, and so does every `incremental` read whose
|
|
402
|
+
* cadence came due or whose guards rejected. The only APRV-217 addition is
|
|
403
|
+
* the hash state handed to {@link VerifiedReadCache.#remember}, which is
|
|
404
|
+
* built from the same single pass over the bytes rather than a second one.
|
|
405
|
+
*/
|
|
406
|
+
#readWhole(logPath, key, options, snapshot, mode) {
|
|
407
|
+
this.#fullReproofs += 1;
|
|
408
|
+
let raw;
|
|
409
|
+
try {
|
|
410
|
+
raw = readFileSync(logPath);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// An absent or unreadable log is not this module's vocabulary: hand it to
|
|
414
|
+
// the cold path, which owns the ENOENT-is-an-empty-log rule and the exact
|
|
415
|
+
// failure messages. A log we cannot read is also a log we must forget.
|
|
416
|
+
this.#entries.delete(key);
|
|
417
|
+
this.#misses += 1;
|
|
418
|
+
return verifyWithRecords(logPath, options);
|
|
419
|
+
}
|
|
420
|
+
let mtimeMs = Number.NaN;
|
|
421
|
+
try {
|
|
422
|
+
mtimeMs = statSync(logPath).mtimeMs;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// No mtime is simply no hint; an entry that wanted one is discarded below.
|
|
426
|
+
}
|
|
427
|
+
const schemaKey = options.schemaDir === undefined ? "" : resolve(options.schemaDir);
|
|
428
|
+
const entry = this.#entries.get(key);
|
|
429
|
+
const cached = entry === undefined ? null : reusablePrefix(entry, raw, schemaKey, mtimeMs);
|
|
430
|
+
// A published snapshot is consulted only where this process has nothing:
|
|
431
|
+
// the in-process proof always wins, because it is the stronger one (these
|
|
432
|
+
// records were walked here). See `core/verified-snapshot.ts` for what the
|
|
433
|
+
// weaker one costs and what it is allowed to skip.
|
|
434
|
+
let prefix = cached;
|
|
435
|
+
/** The digest of the endorsed prefix, already re-proved. */
|
|
436
|
+
let provedDigest = null;
|
|
437
|
+
if (cached === null) {
|
|
438
|
+
this.#entries.delete(key);
|
|
439
|
+
this.#misses += 1;
|
|
440
|
+
if (snapshot.consume === true) {
|
|
441
|
+
const admitted = snapshotPrefix(logPath, raw, options.schemaDir);
|
|
442
|
+
if (admitted.ok) {
|
|
443
|
+
prefix = admitted.prefix;
|
|
444
|
+
this.#resumed += 1;
|
|
445
|
+
// `admitSnapshot` hashed exactly these bytes a moment ago, so when the
|
|
446
|
+
// file has not grown past the endorsed prefix the entry below may
|
|
447
|
+
// carry that digest instead of hashing the same megabytes twice —
|
|
448
|
+
// the APRV-206 argument, with the proof coming from the admission.
|
|
449
|
+
if (raw.length === admitted.prefix.byteLength)
|
|
450
|
+
provedDigest = admitted.digest;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
this.#hits += 1;
|
|
456
|
+
}
|
|
457
|
+
const text = prefix === null ? raw.toString("utf8") : raw.toString("utf8", prefix.byteLength);
|
|
458
|
+
const verified = verifyText(logPath, text, options, prefix);
|
|
459
|
+
// APRV-206. When the file has not grown since the entry that was just
|
|
460
|
+
// re-proved, the digest of these bytes is the digest that entry holds: the
|
|
461
|
+
// prefix hash covered the whole file, and `reusablePrefix` has just shown
|
|
462
|
+
// the file is those same bytes. Re-deriving it would hash the same megabytes
|
|
463
|
+
// a second time in one read, which on a repeat reader (the listener between
|
|
464
|
+
// taps) is the larger half of the read. Nothing is admitted on trust: this
|
|
465
|
+
// is only reached when the hash comparison above passed.
|
|
466
|
+
const known = entry !== undefined && cached !== null && raw.length === entry.byteLength
|
|
467
|
+
? entry.prefixHash
|
|
468
|
+
: provedDigest;
|
|
469
|
+
// The digest of these bytes, computed at most ONCE per read and shared by
|
|
470
|
+
// everything that wants it (APRV-211). Only a clean read has anything to
|
|
471
|
+
// remember or to publish, so a torn or corrupt log is never hashed here at
|
|
472
|
+
// all, and `#remember` still owns the rule about which reads qualify.
|
|
473
|
+
const result = verified.result;
|
|
474
|
+
if (result.status === "clean" && result.head !== null) {
|
|
475
|
+
// Under `full` this is today's line, unchanged. Under `incremental` the
|
|
476
|
+
// entry must also carry the un-finalised state at these bytes, so the
|
|
477
|
+
// digest is taken FROM that state: still one pass over the file, never
|
|
478
|
+
// two, and the state is reused outright when the file has not grown.
|
|
479
|
+
let carried;
|
|
480
|
+
let digest;
|
|
481
|
+
if (entry !== undefined && cached !== null && raw.length === entry.byteLength) {
|
|
482
|
+
// The file has not grown since an entry whose bytes were just re-proved:
|
|
483
|
+
// its digest stands, and so does the state it carries. A `full` read
|
|
484
|
+
// carries that state forward untouched rather than dropping it, so a
|
|
485
|
+
// reader that mixes modes (the daemon's tick reads and the queue
|
|
486
|
+
// renderer's, in one process) does not thrash the anchor.
|
|
487
|
+
digest = entry.prefixHash;
|
|
488
|
+
carried =
|
|
489
|
+
entry.hashState ?? (mode === "incremental" ? hashState(raw) : null);
|
|
490
|
+
}
|
|
491
|
+
else if (known !== null) {
|
|
492
|
+
// A snapshot admission proved these bytes and hashed them elsewhere.
|
|
493
|
+
// There is no state to carry without a second pass, so none is kept;
|
|
494
|
+
// the next read anchors one.
|
|
495
|
+
digest = known;
|
|
496
|
+
carried = null;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
// The one hash of these bytes this read pays, in both modes: `full`
|
|
500
|
+
// spent exactly this before APRV-217, through `sha256(raw)`.
|
|
501
|
+
carried = hashState(raw);
|
|
502
|
+
digest = carried.copy().digest("hex");
|
|
503
|
+
}
|
|
504
|
+
this.#remember(key, raw, schemaKey, mtimeMs, verified, digest, carried);
|
|
505
|
+
// The publisher is handed the digest rather than left to recompute it: it
|
|
506
|
+
// endorses exactly the bytes this read proved, and a second hash of the
|
|
507
|
+
// same megabytes was the larger half of a daemon tick.
|
|
508
|
+
if (snapshot.publish === true) {
|
|
509
|
+
publishVerifiedPrefix(logPath, raw.length, raw.length > 0 && raw[raw.length - 1] === NEWLINE, digest, result.records, result.head, options.schemaDir);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
this.#remember(key, raw, schemaKey, mtimeMs, verified, known, null);
|
|
514
|
+
}
|
|
515
|
+
return verified;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* The incremental read (APRV-217): the head line and the appended bytes.
|
|
519
|
+
*
|
|
520
|
+
* Returns `null` for every guard failure, which sends the caller to the
|
|
521
|
+
* whole-file path — a cold walk, the same fallback a mismatched prefix hash
|
|
522
|
+
* has always taken. Nothing here can produce a verdict the cold walk would
|
|
523
|
+
* not: the tail is handed to the same `verifyText`, chained onto the same
|
|
524
|
+
* cached head, with the same schema options.
|
|
525
|
+
*/
|
|
526
|
+
#readTail(logPath, key, entry, options, snapshot) {
|
|
527
|
+
let fd;
|
|
528
|
+
try {
|
|
529
|
+
fd = openSync(logPath, "r");
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
let size;
|
|
536
|
+
let mtimeMs;
|
|
537
|
+
try {
|
|
538
|
+
const stats = fstatSync(fd);
|
|
539
|
+
if (!stats.isFile())
|
|
540
|
+
return null;
|
|
541
|
+
size = stats.size;
|
|
542
|
+
mtimeMs = stats.mtimeMs;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
// Guards 2 to 4 of the design's ladder, in the order `reusablePrefix`
|
|
548
|
+
// runs them, over the same facts. Guard 1 (the schema key) was answered
|
|
549
|
+
// by the caller; guard 5 (the full digest) is what the cadence decides,
|
|
550
|
+
// and this path is reached only when it is not due.
|
|
551
|
+
if (size < entry.byteLength)
|
|
552
|
+
return null;
|
|
553
|
+
if (size === entry.byteLength && mtimeMs !== entry.mtimeMs)
|
|
554
|
+
return null;
|
|
555
|
+
const headLineEnd = entry.headLineStart + entry.headLine.length;
|
|
556
|
+
if (headLineEnd + 1 !== entry.byteLength)
|
|
557
|
+
return null;
|
|
558
|
+
const headBytes = Buffer.allocUnsafe(entry.headLine.length + 1);
|
|
559
|
+
if (!readExactly(fd, headBytes, entry.headLineStart))
|
|
560
|
+
return null;
|
|
561
|
+
if (headBytes[headBytes.length - 1] !== NEWLINE)
|
|
562
|
+
return null;
|
|
563
|
+
if (!headBytes.subarray(0, entry.headLine.length).equals(entry.headLine))
|
|
564
|
+
return null;
|
|
565
|
+
const tail = Buffer.allocUnsafe(size - entry.byteLength);
|
|
566
|
+
if (tail.length > 0 && !readExactly(fd, tail, entry.byteLength))
|
|
567
|
+
return null;
|
|
568
|
+
// Step 6: the appended bytes go to a COPY of the state, and the copy
|
|
569
|
+
// becomes the state of the entry this read leaves behind. The digest of
|
|
570
|
+
// the whole file falls out of it, so the snapshot publisher and the entry
|
|
571
|
+
// below are served without hashing a byte twice.
|
|
572
|
+
const state = entry.hashState === null ? null : entry.hashState.copy();
|
|
573
|
+
if (state === null)
|
|
574
|
+
return null;
|
|
575
|
+
if (tail.length > 0)
|
|
576
|
+
feedHash(state, tail);
|
|
577
|
+
const digest = state.copy().digest("hex");
|
|
578
|
+
this.#hits += 1;
|
|
579
|
+
const prefix = {
|
|
580
|
+
byteLength: entry.byteLength,
|
|
581
|
+
lines: entry.lines,
|
|
582
|
+
head: entry.head,
|
|
583
|
+
records: entry.records,
|
|
584
|
+
};
|
|
585
|
+
const verified = verifyText(logPath, tail.toString("utf8"), options, prefix);
|
|
586
|
+
const result = verified.result;
|
|
587
|
+
if (result.status !== "clean" || result.head === null) {
|
|
588
|
+
// The same rule the whole-file path applies: nothing is resumed from a
|
|
589
|
+
// read that found damage, and the entry that led here is dropped.
|
|
590
|
+
this.#entries.delete(key);
|
|
591
|
+
return verified;
|
|
592
|
+
}
|
|
593
|
+
// The head line of the file as it now stands. With no appended bytes it
|
|
594
|
+
// is the one the entry already holds; otherwise it is the last line of
|
|
595
|
+
// the tail, whose first byte follows the newline before it.
|
|
596
|
+
let headLineStart = entry.headLineStart;
|
|
597
|
+
let headLine = entry.headLine;
|
|
598
|
+
if (tail.length > 0) {
|
|
599
|
+
const relative = tail.lastIndexOf(NEWLINE, tail.length - 2) + 1;
|
|
600
|
+
headLineStart = entry.byteLength + relative;
|
|
601
|
+
headLine = Buffer.from(tail.subarray(relative, tail.length - 1));
|
|
602
|
+
}
|
|
603
|
+
const records = [...verified.records];
|
|
604
|
+
for (const record of records)
|
|
605
|
+
deepFreeze(record);
|
|
606
|
+
this.#store(key, {
|
|
607
|
+
schemaKey: entry.schemaKey,
|
|
608
|
+
byteLength: size,
|
|
609
|
+
lines: result.records,
|
|
610
|
+
prefixHash: digest,
|
|
611
|
+
headLineStart,
|
|
612
|
+
headLine,
|
|
613
|
+
head: result.head,
|
|
614
|
+
records,
|
|
615
|
+
mtimeMs,
|
|
616
|
+
hashState: state,
|
|
617
|
+
// The cadence carries forward: this read did not re-prove the prefix,
|
|
618
|
+
// so it does not re-anchor the clock or the count.
|
|
619
|
+
lastFullReproofAt: entry.lastFullReproofAt,
|
|
620
|
+
readsSinceFullReproof: entry.readsSinceFullReproof + 1,
|
|
621
|
+
forceFullReproof: false,
|
|
622
|
+
});
|
|
623
|
+
if (snapshot.publish === true) {
|
|
624
|
+
publishVerifiedPrefix(logPath, size, true, digest, result.records, result.head, options.schemaDir);
|
|
625
|
+
}
|
|
626
|
+
return verified;
|
|
627
|
+
}
|
|
628
|
+
finally {
|
|
629
|
+
try {
|
|
630
|
+
closeSync(fd);
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// Nothing actionable: the read is done and the descriptor is the OS's.
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Record a clean read. Only `clean` qualifies: a torn or corrupt log has no
|
|
639
|
+
* prefix this module is willing to resume from, and any entry it had was
|
|
640
|
+
* already dropped by the mismatch that exposed the damage.
|
|
641
|
+
*/
|
|
642
|
+
#remember(key, raw, schemaKey, mtimeMs, verified,
|
|
643
|
+
/**
|
|
644
|
+
* The digest of exactly these bytes, when the caller already re-proved them
|
|
645
|
+
* against a stored one (APRV-206). `null` means "hash them".
|
|
646
|
+
*/
|
|
647
|
+
knownDigest,
|
|
648
|
+
/**
|
|
649
|
+
* The un-finalised SHA-256 state at these bytes (APRV-217), or `null` under
|
|
650
|
+
* `full`, where no state is kept and nothing is spent building one.
|
|
651
|
+
*/
|
|
652
|
+
state) {
|
|
653
|
+
const result = verified.result;
|
|
654
|
+
if (result.status !== "clean" || result.head === null) {
|
|
655
|
+
this.#entries.delete(key);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// A clean, non-empty log ends with a newline, so the head line runs from
|
|
659
|
+
// just after the previous newline to the last byte.
|
|
660
|
+
const headLineStart = raw.lastIndexOf(NEWLINE, raw.length - 2) + 1;
|
|
661
|
+
const records = [...verified.records];
|
|
662
|
+
for (const record of records)
|
|
663
|
+
deepFreeze(record);
|
|
664
|
+
this.#store(key, {
|
|
665
|
+
schemaKey,
|
|
666
|
+
byteLength: raw.length,
|
|
667
|
+
lines: result.records,
|
|
668
|
+
prefixHash: knownDigest ?? sha256(raw),
|
|
669
|
+
headLineStart,
|
|
670
|
+
headLine: Buffer.from(raw.subarray(headLineStart, raw.length - 1)),
|
|
671
|
+
head: result.head,
|
|
672
|
+
records,
|
|
673
|
+
mtimeMs,
|
|
674
|
+
hashState: state,
|
|
675
|
+
// This read hashed the whole prefix, so it IS the anchor: the cadence
|
|
676
|
+
// starts again here, and any pending force is discharged.
|
|
677
|
+
lastFullReproofAt: Date.now(),
|
|
678
|
+
readsSinceFullReproof: 0,
|
|
679
|
+
forceFullReproof: false,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/** Put `entry` in, evicting by insertion order. The one writer of the map. */
|
|
683
|
+
#store(key, entry) {
|
|
684
|
+
this.#entries.delete(key);
|
|
685
|
+
if (this.#entries.size >= MAX_CACHED_LOGS) {
|
|
686
|
+
const oldest = this.#entries.keys().next();
|
|
687
|
+
if (!oldest.done)
|
|
688
|
+
this.#entries.delete(oldest.value);
|
|
689
|
+
}
|
|
690
|
+
this.#entries.set(key, entry);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Decide whether `entry` may stand for the first `entry.byteLength` bytes of
|
|
695
|
+
* `raw`, and return the resume point when it may.
|
|
696
|
+
*
|
|
697
|
+
* The checks run cheapest-first, but only the last one is load-bearing: the
|
|
698
|
+
* prefix hash. Everything above it exists to reject an obviously changed file
|
|
699
|
+
* without hashing it, and every check can only *reject*.
|
|
700
|
+
*/
|
|
701
|
+
function reusablePrefix(entry, raw, schemaKey, mtimeMs) {
|
|
702
|
+
// Records verified against one schema set are not evidence under another.
|
|
703
|
+
if (entry.schemaKey !== schemaKey)
|
|
704
|
+
return null;
|
|
705
|
+
// The file shrank: it cannot contain the prefix, and a truncated log must be
|
|
706
|
+
// re-read cold so the head reported is the file's, not the one remembered.
|
|
707
|
+
if (raw.length < entry.byteLength)
|
|
708
|
+
return null;
|
|
709
|
+
// Same size, moved mtime: an in-place rewrite. The prefix hash would catch it
|
|
710
|
+
// anyway; this rejects it a pass earlier. mtime never admits anything.
|
|
711
|
+
if (raw.length === entry.byteLength && mtimeMs !== entry.mtimeMs)
|
|
712
|
+
return null;
|
|
713
|
+
// The head line, byte for byte, where it was recorded — and still terminated,
|
|
714
|
+
// and still the end of the prefix.
|
|
715
|
+
const headLineEnd = entry.headLineStart + entry.headLine.length;
|
|
716
|
+
if (headLineEnd + 1 !== entry.byteLength)
|
|
717
|
+
return null;
|
|
718
|
+
if (raw[headLineEnd] !== NEWLINE)
|
|
719
|
+
return null;
|
|
720
|
+
if (!raw.subarray(entry.headLineStart, headLineEnd).equals(entry.headLine))
|
|
721
|
+
return null;
|
|
722
|
+
// The proof: these are the bytes this process verified in full.
|
|
723
|
+
if (sha256(raw.subarray(0, entry.byteLength)) !== entry.prefixHash)
|
|
724
|
+
return null;
|
|
725
|
+
return {
|
|
726
|
+
byteLength: entry.byteLength,
|
|
727
|
+
lines: entry.lines,
|
|
728
|
+
head: entry.head,
|
|
729
|
+
records: entry.records,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* The cache {@link readVerifiedRecords} uses when a caller names none.
|
|
734
|
+
*
|
|
735
|
+
* Shared per process, which is what makes a watch loop fast without any wiring:
|
|
736
|
+
* the daemon calls `readVerifiedRecords` exactly as every other consumer does.
|
|
737
|
+
*/
|
|
738
|
+
export const processReadCache = new VerifiedReadCache();
|
|
739
|
+
/**
|
|
740
|
+
* Tell the process cache when this process appends (APRV-217).
|
|
741
|
+
*
|
|
742
|
+
* Registered here rather than called from `core/log.ts` because the dependency
|
|
743
|
+
* runs this way: `core/state.ts` already knows about the log, and a value
|
|
744
|
+
* import in the other direction would close a cycle through `core/verify.ts`.
|
|
745
|
+
* The listener can only ADD work — the named log's next read hashes its whole
|
|
746
|
+
* prefix — so a process that never loads this module simply reads as it always
|
|
747
|
+
* did.
|
|
748
|
+
*/
|
|
749
|
+
onLogAppended((logPath) => {
|
|
750
|
+
processReadCache.requireFullReproof(logPath);
|
|
751
|
+
});
|
|
752
|
+
/**
|
|
753
|
+
* Read the log and refuse unless the whole chain verifies.
|
|
754
|
+
*
|
|
755
|
+
* An absent log is an empty log (nothing has happened yet), exactly as
|
|
756
|
+
* `appendEvent` and `approval log verify` treat it.
|
|
757
|
+
*
|
|
758
|
+
* The file is opened once as a readability probe *before* verification so that
|
|
759
|
+
* "I could not open this file" stays an I/O fact (`log-unreadable`) and never
|
|
760
|
+
* arrives dressed as corruption — the same split the CLI's exit table draws, and
|
|
761
|
+
* the one thing `verify()` alone cannot express, since from inside the chain
|
|
762
|
+
* walker an unreadable log is indistinguishable from a broken one.
|
|
763
|
+
*
|
|
764
|
+
* Torn-tail behavior is unchanged from the pre-APRV-20 reader: the tear is
|
|
765
|
+
* reported as `log-torn-tail` and nothing is repaired, because truncating a torn
|
|
766
|
+
* line is a human decision.
|
|
767
|
+
*
|
|
768
|
+
* Reads go through the verified-read cache by default (see the module header):
|
|
769
|
+
* a prefix re-proved byte-identical is not re-walked, the appended suffix is
|
|
770
|
+
* verified in full, and any mismatch falls back to genesis. The result is the
|
|
771
|
+
* result of a cold read on the same bytes, always. `cache: null` opts out.
|
|
772
|
+
*/
|
|
773
|
+
export function readVerifiedRecords(logPath, options = {}) {
|
|
774
|
+
try {
|
|
775
|
+
closeSync(openSync(logPath, "r"));
|
|
776
|
+
}
|
|
777
|
+
catch (cause) {
|
|
778
|
+
if (cause.code === "ENOENT") {
|
|
779
|
+
return { ok: true, records: [], head: null };
|
|
780
|
+
}
|
|
781
|
+
return refuseRead("log-unreadable", `log ${logPath} could not be read: ${errorMessage(cause)}`);
|
|
782
|
+
}
|
|
783
|
+
const { cache: requested, publishSnapshot: publish, readProof, ...verifyOptions } = options;
|
|
784
|
+
const cache = requested === undefined ? processReadCache : requested;
|
|
785
|
+
// `cache: null` is the explicit cold read an audit asks for, and it stays
|
|
786
|
+
// cold: a caller that opted out of this process's own proved prefix has not
|
|
787
|
+
// opted into another process's.
|
|
788
|
+
const verified = cache === null
|
|
789
|
+
? verifyWithRecords(logPath, verifyOptions)
|
|
790
|
+
: cache.read(logPath, verifyOptions, {
|
|
791
|
+
consume: snapshotReads,
|
|
792
|
+
...(publish === undefined ? {} : { publish }),
|
|
793
|
+
}, readProof ?? processReadProof ?? FULL_READ_PROOF);
|
|
794
|
+
switch (verified.result.status) {
|
|
795
|
+
case "clean":
|
|
796
|
+
return { ok: true, records: verified.records, head: verified.result.head };
|
|
797
|
+
case "torn-tail":
|
|
798
|
+
return refuseRead("log-torn-tail", `log ${logPath} ends without a newline: the final record is truncated, the signature of a crashed write. Nothing is repaired here; run \`approval log verify\`.`);
|
|
799
|
+
case "corrupt":
|
|
800
|
+
return refuseRead("log-corrupt", `log ${logPath} does not verify (${verified.result.reason}${verified.result.firstBadSeq === null ? "" : ` at seq ${verified.result.firstBadSeq}`}): ${verified.result.message}. Nothing may be authorized from a log that does not verify; run \`approval log verify\`.`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Why a requester retracted a pending request (amended SPEC.md §6.3, APRV-106).
|
|
805
|
+
*
|
|
806
|
+
* Closed, and mirrored by `event.schema.json`: a withdrawal that does not say
|
|
807
|
+
* why is a fact audit cannot use. `timeout` is the one the hook writes — the
|
|
808
|
+
* requester stopped waiting, so a decision on this request can no longer be
|
|
809
|
+
* consumed by anyone.
|
|
810
|
+
*/
|
|
811
|
+
export const WITHDRAW_REASONS = ["timeout", "cancelled", "superseded"];
|
|
812
|
+
/** Is `value` one of the closed withdrawal reasons? */
|
|
813
|
+
export function isWithdrawReason(value) {
|
|
814
|
+
return typeof value === "string" && WITHDRAW_REASONS.includes(value);
|
|
815
|
+
}
|
|
816
|
+
/** A record's payload as a map, or `{}` when it has none. */
|
|
817
|
+
export function payloadOf(record) {
|
|
818
|
+
const payload = record.payload;
|
|
819
|
+
return typeof payload === "object" && payload !== null ? payload : {};
|
|
820
|
+
}
|
|
821
|
+
function declaredFrom(record) {
|
|
822
|
+
const payload = payloadOf(record);
|
|
823
|
+
const cls = payload["class"];
|
|
824
|
+
const cost = payload["est_cost_usd"];
|
|
825
|
+
const reversible = payload["reversible"];
|
|
826
|
+
const summary = payload["summary"];
|
|
827
|
+
const hash = payload["payload_hash"];
|
|
828
|
+
const execution = payload["execution"];
|
|
829
|
+
const waitUntil = payload["wait_until"];
|
|
830
|
+
const policySha256 = payload[POLICY_HASH_FIELD];
|
|
831
|
+
return {
|
|
832
|
+
class: typeof cls === "string" ? cls : null,
|
|
833
|
+
est_cost_usd: normalizeUsd(cost),
|
|
834
|
+
reversible: typeof reversible === "boolean" ? reversible : null,
|
|
835
|
+
summary: typeof summary === "string" ? summary : null,
|
|
836
|
+
payload_hash: isPayloadHash(hash) ? hash : null,
|
|
837
|
+
// Recognized values only. An unrecognized `execution` reads as `null`,
|
|
838
|
+
// which is the ordinary token-minting path: a claim the runtime does not
|
|
839
|
+
// understand must not change what the runtime does.
|
|
840
|
+
execution: execution === "harness" ? "harness" : null,
|
|
841
|
+
wait_until: typeof waitUntil === "string" && !Number.isNaN(Date.parse(waitUntil)) ? waitUntil : null,
|
|
842
|
+
// A malformed hash reads as `null`, which is the pre-APRV-118 shape: the
|
|
843
|
+
// grant path then has nothing to compare and proceeds under the current
|
|
844
|
+
// policy. Treating an unreadable value as a mismatch would let a corrupt
|
|
845
|
+
// byte void a pending request, and treating it as a match would let a
|
|
846
|
+
// crafted one claim agreement it cannot prove.
|
|
847
|
+
policy_sha256: isPolicySha256(policySha256) ? policySha256 : null,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Derive `actionKey`'s approval state from `records`.
|
|
852
|
+
*
|
|
853
|
+
* Pure: no I/O, no clock. `ts` is the moment the question is being asked and is
|
|
854
|
+
* **required** — lazy expiry is arithmetic on it, and a state function that read
|
|
855
|
+
* the clock could not be replayed.
|
|
856
|
+
*
|
|
857
|
+
* Sequencing rules, all of them deliberate:
|
|
858
|
+
*
|
|
859
|
+
* - An `approval.requested` **resets** the derivation. A key that was rejected
|
|
860
|
+
* or expired may be requested again; the new request starts a fresh cycle and
|
|
861
|
+
* the old decision no longer governs. (An action that has *executed* is a
|
|
862
|
+
* different matter — `core/gate.ts`'s `request` refuses that on idempotency
|
|
863
|
+
* grounds.)
|
|
864
|
+
* - The **first** decision after a request wins. The gate refuses to append a
|
|
865
|
+
* second one, so a log carrying two is a log written by something else; the
|
|
866
|
+
* fail-closed reading is that the earliest human decision stands rather than
|
|
867
|
+
* that a later append can overwrite it.
|
|
868
|
+
* - Execution facts accumulate across the whole log for the key, independent of
|
|
869
|
+
* the request cycle: an action that has executed has executed, and no
|
|
870
|
+
* subsequent request un-executes it.
|
|
871
|
+
* - Withdrawal (APRV-106): an `approval.withdrawn` settles the request like any
|
|
872
|
+
* other terminal event, and is the only one of them that records no decision.
|
|
873
|
+
* It participates in the "first settlement wins" rule above, so a withdrawal
|
|
874
|
+
* appended after a human's answer does not erase the answer; the gate refuses
|
|
875
|
+
* to append one at all in that case.
|
|
876
|
+
* - Expiry: an `approval.expired` record sets `expiredByEvent`. With no such
|
|
877
|
+
* record, `ttlMs !== null` and `ts > requestTs + ttlMs` sets `expiredLazily`.
|
|
878
|
+
* Both yield `state: "expired"`. An unparseable `requestTs` or `ts` also
|
|
879
|
+
* yields `expired`: liveness that cannot be demonstrated is not assumed. (The
|
|
880
|
+
* event schema's `date-time` format makes that unreachable through the real
|
|
881
|
+
* append path; it is a backstop, not a live branch.)
|
|
882
|
+
*/
|
|
883
|
+
export function requestState(records, actionKey, ts, ttlMs) {
|
|
884
|
+
let task = null;
|
|
885
|
+
let requestSeq = null;
|
|
886
|
+
let requestTs = null;
|
|
887
|
+
let requestActor = null;
|
|
888
|
+
let decision = null;
|
|
889
|
+
let decisionSeq = null;
|
|
890
|
+
let decisionTs = null;
|
|
891
|
+
let expiredByEvent = false;
|
|
892
|
+
let declared = {
|
|
893
|
+
class: null,
|
|
894
|
+
est_cost_usd: null,
|
|
895
|
+
reversible: null,
|
|
896
|
+
summary: null,
|
|
897
|
+
payload_hash: null,
|
|
898
|
+
execution: null,
|
|
899
|
+
wait_until: null,
|
|
900
|
+
policy_sha256: null,
|
|
901
|
+
};
|
|
902
|
+
const execution = { started: null, completed: null, failed: null };
|
|
903
|
+
const settle = (record, value) => {
|
|
904
|
+
if (requestSeq === null)
|
|
905
|
+
return;
|
|
906
|
+
// Revocation is the one decision that legitimately follows another: a
|
|
907
|
+
// human withdraws a grant they already made, so `approval.revoked`
|
|
908
|
+
// supersedes. Every other decision settles only an undecided request —
|
|
909
|
+
// the gate refuses to append a second one, and a log carrying two was
|
|
910
|
+
// written by something else, where the fail-closed reading is that the
|
|
911
|
+
// earliest human answer stands rather than that a later append overwrites it.
|
|
912
|
+
if (decision !== null && value !== "revoked")
|
|
913
|
+
return;
|
|
914
|
+
decision = value;
|
|
915
|
+
decisionSeq = record.seq;
|
|
916
|
+
decisionTs = record.ts;
|
|
917
|
+
if (value === "expired")
|
|
918
|
+
expiredByEvent = true;
|
|
919
|
+
};
|
|
920
|
+
for (const record of records) {
|
|
921
|
+
if (record.action_key !== actionKey)
|
|
922
|
+
continue;
|
|
923
|
+
switch (record.event) {
|
|
924
|
+
case "approval.requested":
|
|
925
|
+
task = record.task ?? task;
|
|
926
|
+
requestSeq = record.seq;
|
|
927
|
+
requestTs = record.ts;
|
|
928
|
+
requestActor = record.actor;
|
|
929
|
+
decision = null;
|
|
930
|
+
decisionSeq = null;
|
|
931
|
+
decisionTs = null;
|
|
932
|
+
expiredByEvent = false;
|
|
933
|
+
declared = declaredFrom(record);
|
|
934
|
+
break;
|
|
935
|
+
case "approval.granted":
|
|
936
|
+
settle(record, "granted");
|
|
937
|
+
break;
|
|
938
|
+
case "approval.rejected":
|
|
939
|
+
settle(record, "rejected");
|
|
940
|
+
break;
|
|
941
|
+
case "approval.revoked":
|
|
942
|
+
settle(record, "revoked");
|
|
943
|
+
break;
|
|
944
|
+
case "approval.expired":
|
|
945
|
+
settle(record, "expired");
|
|
946
|
+
break;
|
|
947
|
+
case "approval.withdrawn":
|
|
948
|
+
settle(record, "withdrawn");
|
|
949
|
+
break;
|
|
950
|
+
case "execution.started":
|
|
951
|
+
execution.started = record.seq;
|
|
952
|
+
task = record.task ?? task;
|
|
953
|
+
break;
|
|
954
|
+
case "execution.completed":
|
|
955
|
+
execution.completed = record.seq;
|
|
956
|
+
break;
|
|
957
|
+
case "execution.failed":
|
|
958
|
+
execution.failed = record.seq;
|
|
959
|
+
break;
|
|
960
|
+
default:
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
let state;
|
|
965
|
+
let expiredLazily = false;
|
|
966
|
+
if (requestSeq === null) {
|
|
967
|
+
state = "none";
|
|
968
|
+
}
|
|
969
|
+
else if (decision !== null) {
|
|
970
|
+
state = decision;
|
|
971
|
+
}
|
|
972
|
+
else if (ttlMs === null) {
|
|
973
|
+
// No `defaults.approval_ttl` means the policy declares no lapse. A request
|
|
974
|
+
// stays live until a human decides it; inventing a default TTL here would
|
|
975
|
+
// silently reject approvals a policy author never asked to expire.
|
|
976
|
+
state = "requested";
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
const requestedAt = Date.parse(requestTs ?? "");
|
|
980
|
+
const now = Date.parse(ts);
|
|
981
|
+
if (Number.isNaN(requestedAt) || Number.isNaN(now)) {
|
|
982
|
+
state = "expired";
|
|
983
|
+
expiredLazily = true;
|
|
984
|
+
}
|
|
985
|
+
else if (now > requestedAt + ttlMs) {
|
|
986
|
+
state = "expired";
|
|
987
|
+
expiredLazily = true;
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
state = "requested";
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
actionKey,
|
|
995
|
+
state,
|
|
996
|
+
task,
|
|
997
|
+
requestSeq,
|
|
998
|
+
requestTs,
|
|
999
|
+
requestActor,
|
|
1000
|
+
decision,
|
|
1001
|
+
decisionSeq,
|
|
1002
|
+
decisionTs,
|
|
1003
|
+
expiredByEvent,
|
|
1004
|
+
expiredLazily,
|
|
1005
|
+
declared,
|
|
1006
|
+
execution,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
//# sourceMappingURL=state.js.map
|