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,903 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local web queue channel (SPEC.md §5.1 `channels.web.port`, §9, §10.3,
|
|
3
|
+
* §10.4, §11 — APRV-25).
|
|
4
|
+
*
|
|
5
|
+
* The third reference channel, and the only *pull* one. `cli` asks a question
|
|
6
|
+
* and waits for an answer; `telegram` pushes a message at a human who is
|
|
7
|
+
* elsewhere. This one serves a page: the queue sits at
|
|
8
|
+
* `http://127.0.0.1:4680/` and a human looks at it when they choose to. That
|
|
9
|
+
* difference is the design, not an accident of the transport — see "notify is a
|
|
10
|
+
* no-op" below.
|
|
11
|
+
*
|
|
12
|
+
* Everything the contract says about a channel still holds:
|
|
13
|
+
*
|
|
14
|
+
* - **It decides nothing.** A submitted form becomes a {@link ChannelDecision}
|
|
15
|
+
* handed to the handler the runtime registered, and that handler calls
|
|
16
|
+
* `recordChannelDecision` / `recordBatchDecisions`, which call the human-only
|
|
17
|
+
* `decide()` in `core/gate.ts`. TTL lapse, budget re-check, attestation,
|
|
18
|
+
* idempotency and compare-and-append apply to a button on a web page exactly
|
|
19
|
+
* as they apply to `approval grant`.
|
|
20
|
+
* - **It writes no log line and holds no decision state.** The only memory here
|
|
21
|
+
* is the map from action key to delivery id, which is delivery bookkeeping.
|
|
22
|
+
* It dies with the process; the approvals live in the log.
|
|
23
|
+
* - **It renders the computed/claimed split** (§9) and the full payload for
|
|
24
|
+
* manual actions (§10.4), and reports what it rendered through
|
|
25
|
+
* {@link WebChannel.lastRendered} so `channels/conformance.ts` can check it.
|
|
26
|
+
*
|
|
27
|
+
* ## 127.0.0.1 ONLY, and there is deliberately no --host
|
|
28
|
+
*
|
|
29
|
+
* {@link WebChannel.start} calls `listen(port, "127.0.0.1")` with the host
|
|
30
|
+
* **hard-coded**. There is no option, no flag and no environment variable that
|
|
31
|
+
* widens it, and adding one would be a SPEC.md amendment rather than a feature:
|
|
32
|
+
* this server has **no authentication at all** (see below), so the loopback
|
|
33
|
+
* interface is the entire access-control mechanism. A `--host 0.0.0.0` would
|
|
34
|
+
* turn "anyone with local access can approve" into "anyone on the coffee shop
|
|
35
|
+
* wifi can approve", silently, from a flag that reads like a convenience. The
|
|
36
|
+
* bound address is asserted by `tests/channels-web.test.ts`.
|
|
37
|
+
*
|
|
38
|
+
* ## No auth in v0.1 — the trust boundary, stated twice
|
|
39
|
+
*
|
|
40
|
+
* SPEC.md §11: *"Human identity in v0.1 is config-declared (an environment
|
|
41
|
+
* variable or flag); the trust boundary is the local machine, and anyone who
|
|
42
|
+
* can set that configuration and write to the log is inside it."*
|
|
43
|
+
*
|
|
44
|
+
* This channel authenticates nobody. Every decision it collects is recorded
|
|
45
|
+
* against the human actor the **runtime** was started with (`--as` /
|
|
46
|
+
* `APPROVAL_HUMAN`), so the guarantee is "someone with access to this machine
|
|
47
|
+
* approved", never "that specific person approved". Because the page *is* the
|
|
48
|
+
* decision surface, that caveat is printed on the page itself
|
|
49
|
+
* ({@link TRUST_BANNER}) as well as in `--help` and here: a human reading a
|
|
50
|
+
* queue in a browser tab should not have to have read the source to know what
|
|
51
|
+
* their click proves.
|
|
52
|
+
*
|
|
53
|
+
* ## CSRF — inside the stated boundary, with cheap hardening. FLAGGED.
|
|
54
|
+
*
|
|
55
|
+
* There is no CSRF token in v0.1, and the reasoning is worth writing down
|
|
56
|
+
* because it is a deliberate omission rather than an oversight. A CSRF token
|
|
57
|
+
* defends a *session* — it stops a page on another origin from acting as an
|
|
58
|
+
* authenticated user. Here there is no session and no authentication: anything
|
|
59
|
+
* that can open a socket to 127.0.0.1:4680 can already POST directly, token or
|
|
60
|
+
* no token, because the trust boundary is the whole local machine. A token
|
|
61
|
+
* would defend against exactly one attacker: a web page, in a browser on this
|
|
62
|
+
* machine, that the human visited, that guessed the port. That attacker is
|
|
63
|
+
* inside the §11 boundary as written — and is also the most realistic one, so:
|
|
64
|
+
*
|
|
65
|
+
* - A same-origin **soft check** rejects (403) any POST whose `Origin` (or
|
|
66
|
+
* failing that, `Referer`) names something other than this server's own
|
|
67
|
+
* loopback origins. It is best effort: a request with neither header is
|
|
68
|
+
* allowed, because `curl`, `fetch` in a test, and a form POST from an older
|
|
69
|
+
* browser all send neither, and refusing them would break the documented
|
|
70
|
+
* scripting path to buy nothing (an attacker can omit a header too).
|
|
71
|
+
* - **Flagged for human review**: if v0.2 puts this server behind anything
|
|
72
|
+
* resembling a session, or widens the bind address, this stance must be
|
|
73
|
+
* revisited and a real anti-CSRF token added. The soft check is a speed bump,
|
|
74
|
+
* not a control.
|
|
75
|
+
*
|
|
76
|
+
* ## The token IS shown on the page — FLAGGED, and it differs from telegram
|
|
77
|
+
*
|
|
78
|
+
* `channels/telegram.ts` refuses to put an execution token in the chat, and
|
|
79
|
+
* prints it on the listener's stdout instead: a chat transcript lives on
|
|
80
|
+
* someone else's servers and is readable by anyone later added to the chat.
|
|
81
|
+
* This channel does the opposite and shows the raw token **once**, in the
|
|
82
|
+
* response page for the grant that minted it. The asymmetry is the point:
|
|
83
|
+
*
|
|
84
|
+
* - the page is served over loopback to the human who is deciding, right now;
|
|
85
|
+
* - the response is generated per request and persisted nowhere — no file, no
|
|
86
|
+
* log line, no `lastRendered()` entry (the notice is rendered and dropped);
|
|
87
|
+
* - the alternative is worse. On telegram the approver and the terminal are
|
|
88
|
+
* often the same person one room apart; here the browser *is* the surface the
|
|
89
|
+
* human is looking at, and sending them to hunt for a token on a daemon's
|
|
90
|
+
* stdout would push them toward copying tokens out of log files.
|
|
91
|
+
*
|
|
92
|
+
* The token is never written to the log (which holds only its SHA-256), never
|
|
93
|
+
* put in a URL (a query string lands in history), and never repeated: reload
|
|
94
|
+
* the response and it is gone. The channel itself does not even hold it — the
|
|
95
|
+
* runtime returns the notice text through {@link WebChannelOptions.decisionNotice}
|
|
96
|
+
* at render time, so a channel that logged its own outcomes would log nothing
|
|
97
|
+
* sensitive. **Flagged for human review** all the same: it is the one place in
|
|
98
|
+
* this codebase where a credential is rendered into a document a browser
|
|
99
|
+
* handles, and browsers cache, autofill and sync more than one expects.
|
|
100
|
+
*
|
|
101
|
+
* ## notify() is a no-op — pull, not push
|
|
102
|
+
*
|
|
103
|
+
* The queue *is* the notification surface. `notify()` records the request as
|
|
104
|
+
* deliverable (so a decision can be routed back to it), returns a synthetic
|
|
105
|
+
* delivery id, and sends nothing anywhere: there is nowhere to send it, since a
|
|
106
|
+
* web page is fetched rather than delivered. Everything the human sees is
|
|
107
|
+
* produced by `GET /`, which re-renders from the runtime's live queue
|
|
108
|
+
* ({@link WebChannelOptions.refresh}) whenever one is supplied. This is why
|
|
109
|
+
* `lastRendered()` reflects the most recent *render* — a GET when there has
|
|
110
|
+
* been one, the last `notify()` otherwise — and why both go through the same
|
|
111
|
+
* {@link renderWebRequest}: the conformance suite must be inspecting the thing a
|
|
112
|
+
* browser was actually served.
|
|
113
|
+
*
|
|
114
|
+
* ## Zero JavaScript required
|
|
115
|
+
*
|
|
116
|
+
* Every flow — grant, reject with a note, select-and-batch — is plain
|
|
117
|
+
* `<form method="post" enctype="application/x-www-form-urlencoded">`. The batch
|
|
118
|
+
* checkboxes live outside the batch form and are attached to it with the HTML5
|
|
119
|
+
* `form=` attribute (nested forms are illegal), so one page can carry per-request
|
|
120
|
+
* forms *and* a batch gesture with no script at all. A four-line inline script
|
|
121
|
+
* offers "select all" as a convenience; disable scripting and every flow still
|
|
122
|
+
* works, which is checked by the tests driving the server with `fetch` alone.
|
|
123
|
+
*/
|
|
124
|
+
import { createServer } from "node:http";
|
|
125
|
+
import { isPromptRow, WEB_PROMPT_LAYOUT } from "../core/prompt-layout.js";
|
|
126
|
+
import { assembleBatch } from "./batch.js";
|
|
127
|
+
import { payloadRegionText } from "./payload-view.js";
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Constants
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/** SPEC.md §5.1 `channels.web.port`. Used when policy and flags say nothing. */
|
|
132
|
+
export const WEB_DEFAULT_PORT = 4680;
|
|
133
|
+
/**
|
|
134
|
+
* The only address this server ever binds. Hard-coded; see the module header.
|
|
135
|
+
*
|
|
136
|
+
* Exported so the tests can assert the bound address against a constant rather
|
|
137
|
+
* than a literal they could drift from — not so a caller can substitute one.
|
|
138
|
+
*/
|
|
139
|
+
export const WEB_LOOPBACK_HOST = "127.0.0.1";
|
|
140
|
+
/** The largest form body accepted. A queue page's POST is a few hundred bytes. */
|
|
141
|
+
export const WEB_MAX_BODY_BYTES = 256 * 1024;
|
|
142
|
+
/** The §11 caveat, rendered at the top of every page. */
|
|
143
|
+
export const TRUST_BANNER = "TRUST BOUNDARY — this page has NO AUTHENTICATION. It is served on 127.0.0.1 only, " +
|
|
144
|
+
"and anyone with access to this machine can decide here as the configured actor. " +
|
|
145
|
+
"A decision recorded from this page proves that someone with local control answered, " +
|
|
146
|
+
"not who. (SPEC.md §11: identity in v0.1 is config-declared.)";
|
|
147
|
+
/** The full-payload delimiters (SPEC.md §10.4). Pinned by tests. */
|
|
148
|
+
export const PAYLOAD_BEGIN = "--- BEGIN FULL PAYLOAD";
|
|
149
|
+
export const PAYLOAD_END = "--- END FULL PAYLOAD ---";
|
|
150
|
+
/** Section headings. The computed/claimed split a reader must not have to infer. */
|
|
151
|
+
export const COMPUTED_HEADING = "COMPUTED — derived by the runtime from the verified log, the attested policy, the budget evaluator, the payload bytes and the clock";
|
|
152
|
+
export const CLAIMED_HEADING_PREFIX = "CLAIMED — authored by";
|
|
153
|
+
export const CLAIMED_HEADING_SUFFIX = "NOT verified by the runtime";
|
|
154
|
+
/** Badge markers, mirroring `channels/cli.ts` so the two channels read alike. */
|
|
155
|
+
export const COMPUTED_MARKER = "[computed]";
|
|
156
|
+
export const CLAIMED_MARKER = "[claimed]";
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Escaping
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
/**
|
|
161
|
+
* Escape every character that could change the structure of the document.
|
|
162
|
+
*
|
|
163
|
+
* A superset of `channels/telegram.ts`'s three: this channel interpolates into
|
|
164
|
+
* HTML **attribute** positions (checkbox values carry action keys, form ids
|
|
165
|
+
* carry them too), where a bare quote is as dangerous as a bare `<`. Applied
|
|
166
|
+
* uniformly to every interpolated value without exception — the claimed fields
|
|
167
|
+
* and the payload are authored by the party under oversight, and they are the
|
|
168
|
+
* entire injection surface of this page.
|
|
169
|
+
*/
|
|
170
|
+
export function escapeHtml(text) {
|
|
171
|
+
return text
|
|
172
|
+
.replace(/&/gu, "&")
|
|
173
|
+
.replace(/</gu, "<")
|
|
174
|
+
.replace(/>/gu, ">")
|
|
175
|
+
.replace(/"/gu, """)
|
|
176
|
+
.replace(/'/gu, "'");
|
|
177
|
+
}
|
|
178
|
+
/** A DOM id that cannot escape its attribute, derived from an action key. */
|
|
179
|
+
function slug(actionKey, index) {
|
|
180
|
+
return `r${index}-${actionKey.replace(/[^A-Za-z0-9_-]/gu, "-")}`;
|
|
181
|
+
}
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Value formatting (mirrors channels/cli.ts, deliberately)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
function formatDuration(ms) {
|
|
186
|
+
if (ms <= 0)
|
|
187
|
+
return "EXPIRED";
|
|
188
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
189
|
+
const minutes = Math.floor(seconds / 60);
|
|
190
|
+
const hours = Math.floor(minutes / 60);
|
|
191
|
+
if (hours > 0)
|
|
192
|
+
return `${hours}h ${minutes % 60}m left`;
|
|
193
|
+
if (minutes > 0)
|
|
194
|
+
return `${minutes}m ${seconds % 60}s left`;
|
|
195
|
+
return `${seconds}s left`;
|
|
196
|
+
}
|
|
197
|
+
function formatBudgets(verdicts) {
|
|
198
|
+
if (verdicts.length === 0)
|
|
199
|
+
return "none configured";
|
|
200
|
+
return verdicts
|
|
201
|
+
.map((verdict) => `${verdict.limit} ${verdict.pass ? "pass" : "FAIL"} (consumed ${verdict.consumed}, requested ${verdict.requested}, remaining ${verdict.remaining}, ${verdict.window})`)
|
|
202
|
+
.join("; ");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* One field's value as display text. Deliberately total: an unrecognized shape
|
|
206
|
+
* is JSON-stringified rather than dropped, because a field the runtime tagged
|
|
207
|
+
* and the channel silently omitted is a field the approver did not see.
|
|
208
|
+
*/
|
|
209
|
+
export function formatValue(field, value) {
|
|
210
|
+
if (value === null || value === undefined)
|
|
211
|
+
return "(none)";
|
|
212
|
+
if (field === "ttl_remaining_ms" && typeof value === "number")
|
|
213
|
+
return formatDuration(value);
|
|
214
|
+
if (field === "budgets" && Array.isArray(value))
|
|
215
|
+
return formatBudgets(value);
|
|
216
|
+
if (field === "attestation" && typeof value === "object") {
|
|
217
|
+
const status = value.status;
|
|
218
|
+
const seq = value.seq;
|
|
219
|
+
if (status === "attested")
|
|
220
|
+
return `attested (policy.updated seq ${String(seq)})`;
|
|
221
|
+
if (status === "not-attested") {
|
|
222
|
+
return "NOT ATTESTED — no human has signed off on this policy file";
|
|
223
|
+
}
|
|
224
|
+
if (status === "hash-mismatch") {
|
|
225
|
+
return `HASH MISMATCH — the policy file changed since attestation seq ${String(seq)}`;
|
|
226
|
+
}
|
|
227
|
+
return `UNREADABLE — ${String(value.message)}`;
|
|
228
|
+
}
|
|
229
|
+
if (field === "chain" && typeof value === "object") {
|
|
230
|
+
const chain = value;
|
|
231
|
+
return `seq ${String(chain.seq)} hash ${String(chain.hash)} (log head seq ${String(chain.head_seq)})`;
|
|
232
|
+
}
|
|
233
|
+
if (field === "est_cost_usd" && typeof value === "number")
|
|
234
|
+
return `$${value.toFixed(2)}`;
|
|
235
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
236
|
+
return String(value);
|
|
237
|
+
}
|
|
238
|
+
return JSON.stringify(value) ?? String(value);
|
|
239
|
+
}
|
|
240
|
+
/** The parenthetical: which derivation, or which author. */
|
|
241
|
+
function attribution(field) {
|
|
242
|
+
return field.kind === "computed" ? field.source : field.author;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Presentation order, and which rows render (APRV-218).
|
|
246
|
+
*
|
|
247
|
+
* The default is {@link WEB_PROMPT_LAYOUT}: computed identity and authority
|
|
248
|
+
* first, claimed persuasion last, every row on, because a page scrolls.
|
|
249
|
+
* `channels.web.prompt` in APPROVAL.md replaces it.
|
|
250
|
+
*
|
|
251
|
+
* Members absent from a request are skipped; members the layout does not NAME
|
|
252
|
+
* are appended, so a widened {@link ChannelRequest} cannot silently lose a
|
|
253
|
+
* field to a policy written before that field existed.
|
|
254
|
+
*/
|
|
255
|
+
function orderedFields(request, layout) {
|
|
256
|
+
const members = Object.keys(request);
|
|
257
|
+
const ordered = layout.order.filter((name) => layout.visibility[name] !== "off" && members.includes(name));
|
|
258
|
+
for (const name of members) {
|
|
259
|
+
if (!ordered.includes(name) && !isPromptRow(name))
|
|
260
|
+
ordered.push(name);
|
|
261
|
+
}
|
|
262
|
+
return ordered;
|
|
263
|
+
}
|
|
264
|
+
/** Build the split. Pure: no I/O, no clock, no HTML. */
|
|
265
|
+
export function renderWebRequest(request, layout = WEB_PROMPT_LAYOUT) {
|
|
266
|
+
const members = request;
|
|
267
|
+
const computedLines = [];
|
|
268
|
+
const claimedLines = [];
|
|
269
|
+
for (const name of orderedFields(request, layout)) {
|
|
270
|
+
const field = members[name];
|
|
271
|
+
if (field === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
// `fullPayload` has a region of its own (§10.4), rendered verbatim inside
|
|
274
|
+
// delimiters below. Emitting it as a field line too would print the payload
|
|
275
|
+
// twice — once as bytes and once as a JSON blob squeezed into a summary
|
|
276
|
+
// line — and the squeezed copy is where an agent's markup would end up
|
|
277
|
+
// sitting in the computed block, which is precisely the authority §9
|
|
278
|
+
// forbids lending it.
|
|
279
|
+
if (name === "fullPayload")
|
|
280
|
+
continue;
|
|
281
|
+
const line = {
|
|
282
|
+
field: name,
|
|
283
|
+
kind: field.kind,
|
|
284
|
+
text: formatValue(name, field.value),
|
|
285
|
+
origin: attribution(field),
|
|
286
|
+
};
|
|
287
|
+
if (field.kind === "computed")
|
|
288
|
+
computedLines.push(line);
|
|
289
|
+
else
|
|
290
|
+
claimedLines.push(line);
|
|
291
|
+
}
|
|
292
|
+
const rendering = request.fullPayload.value;
|
|
293
|
+
const payloadText = rendering === null
|
|
294
|
+
? null
|
|
295
|
+
: [
|
|
296
|
+
`${PAYLOAD_BEGIN} (bound sha256 ${rendering.hash}) ---`,
|
|
297
|
+
payloadRegionText(rendering, request.class.value),
|
|
298
|
+
PAYLOAD_END,
|
|
299
|
+
...(rendering.truncated
|
|
300
|
+
? ["(TRUNCATED — this is not the whole payload; do not grant on it)"]
|
|
301
|
+
: []),
|
|
302
|
+
].join("\n");
|
|
303
|
+
return {
|
|
304
|
+
actionKey: request.action_key.value,
|
|
305
|
+
computed: computedLines,
|
|
306
|
+
claimed: claimedLines,
|
|
307
|
+
author: request.summary.kind === "claimed" ? request.summary.author : "the requesting party",
|
|
308
|
+
payloadText,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** The {@link RenderedRequest} report for the conformance suite. */
|
|
312
|
+
function reportOf(rendering, batchDeliveryId) {
|
|
313
|
+
const fields = [...rendering.computed, ...rendering.claimed].map((line) => ({
|
|
314
|
+
field: line.field,
|
|
315
|
+
kind: line.kind,
|
|
316
|
+
text: line.text,
|
|
317
|
+
}));
|
|
318
|
+
return {
|
|
319
|
+
action_key: rendering.actionKey,
|
|
320
|
+
fields,
|
|
321
|
+
fullPayloadText: rendering.payloadText,
|
|
322
|
+
...(batchDeliveryId === undefined ? {} : { batchDeliveryId }),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// The page
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
/**
|
|
329
|
+
* The stylesheet, inline — a queue page that needed a second request to make
|
|
330
|
+
* the computed/claimed split visible would show an unstyled, indistinguishable
|
|
331
|
+
* page for the duration of that request.
|
|
332
|
+
*
|
|
333
|
+
* Colour is never the only carrier: every claimed line also has the textual
|
|
334
|
+
* `[claimed]` marker and sits under its own heading, so the split survives a
|
|
335
|
+
* monochrome screen, a screen reader and a "reader mode".
|
|
336
|
+
*/
|
|
337
|
+
const STYLE = `
|
|
338
|
+
:root { color-scheme: light dark; }
|
|
339
|
+
body { font: 15px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; margin: 0; padding: 1.5rem; }
|
|
340
|
+
h1 { font-size: 1.1rem; margin: 0 0 .5rem; }
|
|
341
|
+
h2 { font-size: 1rem; margin: 0 0 .25rem; }
|
|
342
|
+
h3 { font-size: .85rem; margin: 1rem 0 .25rem; letter-spacing: .02em; }
|
|
343
|
+
.banner { border: 2px solid #b45309; background: #fffbeb; color: #7c2d12; padding: .75rem; margin-bottom: 1rem; }
|
|
344
|
+
.error { border: 2px solid #b91c1c; background: #fef2f2; color: #7f1d1d; padding: .75rem; margin-bottom: 1rem; }
|
|
345
|
+
.notice { border: 2px dashed #15803d; background: #f0fdf4; color: #14532d; padding: .75rem; margin: .75rem 0; }
|
|
346
|
+
article { border: 1px solid #9ca3af; padding: 1rem; margin-bottom: 1.5rem; }
|
|
347
|
+
.computed { border-left: 4px solid #1d4ed8; padding-left: .75rem; }
|
|
348
|
+
.claimed { border-left: 4px dashed #b45309; padding-left: .75rem; background: #fff7ed; }
|
|
349
|
+
.line { display: block; white-space: pre-wrap; }
|
|
350
|
+
.marker { font-weight: 700; }
|
|
351
|
+
.name { display: inline-block; min-width: 11rem; }
|
|
352
|
+
.origin { opacity: .75; }
|
|
353
|
+
pre.payload { white-space: pre-wrap; word-break: break-all; border: 1px solid #374151; background: #f9fafb; color: #111827; padding: .75rem; }
|
|
354
|
+
form { display: inline-block; margin-right: 1rem; }
|
|
355
|
+
input[type=text] { font: inherit; min-width: 18rem; }
|
|
356
|
+
button { font: inherit; padding: .25rem .75rem; }
|
|
357
|
+
code { word-break: break-all; }
|
|
358
|
+
@media (prefers-color-scheme: dark) {
|
|
359
|
+
body { background: #0b0f14; color: #e5e7eb; }
|
|
360
|
+
.banner { background: #3f2d10; color: #fde68a; }
|
|
361
|
+
.error { background: #3f1214; color: #fecaca; }
|
|
362
|
+
.notice { background: #052e16; color: #bbf7d0; }
|
|
363
|
+
.claimed { background: #221708; }
|
|
364
|
+
pre.payload { background: #111827; color: #e5e7eb; border-color: #6b7280; }
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
/** The optional convenience script. Every flow works without it — see the header. */
|
|
368
|
+
const SELECT_ALL_SCRIPT = `
|
|
369
|
+
document.querySelectorAll('[data-select-all]').forEach(function (box) {
|
|
370
|
+
box.addEventListener('change', function () {
|
|
371
|
+
document.querySelectorAll('input[name="select"]').forEach(function (item) {
|
|
372
|
+
item.checked = box.checked;
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
`;
|
|
377
|
+
function lineHtml(line) {
|
|
378
|
+
const marker = line.kind === "computed" ? COMPUTED_MARKER : CLAIMED_MARKER;
|
|
379
|
+
return (`<span class="line"><span class="marker">${escapeHtml(marker)}</span> ` +
|
|
380
|
+
`<span class="name">${escapeHtml(line.field)}</span> ` +
|
|
381
|
+
`${escapeHtml(line.text)} <span class="origin">(${escapeHtml(line.origin)})</span></span>`);
|
|
382
|
+
}
|
|
383
|
+
function requestHtml(rendering, index) {
|
|
384
|
+
const key = rendering.actionKey;
|
|
385
|
+
const id = slug(key, index);
|
|
386
|
+
const parts = [
|
|
387
|
+
`<article id="${escapeHtml(id)}">`,
|
|
388
|
+
`<h2><code>${escapeHtml(key)}</code></h2>`,
|
|
389
|
+
`<label><input type="checkbox" name="select" value="${escapeHtml(key)}" form="batch-form"> include in batch gesture</label>`,
|
|
390
|
+
`<h3>${escapeHtml(COMPUTED_HEADING)}</h3>`,
|
|
391
|
+
`<div class="computed">${rendering.computed.map(lineHtml).join("")}</div>`,
|
|
392
|
+
`<h3>${escapeHtml(`${CLAIMED_HEADING_PREFIX} ${rendering.author} — ${CLAIMED_HEADING_SUFFIX}`)}</h3>`,
|
|
393
|
+
`<div class="claimed">${rendering.claimed.map(lineHtml).join("")}</div>`,
|
|
394
|
+
];
|
|
395
|
+
if (rendering.payloadText !== null) {
|
|
396
|
+
parts.push("<h3>FULL PAYLOAD — the exact bytes this approval binds to (SPEC.md §10.4)</h3>", `<pre class="payload">${escapeHtml(rendering.payloadText)}</pre>`);
|
|
397
|
+
}
|
|
398
|
+
parts.push("<h3>DECIDE</h3>", `<form method="post" action="/decide">`, `<input type="hidden" name="action_key" value="${escapeHtml(key)}">`, `<input type="hidden" name="decision" value="grant">`, `<label>note (optional) <input type="text" name="note"></label> `, `<button type="submit">Grant</button>`, "</form>", `<form method="post" action="/decide">`, `<input type="hidden" name="action_key" value="${escapeHtml(key)}">`, `<input type="hidden" name="decision" value="reject">`, `<label>note (REQUIRED — say why) <input type="text" name="note" required></label> `, `<button type="submit">Reject</button>`, "</form>", "</article>");
|
|
399
|
+
return parts.join("\n");
|
|
400
|
+
}
|
|
401
|
+
function page(title, body) {
|
|
402
|
+
return [
|
|
403
|
+
"<!doctype html>",
|
|
404
|
+
'<html lang="en"><head><meta charset="utf-8">',
|
|
405
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
406
|
+
`<title>${escapeHtml(title)}</title>`,
|
|
407
|
+
`<style>${STYLE}</style>`,
|
|
408
|
+
"</head><body>",
|
|
409
|
+
`<h1>${escapeHtml(title)}</h1>`,
|
|
410
|
+
`<div class="banner">${escapeHtml(TRUST_BANNER)}</div>`,
|
|
411
|
+
body,
|
|
412
|
+
`<script>${SELECT_ALL_SCRIPT}</script>`,
|
|
413
|
+
"</body></html>",
|
|
414
|
+
].join("\n");
|
|
415
|
+
}
|
|
416
|
+
/** What the page says about the identity decisions are recorded against. */
|
|
417
|
+
function actorLine(actorLabel) {
|
|
418
|
+
if (actorLabel === null)
|
|
419
|
+
return "";
|
|
420
|
+
return `<p>Decisions recorded here are attributed to <code>${escapeHtml(actorLabel)}</code> — the actor this runtime was configured with, not the person at this browser.</p>`;
|
|
421
|
+
}
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// The channel
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
export class WebChannel {
|
|
426
|
+
name;
|
|
427
|
+
configuredPort;
|
|
428
|
+
refresh;
|
|
429
|
+
notice;
|
|
430
|
+
actorLabel;
|
|
431
|
+
complain;
|
|
432
|
+
handler = null;
|
|
433
|
+
batchHandler = null;
|
|
434
|
+
/** Everything ever delivered, by action key. Delivery bookkeeping only. */
|
|
435
|
+
deliveries = new Map();
|
|
436
|
+
/** What the last render showed, in order. */
|
|
437
|
+
pending = [];
|
|
438
|
+
rendered = [];
|
|
439
|
+
httpServer = null;
|
|
440
|
+
counter = 0;
|
|
441
|
+
counters = { notified: 0, views: 0, decisions: 0, refused: 0 };
|
|
442
|
+
/** The policy's row layout for this channel (APRV-218). Pure input to the rendering. */
|
|
443
|
+
layout;
|
|
444
|
+
constructor(options = {}) {
|
|
445
|
+
this.layout = options.layout ?? WEB_PROMPT_LAYOUT;
|
|
446
|
+
this.name = options.name ?? "web";
|
|
447
|
+
this.configuredPort = options.port ?? WEB_DEFAULT_PORT;
|
|
448
|
+
this.refresh = options.refresh ?? null;
|
|
449
|
+
this.notice = options.decisionNotice ?? null;
|
|
450
|
+
this.actorLabel = options.actorLabel ?? null;
|
|
451
|
+
this.complain =
|
|
452
|
+
options.log ??
|
|
453
|
+
((message) => {
|
|
454
|
+
process.stderr.write(`${message}\n`);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// -------------------------------------------------------------------------
|
|
458
|
+
// Channel
|
|
459
|
+
// -------------------------------------------------------------------------
|
|
460
|
+
/**
|
|
461
|
+
* Register a request (or a batch) as deliverable and return its delivery id.
|
|
462
|
+
*
|
|
463
|
+
* Sends nothing: the queue page is pulled, not pushed (see the module
|
|
464
|
+
* header). The rendering split is computed here anyway, so `lastRendered()`
|
|
465
|
+
* is meaningful before anyone has opened a browser — and it is computed by
|
|
466
|
+
* the same {@link renderWebRequest} that produces the served HTML.
|
|
467
|
+
*/
|
|
468
|
+
notify(target) {
|
|
469
|
+
const isBatch = "requests" in target;
|
|
470
|
+
const members = isBatch ? target.requests : [target];
|
|
471
|
+
this.counter += 1;
|
|
472
|
+
const deliveryId = isBatch ? `web-batch-${this.counter}` : `web-${this.counter}`;
|
|
473
|
+
for (const member of members) {
|
|
474
|
+
this.deliveries.set(member.action_key.value, {
|
|
475
|
+
deliveryId,
|
|
476
|
+
...(isBatch ? { batchDeliveryId: deliveryId } : {}),
|
|
477
|
+
});
|
|
478
|
+
this.counters.notified += 1;
|
|
479
|
+
}
|
|
480
|
+
this.pending = members;
|
|
481
|
+
this.rendered = members.map((member) => reportOf(renderWebRequest(member, this.layout), isBatch ? deliveryId : undefined));
|
|
482
|
+
return deliveryId;
|
|
483
|
+
}
|
|
484
|
+
onDecision(handler) {
|
|
485
|
+
this.handler = handler;
|
|
486
|
+
}
|
|
487
|
+
/** Register the runtime's batch recorder. Optional; see {@link WebBatchHandler}. */
|
|
488
|
+
onBatchDecision(handler) {
|
|
489
|
+
this.batchHandler = handler;
|
|
490
|
+
}
|
|
491
|
+
health() {
|
|
492
|
+
const address = this.address();
|
|
493
|
+
if (address === null) {
|
|
494
|
+
return { ok: false, detail: `not listening (configured port ${this.configuredPort})` };
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
ok: true,
|
|
498
|
+
detail: `listening on http://${address.address}:${address.port}/ (loopback only); ` +
|
|
499
|
+
`${this.counters.views} page view(s), ${this.counters.notified} request(s) delivered, ` +
|
|
500
|
+
`${this.counters.decisions} decision(s), ${this.counters.refused} refused POST(s)`,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/** The most recent rendering — a GET when there has been one, else `notify`. */
|
|
504
|
+
lastRendered() {
|
|
505
|
+
return this.rendered;
|
|
506
|
+
}
|
|
507
|
+
stats() {
|
|
508
|
+
return { ...this.counters };
|
|
509
|
+
}
|
|
510
|
+
/** The bound address, or `null` when the server is not listening. */
|
|
511
|
+
address() {
|
|
512
|
+
const address = this.httpServer?.address() ?? null;
|
|
513
|
+
return address === null || typeof address === "string" ? null : address;
|
|
514
|
+
}
|
|
515
|
+
/** The bound port, or the configured one before {@link start}. */
|
|
516
|
+
get port() {
|
|
517
|
+
return this.address()?.port ?? this.configuredPort;
|
|
518
|
+
}
|
|
519
|
+
/** The underlying server, for tests that assert on the socket itself. */
|
|
520
|
+
get server() {
|
|
521
|
+
return this.httpServer;
|
|
522
|
+
}
|
|
523
|
+
/** The page's own origin, as a browser would send it. */
|
|
524
|
+
get origin() {
|
|
525
|
+
return `http://${WEB_LOOPBACK_HOST}:${this.port}`;
|
|
526
|
+
}
|
|
527
|
+
// -------------------------------------------------------------------------
|
|
528
|
+
// The server
|
|
529
|
+
// -------------------------------------------------------------------------
|
|
530
|
+
/**
|
|
531
|
+
* Bind and serve — on `127.0.0.1` and nowhere else.
|
|
532
|
+
*
|
|
533
|
+
* The host argument is a constant. See the module header for why there is
|
|
534
|
+
* deliberately no option to widen it.
|
|
535
|
+
*/
|
|
536
|
+
start() {
|
|
537
|
+
if (this.httpServer !== null) {
|
|
538
|
+
const address = this.address();
|
|
539
|
+
if (address !== null)
|
|
540
|
+
return Promise.resolve(address);
|
|
541
|
+
}
|
|
542
|
+
const server = createServer((request, response) => {
|
|
543
|
+
void this.handle(request, response).catch((cause) => {
|
|
544
|
+
this.complain(`approval: web channel failed to handle a request: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
545
|
+
if (!response.headersSent) {
|
|
546
|
+
this.send(response, 500, page("approval.md — error", "<p>internal error</p>"));
|
|
547
|
+
}
|
|
548
|
+
else
|
|
549
|
+
response.end();
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
this.httpServer = server;
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
const onError = (cause) => {
|
|
555
|
+
server.off("error", onError);
|
|
556
|
+
reject(cause);
|
|
557
|
+
};
|
|
558
|
+
server.on("error", onError);
|
|
559
|
+
server.listen(this.configuredPort, WEB_LOOPBACK_HOST, () => {
|
|
560
|
+
server.off("error", onError);
|
|
561
|
+
const address = this.address();
|
|
562
|
+
if (address === null)
|
|
563
|
+
reject(new Error("the web channel bound no address"));
|
|
564
|
+
else
|
|
565
|
+
resolve(address);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** Stop listening. Safe when never started. */
|
|
570
|
+
close() {
|
|
571
|
+
const server = this.httpServer;
|
|
572
|
+
if (server === null)
|
|
573
|
+
return Promise.resolve();
|
|
574
|
+
this.httpServer = null;
|
|
575
|
+
return new Promise((resolve) => {
|
|
576
|
+
server.closeAllConnections?.();
|
|
577
|
+
server.close(() => resolve());
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
// -------------------------------------------------------------------------
|
|
581
|
+
// Routing
|
|
582
|
+
// -------------------------------------------------------------------------
|
|
583
|
+
async handle(request, response) {
|
|
584
|
+
const path = (request.url ?? "/").split("?", 1)[0] ?? "/";
|
|
585
|
+
if (request.method === "GET" && (path === "/" || path === "/index.html")) {
|
|
586
|
+
this.counters.views += 1;
|
|
587
|
+
this.send(response, 200, this.queuePage());
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (request.method === "POST" && (path === "/decide" || path === "/decide-batch")) {
|
|
591
|
+
const origin = this.checkOrigin(request);
|
|
592
|
+
if (origin !== null) {
|
|
593
|
+
this.counters.refused += 1;
|
|
594
|
+
this.complain(`approval: web channel refused a cross-origin POST (${origin})`);
|
|
595
|
+
this.send(response, 403, page("approval.md — refused", `<div class="error">${escapeHtml(`Refused: this POST came from ${origin}, not from this page. Nothing was recorded. (Best-effort same-origin check; see SPEC.md §11 for the trust boundary this server actually relies on.)`)}</div>`));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const body = await this.readBody(request);
|
|
599
|
+
if (!body.ok) {
|
|
600
|
+
this.counters.refused += 1;
|
|
601
|
+
this.send(response, 413, page("approval.md — refused", `<div class="error">${escapeHtml(body.message)}</div>`));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const form = new URLSearchParams(body.text);
|
|
605
|
+
if (path === "/decide")
|
|
606
|
+
await this.decide(form, response);
|
|
607
|
+
else
|
|
608
|
+
await this.decideBatch(form, response);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (request.method !== "GET" && request.method !== "POST") {
|
|
612
|
+
this.send(response, 405, page("approval.md — method not allowed", "<p>GET or POST.</p>"));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.send(response, 404, page("approval.md — not found", '<p>Nothing here. <a href="/">Queue</a>.</p>'));
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* The best-effort same-origin check. Returns the offending origin, or `null`
|
|
619
|
+
* when the POST is acceptable.
|
|
620
|
+
*
|
|
621
|
+
* A request with neither `Origin` nor `Referer` is accepted: `curl`, a test's
|
|
622
|
+
* `fetch` and some browsers' form posts all send neither, and an attacker can
|
|
623
|
+
* omit a header as easily as anyone else — refusing them would cost the
|
|
624
|
+
* documented scripting path and buy nothing. See the module header's CSRF
|
|
625
|
+
* section, which is flagged for review.
|
|
626
|
+
*/
|
|
627
|
+
checkOrigin(request) {
|
|
628
|
+
const origin = request.headers["origin"];
|
|
629
|
+
const referer = request.headers["referer"];
|
|
630
|
+
const candidate = typeof origin === "string" && origin.length > 0 && origin !== "null"
|
|
631
|
+
? origin
|
|
632
|
+
: typeof referer === "string" && referer.length > 0
|
|
633
|
+
? referer
|
|
634
|
+
: null;
|
|
635
|
+
if (candidate === null)
|
|
636
|
+
return null;
|
|
637
|
+
let parsed;
|
|
638
|
+
try {
|
|
639
|
+
parsed = new URL(candidate);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
return candidate;
|
|
643
|
+
}
|
|
644
|
+
const port = String(this.port);
|
|
645
|
+
const loopback = ["127.0.0.1", "localhost", "[::1]", "::1"];
|
|
646
|
+
if (parsed.protocol !== "http:" || parsed.port !== port)
|
|
647
|
+
return parsed.origin;
|
|
648
|
+
return loopback.includes(parsed.hostname) ? null : parsed.origin;
|
|
649
|
+
}
|
|
650
|
+
readBody(request) {
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
const chunks = [];
|
|
653
|
+
let size = 0;
|
|
654
|
+
let settled = false;
|
|
655
|
+
const finish = (result) => {
|
|
656
|
+
if (settled)
|
|
657
|
+
return;
|
|
658
|
+
settled = true;
|
|
659
|
+
resolve(result);
|
|
660
|
+
};
|
|
661
|
+
request.on("data", (chunk) => {
|
|
662
|
+
size += chunk.length;
|
|
663
|
+
if (size > WEB_MAX_BODY_BYTES) {
|
|
664
|
+
finish({
|
|
665
|
+
ok: false,
|
|
666
|
+
message: `form body exceeds ${WEB_MAX_BODY_BYTES} bytes; nothing was recorded`,
|
|
667
|
+
});
|
|
668
|
+
request.destroy();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
chunks.push(chunk);
|
|
672
|
+
});
|
|
673
|
+
request.on("end", () => finish({ ok: true, text: Buffer.concat(chunks).toString("utf8") }));
|
|
674
|
+
request.on("error", (cause) => finish({ ok: false, message: `form body could not be read: ${cause.message}` }));
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
// -------------------------------------------------------------------------
|
|
678
|
+
// The queue page
|
|
679
|
+
// -------------------------------------------------------------------------
|
|
680
|
+
/** The live pending set: the runtime's queue when one is supplied. */
|
|
681
|
+
currentRequests() {
|
|
682
|
+
if (this.refresh === null)
|
|
683
|
+
return this.pending;
|
|
684
|
+
const requests = this.refresh();
|
|
685
|
+
for (const request of requests) {
|
|
686
|
+
const key = request.action_key.value;
|
|
687
|
+
if (this.deliveries.has(key))
|
|
688
|
+
continue;
|
|
689
|
+
this.counter += 1;
|
|
690
|
+
this.deliveries.set(key, { deliveryId: `web-${this.counter}` });
|
|
691
|
+
this.counters.notified += 1;
|
|
692
|
+
}
|
|
693
|
+
this.pending = requests;
|
|
694
|
+
return requests;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Render the queue. The `lastRendered()` report is built here, from the same
|
|
698
|
+
* values the HTML is built from — not from a parallel description of it.
|
|
699
|
+
*/
|
|
700
|
+
queuePage(error, notice) {
|
|
701
|
+
const requests = this.currentRequests();
|
|
702
|
+
const renderings = requests.map((request) => renderWebRequest(request, this.layout));
|
|
703
|
+
this.rendered = renderings.map((rendering) => reportOf(rendering, this.deliveries.get(rendering.actionKey)?.batchDeliveryId));
|
|
704
|
+
const body = [];
|
|
705
|
+
if (error !== undefined)
|
|
706
|
+
body.push(`<div class="error">${escapeHtml(error)}</div>`);
|
|
707
|
+
if (notice !== undefined)
|
|
708
|
+
body.push(`<div class="notice">${escapeHtml(notice)}</div>`);
|
|
709
|
+
body.push(actorLine(this.actorLabel));
|
|
710
|
+
if (renderings.length === 0) {
|
|
711
|
+
body.push("<p>queue: empty — no requests awaiting a decision.</p>");
|
|
712
|
+
return page("approval.md — pending decisions", body.join("\n"));
|
|
713
|
+
}
|
|
714
|
+
body.push(`<p>${renderings.length} request(s) awaiting a decision. Every field below is marked ${escapeHtml(COMPUTED_MARKER)} (the runtime derived it) or ${escapeHtml(CLAIMED_MARKER)} (the party under oversight wrote it; it is never evidence).</p>`);
|
|
715
|
+
body.push(...renderings.map((rendering, index) => requestHtml(rendering, index)));
|
|
716
|
+
body.push("<h2>BATCH — one gesture over the selected requests</h2>", "<p>Each selected request still receives its own log event carrying this batch's delivery id: the log never batches (SPEC.md §10.3).</p>", '<label><input type="checkbox" data-select-all> select all (needs JavaScript; the checkboxes above do not)</label>', '<form method="post" action="/decide-batch" id="batch-form">', '<label>note (REQUIRED to reject) <input type="text" name="note"></label> ', '<button type="submit" name="decision" value="grant">Grant selected</button> ', '<button type="submit" name="decision" value="reject">Reject selected</button>', "</form>");
|
|
717
|
+
return page("approval.md — pending decisions", body.join("\n"));
|
|
718
|
+
}
|
|
719
|
+
// -------------------------------------------------------------------------
|
|
720
|
+
// Decisions
|
|
721
|
+
// -------------------------------------------------------------------------
|
|
722
|
+
async decide(form, response) {
|
|
723
|
+
const actionKey = (form.get("action_key") ?? "").trim();
|
|
724
|
+
const verb = form.get("decision");
|
|
725
|
+
const note = (form.get("note") ?? "").trim();
|
|
726
|
+
if (actionKey.length === 0 || (verb !== "grant" && verb !== "reject")) {
|
|
727
|
+
this.counters.refused += 1;
|
|
728
|
+
this.send(response, 422, this.queuePage("The form was incomplete: an action key and a decision of grant or reject are required. Nothing was recorded."));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// A refusal an agent cannot read is a refusal it will retry. Enforced on
|
|
732
|
+
// the server, because `required` in the markup is a courtesy the client
|
|
733
|
+
// controls.
|
|
734
|
+
if (verb === "reject" && note.length === 0) {
|
|
735
|
+
this.counters.refused += 1;
|
|
736
|
+
this.send(response, 422, this.queuePage(`A note is REQUIRED to reject ${actionKey}: an unexplained refusal cannot be acted on. Nothing was recorded — say why and submit again.`));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
// A key this process has never delivered is refused rather than reported:
|
|
740
|
+
// the runtime's queue is consulted once first, because a POST may be the
|
|
741
|
+
// first thing a scripted client does and a request that IS pending should
|
|
742
|
+
// be decidable without a page view.
|
|
743
|
+
if (!this.deliveries.has(actionKey))
|
|
744
|
+
this.currentRequests();
|
|
745
|
+
const delivery = this.deliveries.get(actionKey);
|
|
746
|
+
if (delivery === undefined) {
|
|
747
|
+
this.counters.refused += 1;
|
|
748
|
+
this.send(response, 404, this.queuePage(`${actionKey} was never delivered by this server, so no decision was reported for it. Nothing was recorded.`));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const handler = this.handler;
|
|
752
|
+
if (handler === null) {
|
|
753
|
+
this.counters.refused += 1;
|
|
754
|
+
this.send(response, 503, this.queuePage("The runtime has not registered a decision handler yet, so nothing could be recorded. A channel never records its own decisions (SPEC.md §10.3)."));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const decision = {
|
|
758
|
+
action_key: actionKey,
|
|
759
|
+
decision: verb,
|
|
760
|
+
deliveryId: delivery.deliveryId,
|
|
761
|
+
// A per-request form on a member that was delivered as part of a batch
|
|
762
|
+
// still belongs to that gesture, so the id rides along and the event
|
|
763
|
+
// keeps the audit granularity §10.3 asks for.
|
|
764
|
+
...(delivery.batchDeliveryId === undefined
|
|
765
|
+
? {}
|
|
766
|
+
: { batchDeliveryId: delivery.batchDeliveryId }),
|
|
767
|
+
...(note.length === 0 ? {} : { note }),
|
|
768
|
+
};
|
|
769
|
+
const outcome = handler(decision);
|
|
770
|
+
this.counters.decisions += 1;
|
|
771
|
+
const once = this.notice === null ? null : (this.notice(decision, outcome) ?? null);
|
|
772
|
+
this.send(response, outcome.ok ? 200 : 409, this.outcomePage([{ decision, outcome, once }]));
|
|
773
|
+
}
|
|
774
|
+
async decideBatch(form, response) {
|
|
775
|
+
const verb = form.get("decision");
|
|
776
|
+
const note = (form.get("note") ?? "").trim();
|
|
777
|
+
const keys = form.getAll("select").map((key) => key.trim()).filter((key) => key.length > 0);
|
|
778
|
+
if (verb !== "grant" && verb !== "reject") {
|
|
779
|
+
this.counters.refused += 1;
|
|
780
|
+
this.send(response, 422, this.queuePage("A batch gesture must be grant or reject. Nothing was recorded."));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (keys.length === 0) {
|
|
784
|
+
this.counters.refused += 1;
|
|
785
|
+
this.send(response, 422, this.queuePage("No request was selected, so there was no batch to decide. Nothing was recorded."));
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (verb === "reject" && note.length === 0) {
|
|
789
|
+
this.counters.refused += 1;
|
|
790
|
+
this.send(response, 422, this.queuePage("A note is REQUIRED to reject, batch or not: an unexplained refusal cannot be acted on. Nothing was recorded."));
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const requests = this.currentRequests();
|
|
794
|
+
const selected = [];
|
|
795
|
+
for (const key of keys) {
|
|
796
|
+
const request = requests.find((entry) => entry.action_key.value === key);
|
|
797
|
+
if (request === undefined) {
|
|
798
|
+
this.counters.refused += 1;
|
|
799
|
+
this.send(response, 404, this.queuePage(`${key} is not in the pending queue this server is showing, so the batch was not assembled. Nothing was recorded.`));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
selected.push(request);
|
|
803
|
+
}
|
|
804
|
+
// B7 (SPEC.md §10.3). The refusal happens BEFORE anything is recorded, and
|
|
805
|
+
// the page shows the contract's own code and message rather than a
|
|
806
|
+
// paraphrase: the operator needs to be able to look the code up.
|
|
807
|
+
const assembled = assembleBatch(selected);
|
|
808
|
+
if (!assembled.ok) {
|
|
809
|
+
this.counters.refused += 1;
|
|
810
|
+
const refusal = assembled;
|
|
811
|
+
this.send(response, 422, this.queuePage(`${refusal.code}: ${refusal.message} Nothing was recorded.`));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
this.counter += 1;
|
|
815
|
+
const batchDeliveryId = `web-batch-${this.counter}`;
|
|
816
|
+
for (const request of selected) {
|
|
817
|
+
this.deliveries.set(request.action_key.value, {
|
|
818
|
+
deliveryId: batchDeliveryId,
|
|
819
|
+
batchDeliveryId,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const decisions = selected.map((request) => ({
|
|
823
|
+
action_key: request.action_key.value,
|
|
824
|
+
decision: verb,
|
|
825
|
+
deliveryId: batchDeliveryId,
|
|
826
|
+
batchDeliveryId,
|
|
827
|
+
...(note.length === 0 ? {} : { note }),
|
|
828
|
+
}));
|
|
829
|
+
const results = [];
|
|
830
|
+
if (this.batchHandler !== null) {
|
|
831
|
+
const reported = await this.batchHandler(decisions, batchDeliveryId);
|
|
832
|
+
for (const decision of decisions) {
|
|
833
|
+
const entry = reported.find((item) => item.action_key === decision.action_key);
|
|
834
|
+
if (entry === undefined)
|
|
835
|
+
continue;
|
|
836
|
+
this.counters.decisions += 1;
|
|
837
|
+
results.push({ decision, outcome: entry.outcome, once: entry.notice ?? null });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
const handler = this.handler;
|
|
842
|
+
if (handler === null) {
|
|
843
|
+
this.counters.refused += 1;
|
|
844
|
+
this.send(response, 503, this.queuePage("The runtime has not registered a decision handler yet; nothing was recorded."));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
for (const decision of decisions) {
|
|
848
|
+
const outcome = handler(decision);
|
|
849
|
+
this.counters.decisions += 1;
|
|
850
|
+
results.push({
|
|
851
|
+
decision,
|
|
852
|
+
outcome,
|
|
853
|
+
once: this.notice === null ? null : (this.notice(decision, outcome) ?? null),
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const ok = results.every((entry) => entry.outcome.ok);
|
|
858
|
+
this.send(response, ok ? 200 : 409, this.outcomePage(results, batchDeliveryId));
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* The response page for a gesture.
|
|
862
|
+
*
|
|
863
|
+
* Not a redirect. A redirect-after-POST would either drop the one-shot notice
|
|
864
|
+
* or put an execution token in a URL, and a URL is exactly the place a
|
|
865
|
+
* credential must never be: it lands in history, in referrers, and in every
|
|
866
|
+
* proxy log between here and nowhere.
|
|
867
|
+
*/
|
|
868
|
+
outcomePage(results, batchDeliveryId) {
|
|
869
|
+
const body = [];
|
|
870
|
+
if (batchDeliveryId !== undefined) {
|
|
871
|
+
body.push(`<p>Batch <code>${escapeHtml(batchDeliveryId)}</code> — ${results.length} member(s), one log event each.</p>`);
|
|
872
|
+
}
|
|
873
|
+
for (const entry of results) {
|
|
874
|
+
const { decision, outcome } = entry;
|
|
875
|
+
if (outcome.ok) {
|
|
876
|
+
body.push(`<p><strong>${escapeHtml(outcome.decision === "grant" ? "GRANTED" : "REJECTED")}</strong> <code>${escapeHtml(decision.action_key)}</code> → ${escapeHtml(outcome.state)} at seq ${outcome.record.seq}.</p>`);
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
body.push(`<div class="error">REFUSED <code>${escapeHtml(decision.action_key)}</code> (${escapeHtml(outcome.code)}): ${escapeHtml(outcome.message)}</div>`);
|
|
880
|
+
}
|
|
881
|
+
if (entry.once !== null && entry.once.length > 0) {
|
|
882
|
+
body.push(`<div class="notice"><p>SHOWN ONCE — copy it now. Reload this page and it is gone; the log records only its SHA-256 and nothing can recover it.</p><pre class="payload">${escapeHtml(entry.once)}</pre></div>`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
body.push('<p><a href="/">back to the queue</a></p>');
|
|
886
|
+
return page("approval.md — decision recorded", body.join("\n"));
|
|
887
|
+
}
|
|
888
|
+
send(response, status, html) {
|
|
889
|
+
response.writeHead(status, {
|
|
890
|
+
"content-type": "text/html; charset=utf-8",
|
|
891
|
+
// No caching anywhere: a queue page is a claim about *now*, and one of
|
|
892
|
+
// these responses may carry a single-use token.
|
|
893
|
+
"cache-control": "no-store, no-cache, must-revalidate, private",
|
|
894
|
+
"referrer-policy": "no-referrer",
|
|
895
|
+
"x-content-type-options": "nosniff",
|
|
896
|
+
// The page is entirely self-contained; nothing it renders should be able
|
|
897
|
+
// to reach the network even if an escaping bug ever let markup through.
|
|
898
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'; base-uri 'none'",
|
|
899
|
+
});
|
|
900
|
+
response.end(html);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
//# sourceMappingURL=web.js.map
|