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.
Files changed (266) hide show
  1. package/README.md +584 -553
  2. package/SPEC.md +42 -13
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +623 -0
  27. package/dist/src/channels/payload-view.d.ts +35 -0
  28. package/dist/src/channels/render-queue.d.ts +149 -0
  29. package/dist/src/channels/tagging.d.ts +196 -0
  30. package/dist/src/channels/telegram.d.ts +1832 -0
  31. package/dist/src/channels/web.d.ts +341 -0
  32. package/dist/src/cli/adapter.d.ts +90 -0
  33. package/dist/src/cli/adapter.js +25 -15
  34. package/dist/src/cli/adapter.js.map +1 -1
  35. package/dist/src/cli/amend.d.ts +59 -0
  36. package/dist/src/cli/args.d.ts +43 -0
  37. package/dist/src/cli/attest.d.ts +41 -0
  38. package/dist/src/cli/audit-card.d.ts +62 -0
  39. package/dist/src/cli/audit.d.ts +59 -0
  40. package/dist/src/cli/channel-telegram.d.ts +806 -0
  41. package/dist/src/cli/channel-web.d.ts +131 -0
  42. package/dist/src/cli/channel.d.ts +71 -0
  43. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  44. package/dist/src/cli/codex.d.ts +2 -0
  45. package/dist/src/cli/codex.js +172 -0
  46. package/dist/src/cli/codex.js.map +1 -0
  47. package/dist/src/cli/coverage.d.ts +61 -0
  48. package/dist/src/cli/daemon.d.ts +120 -0
  49. package/dist/src/cli/doctor.d.ts +129 -0
  50. package/dist/src/cli/doctor.js +119 -5
  51. package/dist/src/cli/doctor.js.map +1 -1
  52. package/dist/src/cli/env.d.ts +65 -0
  53. package/dist/src/cli/execute.d.ts +202 -0
  54. package/dist/src/cli/exit-codes.d.ts +73 -0
  55. package/dist/src/cli/feedback.d.ts +60 -0
  56. package/dist/src/cli/gate-window.d.ts +40 -0
  57. package/dist/src/cli/gate.d.ts +68 -0
  58. package/dist/src/cli/git-scope.d.ts +190 -0
  59. package/dist/src/cli/gloss-attach.d.ts +85 -0
  60. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  61. package/dist/src/cli/gloss-codex.d.ts +24 -0
  62. package/dist/src/cli/gloss-options.d.ts +42 -0
  63. package/dist/src/cli/gloss.d.ts +265 -0
  64. package/dist/src/cli/help.d.ts +103 -0
  65. package/dist/src/cli/help.js +173 -51
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +78 -0
  68. package/dist/src/cli/hook-codex.js +167 -0
  69. package/dist/src/cli/hook-codex.js.map +1 -0
  70. package/dist/src/cli/hook.d.ts +331 -0
  71. package/dist/src/cli/hook.js +186 -80
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.d.ts +35 -0
  74. package/dist/src/cli/init.d.ts +84 -0
  75. package/dist/src/cli/init.js +2 -2
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.d.ts +23 -0
  78. package/dist/src/cli/journal.d.ts +41 -0
  79. package/dist/src/cli/log-advance.d.ts +287 -0
  80. package/dist/src/cli/log-advance.js +102 -11
  81. package/dist/src/cli/log-advance.js.map +1 -1
  82. package/dist/src/cli/log-anchor.d.ts +176 -0
  83. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  84. package/dist/src/cli/log-sync.d.ts +243 -0
  85. package/dist/src/cli/log-verbs.d.ts +16 -0
  86. package/dist/src/cli/log-verbs.js +7 -1
  87. package/dist/src/cli/log-verbs.js.map +1 -1
  88. package/dist/src/cli/long-help.d.ts +70 -0
  89. package/dist/src/cli/main.d.ts +77 -0
  90. package/dist/src/cli/main.js +155 -5
  91. package/dist/src/cli/main.js.map +1 -1
  92. package/dist/src/cli/mcp.d.ts +52 -0
  93. package/dist/src/cli/paths.d.ts +56 -0
  94. package/dist/src/cli/payload.d.ts +58 -0
  95. package/dist/src/cli/policy.d.ts +43 -0
  96. package/dist/src/cli/preflight.d.ts +363 -0
  97. package/dist/src/cli/preflight.js +294 -7
  98. package/dist/src/cli/preflight.js.map +1 -1
  99. package/dist/src/cli/progress.d.ts +78 -0
  100. package/dist/src/cli/prompt.d.ts +209 -0
  101. package/dist/src/cli/quickstart.d.ts +46 -0
  102. package/dist/src/cli/quickstart.js +297 -0
  103. package/dist/src/cli/quickstart.js.map +1 -0
  104. package/dist/src/cli/records.d.ts +34 -0
  105. package/dist/src/cli/render.d.ts +22 -0
  106. package/dist/src/cli/sandbox.d.ts +51 -0
  107. package/dist/src/cli/scaffold.d.ts +79 -0
  108. package/dist/src/cli/setup-adapter.d.ts +137 -0
  109. package/dist/src/cli/setup-adapter.js +38 -4
  110. package/dist/src/cli/setup-adapter.js.map +1 -1
  111. package/dist/src/cli/setup-channel.d.ts +117 -0
  112. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  113. package/dist/src/cli/setup-common.d.ts +275 -0
  114. package/dist/src/cli/setup-flow.d.ts +287 -0
  115. package/dist/src/cli/setup-service.d.ts +96 -0
  116. package/dist/src/cli/setup.d.ts +202 -0
  117. package/dist/src/cli/style.d.ts +320 -0
  118. package/dist/src/cli/token.d.ts +39 -0
  119. package/dist/src/cli/up.d.ts +155 -0
  120. package/dist/src/cli/up.js +4 -2
  121. package/dist/src/cli/up.js.map +1 -1
  122. package/dist/src/cli/usage.d.ts +37 -0
  123. package/dist/src/cli/values.d.ts +40 -0
  124. package/dist/src/cli/vault.d.ts +59 -0
  125. package/dist/src/cli/vault.js +2 -2
  126. package/dist/src/cli/vault.js.map +1 -1
  127. package/dist/src/cli/verb-registry.d.ts +76 -0
  128. package/dist/src/cli/verb-registry.js +176 -8
  129. package/dist/src/cli/verb-registry.js.map +1 -1
  130. package/dist/src/cli/wordmark.d.ts +31 -0
  131. package/dist/src/cli/wordmark.js +2 -2
  132. package/dist/src/codex/doctor.d.ts +13 -0
  133. package/dist/src/codex/doctor.js +41 -0
  134. package/dist/src/codex/doctor.js.map +1 -0
  135. package/dist/src/codex/manifest.d.ts +49 -0
  136. package/dist/src/codex/manifest.js +103 -0
  137. package/dist/src/codex/manifest.js.map +1 -0
  138. package/dist/src/codex/templates.d.ts +41 -0
  139. package/dist/src/codex/templates.js +319 -0
  140. package/dist/src/codex/templates.js.map +1 -0
  141. package/dist/src/codex/trust.d.ts +19 -0
  142. package/dist/src/codex/trust.js +183 -0
  143. package/dist/src/codex/trust.js.map +1 -0
  144. package/dist/src/codex/workspace-plan.d.ts +131 -0
  145. package/dist/src/codex/workspace-plan.js +561 -0
  146. package/dist/src/codex/workspace-plan.js.map +1 -0
  147. package/dist/src/core/actor.d.ts +2 -0
  148. package/dist/src/core/actor.js +5 -0
  149. package/dist/src/core/actor.js.map +1 -0
  150. package/dist/src/core/advance-cycle.d.ts +170 -0
  151. package/dist/src/core/agents-md.d.ts +276 -0
  152. package/dist/src/core/apply-patch.d.ts +49 -0
  153. package/dist/src/core/apply-patch.js +266 -0
  154. package/dist/src/core/apply-patch.js.map +1 -0
  155. package/dist/src/core/attest.d.ts +420 -0
  156. package/dist/src/core/attest.js +13 -1
  157. package/dist/src/core/attest.js.map +1 -1
  158. package/dist/src/core/audit.d.ts +492 -0
  159. package/dist/src/core/budgets.d.ts +238 -0
  160. package/dist/src/core/checkpoint.d.ts +500 -0
  161. package/dist/src/core/child-env.d.ts +88 -0
  162. package/dist/src/core/clock.d.ts +52 -0
  163. package/dist/src/core/command-class.d.ts +543 -0
  164. package/dist/src/core/command-class.js +43 -8
  165. package/dist/src/core/command-class.js.map +1 -1
  166. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  167. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  168. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  169. package/dist/src/core/coverage.d.ts +217 -0
  170. package/dist/src/core/credential-spec.d.ts +72 -0
  171. package/dist/src/core/dark-session.d.ts +331 -0
  172. package/dist/src/core/decision-refusal.d.ts +185 -0
  173. package/dist/src/core/env-file.d.ts +450 -0
  174. package/dist/src/core/execute.d.ts +858 -0
  175. package/dist/src/core/execute.js +44 -6
  176. package/dist/src/core/execute.js.map +1 -1
  177. package/dist/src/core/frontmatter.d.ts +78 -0
  178. package/dist/src/core/gate-window.d.ts +312 -0
  179. package/dist/src/core/gate.d.ts +1364 -0
  180. package/dist/src/core/gate.js +68 -13
  181. package/dist/src/core/gate.js.map +1 -1
  182. package/dist/src/core/git-run.d.ts +73 -0
  183. package/dist/src/core/harness-version.d.ts +157 -0
  184. package/dist/src/core/harness-version.js +2 -1
  185. package/dist/src/core/harness-version.js.map +1 -1
  186. package/dist/src/core/harness-wait.d.ts +55 -0
  187. package/dist/src/core/head-retry.d.ts +107 -0
  188. package/dist/src/core/instance.d.ts +253 -0
  189. package/dist/src/core/intake-limits.d.ts +247 -0
  190. package/dist/src/core/jcs.d.ts +52 -0
  191. package/dist/src/core/journal.d.ts +144 -0
  192. package/dist/src/core/live-draw.d.ts +436 -0
  193. package/dist/src/core/log-reconcile.d.ts +89 -0
  194. package/dist/src/core/log-subscribe.d.ts +36 -0
  195. package/dist/src/core/log-subscribe.js +162 -0
  196. package/dist/src/core/log-subscribe.js.map +1 -0
  197. package/dist/src/core/log.d.ts +278 -0
  198. package/dist/src/core/loop.d.ts +274 -0
  199. package/dist/src/core/loop.js +11 -0
  200. package/dist/src/core/loop.js.map +1 -1
  201. package/dist/src/core/md-fence.d.ts +41 -0
  202. package/dist/src/core/money.d.ts +147 -0
  203. package/dist/src/core/payload-census.d.ts +74 -0
  204. package/dist/src/core/payload-store.d.ts +175 -0
  205. package/dist/src/core/payload.d.ts +71 -0
  206. package/dist/src/core/policy-diff.d.ts +292 -0
  207. package/dist/src/core/policy-diff.js +27 -4
  208. package/dist/src/core/policy-diff.js.map +1 -1
  209. package/dist/src/core/policy-expectations.d.ts +199 -0
  210. package/dist/src/core/policy-explain.d.ts +150 -0
  211. package/dist/src/core/policy-explain.js +31 -3
  212. package/dist/src/core/policy-explain.js.map +1 -1
  213. package/dist/src/core/policy-load.d.ts +527 -0
  214. package/dist/src/core/policy-load.js +15 -3
  215. package/dist/src/core/policy-load.js.map +1 -1
  216. package/dist/src/core/policy-match.d.ts +281 -0
  217. package/dist/src/core/policy-match.js +20 -9
  218. package/dist/src/core/policy-match.js.map +1 -1
  219. package/dist/src/core/policy-proposal.d.ts +265 -0
  220. package/dist/src/core/prompt-layout.d.ts +221 -0
  221. package/dist/src/core/protected-path-guard.d.ts +453 -0
  222. package/dist/src/core/protected-path-guard.js +514 -35
  223. package/dist/src/core/protected-path-guard.js.map +1 -1
  224. package/dist/src/core/registration.d.ts +25 -0
  225. package/dist/src/core/reindex.d.ts +99 -0
  226. package/dist/src/core/sampler.d.ts +313 -0
  227. package/dist/src/core/sandbox.d.ts +290 -0
  228. package/dist/src/core/seal.d.ts +165 -0
  229. package/dist/src/core/state.d.ts +505 -0
  230. package/dist/src/core/task-file.d.ts +185 -0
  231. package/dist/src/core/telegram-config.d.ts +93 -0
  232. package/dist/src/core/token.d.ts +409 -0
  233. package/dist/src/core/token.js +21 -38
  234. package/dist/src/core/token.js.map +1 -1
  235. package/dist/src/core/validate.d.ts +138 -0
  236. package/dist/src/core/values.d.ts +137 -0
  237. package/dist/src/core/vault.d.ts +291 -0
  238. package/dist/src/core/verified-snapshot.d.ts +204 -0
  239. package/dist/src/core/verify.d.ts +336 -0
  240. package/dist/src/core/version.d.ts +8 -0
  241. package/dist/src/core/wysiwys.d.ts +370 -0
  242. package/dist/src/daemon/advance-child.d.ts +39 -0
  243. package/dist/src/daemon/advance.d.ts +466 -0
  244. package/dist/src/daemon/audit.d.ts +87 -0
  245. package/dist/src/daemon/daemon.d.ts +1180 -0
  246. package/dist/src/daemon/dark-session.d.ts +64 -0
  247. package/dist/src/daemon/draw-child.d.ts +36 -0
  248. package/dist/src/daemon/draw.d.ts +154 -0
  249. package/dist/src/daemon/git-evidence.d.ts +173 -0
  250. package/dist/src/daemon/git-evidence.js +1 -1
  251. package/dist/src/daemon/projection.d.ts +180 -0
  252. package/dist/src/daemon/prune.d.ts +207 -0
  253. package/dist/src/mcp/http.d.ts +113 -0
  254. package/dist/src/mcp/server.d.ts +265 -0
  255. package/dist/src/mcp/server.js +9 -1
  256. package/dist/src/mcp/server.js.map +1 -1
  257. package/docs/adapter-api.md +106 -0
  258. package/docs/cli-reference.md +389 -36
  259. package/docs/codex-enforced-session.md +30 -0
  260. package/package.json +12 -2
  261. package/schema/codex-instance.schema.json +82 -0
  262. package/schema/event.schema.json +2 -1
  263. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  264. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  265. package/schema/policy.schema.json +21 -1
  266. 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-token window the contract opens. The agent can therefore
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-token window and `approval setup adapter agentmail` calls it to probe
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>;