approval-md 0.1.0 → 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/README.md +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -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 +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- 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/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -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/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- 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 +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -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/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- 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/agents-md.d.ts +276 -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 +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -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/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,426 @@
|
|
|
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 type { CredentialSpec } from "../core/credential-spec.js";
|
|
125
|
+
import { type Adapter, type CredentialProvider, type JsonValue } from "./contract.js";
|
|
126
|
+
/** The one class this adapter serves. The same string the email adapter serves. */
|
|
127
|
+
export declare const AGENTMAIL_CLASS = "communicate.email.external";
|
|
128
|
+
/** The public API. Overridable so the whole adapter can run against loopback. */
|
|
129
|
+
export declare const AGENTMAIL_DEFAULT_API_BASE = "https://api.agentmail.to";
|
|
130
|
+
/** Whole-request budget for one HTTP call. */
|
|
131
|
+
export declare const AGENTMAIL_DEFAULT_TIMEOUT_MS = 15000;
|
|
132
|
+
/**
|
|
133
|
+
* The two vault names, as an open record rather than a literal type: a
|
|
134
|
+
* deployment that stores these under other names must be able to SAY so, and a
|
|
135
|
+
* `typeof` of the defaults would type `credentialNames` as the defaults.
|
|
136
|
+
*/
|
|
137
|
+
export interface AgentmailCredentialNames {
|
|
138
|
+
apiKey: string;
|
|
139
|
+
inboxId: string;
|
|
140
|
+
}
|
|
141
|
+
/** The vault names, overridable per deployment. */
|
|
142
|
+
export declare const DEFAULT_AGENTMAIL_CREDENTIAL_NAMES: AgentmailCredentialNames;
|
|
143
|
+
export declare const AGENTMAIL_FAILURE_CODES: readonly ["agentmail-payload-invalid", "agentmail-payload-ambiguous", "agentmail-config-invalid", "agentmail-inbox-mismatch", "agentmail-from-mismatch", "agentmail-draft-missing", "agentmail-draft-drifted", "agentmail-unreachable", "agentmail-unauthorized", "agentmail-not-found", "agentmail-conflict", "agentmail-rate-limited", "agentmail-rejected", "agentmail-server-error", "credential-unavailable", "credential-refused", "credential-window-closed"];
|
|
144
|
+
export type AgentmailFailureCode = (typeof AGENTMAIL_FAILURE_CODES)[number];
|
|
145
|
+
export declare function isAgentmailFailureCode(value: string): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* What this adapter reads from the vault, declared rather than discovered, so
|
|
148
|
+
* `approval setup adapter agentmail` can ask for it without knowing what
|
|
149
|
+
* AgentMail is. DERIVED from {@link DEFAULT_AGENTMAIL_CREDENTIAL_NAMES} rather
|
|
150
|
+
* than restating the strings.
|
|
151
|
+
*/
|
|
152
|
+
export declare const AGENTMAIL_CREDENTIAL_SPECS: readonly CredentialSpec[];
|
|
153
|
+
/**
|
|
154
|
+
* The names this adapter cannot act without (APRV-169), for the contract's
|
|
155
|
+
* pre-token resolution. Derived from the manifest, mapped through `names` so a
|
|
156
|
+
* deployment that renamed one gets the name it actually stored.
|
|
157
|
+
*/
|
|
158
|
+
export declare function requiredAgentmailCredentials(names?: AgentmailCredentialNames): readonly string[];
|
|
159
|
+
/** What {@link readAgentmailConfig} resolved. */
|
|
160
|
+
export interface AgentmailConfig {
|
|
161
|
+
apiKey: string;
|
|
162
|
+
inboxId: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* What one configuration read produced.
|
|
166
|
+
*
|
|
167
|
+
* `secrets` is the redaction corpus and holds ONLY the secret-kind values (the
|
|
168
|
+
* API key). The inbox id is not a secret and is deliberately left scrubbable-not:
|
|
169
|
+
* it appears in refusal sentences that are useless without it. Returned on both
|
|
170
|
+
* branches, because a caller that goes on to make requests must scrub the far
|
|
171
|
+
* side's sentences with the same corpus this read built.
|
|
172
|
+
*/
|
|
173
|
+
export type AgentmailConfigOutcome = {
|
|
174
|
+
ok: true;
|
|
175
|
+
config: AgentmailConfig;
|
|
176
|
+
secrets: readonly string[];
|
|
177
|
+
} | {
|
|
178
|
+
ok: false;
|
|
179
|
+
code: AgentmailFailureCode;
|
|
180
|
+
message: string;
|
|
181
|
+
secrets: readonly string[];
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Read this adapter's whole configuration from a credential provider.
|
|
185
|
+
*
|
|
186
|
+
* The single place in the repository that turns a {@link CredentialProvider}
|
|
187
|
+
* into AgentMail settings — the names it asks for, the order, the shape rules —
|
|
188
|
+
* mirroring `readEmailSmtpConfig` for the same reason: `act` calls it inside the
|
|
189
|
+
* verified execution window and `approval setup adapter agentmail` calls it to probe
|
|
190
|
+
* a configuration it only partly typed, and a second reader would be a second
|
|
191
|
+
* opinion about what "configured" means.
|
|
192
|
+
*/
|
|
193
|
+
export declare function readAgentmailConfig(credentials: CredentialProvider, names?: AgentmailCredentialNames): AgentmailConfigOutcome;
|
|
194
|
+
/**
|
|
195
|
+
* The slice of `fetch` this module uses, structurally.
|
|
196
|
+
*
|
|
197
|
+
* Declared here rather than imported so the adapter depends on a shape and not
|
|
198
|
+
* on a lib: a test hands over a stub, and the default is the global `fetch`
|
|
199
|
+
* Node ≥ 20 ships.
|
|
200
|
+
*/
|
|
201
|
+
export type AgentmailFetch = (url: string, init: {
|
|
202
|
+
method: string;
|
|
203
|
+
headers: Record<string, string>;
|
|
204
|
+
body?: string;
|
|
205
|
+
signal: AbortSignal;
|
|
206
|
+
}) => Promise<{
|
|
207
|
+
ok: boolean;
|
|
208
|
+
status: number;
|
|
209
|
+
text(): Promise<string>;
|
|
210
|
+
}>;
|
|
211
|
+
/** The one non-sending read, exported so the setup wizard can probe a key. */
|
|
212
|
+
export type AgentmailProbe = {
|
|
213
|
+
ok: true;
|
|
214
|
+
address: string;
|
|
215
|
+
http_status: number;
|
|
216
|
+
/**
|
|
217
|
+
* The permissions the inbox read DISCLOSED about the calling key, or
|
|
218
|
+
* `null` when it disclosed none (APRV-223).
|
|
219
|
+
*
|
|
220
|
+
* Read from the one response this adapter already asks for, never from a
|
|
221
|
+
* second endpoint: a setup verb that probed a URL nobody has confirmed
|
|
222
|
+
* exists would report a 404 as a permissions problem, which is a worse
|
|
223
|
+
* answer than "not disclosed". `null` therefore means UNKNOWN and never
|
|
224
|
+
* "none", and every caller must treat it as the reminder it is.
|
|
225
|
+
*/
|
|
226
|
+
permissions: readonly string[] | null;
|
|
227
|
+
} | {
|
|
228
|
+
ok: false;
|
|
229
|
+
code: AgentmailFailureCode;
|
|
230
|
+
message: string;
|
|
231
|
+
};
|
|
232
|
+
/** The two permissions a key must hold to send anything for this adapter. */
|
|
233
|
+
export declare const AGENTMAIL_SEND_PERMISSIONS: readonly ["draft_send", "message_send"];
|
|
234
|
+
export interface AgentmailProbeOptions {
|
|
235
|
+
fetch?: AgentmailFetch;
|
|
236
|
+
apiBase?: string;
|
|
237
|
+
timeoutMs?: number;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `GET /v0/inboxes/{inbox_id}`: does this key open this inbox, and what address
|
|
241
|
+
* does it send as? Sends nothing, changes nothing, and is the credential check
|
|
242
|
+
* both `act` and `approval setup adapter agentmail` use.
|
|
243
|
+
*/
|
|
244
|
+
export declare function probeAgentmail(config: AgentmailConfig, options?: AgentmailProbeOptions): Promise<AgentmailProbe>;
|
|
245
|
+
/** The snapshot of a composed draft a grant binds to. */
|
|
246
|
+
export interface AgentmailDraftPayload {
|
|
247
|
+
inbox_id: string;
|
|
248
|
+
draft_id: string;
|
|
249
|
+
to: string[];
|
|
250
|
+
cc?: string[];
|
|
251
|
+
bcc?: string[];
|
|
252
|
+
subject: string;
|
|
253
|
+
text: string;
|
|
254
|
+
}
|
|
255
|
+
/** The fields the drift check covers: everything a reader of the draft saw. */
|
|
256
|
+
export declare const AGENTMAIL_DRAFT_FIELDS: readonly ["to", "cc", "bcc", "subject", "text"];
|
|
257
|
+
export type AgentmailMode = "direct" | "draft";
|
|
258
|
+
export type AgentmailDraftValidation = {
|
|
259
|
+
ok: true;
|
|
260
|
+
payload: AgentmailDraftPayload;
|
|
261
|
+
} | {
|
|
262
|
+
ok: false;
|
|
263
|
+
message: string;
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Structural validation of a draft payload. Never throws; returns the reason.
|
|
267
|
+
*
|
|
268
|
+
* Exported so a caller can check a payload BEFORE requesting approval for it,
|
|
269
|
+
* which is the only place a shape error can still be fixed cheaply.
|
|
270
|
+
*/
|
|
271
|
+
export declare function validateAgentmailDraftPayload(value: JsonValue): AgentmailDraftValidation;
|
|
272
|
+
/**
|
|
273
|
+
* Which mode a payload is in, by the markers it carries.
|
|
274
|
+
*
|
|
275
|
+
* A payload with markers of both modes is ambiguous and a payload with markers
|
|
276
|
+
* of neither is not addressed to this adapter at all. Both are refused: a send
|
|
277
|
+
* mode chosen by inference is a side effect chosen by inference.
|
|
278
|
+
*/
|
|
279
|
+
export declare function agentmailMode(value: JsonValue): {
|
|
280
|
+
ok: true;
|
|
281
|
+
mode: AgentmailMode;
|
|
282
|
+
} | {
|
|
283
|
+
ok: false;
|
|
284
|
+
code: AgentmailFailureCode;
|
|
285
|
+
message: string;
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Which of {@link AGENTMAIL_DRAFT_FIELDS} differ between the approved snapshot
|
|
289
|
+
* and what the server now holds. Names only — never values.
|
|
290
|
+
*/
|
|
291
|
+
export declare function draftDrift(approved: AgentmailDraftPayload, fetched: Record<string, unknown>): string[];
|
|
292
|
+
export type AgentmailDraftSnapshot = {
|
|
293
|
+
ok: true;
|
|
294
|
+
payload: AgentmailDraftPayload;
|
|
295
|
+
} | {
|
|
296
|
+
ok: false;
|
|
297
|
+
message: string;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* The payload a grant should bind to, built from what the API holds RIGHT NOW
|
|
301
|
+
* (APRV-223).
|
|
302
|
+
*
|
|
303
|
+
* `approval payload agentmail-draft` prints this and nothing else, and it lives
|
|
304
|
+
* here rather than in the CLI for one reason: the bytes it prints are the bytes
|
|
305
|
+
* {@link draftDrift} will compare against the same draft at send time, so the
|
|
306
|
+
* two must be one piece of code. A second opinion in the CLI about what "the
|
|
307
|
+
* draft's cc" is would be a snapshot that drifts from a draft nobody changed.
|
|
308
|
+
*
|
|
309
|
+
* The rules follow {@link canonicalField} exactly. `cc`/`bcc` are OMITTED when
|
|
310
|
+
* the draft holds nothing for them, because absent, `null` and `[]` are one
|
|
311
|
+
* fact there; `to` is copied through as the array it is, unnormalized, because
|
|
312
|
+
* a reordered or re-shaped recipient list is a different message. Anything this
|
|
313
|
+
* function cannot turn into a well-formed snapshot is refused with the reason:
|
|
314
|
+
* a payload that fails {@link validateAgentmailDraftPayload} at send time is a
|
|
315
|
+
* refusal a human has already been asked to approve.
|
|
316
|
+
*/
|
|
317
|
+
export declare function draftSnapshot(inboxId: string, draftId: string, fetched: Record<string, unknown>): AgentmailDraftSnapshot;
|
|
318
|
+
export type AgentmailDraftRead = {
|
|
319
|
+
ok: true;
|
|
320
|
+
draft: Record<string, unknown>;
|
|
321
|
+
http_status: number;
|
|
322
|
+
} | {
|
|
323
|
+
ok: false;
|
|
324
|
+
code: AgentmailFailureCode;
|
|
325
|
+
message: string;
|
|
326
|
+
};
|
|
327
|
+
export interface AgentmailDraftReadOptions extends AgentmailProbeOptions {
|
|
328
|
+
/** The key that reads the draft. The AGENT's key here, not the vault's. */
|
|
329
|
+
apiKey: string;
|
|
330
|
+
inboxId: string;
|
|
331
|
+
draftId: string;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* `GET /v0/inboxes/{inbox}/drafts/{draft}`: one draft, read and nothing else.
|
|
335
|
+
*
|
|
336
|
+
* The read half of the draft flow, exported for `approval payload
|
|
337
|
+
* agentmail-draft` (APRV-223), which runs BEFORE any approval exists and with
|
|
338
|
+
* the agent's own key rather than the vault's. It sends nothing and spends no
|
|
339
|
+
* token: what it produces is a proposal a human has yet to see.
|
|
340
|
+
*/
|
|
341
|
+
export declare function readAgentmailDraft(options: AgentmailDraftReadOptions): Promise<AgentmailDraftRead>;
|
|
342
|
+
/**
|
|
343
|
+
* How many messages one page asks for. The API's own cap is higher; this is the
|
|
344
|
+
* page size, and {@link OBSERVE_MAX_PAGES} bounds how many pages are walked.
|
|
345
|
+
*/
|
|
346
|
+
export declare const AGENTMAIL_OBSERVE_PAGE_SIZE = 100;
|
|
347
|
+
/**
|
|
348
|
+
* How many pages one observation walks.
|
|
349
|
+
*
|
|
350
|
+
* A bound rather than a full drain, because a reporting verb must terminate
|
|
351
|
+
* against an inbox of any size. A run that hits the bound says so, so a reader
|
|
352
|
+
* never mistakes a truncated page walk for a quiet mailbox.
|
|
353
|
+
*/
|
|
354
|
+
export declare const OBSERVE_MAX_PAGES = 10;
|
|
355
|
+
/** One sent message, reduced to what a coverage report may say out loud. */
|
|
356
|
+
export interface AgentmailObservedMessage {
|
|
357
|
+
messageId: string;
|
|
358
|
+
/** RFC 3339, as the provider reported it. */
|
|
359
|
+
at: string;
|
|
360
|
+
subject: string;
|
|
361
|
+
recipients: number;
|
|
362
|
+
}
|
|
363
|
+
export type AgentmailObservation = {
|
|
364
|
+
ok: true;
|
|
365
|
+
messages: AgentmailObservedMessage[];
|
|
366
|
+
/** Set when the page bound stopped the walk before the far side ran out. */
|
|
367
|
+
truncated: boolean;
|
|
368
|
+
} | {
|
|
369
|
+
ok: false;
|
|
370
|
+
code: AgentmailFailureCode;
|
|
371
|
+
message: string;
|
|
372
|
+
};
|
|
373
|
+
export interface AgentmailObserveOptions extends AgentmailProbeOptions {
|
|
374
|
+
/** Override {@link AGENTMAIL_OBSERVE_PAGE_SIZE}. */
|
|
375
|
+
pageSize?: number;
|
|
376
|
+
/** Override {@link OBSERVE_MAX_PAGES}. */
|
|
377
|
+
maxPages?: number;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* `GET /v0/inboxes/{inbox_id}/messages`: what this inbox actually sent.
|
|
381
|
+
*
|
|
382
|
+
* The endpoint and its fields are AgentMail's own, documented at
|
|
383
|
+
* https://docs.agentmail.to/api-reference/inboxes/messages/list — `message_id`,
|
|
384
|
+
* `labels`, `timestamp`, `to` and `subject`. The query carries `after`, `before`
|
|
385
|
+
* and `limit`, and `page_token` on every page after the first.
|
|
386
|
+
*
|
|
387
|
+
* **The sent filter is client-side, and that is a limit worth stating.** The
|
|
388
|
+
* documented list endpoint exposes no sent-only parameter, so this asks for the
|
|
389
|
+
* window's messages and keeps the ones whose `labels` include `sent`. Two
|
|
390
|
+
* consequences follow and neither is papered over: the request reads received
|
|
391
|
+
* mail as well as sent (a read, changing nothing), and a provider that stopped
|
|
392
|
+
* labelling sent mail would make this source report an empty window rather than
|
|
393
|
+
* an error. The remedy for the second is the same as for everything else here:
|
|
394
|
+
* the source reports what the provider said, and a source that says nothing is
|
|
395
|
+
* a gap a reader can see rather than a pass.
|
|
396
|
+
*
|
|
397
|
+
* Sends nothing. Spends no token. Reads no clock: the window is the caller's.
|
|
398
|
+
*/
|
|
399
|
+
export declare function observeAgentmail(config: AgentmailConfig, window: {
|
|
400
|
+
since: string;
|
|
401
|
+
until: string;
|
|
402
|
+
}, options?: AgentmailObserveOptions): Promise<AgentmailObservation>;
|
|
403
|
+
export interface AgentmailAdapterOptions {
|
|
404
|
+
/**
|
|
405
|
+
* Additional classes this adapter serves, **added** to {@link AGENTMAIL_CLASS}
|
|
406
|
+
* rather than replacing it: the class list is routing, and a list that
|
|
407
|
+
* replaced the default would make "add one class" silently stop serving the
|
|
408
|
+
* canonical one.
|
|
409
|
+
*/
|
|
410
|
+
classes?: readonly string[];
|
|
411
|
+
/** Injectable `fetch`, for tests. Defaults to the global. */
|
|
412
|
+
fetch?: AgentmailFetch;
|
|
413
|
+
/** API base. Defaults to {@link AGENTMAIL_DEFAULT_API_BASE}. */
|
|
414
|
+
apiBase?: string;
|
|
415
|
+
/** Per-request budget. Defaults to {@link AGENTMAIL_DEFAULT_TIMEOUT_MS}. */
|
|
416
|
+
timeoutMs?: number;
|
|
417
|
+
/** Override the vault names. Partial: unnamed entries keep their default. */
|
|
418
|
+
credentialNames?: Partial<AgentmailCredentialNames>;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* A fresh AgentMail adapter.
|
|
422
|
+
*
|
|
423
|
+
* Stateless and reusable: it holds no connection and nothing from a previous
|
|
424
|
+
* send. Two concurrent executions through the same instance share nothing.
|
|
425
|
+
*/
|
|
426
|
+
export declare function agentmailAdapter(options?: AgentmailAdapterOptions): Adapter;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* deployment this adapter is written for gives the agent a key WITHOUT the two
|
|
21
21
|
* send permissions and puts a key WITH them in the vault under
|
|
22
22
|
* {@link DEFAULT_AGENTMAIL_CREDENTIAL_NAMES}.apiKey, where it is readable only
|
|
23
|
-
* inside the verified
|
|
23
|
+
* inside the verified execution window the contract opens. The agent can therefore
|
|
24
24
|
* compose all day and cannot send at all; the sending key answers to a grant.
|
|
25
25
|
*
|
|
26
26
|
* ## Two payload modes, discriminated by shape, ambiguity refused
|
|
@@ -242,7 +242,7 @@ export function requiredAgentmailCredentials(names = DEFAULT_AGENTMAIL_CREDENTIA
|
|
|
242
242
|
* The single place in the repository that turns a {@link CredentialProvider}
|
|
243
243
|
* into AgentMail settings — the names it asks for, the order, the shape rules —
|
|
244
244
|
* mirroring `readEmailSmtpConfig` for the same reason: `act` calls it inside the
|
|
245
|
-
* verified
|
|
245
|
+
* verified execution window and `approval setup adapter agentmail` calls it to probe
|
|
246
246
|
* a configuration it only partly typed, and a second reader would be a second
|
|
247
247
|
* opinion about what "configured" means.
|
|
248
248
|
*/
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared adapter conformance suite (SPEC.md §10.4, §11.1).
|
|
3
|
+
*
|
|
4
|
+
* `channels/conformance.ts` exists because SPEC.md §9 names a display rule and
|
|
5
|
+
* then names its consequence: rendering claimed fields as computed "is a
|
|
6
|
+
* conformance failure for a channel". This module is the same idea one boundary
|
|
7
|
+
* further out. §10.4 says an adapter "MUST require a valid, unexpired,
|
|
8
|
+
* single-use execution token bound to the action's `idempotency_key`", and the
|
|
9
|
+
* only way to find out whether a given adapter does is to hand it a bad token
|
|
10
|
+
* and watch.
|
|
11
|
+
*
|
|
12
|
+
* What is being tested is mostly **the contract**, not the adapter: an adapter
|
|
13
|
+
* that goes through {@link executeThroughAdapter} inherits the sequence and
|
|
14
|
+
* cannot skip a step. That is the point. The suite is what turns "cannot skip"
|
|
15
|
+
* from an assertion in a module header into a thing someone has watched fail,
|
|
16
|
+
* and it is what a third-party adapter runs to learn whether it is wired into
|
|
17
|
+
* the gate or merely near it.
|
|
18
|
+
*
|
|
19
|
+
* ## What it checks
|
|
20
|
+
*
|
|
21
|
+
* 1. **Bad token, no side effect.** A garbage token refuses and `act` is never
|
|
22
|
+
* called. The log is unchanged.
|
|
23
|
+
* 2. **Wrong bytes, no side effect.** A payload that is not the approved one
|
|
24
|
+
* refuses `payload-mismatch`, `act` is never called, the token stays live.
|
|
25
|
+
* 3. **Wrong class, nothing appended.** An adapter that does not serve the
|
|
26
|
+
* declared class refuses `adapter-class-mismatch` before the log is touched.
|
|
27
|
+
* 4. **`started` precedes the effect.** On the happy path `act` observes an
|
|
28
|
+
* `execution.started` for its own key already in the verified log at the
|
|
29
|
+
* moment it is called, and `execution.completed` lands after it returns. The
|
|
30
|
+
* same check reads the provider reference of APRV-251 off that record: a
|
|
31
|
+
* detail naming one is on the record under this adapter's name, and a detail
|
|
32
|
+
* naming none leaves the record carrying none.
|
|
33
|
+
* 5. **Single use.** A second execution with the same token and key refuses
|
|
34
|
+
* without calling `act`.
|
|
35
|
+
* 6. **Credentials are scoped and never leak.** A value handed out inside `act`
|
|
36
|
+
* appears in no log line and in no field of the result, and the provider
|
|
37
|
+
* refuses once `act` has returned.
|
|
38
|
+
* 7. **Failure is recorded, not swallowed.** An adapter reporting failure
|
|
39
|
+
* produces `execution.failed` and an `adapter-failed` refusal.
|
|
40
|
+
*
|
|
41
|
+
* The chain is verified after every check.
|
|
42
|
+
*
|
|
43
|
+
* ## How it is run
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* test("my adapter conforms", async (t) => {
|
|
47
|
+
* await runAdapterConformance(t, () => new MyAdapter(), harness);
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* As in the channel suite, `t` is used only for `diagnostic()` labels: the
|
|
52
|
+
* checks run inline and **throw** on the first failure, so
|
|
53
|
+
* `tests/adapters-contract.test.ts` can assert the suite goes RED against
|
|
54
|
+
* deliberately broken adapters. A conformance suite nobody has watched fail is a
|
|
55
|
+
* suite that might pass anything.
|
|
56
|
+
*
|
|
57
|
+
* Several checks wrap the adapter under test in a hostile shell — one that reads
|
|
58
|
+
* a credential it was not asked to read, one that returns the secret inside its
|
|
59
|
+
* own detail, one that reports failure. The wrapper delegates to the real `act`
|
|
60
|
+
* where the check allows it. Wrapping rather than requiring cooperation is what
|
|
61
|
+
* lets the suite test properties (scoping, redaction) that a well-behaved
|
|
62
|
+
* adapter would never exercise on its own.
|
|
63
|
+
*/
|
|
64
|
+
import { type Adapter, type AdapterExecuteOptions, type JsonValue } from "./contract.js";
|
|
65
|
+
/** Anything with a `diagnostic` method — `node:test`'s `TestContext` qualifies. */
|
|
66
|
+
export interface ConformanceContext {
|
|
67
|
+
diagnostic?(message: string): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One scenario the harness prepares: a real log carrying a real grant, built
|
|
71
|
+
* through the real gate, and the token that grant printed.
|
|
72
|
+
*
|
|
73
|
+
* Everything here is a fact about the world the suite executes against. The
|
|
74
|
+
* suite never builds a grant itself and never hand-writes a log line; a
|
|
75
|
+
* conformance suite that fabricated its own authorization would be testing the
|
|
76
|
+
* fabrication.
|
|
77
|
+
*/
|
|
78
|
+
export interface AdapterConformanceCase {
|
|
79
|
+
/** Path to a real `events.jsonl` holding a granted, unspent manual action. */
|
|
80
|
+
logPath: string;
|
|
81
|
+
actionKey: string;
|
|
82
|
+
/** The bytes the grant bound to. Must hash to the grant's `payload_hash`. */
|
|
83
|
+
payload: JsonValue;
|
|
84
|
+
/** The raw single-use token `approval grant` printed. */
|
|
85
|
+
token: string;
|
|
86
|
+
/** The executing identity: `agent:<id>` or `human:<id>`. */
|
|
87
|
+
actor: string;
|
|
88
|
+
/** The class the action was declared under; the adapter must serve it. */
|
|
89
|
+
class: string;
|
|
90
|
+
/** Policy location, schema dir, injected clock. Merged into every call. */
|
|
91
|
+
options?: AdapterExecuteOptions;
|
|
92
|
+
/** Called when the suite is done with this case. */
|
|
93
|
+
cleanup?(): void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What an adapter's test file must provide.
|
|
97
|
+
*
|
|
98
|
+
* `setup()` returns a *fresh* case each call: a token is single-use, so the
|
|
99
|
+
* suite cannot reuse one across checks. `credential` names a secret the suite
|
|
100
|
+
* will ask for from inside `act` and then hunt for in the log and the result;
|
|
101
|
+
* its value must be a distinctive string that could not occur by accident.
|
|
102
|
+
* A class the adapter does NOT serve is needed for the routing check, and
|
|
103
|
+
* `foreignClass` supplies it.
|
|
104
|
+
*/
|
|
105
|
+
export interface AdapterConformanceHarness {
|
|
106
|
+
setup(): AdapterConformanceCase | Promise<AdapterConformanceCase>;
|
|
107
|
+
credential: {
|
|
108
|
+
name: string;
|
|
109
|
+
value: string;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Everything else the adapter needs to reach its far side — a host, a port, a
|
|
113
|
+
* transport setting, a second half of a login.
|
|
114
|
+
*
|
|
115
|
+
* A real adapter rarely needs exactly one credential (the email adapter needs
|
|
116
|
+
* five), and the checks that must SUCCEED — the happy path, single use, the
|
|
117
|
+
* live token after a payload mismatch — cannot succeed against an adapter that
|
|
118
|
+
* cannot configure itself. `credential` stays the one the suite hunts for in
|
|
119
|
+
* the log and the result; these are merely present, and `credential` wins any
|
|
120
|
+
* collision so the hunted value cannot be shadowed. (Added APRV-69.)
|
|
121
|
+
*/
|
|
122
|
+
credentials?: Readonly<Record<string, string>>;
|
|
123
|
+
/** A declared class this adapter must refuse. Defaults to a synthetic one. */
|
|
124
|
+
foreignClass?: string;
|
|
125
|
+
/**
|
|
126
|
+
* What the optional `observe` check needs (APRV-245). Ignored by an adapter
|
|
127
|
+
* that implements no `observe`.
|
|
128
|
+
*
|
|
129
|
+
* `writes` is how the fixture reports the number of WRITE requests its far
|
|
130
|
+
* side has received; the check reads it before and after and requires the
|
|
131
|
+
* number not to move. Without it the check still runs and still proves the
|
|
132
|
+
* log was untouched, but the "did not POST" claim rests on the log alone, so
|
|
133
|
+
* an adapter with a reachable fixture should supply it.
|
|
134
|
+
*/
|
|
135
|
+
observeProbe?: {
|
|
136
|
+
/** The window to ask about. Defaults to one wide enough to include anything. */
|
|
137
|
+
window?: {
|
|
138
|
+
since: string;
|
|
139
|
+
until: string;
|
|
140
|
+
};
|
|
141
|
+
/** Write requests the far side has received so far. */
|
|
142
|
+
writes?(): number;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Run the suite. Resolves when every check passes; throws (an `AssertionError`)
|
|
147
|
+
* on the first failure.
|
|
148
|
+
*/
|
|
149
|
+
export declare function runAdapterConformance(t: ConformanceContext, makeAdapter: () => Adapter, harness: AdapterConformanceHarness): Promise<void>;
|