approval-md 0.3.0 → 0.4.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 (226) hide show
  1. package/README.md +83 -914
  2. package/SPEC.md +98 -13
  3. package/cli.js +66 -7
  4. package/dist/src/adapters/smtp.d.ts +18 -0
  5. package/dist/src/adapters/smtp.js +24 -3
  6. package/dist/src/adapters/smtp.js.map +1 -1
  7. package/dist/src/channels/contract.d.ts +19 -0
  8. package/dist/src/channels/contract.js +105 -1
  9. package/dist/src/channels/contract.js.map +1 -1
  10. package/dist/src/channels/relay-server.d.ts +90 -0
  11. package/dist/src/channels/relay-server.js +295 -0
  12. package/dist/src/channels/relay-server.js.map +1 -0
  13. package/dist/src/channels/relay.d.ts +273 -0
  14. package/dist/src/channels/relay.js +564 -0
  15. package/dist/src/channels/relay.js.map +1 -0
  16. package/dist/src/channels/tagging.js +6 -1
  17. package/dist/src/channels/tagging.js.map +1 -1
  18. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  19. package/dist/src/channels/telegram-webhook.js +536 -0
  20. package/dist/src/channels/telegram-webhook.js.map +1 -0
  21. package/dist/src/channels/telegram.d.ts +271 -1
  22. package/dist/src/channels/telegram.js +403 -45
  23. package/dist/src/channels/telegram.js.map +1 -1
  24. package/dist/src/cli/amend.js +350 -4
  25. package/dist/src/cli/amend.js.map +1 -1
  26. package/dist/src/cli/attest.js +43 -6
  27. package/dist/src/cli/attest.js.map +1 -1
  28. package/dist/src/cli/channel-relay.d.ts +108 -0
  29. package/dist/src/cli/channel-relay.js +334 -0
  30. package/dist/src/cli/channel-relay.js.map +1 -0
  31. package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
  32. package/dist/src/cli/channel-telegram-webhook.js +709 -0
  33. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  34. package/dist/src/cli/channel-telegram.d.ts +272 -26
  35. package/dist/src/cli/channel-telegram.js +478 -41
  36. package/dist/src/cli/channel-telegram.js.map +1 -1
  37. package/dist/src/cli/channel.d.ts +1 -1
  38. package/dist/src/cli/channel.js +5 -1
  39. package/dist/src/cli/channel.js.map +1 -1
  40. package/dist/src/cli/daemon.js +37 -5
  41. package/dist/src/cli/daemon.js.map +1 -1
  42. package/dist/src/cli/doctor.d.ts +63 -0
  43. package/dist/src/cli/doctor.js +856 -111
  44. package/dist/src/cli/doctor.js.map +1 -1
  45. package/dist/src/cli/execute.d.ts +17 -1
  46. package/dist/src/cli/execute.js +341 -20
  47. package/dist/src/cli/execute.js.map +1 -1
  48. package/dist/src/cli/exit-codes.d.ts +12 -0
  49. package/dist/src/cli/exit-codes.js +13 -0
  50. package/dist/src/cli/exit-codes.js.map +1 -1
  51. package/dist/src/cli/gate.d.ts +2 -0
  52. package/dist/src/cli/gate.js +233 -2
  53. package/dist/src/cli/gate.js.map +1 -1
  54. package/dist/src/cli/help.d.ts +9 -1
  55. package/dist/src/cli/help.js +246 -38
  56. package/dist/src/cli/help.js.map +1 -1
  57. package/dist/src/cli/hook.d.ts +371 -3
  58. package/dist/src/cli/hook.js +1926 -44
  59. package/dist/src/cli/hook.js.map +1 -1
  60. package/dist/src/cli/init.js +16 -7
  61. package/dist/src/cli/init.js.map +1 -1
  62. package/dist/src/cli/instructions.js +24 -3
  63. package/dist/src/cli/instructions.js.map +1 -1
  64. package/dist/src/cli/log-anchor.d.ts +55 -0
  65. package/dist/src/cli/log-anchor.js +199 -60
  66. package/dist/src/cli/log-anchor.js.map +1 -1
  67. package/dist/src/cli/main.d.ts +0 -12
  68. package/dist/src/cli/main.js +51 -0
  69. package/dist/src/cli/main.js.map +1 -1
  70. package/dist/src/cli/muse.d.ts +2 -0
  71. package/dist/src/cli/muse.js +62 -0
  72. package/dist/src/cli/muse.js.map +1 -0
  73. package/dist/src/cli/payload.js +79 -1
  74. package/dist/src/cli/payload.js.map +1 -1
  75. package/dist/src/cli/scaffold.js +6 -0
  76. package/dist/src/cli/scaffold.js.map +1 -1
  77. package/dist/src/cli/serve.d.ts +53 -0
  78. package/dist/src/cli/serve.js +270 -0
  79. package/dist/src/cli/serve.js.map +1 -0
  80. package/dist/src/cli/up.js +21 -2
  81. package/dist/src/cli/up.js.map +1 -1
  82. package/dist/src/cli/verb-registry.d.ts +2 -0
  83. package/dist/src/cli/verb-registry.js +337 -39
  84. package/dist/src/cli/verb-registry.js.map +1 -1
  85. package/dist/src/cli/wordmark.d.ts +2 -2
  86. package/dist/src/cli/wordmark.js +2 -2
  87. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  88. package/dist/src/core/attest-bootstrap.js +135 -0
  89. package/dist/src/core/attest-bootstrap.js.map +1 -0
  90. package/dist/src/core/attest.d.ts +20 -3
  91. package/dist/src/core/attest.js +25 -0
  92. package/dist/src/core/attest.js.map +1 -1
  93. package/dist/src/core/audit.d.ts +55 -3
  94. package/dist/src/core/audit.js +52 -5
  95. package/dist/src/core/audit.js.map +1 -1
  96. package/dist/src/core/channel-lease.d.ts +322 -0
  97. package/dist/src/core/channel-lease.js +663 -0
  98. package/dist/src/core/channel-lease.js.map +1 -0
  99. package/dist/src/core/command-class.d.ts +51 -0
  100. package/dist/src/core/command-class.js +760 -8
  101. package/dist/src/core/command-class.js.map +1 -1
  102. package/dist/src/core/daemon-host.d.ts +178 -0
  103. package/dist/src/core/daemon-host.js +228 -0
  104. package/dist/src/core/daemon-host.js.map +1 -0
  105. package/dist/src/core/daemon-identity.d.ts +238 -0
  106. package/dist/src/core/daemon-identity.js +252 -0
  107. package/dist/src/core/daemon-identity.js.map +1 -0
  108. package/dist/src/core/gate.d.ts +306 -5
  109. package/dist/src/core/gate.js +772 -24
  110. package/dist/src/core/gate.js.map +1 -1
  111. package/dist/src/core/harness-version.d.ts +128 -4
  112. package/dist/src/core/harness-version.js +179 -12
  113. package/dist/src/core/harness-version.js.map +1 -1
  114. package/dist/src/core/harness-wait.d.ts +82 -3
  115. package/dist/src/core/harness-wait.js +89 -3
  116. package/dist/src/core/harness-wait.js.map +1 -1
  117. package/dist/src/core/log-subscribe.d.ts +21 -0
  118. package/dist/src/core/log-subscribe.js +16 -6
  119. package/dist/src/core/log-subscribe.js.map +1 -1
  120. package/dist/src/core/log-write-layer.d.ts +55 -0
  121. package/dist/src/core/log-write-layer.js +91 -0
  122. package/dist/src/core/log-write-layer.js.map +1 -0
  123. package/dist/src/core/log.d.ts +50 -21
  124. package/dist/src/core/log.js +197 -19
  125. package/dist/src/core/log.js.map +1 -1
  126. package/dist/src/core/payload-census.d.ts +51 -0
  127. package/dist/src/core/payload-census.js +125 -2
  128. package/dist/src/core/payload-census.js.map +1 -1
  129. package/dist/src/core/payload-store.d.ts +31 -0
  130. package/dist/src/core/payload-store.js +113 -8
  131. package/dist/src/core/payload-store.js.map +1 -1
  132. package/dist/src/core/payload.d.ts +18 -3
  133. package/dist/src/core/payload.js +20 -4
  134. package/dist/src/core/payload.js.map +1 -1
  135. package/dist/src/core/policy-diff.d.ts +10 -0
  136. package/dist/src/core/policy-diff.js +10 -2
  137. package/dist/src/core/policy-diff.js.map +1 -1
  138. package/dist/src/core/policy-explain.d.ts +7 -1
  139. package/dist/src/core/policy-explain.js +16 -1
  140. package/dist/src/core/policy-explain.js.map +1 -1
  141. package/dist/src/core/policy-load.d.ts +24 -0
  142. package/dist/src/core/policy-load.js.map +1 -1
  143. package/dist/src/core/policy-match.d.ts +38 -0
  144. package/dist/src/core/policy-match.js +35 -0
  145. package/dist/src/core/policy-match.js.map +1 -1
  146. package/dist/src/core/policy-proposal.d.ts +23 -0
  147. package/dist/src/core/policy-proposal.js +8 -2
  148. package/dist/src/core/policy-proposal.js.map +1 -1
  149. package/dist/src/core/read-scope.d.ts +10 -0
  150. package/dist/src/core/read-scope.js +4 -0
  151. package/dist/src/core/read-scope.js.map +1 -1
  152. package/dist/src/core/run-payload.d.ts +160 -0
  153. package/dist/src/core/run-payload.js +304 -0
  154. package/dist/src/core/run-payload.js.map +1 -0
  155. package/dist/src/core/sender-identity.d.ts +35 -10
  156. package/dist/src/core/sender-identity.js +48 -11
  157. package/dist/src/core/sender-identity.js.map +1 -1
  158. package/dist/src/core/state.d.ts +52 -1
  159. package/dist/src/core/state.js +52 -6
  160. package/dist/src/core/state.js.map +1 -1
  161. package/dist/src/core/telegram-config.d.ts +79 -0
  162. package/dist/src/core/telegram-config.js +108 -0
  163. package/dist/src/core/telegram-config.js.map +1 -1
  164. package/dist/src/core/verify.d.ts +23 -1
  165. package/dist/src/core/verify.js +20 -2
  166. package/dist/src/core/verify.js.map +1 -1
  167. package/dist/src/daemon/audit.d.ts +80 -5
  168. package/dist/src/daemon/audit.js +126 -6
  169. package/dist/src/daemon/audit.js.map +1 -1
  170. package/dist/src/daemon/daemon.d.ts +174 -2
  171. package/dist/src/daemon/daemon.js +298 -7
  172. package/dist/src/daemon/daemon.js.map +1 -1
  173. package/dist/src/daemon/git-evidence.d.ts +2 -2
  174. package/dist/src/daemon/git-evidence.js +1 -1
  175. package/dist/src/daemon/projection.d.ts +13 -2
  176. package/dist/src/daemon/projection.js +39 -6
  177. package/dist/src/daemon/projection.js.map +1 -1
  178. package/dist/src/mcp/server.d.ts +39 -3
  179. package/dist/src/mcp/server.js +22 -3
  180. package/dist/src/mcp/server.js.map +1 -1
  181. package/dist/src/muse/server.d.ts +16 -0
  182. package/dist/src/muse/server.js +238 -0
  183. package/dist/src/muse/server.js.map +1 -0
  184. package/dist/src/serve/archive.d.ts +158 -0
  185. package/dist/src/serve/archive.js +354 -0
  186. package/dist/src/serve/archive.js.map +1 -0
  187. package/dist/src/serve/arguments.d.ts +93 -0
  188. package/dist/src/serve/arguments.js +228 -0
  189. package/dist/src/serve/arguments.js.map +1 -0
  190. package/dist/src/serve/credentials.d.ts +91 -0
  191. package/dist/src/serve/credentials.js +143 -0
  192. package/dist/src/serve/credentials.js.map +1 -0
  193. package/dist/src/serve/follow.d.ts +86 -0
  194. package/dist/src/serve/follow.js +101 -0
  195. package/dist/src/serve/follow.js.map +1 -0
  196. package/dist/src/serve/hook-thread.d.ts +134 -0
  197. package/dist/src/serve/hook-thread.js +333 -0
  198. package/dist/src/serve/hook-thread.js.map +1 -0
  199. package/dist/src/serve/hook-worker.d.ts +134 -0
  200. package/dist/src/serve/hook-worker.js +173 -0
  201. package/dist/src/serve/hook-worker.js.map +1 -0
  202. package/dist/src/serve/server.d.ts +412 -0
  203. package/dist/src/serve/server.js +1251 -0
  204. package/dist/src/serve/server.js.map +1 -0
  205. package/docs/README-extended.md +766 -0
  206. package/docs/cli-reference.md +1633 -43
  207. package/package.json +4 -2
  208. package/schema/event.schema.json +59 -2
  209. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  210. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  211. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  212. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  213. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  214. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  215. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  216. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  217. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  218. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  219. package/schema/fixtures/event/valid/daemon-identity.json +15 -0
  220. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  221. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  222. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  223. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  224. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  225. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  226. package/schema/policy.schema.json +50 -6
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The thread a `POST /hook/<harness>` call runs on (APRV-427).
3
+ *
4
+ * A harness hook is synchronous from its first line to its verdict, and its
5
+ * wait sleeps with `Atomics.wait`. Run on the listener's main thread, one hook
6
+ * waiting on a human froze the event loop for the whole of that wait: no socket
7
+ * was read, so the tenant's `GET /status` and `POST /verb/queue` sat unanswered
8
+ * until the question was decided, and N gated calls from one sandbox queued
9
+ * behind each other. Scoping a lock cannot fix a thread that does not run, so
10
+ * the call runs here instead and the main thread stays free.
11
+ *
12
+ * This file runs the hook and nothing else. The verdict is `commandHook`'s, with
13
+ * the request body as the stdin it would have read, exactly as before; what is
14
+ * added is the {@link HookWaitSeam}, which is how this thread hands the store
15
+ * lock back to the listener while the hook polls and asks for it again before
16
+ * the hook appends.
17
+ *
18
+ * ## The handshake
19
+ *
20
+ * The lock lives on the main thread (`serve/server.ts`), because every other
21
+ * writer in the process takes it there. This thread holds it BY PROXY: the
22
+ * main thread acquires it on this thread's behalf and flips a shared flag.
23
+ *
24
+ * - A job arrives with the flag at 0. This thread first reads the verified log
25
+ * ONCE, outside the lock, to warm its own read cache (below), then posts
26
+ * `resume` and blocks on the flag before it runs a line of the hook, so
27
+ * intake, the sweep, register and request all happen under the lock.
28
+ * - `enterWait` clears the flag FIRST and then posts `wait`: the main thread
29
+ * releases the lock, and a later `leaveWait` cannot be satisfied by the grant
30
+ * that has just ended.
31
+ * - `leaveWait` posts `resume` and blocks on the flag until the main thread has
32
+ * re-acquired the lock and set it to 1.
33
+ * - `done` carries the exit code and both streams, and ends the last section.
34
+ *
35
+ * A blocked `Atomics.wait` here costs this thread and nothing else, which is
36
+ * the point of the thread.
37
+ *
38
+ * ## The warm read, outside the lock (APRV-427 review)
39
+ *
40
+ * A new thread's cache is empty, and its first verified read walks the whole
41
+ * chain: 633 ms on a mature tenant log. Taken inside the lock, five threads
42
+ * arriving together serialised five cold walks and held the tenant's `status`
43
+ * behind all of them. So the cold walk happens here, before the lock is asked
44
+ * for, and in parallel with every other thread's. The hook's own reads under
45
+ * the lock are then warm: a hash over the prefix this thread already proved,
46
+ * plus a walk of whatever was appended since. Nothing is decided from the warm
47
+ * read itself, and nothing is appended on the strength of it; every append
48
+ * inside the lock is still compare-and-append against a head read inside the
49
+ * lock (SPEC.md §11.1 invariant 5).
50
+ */
51
+ import { parentPort, workerData } from "node:worker_threads";
52
+ import { commandHook } from "../cli/hook.js";
53
+ import { declareResolvedDaemonIdentity, narrowerAllowlist, refreshDaemonAllowlistFrom, } from "../core/daemon-host.js";
54
+ import { daemonIdentity, setDaemonAllowlist } from "../core/daemon-identity.js";
55
+ import { harnessCapFitsMargin } from "../core/harness-wait.js";
56
+ import { processReadCache, readVerifiedRecords, useVerifiedSnapshots } from "../core/state.js";
57
+ const port = parentPort;
58
+ if (port === null) {
59
+ throw new Error("serve/hook-worker.ts runs as a worker thread and was loaded on the main thread");
60
+ }
61
+ // This thread is an `approval hook` process in every way that matters to a
62
+ // read, so it makes the same opt-in the hook makes on its gated path (APRV-188):
63
+ // a read may resume behind the daemon's published snapshot, admitted only
64
+ // against a hash this thread computes itself. Set once, before the warm read,
65
+ // so the warm read and the hook's own reads are one cache under one proof.
66
+ useVerifiedSnapshots(true);
67
+ // APRV-448. This thread appends on the gate's behalf exactly as the listener's
68
+ // own thread does, so it declares the same identity before it runs a line of
69
+ // any hook. Declared once: the resolution is the listener's, fixed at startup.
70
+ const writer = workerData;
71
+ declareResolvedDaemonIdentity(writer.daemon);
72
+ port.on("message", (job) => {
73
+ const flag = new Int32Array(job.flag);
74
+ const post = (message) => port.postMessage(message);
75
+ // Cold reads (cache misses) made inside a locked section, counted section
76
+ // by section: `mark` is taken when the lock arrives and settled when it goes.
77
+ let coldReadsInLock = 0;
78
+ let mark = 0;
79
+ const settle = () => {
80
+ coldReadsInLock += processReadCache.stats.misses - mark;
81
+ };
82
+ /** Block until the main thread has taken the store lock for this thread. */
83
+ const held = () => {
84
+ while (Atomics.load(flag, 0) !== 1)
85
+ Atomics.wait(flag, 0, 0);
86
+ mark = processReadCache.stats.misses;
87
+ };
88
+ const seam = {
89
+ enterWait: () => {
90
+ settle();
91
+ Atomics.store(flag, 0, 0);
92
+ post({ type: "wait" });
93
+ },
94
+ leaveWait: () => {
95
+ post({ type: "resume" });
96
+ held();
97
+ // Every append after a wait (the execution after a grant, an expiry) is
98
+ // judged against the list in force NOW, not the one read before a wait
99
+ // that may have lasted minutes (APRV-448 review).
100
+ refreshDaemonAllowlistFrom(job.logPath, writer.policy);
101
+ },
102
+ cancelled: () => Atomics.load(flag, 1) === 1,
103
+ };
104
+ const gone = () => Atomics.load(flag, 1) === 1;
105
+ const out = [];
106
+ const err = [];
107
+ try {
108
+ // Outside the lock, and its answer is thrown away: its only effect is the
109
+ // proved prefix this thread's cache now holds. A read that fails here is
110
+ // the hook's to meet and report, inside the lock, in its own words.
111
+ //
112
+ // A caller that went away before the hook began is owed nothing and has
113
+ // asked nothing yet (APRV-427 review), so the call ends without running a
114
+ // line of the hook: looked at before the warm read, after it, and once
115
+ // more when the lock arrives, because the wait for the lock is where a
116
+ // queue of busy sections makes a client give up. Past this point the
117
+ // hook's own poll is what notices.
118
+ if (gone()) {
119
+ post({ type: "cancelled" });
120
+ return;
121
+ }
122
+ readVerifiedRecords(job.logPath);
123
+ if (gone()) {
124
+ post({ type: "cancelled" });
125
+ return;
126
+ }
127
+ post({ type: "resume" });
128
+ held();
129
+ if (gone()) {
130
+ settle();
131
+ post({ type: "cancelled" });
132
+ return;
133
+ }
134
+ // The `daemons` allowlist in force for this call (APRV-448), resolved from
135
+ // the attested policy and this thread's verified read, inside the lock and
136
+ // before the hook appends anything: the listener's thread does the same
137
+ // before every verb call. A resolution that fails leaves the previous one
138
+ // standing, as the daemon's tick does; and the standing one is never wider
139
+ // than the listener's, so a thread spawned after the policy lost its
140
+ // attestation does not start unrestricted (APRV-448 review).
141
+ setDaemonAllowlist(narrowerAllowlist(daemonIdentity()?.allowed ?? null, job.allowed));
142
+ refreshDaemonAllowlistFrom(job.logPath, writer.policy);
143
+ // The budget, looked at once more now the lock is here (APRV-427 review):
144
+ // a call that had room when it arrived and spent it waiting in line or for
145
+ // the lock would open a question the harness kills its asker before
146
+ // anyone can answer. Refused as saturation instead, appending nothing.
147
+ //
148
+ // Measured ONCE, and the same number is what the hook judges by, waits by
149
+ // and records: a remainder re-measured inside the hook a few milliseconds
150
+ // later could fall under the margin after this check had admitted it, and
151
+ // turn an admitted call into `hook-harness-cap-too-short`.
152
+ const { arrivedAt, capMs } = job.budget;
153
+ if (capMs !== null) {
154
+ const remainingMs = Math.max(0, capMs - Math.max(0, Date.now() - arrivedAt));
155
+ if (harnessCapFitsMargin(capMs) && !harnessCapFitsMargin(remainingMs)) {
156
+ settle();
157
+ post({ type: "saturated", remainingMs });
158
+ return;
159
+ }
160
+ seam.remainingCapMs = remainingMs;
161
+ }
162
+ const code = commandHook(job.argv, { out: (text) => out.push(text), err: (text) => err.push(text) }, job.cwd, () => job.body, seam);
163
+ settle();
164
+ post({ type: "done", code, stdout: out.join(""), stderr: err.join(""), coldReadsInLock });
165
+ }
166
+ catch (cause) {
167
+ // `commandHook` already turns a throw inside the harness path into an
168
+ // ordinary deny, so reaching this is a failure of the thread itself. It is
169
+ // reported as one, and the listener answers it as a refusal: never an allow.
170
+ post({ type: "failed", message: cause instanceof Error ? cause.message : String(cause) });
171
+ }
172
+ });
173
+ //# sourceMappingURL=hook-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-worker.js","sourceRoot":"","sources":["../../../src/serve/hook-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,412 @@
1
+ /**
2
+ * `approval serve` — the agent-facing surface over HTTP, for a harness that
3
+ * has no local log and no local policy (APRV-421).
4
+ *
5
+ * A tenant whose harness runs in a sandbox (Agent Village's Hermes tenants,
6
+ * the Meta Muse connector packet) has nowhere for `approval hook <harness>` to
7
+ * write and nothing for it to read. The hosted daemon runs elsewhere, one
8
+ * process per tenant (design/hosted-daemon-identity.md); what was missing was a
9
+ * way to reach it. This is that door, and it is a DOOR rather than a second
10
+ * gate.
11
+ *
12
+ * ## Transport only, and the file says so by importing rather than repeating
13
+ *
14
+ * Every verdict, every verb and every batch of records here comes from the
15
+ * function the CLI dispatches to:
16
+ *
17
+ * - the verb surface is `mcp/server.ts`'s — {@link publishedVerbs},
18
+ * {@link toolDefinitions}, {@link buildArgv} and {@link invokeVerb}, so the
19
+ * catalog MATCHES `approval mcp serve` because it IS `approval mcp serve`'s,
20
+ * `--as` is absent from every published schema for the same one reason, and
21
+ * the launch identity is appended last by the same one line;
22
+ * - a hook call is `commandHook` with the request body as its stdin, so the
23
+ * bytes on the response are the bytes the stdin form prints, for every
24
+ * adapter in `HARNESS_ADAPTERS`, with no dialect knowledge in this file;
25
+ * - a follow page is `core/log-subscribe.ts`'s verified subscription, drained
26
+ * once (`serve/follow.ts`).
27
+ *
28
+ * Nothing here classifies a command, resolves a policy, mints a token or
29
+ * verifies a chain. Two implementations of the gate sequence would be two
30
+ * gates, and the second would be the one nobody reviewed.
31
+ *
32
+ * ## Two credentials, two parties
33
+ *
34
+ * `serve/credentials.ts` holds the reasoning. The short form: the agent
35
+ * credential opens the verbs and the hook, the tenant credential opens the
36
+ * log, the export and status, and the agent credential never reads the log it
37
+ * is judged by.
38
+ *
39
+ * ## Which daemon wrote it (APRV-448)
40
+ *
41
+ * Every record this process appends carries the `daemon` field, with the id
42
+ * `approval status` reports for the same store: `APPROVAL_DAEMON_ID` from the
43
+ * launch environment, or the id derived from the store's instance, resolved
44
+ * ONCE at startup by `core/daemon-host.ts`'s `resolveDaemonId`, the function
45
+ * the daemon loop resolves its own id with. The listener's thread declares it
46
+ * before it binds, and every hook thread declares the same resolution before
47
+ * its first job (`serve/hook-worker.ts`), so a verb's records and a hook's
48
+ * carry one id. No request names any part of it.
49
+ *
50
+ * The attested policy's `daemons` allowlist is enforced at the write boundary
51
+ * exactly as it is for the daemon: refreshed from the verified log inside the
52
+ * store lock before every verb call and every hook call, and an id the list
53
+ * does not admit is refused `daemon-not-allowed` with nothing written. This
54
+ * process does NOT mark itself the daemon (`core/daemon-actor.ts`): that mark
55
+ * routes an advance under the daemon's own autonomous class, and a process that
56
+ * dispatches the verbs an agent asks for must never hold it.
57
+ *
58
+ * ## What it does not do
59
+ *
60
+ * - **No TLS.** There is no certificate handling in this process and there is
61
+ * not going to be: the supported deployment is a loopback bind behind a
62
+ * proxy the operator owns, and a non-loopback bind requires an explicit flag
63
+ * and prints a banner (`cli/serve.ts`).
64
+ * - **No record of its own.** This server appends nothing on its own account.
65
+ * Every event in the log under it was written by a verb a caller asked for,
66
+ * under the identity the operator fixed at launch.
67
+ * - **No state a restart loses.** The only things held between requests are
68
+ * the store lock and a few warm hook threads, which are properties of the
69
+ * process rather than of any caller. Follow cursors belong to the caller, so
70
+ * a host that sleeps and wakes serves the same next page it would have served
71
+ * before.
72
+ *
73
+ * ## What is serialised, and what is not (APRV-427)
74
+ *
75
+ * One lock per store ({@link storeLock}). It covers every stretch of work in
76
+ * this process that may append to the store or must read it as one snapshot:
77
+ * a verb call, a follow page, the export, and a hook call's MUTATION sections
78
+ * (everything up to its poll loop, and everything after it). It does not cover
79
+ * a hook call's WAIT: the call runs on a worker thread (`serve/hook-thread.ts`)
80
+ * and hands the lock back for as long as it polls, so one question waiting on
81
+ * a human no longer holds the tenant's `status`, `queue` and follow, or the
82
+ * next hook call's own question, until the human answers. The catalog takes no
83
+ * lock at all. Across processes the guarantee is unchanged and is not this
84
+ * lock's: every append goes through `core/log.ts`'s lockfile and
85
+ * compare-and-append.
86
+ * - **No `.approval/env`.** SPEC.md §11.1 invariant 7, inherited verbatim from
87
+ * the MCP server: the environment a gate operation runs under is the one the
88
+ * host launched this process with.
89
+ */
90
+ import { type HookThreadStats, type StoreLock } from "./hook-thread.js";
91
+ /**
92
+ * Identity is resolved by the MCP server's own function, re-exported here so
93
+ * that `cli/serve.ts` reaches it through ITS server exactly as `cli/mcp.ts`
94
+ * reaches it through the MCP one.
95
+ *
96
+ * Two transports fixing an identity by two rules would be two answers to
97
+ * "which identities may this process act as", and the answer has to be one:
98
+ * `agent:<id>` and nothing else, refused at startup, never supplied by a
99
+ * caller.
100
+ */
101
+ export { resolveAgentActor, type IdentityCheck } from "../mcp/server.js";
102
+ import { type ServeCredentials, type ServeScope } from "./credentials.js";
103
+ /** Largest request body accepted. A tool call and a hook envelope are small. */
104
+ export declare const MAX_BODY_BYTES: number;
105
+ /**
106
+ * How much of an oversized body this server will read and throw away before
107
+ * dropping the socket.
108
+ *
109
+ * An oversized request is refused at {@link MAX_BODY_BYTES}, but the refusal
110
+ * cannot be WRITTEN until the client stops writing, or the response closes the
111
+ * socket under it and the caller sees a transport error instead of a verdict.
112
+ * So the remainder is discarded as it arrives, up to this much, and beyond it
113
+ * the connection is simply dropped: a caller streaming without end is not a
114
+ * caller owed a polite answer.
115
+ */
116
+ export declare const DRAIN_LIMIT_BYTES: number;
117
+ /**
118
+ * How much of a captured stream travels back in a response body.
119
+ *
120
+ * Both streams are bounded, and both carry a flag saying whether they were
121
+ * clipped. Generous enough that a verdict is never clipped in practice, and
122
+ * present so that a verb which decided to print a megabyte cannot make a
123
+ * response unsendable. Clipping happens at a CODEPOINT boundary
124
+ * ({@link clip}), so what comes back is always valid text.
125
+ */
126
+ export declare const MAX_STREAM_BYTES: number;
127
+ /**
128
+ * The exit code every server-authored refusal carries.
129
+ *
130
+ * NON-ZERO, always, and this is the finding that made the whole response
131
+ * contract change. A refusal used to travel as an HTTP status with the exit
132
+ * code in a header that refusals did not set, so a client following the
133
+ * documented rule — write stdout, exit `x-approval-exit-code` — read a missing
134
+ * header as zero, and zero is ALLOW on Claude Code, Cursor, Codex and Muse. A
135
+ * 413 on an oversized hook envelope was therefore an allow. Nothing on this
136
+ * surface may be able to turn a refusal into a permission, so the exit code is
137
+ * in the body, it is always present, and on a refusal it is always this.
138
+ */
139
+ export declare const REFUSAL_EXIT_CODE = 2;
140
+ /**
141
+ * The verbs the AGENT credential may call. An ALLOWLIST, and the direction is
142
+ * the whole of it.
143
+ *
144
+ * The catalog is the registry's and does not change (`mcp/server.ts` owns it,
145
+ * and this surface publishes exactly what `approval mcp serve` publishes).
146
+ * This is authorization, which is a different question asked one layer later,
147
+ * and it is asked per verb.
148
+ *
149
+ * ## Why a list of what a harness MAY do
150
+ *
151
+ * The threat model this transport exists for is a sandboxed harness on another
152
+ * machine, not an operator at their own laptop. `approval mcp serve` is run BY
153
+ * the operator, on the operator's box, over a pipe the operator handed it; its
154
+ * whole catalog is reachable there because the party at the other end is the
155
+ * party who started it. Here the party at the other end is the party under
156
+ * oversight, and the operative rule is that it never reads the log it is
157
+ * judged by, nor spends the host's credentials, nor runs argv on the host.
158
+ *
159
+ * A denylist would have to name `log tail`, `log export`, `queue`, `coverage`,
160
+ * `doctor`, every `adapter <name>`, `run`, `sandbox`, `token`, `reindex`,
161
+ * `render` and every verb that lands next. This list names what a harness
162
+ * under oversight needs in order to ASK and to ACT ON a grant, so a verb added
163
+ * to the registry tomorrow is TENANT-scoped until somebody decides otherwise.
164
+ * Fail closed, per SPEC.md §11, which is the same reasoning `GUEST_VERBS`
165
+ * gives for being positive.
166
+ *
167
+ * What is on it, and why each:
168
+ *
169
+ * - `instructions`, `hook_classify` — read the guide, and find out what the
170
+ * classifier makes of a command. Neither reads the log or the policy file;
171
+ * both tell an agent what it is about to be judged for before it asks.
172
+ * - `request`, `wait`, `withdraw` — ask, wait for the answer, and retract your
173
+ * own question. This is the gate sequence, and it is the reason the agent
174
+ * credential exists.
175
+ * - `propose`, `start` (APRV-445) — the same sequence for an action the agent
176
+ * declares itself, with its payload as a flag value: register-and-request in
177
+ * a class the operator declared by name and opened with `agent_may_request`,
178
+ * and record the execution the grant or the policy authorized. Neither takes
179
+ * a path, and `start` is requester-only.
180
+ * That is the whole list. It is seven verbs, and the shortness is the point: a
181
+ * harness under oversight asks and is answered, and everything else about the
182
+ * gate belongs to the party the gate is for.
183
+ *
184
+ * ## What came OFF this list in review, and why
185
+ *
186
+ * - `register` takes a task-file POSITIONAL, which is a host path. The hook
187
+ * endpoint is a harness's way to register: it synthesises the envelope from
188
+ * the tool call, so nothing is lost and a host path stops being something a
189
+ * remote caller names.
190
+ * - `log_verify` and `gate_status` hand back the log's length, its head hash
191
+ * and the live state of the gate. "Not a record" is not the same as "not
192
+ * about the log": a head hash moving is a decision being recorded, and an
193
+ * open window is the human's current posture toward this very agent.
194
+ * - `policy_check` and `policy_test` answer what the policy does with a class,
195
+ * and `policy_check` names the absolute store path while doing it.
196
+ * - **Both payload builders**, under the rule that a builder is the agent's
197
+ * only if it builds from the request's own arguments and never reaches a
198
+ * host resource. `payload_hash` takes a FILE, and on this transport that
199
+ * file is on the daemon's machine while the caller is not: every path it
200
+ * could name is one the host put there, so answering would be a hash oracle
201
+ * that also files the bytes into the payload store.
202
+ * `payload_agentmail-draft` takes two ids rather than a path, but it reads
203
+ * the draft over HTTPS with `AGENTMAIL_API_KEY` FROM THIS SERVER'S OWN
204
+ * ENVIRONMENT, so a remote caller would be spending the host's credential on
205
+ * ids it chose — the same fault wearing a network instead of a filesystem.
206
+ * The narrow reading was taken on purpose: publishing a builder that should
207
+ * have been withheld is a capability leak, and withholding one that should
208
+ * have been published is a line of configuration.
209
+ *
210
+ * Everything else is the tenant's, including the ones easiest to wave through:
211
+ * `log_tail` and `log_export` return RECORDS, `queue` returns the tenant's
212
+ * pending decisions, `run` and `sandbox` spawn argv on the daemon's machine,
213
+ * every `adapter_<name>` spends vault credentials, and `status` is the
214
+ * tenant's view of the oversight the agent is under. All of them stay
215
+ * PUBLISHED, so the surface is honest about what exists, and all of them
216
+ * answer the tenant credential only.
217
+ */
218
+ export declare const AGENT_VERBS: ReadonlySet<string>;
219
+ /**
220
+ * The frozen refusal vocabulary this server authors itself (SPEC.md §11.1
221
+ * invariant 6).
222
+ *
223
+ * A refusal a VERB produced keeps the verb's own code, unchanged, because it
224
+ * is the CLI's refusal arriving over a different transport; so does a refusal
225
+ * `buildArgv` produced, `mcp-` prefix and all, because that function is the
226
+ * one both transports call and renaming its codes here would make one surface
227
+ * lie about the other. What is listed below is only what this file decides.
228
+ */
229
+ export declare const SERVE_REFUSAL_CODES: readonly ["serve-unauthorized", "serve-agent-forbidden", "serve-tenant-forbidden", "serve-unknown-path", "serve-malformed-url", "serve-unknown-verb", "serve-unknown-harness", "serve-method-not-allowed", "serve-body-too-large", "serve-body-unreadable", "serve-invalid-cursor", "serve-path-pinned", "serve-path-outside-store", "serve-positional-flag", "serve-flag-not-permitted", "serve-export-symlink", "serve-export-hardlink", "serve-export-failed", "serve-hook-failed", "serve-hook-saturated"];
230
+ export type ServeRefusalCode = (typeof SERVE_REFUSAL_CODES)[number];
231
+ export interface ServeOptions {
232
+ /** `agent:<id>`, already validated. Every verb call is recorded under it. */
233
+ actor: string;
234
+ /** The store root: the working directory every relative path resolves against. */
235
+ cwd: string;
236
+ /** `--log`, when the operator pinned one. */
237
+ log?: string;
238
+ /** `--policy`, when the operator pinned one. */
239
+ policy?: string;
240
+ /** The two bearer credentials, already resolved from the environment. */
241
+ credentials: ServeCredentials;
242
+ /**
243
+ * The launch environment the daemon id is resolved from (APRV-448):
244
+ * `APPROVAL_DAEMON_ID` when it holds one, otherwise the id derived from the
245
+ * store's instance. Defaults to `process.env`. Nothing else in this server
246
+ * reads it; the verbs read the environment the process was launched with, as
247
+ * before. An unusable declared id makes {@link serveApproval} reject before
248
+ * it binds, which the CLI has already refused in its own words.
249
+ */
250
+ env?: NodeJS.ProcessEnv;
251
+ /** Interface to bind. The CLI owns the widening decision; this defaults to loopback. */
252
+ host?: string;
253
+ /** TCP port. `0` asks the kernel for an ephemeral one, which is what tests use. */
254
+ port: number;
255
+ /**
256
+ * A unix-domain socket to listen on INSTEAD of `host`/`port` (APRV-445). An
257
+ * absolute path whose directory the serving uid owns; see
258
+ * {@link checkUnixSocketTarget} and {@link UNIX_SOCKET_MODE}.
259
+ */
260
+ socketPath?: string;
261
+ /** `--timeout` pinned on every hook call, when the operator chose one. */
262
+ hookTimeout?: string;
263
+ /**
264
+ * `--harness-cap` pinned on every hook call, when the operator stated one
265
+ * (APRV-423).
266
+ *
267
+ * Independent of {@link ServeOptions.hookTimeout} and composed with it: that
268
+ * one bounds how long a call WAITS, this one states the ceiling the harness
269
+ * on the far side of the call imposes on its own hook process. The request
270
+ * this server opens is then judged by the shorter of the policy's TTL and
271
+ * that ceiling minus a margin, so the expiry is recorded while the tenant's
272
+ * harness is still listening.
273
+ */
274
+ hookHarnessCap?: string;
275
+ /**
276
+ * Hook calls that may run at once, each on its own thread (APRV-427 review).
277
+ * Defaults to {@link DEFAULT_HOOK_THREADS}. See `serve/hook-thread.ts`.
278
+ */
279
+ hookThreads?: number;
280
+ /**
281
+ * Hook calls that may wait for a thread before one is refused
282
+ * `serve-hook-saturated`. Defaults to {@link DEFAULT_HOOK_QUEUE}.
283
+ */
284
+ hookQueue?: number;
285
+ /** Request lines. The CLI passes stderr; stdout is never written to. */
286
+ notice?: (text: string) => void;
287
+ }
288
+ export interface ServeHandle {
289
+ readonly host: string;
290
+ readonly port: number;
291
+ /** The unix socket this listener is bound to, or `null` for a TCP bind. */
292
+ readonly socketPath: string | null;
293
+ /** The id every record this process appends carries (APRV-448). */
294
+ readonly daemonId: string;
295
+ /** How many requests this listener has answered. Diagnostics and tests. */
296
+ requests(): number;
297
+ /** The hook thread pool's state now. Diagnostics and tests. */
298
+ hookThreads(): HookThreadStats;
299
+ close(): Promise<void>;
300
+ }
301
+ /**
302
+ * One captured stream, bounded and clipped at a CODEPOINT boundary.
303
+ *
304
+ * A naive byte slice can cut a multi-byte sequence in half and produce a
305
+ * string that is not valid text, which for a JSON body means either a
306
+ * replacement character where a verdict had a word or, worse, a body a strict
307
+ * client refuses to parse. `StringDecoder` emits only the complete codepoints
308
+ * in what it was given and keeps the incomplete tail to itself, which is
309
+ * exactly the clip wanted here.
310
+ */
311
+ export declare function clip(text: string): {
312
+ text: string;
313
+ truncated: boolean;
314
+ };
315
+ export interface CatalogEntry {
316
+ name: string;
317
+ title: string;
318
+ description: string;
319
+ inputSchema: unknown;
320
+ /** Which credential opens this verb. Annotation only; the check is below. */
321
+ scope: ServeScope;
322
+ }
323
+ /**
324
+ * The published surface: `approval mcp serve`'s tool list, with each entry's
325
+ * scope stated.
326
+ *
327
+ * Derived, not restated. A verb added to the registry tomorrow appears here
328
+ * the same day it appears as an MCP tool, with `--as` already deleted from its
329
+ * schema by the same function, and `grant` is absent for the same reason it is
330
+ * absent there — the registry marks it `human_only` and no transport publishes
331
+ * a human's authority. Its SCOPE is tenant until somebody puts it on
332
+ * {@link AGENT_VERBS}, which is the fail-closed half of the same derivation.
333
+ */
334
+ export declare function serveCatalog(): CatalogEntry[];
335
+ /**
336
+ * The flags every hook call is made with.
337
+ *
338
+ * Exported so a test can hand the SAME argv to `commandHook` directly and
339
+ * compare the bytes. That comparison is the acceptance criterion, and it is
340
+ * only honest if both sides are the same invocation of the same verb: a test
341
+ * that built its own flag list would be comparing two spellings.
342
+ *
343
+ * `--as` is appended LAST, as everywhere else on this surface, so the identity
344
+ * the operator fixed when they started the process wins over anything that
345
+ * arrived by another route (`parseFlags` keeps the last occurrence).
346
+ */
347
+ export declare function hookArgv(options: Pick<ServeOptions, "actor" | "cwd" | "log" | "policy" | "hookTimeout" | "hookHarnessCap">): string[];
348
+ type Route = {
349
+ kind: "catalog";
350
+ } | {
351
+ kind: "verb";
352
+ name: string;
353
+ } | {
354
+ kind: "hook";
355
+ harness: string;
356
+ } | {
357
+ kind: "follow";
358
+ } | {
359
+ kind: "export";
360
+ } | {
361
+ kind: "status";
362
+ };
363
+ /** The route a path names, or null. Method is checked by the handler. */
364
+ export declare function routeOf(pathname: string): Route | null;
365
+ /**
366
+ * Which credential opens a route.
367
+ *
368
+ * `null` means either one does, which is true of the catalog alone: it is the
369
+ * published contract, it reads no log and it appends nothing, and both parties
370
+ * have a reason to ask what this server is.
371
+ */
372
+ export declare function scopeOf(route: Route): ServeScope | null;
373
+ /**
374
+ * Start the listener. Resolves once it is bound and its real port is known;
375
+ * rejects when the bind fails, so the CLI can report it and exit.
376
+ */
377
+ export declare function serveApproval(options: ServeOptions): Promise<ServeHandle>;
378
+ /**
379
+ * The mode a unix socket is given once bound: everyone the DIRECTORY admits.
380
+ *
381
+ * A hosted tenant runs the agent's sandbox as a different uid from this server,
382
+ * on the same machine, and the socket is how the two meet without a TCP port
383
+ * anybody on the host could dial. Who may connect is therefore decided by the
384
+ * directory the socket sits in, which the operator owns and sets (`0710` with
385
+ * the sandbox's group, an ACL, a bind mount), and the socket itself does not
386
+ * second-guess it.
387
+ */
388
+ export declare const UNIX_SOCKET_MODE = 438;
389
+ /**
390
+ * Refuse a socket path this process should not bind (APRV-445).
391
+ *
392
+ * Absolute, inside an existing directory OWNED BY THE SERVING UID (the directory
393
+ * is the access control, so a directory somebody else owns is somebody else's
394
+ * access control), and not an existing file of any kind other than a socket: a
395
+ * stale socket is replaced, and anything else at that path is a file this
396
+ * server would destroy by binding over it. Synchronous and side-effect free, so
397
+ * the CLI can refuse before any listener exists.
398
+ */
399
+ export declare function checkUnixSocketTarget(socketPath: string): {
400
+ ok: true;
401
+ } | {
402
+ ok: false;
403
+ message: string;
404
+ };
405
+ /**
406
+ * The lock for the store whose log is `logPath` (resolved against `root`).
407
+ *
408
+ * Exported so a test can hold the SAME lock the listener takes and watch what
409
+ * waits on it: that is how the suite shows a hook call's appends are inside
410
+ * the lock and its wait is outside it, rather than inferring it from timing.
411
+ */
412
+ export declare function storeLock(logPath: string, root: string): StoreLock;