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,290 @@
1
+ /**
2
+ * The egress sandbox (APRV-193): the room an allowed command runs in.
3
+ *
4
+ * ## What this is for
5
+ *
6
+ * Arbitrary code execution is capability-complete. The gate classifies a
7
+ * command by its TEXT, and `npm test` runs whatever the agent wrote a minute
8
+ * ago, so for laundered exec the text has stopped describing the effect. No
9
+ * classifier over shell strings can close that, and `core/child-env.ts`
10
+ * (APRV-205) closed only half of it: the child no longer inherits the session's
11
+ * credentials, and it still inherits the session's network.
12
+ *
13
+ * This module removes the network. It does not try to predict what the code
14
+ * will do; it takes away what the code would need. Where APRV-205 starves the
15
+ * child of secrets, this starves it of a place to send them.
16
+ *
17
+ * ## The shape, and why it is this shape
18
+ *
19
+ * A NETWORK-ONLY sandbox, with one filesystem exception. The filesystem is left
20
+ * alone because the gate's own IPC is a FILE: `src/daemon/` opens no socket, it
21
+ * polls `.approval/log/events.jsonl`. So denying egress costs the gate nothing
22
+ * and needs no plumbing to stay reachable, and the "one door" other agent
23
+ * sandboxes build an egress proxy for already exists as an append to a file.
24
+ * That is the single largest simplification available here, and it is why the
25
+ * default profile denies the network, denies reads of the credential material
26
+ * beside the log, and touches nothing else.
27
+ *
28
+ * Loopback is denied WITH the rest. There is no gate socket to except: the
29
+ * daemon is a file reader. The one thing loopback costs is a test suite that
30
+ * starts its own localhost server, which is what {@link EgressAllowance.loopback}
31
+ * is for, and that allowance is an operator's decision recorded where it is
32
+ * used (`docs/sandboxed-exec.md` has the survey).
33
+ *
34
+ * ## The mechanism, and what is verified rather than assumed
35
+ *
36
+ * macOS only in this build: `/usr/bin/sandbox-exec` with a Sandbox Profile
37
+ * Language (SBPL) profile. It ships with every macOS, needs no privilege and no
38
+ * daemon, and every macOS agent sandbox in the field uses it. Linux
39
+ * (`bwrap --unshare-net`, `unshare --net`) is a stated follow-up rather than a
40
+ * silent omission: {@link detectSandbox} reports `supported: false` there and
41
+ * says so, and `docs/sandboxed-exec.md` carries the gap.
42
+ *
43
+ * Three Seatbelt behaviours are measured, not assumed. Each of them, got wrong,
44
+ * ships a profile that silently protects nothing, which is the worst artifact
45
+ * this design could produce:
46
+ *
47
+ * 1. `network-outbound` covers AF_UNIX connects as well as AF_INET. A bare
48
+ * `(deny network-outbound)` kills local IPC — DNS's mDNSResponder socket
49
+ * included — and the process dies before it can do anything. The unix
50
+ * exception is mandatory.
51
+ * 2. `subpath` and `literal` filters match the kernel's RESOLVED path. On macOS
52
+ * `/tmp` is a symlink to `/private/tmp`, so a profile naming `/tmp/x` denies
53
+ * NOTHING. Every path is realpath-resolved before it is written into a
54
+ * profile, and `tests/sandbox.test.ts` pins the unresolved spelling as the
55
+ * no-op it is.
56
+ * 3. `sandbox-exec` execs through `execvp`, so a PATH lookup still happens
57
+ * inside the wrapper — but a lookup that FAILS exits 71 (EX_OSERR), which
58
+ * would be recorded as the child's own exit code. So {@link resolveExecutable}
59
+ * does the lookup first and a command that cannot be found is never wrapped:
60
+ * it reaches `spawnSync` unwrapped and fails as ENOENT exactly as it does
61
+ * today.
62
+ *
63
+ * ## What it does not claim
64
+ *
65
+ * A sandboxed child can still WRITE files, including files a later unsandboxed
66
+ * process will run. That is the same laundering one step removed, and it is why
67
+ * `APPROVAL_HOOK_REQUIRE_SANDBOX` (which makes the hook insist that every
68
+ * code-running command wear the room) matters more than any single wrapper. It
69
+ * would be closed properly by running the whole SESSION under the profile,
70
+ * which this build cannot do: an agent harness needs the model API, and denying
71
+ * egress denies exactly that. A session-wide sandbox needs an allowlist
72
+ * reaching one host, which Seatbelt cannot express by hostname and which the
73
+ * prior art solves with a local proxy.
74
+ *
75
+ * Inbound sockets are not denied, so a child could in principle bind a port and
76
+ * wait to be contacted; that needs a peer that can reach this machine and is
77
+ * recorded here as a stated limit rather than an oversight. And the sandbox is
78
+ * not isolation: it is one capability removed from a process that otherwise has
79
+ * the ordinary powers of the session.
80
+ */
81
+ /**
82
+ * The mechanisms this build knows. One, today.
83
+ *
84
+ * A union rather than a string so the Linux follow-up (APRV-193g) is an
85
+ * addition the compiler finds every site of, rather than a value that quietly
86
+ * flows through code written for Seatbelt.
87
+ */
88
+ export declare const SANDBOX_MECHANISMS: readonly ["sandbox-exec"];
89
+ export type SandboxMechanism = (typeof SANDBOX_MECHANISMS)[number];
90
+ /**
91
+ * What an execution's record says about the room it ran in.
92
+ *
93
+ * Written to `execution.started` as `sandbox`, and it is a fact about the
94
+ * runtime's own behaviour rather than anything a caller asserted: three of the
95
+ * four values are computed here, and the fourth (`opted-out`) is a flag the
96
+ * operator typed, which is exactly why it is RECORDED — an opt-out nobody can
97
+ * see afterwards is an opt-out that costs nothing to take.
98
+ */
99
+ export declare const SANDBOX_STATES: readonly [
100
+ /** The child ran with outbound network denied. */
101
+ "egress-denied",
102
+ /** `--no-sandbox` was passed. The child ran with the session's own network. */
103
+ "opted-out",
104
+ /**
105
+ * The manual path: a human granted these exact bytes and the grant is the
106
+ * authority to reach the world. `approval run` on a token is the one door,
107
+ * and a door that denied egress would not be a door.
108
+ */
109
+ "granted-egress",
110
+ /**
111
+ * This platform has no mechanism in this build (anything but macOS today).
112
+ * Recorded on every execution so an auditor can see exactly which runs were
113
+ * unprotected, rather than inferring it from the absence of a field.
114
+ */
115
+ "unsupported"];
116
+ export type SandboxState = (typeof SANDBOX_STATES)[number];
117
+ /** What the machine can do, probed rather than inferred. */
118
+ export interface SandboxDetection {
119
+ /** Can a command be run egress-denied here, right now? */
120
+ readonly available: boolean;
121
+ /** The mechanism that would be used, or `null`. */
122
+ readonly mechanism: SandboxMechanism | null;
123
+ /**
124
+ * Does this build have a mechanism for this platform AT ALL?
125
+ *
126
+ * The distinction from {@link available} is load-bearing and it is the one
127
+ * place this module is deliberately not maximally strict. `supported: false`
128
+ * means "nothing was ever written for this platform", which is a known gap
129
+ * with a follow-up task and a docs row. `supported: true, available: false`
130
+ * means "the mechanism that should be here did not work", which is a broken
131
+ * promise, and the callers treat the two differently: the first proceeds and
132
+ * records `unsupported`, the second refuses to run the command at all.
133
+ */
134
+ readonly supported: boolean;
135
+ /** Can loopback be carved back in? (Seatbelt yes; a Linux netns cannot.) */
136
+ readonly loopback: boolean;
137
+ /** Why, when {@link available} is false. Empty otherwise. */
138
+ readonly reason: string;
139
+ }
140
+ /** What the profile lets through. Everything absent here is denied. */
141
+ export interface EgressAllowance {
142
+ /**
143
+ * Allow connections to `localhost:*`.
144
+ *
145
+ * Off by default. The gate needs nothing: its IPC is a file. This exists for
146
+ * the one legitimate case the survey found, a test suite that starts its own
147
+ * server, and it is a real widening — a loopback port is a port, and anything
148
+ * listening on one is reachable from inside.
149
+ */
150
+ readonly loopback: boolean;
151
+ /**
152
+ * Absolute paths whose CONTENTS the child may not read. Resolved before they
153
+ * reach the profile. Directories deny their whole subtree.
154
+ */
155
+ readonly denyRead: readonly string[];
156
+ }
157
+ /** The default: nothing allowed, nothing denied beyond the network. */
158
+ export declare const DENY_ALL_EGRESS: EgressAllowance;
159
+ /**
160
+ * The files beside a log that hold credential material, for the profile's
161
+ * `denyRead`.
162
+ *
163
+ * Named individually rather than by denying the whole approval home. The home
164
+ * also holds the log, the payload store and the queue, and a child that could
165
+ * not read the log could not read the decision that authorized it. What is
166
+ * denied is what a laundering script would want: the vault's ciphertext, the
167
+ * environment source map that says where the secrets come from, and the
168
+ * per-request private keys that seal tokens.
169
+ *
170
+ * This is defence in depth over material that is already encrypted or already
171
+ * useless without the passphrase — the load-bearing custody control is that
172
+ * the passphrase is not in the child's environment at all (APRV-205). It is
173
+ * worth having on the one platform where it costs nothing.
174
+ */
175
+ export declare function credentialPathsFor(logPath: string): string[];
176
+ /**
177
+ * Forces the unavailable branch. A STRICTNESS increase in every caller — the
178
+ * run path refuses and the session launcher refuses — so a forged value cannot
179
+ * widen anything, which is what keeps it clear of SPEC.md §11.1 invariant 4.
180
+ */
181
+ export declare const FORCE_UNAVAILABLE_ENV = "APPROVAL_SANDBOX_FORCE_UNAVAILABLE";
182
+ /**
183
+ * Treat an unsupported platform as a broken promise rather than a known gap:
184
+ * with this set, a machine with no mechanism refuses instead of proceeding.
185
+ * The same one-way property holds — it can only ever refuse more.
186
+ */
187
+ export declare const REQUIRE_SANDBOX_ENV = "APPROVAL_SANDBOX_REQUIRED";
188
+ export interface DetectOptions {
189
+ readonly platform?: NodeJS.Platform;
190
+ readonly env?: NodeJS.ProcessEnv;
191
+ }
192
+ /**
193
+ * What this machine can do. Cached for the default call, because every verb
194
+ * that asks would otherwise spawn a probe per invocation, and the answer cannot
195
+ * change inside one process.
196
+ */
197
+ export declare function detectSandbox(options?: DetectOptions): SandboxDetection;
198
+ /** Testing seam: forget the cached probe. Never called by the runtime. */
199
+ export declare function resetSandboxDetection(): void;
200
+ /**
201
+ * A path as the KERNEL will see it.
202
+ *
203
+ * A path that does not exist yet cannot be resolved, so its directory is
204
+ * resolved and the name re-joined. A profile naming an unresolved path denies
205
+ * nothing at all and reports no error, which is the failure this function
206
+ * exists to prevent.
207
+ */
208
+ export declare function resolveForProfile(path: string): string;
209
+ /**
210
+ * The SBPL profile.
211
+ *
212
+ * `(allow default)` first, then the denial: SBPL takes the LAST matching rule,
213
+ * so exceptions are written after the deny. This is deliberately a deny-LIST
214
+ * and not the `(deny default)` posture a true isolation sandbox takes. The
215
+ * property being enforced is egress, and a deny-default profile spends itself
216
+ * re-allowing dyld, `/dev/urandom`, the process's own binary and every
217
+ * temporary directory, which is a different task with a different failure mode
218
+ * (and a much larger chance of breaking ordinary development, which is how a
219
+ * control gets switched off).
220
+ */
221
+ export declare function seatbeltProfile(allowance: EgressAllowance): string;
222
+ /** The argv to spawn, and the temporary directory the caller must remove. */
223
+ export interface WrappedSpawn {
224
+ readonly command: string;
225
+ readonly args: string[];
226
+ /** A directory holding the profile file; remove it once the child is gone. */
227
+ readonly cleanup: string;
228
+ /** What the caller records. */
229
+ readonly mechanism: SandboxMechanism;
230
+ }
231
+ /**
232
+ * Resolve a command the way `execvp` would, and say when it cannot be found.
233
+ *
234
+ * Done HERE rather than left to the wrapper for the reason in the header: an
235
+ * execvp failure inside `sandbox-exec` exits 71, and 71 recorded as the child's
236
+ * exit code is a lie about a command that never ran. A command that does not
237
+ * resolve is returned as `null`, and the caller spawns it unwrapped so the
238
+ * ENOENT surfaces exactly as it does with no sandbox in the picture.
239
+ */
240
+ export declare function resolveExecutable(command: string, env?: NodeJS.ProcessEnv): string | null;
241
+ /**
242
+ * Build the wrapped spawn for `argv`.
243
+ *
244
+ * Writes the profile to a fresh 0600 file under a private directory: the
245
+ * profile names the paths whose reads are denied, which is not secret, but a
246
+ * world-writable profile would be a profile another process could rewrite
247
+ * between this write and the exec.
248
+ */
249
+ export declare function wrapForSandbox(mechanism: SandboxMechanism, command: string, args: readonly string[], allowance?: EgressAllowance): WrappedSpawn;
250
+ export interface PostureInput {
251
+ /** `--no-sandbox` was passed. */
252
+ readonly optedOut: boolean;
253
+ /**
254
+ * A human's grant over these exact bytes is in the caller's hand (the manual
255
+ * path presented a token). The grant IS the authority to reach the world.
256
+ *
257
+ * Not a self-report that widens anything: a token that does not verify runs
258
+ * no command at all, so the loosening is reachable only by holding something
259
+ * a human minted. SPEC.md §11.1 invariant 4 is about a declaration the
260
+ * executing party authors; this is a secret it cannot author.
261
+ */
262
+ readonly granted: boolean;
263
+ /** The machine's capability. */
264
+ readonly detection: SandboxDetection;
265
+ /** `APPROVAL_SANDBOX_REQUIRED=1` promotes an unsupported platform to a refusal. */
266
+ readonly requireSupported?: boolean;
267
+ }
268
+ export type SandboxPosture = {
269
+ readonly kind: "apply";
270
+ readonly state: "egress-denied";
271
+ readonly mechanism: SandboxMechanism;
272
+ } | {
273
+ readonly kind: "skip";
274
+ readonly state: Exclude<SandboxState, "egress-denied">;
275
+ } | {
276
+ readonly kind: "refuse";
277
+ readonly reason: string;
278
+ };
279
+ /**
280
+ * What to do about the sandbox for one execution.
281
+ *
282
+ * Total, pure, and exhaustively testable: it reads a detection and two booleans
283
+ * and returns one of three answers. The ORDER of the branches is the policy —
284
+ * a broken mechanism refuses before an opt-out is considered, so `--no-sandbox`
285
+ * on a machine whose sandbox is broken is still a refusal rather than a way to
286
+ * turn the noise off.
287
+ */
288
+ export declare function sandboxPosture(input: PostureInput): SandboxPosture;
289
+ /** Is `APPROVAL_SANDBOX_REQUIRED` set on this environment? */
290
+ export declare function sandboxRequired(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Sealed token delivery (amended SPEC.md §6.3, §10.4, §11.1 — APRV-105).
3
+ *
4
+ * The problem this closes is a handover, not an authorization. A token is minted
5
+ * at exactly one site — the grant path of `core/gate.ts` — and spent at another,
6
+ * `approval run`. The two are different processes, and until now the only
7
+ * transport between them was a human with a clipboard: `approval grant` (or the
8
+ * Telegram listener) printed the raw token on ITS stdout, and `approval wait`,
9
+ * which reads only the verified log, had nothing to return because the log
10
+ * carries hashes. On one machine that is a paste. Across two — the listener on a
11
+ * laptop, the agent elsewhere, the log synced through git — it is a paste that
12
+ * cannot happen.
13
+ *
14
+ * So the requester publishes an ADDRESS. `approval request` mints an ephemeral
15
+ * X25519 keypair, keeps the private half in a 0600 file beside the log, and puts
16
+ * the public half on `approval.requested` as `token_recipient_key`. On grant the
17
+ * mint site seals the raw token to that key and records the ciphertext beside
18
+ * the digest as `token_sealed`. `approval wait` opens it with the local private
19
+ * key and hands the token back to the process that asked for it.
20
+ *
21
+ * ## The keypair ADDRESSES; it does not AUTHORIZE
22
+ *
23
+ * The obvious objection is that the agent creates both halves of the key, so how
24
+ * does this control the agent? It does not, and it does not need to. The four
25
+ * properties that make an action human-gated are untouched:
26
+ *
27
+ * - a token exists only after a human grant (one mint site);
28
+ * - it binds to the exact payload bytes;
29
+ * - it is single-use, and consumption is an `execution.started` in the log;
30
+ * - credentials answer only to tokens.
31
+ *
32
+ * A keypair cannot mint a token, forge a grant, alter the bound payload, or
33
+ * reuse a spent one. What changes is who can READ a minted token: the requesting
34
+ * process instead of the human's clipboard, which is where the human was
35
+ * carrying it anyway. A malicious agent that substitutes a key substitutes it
36
+ * only in its OWN request, which the approver sees and approves as that agent's
37
+ * request; identity spoofing is exactly as it was in SPEC.md §11, where the
38
+ * trust boundary is the machine.
39
+ *
40
+ * What is lost, stated plainly: the marginal ability to approve on the phone and
41
+ * then withhold delivery.
42
+ *
43
+ * ## Residual risk, stated plainly
44
+ *
45
+ * Someone who steals the private-key file AND reads the log can recover an
46
+ * unspent token inside its TTL. That window equals the terminal-paste window
47
+ * today, and it closes at consume or at TTL, whichever comes first — this module
48
+ * unlinks the key file at both. The ciphertext lives in a public, permanent log,
49
+ * so the seal must be sound and the token short-lived; it already is.
50
+ *
51
+ * ## The construction
52
+ *
53
+ * X25519 ECDH to a fresh ephemeral sender key, HKDF-SHA-256 to a 32-byte AEAD
54
+ * key, AES-256-GCM with a fresh 12-byte nonce. All from `node:crypto`; no
55
+ * dependency is added. The sender's ephemeral public key travels with the
56
+ * ciphertext, so the recipient needs nothing but its own private key and the
57
+ * record. The HKDF `info` binds the ciphertext to this scheme and to the action
58
+ * it was minted for, so a seal lifted from one grant cannot be replayed as
59
+ * another's even by someone holding both key files.
60
+ */
61
+ /** The payload key carrying the requester's public key on `approval.requested`. */
62
+ export declare const RECIPIENT_KEY_FIELD = "token_recipient_key";
63
+ /** The payload key carrying the sealed token on `approval.granted`. */
64
+ export declare const SEALED_TOKEN_FIELD = "token_sealed";
65
+ /**
66
+ * The payload key on `approval.requested` saying the requester will consume its
67
+ * own grant in its own process (APRV-211), so the raw token is delivered ONLY
68
+ * through the seal and is never returned to the granting surface.
69
+ *
70
+ * A fact about delivery, recorded where every other delivery fact is recorded.
71
+ * It authorizes nothing and relaxes nothing: the token is still minted only by
72
+ * a human's grant, still bound to the payload bytes, still single-use. What it
73
+ * removes is a reader — a terminal that was being handed a live credential it
74
+ * had no use for.
75
+ */
76
+ export declare const SELF_DELIVERY_FIELD = "token_delivery_self";
77
+ /** `.approval/keys/`, the sibling of the log directory the private keys live in. */
78
+ export declare const KEY_STORE_DIRNAME = "keys";
79
+ /** The scheme identifier recorded inside every seal. One value in v0.1. */
80
+ export declare const SEAL_ALG = "x25519-hkdf-sha256/aes-256-gcm";
81
+ /** The key store for a log path, by the same rule the payload store uses. */
82
+ export declare function keyStoreDirFor(logPath: string): string;
83
+ /**
84
+ * The file one action key's private key lives in.
85
+ *
86
+ * The action key is used verbatim wherever it is already a safe file name, and
87
+ * percent-encoded elsewhere. Percent-encoding is injective, so two distinct
88
+ * action keys can never name one file — which matters more than readability,
89
+ * since a collision would hand one action's token to another. The common case
90
+ * (`task-042:chaser:2026-08-04`) survives unchanged, so the directory listing
91
+ * still reads as the action keys it holds.
92
+ */
93
+ export declare function keyPath(keyDir: string, actionKey: string): string;
94
+ /** A freshly minted recipient keypair: the public half travels, the private stays. */
95
+ export interface RecipientKeypair {
96
+ /** Base64 DER SPKI. What rides on `approval.requested`. */
97
+ publicKey: string;
98
+ /** Base64 DER PKCS#8. What is written 0600 and never leaves the machine. */
99
+ privateKey: string;
100
+ }
101
+ /** Mint an ephemeral X25519 keypair for one request. */
102
+ export declare function mintRecipientKeypair(): RecipientKeypair;
103
+ /** Is this a value that could be a recipient public key? Shape only. */
104
+ export declare function isRecipientKey(value: unknown): value is string;
105
+ export type KeyWriteResult = {
106
+ ok: true;
107
+ path: string;
108
+ } | {
109
+ ok: false;
110
+ message: string;
111
+ };
112
+ /**
113
+ * Write one private key, atomically and 0600.
114
+ *
115
+ * The mode is set by `openSync`'s mode argument on a file created `wx`, so the
116
+ * key never exists at a wider mode even for an instant — a `chmod` after the
117
+ * fact would leave a window in which any process on the machine could read it.
118
+ * The rename is atomic, so a reader either sees the whole key or no file.
119
+ */
120
+ export declare function writePrivateKey(keyDir: string, actionKey: string, privateKey: string): KeyWriteResult;
121
+ /** The private key for an action, or `null` when this machine holds none. */
122
+ export declare function readPrivateKey(keyDir: string, actionKey: string): string | null;
123
+ /**
124
+ * Remove one action's private key. Idempotent and never throws.
125
+ *
126
+ * Called at every death of the authorization it addresses: consumption, expiry,
127
+ * revocation. A key that outlived its grant would be a decryption capability for
128
+ * a ciphertext that is in the log forever, kept for no reason at all.
129
+ */
130
+ export declare function forgetPrivateKey(keyDir: string, actionKey: string): boolean;
131
+ /** What `approval.granted` carries beside `token_sha256`. */
132
+ export interface SealedToken {
133
+ /** The scheme. One value in v0.1; present so a reader never has to guess. */
134
+ alg: string;
135
+ /** The sender's ephemeral X25519 public key, base64 DER SPKI. */
136
+ epk: string;
137
+ /** The AES-GCM nonce, base64. Fresh for every seal. */
138
+ nonce: string;
139
+ /** Ciphertext, base64. */
140
+ ct: string;
141
+ /** The GCM authentication tag, base64. */
142
+ tag: string;
143
+ }
144
+ /**
145
+ * Seal a raw token to a recipient public key.
146
+ *
147
+ * Returns `null` for a recipient key that cannot be parsed. A grant is not
148
+ * refused over an unusable recipient key: the authorization is the human's
149
+ * decision and stands, the digest still binds it, and the raw token is still
150
+ * printed once on the granting surface. What fails is the convenience, and a
151
+ * convenience must never be able to void a human's yes.
152
+ */
153
+ export declare function sealToken(token: string, recipientPublicKey: string, actionKey: string): SealedToken | null;
154
+ /** Read a `token_sealed` payload value as a {@link SealedToken}, or `null`. */
155
+ export declare function asSealedToken(value: unknown): SealedToken | null;
156
+ /**
157
+ * Open a sealed token with a recipient private key.
158
+ *
159
+ * Returns `null` for anything that does not authenticate: a wrong key, a
160
+ * tampered ciphertext, a seal minted for a different action key. There is one
161
+ * failure, deliberately: distinguishing "wrong key" from "wrong ciphertext"
162
+ * would be an oracle, and the caller's response to either is identical — fall
163
+ * back to the raw token the granting surface printed.
164
+ */
165
+ export declare function openSealedToken(sealed: SealedToken, recipientPrivateKey: string, actionKey: string): string | null;