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,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-signed log checkpoints (APRV-220).
|
|
3
|
+
*
|
|
4
|
+
* ## The hole this fills
|
|
5
|
+
*
|
|
6
|
+
* The chain in `.approval/log/events.jsonl` is unkeyed, and
|
|
7
|
+
* `docs/proposals/incremental-prefix-proof.md` §3 states the consequence: a
|
|
8
|
+
* process with write access to that file can truncate it and recompute a chain
|
|
9
|
+
* that is self-consistent from genesis. Nothing INSIDE the file contradicts the
|
|
10
|
+
* forgery, so no walk of it ever will. The conformance suite says the same from
|
|
11
|
+
* the other side — `chain-verification/truncation-unanchored` is a boundary
|
|
12
|
+
* vector, and an implementation that claims to catch an unanchored truncation
|
|
13
|
+
* is claiming more than a hash chain can give.
|
|
14
|
+
*
|
|
15
|
+
* §12 of that proposal named three ways out: external anchoring, a keyed chain,
|
|
16
|
+
* and human-signed checkpoints. APRV-219 built the first. This module builds
|
|
17
|
+
* the third.
|
|
18
|
+
*
|
|
19
|
+
* ## How the two witnesses relate
|
|
20
|
+
*
|
|
21
|
+
* They are independent, and neither weakens the other.
|
|
22
|
+
*
|
|
23
|
+
* The ANCHOR (`cli/log-anchor.ts`) asks "does somebody else hold a copy of
|
|
24
|
+
* these bytes?" and answers from git. It is exactly as fresh as the last push,
|
|
25
|
+
* and on a machine with no remote and no records branch it says nothing at all
|
|
26
|
+
* (a skip, never a pass).
|
|
27
|
+
*
|
|
28
|
+
* A CHECKPOINT asks "did a key that no agent process holds sign this head?" and
|
|
29
|
+
* answers from the log itself plus the policy. It works offline, it covers the
|
|
30
|
+
* window since the last push, and it survives being copied to another machine.
|
|
31
|
+
*
|
|
32
|
+
* Against the forger of §3 they fail in different directions, which is the
|
|
33
|
+
* point of having both: the anchor catches a truncation whose records somebody
|
|
34
|
+
* else already holds, and a checkpoint catches a truncation inside the window
|
|
35
|
+
* nobody has pushed yet — because every checkpoint in the rewritten range names
|
|
36
|
+
* a `(seq, hash)` the rewritten chain does not carry, and the forger cannot
|
|
37
|
+
* produce a signature over the hashes they DID recompute.
|
|
38
|
+
*
|
|
39
|
+
* `approval log verify` runs the two independently and reports both. Neither
|
|
40
|
+
* check may be weakened to make the other pass.
|
|
41
|
+
*
|
|
42
|
+
* ## The key
|
|
43
|
+
*
|
|
44
|
+
* A DEDICATED Ed25519 keypair, not the attestation identity. Attestation
|
|
45
|
+
* (`core/attest.ts`) has no keypair to reuse: human identity at v0.1 is
|
|
46
|
+
* config-declared (`--as human:<id>`), and its whole documented claim is that
|
|
47
|
+
* *someone with local control* signed off, not who. A checkpoint has to claim
|
|
48
|
+
* more than that or it claims nothing, because the party it defends against is
|
|
49
|
+
* a process with local control.
|
|
50
|
+
*
|
|
51
|
+
* - The PRIVATE half never appears in the log, in a policy, or in any file an
|
|
52
|
+
* agent may read: it lives in the credential vault (`core/vault.ts`),
|
|
53
|
+
* encrypted at rest under a passphrase `core/child-env.ts` strips from every
|
|
54
|
+
* spawned child, behind a file whose reading classifies `account.credential`
|
|
55
|
+
* (human-only). This module never reads it from anywhere — the caller passes
|
|
56
|
+
* the bytes, so the custody decision lives in one place, the CLI verb.
|
|
57
|
+
* - The PUBLIC half is listed in the policy, `audit.checkpoint_keys`. The
|
|
58
|
+
* policy is the human's own committed, attested artifact: editing it is a
|
|
59
|
+
* visible diff AND de-attests the policy, so gate operations refuse until a
|
|
60
|
+
* human re-attests. That is a materially harder thing to do quietly than
|
|
61
|
+
* rewriting a log line.
|
|
62
|
+
*
|
|
63
|
+
* A list rather than a scalar, so rotation can RETAIN a retired key. A
|
|
64
|
+
* checkpoint signed by a key the policy no longer lists is a refusal here, on
|
|
65
|
+
* purpose (see {@link CHECKPOINT_REFUSAL_CODES}), which makes dropping a key
|
|
66
|
+
* that signed anything a de-verification rather than a cleanup.
|
|
67
|
+
*
|
|
68
|
+
* ## What is signed, and why not more
|
|
69
|
+
*
|
|
70
|
+
* `"approval.md/log-checkpoint/v1\n" + JCS({alg, hash, seq})`. The prefix is
|
|
71
|
+
* domain separation: a signature made here cannot be lifted into any other use
|
|
72
|
+
* of the same key, and a signature made elsewhere cannot be presented as a
|
|
73
|
+
* checkpoint. The head's `hash` is a 256-bit chain digest, so the message is
|
|
74
|
+
* already specific to one chain at one position and needs no further binding.
|
|
75
|
+
*
|
|
76
|
+
* The signature deliberately does NOT cover the rest of the record. It could
|
|
77
|
+
* not: the record's own `hash` covers its payload, which covers the signature.
|
|
78
|
+
* What a checkpoint asserts is exactly "a key holder saw this head" — every
|
|
79
|
+
* other field of the record is covered by the chain, and by the anchor, and by
|
|
80
|
+
* this module refusing a checkpoint whose signed head is not the head the log
|
|
81
|
+
* actually carries.
|
|
82
|
+
*
|
|
83
|
+
* ## Fail closed on a bad signature, fail open on an absent one
|
|
84
|
+
*
|
|
85
|
+
* An invalid signature, an unknown key, or a signed hash the log contradicts is
|
|
86
|
+
* a refusal. A log with no checkpoints at all is not: a human who has been away
|
|
87
|
+
* is not a forger, and a runtime that refused a log for want of a tap would
|
|
88
|
+
* teach its operator to turn the check off. A configured cadence that has
|
|
89
|
+
* lapsed is a WARNING, at every layer, and there is no path in this module from
|
|
90
|
+
* "due" to "refused".
|
|
91
|
+
*
|
|
92
|
+
* A missing PUBLIC KEY is a skip naming why, never a pass — the same rule the
|
|
93
|
+
* anchor check follows for a missing anchor. Nothing has been verified, and
|
|
94
|
+
* reporting silence as a pass is how a check stops being one.
|
|
95
|
+
*/
|
|
96
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, } from "node:crypto";
|
|
97
|
+
import { tick } from "./clock.js";
|
|
98
|
+
import { attemptsOf, withHeadRetry } from "./head-retry.js";
|
|
99
|
+
import { canonicalize } from "./jcs.js";
|
|
100
|
+
import { appendEvent, } from "./log.js";
|
|
101
|
+
import { loadPolicy } from "./policy-load.js";
|
|
102
|
+
import { readVerifiedRecords } from "./state.js";
|
|
103
|
+
/** The one signature scheme at v0.1. Recorded on every checkpoint. */
|
|
104
|
+
export const CHECKPOINT_ALG = "ed25519";
|
|
105
|
+
/** The event type a checkpoint is written as. */
|
|
106
|
+
export const CHECKPOINT_EVENT = "log.checkpoint";
|
|
107
|
+
/**
|
|
108
|
+
* Domain separation for the signed message. A constant, and a versioned one:
|
|
109
|
+
* if the signed shape ever changes, the prefix changes with it, so a signature
|
|
110
|
+
* over the old shape can never be read as one over the new.
|
|
111
|
+
*/
|
|
112
|
+
export const CHECKPOINT_DOMAIN = "approval.md/log-checkpoint/v1";
|
|
113
|
+
/** The credential name the CLI reads the private half from. */
|
|
114
|
+
export const CHECKPOINT_KEY_CREDENTIAL = "approval.checkpoint.key";
|
|
115
|
+
/** Actors permitted to sign a checkpoint, in code as well as in the schema. */
|
|
116
|
+
const HUMAN_ACTOR = /^human:.+/u;
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// The refusal union
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Every way a checkpoint can refuse a range. A closed union per SPEC.md §11.1
|
|
122
|
+
* invariant 6, frozen the way the others are: callers branch on the string.
|
|
123
|
+
*
|
|
124
|
+
* Five codes, because they are five different facts about how a checkpoint
|
|
125
|
+
* failed and they have five different repairs. Collapsing them would leave the
|
|
126
|
+
* one message a person needs — which record, which key, which seq — inside a
|
|
127
|
+
* free-text blob nothing can branch on.
|
|
128
|
+
*
|
|
129
|
+
* `checkpoint-key-unknown` is a refusal rather than a warning, and that is the
|
|
130
|
+
* load-bearing choice in this union. Softening it would hand a forger the
|
|
131
|
+
* escape hatch: rewrite each checkpoint's `key_sha256` to name a key nobody
|
|
132
|
+
* lists, and every refusal in the range becomes a shrug. The cost is that
|
|
133
|
+
* removing a retired key from `audit.checkpoint_keys` stops the checkpoints it
|
|
134
|
+
* signed from verifying, which is why the policy field is a LIST and why
|
|
135
|
+
* APRV-257's rotation verb refuses to drop a key that signed anything.
|
|
136
|
+
*/
|
|
137
|
+
export const CHECKPOINT_REFUSAL_CODES = [
|
|
138
|
+
/**
|
|
139
|
+
* The record names a `key_sha256` that no configured public key hashes to.
|
|
140
|
+
* Either a key was retired out of the policy, or somebody wrote a checkpoint
|
|
141
|
+
* with a key of their own — and this runtime cannot tell which, so it refuses.
|
|
142
|
+
*/
|
|
143
|
+
"checkpoint-key-unknown",
|
|
144
|
+
/**
|
|
145
|
+
* The signature does not verify under the key the record names. The bytes
|
|
146
|
+
* signed are not the bytes presented; nothing further is inferred, because
|
|
147
|
+
* distinguishing "wrong key" from "tampered payload" would be an oracle and
|
|
148
|
+
* the repair is identical.
|
|
149
|
+
*/
|
|
150
|
+
"checkpoint-signature-invalid",
|
|
151
|
+
/**
|
|
152
|
+
* The signature is good and the log disagrees with it: the record at the
|
|
153
|
+
* signed `seq` carries a hash the signature does not name (or the log carries
|
|
154
|
+
* no record at that seq at all). THIS is the forged-chain catch — a chain
|
|
155
|
+
* recomputed after a checkpoint cannot reproduce a signature over the hashes
|
|
156
|
+
* it replaced.
|
|
157
|
+
*/
|
|
158
|
+
"checkpoint-hash-mismatch",
|
|
159
|
+
/**
|
|
160
|
+
* The signed `seq` is not below the checkpoint record's own. A checkpoint
|
|
161
|
+
* signs the past; one naming itself or the future is not a checkpoint, and a
|
|
162
|
+
* runtime that accepted one would accept a record vouching for a head that
|
|
163
|
+
* did not exist when it was written.
|
|
164
|
+
*/
|
|
165
|
+
"checkpoint-out-of-order",
|
|
166
|
+
/**
|
|
167
|
+
* The payload is not a checkpoint payload: a missing field, a hash that is
|
|
168
|
+
* not 64 hex, an `alg` this build does not implement. The write boundary
|
|
169
|
+
* refuses these, so reaching one means the record came from elsewhere — and a
|
|
170
|
+
* `log.checkpoint` nobody can read is not a checkpoint that passes.
|
|
171
|
+
*/
|
|
172
|
+
"checkpoint-malformed",
|
|
173
|
+
];
|
|
174
|
+
/** Mint a checkpoint keypair. Ed25519 from `node:crypto`; no dependency added. */
|
|
175
|
+
export function mintCheckpointKeypair() {
|
|
176
|
+
const pair = generateKeyPairSync("ed25519");
|
|
177
|
+
const spki = pair.publicKey.export({ type: "spki", format: "der" });
|
|
178
|
+
return {
|
|
179
|
+
publicKey: spki.toString("base64"),
|
|
180
|
+
privateKey: pair.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64"),
|
|
181
|
+
fingerprint: createHash("sha256").update(spki).digest("hex"),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* SHA-256 of a public key's DER SPKI **bytes**, hex.
|
|
186
|
+
*
|
|
187
|
+
* Over the bytes rather than over their base64 spelling, so a key that was
|
|
188
|
+
* re-wrapped, re-encoded, or copied through a text editor still fingerprints to
|
|
189
|
+
* the same value. Returns `null` for anything that is not a public key this
|
|
190
|
+
* build can parse — an unreadable key is not a key, and callers turn that into
|
|
191
|
+
* a skip or a refusal rather than into a match against nothing.
|
|
192
|
+
*/
|
|
193
|
+
export function checkpointKeyFingerprint(publicKey) {
|
|
194
|
+
const spki = derOf(publicKey);
|
|
195
|
+
return spki === null ? null : createHash("sha256").update(spki).digest("hex");
|
|
196
|
+
}
|
|
197
|
+
/** The DER SPKI bytes of a base64 public key, or `null` when it will not parse. */
|
|
198
|
+
function derOf(publicKey) {
|
|
199
|
+
try {
|
|
200
|
+
const key = createPublicKey({
|
|
201
|
+
key: Buffer.from(publicKey, "base64"),
|
|
202
|
+
format: "der",
|
|
203
|
+
type: "spki",
|
|
204
|
+
});
|
|
205
|
+
if (key.asymmetricKeyType !== "ed25519")
|
|
206
|
+
return null;
|
|
207
|
+
return key.export({ type: "spki", format: "der" });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The configured public keys, indexed by fingerprint.
|
|
215
|
+
*
|
|
216
|
+
* Keys that do not parse are DROPPED rather than throwing, and the count of
|
|
217
|
+
* what survived is what a caller reports: a policy listing one good key and one
|
|
218
|
+
* typo must still verify the checkpoints the good key signed, and it must not
|
|
219
|
+
* be able to claim the typo verified anything.
|
|
220
|
+
*/
|
|
221
|
+
export function checkpointKeyIndex(publicKeys) {
|
|
222
|
+
const keys = new Map();
|
|
223
|
+
let unreadable = 0;
|
|
224
|
+
for (const key of publicKeys) {
|
|
225
|
+
const fingerprint = checkpointKeyFingerprint(key);
|
|
226
|
+
if (fingerprint === null) {
|
|
227
|
+
unreadable += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
keys.set(fingerprint, key);
|
|
231
|
+
}
|
|
232
|
+
return { keys, unreadable };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The bytes a checkpoint signature covers.
|
|
236
|
+
*
|
|
237
|
+
* JCS (RFC 8785) over `{alg, hash, seq}`, behind {@link CHECKPOINT_DOMAIN} and
|
|
238
|
+
* a newline. Canonical, so two runtimes that agree on the head agree on the
|
|
239
|
+
* bytes; domain-separated, so the signature means one thing.
|
|
240
|
+
*/
|
|
241
|
+
export function checkpointMessage(head) {
|
|
242
|
+
const body = canonicalize({
|
|
243
|
+
alg: head.alg ?? CHECKPOINT_ALG,
|
|
244
|
+
hash: head.hash,
|
|
245
|
+
seq: head.seq,
|
|
246
|
+
});
|
|
247
|
+
return Buffer.from(`${CHECKPOINT_DOMAIN}\n${body}`, "utf8");
|
|
248
|
+
}
|
|
249
|
+
/** Sign a head with a base64 PKCS#8 private key. `null` when the key is unusable. */
|
|
250
|
+
export function signCheckpoint(head, privateKey) {
|
|
251
|
+
try {
|
|
252
|
+
return sign(null, checkpointMessage(head), {
|
|
253
|
+
key: Buffer.from(privateKey, "base64"),
|
|
254
|
+
format: "der",
|
|
255
|
+
type: "pkcs8",
|
|
256
|
+
}).toString("base64");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Does `signature` verify over `head` under `publicKey`? Never throws. */
|
|
263
|
+
export function verifyCheckpointSignature(head, signature, publicKey) {
|
|
264
|
+
try {
|
|
265
|
+
return verify(null, checkpointMessage(head), { key: Buffer.from(publicKey, "base64"), format: "der", type: "spki" }, Buffer.from(signature, "base64"));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const HEX64 = /^[a-f0-9]{64}$/u;
|
|
272
|
+
const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/u;
|
|
273
|
+
/** Is this record a `log.checkpoint`? Type only; the payload is read separately. */
|
|
274
|
+
export function isCheckpointRecord(record) {
|
|
275
|
+
return record.event === CHECKPOINT_EVENT;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Read a checkpoint record's payload, or `null` when it is not one.
|
|
279
|
+
*
|
|
280
|
+
* Strict, and deliberately so: the write boundary already refuses a malformed
|
|
281
|
+
* checkpoint, so a payload that fails here arrived from somewhere else, and the
|
|
282
|
+
* caller's answer to that is {@link CHECKPOINT_REFUSAL_CODES}'s
|
|
283
|
+
* `checkpoint-malformed` rather than a pass.
|
|
284
|
+
*/
|
|
285
|
+
export function readCheckpointPayload(record) {
|
|
286
|
+
const payload = record.payload;
|
|
287
|
+
if (typeof payload !== "object" || payload === null)
|
|
288
|
+
return null;
|
|
289
|
+
const seq = payload["seq"];
|
|
290
|
+
const hash = payload["hash"];
|
|
291
|
+
const alg = payload["alg"];
|
|
292
|
+
const keySha256 = payload["key_sha256"];
|
|
293
|
+
const signature = payload["signature"];
|
|
294
|
+
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1)
|
|
295
|
+
return null;
|
|
296
|
+
if (typeof hash !== "string" || !HEX64.test(hash))
|
|
297
|
+
return null;
|
|
298
|
+
if (alg !== CHECKPOINT_ALG)
|
|
299
|
+
return null;
|
|
300
|
+
if (typeof keySha256 !== "string" || !HEX64.test(keySha256))
|
|
301
|
+
return null;
|
|
302
|
+
if (typeof signature !== "string" || signature.length === 0 || !BASE64.test(signature)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return { seq, hash, alg, keySha256, signature };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Which keys have signed checkpoints in this log, and at which seqs (APRV-257).
|
|
309
|
+
*
|
|
310
|
+
* The question rotation has to ask before it retires anything. Removing a key
|
|
311
|
+
* from `audit.checkpoint_keys` turns every checkpoint it signed into
|
|
312
|
+
* `checkpoint-key-unknown` — a REFUSAL, by the deliberate choice recorded in
|
|
313
|
+
* {@link CHECKPOINT_REFUSAL_CODES} — so a verb that let an operator drop a key
|
|
314
|
+
* casually would be a verb that broke a log's verification to tidy a list.
|
|
315
|
+
*
|
|
316
|
+
* Keyed by the record's self-reported `key_sha256`, which is the only thing
|
|
317
|
+
* `checkLogCheckpoints` looks a key up by, so the answer is exactly the set of
|
|
318
|
+
* names whose removal would change a verdict. Records with an unreadable
|
|
319
|
+
* payload are skipped: they refuse for a different reason already, and no
|
|
320
|
+
* removal makes that better or worse.
|
|
321
|
+
*/
|
|
322
|
+
export function checkpointSignersIn(records) {
|
|
323
|
+
const signers = new Map();
|
|
324
|
+
for (const record of records) {
|
|
325
|
+
if (!isCheckpointRecord(record))
|
|
326
|
+
continue;
|
|
327
|
+
const payload = readCheckpointPayload(record);
|
|
328
|
+
if (payload === null)
|
|
329
|
+
continue;
|
|
330
|
+
const seqs = signers.get(payload.keySha256);
|
|
331
|
+
if (seqs === undefined)
|
|
332
|
+
signers.set(payload.keySha256, [record.seq]);
|
|
333
|
+
else
|
|
334
|
+
seqs.push(record.seq);
|
|
335
|
+
}
|
|
336
|
+
return signers;
|
|
337
|
+
}
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Appending one
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
/** Why a checkpoint was not appended. Nothing was written in any of these. */
|
|
342
|
+
export const CHECKPOINT_APPEND_REFUSAL_CODES = [
|
|
343
|
+
/** The actor is not `human:`-prefixed. Refused here and in the schema. */
|
|
344
|
+
"actor-not-human",
|
|
345
|
+
/** The private key will not parse, or would not sign. */
|
|
346
|
+
"checkpoint-key-unusable",
|
|
347
|
+
/** The log has no records yet: an empty chain has no head to sign. */
|
|
348
|
+
"log-empty",
|
|
349
|
+
/**
|
|
350
|
+
* A caller named a head this log does not carry (APRV-257).
|
|
351
|
+
*
|
|
352
|
+
* Only {@link appendCheckpointAt} can reach it, and only from the tap: the
|
|
353
|
+
* human is shown a `(seq, hash)`, the head moves while the phone is in a
|
|
354
|
+
* pocket, and the record they sign still names the head they SAW. That is
|
|
355
|
+
* allowed — a checkpoint signs any seq below its own — but only while the
|
|
356
|
+
* log actually carries that hash at that seq. When it does not, the thing in
|
|
357
|
+
* front of the human was derived from a different chain than the one being
|
|
358
|
+
* written to, and signing it would mint a checkpoint that
|
|
359
|
+
* `checkpoint-hash-mismatch` refuses forever after.
|
|
360
|
+
*/
|
|
361
|
+
"checkpoint-head-unknown",
|
|
362
|
+
/** The log could not be opened. */
|
|
363
|
+
"log-unreadable",
|
|
364
|
+
/** The log's last line is truncated. Nothing may chain onto it. */
|
|
365
|
+
"log-torn-tail",
|
|
366
|
+
/** The chain does not verify, so no head derived from it may be signed. */
|
|
367
|
+
"log-corrupt",
|
|
368
|
+
/** The append itself was refused; `append` carries the writer's own code. */
|
|
369
|
+
"append-failed",
|
|
370
|
+
];
|
|
371
|
+
/**
|
|
372
|
+
* Append one `log.checkpoint` signing the log's current head.
|
|
373
|
+
*
|
|
374
|
+
* The head is read, then signed, then written with that head as
|
|
375
|
+
* `expectedHead`, so the record cannot land on a chain that moved underneath it
|
|
376
|
+
* (SPEC.md §11.1: every check-then-append passes through compare-and-append).
|
|
377
|
+
* A concurrent append is `head-moved`, and the repair is to run it again — the
|
|
378
|
+
* signature would otherwise vouch for a head that is no longer this record's
|
|
379
|
+
* predecessor, which is a checkpoint that verifies and means less than it looks
|
|
380
|
+
* like it means.
|
|
381
|
+
*
|
|
382
|
+
* `ts` is stamped from the clock at the write boundary and is never a
|
|
383
|
+
* parameter: `log.checkpoint` is gate-typed under amended SPEC.md §8 (A2), and
|
|
384
|
+
* a caller who could choose the moment could backdate the one record whose
|
|
385
|
+
* whole content is a claim about a moment.
|
|
386
|
+
*
|
|
387
|
+
* The private key arrives as a value. This module never reads it from a vault,
|
|
388
|
+
* a file, or an environment variable, so there is exactly one place in the
|
|
389
|
+
* codebase that decides where a checkpoint key may come from, and it is the CLI
|
|
390
|
+
* verb a human runs.
|
|
391
|
+
*/
|
|
392
|
+
export function appendCheckpoint(logPath, privateKey, actor, options = {}) {
|
|
393
|
+
return signAndAppend(logPath, privateKey, actor, null, options);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Append one `log.checkpoint` signing a head the CALLER names (APRV-257).
|
|
397
|
+
*
|
|
398
|
+
* The tap's entry point, and the reason APRV-220's verify rule asks only that a
|
|
399
|
+
* checkpoint signs a seq BELOW its own rather than its immediate predecessor.
|
|
400
|
+
* A human is shown `(seq, hash)` on a phone; by the time they tap, the daemon
|
|
401
|
+
* has appended three records. The honest thing to sign is the head they SAW,
|
|
402
|
+
* because that is what they looked at, and a runtime that quietly re-read the
|
|
403
|
+
* head and signed something else would be putting a human's key over bytes
|
|
404
|
+
* nobody inspected.
|
|
405
|
+
*
|
|
406
|
+
* The named head is checked against the log before anything is signed: it must
|
|
407
|
+
* be a `(seq, hash)` this chain actually carries ({@link
|
|
408
|
+
* CHECKPOINT_APPEND_REFUSAL_CODES}'s `checkpoint-head-unknown`). So a stale
|
|
409
|
+
* prompt from a chain that has since been rewritten cannot be turned into a
|
|
410
|
+
* signature, and the checkpoint that lands is one `checkLogCheckpoints` will
|
|
411
|
+
* accept rather than one it will refuse forever.
|
|
412
|
+
*/
|
|
413
|
+
export function appendCheckpointAt(logPath, privateKey, actor, head, options = {}) {
|
|
414
|
+
return signAndAppend(logPath, privateKey, actor, { seq: head.seq, hash: head.hash }, options);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The one body behind both entry points.
|
|
418
|
+
*
|
|
419
|
+
* `want` is `null` for "whatever the head is when this runs" and a `(seq,
|
|
420
|
+
* hash)` for "the head the human was shown". Everything else — the human-only
|
|
421
|
+
* rule, the verified read, the compare-and-append, the runtime-stamped `ts` —
|
|
422
|
+
* is identical, because the two verbs differ in exactly one decision and
|
|
423
|
+
* writing them twice would be two chances to get the rest of it wrong.
|
|
424
|
+
*/
|
|
425
|
+
function signAndAppend(logPath, privateKey, actor, want, options) {
|
|
426
|
+
return withHeadRetry(attemptsOf(options.attempts), () => signAndAppendOnce(logPath, privateKey, actor, want, options));
|
|
427
|
+
}
|
|
428
|
+
function signAndAppendOnce(logPath, privateKey, actor, want, options) {
|
|
429
|
+
if (!HUMAN_ACTOR.test(actor)) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
code: "actor-not-human",
|
|
433
|
+
message: `signing a checkpoint requires a human actor matching ^human:.+, got ${JSON.stringify(actor)}; a checkpoint is the one witness an agent process is not supposed to be able to produce, and the log was left unchanged`,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const read = readVerifiedRecords(logPath, options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir });
|
|
437
|
+
if (!read.ok) {
|
|
438
|
+
return { ok: false, code: read.code, message: read.message };
|
|
439
|
+
}
|
|
440
|
+
if (read.head === null) {
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
code: "log-empty",
|
|
444
|
+
message: `${logPath} carries no records, and an empty chain has no head to sign; nothing was appended`,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
// The head the record will name: the current one, or the one the human was
|
|
448
|
+
// shown — and in the second case only after this log is asked whether it
|
|
449
|
+
// carries those bytes at that seq. A signature over a head this chain does
|
|
450
|
+
// not have is a checkpoint that refuses for the life of the log.
|
|
451
|
+
let head;
|
|
452
|
+
if (want === null) {
|
|
453
|
+
head = { seq: read.head.seq, hash: read.head.hash };
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const carried = read.records.find((record) => record.seq === want.seq);
|
|
457
|
+
if (carried === undefined || carried.hash !== want.hash) {
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
code: "checkpoint-head-unknown",
|
|
461
|
+
message: `this log does not carry ${want.hash} at seq ${String(want.seq)} (${carried === undefined
|
|
462
|
+
? `it has no record at that seq; its head is seq ${String(read.head.seq)}`
|
|
463
|
+
: `it carries ${carried.hash} there`}). The head you were shown belongs to a different chain than the one being written to, so nothing was signed and nothing was appended`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
head = { seq: want.seq, hash: want.hash };
|
|
467
|
+
}
|
|
468
|
+
const signature = signCheckpoint(head, privateKey);
|
|
469
|
+
if (signature === null) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
code: "checkpoint-key-unusable",
|
|
473
|
+
message: "the checkpoint signing key could not be read as a base64 PKCS#8 Ed25519 private key; nothing was appended",
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const fingerprint = privateKeyFingerprint(privateKey);
|
|
477
|
+
if (fingerprint === null) {
|
|
478
|
+
return {
|
|
479
|
+
ok: false,
|
|
480
|
+
code: "checkpoint-key-unusable",
|
|
481
|
+
message: "the checkpoint signing key signed but its public half could not be derived, so the record could not name which key to verify it against; nothing was appended",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const appended = appendEvent(logPath, {
|
|
485
|
+
ts: tick(options),
|
|
486
|
+
event: CHECKPOINT_EVENT,
|
|
487
|
+
actor,
|
|
488
|
+
channel: options.channel ?? "cli",
|
|
489
|
+
payload: {
|
|
490
|
+
seq: head.seq,
|
|
491
|
+
hash: head.hash,
|
|
492
|
+
alg: CHECKPOINT_ALG,
|
|
493
|
+
key_sha256: fingerprint,
|
|
494
|
+
signature,
|
|
495
|
+
},
|
|
496
|
+
}, {
|
|
497
|
+
...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
|
|
498
|
+
...options.append,
|
|
499
|
+
expectedHead: read.head,
|
|
500
|
+
});
|
|
501
|
+
if (!appended.ok) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
code: "append-failed",
|
|
505
|
+
message: `the checkpoint was not appended: ${appended.error.code}: ${appended.error.message}`,
|
|
506
|
+
append: appended.error,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
// The head this checkpoint SIGNED, which is the head the human was shown and
|
|
510
|
+
// not the head the append landed on. The two are the same only for
|
|
511
|
+
// `appendCheckpoint`; reporting the log's head here would tell the tap's
|
|
512
|
+
// caller that a different `(seq, hash)` was covered than the signature covers.
|
|
513
|
+
return {
|
|
514
|
+
ok: true,
|
|
515
|
+
record: appended.record,
|
|
516
|
+
head: { seq: head.seq, hash: head.hash },
|
|
517
|
+
fingerprint,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** The fingerprint of the public half of a private key, or `null`. */
|
|
521
|
+
export function privateKeyFingerprint(privateKey) {
|
|
522
|
+
try {
|
|
523
|
+
const spki = createPublicKey(createPrivateKey({ key: Buffer.from(privateKey, "base64"), format: "der", type: "pkcs8" })).export({ type: "spki", format: "der" });
|
|
524
|
+
return createHash("sha256").update(spki).digest("hex");
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Demand every checkpoint inside the walked range.
|
|
532
|
+
*
|
|
533
|
+
* Every `log.checkpoint` record in `records` must carry a readable payload,
|
|
534
|
+
* name a configured key, verify under it, and name the hash the log actually
|
|
535
|
+
* carries at the seq it signed. The FIRST failure refuses, carrying the seq of
|
|
536
|
+
* the offending record and the checkpoints that validated ahead of it: a person
|
|
537
|
+
* reading a divergence needs to know how far the log was still good.
|
|
538
|
+
*
|
|
539
|
+
* Three things are deliberately not refusals.
|
|
540
|
+
*
|
|
541
|
+
* - **No configured key** is a skip naming why, and naming how many checkpoint
|
|
542
|
+
* records went unchecked. Nothing was verified, and a check that reported
|
|
543
|
+
* that as a pass would have stopped being a check.
|
|
544
|
+
* - **A signed seq below the walked range** is counted and named, not refused.
|
|
545
|
+
* A full walk starts at genesis so this cannot arise there; a caller walking
|
|
546
|
+
* a suffix gets an honest count of what its range could not speak to.
|
|
547
|
+
* - **A lapsed cadence** is a warning. A human who has been away is not a
|
|
548
|
+
* forger, and a runtime that refused a log for want of a tap is a runtime
|
|
549
|
+
* whose operator turns the check off.
|
|
550
|
+
*/
|
|
551
|
+
export function checkLogCheckpoints(options) {
|
|
552
|
+
const found = options.records.filter(isCheckpointRecord);
|
|
553
|
+
const { keys, unreadable } = checkpointKeyIndex(options.publicKeys);
|
|
554
|
+
if (keys.size === 0) {
|
|
555
|
+
return {
|
|
556
|
+
status: "skip",
|
|
557
|
+
checkpoints: 0,
|
|
558
|
+
reason: options.publicKeys.length === 0
|
|
559
|
+
? `${options.keysUnavailable ?? "no checkpoint public key is configured (audit.checkpoint_keys)"}, so the ${String(found.length)} checkpoint record(s) in this range could not be verified against anything`
|
|
560
|
+
: `none of the ${String(options.publicKeys.length)} configured checkpoint key(s) could be read as an Ed25519 public key (${String(unreadable)} unreadable), so the ${String(found.length)} checkpoint record(s) in this range could not be verified`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
// Hash by seq, once, and only when there is something to look up. A range
|
|
564
|
+
// with many checkpoints would otherwise scan the whole record list per
|
|
565
|
+
// checkpoint; a log with none would build an index of every record it holds
|
|
566
|
+
// to answer no questions, and this check runs on a daemon tick over a log
|
|
567
|
+
// that grows without bound.
|
|
568
|
+
const hashAt = new Map();
|
|
569
|
+
if (found.length > 0) {
|
|
570
|
+
for (const record of options.records)
|
|
571
|
+
hashAt.set(record.seq, record.hash);
|
|
572
|
+
}
|
|
573
|
+
const firstSeq = options.records[0]?.seq ?? 0;
|
|
574
|
+
const verified = [];
|
|
575
|
+
let unchecked = 0;
|
|
576
|
+
for (const record of found) {
|
|
577
|
+
const payload = readCheckpointPayload(record);
|
|
578
|
+
if (payload === null) {
|
|
579
|
+
return refusedAt(record, "checkpoint-malformed", `the log.checkpoint at seq ${String(record.seq)} does not carry a readable checkpoint payload (seq, hash, alg ${CHECKPOINT_ALG}, key_sha256, signature). The write boundary refuses such a record, so this one was written by something else, and a checkpoint nobody can read is not one that passes`, verified);
|
|
580
|
+
}
|
|
581
|
+
if (payload.seq >= record.seq) {
|
|
582
|
+
return refusedAt(record, "checkpoint-out-of-order", `the log.checkpoint at seq ${String(record.seq)} signs seq ${String(payload.seq)}, which is not below its own. A checkpoint signs the past; one naming itself or the future vouches for a head that did not exist when it was written`, verified);
|
|
583
|
+
}
|
|
584
|
+
const publicKey = keys.get(payload.keySha256);
|
|
585
|
+
if (publicKey === undefined) {
|
|
586
|
+
return refusedAt(record, "checkpoint-key-unknown", `the log.checkpoint at seq ${String(record.seq)} names key ${payload.keySha256}, which none of the ${String(keys.size)} configured checkpoint key(s) hashes to. Either that key was retired out of audit.checkpoint_keys — in which case put it back, since removing a key that signed a checkpoint de-verifies the range it signed — or this record was signed with a key the policy does not vouch for`, verified);
|
|
587
|
+
}
|
|
588
|
+
if (!verifyCheckpointSignature({ seq: payload.seq, hash: payload.hash, alg: payload.alg }, payload.signature, publicKey)) {
|
|
589
|
+
return refusedAt(record, "checkpoint-signature-invalid", `the signature on the log.checkpoint at seq ${String(record.seq)} does not verify under the key it names (${payload.keySha256}) over seq ${String(payload.seq)} ${payload.hash}. The bytes signed are not the bytes presented`, verified);
|
|
590
|
+
}
|
|
591
|
+
if (payload.seq < firstSeq) {
|
|
592
|
+
unchecked += 1;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const actual = hashAt.get(payload.seq);
|
|
596
|
+
if (actual !== payload.hash) {
|
|
597
|
+
return refusedAt(record, "checkpoint-hash-mismatch", `the log.checkpoint at seq ${String(record.seq)} carries a valid signature over seq ${String(payload.seq)} ${payload.hash}, and this log ${actual === undefined
|
|
598
|
+
? "carries no record at that seq"
|
|
599
|
+
: `carries ${actual} there`}. A human's key signed a head this chain does not have: the chain was rewritten after the checkpoint was taken, and whoever rewrote it recomputed the hashes but could not recompute the signature`, verified);
|
|
600
|
+
}
|
|
601
|
+
verified.push({
|
|
602
|
+
at: record.seq,
|
|
603
|
+
seq: payload.seq,
|
|
604
|
+
hash: payload.hash,
|
|
605
|
+
ts: record.ts,
|
|
606
|
+
actor: record.actor,
|
|
607
|
+
keySha256: payload.keySha256,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
status: "pass",
|
|
612
|
+
checkpoints: verified,
|
|
613
|
+
unchecked,
|
|
614
|
+
keys: keys.size,
|
|
615
|
+
warning: cadenceWarning(verified, options),
|
|
616
|
+
detail: verified.length === 0
|
|
617
|
+
? `no checkpoint has been signed in this range; ${String(keys.size)} key(s) are configured and nothing contradicts the chain`
|
|
618
|
+
: `${String(verified.length)} checkpoint(s) validate against ${String(keys.size)} configured key(s), the newest signing seq ${String(verified[verified.length - 1].seq)}`,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function refusedAt(record, code, message, checkpoints) {
|
|
622
|
+
return { status: "refused", code, at: record.seq, message, checkpoints };
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* The cadence sentence, or `null`.
|
|
626
|
+
*
|
|
627
|
+
* Report-only in every direction, like `audit.skew_tolerance`: it decides what
|
|
628
|
+
* a human is shown about a log that already verified, and no verdict, exit code
|
|
629
|
+
* or authorization reads it. A cadence nobody set says nothing; a cadence set
|
|
630
|
+
* and never met says so from the newest record in the range, since a log whose
|
|
631
|
+
* first checkpoint is still owed is exactly the case an operator most wants
|
|
632
|
+
* named.
|
|
633
|
+
*/
|
|
634
|
+
function cadenceWarning(verified, options) {
|
|
635
|
+
const offer = offerFrom(verified, options);
|
|
636
|
+
return offer === null ? null : offer.warning;
|
|
637
|
+
}
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// The cadence, as something to ASK rather than only to report (APRV-257)
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
/** Milliseconds as a rounded hour count, for every cadence sentence. */
|
|
642
|
+
function hours(ms) {
|
|
643
|
+
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The due-ness rule, over an already-computed set of verified checkpoints.
|
|
647
|
+
*
|
|
648
|
+
* ONE rule, and that is the point of extracting it: the verify verdict's
|
|
649
|
+
* warning, the daemon's `checkpoint-due`, `approval doctor`'s row and the
|
|
650
|
+
* channel prompt all read this, so there is no arrangement in which the daemon
|
|
651
|
+
* says a checkpoint is due and the listener declines to ask for one, or the
|
|
652
|
+
* other way around.
|
|
653
|
+
*
|
|
654
|
+
* Returns `null` — never due — when the cadence is off, when the log is empty,
|
|
655
|
+
* or when nothing has aged past the interval. Report-only in every direction:
|
|
656
|
+
* the return type carries a sentence and a head, and there is no code path in
|
|
657
|
+
* this runtime from an offer to a refusal.
|
|
658
|
+
*/
|
|
659
|
+
function offerFrom(verified, options) {
|
|
660
|
+
const every = options.checkpointEveryMs;
|
|
661
|
+
if (every === undefined || every === null || every <= 0)
|
|
662
|
+
return null;
|
|
663
|
+
const now = options.now ?? Date.now();
|
|
664
|
+
const newest = verified[verified.length - 1];
|
|
665
|
+
const since = newest === undefined ? oldestTs(options.records) : Date.parse(newest.ts);
|
|
666
|
+
if (since === null || Number.isNaN(since))
|
|
667
|
+
return null;
|
|
668
|
+
const age = now - since;
|
|
669
|
+
if (age <= every)
|
|
670
|
+
return null;
|
|
671
|
+
// The head to sign is this log's last record. An empty range has none, and a
|
|
672
|
+
// prompt naming nothing is not a prompt.
|
|
673
|
+
const last = options.records[options.records.length - 1];
|
|
674
|
+
if (last === undefined)
|
|
675
|
+
return null;
|
|
676
|
+
return {
|
|
677
|
+
head: { seq: last.seq, hash: last.hash },
|
|
678
|
+
since: newest?.at ?? null,
|
|
679
|
+
ageMs: age,
|
|
680
|
+
everyMs: every,
|
|
681
|
+
warning: newest === undefined
|
|
682
|
+
? `audit.checkpoint_every is ${hours(every)} and this log has never been checkpointed (its oldest record is ${hours(age)} old). A missing checkpoint is not tampering and nothing is refused; sign one with \`approval log checkpoint\``
|
|
683
|
+
: `audit.checkpoint_every is ${hours(every)} and the newest checkpoint (seq ${String(newest.at)}, signing seq ${String(newest.seq)}) is ${hours(age)} old. A checkpoint that is due is not a checkpoint that is missing on purpose; nothing is refused`,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Is a checkpoint due, and over which head? `null` when it is not.
|
|
688
|
+
*
|
|
689
|
+
* The whole question in one call, over already-verified records: it runs
|
|
690
|
+
* {@link checkLogCheckpoints} and offers only from a PASS. A refused range is
|
|
691
|
+
* not a range to ask for another signature over — the thing to do with a
|
|
692
|
+
* checkpoint that does not verify is look at it, not sign a new one on top —
|
|
693
|
+
* and a skipped one has no key configured, so there is nothing to sign with and
|
|
694
|
+
* nobody to ask.
|
|
695
|
+
*/
|
|
696
|
+
export function checkpointDue(options) {
|
|
697
|
+
const check = checkLogCheckpoints(options);
|
|
698
|
+
if (check.status !== "pass")
|
|
699
|
+
return null;
|
|
700
|
+
return offerFrom(check.checkpoints, options);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* The checkpoint half of a policy, read the way `skewToleranceMsOf` reads its
|
|
704
|
+
* key — except that this one fails to a SKIP rather than to a default.
|
|
705
|
+
*
|
|
706
|
+
* A policy that cannot be loaded configures no keys, and a caller with no keys
|
|
707
|
+
* skips with a reason. There is no safe default here: falling back to "no keys"
|
|
708
|
+
* and calling it a pass would report an unreadable policy as a verified log,
|
|
709
|
+
* and falling back to a built-in key would be a key nobody chose.
|
|
710
|
+
*/
|
|
711
|
+
export function checkpointPolicyOf(policy, schemaDir) {
|
|
712
|
+
const where = policy.file !== undefined ? { file: policy.file } : { dir: policy.dir ?? process.cwd() };
|
|
713
|
+
if (schemaDir !== undefined)
|
|
714
|
+
where.schemaDir = schemaDir;
|
|
715
|
+
const load = loadPolicy(where);
|
|
716
|
+
if (!load.ok) {
|
|
717
|
+
return {
|
|
718
|
+
publicKeys: [],
|
|
719
|
+
checkpointEveryMs: null,
|
|
720
|
+
unloadable: `the policy could not be loaded (${load.code}: ${load.message}) and configures no checkpoint key`,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const keys = load.policy.audit?.checkpoint_keys;
|
|
724
|
+
return {
|
|
725
|
+
publicKeys: Array.isArray(keys) ? [...keys] : [],
|
|
726
|
+
checkpointEveryMs: load.durations.checkpointEveryMs,
|
|
727
|
+
unloadable: null,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/** Milliseconds of the oldest record's `ts`, or `null` when there is none. */
|
|
731
|
+
function oldestTs(records) {
|
|
732
|
+
const first = records[0];
|
|
733
|
+
if (first === undefined)
|
|
734
|
+
return null;
|
|
735
|
+
const parsed = Date.parse(first.ts);
|
|
736
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=checkpoint.js.map
|