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,837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.approval/env` — the environment SOURCE MAP (SPEC.md §5.2, §11; APRV-73).
|
|
3
|
+
*
|
|
4
|
+
* Every secret this runtime touches is named by the policy and held in an
|
|
5
|
+
* environment variable: `channels.telegram.token_env` (§5.1), the chat id
|
|
6
|
+
* beside it, `audit.sampling_secret_env` (§5.2), `vault.passphrase_env` (§5.2),
|
|
7
|
+
* and `APPROVAL_HUMAN`, which is human identity itself (§11). The policy carries
|
|
8
|
+
* NAMES and never values, which is the right boundary and leaves an operator
|
|
9
|
+
* with five variables to get into a shell before any gate operation works, and
|
|
10
|
+
* no written-down place to say where they come from.
|
|
11
|
+
*
|
|
12
|
+
* This module is that written-down place. `.approval/env` is a source map, not
|
|
13
|
+
* a secret store: `KEY=VALUE` lines whose VALUE says WHERE the value lives.
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* # one line per variable; # comments and blank lines are ignored
|
|
17
|
+
* APPROVAL_HUMAN=human:alice
|
|
18
|
+
* APPROVAL_TG_TOKEN=keychain:approval-telegram-token
|
|
19
|
+
* APPROVAL_VAULT_PASSPHRASE=secret-service:vault-passphrase
|
|
20
|
+
* APPROVAL_AUDIT_SECRET=env:
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Four value forms, and one of them is deliberately unpleasant:
|
|
24
|
+
*
|
|
25
|
+
* - `keychain:<service>` — macOS, `security find-generic-password -a "$USER"
|
|
26
|
+
* -s <service> -w`. The value comes back on stdout and is never in an argv.
|
|
27
|
+
* - `secret-service:<label>` — Linux desktop, `secret-tool lookup approval
|
|
28
|
+
* <label>`. Same property.
|
|
29
|
+
* - `env:` — inherited from the ambient environment. A documentation form: it
|
|
30
|
+
* asserts "this one comes from the shell that launched you", and resolves to
|
|
31
|
+
* nothing on its own.
|
|
32
|
+
* - a bare literal — permitted, and ALWAYS reported as plaintext. A rule people
|
|
33
|
+
* route around is not a control: an operator who is told plainly that their
|
|
34
|
+
* token is sitting in a file in the working tree can decide to accept that,
|
|
35
|
+
* and an operator who is forbidden from writing it there writes it into their
|
|
36
|
+
* shell profile instead, where nothing in this runtime can see it to say so.
|
|
37
|
+
*
|
|
38
|
+
* A value carrying some OTHER `word:` prefix is a literal, not an error, because
|
|
39
|
+
* `APPROVAL_HUMAN=human:alice` is the commonest line this file will ever hold.
|
|
40
|
+
* Near misses of the real schemes are reserved by name and refused rather than
|
|
41
|
+
* silently exported as text; see {@link RESERVED_SCHEMES}.
|
|
42
|
+
*
|
|
43
|
+
* ## THE LOAD-BEARING RULE: nothing loads this file implicitly
|
|
44
|
+
*
|
|
45
|
+
* No verb in this runtime reads `.approval/env` into its own environment. One
|
|
46
|
+
* verb, `approval env`, resolves it and prints an export block for a shell to
|
|
47
|
+
* evaluate; every other command sees only the environment the human who
|
|
48
|
+
* launched it had already established.
|
|
49
|
+
*
|
|
50
|
+
* The reason is `APPROVAL_HUMAN`. SPEC.md §11 states that human identity in v0.1
|
|
51
|
+
* is config-declared and that "the trust boundary is the local machine, and
|
|
52
|
+
* anyone who can set that configuration and write to the log is inside it". A
|
|
53
|
+
* working-tree file that could set `APPROVAL_HUMAN` would move that boundary to
|
|
54
|
+
* "anyone who can write a file in the repository": an agent that can edit source
|
|
55
|
+
* — which every agent working in a repository can — would thereby be able to
|
|
56
|
+
* attest a policy, grant its own request, and set a vault credential, on every
|
|
57
|
+
* invocation, as the human. The whole human-only gate would be one `echo >>`
|
|
58
|
+
* away from being nobody's gate.
|
|
59
|
+
*
|
|
60
|
+
* So the file is inert. A human evaluates `approval env` in their own shell,
|
|
61
|
+
* sees the export block that is about to run (or checks it value-free first with
|
|
62
|
+
* `--check`), and the process that performs a gate operation inherits an
|
|
63
|
+
* environment a human established. This is SPEC.md §11.1 invariant 7, and
|
|
64
|
+
* `tests/cli-env.test.ts` pins it by spawning `doctor`, `policy attest` and
|
|
65
|
+
* `channel telegram health` in a directory holding a complete `.approval/env`
|
|
66
|
+
* and asserting that none of them saw a byte of it.
|
|
67
|
+
*
|
|
68
|
+
* ## Mode 0600
|
|
69
|
+
*
|
|
70
|
+
* A file that may hold a literal secret is refused unless its mode is exactly
|
|
71
|
+
* `0600`, and the refusal prints the `chmod`. This is a lock on a door whose
|
|
72
|
+
* wall is missing (the same session can chmod it back), and it is worth having
|
|
73
|
+
* for the reason `umask` is worth having: the common failure is a
|
|
74
|
+
* world-readable file nobody looked at, not an adversary in the room.
|
|
75
|
+
*
|
|
76
|
+
* ## Determinism, and the one place it stops
|
|
77
|
+
*
|
|
78
|
+
* Parsing is a pure function of the bytes. Resolution is not: it shells out to
|
|
79
|
+
* helper binaries, which is why {@link SourceRunner} exists as an injectable
|
|
80
|
+
* seam and why the tests drive stub `security` / `secret-tool` scripts through
|
|
81
|
+
* PATH rather than touching a real Keychain.
|
|
82
|
+
*
|
|
83
|
+
* Nothing here throws. Every failure is a `{ ok: false, code, message }` from
|
|
84
|
+
* the frozen union {@link ENV_FILE_REFUSAL_CODES}. No credential VALUE appears
|
|
85
|
+
* in a refusal, a message, or a `source` label on any path.
|
|
86
|
+
*/
|
|
87
|
+
import { spawnSync } from "node:child_process";
|
|
88
|
+
import { createHash } from "node:crypto";
|
|
89
|
+
import { chmodSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
90
|
+
import { userInfo } from "node:os";
|
|
91
|
+
import { basename, dirname, join } from "node:path";
|
|
92
|
+
import { HUMAN_ACTOR_ENV } from "./attest.js";
|
|
93
|
+
import { telegramChatEnvFor, telegramTokenEnvFor } from "./telegram-config.js";
|
|
94
|
+
import { passphraseEnvFor } from "./vault.js";
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Location
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/** The source map's filename, beside the log's home: `.approval/env`. */
|
|
99
|
+
export const ENV_FILENAME = "env";
|
|
100
|
+
/**
|
|
101
|
+
* The env file for a given log path — derived exactly as `vaultPathFor` derives
|
|
102
|
+
* the vault, so the log, the payload store, the vault and this file stay under
|
|
103
|
+
* one home: SPEC.md §9 fixes the log at `<home>/log/events.jsonl`, so the source
|
|
104
|
+
* map is `<home>/env`, a sibling of the log DIRECTORY and never inside it.
|
|
105
|
+
*/
|
|
106
|
+
export function envFilePathFor(logPath) {
|
|
107
|
+
const logDir = dirname(logPath);
|
|
108
|
+
const home = basename(logDir) === "log" ? dirname(logDir) : logDir;
|
|
109
|
+
return join(home, ENV_FILENAME);
|
|
110
|
+
}
|
|
111
|
+
/** The mode the file must have. Anything else is refused. */
|
|
112
|
+
export const ENV_FILE_REQUIRED_MODE = 0o600;
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Refusals
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Everything this module can refuse. Frozen public API, per SPEC.md §11.1(6).
|
|
118
|
+
*
|
|
119
|
+
* Each code names a different repair, which is the test of whether a code earns
|
|
120
|
+
* its place. The three helper codes are separate for exactly that reason: "you
|
|
121
|
+
* are on a machine without `secret-tool`", "the item is not in your keychain",
|
|
122
|
+
* and "the helper ran and failed" are three different mornings.
|
|
123
|
+
*/
|
|
124
|
+
export const ENV_FILE_REFUSAL_CODES = [
|
|
125
|
+
/** The file's mode is not 0600. The refusal carries the `chmod` to run. */
|
|
126
|
+
"env-file-mode",
|
|
127
|
+
/**
|
|
128
|
+
* The file exists and could not be read, stat'd, or (APRV-74) written. A
|
|
129
|
+
* filesystem fact in every case, with a filesystem repair, which is why the
|
|
130
|
+
* write path reuses this code rather than adding a fourth I/O name to a
|
|
131
|
+
* frozen union: "the directory is read-only" and "the file is unreadable"
|
|
132
|
+
* are the same morning and the same exit code.
|
|
133
|
+
*/
|
|
134
|
+
"env-file-io",
|
|
135
|
+
/** A line is neither blank, nor a comment, nor `KEY=VALUE`. */
|
|
136
|
+
"env-file-syntax",
|
|
137
|
+
/** A KEY does not match `[A-Z_][A-Z0-9_]*`. No `export ` prefix is accepted. */
|
|
138
|
+
"env-file-key-invalid",
|
|
139
|
+
/** The same KEY appears twice. Which one wins is not a thing to guess at. */
|
|
140
|
+
"env-file-duplicate-key",
|
|
141
|
+
/** A VALUE carries a `scheme:` prefix this build does not implement. */
|
|
142
|
+
"env-file-unknown-scheme",
|
|
143
|
+
/** A KEY with an empty VALUE. An empty secret is a configuration error. */
|
|
144
|
+
"env-file-empty-value",
|
|
145
|
+
/** The helper binary for a scheme is not on PATH. Not the operator's fault. */
|
|
146
|
+
"helper-binary-missing",
|
|
147
|
+
/** The helper ran and the named item is not there. Store it, or fix the name. */
|
|
148
|
+
"helper-item-missing",
|
|
149
|
+
/** The helper ran and failed for some other reason (locked keyring, …). */
|
|
150
|
+
"helper-failed",
|
|
151
|
+
/**
|
|
152
|
+
* A policy declared an `_env` NAME that is not a usable shell variable name.
|
|
153
|
+
* Never emitted as an `export` line: the export block is evaluated by a shell,
|
|
154
|
+
* and a name carrying a space or a `;` would be a policy file executing code.
|
|
155
|
+
*/
|
|
156
|
+
"invalid-variable-name",
|
|
157
|
+
];
|
|
158
|
+
function refuse(code, path, message, line) {
|
|
159
|
+
return line === undefined
|
|
160
|
+
? { ok: false, code, message, path }
|
|
161
|
+
: { ok: false, code, message, path, line };
|
|
162
|
+
}
|
|
163
|
+
function detail(cause) {
|
|
164
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
165
|
+
}
|
|
166
|
+
/** `[A-Z_][A-Z0-9_]*` — the shape of an environment variable name. */
|
|
167
|
+
const KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/u;
|
|
168
|
+
/**
|
|
169
|
+
* A shell-safe variable name. Deliberately laxer than {@link KEY_PATTERN}
|
|
170
|
+
* (lower case is legal in a shell) and still a closed character set, because
|
|
171
|
+
* these names arrive from a policy file and are emitted into an `export` line.
|
|
172
|
+
*/
|
|
173
|
+
const SHELL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
174
|
+
/**
|
|
175
|
+
* A `scheme:` prefix. Lower-case letters only, so a bare literal that happens to
|
|
176
|
+
* contain a colon (`7654321:AA…`, a Telegram token, is the case that matters) is
|
|
177
|
+
* a literal and not a mystery scheme.
|
|
178
|
+
*/
|
|
179
|
+
const SCHEME_PATTERN = /^([a-z][a-z0-9-]*):(.*)$/su;
|
|
180
|
+
/** The schemes this build implements. */
|
|
181
|
+
const KNOWN_SCHEMES = new Set([
|
|
182
|
+
"keychain",
|
|
183
|
+
"secret-service",
|
|
184
|
+
"env",
|
|
185
|
+
"literal",
|
|
186
|
+
]);
|
|
187
|
+
/**
|
|
188
|
+
* The scheme namespace is a CLOSED, RESERVED LIST, and a `word:` prefix outside
|
|
189
|
+
* it is a literal rather than an error.
|
|
190
|
+
*
|
|
191
|
+
* The rule earns its complexity from one line, which is the commonest line this
|
|
192
|
+
* file will ever hold:
|
|
193
|
+
*
|
|
194
|
+
* ```
|
|
195
|
+
* APPROVAL_HUMAN=human:alice
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* `human:<id>` is the actor syntax of SPEC.md §8. A parser that treated every
|
|
199
|
+
* `word:` prefix as a scheme would refuse the identity line every operator
|
|
200
|
+
* writes first and send them to `literal:human:alice`, which nobody would ever
|
|
201
|
+
* guess and nobody should have to.
|
|
202
|
+
*
|
|
203
|
+
* A silent literal reading is still wrong for a MISTYPED source, though:
|
|
204
|
+
* `keyring:approval-token` would export the eleven characters "keyring:app…" as
|
|
205
|
+
* a bot token, and the failure would surface as a 401 from Telegram hours later.
|
|
206
|
+
* So the near misses and the plausible future schemes are reserved by name and
|
|
207
|
+
* refused, and everything else is a literal. Adding a scheme later means moving
|
|
208
|
+
* a string from this set to {@link KNOWN_SCHEMES}, which is a reviewable diff
|
|
209
|
+
* and never a change in meaning for an existing file: a reserved prefix has
|
|
210
|
+
* never been readable as a literal.
|
|
211
|
+
*/
|
|
212
|
+
const RESERVED_SCHEMES = new Set([
|
|
213
|
+
// Misspellings and near neighbours of the two implemented helpers.
|
|
214
|
+
"keyring",
|
|
215
|
+
"keychains",
|
|
216
|
+
"keychain-service",
|
|
217
|
+
"secret_service",
|
|
218
|
+
"secretservice",
|
|
219
|
+
"secret-tool",
|
|
220
|
+
"secrettool",
|
|
221
|
+
"secrets",
|
|
222
|
+
// Words that could only be meant as a source.
|
|
223
|
+
"environment",
|
|
224
|
+
"plaintext",
|
|
225
|
+
"plain",
|
|
226
|
+
"file",
|
|
227
|
+
"exec",
|
|
228
|
+
"command",
|
|
229
|
+
"shell",
|
|
230
|
+
// Credential managers a future scheme would plausibly be named after.
|
|
231
|
+
"pass",
|
|
232
|
+
"gopass",
|
|
233
|
+
"op",
|
|
234
|
+
"onepassword",
|
|
235
|
+
"bitwarden",
|
|
236
|
+
"lastpass",
|
|
237
|
+
"vault",
|
|
238
|
+
"wincred",
|
|
239
|
+
]);
|
|
240
|
+
/**
|
|
241
|
+
* Parse the file's text. No interpolation, no quote stripping, no `export `
|
|
242
|
+
* prefix, no line continuations: this is a source map, not a shell script, and
|
|
243
|
+
* every one of those features is a way for a file to mean something other than
|
|
244
|
+
* what it looks like.
|
|
245
|
+
*
|
|
246
|
+
* Quotes are NOT stripped, which is worth saying out loud because `.env` files
|
|
247
|
+
* elsewhere do strip them: here `A="b"` is the five-character literal `"b"`.
|
|
248
|
+
*/
|
|
249
|
+
export function parseEnvFile(text, path) {
|
|
250
|
+
const entries = [];
|
|
251
|
+
const seen = new Map();
|
|
252
|
+
const lines = text.split("\n");
|
|
253
|
+
for (const [index, raw] of lines.entries()) {
|
|
254
|
+
const line = index + 1;
|
|
255
|
+
const trimmed = raw.trim();
|
|
256
|
+
if (trimmed.length === 0 || trimmed.startsWith("#"))
|
|
257
|
+
continue;
|
|
258
|
+
if (trimmed.startsWith("export ")) {
|
|
259
|
+
return refuse("env-file-syntax", path, `line ${String(line)} begins with \`export \`: this file is a source map read by \`approval env\`, not a shell script, and every line is \`KEY=VALUE\`. Drop the \`export \`.`, line);
|
|
260
|
+
}
|
|
261
|
+
const equals = trimmed.indexOf("=");
|
|
262
|
+
if (equals === -1) {
|
|
263
|
+
return refuse("env-file-syntax", path, `line ${String(line)} is neither blank, a # comment, nor KEY=VALUE`, line);
|
|
264
|
+
}
|
|
265
|
+
const key = trimmed.slice(0, equals);
|
|
266
|
+
const value = trimmed.slice(equals + 1);
|
|
267
|
+
if (!KEY_PATTERN.test(key)) {
|
|
268
|
+
return refuse("env-file-key-invalid", path, `line ${String(line)}: ${JSON.stringify(key)} is not an environment variable name (it must match [A-Z_][A-Z0-9_]*)`, line);
|
|
269
|
+
}
|
|
270
|
+
const first = seen.get(key);
|
|
271
|
+
if (first !== undefined) {
|
|
272
|
+
return refuse("env-file-duplicate-key", path, `${key} is set twice, on lines ${String(first)} and ${String(line)}; which one wins is not something a runtime should guess`, line);
|
|
273
|
+
}
|
|
274
|
+
seen.set(key, line);
|
|
275
|
+
if (value.length === 0) {
|
|
276
|
+
return refuse("env-file-empty-value", path, `line ${String(line)}: ${key} has an empty value. Use \`${key}=env:\` to say it is inherited from the shell, or delete the line.`, line);
|
|
277
|
+
}
|
|
278
|
+
const scheme = SCHEME_PATTERN.exec(value);
|
|
279
|
+
if (scheme === null) {
|
|
280
|
+
entries.push({ key, kind: "literal", argument: value, line });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const name = scheme[1];
|
|
284
|
+
const argument = scheme[2];
|
|
285
|
+
if (!KNOWN_SCHEMES.has(name)) {
|
|
286
|
+
if (!RESERVED_SCHEMES.has(name)) {
|
|
287
|
+
// Not a scheme at all: `human:alice` and every other value that merely
|
|
288
|
+
// contains a colon. See RESERVED_SCHEMES for why this is the default.
|
|
289
|
+
entries.push({ key, kind: "literal", argument: value, line });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
return refuse("env-file-unknown-scheme", path, `line ${String(line)}: ${key} names the source scheme ${JSON.stringify(`${name}:`)}, which is reserved and which this build does not implement. Implemented: keychain:<service>, secret-service:<label>, env: (inherited), literal:<value>, or a bare value (a plaintext literal). If you really meant the literal text, write \`${key}=literal:${name}:…\` — a reserved prefix is never read as a literal by accident.`, line);
|
|
293
|
+
}
|
|
294
|
+
if (name === "literal") {
|
|
295
|
+
if (argument.length === 0) {
|
|
296
|
+
return refuse("env-file-empty-value", path, `line ${String(line)}: ${key} is \`literal:\` with nothing after it`, line);
|
|
297
|
+
}
|
|
298
|
+
entries.push({ key, kind: "literal", argument, line });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (name === "env") {
|
|
302
|
+
// `env:whatever` is not a rename facility; the ambient variable is the
|
|
303
|
+
// one with this KEY, and anything after the colon would be a second,
|
|
304
|
+
// silent naming convention.
|
|
305
|
+
if (argument.length > 0) {
|
|
306
|
+
return refuse("env-file-syntax", path, `line ${String(line)}: \`env:\` takes nothing after the colon — it means "${key} is inherited from the shell that launched the process", and there is deliberately no way to inherit one variable under another's name`, line);
|
|
307
|
+
}
|
|
308
|
+
entries.push({ key, kind: "env", argument: "", line });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (argument.length === 0) {
|
|
312
|
+
return refuse("env-file-syntax", path, `line ${String(line)}: \`${name}:\` needs ${name === "keychain" ? "a service name" : "a label"} after the colon`, line);
|
|
313
|
+
}
|
|
314
|
+
entries.push({ key, kind: name, argument, line });
|
|
315
|
+
}
|
|
316
|
+
return { ok: true, entries };
|
|
317
|
+
}
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Reading
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
/**
|
|
322
|
+
* The digest of an env file's bytes, as read.
|
|
323
|
+
*
|
|
324
|
+
* Not a secret and not derived from one in the sense that matters: the file may
|
|
325
|
+
* carry a plaintext literal, so this is a hash and never the text, and it is
|
|
326
|
+
* one-way. Its only consumer is `core/instance.ts`, which uses it to ask "was
|
|
327
|
+
* this exported value produced from the file as it now reads?" — a question
|
|
328
|
+
* about VERSIONS of a file, which needs an identifier for a version and nothing
|
|
329
|
+
* else (APRV-278).
|
|
330
|
+
*/
|
|
331
|
+
export function envFileDigest(text) {
|
|
332
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
333
|
+
}
|
|
334
|
+
/** An absent file's digest: the digest of the nothing that was read. */
|
|
335
|
+
export const ABSENT_ENV_FILE_DIGEST = envFileDigest("");
|
|
336
|
+
/**
|
|
337
|
+
* Read and parse the source map.
|
|
338
|
+
*
|
|
339
|
+
* **An absent file is not an error.** Nobody has written one, which is the state
|
|
340
|
+
* of every working directory that keeps its variables in a shell profile, and it
|
|
341
|
+
* is the state `approval init` leaves behind. Every variable then falls to
|
|
342
|
+
* "inherited from the environment" or "unset", which is exactly the world
|
|
343
|
+
* before this file existed.
|
|
344
|
+
*/
|
|
345
|
+
export function readEnvFile(path) {
|
|
346
|
+
let stats;
|
|
347
|
+
try {
|
|
348
|
+
stats = statSync(path);
|
|
349
|
+
}
|
|
350
|
+
catch (cause) {
|
|
351
|
+
if (cause.code === "ENOENT") {
|
|
352
|
+
return { ok: true, present: false, path, entries: [], digest: ABSENT_ENV_FILE_DIGEST };
|
|
353
|
+
}
|
|
354
|
+
return refuse("env-file-io", path, `${path} could not be stat'd: ${detail(cause)}`);
|
|
355
|
+
}
|
|
356
|
+
if (!stats.isFile()) {
|
|
357
|
+
return refuse("env-file-io", path, `${path} is not a regular file`);
|
|
358
|
+
}
|
|
359
|
+
const mode = stats.mode & 0o777;
|
|
360
|
+
if (mode !== ENV_FILE_REQUIRED_MODE) {
|
|
361
|
+
return refuse("env-file-mode", path, `${path} has mode ${mode.toString(8).padStart(4, "0")}, and this file may carry a plaintext secret, so it is read only at 0600. Run:\n chmod 600 ${path}`);
|
|
362
|
+
}
|
|
363
|
+
let text;
|
|
364
|
+
try {
|
|
365
|
+
text = readFileSync(path, "utf8");
|
|
366
|
+
}
|
|
367
|
+
catch (cause) {
|
|
368
|
+
return refuse("env-file-io", path, `${path} could not be read: ${detail(cause)}`);
|
|
369
|
+
}
|
|
370
|
+
const parsed = parseEnvFile(text, path);
|
|
371
|
+
if (!parsed.ok)
|
|
372
|
+
return parsed;
|
|
373
|
+
return { ok: true, present: true, path, entries: parsed.entries, digest: envFileDigest(text) };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Add or replace `KEY=VALUE` lines, preserving everything else in the file.
|
|
377
|
+
*
|
|
378
|
+
* **Line-oriented, not a rewrite.** The file is read as text, the line whose
|
|
379
|
+
* KEY matches is replaced IN PLACE, and a key that is not present is appended
|
|
380
|
+
* at the end. Comments, blank lines, ordering, and every entry this call was
|
|
381
|
+
* not asked about survive byte for byte. A writer that reparsed and re-emitted
|
|
382
|
+
* would be simpler and would quietly delete the operator's own comments the
|
|
383
|
+
* first time `approval setup channel telegram` ran — this file is one a human edits by
|
|
384
|
+
* hand, and round-trip fidelity for a hand-edited file is the same requirement
|
|
385
|
+
* the Backlog.md task files carry.
|
|
386
|
+
*
|
|
387
|
+
* The file is validated before it is touched: {@link readEnvFile}'s mode check
|
|
388
|
+
* and full parse both run, so `setup` never appends a line to a file it could
|
|
389
|
+
* not have read, and never lands a valid line in a file whose earlier line is a
|
|
390
|
+
* syntax error. A file that does not exist is created at 0600, along with its
|
|
391
|
+
* directory.
|
|
392
|
+
*
|
|
393
|
+
* Callers pass values, and a value here is a SOURCE (`keychain:<service>`), a
|
|
394
|
+
* chat id, or an identity — never a credential, except on the one path where an
|
|
395
|
+
* operator explicitly chose a plaintext literal after being told what it means.
|
|
396
|
+
* Nothing in this function prints anything.
|
|
397
|
+
*/
|
|
398
|
+
export function upsertEnvFileEntries(path, entries) {
|
|
399
|
+
const existing = readEnvFile(path);
|
|
400
|
+
if (!existing.ok)
|
|
401
|
+
return existing;
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
if (!KEY_PATTERN.test(entry.key)) {
|
|
404
|
+
return refuse("env-file-key-invalid", path, `${JSON.stringify(entry.key)} is not an environment variable name (it must match [A-Z_][A-Z0-9_]*), so no line was written`);
|
|
405
|
+
}
|
|
406
|
+
if (entry.value.length === 0) {
|
|
407
|
+
return refuse("env-file-empty-value", path, `${entry.key} would be written with an empty value, which is not a source; nothing was written`);
|
|
408
|
+
}
|
|
409
|
+
if (entry.value.includes("\n")) {
|
|
410
|
+
return refuse("env-file-syntax", path, `the value for ${entry.key} contains a newline, and one line is one variable; nothing was written`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
let text = "";
|
|
414
|
+
if (existing.present) {
|
|
415
|
+
try {
|
|
416
|
+
text = readFileSync(path, "utf8");
|
|
417
|
+
}
|
|
418
|
+
catch (cause) {
|
|
419
|
+
return refuse("env-file-io", path, `${path} could not be re-read: ${detail(cause)}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Split into lines WITHOUT the trailing terminator, so an append lands on its
|
|
423
|
+
// own line and the file ends with exactly one newline whichever state it was
|
|
424
|
+
// in. An empty (or absent) file is zero lines, not one empty one.
|
|
425
|
+
const lines = text.length === 0 ? [] : (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
|
|
426
|
+
const changes = [];
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
const line = `${entry.key}=${entry.value}`;
|
|
429
|
+
const index = lines.findIndex((candidate) => {
|
|
430
|
+
const trimmed = candidate.trim();
|
|
431
|
+
if (trimmed.startsWith("#"))
|
|
432
|
+
return false;
|
|
433
|
+
const equals = trimmed.indexOf("=");
|
|
434
|
+
return equals !== -1 && trimmed.slice(0, equals) === entry.key;
|
|
435
|
+
});
|
|
436
|
+
if (index === -1) {
|
|
437
|
+
lines.push(line);
|
|
438
|
+
changes.push({ key: entry.key, value: entry.value, previous: null, unchanged: false });
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const before = lines[index].trim();
|
|
442
|
+
const previous = before.slice(before.indexOf("=") + 1);
|
|
443
|
+
lines[index] = line;
|
|
444
|
+
changes.push({
|
|
445
|
+
key: entry.key,
|
|
446
|
+
value: entry.value,
|
|
447
|
+
previous,
|
|
448
|
+
unchanged: previous === entry.value,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const body = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
452
|
+
try {
|
|
453
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
454
|
+
// 0600 at creation, and re-asserted after: `writeFileSync`'s mode argument
|
|
455
|
+
// is a request against the umask on creation and is ignored entirely for an
|
|
456
|
+
// existing file, so the explicit chmod is what actually holds the guarantee
|
|
457
|
+
// the reader depends on.
|
|
458
|
+
writeFileSync(path, body, { encoding: "utf8", mode: ENV_FILE_REQUIRED_MODE });
|
|
459
|
+
chmodSync(path, ENV_FILE_REQUIRED_MODE);
|
|
460
|
+
}
|
|
461
|
+
catch (cause) {
|
|
462
|
+
return refuse("env-file-io", path, `${path} could not be written: ${detail(cause)}`);
|
|
463
|
+
}
|
|
464
|
+
return { ok: true, path, created: !existing.present, changes };
|
|
465
|
+
}
|
|
466
|
+
/** One trailing newline, and nothing else, is removed. `security -w` adds one. */
|
|
467
|
+
function stripOneNewline(text) {
|
|
468
|
+
return text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* `security find-generic-password -a "$USER" -s <service> -w` and
|
|
472
|
+
* `secret-tool lookup approval <label>`.
|
|
473
|
+
*
|
|
474
|
+
* THE VALUE IS NEVER IN AN ARGV, in either direction: the argv carries a service
|
|
475
|
+
* name or a label, and the secret comes back on stdout. An argv is world-readable
|
|
476
|
+
* in `ps` for the length of the call, which is the whole reason `approval vault
|
|
477
|
+
* set` has no `--value` flag either.
|
|
478
|
+
*
|
|
479
|
+
* The exit-status readings are documented heuristics, not contracts. `security`
|
|
480
|
+
* exits 44 (`errSecItemNotFound`) for a missing item; `secret-tool` exits 0 with
|
|
481
|
+
* empty output when the lookup matches nothing. Anything else is
|
|
482
|
+
* {@link "helper-failed"}, which is the honest answer for a locked keyring or a
|
|
483
|
+
* D-Bus that is not running: the repair is not "store the item".
|
|
484
|
+
*/
|
|
485
|
+
/**
|
|
486
|
+
* The prefix a deferred lookup carries. See {@link NON_RESOLVING_RUNNER}.
|
|
487
|
+
*/
|
|
488
|
+
export const KEYSTORE_DEFERRED = "not resolved by doctor";
|
|
489
|
+
/**
|
|
490
|
+
* A {@link SourceRunner} that looks nothing up (moved here by APRV-178).
|
|
491
|
+
*
|
|
492
|
+
* `security find-generic-password -w` can raise a keychain-unlock or ACL dialog
|
|
493
|
+
* and `secret-tool lookup` can block on a keyring prompt. Either would hang a
|
|
494
|
+
* command run over ssh or from CI, and a command that pops a keychain prompt
|
|
495
|
+
* also TEACHES people to click through keychain prompts. So the diagnostics —
|
|
496
|
+
* `approval doctor`, and `approval up`'s cross-instance report — resolve
|
|
497
|
+
* keystore-backed variables not at all: they report the scheme and the service
|
|
498
|
+
* name, which `.approval/env` already carries in the open, and leave the actual
|
|
499
|
+
* lookup to `approval env --check`, which a human runs deliberately and watches.
|
|
500
|
+
*
|
|
501
|
+
* It lives beside {@link defaultSourceRunner} rather than in one of its callers
|
|
502
|
+
* because two of them now need it and a second copy would be a second set of
|
|
503
|
+
* words for the same refusal.
|
|
504
|
+
*/
|
|
505
|
+
export const NON_RESOLVING_RUNNER = {
|
|
506
|
+
keychain(service) {
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
code: "helper-failed",
|
|
510
|
+
message: `${KEYSTORE_DEFERRED}: keychain:${service} is declared here and looked up by \`approval env --check\`. \`security find-generic-password -w\` can block on a keychain-unlock or ACL prompt, and a diagnostic must never hang or ask a human for a password`,
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
secretService(label) {
|
|
514
|
+
return {
|
|
515
|
+
ok: false,
|
|
516
|
+
code: "helper-failed",
|
|
517
|
+
message: `${KEYSTORE_DEFERRED}: secret-service:${label} is declared here and looked up by \`approval env --check\`. \`secret-tool lookup\` can block on a keyring-unlock prompt, and a diagnostic must never hang or ask a human for a password`,
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
export const defaultSourceRunner = {
|
|
522
|
+
keychain(service) {
|
|
523
|
+
const account = process.env["USER"] ?? userInfo().username;
|
|
524
|
+
const result = spawnSync("security", ["find-generic-password", "-a", account, "-s", service, "-w"], { encoding: "utf8" });
|
|
525
|
+
if (result.error !== undefined) {
|
|
526
|
+
const code = result.error.code;
|
|
527
|
+
if (code === "ENOENT") {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
code: "helper-binary-missing",
|
|
531
|
+
message: "`security` is not on PATH — keychain: sources are macOS-only; on Linux use secret-service:<label>",
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return { ok: false, code: "helper-failed", message: `security could not be run: ${detail(result.error)}` };
|
|
535
|
+
}
|
|
536
|
+
if (result.status === 0) {
|
|
537
|
+
return { ok: true, value: stripOneNewline(result.stdout) };
|
|
538
|
+
}
|
|
539
|
+
if (result.status === 44) {
|
|
540
|
+
return {
|
|
541
|
+
ok: false,
|
|
542
|
+
code: "helper-item-missing",
|
|
543
|
+
message: `no generic password for service ${JSON.stringify(service)} and account ${JSON.stringify(account)} in the login keychain`,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
code: "helper-failed",
|
|
549
|
+
message: `security exited ${String(result.status)} looking up service ${JSON.stringify(service)}`,
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
secretService(label) {
|
|
553
|
+
const result = spawnSync("secret-tool", ["lookup", "approval", label], { encoding: "utf8" });
|
|
554
|
+
if (result.error !== undefined) {
|
|
555
|
+
const code = result.error.code;
|
|
556
|
+
if (code === "ENOENT") {
|
|
557
|
+
return {
|
|
558
|
+
ok: false,
|
|
559
|
+
code: "helper-binary-missing",
|
|
560
|
+
message: "`secret-tool` is not on PATH — install libsecret-tools (Debian/Ubuntu) or libsecret (Fedora/Arch); on macOS use keychain:<service>",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
code: "helper-failed",
|
|
566
|
+
message: `secret-tool could not be run: ${detail(result.error)}`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (result.status === 0) {
|
|
570
|
+
const value = stripOneNewline(result.stdout);
|
|
571
|
+
if (value.length === 0) {
|
|
572
|
+
return {
|
|
573
|
+
ok: false,
|
|
574
|
+
code: "helper-item-missing",
|
|
575
|
+
message: `no secret-service item with attribute approval=${JSON.stringify(label)}`,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return { ok: true, value };
|
|
579
|
+
}
|
|
580
|
+
if (result.status === 1) {
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
code: "helper-item-missing",
|
|
584
|
+
message: `no secret-service item with attribute approval=${JSON.stringify(label)}`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
code: "helper-failed",
|
|
590
|
+
message: `secret-tool exited ${String(result.status)} looking up ${JSON.stringify(label)}`,
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
/** The declared source of an entry, with a literal's value dropped. */
|
|
595
|
+
function declaredSourceOf(entry) {
|
|
596
|
+
return entry.kind === "keychain" || entry.kind === "secret-service"
|
|
597
|
+
? { kind: entry.kind, service: entry.argument, line: entry.line }
|
|
598
|
+
: { kind: entry.kind, line: entry.line };
|
|
599
|
+
}
|
|
600
|
+
/** A declared source in words, and never a value. */
|
|
601
|
+
export function describeDeclaredSource(source) {
|
|
602
|
+
if (source.kind === "literal")
|
|
603
|
+
return LITERAL_SOURCE;
|
|
604
|
+
if (source.kind === "env")
|
|
605
|
+
return "env: (inherited)";
|
|
606
|
+
return `${source.kind}:${source.service ?? ""}`;
|
|
607
|
+
}
|
|
608
|
+
/** `approval setup <thing>`, or the generic advice for a policy-invented name. */
|
|
609
|
+
function setupFix(thing, name) {
|
|
610
|
+
return thing === null
|
|
611
|
+
? `no setup verb knows this variable: export ${name}=… in your shell, or add a \`${name}=…\` line to .approval/env`
|
|
612
|
+
: `run \`approval setup ${thing}\` (APRV-74), or add a \`${name}=…\` line to .approval/env`;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Walk the loaded policy for every string-valued key whose name ends in `_env`.
|
|
616
|
+
*
|
|
617
|
+
* Depth-unbounded and key-name-driven on purpose. `_env` is this project's whole
|
|
618
|
+
* naming convention for "the policy carries a NAME" (§5.1 `token_env`,
|
|
619
|
+
* `chat_id_env`; §5.2 `sampling_secret_env`, `vault.passphrase_env`), and a
|
|
620
|
+
* future key that follows the convention should appear in `approval env` on the
|
|
621
|
+
* day it is added to the schema, not on the day someone remembers to add it
|
|
622
|
+
* here. The four known keys above are added by name first and win the dedupe, so
|
|
623
|
+
* this walk contributes only the ones nothing else knows about.
|
|
624
|
+
*
|
|
625
|
+
* Arrays are walked; non-string values under an `_env` key are skipped, because
|
|
626
|
+
* `token_env: 42` is a schema problem and not a variable name.
|
|
627
|
+
*/
|
|
628
|
+
function walkEnvNames(node, found) {
|
|
629
|
+
if (Array.isArray(node)) {
|
|
630
|
+
for (const item of node)
|
|
631
|
+
walkEnvNames(item, found);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (node === null || typeof node !== "object")
|
|
635
|
+
return;
|
|
636
|
+
for (const [key, value] of Object.entries(node)) {
|
|
637
|
+
if (key.endsWith("_env") && typeof value === "string" && value.length > 0) {
|
|
638
|
+
found.set(value, true);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
walkEnvNames(value, found);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* The variables `approval env` answers for, in a stable order.
|
|
646
|
+
*
|
|
647
|
+
* Five by name — human identity, the Telegram token and chat id, the vault
|
|
648
|
+
* passphrase, and the sampling secret — plus whatever else the policy names by
|
|
649
|
+
* the `_env` convention.
|
|
650
|
+
*
|
|
651
|
+
* The sampling secret is the one conditional member: `audit.sampling_secret_env`
|
|
652
|
+
* has NO default (an unnamed one disables sampling, SPEC.md §5.2), so listing a
|
|
653
|
+
* made-up variable for it would invent configuration the operator never chose.
|
|
654
|
+
* The other four all have defaults and are always listed.
|
|
655
|
+
*/
|
|
656
|
+
export function wantedVariables(load) {
|
|
657
|
+
const policy = load.ok ? load.policy : null;
|
|
658
|
+
const declaredTelegram = (key) => {
|
|
659
|
+
const telegram = policy?.channels?.["telegram"];
|
|
660
|
+
const value = telegram === undefined ? undefined : telegram[key];
|
|
661
|
+
return typeof value === "string" && value.length > 0;
|
|
662
|
+
};
|
|
663
|
+
const wanted = [
|
|
664
|
+
{
|
|
665
|
+
name: HUMAN_ACTOR_ENV,
|
|
666
|
+
declared: false,
|
|
667
|
+
secretBearing: false,
|
|
668
|
+
fix: setupFix("identity", HUMAN_ACTOR_ENV),
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: telegramTokenEnvFor(load),
|
|
672
|
+
declared: declaredTelegram("token_env"),
|
|
673
|
+
secretBearing: true,
|
|
674
|
+
fix: setupFix("channel telegram", telegramTokenEnvFor(load)),
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: telegramChatEnvFor(load),
|
|
678
|
+
declared: declaredTelegram("chat_id_env"),
|
|
679
|
+
secretBearing: false,
|
|
680
|
+
fix: setupFix("channel telegram", telegramChatEnvFor(load)),
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: passphraseEnvFor(load),
|
|
684
|
+
declared: typeof policy?.vault?.passphrase_env === "string" &&
|
|
685
|
+
policy.vault.passphrase_env.length > 0,
|
|
686
|
+
secretBearing: true,
|
|
687
|
+
fix: setupFix("vault", passphraseEnvFor(load)),
|
|
688
|
+
},
|
|
689
|
+
];
|
|
690
|
+
const samplingEnv = policy?.audit?.sampling_secret_env;
|
|
691
|
+
if (typeof samplingEnv === "string" && samplingEnv.length > 0) {
|
|
692
|
+
wanted.push({
|
|
693
|
+
name: samplingEnv,
|
|
694
|
+
declared: true,
|
|
695
|
+
secretBearing: true,
|
|
696
|
+
fix: setupFix("sampling", samplingEnv),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const taken = new Set(wanted.map((entry) => entry.name));
|
|
700
|
+
const walked = new Map();
|
|
701
|
+
if (policy !== null)
|
|
702
|
+
walkEnvNames(policy, walked);
|
|
703
|
+
for (const name of [...walked.keys()].sort()) {
|
|
704
|
+
if (taken.has(name))
|
|
705
|
+
continue;
|
|
706
|
+
taken.add(name);
|
|
707
|
+
wanted.push({
|
|
708
|
+
// A name nothing in this build understands. Treated as secret-bearing,
|
|
709
|
+
// which is the stricter reading and the right default for a variable
|
|
710
|
+
// whose whole purpose, by the `_env` convention, is to hold a value the
|
|
711
|
+
// policy refused to write down.
|
|
712
|
+
name,
|
|
713
|
+
declared: true,
|
|
714
|
+
secretBearing: true,
|
|
715
|
+
fix: setupFix(null, name),
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return wanted;
|
|
719
|
+
}
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
// Resolution
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
const LITERAL_SOURCE = "literal (plaintext in .approval/env)";
|
|
724
|
+
/**
|
|
725
|
+
* Resolve every variable the policy implies, against the ambient environment
|
|
726
|
+
* first and the source map second.
|
|
727
|
+
*
|
|
728
|
+
* **The ambient environment always wins.** A variable already exported in the
|
|
729
|
+
* calling shell is reported `set-in-environment` and its file entry is not even
|
|
730
|
+
* consulted: the human's shell is the authority (that is invariant 7's whole
|
|
731
|
+
* point), and a file that could override an exported value would be a file that
|
|
732
|
+
* silently redirects a gate operation's credentials.
|
|
733
|
+
*
|
|
734
|
+
* A whole-file refusal (bad mode, unreadable, unparseable) is returned as-is:
|
|
735
|
+
* partial resolution of a file the runtime cannot fully read is how a typo turns
|
|
736
|
+
* into a half-configured environment.
|
|
737
|
+
*/
|
|
738
|
+
export function resolveEnvironment(load, envFilePath, runner = defaultSourceRunner, ambientEnv = process.env) {
|
|
739
|
+
const file = readEnvFile(envFilePath);
|
|
740
|
+
if (!file.ok)
|
|
741
|
+
return file;
|
|
742
|
+
const byKey = new Map();
|
|
743
|
+
for (const entry of file.entries)
|
|
744
|
+
byKey.set(entry.key, entry);
|
|
745
|
+
const variables = [];
|
|
746
|
+
for (const want of wantedVariables(load)) {
|
|
747
|
+
variables.push(resolveOne(want, byKey.get(want.name), runner, ambientEnv));
|
|
748
|
+
}
|
|
749
|
+
return { ok: true, present: file.present, path: file.path, digest: file.digest, variables };
|
|
750
|
+
}
|
|
751
|
+
function resolveOne(want, entry, runner, ambientEnv) {
|
|
752
|
+
const base = {
|
|
753
|
+
name: want.name,
|
|
754
|
+
declared: want.declared,
|
|
755
|
+
secretBearing: want.secretBearing,
|
|
756
|
+
...(entry === undefined ? {} : { fileSource: declaredSourceOf(entry) }),
|
|
757
|
+
};
|
|
758
|
+
if (!SHELL_NAME_PATTERN.test(want.name)) {
|
|
759
|
+
return {
|
|
760
|
+
...base,
|
|
761
|
+
status: "unset",
|
|
762
|
+
source: "unusable name",
|
|
763
|
+
plaintext: false,
|
|
764
|
+
fix: `the policy declares this as an environment variable NAME and it is not one; fix the _env key in APPROVAL.md`,
|
|
765
|
+
refusal: {
|
|
766
|
+
code: "invalid-variable-name",
|
|
767
|
+
message: `${JSON.stringify(want.name)} is not a usable shell variable name, so no export line is emitted for it (a name carrying a space or a ; would make a policy file executable)`,
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const ambient = ambientEnv[want.name];
|
|
772
|
+
if (typeof ambient === "string" && ambient.length > 0) {
|
|
773
|
+
return {
|
|
774
|
+
...base,
|
|
775
|
+
status: "set-in-environment",
|
|
776
|
+
value: ambient,
|
|
777
|
+
source: "the environment (already exported; the file was not consulted)",
|
|
778
|
+
plaintext: false,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
if (entry === undefined) {
|
|
782
|
+
return { ...base, status: "unset", source: "unset", plaintext: false, fix: want.fix };
|
|
783
|
+
}
|
|
784
|
+
if (entry.kind === "literal") {
|
|
785
|
+
return {
|
|
786
|
+
...base,
|
|
787
|
+
status: "resolved-literal",
|
|
788
|
+
value: entry.argument,
|
|
789
|
+
source: LITERAL_SOURCE,
|
|
790
|
+
plaintext: want.secretBearing,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (entry.kind === "env") {
|
|
794
|
+
return {
|
|
795
|
+
...base,
|
|
796
|
+
status: "unset",
|
|
797
|
+
source: "env: (inherited) — and it is not set in this environment",
|
|
798
|
+
plaintext: false,
|
|
799
|
+
fix: want.fix,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const outcome = entry.kind === "keychain"
|
|
803
|
+
? runner.keychain(entry.argument)
|
|
804
|
+
: runner.secretService(entry.argument);
|
|
805
|
+
const label = `${entry.kind}:${entry.argument}`;
|
|
806
|
+
if (!outcome.ok) {
|
|
807
|
+
return {
|
|
808
|
+
...base,
|
|
809
|
+
status: "unset",
|
|
810
|
+
source: label,
|
|
811
|
+
plaintext: false,
|
|
812
|
+
fix: want.fix,
|
|
813
|
+
refusal: { code: outcome.code, message: outcome.message },
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (outcome.value.length === 0) {
|
|
817
|
+
return {
|
|
818
|
+
...base,
|
|
819
|
+
status: "unset",
|
|
820
|
+
source: label,
|
|
821
|
+
plaintext: false,
|
|
822
|
+
fix: want.fix,
|
|
823
|
+
refusal: {
|
|
824
|
+
code: "helper-item-missing",
|
|
825
|
+
message: `${label} resolved to an empty value, which is not a credential`,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
...base,
|
|
831
|
+
status: entry.kind === "keychain" ? "resolved-from-keychain" : "resolved-from-secret-service",
|
|
832
|
+
value: outcome.value,
|
|
833
|
+
source: label,
|
|
834
|
+
plaintext: false,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=env-file.js.map
|