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,1200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AgentMail adapter (SPEC.md §6.1, §6.2, §10.4, §11; APRV-222).
|
|
3
|
+
*
|
|
4
|
+
* A second executor for `communicate.email.external`, over the AgentMail HTTPS
|
|
5
|
+
* API. It exists because AgentMail is becoming the way an agent holds a mailbox,
|
|
6
|
+
* and because its Drafts primitive is documented as mail that only leaves when
|
|
7
|
+
* something outside the agent says so. approval.md is that something, with a
|
|
8
|
+
* hash-chained log behind it.
|
|
9
|
+
*
|
|
10
|
+
* Like every adapter it implements exactly one method, {@link Adapter.act}, and
|
|
11
|
+
* `adapters/contract.ts` owns everything around the call: the hash
|
|
12
|
+
* recomputation, the token spend, `execution.started`, the credential window,
|
|
13
|
+
* the outcome event, and the redaction sweep. Nothing here touches a token or
|
|
14
|
+
* the log.
|
|
15
|
+
*
|
|
16
|
+
* ## The enforcement model this adapter assumes
|
|
17
|
+
*
|
|
18
|
+
* AgentMail API keys carry per-permission booleans (`draft_create`,
|
|
19
|
+
* `draft_update`, `draft_read`, `draft_send`, `message_send` are separate). The
|
|
20
|
+
* deployment this adapter is written for gives the agent a key WITHOUT the two
|
|
21
|
+
* send permissions and puts a key WITH them in the vault under
|
|
22
|
+
* {@link DEFAULT_AGENTMAIL_CREDENTIAL_NAMES}.apiKey, where it is readable only
|
|
23
|
+
* inside the verified execution window the contract opens. The agent can therefore
|
|
24
|
+
* compose all day and cannot send at all; the sending key answers to a grant.
|
|
25
|
+
*
|
|
26
|
+
* ## Two payload modes, discriminated by shape, ambiguity refused
|
|
27
|
+
*
|
|
28
|
+
* **Direct send.** The email adapter's own payload, validated by the email
|
|
29
|
+
* adapter's own {@link validateEmailPayload} rather than a second copy of it:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* { from, to: string[], cc?, bcc?, subject, body, content_type? }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* posted to `POST /v0/inboxes/{inbox_id}/messages/send`.
|
|
36
|
+
*
|
|
37
|
+
* **Draft send.** A snapshot of a draft the agent has already composed, taken at
|
|
38
|
+
* request time so a human approves the words rather than an id:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* { inbox_id, draft_id, to: string[], cc?, bcc?, subject, text }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* The adapter re-fetches the draft, canonicalizes those same fields (RFC 8785,
|
|
45
|
+
* the same `core/jcs.ts` the hash chain uses) on both sides, and refuses
|
|
46
|
+
* `agentmail-draft-drifted` on any difference before calling
|
|
47
|
+
* `POST .../drafts/{draft_id}/send`. That check is the whole point of the mode:
|
|
48
|
+
* a draft is mutable server-side state, and an approval of a draft id would
|
|
49
|
+
* otherwise be an approval of whatever the agent last wrote into it. The
|
|
50
|
+
* refusal names WHICH fields differ and never what they now hold — a drift
|
|
51
|
+
* message is written to a log and read by a human who did not approve the new
|
|
52
|
+
* text, and quoting it there would publish unapproved content through the
|
|
53
|
+
* refusal path.
|
|
54
|
+
*
|
|
55
|
+
* ## The comparison runs before the token is spent (APRV-276)
|
|
56
|
+
*
|
|
57
|
+
* It happens TWICE, through one function ({@link checkDraftBeforeSend}), and
|
|
58
|
+
* the first of the two is the one this section exists for.
|
|
59
|
+
*
|
|
60
|
+
* The first call is {@link Adapter.precheck}: the contract runs it after the
|
|
61
|
+
* declared credentials resolve and BEFORE the token is consumed, so a drifted
|
|
62
|
+
* draft refuses with nothing appended and the grant intact. Until APRV-276 the
|
|
63
|
+
* comparison ran only inside `act`, which meant a drift refused correctly and
|
|
64
|
+
* spent the single-use token doing it: the human's tap was consumed by an
|
|
65
|
+
* attempt that sent nothing, and restoring the approved text then refused
|
|
66
|
+
* `token-consumed`. That is the failure mode the whole design is against — a
|
|
67
|
+
* refusal that costs a human another tap teaches operators to stop checking.
|
|
68
|
+
*
|
|
69
|
+
* The second call is `act`'s own, inside the consumed-token window and
|
|
70
|
+
* immediately before the POST. It is not redundant: AgentMail sends a draft by
|
|
71
|
+
* id, so something must stand between the last read and the send, and this is
|
|
72
|
+
* what makes that gap as small as the transport allows. A drift caught there is
|
|
73
|
+
* `execution.started` then `execution.failed`, which is the honest record of a
|
|
74
|
+
* window that was open when the far side moved.
|
|
75
|
+
*
|
|
76
|
+
* **Which key performs the pre-spend read.** The same sending key from the
|
|
77
|
+
* vault, handed over by the credential provider the contract scopes to the
|
|
78
|
+
* precheck call. Not the agent's own `AGENTMAIL_API_KEY`, even though the demo
|
|
79
|
+
* has one in the environment: a comparison whose input the party under
|
|
80
|
+
* oversight chooses is scrutiny that party controls, and SPEC.md §11.1
|
|
81
|
+
* invariant 4 puts that the other way round. Nothing new is opened to make this
|
|
82
|
+
* possible — the contract already reads the vault before the spend, to resolve
|
|
83
|
+
* {@link Adapter.requiredCredentials} (APRV-169), inside APRV-168's
|
|
84
|
+
* `presented`-phase grant, which is minted only when the caller's token matches
|
|
85
|
+
* the digest the human's grant recorded. SPEC.md §10.4 carries the decision.
|
|
86
|
+
*
|
|
87
|
+
* A payload carrying markers of both modes, or of neither, is refused rather
|
|
88
|
+
* than guessed at (`agentmail-payload-ambiguous`, `agentmail-payload-invalid`):
|
|
89
|
+
* choosing a send mode by inference is choosing a side effect by inference.
|
|
90
|
+
*
|
|
91
|
+
* ## `from`, and why it costs one extra read
|
|
92
|
+
*
|
|
93
|
+
* AgentMail's send endpoint has no `from` field. The inbox IS the sender, so
|
|
94
|
+
* the payload cannot bind the From address the way the SMTP adapter's can, and
|
|
95
|
+
* a human who approved a message "from carter@…" would otherwise be approving a
|
|
96
|
+
* sender this adapter never checked.
|
|
97
|
+
*
|
|
98
|
+
* The resolution: `from` stays in the payload as the human-facing claim about
|
|
99
|
+
* the sender (reusing {@link validateEmailPayload} keeps it required and
|
|
100
|
+
* well-formed), it is sent to AgentMail in no field at all, and `act` performs
|
|
101
|
+
* one extra read — `GET /v0/inboxes/{inbox_id}` — before the send, refusing
|
|
102
|
+
* `agentmail-from-mismatch` (case-insensitively) when the inbox's own address is
|
|
103
|
+
* not the approved one. So `from` is informational on the wire and binding here.
|
|
104
|
+
* The read runs on every direct send because it doubles as the credential
|
|
105
|
+
* check: a key that cannot open its own inbox is a key that should not discover
|
|
106
|
+
* this by half-sending. It is a GET, it is idempotent, it puts no message
|
|
107
|
+
* anywhere, and a transport failure on it is `agentmail-unreachable` precisely
|
|
108
|
+
* because nothing was attempted.
|
|
109
|
+
*
|
|
110
|
+
* ## Failures
|
|
111
|
+
*
|
|
112
|
+
* Every HTTP refusal is a RETURNED failure ({@link ActOutcome} `ok: false`), so
|
|
113
|
+
* the contract records `execution.failed`: the far side answered, and an answer
|
|
114
|
+
* is knowledge. A throw from the SEND call is deliberately NOT caught — it
|
|
115
|
+
* propagates, the contract records `execution.indeterminate`, and a human finds
|
|
116
|
+
* out that nobody knows whether the message went (APRV-120). A throw from the
|
|
117
|
+
* pre-send GETs is returned as `agentmail-unreachable`, because those run before
|
|
118
|
+
* anything is attempted.
|
|
119
|
+
*
|
|
120
|
+
* Deterministic apart from its transport: no randomness, no clock, no
|
|
121
|
+
* environment reads. Every string this file returns has been through
|
|
122
|
+
* {@link redactSecrets} against the API key.
|
|
123
|
+
*/
|
|
124
|
+
import { canonicalize } from "../core/jcs.js";
|
|
125
|
+
import { payloadHash } from "../core/payload.js";
|
|
126
|
+
import { CREDENTIAL_REFUSAL_CODES, PROVIDER_REF_DETAIL_KEY, redactSecrets, } from "./contract.js";
|
|
127
|
+
import { EMAIL_CLASS, envelopeRecipients, validateEmailPayload } from "./email.js";
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Constants
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/** The one class this adapter serves. The same string the email adapter serves. */
|
|
132
|
+
export const AGENTMAIL_CLASS = EMAIL_CLASS;
|
|
133
|
+
/** The public API. Overridable so the whole adapter can run against loopback. */
|
|
134
|
+
export const AGENTMAIL_DEFAULT_API_BASE = "https://api.agentmail.to";
|
|
135
|
+
/** Whole-request budget for one HTTP call. */
|
|
136
|
+
export const AGENTMAIL_DEFAULT_TIMEOUT_MS = 15_000;
|
|
137
|
+
/** The vault names, overridable per deployment. */
|
|
138
|
+
export const DEFAULT_AGENTMAIL_CREDENTIAL_NAMES = {
|
|
139
|
+
apiKey: "agentmail.api_key",
|
|
140
|
+
inboxId: "agentmail.inbox_id",
|
|
141
|
+
};
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// The failure vocabulary (SPEC.md §11.1 invariant 6: frozen, additive only)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
export const AGENTMAIL_FAILURE_CODES = [
|
|
146
|
+
/** The payload is not a well-formed value for either mode. Nothing was called. */
|
|
147
|
+
"agentmail-payload-invalid",
|
|
148
|
+
/** The payload carries markers of both modes; a send mode is never inferred. */
|
|
149
|
+
"agentmail-payload-ambiguous",
|
|
150
|
+
/** The vault answered, and what it holds is not usable configuration. */
|
|
151
|
+
"agentmail-config-invalid",
|
|
152
|
+
/** A draft payload names an inbox that is not the configured one. */
|
|
153
|
+
"agentmail-inbox-mismatch",
|
|
154
|
+
/** The approved `from` is not the address this inbox sends as. Nothing sent. */
|
|
155
|
+
"agentmail-from-mismatch",
|
|
156
|
+
/** The draft the grant was taken over is gone (404 on the draft read). */
|
|
157
|
+
"agentmail-draft-missing",
|
|
158
|
+
/** The draft changed after the snapshot the human approved. Nothing sent. */
|
|
159
|
+
"agentmail-draft-drifted",
|
|
160
|
+
/** A transport failure on a pre-send read: nothing was attempted. */
|
|
161
|
+
"agentmail-unreachable",
|
|
162
|
+
/** 401 or 403: the key is not accepted, or lacks the permission. */
|
|
163
|
+
"agentmail-unauthorized",
|
|
164
|
+
/** 404 on something other than the draft read. */
|
|
165
|
+
"agentmail-not-found",
|
|
166
|
+
/** 409: the far side says this conflicts with state it already holds. */
|
|
167
|
+
"agentmail-conflict",
|
|
168
|
+
/** 429: rate limited. */
|
|
169
|
+
"agentmail-rate-limited",
|
|
170
|
+
/** Any other 4xx: the request was refused on its merits. */
|
|
171
|
+
"agentmail-rejected",
|
|
172
|
+
/** Any 5xx. */
|
|
173
|
+
"agentmail-server-error",
|
|
174
|
+
...CREDENTIAL_REFUSAL_CODES,
|
|
175
|
+
];
|
|
176
|
+
export function isAgentmailFailureCode(value) {
|
|
177
|
+
return AGENTMAIL_FAILURE_CODES.includes(value);
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// The credential manifest (APRV-78's shape, APRV-222's two values)
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
/**
|
|
183
|
+
* The refusal sentences, written once and used twice: the setup wizard refuses
|
|
184
|
+
* a value at the moment an operator could still fix it, in the same words `act`
|
|
185
|
+
* would use at send time.
|
|
186
|
+
*/
|
|
187
|
+
function opaqueSentence(name, what) {
|
|
188
|
+
return `the vault's ${name} is empty or carries whitespace; ${what} is a single opaque token with no spaces in it`;
|
|
189
|
+
}
|
|
190
|
+
/** Non-empty and whitespace-free, which is all either value's shape asserts. */
|
|
191
|
+
function checkOpaque(value, sentence) {
|
|
192
|
+
if (value.length === 0 || /\s/u.test(value))
|
|
193
|
+
return { ok: false, message: sentence };
|
|
194
|
+
return { ok: true };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* What this adapter reads from the vault, declared rather than discovered, so
|
|
198
|
+
* `approval setup adapter agentmail` can ask for it without knowing what
|
|
199
|
+
* AgentMail is. DERIVED from {@link DEFAULT_AGENTMAIL_CREDENTIAL_NAMES} rather
|
|
200
|
+
* than restating the strings.
|
|
201
|
+
*/
|
|
202
|
+
export const AGENTMAIL_CREDENTIAL_SPECS = [
|
|
203
|
+
{
|
|
204
|
+
name: DEFAULT_AGENTMAIL_CREDENTIAL_NAMES.inboxId,
|
|
205
|
+
kind: "config",
|
|
206
|
+
label: "AgentMail inbox id",
|
|
207
|
+
describe: "the inbox this runtime sends from; AgentMail has no per-message From",
|
|
208
|
+
required: true,
|
|
209
|
+
validate(value) {
|
|
210
|
+
return checkOpaque(value.trim(), opaqueSentence(DEFAULT_AGENTMAIL_CREDENTIAL_NAMES.inboxId, "an inbox id"));
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: DEFAULT_AGENTMAIL_CREDENTIAL_NAMES.apiKey,
|
|
215
|
+
kind: "secret",
|
|
216
|
+
label: "AgentMail API key",
|
|
217
|
+
describe: "the key that carries message_send and draft_send; the agent's own key must not have them",
|
|
218
|
+
required: true,
|
|
219
|
+
validate(value) {
|
|
220
|
+
return checkOpaque(value.trim(), opaqueSentence(DEFAULT_AGENTMAIL_CREDENTIAL_NAMES.apiKey, "an API key"));
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
/**
|
|
225
|
+
* The names this adapter cannot act without (APRV-169), for the contract's
|
|
226
|
+
* pre-token resolution. Derived from the manifest, mapped through `names` so a
|
|
227
|
+
* deployment that renamed one gets the name it actually stored.
|
|
228
|
+
*/
|
|
229
|
+
export function requiredAgentmailCredentials(names = DEFAULT_AGENTMAIL_CREDENTIAL_NAMES) {
|
|
230
|
+
const keyOfDefault = new Map(Object.entries(DEFAULT_AGENTMAIL_CREDENTIAL_NAMES).map(([key, value]) => [
|
|
231
|
+
value,
|
|
232
|
+
key,
|
|
233
|
+
]));
|
|
234
|
+
return AGENTMAIL_CREDENTIAL_SPECS.filter((spec) => spec.required === true).map((spec) => {
|
|
235
|
+
const key = keyOfDefault.get(spec.name);
|
|
236
|
+
return key === undefined ? spec.name : names[key];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Read this adapter's whole configuration from a credential provider.
|
|
241
|
+
*
|
|
242
|
+
* The single place in the repository that turns a {@link CredentialProvider}
|
|
243
|
+
* into AgentMail settings — the names it asks for, the order, the shape rules —
|
|
244
|
+
* mirroring `readEmailSmtpConfig` for the same reason: `act` calls it inside the
|
|
245
|
+
* verified execution window and `approval setup adapter agentmail` calls it to probe
|
|
246
|
+
* a configuration it only partly typed, and a second reader would be a second
|
|
247
|
+
* opinion about what "configured" means.
|
|
248
|
+
*/
|
|
249
|
+
export function readAgentmailConfig(credentials, names = DEFAULT_AGENTMAIL_CREDENTIAL_NAMES) {
|
|
250
|
+
const secrets = [];
|
|
251
|
+
const scrub = (text) => redactSecrets(text, secrets).text;
|
|
252
|
+
const inbox = credentials.get(names.inboxId);
|
|
253
|
+
if (!inbox.ok) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
code: inbox.code,
|
|
257
|
+
secrets,
|
|
258
|
+
message: `the agentmail adapter needs the credential ${JSON.stringify(names.inboxId)}: ${inbox.message}`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const key = credentials.get(names.apiKey);
|
|
262
|
+
if (key.ok && key.value.length > 0)
|
|
263
|
+
secrets.push(key.value);
|
|
264
|
+
if (!key.ok) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
code: key.code,
|
|
268
|
+
secrets,
|
|
269
|
+
message: `the agentmail adapter needs the credential ${JSON.stringify(names.apiKey)}: ${key.message}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const inboxShape = checkOpaque(inbox.value, opaqueSentence(names.inboxId, "an inbox id"));
|
|
273
|
+
if (!inboxShape.ok) {
|
|
274
|
+
return { ok: false, code: "agentmail-config-invalid", message: inboxShape.message, secrets };
|
|
275
|
+
}
|
|
276
|
+
const keyShape = checkOpaque(key.value, opaqueSentence(names.apiKey, "an API key"));
|
|
277
|
+
if (!keyShape.ok) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
code: "agentmail-config-invalid",
|
|
281
|
+
message: scrub(keyShape.message),
|
|
282
|
+
secrets,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return { ok: true, secrets, config: { apiKey: key.value, inboxId: inbox.value } };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* One request. Throws on transport failure — the caller decides whether that is
|
|
289
|
+
* `agentmail-unreachable` (a pre-send read) or a propagated indeterminacy (the
|
|
290
|
+
* send itself).
|
|
291
|
+
*
|
|
292
|
+
* `path` is appended to the API base verbatim, so it may carry a query string
|
|
293
|
+
* (`/v0/inboxes/x/messages?limit=100`). The callers that need one build it with
|
|
294
|
+
* `URLSearchParams`, so the escaping question is asked in one place and this
|
|
295
|
+
* function stays "put these bytes after the base".
|
|
296
|
+
*/
|
|
297
|
+
async function call(transport, method, path, body) {
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timer = setTimeout(() => controller.abort(), transport.timeoutMs);
|
|
300
|
+
const outer = transport.signal;
|
|
301
|
+
const relay = () => controller.abort();
|
|
302
|
+
outer?.addEventListener("abort", relay, { once: true });
|
|
303
|
+
try {
|
|
304
|
+
const response = await transport.fetch(`${transport.apiBase}${path}`, {
|
|
305
|
+
method,
|
|
306
|
+
headers: {
|
|
307
|
+
authorization: `Bearer ${transport.apiKey}`,
|
|
308
|
+
accept: "application/json",
|
|
309
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
310
|
+
},
|
|
311
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
312
|
+
signal: controller.signal,
|
|
313
|
+
});
|
|
314
|
+
return { status: response.status, body: await response.text() };
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
outer?.removeEventListener("abort", relay);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** The HTTP status → failure code mapping, in one place. */
|
|
322
|
+
function codeForStatus(status) {
|
|
323
|
+
if (status === 401 || status === 403)
|
|
324
|
+
return "agentmail-unauthorized";
|
|
325
|
+
if (status === 404)
|
|
326
|
+
return "agentmail-not-found";
|
|
327
|
+
if (status === 409)
|
|
328
|
+
return "agentmail-conflict";
|
|
329
|
+
if (status === 429)
|
|
330
|
+
return "agentmail-rate-limited";
|
|
331
|
+
if (status >= 500)
|
|
332
|
+
return "agentmail-server-error";
|
|
333
|
+
return "agentmail-rejected";
|
|
334
|
+
}
|
|
335
|
+
/** A JSON object, or `null` when the body was not one. Never throws. */
|
|
336
|
+
function parseObject(body) {
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse(body);
|
|
339
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
340
|
+
return parsed;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
/* a non-JSON body is a fact about the far side, not an exception here */
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The far side's own words for a refusal, kept short and scrubbed by the
|
|
350
|
+
* caller. A body that is not JSON is reported by length rather than quoted:
|
|
351
|
+
* an HTML error page in a refusal message is noise, and an unbounded one is a
|
|
352
|
+
* log-flooding hole.
|
|
353
|
+
*/
|
|
354
|
+
function describeBody(body) {
|
|
355
|
+
const parsed = parseObject(body);
|
|
356
|
+
const said = parsed === null ? null : parsed["message"] ?? parsed["error"] ?? parsed["detail"];
|
|
357
|
+
if (typeof said === "string" && said.length > 0)
|
|
358
|
+
return said.slice(0, 400);
|
|
359
|
+
if (body.trim().length === 0)
|
|
360
|
+
return "no body";
|
|
361
|
+
return `a ${String(body.length)}-byte body that named no error`;
|
|
362
|
+
}
|
|
363
|
+
/** The two permissions a key must hold to send anything for this adapter. */
|
|
364
|
+
export const AGENTMAIL_SEND_PERMISSIONS = ["draft_send", "message_send"];
|
|
365
|
+
/**
|
|
366
|
+
* The permissions an inbox body disclosed, or `null` when it disclosed none.
|
|
367
|
+
*
|
|
368
|
+
* Accepts either spelling an API of this shape uses (`permissions`, `scopes`),
|
|
369
|
+
* at the top level or under a `key`/`api_key` object, and answers `null` for
|
|
370
|
+
* anything else. Every unknown shape is UNKNOWN rather than empty: an empty
|
|
371
|
+
* list is the claim "this key holds nothing", and inferring that from silence
|
|
372
|
+
* would make a setup run refuse a perfectly good key.
|
|
373
|
+
*/
|
|
374
|
+
function disclosedPermissions(body) {
|
|
375
|
+
if (body === null)
|
|
376
|
+
return null;
|
|
377
|
+
const holders = [body, body["key"], body["api_key"]];
|
|
378
|
+
for (const holder of holders) {
|
|
379
|
+
if (typeof holder !== "object" || holder === null || Array.isArray(holder))
|
|
380
|
+
continue;
|
|
381
|
+
for (const field of ["permissions", "scopes"]) {
|
|
382
|
+
const held = holder[field];
|
|
383
|
+
if (Array.isArray(held) && held.every((entry) => typeof entry === "string")) {
|
|
384
|
+
return held;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* `GET /v0/inboxes/{inbox_id}`: does this key open this inbox, and what address
|
|
392
|
+
* does it send as? Sends nothing, changes nothing, and is the credential check
|
|
393
|
+
* both `act` and `approval setup adapter agentmail` use.
|
|
394
|
+
*/
|
|
395
|
+
export async function probeAgentmail(config, options = {}) {
|
|
396
|
+
const transport = {
|
|
397
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
398
|
+
apiBase: (options.apiBase ?? AGENTMAIL_DEFAULT_API_BASE).replace(/\/+$/u, ""),
|
|
399
|
+
timeoutMs: options.timeoutMs ?? AGENTMAIL_DEFAULT_TIMEOUT_MS,
|
|
400
|
+
apiKey: config.apiKey,
|
|
401
|
+
};
|
|
402
|
+
const scrub = (text) => redactSecrets(text, [config.apiKey]).text;
|
|
403
|
+
let answer;
|
|
404
|
+
try {
|
|
405
|
+
answer = await call(transport, "GET", inboxPath(config.inboxId));
|
|
406
|
+
}
|
|
407
|
+
catch (cause) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
code: "agentmail-unreachable",
|
|
411
|
+
message: scrub(`the AgentMail API could not be reached to read the inbox ${config.inboxId}: ${describeThrow(cause)}. Nothing was sent`),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
code: codeForStatus(answer.status),
|
|
418
|
+
message: scrub(`reading the inbox ${config.inboxId} answered HTTP ${String(answer.status)}: ${describeBody(answer.body)}. Nothing was sent`),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const parsed = parseObject(answer.body);
|
|
422
|
+
const address = addressOf(parsed, config.inboxId);
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
address,
|
|
426
|
+
http_status: answer.status,
|
|
427
|
+
permissions: disclosedPermissions(parsed),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The address an inbox sends as.
|
|
432
|
+
*
|
|
433
|
+
* AgentMail inbox ids are themselves addresses, so `inbox_id` is the fallback
|
|
434
|
+
* and an explicit `address` field wins when the API returns one. This is the
|
|
435
|
+
* only place that decision is made.
|
|
436
|
+
*/
|
|
437
|
+
function addressOf(inbox, inboxId) {
|
|
438
|
+
const address = inbox?.["address"];
|
|
439
|
+
if (typeof address === "string" && address.length > 0)
|
|
440
|
+
return address;
|
|
441
|
+
const id = inbox?.["inbox_id"];
|
|
442
|
+
if (typeof id === "string" && id.length > 0)
|
|
443
|
+
return id;
|
|
444
|
+
return inboxId;
|
|
445
|
+
}
|
|
446
|
+
/** Only the message; never the stack, which routinely quotes arguments. */
|
|
447
|
+
function describeThrow(cause) {
|
|
448
|
+
if (cause instanceof Error)
|
|
449
|
+
return cause.message;
|
|
450
|
+
return String(cause);
|
|
451
|
+
}
|
|
452
|
+
function inboxPath(inboxId) {
|
|
453
|
+
return `/v0/inboxes/${encodeURIComponent(inboxId)}`;
|
|
454
|
+
}
|
|
455
|
+
/** Every key a draft payload may carry. Anything else is refused. */
|
|
456
|
+
const DRAFT_PAYLOAD_KEYS = [
|
|
457
|
+
"inbox_id",
|
|
458
|
+
"draft_id",
|
|
459
|
+
"to",
|
|
460
|
+
"cc",
|
|
461
|
+
"bcc",
|
|
462
|
+
"subject",
|
|
463
|
+
"text",
|
|
464
|
+
];
|
|
465
|
+
/** The fields the drift check covers: everything a reader of the draft saw. */
|
|
466
|
+
export const AGENTMAIL_DRAFT_FIELDS = ["to", "cc", "bcc", "subject", "text"];
|
|
467
|
+
/** Keys that occur in one mode and not the other. The discriminator. */
|
|
468
|
+
const DRAFT_MARKERS = ["draft_id", "inbox_id", "text"];
|
|
469
|
+
const DIRECT_MARKERS = ["from", "body", "content_type"];
|
|
470
|
+
function isStringArray(value) {
|
|
471
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Structural validation of a draft payload. Never throws; returns the reason.
|
|
475
|
+
*
|
|
476
|
+
* Exported so a caller can check a payload BEFORE requesting approval for it,
|
|
477
|
+
* which is the only place a shape error can still be fixed cheaply.
|
|
478
|
+
*/
|
|
479
|
+
export function validateAgentmailDraftPayload(value) {
|
|
480
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
481
|
+
return { ok: false, message: "the payload must be a JSON object" };
|
|
482
|
+
}
|
|
483
|
+
const record = value;
|
|
484
|
+
const unknown = Object.keys(record).filter((key) => !DRAFT_PAYLOAD_KEYS.includes(key));
|
|
485
|
+
if (unknown.length > 0) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
message: `the payload carries ${unknown.map((key) => JSON.stringify(key)).join(", ")}, which a draft send does not implement. An unknown key is refused rather than ignored: a human approved every byte of this payload. Supported keys: ${DRAFT_PAYLOAD_KEYS.join(", ")}`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
for (const field of ["inbox_id", "draft_id", "subject", "text"]) {
|
|
492
|
+
const held = record[field];
|
|
493
|
+
if (typeof held !== "string") {
|
|
494
|
+
return { ok: false, message: `${field} must be a string` };
|
|
495
|
+
}
|
|
496
|
+
if ((field === "inbox_id" || field === "draft_id") && held.length === 0) {
|
|
497
|
+
return { ok: false, message: `${field} must be a non-empty string` };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const to = record["to"];
|
|
501
|
+
if (!isStringArray(to) || to.length === 0) {
|
|
502
|
+
return { ok: false, message: "to must be a non-empty array of addresses" };
|
|
503
|
+
}
|
|
504
|
+
const lists = {};
|
|
505
|
+
for (const field of ["cc", "bcc"]) {
|
|
506
|
+
const list = record[field];
|
|
507
|
+
if (list === undefined)
|
|
508
|
+
continue;
|
|
509
|
+
if (!isStringArray(list)) {
|
|
510
|
+
return { ok: false, message: `${field}, when present, must be an array of addresses` };
|
|
511
|
+
}
|
|
512
|
+
lists[field] = list;
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
payload: {
|
|
517
|
+
inbox_id: record["inbox_id"],
|
|
518
|
+
draft_id: record["draft_id"],
|
|
519
|
+
to,
|
|
520
|
+
...(lists.cc === undefined ? {} : { cc: lists.cc }),
|
|
521
|
+
...(lists.bcc === undefined ? {} : { bcc: lists.bcc }),
|
|
522
|
+
subject: record["subject"],
|
|
523
|
+
text: record["text"],
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Which mode a payload is in, by the markers it carries.
|
|
529
|
+
*
|
|
530
|
+
* A payload with markers of both modes is ambiguous and a payload with markers
|
|
531
|
+
* of neither is not addressed to this adapter at all. Both are refused: a send
|
|
532
|
+
* mode chosen by inference is a side effect chosen by inference.
|
|
533
|
+
*/
|
|
534
|
+
export function agentmailMode(value) {
|
|
535
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
code: "agentmail-payload-invalid",
|
|
539
|
+
message: "the payload must be a JSON object",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const keys = new Set(Object.keys(value));
|
|
543
|
+
const draft = DRAFT_MARKERS.filter((key) => keys.has(key));
|
|
544
|
+
const direct = DIRECT_MARKERS.filter((key) => keys.has(key));
|
|
545
|
+
if (draft.length > 0 && direct.length > 0) {
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
code: "agentmail-payload-ambiguous",
|
|
549
|
+
message: `the payload carries keys of both send modes (${draft.join(", ")} from a draft send, ${direct.join(", ")} from a direct send). This adapter will not infer which side effect a human approved`,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (draft.length > 0)
|
|
553
|
+
return { ok: true, mode: "draft" };
|
|
554
|
+
if (direct.length > 0)
|
|
555
|
+
return { ok: true, mode: "direct" };
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
code: "agentmail-payload-invalid",
|
|
559
|
+
message: `the payload names neither a direct send (${DIRECT_MARKERS.join(", ")}) nor a draft send (${DRAFT_MARKERS.join(", ")})`,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* The canonical form of one drift-checked field, RFC 8785.
|
|
564
|
+
*
|
|
565
|
+
* Absent and `null` are the same fact ("no cc") and so is an empty list, which
|
|
566
|
+
* is the third spelling an API may use for it; anything else is compared
|
|
567
|
+
* exactly, array order included, because a recipient list read in a different
|
|
568
|
+
* order is a different message to the person reading it.
|
|
569
|
+
*/
|
|
570
|
+
function canonicalField(value) {
|
|
571
|
+
if (value === undefined || value === null)
|
|
572
|
+
return canonicalize(null);
|
|
573
|
+
if (Array.isArray(value) && value.length === 0)
|
|
574
|
+
return canonicalize(null);
|
|
575
|
+
return canonicalize(value);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Which of {@link AGENTMAIL_DRAFT_FIELDS} differ between the approved snapshot
|
|
579
|
+
* and what the server now holds. Names only — never values.
|
|
580
|
+
*/
|
|
581
|
+
export function draftDrift(approved, fetched) {
|
|
582
|
+
const snapshot = { ...approved };
|
|
583
|
+
return AGENTMAIL_DRAFT_FIELDS.filter((field) => canonicalField(snapshot[field]) !== canonicalField(fetched[field]));
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The payload a grant should bind to, built from what the API holds RIGHT NOW
|
|
587
|
+
* (APRV-223).
|
|
588
|
+
*
|
|
589
|
+
* `approval payload agentmail-draft` prints this and nothing else, and it lives
|
|
590
|
+
* here rather than in the CLI for one reason: the bytes it prints are the bytes
|
|
591
|
+
* {@link draftDrift} will compare against the same draft at send time, so the
|
|
592
|
+
* two must be one piece of code. A second opinion in the CLI about what "the
|
|
593
|
+
* draft's cc" is would be a snapshot that drifts from a draft nobody changed.
|
|
594
|
+
*
|
|
595
|
+
* The rules follow {@link canonicalField} exactly. `cc`/`bcc` are OMITTED when
|
|
596
|
+
* the draft holds nothing for them, because absent, `null` and `[]` are one
|
|
597
|
+
* fact there; `to` is copied through as the array it is, unnormalized, because
|
|
598
|
+
* a reordered or re-shaped recipient list is a different message. Anything this
|
|
599
|
+
* function cannot turn into a well-formed snapshot is refused with the reason:
|
|
600
|
+
* a payload that fails {@link validateAgentmailDraftPayload} at send time is a
|
|
601
|
+
* refusal a human has already been asked to approve.
|
|
602
|
+
*/
|
|
603
|
+
export function draftSnapshot(inboxId, draftId, fetched) {
|
|
604
|
+
const to = fetched["to"];
|
|
605
|
+
if (!isStringArray(to) || to.length === 0) {
|
|
606
|
+
return {
|
|
607
|
+
ok: false,
|
|
608
|
+
message: "the draft's `to` is not a non-empty array of addresses. A snapshot is taken from the draft as the API holds it, and this one could not be sent as it stands",
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const lists = {};
|
|
612
|
+
for (const field of ["cc", "bcc"]) {
|
|
613
|
+
const held = fetched[field];
|
|
614
|
+
if (held === undefined || held === null)
|
|
615
|
+
continue;
|
|
616
|
+
if (!isStringArray(held)) {
|
|
617
|
+
return { ok: false, message: `the draft's \`${field}\`, when present, must be an array of addresses` };
|
|
618
|
+
}
|
|
619
|
+
// Empty is the same fact as absent for the drift check, so it is omitted
|
|
620
|
+
// rather than carried: a snapshot with "cc":[] would hash differently from
|
|
621
|
+
// an identical one taken by hand.
|
|
622
|
+
if (held.length > 0)
|
|
623
|
+
lists[field] = held;
|
|
624
|
+
}
|
|
625
|
+
for (const field of ["subject", "text"]) {
|
|
626
|
+
if (typeof fetched[field] !== "string") {
|
|
627
|
+
return {
|
|
628
|
+
ok: false,
|
|
629
|
+
message: `the draft's \`${field}\` is not a string, so there is nothing here for a human to approve`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
payload: {
|
|
636
|
+
inbox_id: inboxId,
|
|
637
|
+
draft_id: draftId,
|
|
638
|
+
to,
|
|
639
|
+
...(lists.cc === undefined ? {} : { cc: lists.cc }),
|
|
640
|
+
...(lists.bcc === undefined ? {} : { bcc: lists.bcc }),
|
|
641
|
+
subject: fetched["subject"],
|
|
642
|
+
text: fetched["text"],
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/** Where `GET`ting one draft lives, so no caller spells the path twice. */
|
|
647
|
+
function draftPathFor(inboxId, draftId) {
|
|
648
|
+
return `${inboxPath(inboxId)}/drafts/${encodeURIComponent(draftId)}`;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* `GET /v0/inboxes/{inbox}/drafts/{draft}`: one draft, read and nothing else.
|
|
652
|
+
*
|
|
653
|
+
* The read half of the draft flow, exported for `approval payload
|
|
654
|
+
* agentmail-draft` (APRV-223), which runs BEFORE any approval exists and with
|
|
655
|
+
* the agent's own key rather than the vault's. It sends nothing and spends no
|
|
656
|
+
* token: what it produces is a proposal a human has yet to see.
|
|
657
|
+
*/
|
|
658
|
+
export async function readAgentmailDraft(options) {
|
|
659
|
+
const transport = {
|
|
660
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
661
|
+
apiBase: (options.apiBase ?? AGENTMAIL_DEFAULT_API_BASE).replace(/\/+$/u, ""),
|
|
662
|
+
timeoutMs: options.timeoutMs ?? AGENTMAIL_DEFAULT_TIMEOUT_MS,
|
|
663
|
+
apiKey: options.apiKey,
|
|
664
|
+
};
|
|
665
|
+
const scrub = (text) => redactSecrets(text, [options.apiKey]).text;
|
|
666
|
+
let answer;
|
|
667
|
+
try {
|
|
668
|
+
answer = await call(transport, "GET", draftPathFor(options.inboxId, options.draftId));
|
|
669
|
+
}
|
|
670
|
+
catch (cause) {
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
code: "agentmail-unreachable",
|
|
674
|
+
message: scrub(`the AgentMail API could not be reached to read the draft ${options.draftId}: ${describeThrow(cause)}`),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (answer.status === 404) {
|
|
678
|
+
return {
|
|
679
|
+
ok: false,
|
|
680
|
+
code: "agentmail-draft-missing",
|
|
681
|
+
message: `there is no draft ${options.draftId} in the inbox ${options.inboxId}`,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
code: codeForStatus(answer.status),
|
|
688
|
+
message: scrub(`reading the draft ${options.draftId} answered HTTP ${String(answer.status)}: ${describeBody(answer.body)}`),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const body = parseObject(answer.body);
|
|
692
|
+
if (body === null) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
code: "agentmail-draft-drifted",
|
|
696
|
+
message: `the draft ${options.draftId} did not read back as a JSON object, so no snapshot of it can be taken`,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return { ok: true, draft: body, http_status: answer.status };
|
|
700
|
+
}
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// The draft comparison, written once and performed twice (APRV-276)
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
/** A pre-send read: a throw is `agentmail-unreachable`, nothing attempted. */
|
|
705
|
+
async function preSendRead(transport, scrub, path, what) {
|
|
706
|
+
try {
|
|
707
|
+
return { ok: true, answer: await call(transport, "GET", path) };
|
|
708
|
+
}
|
|
709
|
+
catch (cause) {
|
|
710
|
+
return {
|
|
711
|
+
ok: false,
|
|
712
|
+
code: "agentmail-unreachable",
|
|
713
|
+
message: scrub(`the AgentMail API could not be reached to read ${what}: ${describeThrow(cause)}. Nothing was sent`),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Everything that must hold before a draft send, up to but not including the
|
|
719
|
+
* send: the payload's shape, the inbox it names, the draft's existence, and the
|
|
720
|
+
* comparison of the approved snapshot against what the far side holds now.
|
|
721
|
+
*
|
|
722
|
+
* One function because it is performed twice, and the two calls answer two
|
|
723
|
+
* different questions (APRV-276):
|
|
724
|
+
*
|
|
725
|
+
* - As {@link Adapter.precheck}, BEFORE the token is consumed. What it protects
|
|
726
|
+
* there is the grant: a draft the agent edited after the human read it is a
|
|
727
|
+
* refusal this runtime can reach without attempting anything, so it must not
|
|
728
|
+
* cost the human another tap. A refusal there appends nothing and spends
|
|
729
|
+
* nothing, and the same token sends once the approved text is restored.
|
|
730
|
+
* - Inside `act`, in the consumed-token window, immediately before the POST.
|
|
731
|
+
* What it protects there is the bytes: AgentMail sends a draft by id, so the
|
|
732
|
+
* gap between the last read and the send can never be zero, and this is the
|
|
733
|
+
* check that makes it as small as the transport allows. A drift found here is
|
|
734
|
+
* an execution that started and failed, which is the honest record — the
|
|
735
|
+
* window was open and the far side moved inside it.
|
|
736
|
+
*
|
|
737
|
+
* Reads only, so calling it twice sends nothing twice. The message names WHICH
|
|
738
|
+
* fields differ and never what they now hold, on both calls, for the reason the
|
|
739
|
+
* module header gives.
|
|
740
|
+
*/
|
|
741
|
+
async function checkDraftBeforeSend(actionKey, value, config, transport, scrub) {
|
|
742
|
+
const validated = validateAgentmailDraftPayload(value);
|
|
743
|
+
if (!validated.ok) {
|
|
744
|
+
return {
|
|
745
|
+
ok: false,
|
|
746
|
+
code: "agentmail-payload-invalid",
|
|
747
|
+
message: `the approved payload for ${actionKey} is not a well-formed draft send: ${validated.message}. Nothing was requested and nothing was sent`,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const payload = validated.payload;
|
|
751
|
+
// The inbox the payload names must be the inbox the vault configures.
|
|
752
|
+
// Checked before any request: a payload naming another inbox is a wiring
|
|
753
|
+
// mistake, and asking the far side about it would be asking it to arbitrate
|
|
754
|
+
// whose mailbox this grant covers.
|
|
755
|
+
if (payload.inbox_id !== config.inboxId) {
|
|
756
|
+
return {
|
|
757
|
+
ok: false,
|
|
758
|
+
code: "agentmail-inbox-mismatch",
|
|
759
|
+
message: `the approved draft names the inbox ${payload.inbox_id}, and this runtime is configured for ${config.inboxId}. Nothing was requested and nothing was sent`,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const draftPath = draftPathFor(config.inboxId, payload.draft_id);
|
|
763
|
+
const fetched = await preSendRead(transport, scrub, draftPath, `the draft ${payload.draft_id}`);
|
|
764
|
+
if (!fetched.ok)
|
|
765
|
+
return { ok: false, code: fetched.code, message: fetched.message };
|
|
766
|
+
if (fetched.answer.status === 404) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
code: "agentmail-draft-missing",
|
|
770
|
+
message: scrub(`the draft ${payload.draft_id} in inbox ${config.inboxId} no longer exists. A grant is over a snapshot of a draft, and the draft it named is gone; nothing was sent`),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (fetched.answer.status < 200 || fetched.answer.status >= 300) {
|
|
774
|
+
return {
|
|
775
|
+
ok: false,
|
|
776
|
+
code: codeForStatus(fetched.answer.status),
|
|
777
|
+
message: scrub(`reading the draft ${payload.draft_id} answered HTTP ${String(fetched.answer.status)}: ${describeBody(fetched.answer.body)}. Nothing was sent`),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const body = parseObject(fetched.answer.body);
|
|
781
|
+
if (body === null) {
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
code: "agentmail-draft-drifted",
|
|
785
|
+
message: `the draft ${payload.draft_id} did not read back as a JSON object, so what the human approved cannot be compared with what would be sent. Nothing was sent`,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
// The drift check: the whole point of the mode. Field NAMES only.
|
|
789
|
+
const drifted = draftDrift(payload, body);
|
|
790
|
+
if (drifted.length > 0) {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
code: "agentmail-draft-drifted",
|
|
794
|
+
message: `the draft ${payload.draft_id} has changed since the snapshot a human approved: ${drifted.join(", ")} ${drifted.length === 1 ? "differs" : "differ"}. The differing content is deliberately not quoted here — it is unapproved text, and a refusal is not a channel for publishing it. Nothing was sent; re-request approval for the current draft`,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return { ok: true, payload, draftPath };
|
|
798
|
+
}
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
// Observation (APRV-245): what the provider says this inbox actually sent
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
/**
|
|
803
|
+
* How many messages one page asks for. The API's own cap is higher; this is the
|
|
804
|
+
* page size, and {@link OBSERVE_MAX_PAGES} bounds how many pages are walked.
|
|
805
|
+
*/
|
|
806
|
+
export const AGENTMAIL_OBSERVE_PAGE_SIZE = 100;
|
|
807
|
+
/**
|
|
808
|
+
* How many pages one observation walks.
|
|
809
|
+
*
|
|
810
|
+
* A bound rather than a full drain, because a reporting verb must terminate
|
|
811
|
+
* against an inbox of any size. A run that hits the bound says so, so a reader
|
|
812
|
+
* never mistakes a truncated page walk for a quiet mailbox.
|
|
813
|
+
*/
|
|
814
|
+
export const OBSERVE_MAX_PAGES = 10;
|
|
815
|
+
/** The list of messages a page carries, under whichever key the body used. */
|
|
816
|
+
function messagesOf(body) {
|
|
817
|
+
if (body === null)
|
|
818
|
+
return [];
|
|
819
|
+
for (const key of ["messages", "data", "items"]) {
|
|
820
|
+
const held = body[key];
|
|
821
|
+
if (!Array.isArray(held))
|
|
822
|
+
continue;
|
|
823
|
+
return held.filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
|
|
824
|
+
}
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
/** The next page token a body offers, or `null` when it offers none. */
|
|
828
|
+
function pageTokenOf(body) {
|
|
829
|
+
if (body === null)
|
|
830
|
+
return null;
|
|
831
|
+
for (const key of ["next_page_token", "page_token", "next_cursor"]) {
|
|
832
|
+
const held = body[key];
|
|
833
|
+
if (typeof held === "string" && held.length > 0)
|
|
834
|
+
return held;
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
/** How many addresses a message went to, counting `to`, `cc` and `bcc`. */
|
|
839
|
+
function recipientCount(message) {
|
|
840
|
+
let count = 0;
|
|
841
|
+
for (const key of ["to", "cc", "bcc"]) {
|
|
842
|
+
const held = message[key];
|
|
843
|
+
if (Array.isArray(held))
|
|
844
|
+
count += held.filter((entry) => typeof entry === "string").length;
|
|
845
|
+
else if (typeof held === "string" && held.length > 0)
|
|
846
|
+
count += 1;
|
|
847
|
+
}
|
|
848
|
+
return count;
|
|
849
|
+
}
|
|
850
|
+
/** Does this message's `labels` array carry `sent`? Case-insensitively. */
|
|
851
|
+
function isSent(message) {
|
|
852
|
+
const labels = message["labels"];
|
|
853
|
+
if (!Array.isArray(labels))
|
|
854
|
+
return false;
|
|
855
|
+
return labels.some((label) => typeof label === "string" && label.toLowerCase() === "sent");
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* `GET /v0/inboxes/{inbox_id}/messages`: what this inbox actually sent.
|
|
859
|
+
*
|
|
860
|
+
* The endpoint and its fields are AgentMail's own, documented at
|
|
861
|
+
* https://docs.agentmail.to/api-reference/inboxes/messages/list — `message_id`,
|
|
862
|
+
* `labels`, `timestamp`, `to` and `subject`. The query carries `after`, `before`
|
|
863
|
+
* and `limit`, and `page_token` on every page after the first.
|
|
864
|
+
*
|
|
865
|
+
* **The sent filter is client-side, and that is a limit worth stating.** The
|
|
866
|
+
* documented list endpoint exposes no sent-only parameter, so this asks for the
|
|
867
|
+
* window's messages and keeps the ones whose `labels` include `sent`. Two
|
|
868
|
+
* consequences follow and neither is papered over: the request reads received
|
|
869
|
+
* mail as well as sent (a read, changing nothing), and a provider that stopped
|
|
870
|
+
* labelling sent mail would make this source report an empty window rather than
|
|
871
|
+
* an error. The remedy for the second is the same as for everything else here:
|
|
872
|
+
* the source reports what the provider said, and a source that says nothing is
|
|
873
|
+
* a gap a reader can see rather than a pass.
|
|
874
|
+
*
|
|
875
|
+
* Sends nothing. Spends no token. Reads no clock: the window is the caller's.
|
|
876
|
+
*/
|
|
877
|
+
export async function observeAgentmail(config, window, options = {}) {
|
|
878
|
+
const transport = {
|
|
879
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
880
|
+
apiBase: (options.apiBase ?? AGENTMAIL_DEFAULT_API_BASE).replace(/\/+$/u, ""),
|
|
881
|
+
timeoutMs: options.timeoutMs ?? AGENTMAIL_DEFAULT_TIMEOUT_MS,
|
|
882
|
+
apiKey: config.apiKey,
|
|
883
|
+
};
|
|
884
|
+
const scrub = (text) => redactSecrets(text, [config.apiKey]).text;
|
|
885
|
+
const pageSize = options.pageSize ?? AGENTMAIL_OBSERVE_PAGE_SIZE;
|
|
886
|
+
const maxPages = options.maxPages ?? OBSERVE_MAX_PAGES;
|
|
887
|
+
const messages = [];
|
|
888
|
+
let token = null;
|
|
889
|
+
let truncated = false;
|
|
890
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
891
|
+
const query = new URLSearchParams({
|
|
892
|
+
after: window.since,
|
|
893
|
+
before: window.until,
|
|
894
|
+
limit: String(pageSize),
|
|
895
|
+
});
|
|
896
|
+
if (token !== null)
|
|
897
|
+
query.set("page_token", token);
|
|
898
|
+
const path = `${inboxPath(config.inboxId)}/messages?${query.toString()}`;
|
|
899
|
+
let answer;
|
|
900
|
+
try {
|
|
901
|
+
answer = await call(transport, "GET", path);
|
|
902
|
+
}
|
|
903
|
+
catch (cause) {
|
|
904
|
+
return {
|
|
905
|
+
ok: false,
|
|
906
|
+
code: "agentmail-unreachable",
|
|
907
|
+
message: scrub(`the AgentMail API could not be reached to list the messages of ${config.inboxId}: ${describeThrow(cause)}. Nothing was sent and nothing was changed`),
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
code: codeForStatus(answer.status),
|
|
914
|
+
message: scrub(`listing the messages of ${config.inboxId} answered HTTP ${String(answer.status)}: ${describeBody(answer.body)}. Nothing was sent and nothing was changed`),
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const body = parseObject(answer.body);
|
|
918
|
+
for (const message of messagesOf(body)) {
|
|
919
|
+
if (!isSent(message))
|
|
920
|
+
continue;
|
|
921
|
+
const id = message["message_id"];
|
|
922
|
+
const at = message["timestamp"];
|
|
923
|
+
if (typeof id !== "string" || id.length === 0)
|
|
924
|
+
continue;
|
|
925
|
+
const subject = message["subject"];
|
|
926
|
+
messages.push({
|
|
927
|
+
messageId: id,
|
|
928
|
+
at: typeof at === "string" ? at : "",
|
|
929
|
+
subject: typeof subject === "string" ? subject : "",
|
|
930
|
+
recipients: recipientCount(message),
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
token = pageTokenOf(body);
|
|
934
|
+
if (token === null)
|
|
935
|
+
break;
|
|
936
|
+
if (page === maxPages - 1)
|
|
937
|
+
truncated = true;
|
|
938
|
+
}
|
|
939
|
+
return { ok: true, messages, truncated };
|
|
940
|
+
}
|
|
941
|
+
/** The body a direct send puts on the wire. */
|
|
942
|
+
function directBody(payload) {
|
|
943
|
+
const html = payload.content_type === "text/html";
|
|
944
|
+
return {
|
|
945
|
+
to: payload.to,
|
|
946
|
+
...(payload.cc === undefined ? {} : { cc: payload.cc }),
|
|
947
|
+
...(payload.bcc === undefined ? {} : { bcc: payload.bcc }),
|
|
948
|
+
subject: payload.subject,
|
|
949
|
+
...(html ? { html: payload.body } : { text: payload.body }),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* The success detail. Nothing secret, and nothing the log has not earned.
|
|
954
|
+
*
|
|
955
|
+
* `message_id` is what AgentMail calls its identifier and is what a person
|
|
956
|
+
* pastes into the provider's own console. `provider_ref` is the same string
|
|
957
|
+
* under the ONE key the adapter contract lifts onto the record (APRV-251,
|
|
958
|
+
* `PROVIDER_REF_DETAIL_KEY`), so that `approval coverage` can join what this
|
|
959
|
+
* inbox actually sent to this log by id. The duplication is deliberate: the
|
|
960
|
+
* contract lifts one conventional key rather than guessing which field of a
|
|
961
|
+
* receipt is the id, and this adapter keeps its own vocabulary intact for the
|
|
962
|
+
* reader of the CLI result.
|
|
963
|
+
*
|
|
964
|
+
* Written only when the provider returned an id. A send whose answer carries no
|
|
965
|
+
* `message_id` names no reference, and the completion records none, which is the
|
|
966
|
+
* pre-amendment record and always valid.
|
|
967
|
+
*/
|
|
968
|
+
function receipt(mode, answer, hash, recipients) {
|
|
969
|
+
const parsed = parseObject(answer.body);
|
|
970
|
+
const messageId = parsed?.["message_id"];
|
|
971
|
+
const threadId = parsed?.["thread_id"];
|
|
972
|
+
const named = typeof messageId === "string" && messageId.length > 0 ? messageId : null;
|
|
973
|
+
return {
|
|
974
|
+
mode,
|
|
975
|
+
message_id: named,
|
|
976
|
+
...(named === null ? {} : { [PROVIDER_REF_DETAIL_KEY]: named }),
|
|
977
|
+
...(typeof threadId === "string" ? { thread_id: threadId } : {}),
|
|
978
|
+
payload_hash: hash,
|
|
979
|
+
recipients,
|
|
980
|
+
http_status: answer.status,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* A fresh AgentMail adapter.
|
|
985
|
+
*
|
|
986
|
+
* Stateless and reusable: it holds no connection and nothing from a previous
|
|
987
|
+
* send. Two concurrent executions through the same instance share nothing.
|
|
988
|
+
*/
|
|
989
|
+
export function agentmailAdapter(options = {}) {
|
|
990
|
+
const names = {
|
|
991
|
+
...DEFAULT_AGENTMAIL_CREDENTIAL_NAMES,
|
|
992
|
+
...options.credentialNames,
|
|
993
|
+
};
|
|
994
|
+
const classes = [
|
|
995
|
+
AGENTMAIL_CLASS,
|
|
996
|
+
...(options.classes ?? []).filter((cls) => cls !== AGENTMAIL_CLASS),
|
|
997
|
+
];
|
|
998
|
+
const apiBase = (options.apiBase ?? AGENTMAIL_DEFAULT_API_BASE).replace(/\/+$/u, "");
|
|
999
|
+
const timeoutMs = options.timeoutMs ?? AGENTMAIL_DEFAULT_TIMEOUT_MS;
|
|
1000
|
+
/** The transport for one call, built in one place for both entry points. */
|
|
1001
|
+
const transportFor = (config, signal) => ({
|
|
1002
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
1003
|
+
apiBase,
|
|
1004
|
+
timeoutMs,
|
|
1005
|
+
apiKey: config.apiKey,
|
|
1006
|
+
signal,
|
|
1007
|
+
});
|
|
1008
|
+
return {
|
|
1009
|
+
name: "agentmail",
|
|
1010
|
+
classes,
|
|
1011
|
+
requiredCredentials: requiredAgentmailCredentials(names),
|
|
1012
|
+
/**
|
|
1013
|
+
* What this inbox actually sent in `window` (APRV-245).
|
|
1014
|
+
*
|
|
1015
|
+
* Read-only and outside any grant window, as the contract requires. It
|
|
1016
|
+
* reads the vault through the provider the CALLER built — the same
|
|
1017
|
+
* `vaultCredentialProvider` `approval setup adapter agentmail` uses for its
|
|
1018
|
+
* probe, and never the `.approval/env` passphrase fallback, which is
|
|
1019
|
+
* defensible only inside a consumed-token window.
|
|
1020
|
+
*
|
|
1021
|
+
* The effect id is the provider's `message_id`, and since APRV-251 it is
|
|
1022
|
+
* the join key rather than a string for a person to read. A send that went
|
|
1023
|
+
* through this adapter recorded the same id on its `execution.completed`
|
|
1024
|
+
* (the receipt names it under `provider_ref`, and the contract lifts it), so
|
|
1025
|
+
* `approval coverage` answers about THAT message. A message this inbox sent
|
|
1026
|
+
* that no record names by id falls through to the class-and-window rule
|
|
1027
|
+
* every other source gets, which is the honest weaker answer rather than a
|
|
1028
|
+
* gap: a send made before the amendment carries no reference.
|
|
1029
|
+
*/
|
|
1030
|
+
async observe(window, credentials) {
|
|
1031
|
+
const configured = readAgentmailConfig(credentials, names);
|
|
1032
|
+
const scrub = (text) => redactSecrets(text, configured.secrets).text;
|
|
1033
|
+
if (!configured.ok) {
|
|
1034
|
+
// Already scrubbed with everything read before it failed. A throw
|
|
1035
|
+
// rather than an empty list, because "the vault would not open" and
|
|
1036
|
+
// "this inbox sent nothing" are different facts and the source layer
|
|
1037
|
+
// reports them differently.
|
|
1038
|
+
throw new Error(configured.message);
|
|
1039
|
+
}
|
|
1040
|
+
const observed = await observeAgentmail(configured.config, window, {
|
|
1041
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
1042
|
+
apiBase,
|
|
1043
|
+
timeoutMs,
|
|
1044
|
+
});
|
|
1045
|
+
if (!observed.ok)
|
|
1046
|
+
throw new Error(scrub(observed.message));
|
|
1047
|
+
return observed.messages.map((message) => ({
|
|
1048
|
+
source: "agentmail",
|
|
1049
|
+
id: message.messageId,
|
|
1050
|
+
class: AGENTMAIL_CLASS,
|
|
1051
|
+
at: message.at,
|
|
1052
|
+
actorHint: configured.config.inboxId,
|
|
1053
|
+
// The subject and a COUNT. Never the body, and never the addresses:
|
|
1054
|
+
// this line is read by somebody who did not approve the message.
|
|
1055
|
+
detail: scrub(`sent ${JSON.stringify(message.subject)} to ${String(message.recipients)} recipient(s)`),
|
|
1056
|
+
}));
|
|
1057
|
+
},
|
|
1058
|
+
/**
|
|
1059
|
+
* The draft comparison, run BEFORE the token is consumed (APRV-276).
|
|
1060
|
+
*
|
|
1061
|
+
* The bug this closes was found on a live inbox: the comparison used to run
|
|
1062
|
+
* only inside `act`, so a draft the agent had edited after the grant refused
|
|
1063
|
+
* `agentmail-draft-drifted` with `execution.started` and `execution.failed`
|
|
1064
|
+
* already on the log. The refusal was right and the accounting was wrong —
|
|
1065
|
+
* the single-use token was spent by an attempt that sent nothing, and
|
|
1066
|
+
* restoring the approved text could not send under the grant the human had
|
|
1067
|
+
* already given. A refusal that costs a human another tap teaches operators
|
|
1068
|
+
* to stop checking, which is the one lesson this project cannot afford to
|
|
1069
|
+
* teach.
|
|
1070
|
+
*
|
|
1071
|
+
* DRAFT MODE ONLY, and the asymmetry is the point. A draft is state the far
|
|
1072
|
+
* side holds and the agent can rewrite, so what a grant binds and what would
|
|
1073
|
+
* be sent can diverge with nobody at fault. A direct send has no such
|
|
1074
|
+
* object: its bytes are the payload, the payload hash binds them, and the
|
|
1075
|
+
* only pre-send refusal left is `agentmail-from-mismatch`, which is a fact
|
|
1076
|
+
* about the configured inbox rather than about anything that moved. That
|
|
1077
|
+
* check stays in `act`, where its inbox read doubles as the credential
|
|
1078
|
+
* check, and a direct send therefore costs no extra request here.
|
|
1079
|
+
*
|
|
1080
|
+
* Reads and compares; sends nothing. The API key comes from the vault
|
|
1081
|
+
* through the provider the contract scopes to this call, which is the same
|
|
1082
|
+
* sending key `act` uses: see SPEC.md §10.4 for why the comparison is not
|
|
1083
|
+
* performed with a key the caller supplies.
|
|
1084
|
+
*/
|
|
1085
|
+
async precheck(input) {
|
|
1086
|
+
const mode = agentmailMode(input.payload);
|
|
1087
|
+
if (!mode.ok) {
|
|
1088
|
+
return {
|
|
1089
|
+
ok: false,
|
|
1090
|
+
code: mode.code,
|
|
1091
|
+
message: `the approved payload for ${input.actionKey} is not a well-formed AgentMail send: ${mode.message}. Nothing was requested and nothing was sent`,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
// Nothing server-side to compare, so nothing to spend a request on.
|
|
1095
|
+
if (mode.mode === "direct")
|
|
1096
|
+
return { ok: true };
|
|
1097
|
+
const configured = readAgentmailConfig(input.credentials, names);
|
|
1098
|
+
if (!configured.ok) {
|
|
1099
|
+
// Already scrubbed with everything that had been read when it failed.
|
|
1100
|
+
return { ok: false, code: configured.code, message: configured.message };
|
|
1101
|
+
}
|
|
1102
|
+
const config = configured.config;
|
|
1103
|
+
const scrub = (text) => redactSecrets(text, configured.secrets).text;
|
|
1104
|
+
const checked = await checkDraftBeforeSend(input.actionKey, input.payload, config, transportFor(config, input.signal), scrub);
|
|
1105
|
+
return checked.ok ? { ok: true } : { ok: false, code: checked.code, message: checked.message };
|
|
1106
|
+
},
|
|
1107
|
+
async act(input) {
|
|
1108
|
+
// (1) The mode, then the shape. Both refused before any credential is
|
|
1109
|
+
// read: a malformed payload is not a reason to touch the vault.
|
|
1110
|
+
const mode = agentmailMode(input.payload);
|
|
1111
|
+
if (!mode.ok) {
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
code: mode.code,
|
|
1115
|
+
message: `the approved payload for ${input.actionKey} is not a well-formed AgentMail send: ${mode.message}. Nothing was requested and nothing was sent`,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
// (2) The configuration, from the vault, inside the window, through the
|
|
1119
|
+
// one reader.
|
|
1120
|
+
const configured = readAgentmailConfig(input.credentials, names);
|
|
1121
|
+
const scrub = (text) => redactSecrets(text, configured.secrets).text;
|
|
1122
|
+
if (!configured.ok) {
|
|
1123
|
+
// Already scrubbed with everything that had been read when it failed.
|
|
1124
|
+
return { ok: false, code: configured.code, message: configured.message };
|
|
1125
|
+
}
|
|
1126
|
+
const config = configured.config;
|
|
1127
|
+
const transport = transportFor(config, input.signal);
|
|
1128
|
+
const hash = payloadHash(input.payload);
|
|
1129
|
+
if (mode.mode === "direct") {
|
|
1130
|
+
const validated = validateEmailPayload(input.payload);
|
|
1131
|
+
if (!validated.ok) {
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
code: "agentmail-payload-invalid",
|
|
1135
|
+
message: `the approved payload for ${input.actionKey} is not a well-formed email: ${validated.message}. Nothing was requested and nothing was sent`,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const payload = validated.payload;
|
|
1139
|
+
// (3) The inbox read. It answers the `from` question AND is the
|
|
1140
|
+
// credential check; see the module header.
|
|
1141
|
+
const inbox = await preSendRead(transport, scrub, inboxPath(config.inboxId), `the inbox ${config.inboxId}`);
|
|
1142
|
+
if (!inbox.ok)
|
|
1143
|
+
return { ok: false, code: inbox.code, message: inbox.message };
|
|
1144
|
+
if (inbox.answer.status < 200 || inbox.answer.status >= 300) {
|
|
1145
|
+
return {
|
|
1146
|
+
ok: false,
|
|
1147
|
+
code: codeForStatus(inbox.answer.status),
|
|
1148
|
+
message: scrub(`reading the inbox ${config.inboxId} answered HTTP ${String(inbox.answer.status)}: ${describeBody(inbox.answer.body)}. Nothing was sent`),
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
const address = addressOf(parseObject(inbox.answer.body), config.inboxId);
|
|
1152
|
+
if (payload.from.toLowerCase() !== address.toLowerCase()) {
|
|
1153
|
+
return {
|
|
1154
|
+
ok: false,
|
|
1155
|
+
code: "agentmail-from-mismatch",
|
|
1156
|
+
message: scrub(`the approved message says it is from ${payload.from}, and the inbox ${config.inboxId} sends as ${address}. AgentMail has no per-message From — the inbox is the sender — so this message would have gone out under a different address than the one a human read. Nothing was sent`),
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
// (4) The send. Deliberately NOT wrapped: a throw here means the
|
|
1160
|
+
// request may have left the process, and the contract's
|
|
1161
|
+
// execution.indeterminate is the honest record of that.
|
|
1162
|
+
const answer = await call(transport, "POST", `${inboxPath(config.inboxId)}/messages/send`, directBody(payload));
|
|
1163
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
1164
|
+
return {
|
|
1165
|
+
ok: false,
|
|
1166
|
+
code: codeForStatus(answer.status),
|
|
1167
|
+
message: scrub(`the AgentMail send answered HTTP ${String(answer.status)}: ${describeBody(answer.body)}`),
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
ok: true,
|
|
1172
|
+
detail: receipt("direct", answer, hash, envelopeRecipients(payload).length),
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
// ---- draft mode --------------------------------------------------
|
|
1176
|
+
//
|
|
1177
|
+
// (3) and (4): the shape, the inbox, the draft read and the drift
|
|
1178
|
+
// comparison, through the one function `precheck` also calls. This
|
|
1179
|
+
// call is the one that binds the bytes: it runs inside the consumed-
|
|
1180
|
+
// token window, immediately before the POST, so the gap between the
|
|
1181
|
+
// comparison and the send is as small as the transport allows.
|
|
1182
|
+
const checked = await checkDraftBeforeSend(input.actionKey, input.payload, config, transport, scrub);
|
|
1183
|
+
if (!checked.ok)
|
|
1184
|
+
return { ok: false, code: checked.code, message: checked.message };
|
|
1185
|
+
const payload = checked.payload;
|
|
1186
|
+
// (5) The send. Unwrapped, for the reason given in direct mode.
|
|
1187
|
+
const answer = await call(transport, "POST", `${checked.draftPath}/send`);
|
|
1188
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
1189
|
+
return {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
code: codeForStatus(answer.status),
|
|
1192
|
+
message: scrub(`the AgentMail draft send answered HTTP ${String(answer.status)}: ${describeBody(answer.body)}`),
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const recipients = payload.to.length + (payload.cc?.length ?? 0) + (payload.bcc?.length ?? 0);
|
|
1196
|
+
return { ok: true, detail: receipt("draft", answer, hash, recipients) };
|
|
1197
|
+
},
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
//# sourceMappingURL=agentmail.js.map
|