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
@@ -54,7 +54,14 @@
54
54
  import { spawnSync } from "node:child_process";
55
55
  import { childEnvironment } from "./child-env.js";
56
56
  /** The harnesses this runtime speaks a hook protocol for. */
57
- export const HARNESS_KINDS = ["claude-code", "cursor", "codex", "grok", "muse"];
57
+ export const HARNESS_KINDS = [
58
+ "claude-code",
59
+ "cursor",
60
+ "codex",
61
+ "grok",
62
+ "muse",
63
+ "hermes",
64
+ ];
58
65
  /** Is `value` one of the harnesses this runtime knows? */
59
66
  export function isHarnessKind(value) {
60
67
  return typeof value === "string" && HARNESS_KINDS.includes(value);
@@ -71,6 +78,7 @@ export const HARNESS_BINARY = {
71
78
  codex: "codex",
72
79
  grok: "grok",
73
80
  muse: "muse",
81
+ hermes: "hermes",
74
82
  };
75
83
  /**
76
84
  * How long a version string may be, and the reason there is a cap at all.
@@ -121,7 +129,40 @@ export function normalizeHarnessVersion(raw) {
121
129
  */
122
130
  export const HARNESS_PROBE_TIMEOUT_MS = 10_000;
123
131
  /**
124
- * Run `<binary> --version` once, uncached, and normalize what came back.
132
+ * How long the RAW first line of `<binary> --version` may be.
133
+ *
134
+ * Wider than {@link HARNESS_VERSION_LIMIT} because it holds a different kind of
135
+ * value for a different reader: nothing capped by this number is ever appended to
136
+ * the log (only {@link normalizeHarnessVersion}'s output is), and what needs the
137
+ * raw line is a diagnostic that must be able to read a build stamp a harness
138
+ * prints beside its semver. Hermes prints
139
+ * `Hermes Agent v0.21.3 (2026.9.14) · upstream 913d4098`, which the log's own
140
+ * rule rejects on the non-ASCII separator alone. Capped and stripped of control
141
+ * characters all the same: this is third-party output on its way to a terminal.
142
+ */
143
+ export const HARNESS_RAW_VERSION_LIMIT = 200;
144
+ /**
145
+ * The first line of a `--version` output, as a line a diagnostic may print.
146
+ *
147
+ * One line, trimmed, control characters removed, capped. `null` for empty output
148
+ * or a line past the cap. It keeps non-ASCII bytes, which is the whole difference
149
+ * from {@link normalizeHarnessVersion}: a build stamp behind a typographic
150
+ * separator is exactly the fact a version floor has to read, and it is exactly
151
+ * the fact the write boundary will not accept.
152
+ */
153
+ export function rawHarnessVersionLine(raw) {
154
+ if (typeof raw !== "string")
155
+ return null;
156
+ const first = raw.split("\n", 1)[0] ?? "";
157
+ // `\p{Cc}` rather than an escaped range: the class is the Unicode CONTROL
158
+ // category, which is the thing being removed, and it reads as that.
159
+ const text = first.replace(/\p{Cc}/gu, "").trim();
160
+ if (text.length === 0 || text.length > HARNESS_RAW_VERSION_LIMIT)
161
+ return null;
162
+ return text;
163
+ }
164
+ /**
165
+ * Run `<binary> --version` once, uncached, and return its raw first line.
125
166
  *
126
167
  * Every failure is a value rather than an exception, in the manner of
127
168
  * {@link spawnGloss}: a missing binary, a non-zero exit and a timeout kill are
@@ -132,7 +173,7 @@ export const HARNESS_PROBE_TIMEOUT_MS = 10_000;
132
173
  * vault passphrase, and it has no use for either. Nothing is declared, because
133
174
  * reading a version is not a granted action.
134
175
  */
135
- export function probeHarnessVersion(kind) {
176
+ export function probeHarnessVersionRaw(kind) {
136
177
  let result;
137
178
  try {
138
179
  result = spawnSync(HARNESS_BINARY[kind], ["--version"], {
@@ -150,32 +191,158 @@ export function probeHarnessVersion(kind) {
150
191
  }
151
192
  if (result.error !== undefined || result.status !== 0)
152
193
  return null;
153
- return normalizeHarnessVersion(result.stdout);
194
+ return rawHarnessVersionLine(result.stdout);
195
+ }
196
+ /**
197
+ * Run `<binary> --version` once, uncached, and normalize what came back.
198
+ *
199
+ * The recordable form: one line, printable ASCII, capped. Exported for the test
200
+ * that proves the memo below is a memo.
201
+ */
202
+ export function probeHarnessVersion(kind) {
203
+ return normalizeHarnessVersion(probeHarnessVersionRaw(kind));
154
204
  }
155
205
  /**
156
206
  * The memo. Holds the FAILURES too: a harness that is not on PATH is not on
157
207
  * PATH, and asking a second time in the same process would pay a second spawn
158
208
  * for the same `null`.
159
- */
160
- const probed = new Map();
161
- /**
162
- * The installed version of `kind`, read at most once per process.
163
209
  *
164
- * This is the only entry point callers should use. `probeHarnessVersion` is
165
- * exported for the test that proves the memo is a memo.
210
+ * It holds the RAW line rather than the recordable one, so the two readers — the
211
+ * provenance field and the version floor — still cost ONE spawn per process
212
+ * between them. A harness whose raw line the write boundary rejects has a
213
+ * `null` recordable version and a readable raw one, which is precisely Hermes's
214
+ * case and precisely why the floor is not read off a record.
166
215
  */
167
- export function installedHarnessVersion(kind) {
216
+ const probed = new Map();
217
+ /** The raw first line of `<binary> --version`, read at most once per process. */
218
+ export function installedHarnessVersionRaw(kind) {
168
219
  const memo = probed.get(kind);
169
220
  if (memo !== undefined)
170
221
  return memo;
171
- const value = probeHarnessVersion(kind);
222
+ const value = probeHarnessVersionRaw(kind);
172
223
  probed.set(kind, value);
173
224
  return value;
174
225
  }
226
+ /**
227
+ * The installed version of `kind`, read at most once per process.
228
+ *
229
+ * This is the only entry point callers should use for a RECORDABLE version.
230
+ * `probeHarnessVersion` is exported for the test that proves the memo is a memo.
231
+ */
232
+ export function installedHarnessVersion(kind) {
233
+ return normalizeHarnessVersion(installedHarnessVersionRaw(kind));
234
+ }
175
235
  /** Drop the memo. TEST ONLY: a process reads a version once, by design. */
176
236
  export function resetHarnessVersionCache() {
177
237
  probed.clear();
178
238
  }
239
+ // ---------------------------------------------------------------------------
240
+ // The Hermes fail-closed version floor (APRV-415)
241
+ // ---------------------------------------------------------------------------
242
+ /**
243
+ * The build at which Hermes Agent starts honouring `fail_closed` (APRV-415).
244
+ *
245
+ * ## Why a floor exists at all, and why it is a DATE rather than a version
246
+ *
247
+ * `approval hook hermes` is enforcement rather than a backstop only because of a
248
+ * per-entry `fail_closed: true` that makes a hook crash, a hook timeout and
249
+ * unparseable hook output BLOCK. The live probe measured both halves of that on
250
+ * two builds of the same harness, and the answer differs between them:
251
+ *
252
+ * - `main` at `118984d7`, built 2026-09-20: crash, garbage and hang were all
253
+ * REFUSED (the hang at the 300s per-entry cap). The key works;
254
+ * - `v0.21.3`, built 2026.9.14: all three PROCEEDED. That build does not know the
255
+ * key and ignores it SILENTLY. `hermes hooks list` renders no `fail_closed`
256
+ * flag on either build, so the listing cannot be used to tell them apart.
257
+ *
258
+ * The two builds report the SAME semver. `hermes --version` on the older one
259
+ * prints `Hermes Agent v0.21.3 (2026.9.14) · upstream 913d4098`, and the update
260
+ * that fixed the behaviour moved 670 commits without moving `0.21.3`. So a floor
261
+ * expressed as a semantic version would compare the one field that did not
262
+ * change, and would pass a build that fails open. The build DATE and the upstream
263
+ * commit are the two fields that did move, and they are what this compares.
264
+ *
265
+ * ## What it can and cannot do
266
+ *
267
+ * It can only ADD a red line to `approval doctor` (SPEC.md §11.1 invariant 4: a
268
+ * self-reported field never reduces scrutiny). A build claiming a later date buys
269
+ * nothing — it defeats a row that would have asked a human to look — and nothing
270
+ * in the runtime reads this to widen a verdict, skip a wait or lower a class. The
271
+ * refusals the adapter emits are the same on every build; what changes below the
272
+ * floor is whether a BROKEN hook stops the call, which no code in this repository
273
+ * can observe from inside the hook it is broken in.
274
+ */
275
+ export const HERMES_FAIL_CLOSED_FLOOR = {
276
+ /** The first build observed to honour the key, by upstream commit. */
277
+ upstream: "118984d7",
278
+ /** That build's date, in the `YYYY.M.D` stamp `hermes --version` prints. */
279
+ date: { year: 2026, month: 9, day: 20 },
280
+ /** The one sentence every surface quotes, so they cannot drift apart. */
281
+ statement: "a build at or after main 118984d7 of 2026-09-20; v0.21.3 (2026.9.14) fails open silently",
282
+ };
283
+ /**
284
+ * Read the fields out of `hermes --version`'s first line, or `null`.
285
+ *
286
+ * Shape-only and forgiving in one direction: a field this cannot find is `null`
287
+ * rather than a guess, and a line naming none of the three is not a Hermes
288
+ * version line at all. The observed line is
289
+ * `Hermes Agent v0.21.3 (2026.9.14) · upstream 913d4098`; the separator is read
290
+ * past rather than required, because a release that changes its punctuation must
291
+ * not silently turn the floor check into an unknown.
292
+ */
293
+ export function parseHermesVersion(raw) {
294
+ const line = rawHarnessVersionLine(raw);
295
+ if (line === null)
296
+ return null;
297
+ const semver = /\bv(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/u.exec(line);
298
+ const stamp = /\((\d{4})\.(\d{1,2})\.(\d{1,2})\)/u.exec(line);
299
+ const upstream = /\bupstream\s+([0-9a-f]{7,40})\b/iu.exec(line);
300
+ if (semver === null && stamp === null && upstream === null)
301
+ return null;
302
+ return {
303
+ semver: semver?.[1] ?? null,
304
+ date: stamp === null
305
+ ? null
306
+ : {
307
+ year: Number(stamp[1]),
308
+ month: Number(stamp[2]),
309
+ day: Number(stamp[3]),
310
+ },
311
+ upstream: upstream?.[1]?.toLowerCase() ?? null,
312
+ };
313
+ }
314
+ /**
315
+ * Does this Hermes build honour `fail_closed`?
316
+ *
317
+ * - `"honours"` — its upstream commit IS the floor's, or its build date is on or
318
+ * after the floor's date;
319
+ * - `"ignores"` — its build date is BEFORE the floor's. This is the one answer
320
+ * that names a fault, and it is the state a live probe measured rather than
321
+ * inferred;
322
+ * - `"unknown"` — no version could be read, or the line carries neither a build
323
+ * date nor the floor's commit. Two commit hashes cannot be ORDERED without a
324
+ * repository, so a build stamped with an unfamiliar commit and no date is
325
+ * honestly unknown rather than quietly either.
326
+ *
327
+ * `"unknown"` is not a synonym for `"ignores"`, deliberately. This value feeds a
328
+ * health check, and a health check that cried fault on every unreadable banner
329
+ * would train an operator to stop reading it, which costs more than the case it
330
+ * would catch. What guards the unknown case instead is the doc, the help block
331
+ * and the probe's own report, all of which state {@link
332
+ * HERMES_FAIL_CLOSED_FLOOR.statement} verbatim.
333
+ */
334
+ export function hermesFailClosedSupport(raw) {
335
+ const fields = parseHermesVersion(raw);
336
+ if (fields === null)
337
+ return "unknown";
338
+ const floor = HERMES_FAIL_CLOSED_FLOOR;
339
+ if (fields.upstream !== null && fields.upstream.startsWith(floor.upstream))
340
+ return "honours";
341
+ if (fields.date === null)
342
+ return "unknown";
343
+ const asNumber = (date) => date.year * 10_000 + date.month * 100 + date.day;
344
+ return asNumber(fields.date) >= asNumber(floor.date) ? "honours" : "ignores";
345
+ }
179
346
  /**
180
347
  * Build the pair for `kind`, preferring the version the hook event supplied.
181
348
  *
@@ -1 +1 @@
1
- {"version":3,"file":"harness-version.js","sourceRoot":"","sources":["../../../src/core/harness-version.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness-version.js","sourceRoot":"","sources":["../../../src/core/harness-version.ts"],"names":[],"mappings":"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"}
@@ -2,13 +2,16 @@
2
2
  * How long a harness hook waits, and how long the question it opened outlives
3
3
  * that wait (APRV-287).
4
4
  *
5
- * Two numbers with two readers each, which is the whole reason they live here
6
- * rather than beside either one. `cli/hook.ts` uses them to decide when a
7
- * request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
5
+ * Three numbers with two readers each, which is the whole reason they live
6
+ * here rather than beside any one of them. `cli/hook.ts` uses them to decide
7
+ * when a request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
8
8
  * to decide which pending requests on a listener's first cycle are stale enough
9
9
  * to collapse into one message. Two copies of the same duration would be two
10
10
  * answers to "is anybody still waiting on this", and the channel would collapse
11
11
  * questions the hook was still holding open, or leave a dead queue on a phone.
12
+ * The third, APRV-423's margin, is read by the hook (which refuses a harness
13
+ * ceiling too short to hold a human's answer) and by `core/state.ts` (which
14
+ * judges every request against the window that ceiling leaves).
12
15
  *
13
16
  * Nothing here reads a clock, a policy or the log: these are durations, and
14
17
  * every judgment made with them is made by a caller that already holds the
@@ -53,3 +56,79 @@ export declare const HOOK_RETRY_GRACE_MS: number;
53
56
  * caller states about when it started waiting.
54
57
  */
55
58
  export declare function abandonedAfterMs(waitMs: number, graceMs: number): number;
59
+ /**
60
+ * How far below a harness's own timeout a hook-originated request must lapse
61
+ * (APRV-423).
62
+ *
63
+ * A harness bounds the hook process: Hermes caps a `pre_tool_call` entry at
64
+ * 300 s, Claude Code kills the hook at the `timeout` in its settings file, and
65
+ * every one of them treats the killed process as a NON-BLOCKING error and runs
66
+ * the tool call. So when the harness gives up first the hook is already gone,
67
+ * the request is still pending on somebody's phone, and the tap that arrives
68
+ * afterwards is recorded as a grant on a call nobody holds (APRV-410, observed
69
+ * 2026-09-20). The two records then disagree about the same moment: one says
70
+ * denied, the other says approved.
71
+ *
72
+ * The fix is to make the question end BEFORE the process that asked it does,
73
+ * and this is the distance between the two. It is 60 s because the daemon's
74
+ * TTL sweep runs on a 30 s interval (`daemon/daemon.ts`'s
75
+ * `DEFAULT_INTERVAL_MS`): a request that lapses one millisecond after a sweep
76
+ * waits a whole interval for the next one, so a margin of two intervals leaves
77
+ * the `approval.expired` append a full tick of room in the common case, and it
78
+ * lands inside the cap. Anything smaller would make the ordering this task is
79
+ * about depend on where in the sweep cycle the lapse fell.
80
+ *
81
+ * The common case, and not a guarantee. `Daemon.tick` skips a sweep when the
82
+ * previous tick is still running, and the request record's `ts` is stamped at
83
+ * the write boundary, some intake latency after the harness spawned the hook,
84
+ * so the cap runs from an instant slightly before the one the margin counts
85
+ * from. An overrunning tick plus a slow intake can put the `approval.expired`
86
+ * record after the kill. What keeps a late tap safe regardless is not this
87
+ * margin but the lazy refusal: `core/state.ts` judges the lapse by arithmetic
88
+ * whether or not the record exists, so `decide` refuses the tap `expired` and
89
+ * materialises the record then. The margin buys the ORDERING in the common
90
+ * case, so the two records agree without a human ever seeing the disagreement;
91
+ * the refusal is what makes the grant impossible in every case.
92
+ *
93
+ * It is a floor on the cap as well as a subtraction from it. A cap at or below
94
+ * the margin leaves no window in which a human could answer at all: `cli/hook.ts`
95
+ * refuses such a configuration rather than opening a question that is already
96
+ * dead, and `schema/event.schema.json` refuses a record carrying one at the
97
+ * write boundary (its `minimum` is this constant plus one, pinned equal by
98
+ * `tests/harness-cap-ttl.test.ts`).
99
+ */
100
+ export declare const HARNESS_CAP_MARGIN_MS = 60000;
101
+ /**
102
+ * The deadline a hook-originated request is judged by: the SHORTER of the
103
+ * policy's TTL and the harness's cap minus {@link HARNESS_CAP_MARGIN_MS}
104
+ * (APRV-423).
105
+ *
106
+ * `null` means no deadline, exactly as `defaults.approval_ttl`'s absence means
107
+ * it (SPEC.md §6): nothing lapses, and no duration is invented for a policy
108
+ * that declared none.
109
+ *
110
+ * **The cap may only shorten.** It is a self-reported field (the hook states
111
+ * the ceiling its own harness runs it under), and SPEC.md §11.1 invariant 4
112
+ * says such a field never reduces scrutiny. Recording it raw and taking the
113
+ * minimum HERE is what makes that structural: a cap larger than the policy TTL
114
+ * changes nothing, a cap of an hour under a one-minute TTL changes nothing, and
115
+ * the only direction a requester can move its own deadline is earlier. The same
116
+ * shape as `core/gate-window.ts`'s `expires_at`, which clamps a claimed window
117
+ * against the derived one for the same reason.
118
+ *
119
+ * A cap that does not clear the margin yields `0`, which reads as lapsed from
120
+ * the instant the request is written. That is the fail-closed answer for a
121
+ * harness whose ceiling leaves no room for a human, and it is unreachable
122
+ * through the write path: the hook refuses the configuration before it
123
+ * appends, and the event schema refuses the record for any caller that did
124
+ * not come through the hook.
125
+ */
126
+ export declare function harnessCappedTtlMs(policyTtlMs: number | null, capMs: number | null): number | null;
127
+ /**
128
+ * Does `capMs` leave a window a human could answer in?
129
+ *
130
+ * The one question `cli/hook.ts` asks before it opens a request under a cap.
131
+ * Strictly greater: a cap EQUAL to the margin yields a zero-length window,
132
+ * which is a question that expires as it is asked.
133
+ */
134
+ export declare function harnessCapFitsMargin(capMs: number): boolean;
@@ -2,13 +2,16 @@
2
2
  * How long a harness hook waits, and how long the question it opened outlives
3
3
  * that wait (APRV-287).
4
4
  *
5
- * Two numbers with two readers each, which is the whole reason they live here
6
- * rather than beside either one. `cli/hook.ts` uses them to decide when a
7
- * request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
5
+ * Three numbers with two readers each, which is the whole reason they live
6
+ * here rather than beside any one of them. `cli/hook.ts` uses them to decide
7
+ * when a request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
8
8
  * to decide which pending requests on a listener's first cycle are stale enough
9
9
  * to collapse into one message. Two copies of the same duration would be two
10
10
  * answers to "is anybody still waiting on this", and the channel would collapse
11
11
  * questions the hook was still holding open, or leave a dead queue on a phone.
12
+ * The third, APRV-423's margin, is read by the hook (which refuses a harness
13
+ * ceiling too short to hold a human's answer) and by `core/state.ts` (which
14
+ * judges every request against the window that ceiling leaves).
12
15
  *
13
16
  * Nothing here reads a clock, a policy or the log: these are durations, and
14
17
  * every judgment made with them is made by a caller that already holds the
@@ -55,4 +58,87 @@ export const HOOK_RETRY_GRACE_MS = 5 * 60_000;
55
58
  export function abandonedAfterMs(waitMs, graceMs) {
56
59
  return Math.max(0, waitMs) + Math.max(0, graceMs);
57
60
  }
61
+ /**
62
+ * How far below a harness's own timeout a hook-originated request must lapse
63
+ * (APRV-423).
64
+ *
65
+ * A harness bounds the hook process: Hermes caps a `pre_tool_call` entry at
66
+ * 300 s, Claude Code kills the hook at the `timeout` in its settings file, and
67
+ * every one of them treats the killed process as a NON-BLOCKING error and runs
68
+ * the tool call. So when the harness gives up first the hook is already gone,
69
+ * the request is still pending on somebody's phone, and the tap that arrives
70
+ * afterwards is recorded as a grant on a call nobody holds (APRV-410, observed
71
+ * 2026-09-20). The two records then disagree about the same moment: one says
72
+ * denied, the other says approved.
73
+ *
74
+ * The fix is to make the question end BEFORE the process that asked it does,
75
+ * and this is the distance between the two. It is 60 s because the daemon's
76
+ * TTL sweep runs on a 30 s interval (`daemon/daemon.ts`'s
77
+ * `DEFAULT_INTERVAL_MS`): a request that lapses one millisecond after a sweep
78
+ * waits a whole interval for the next one, so a margin of two intervals leaves
79
+ * the `approval.expired` append a full tick of room in the common case, and it
80
+ * lands inside the cap. Anything smaller would make the ordering this task is
81
+ * about depend on where in the sweep cycle the lapse fell.
82
+ *
83
+ * The common case, and not a guarantee. `Daemon.tick` skips a sweep when the
84
+ * previous tick is still running, and the request record's `ts` is stamped at
85
+ * the write boundary, some intake latency after the harness spawned the hook,
86
+ * so the cap runs from an instant slightly before the one the margin counts
87
+ * from. An overrunning tick plus a slow intake can put the `approval.expired`
88
+ * record after the kill. What keeps a late tap safe regardless is not this
89
+ * margin but the lazy refusal: `core/state.ts` judges the lapse by arithmetic
90
+ * whether or not the record exists, so `decide` refuses the tap `expired` and
91
+ * materialises the record then. The margin buys the ORDERING in the common
92
+ * case, so the two records agree without a human ever seeing the disagreement;
93
+ * the refusal is what makes the grant impossible in every case.
94
+ *
95
+ * It is a floor on the cap as well as a subtraction from it. A cap at or below
96
+ * the margin leaves no window in which a human could answer at all: `cli/hook.ts`
97
+ * refuses such a configuration rather than opening a question that is already
98
+ * dead, and `schema/event.schema.json` refuses a record carrying one at the
99
+ * write boundary (its `minimum` is this constant plus one, pinned equal by
100
+ * `tests/harness-cap-ttl.test.ts`).
101
+ */
102
+ export const HARNESS_CAP_MARGIN_MS = 60_000;
103
+ /**
104
+ * The deadline a hook-originated request is judged by: the SHORTER of the
105
+ * policy's TTL and the harness's cap minus {@link HARNESS_CAP_MARGIN_MS}
106
+ * (APRV-423).
107
+ *
108
+ * `null` means no deadline, exactly as `defaults.approval_ttl`'s absence means
109
+ * it (SPEC.md §6): nothing lapses, and no duration is invented for a policy
110
+ * that declared none.
111
+ *
112
+ * **The cap may only shorten.** It is a self-reported field (the hook states
113
+ * the ceiling its own harness runs it under), and SPEC.md §11.1 invariant 4
114
+ * says such a field never reduces scrutiny. Recording it raw and taking the
115
+ * minimum HERE is what makes that structural: a cap larger than the policy TTL
116
+ * changes nothing, a cap of an hour under a one-minute TTL changes nothing, and
117
+ * the only direction a requester can move its own deadline is earlier. The same
118
+ * shape as `core/gate-window.ts`'s `expires_at`, which clamps a claimed window
119
+ * against the derived one for the same reason.
120
+ *
121
+ * A cap that does not clear the margin yields `0`, which reads as lapsed from
122
+ * the instant the request is written. That is the fail-closed answer for a
123
+ * harness whose ceiling leaves no room for a human, and it is unreachable
124
+ * through the write path: the hook refuses the configuration before it
125
+ * appends, and the event schema refuses the record for any caller that did
126
+ * not come through the hook.
127
+ */
128
+ export function harnessCappedTtlMs(policyTtlMs, capMs) {
129
+ if (capMs === null)
130
+ return policyTtlMs;
131
+ const fromCap = Math.max(0, capMs - HARNESS_CAP_MARGIN_MS);
132
+ return policyTtlMs === null ? fromCap : Math.min(policyTtlMs, fromCap);
133
+ }
134
+ /**
135
+ * Does `capMs` leave a window a human could answer in?
136
+ *
137
+ * The one question `cli/hook.ts` asks before it opens a request under a cap.
138
+ * Strictly greater: a cap EQUAL to the margin yields a zero-length window,
139
+ * which is a question that expires as it is asked.
140
+ */
141
+ export function harnessCapFitsMargin(capMs) {
142
+ return capMs > HARNESS_CAP_MARGIN_MS;
143
+ }
58
144
  //# sourceMappingURL=harness-wait.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness-wait.js","sourceRoot":"","sources":["../../../src/core/harness-wait.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC,iDAAiD;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,MAAM,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,OAAe;IAC9D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC"}
1
+ {"version":3,"file":"harness-wait.js","sourceRoot":"","sources":["../../../src/core/harness-wait.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC,iDAAiD;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,MAAM,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,OAAe;IAC9D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAA0B,EAC1B,KAAoB;IAEpB,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,qBAAqB,CAAC,CAAC;IAC3D,OAAO,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAO,KAAK,GAAG,qBAAqB,CAAC;AACvC,CAAC"}
@@ -25,6 +25,27 @@ export interface LogSubscriptionOptions {
25
25
  signal?: AbortSignal;
26
26
  /** Bounded fallback when filesystem notifications are missing. */
27
27
  pollIntervalMs?: number;
28
+ /**
29
+ * Drain the currently verified snapshot and RETURN, rather than waiting for
30
+ * appends (APRV-421).
31
+ *
32
+ * The streaming form is what `approval log follow` wants: a foreground
33
+ * process that stays. A PAGED reader wants the opposite and has nowhere to
34
+ * put a wait. `approval serve`'s `log/follow` answers one request with the
35
+ * records after a cursor and the cursor they end at, and the caller comes
36
+ * back with that cursor when it wants more. Expressing "we are caught up" as
37
+ * a race between the iterator and a timer is a timing heuristic; expressing
38
+ * it here is one flag, because this module already knows exactly when the
39
+ * verified snapshot is exhausted.
40
+ *
41
+ * It changes nothing about what is emitted or what is refused. The
42
+ * genesis-to-head verification, the cursor binding, the retained hash and
43
+ * every terminal failure are the ones below; this decides only whether the
44
+ * generator waits after the drain or ends. No watcher is established either,
45
+ * which matters for the caller this exists for: a server that left one file
46
+ * watcher behind per request would leak handles at the rate it is polled.
47
+ */
48
+ once?: boolean;
28
49
  }
29
50
  /**
30
51
  * Yield verified records after an exclusive cursor, then wait for appends.
@@ -86,12 +86,16 @@ export async function* subscribeVerifiedLog(logPath, options = {}) {
86
86
  options.signal?.addEventListener("abort", abort, { once: false });
87
87
  // Watch the directory so creation of an initially absent log is visible.
88
88
  // Failure to establish a watch is harmless: bounded polling remains active.
89
- try {
90
- watcher = watch(dirname(logPath), { persistent: false }, hint);
91
- watcher.on("error", hint);
92
- }
93
- catch {
94
- watcher = null;
89
+ // A `once` subscription never reaches the wait below, so it establishes no
90
+ // watch at all rather than opening and closing one per call.
91
+ if (options.once !== true) {
92
+ try {
93
+ watcher = watch(dirname(logPath), { persistent: false }, hint);
94
+ watcher.on("error", hint);
95
+ }
96
+ catch {
97
+ watcher = null;
98
+ }
95
99
  }
96
100
  try {
97
101
  while (!options.signal?.aborted) {
@@ -131,6 +135,12 @@ export async function* subscribeVerifiedLog(logPath, options = {}) {
131
135
  }
132
136
  if (options.signal?.aborted)
133
137
  return;
138
+ // The verified snapshot is exhausted and this caller asked for exactly
139
+ // that much (APRV-421). Everything above ran unchanged: the chain was
140
+ // verified from genesis, the cursor was bound, and a mismatch already
141
+ // threw. What is skipped is only the waiting.
142
+ if (options.once === true)
143
+ return;
134
144
  // An append between watcher setup/read/drain and this point changes the
135
145
  // version and causes an immediate verification instead of a lost wakeup.
136
146
  if (wakeVersion !== versionBeforeRead)
@@ -1 +1 @@
1
- {"version":3,"file":"log-subscribe.js","sourceRoot":"","sources":["../../../src/core/log-subscribe.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,EAAkB,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,UAAU,EAA4B,MAAM,aAAa,CAAC;AAInE,gFAAgF;AAChF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,IAAI,CAA6B;IACjC,MAAM,CAAiD;IAEhE,YACE,IAAgC,EAChC,OAAe,EACf,MAAM,GAAmD,IAAI;QAE7D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAaD,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAErC,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,oBAAoB,CAAC,IAAI,EAAE,OAAO,OAAO,uBAAuB,MAAM,EAAE,CAAC,CAAC;IACtF,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QAC3C,MAAM,IAAI,oBAAoB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACzC,MAAM,IAAI,oBAAoB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,QAAQ,CAAC,OAAO,CAAC;AAC1B,CAAC;AAED,SAAS,eAAe,CAAC,OAA+B;IAKtD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,iDAAiD,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,IAAI,IAAI,KAAK,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAC1F,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,SAAS,CAAC,8DAA8D,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IACD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,wBAAwB,CAAC;IAC1E,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CACjB,uDAAuD,MAAM,CAAC,cAAc,CAAC,EAAE,CAChF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,cAAc,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,oBAAoB,CACzC,OAAe,EACf,OAAO,GAA2B,EAAE;IAEpC,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC;IAC5B,IAAI,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC;IACrC,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,IAAI,GAAwB,IAAI,CAAC;IACrC,IAAI,OAAO,GAAqB,IAAI,CAAC;IACrC,IAAI,KAAK,GAAyC,IAAI,CAAC;IAEvD,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,WAAW,IAAI,CAAC,CAAC;QACjB,MAAM,OAAO,GAAG,IAAI,CAAC;QACrB,IAAI,GAAG,IAAI,CAAC;QACZ,OAAO,EAAE,EAAE,CAAC;IACd,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,GAAS,EAAE,CAAC,IAAI,EAAE,CAAC;IACjC,OAAO,CAAC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAElE,yEAAyE;IACzE,4EAA4E;IAC5E,IAAI,CAAC;QACH,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAC/D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IAED,IAAI,CAAC;QACH,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YAChC,MAAM,iBAAiB,GAAG,WAAW,CAAC;YACtC,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;YAEzC,IAAI,SAAS,GAAG,OAAO,EAAE,CAAC;gBACxB,MAAM,IAAI,oBAAoB,CAC5B,WAAW,EACX,OAAO,OAAO,gBAAgB,MAAM,CAAC,OAAO,CAAC,oCAAoC,MAAM,CAAC,SAAS,CAAC,mEAAmE,EACrK,iBAAiB,CAClB,CAAC;YACJ,CAAC;YACD,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAClB,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;gBACtC,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,EAAE,CAAC;oBACrF,MAAM,IAAI,oBAAoB,CAC5B,WAAW,EACX,OAAO,OAAO,2CAA2C,MAAM,CAAC,SAAS,CAAC,GACxE,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,UAAU,EACrD,0EAA0E,EAC1E,iBAAiB,CAClB,CAAC;gBACJ,CAAC;gBACD,oEAAoE;gBACpE,mEAAmE;gBACnE,uEAAuE;gBACvE,UAAU,KAAK,MAAM,CAAC,IAAI,CAAC;YAC7B,CAAC;YAED,yEAAyE;YACzE,wEAAwE;YACxE,KAAK,IAAI,KAAK,GAAG,SAAS,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;gBAC/D,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;oBAAE,OAAO;gBACpC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC9B,IAAI,MAAM,KAAK,SAAS;oBAAE,MAAM;gBAChC,iEAAiE;gBACjE,qEAAqE;gBACrE,uEAAuE;gBACvE,kCAAkC;gBAClC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC;gBAC/B,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC;gBACjC,MAAM,MAAM,CAAC;gBACb,SAAS,GAAG,WAAW,CAAC;gBACxB,UAAU,GAAG,YAAY,CAAC;YAC5B,CAAC;YAED,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;gBAAE,OAAO;YACpC,wEAAwE;YACxE,yEAAyE;YACzE,IAAI,WAAW,KAAK,iBAAiB;gBAAE,SAAS;YAEhD,MAAM,iBAAiB,GAAG,WAAW,CAAC;YACtC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBAClC,IAAI,GAAG,OAAO,CAAC;gBACf,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;gBAChD,mEAAmE;gBACnE,qEAAqE;gBACrE,gEAAgE;gBAChE,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,IAAI,WAAW,KAAK,iBAAiB;oBAAE,IAAI,EAAE,CAAC;YAC3E,CAAC,CAAC,CAAC;YACH,IAAI,KAAK,KAAK,IAAI;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YACxC,KAAK,GAAG,IAAI,CAAC;YACb,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;IACH,CAAC;YAAS,CAAC;QACT,IAAI,KAAK,KAAK,IAAI;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,GAAG,IAAI,CAAC;QACZ,OAAO,EAAE,KAAK,EAAE,CAAC;QACjB,OAAO,CAAC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"log-subscribe.js","sourceRoot":"","sources":["../../../src/core/log-subscribe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The filesystem calls that put durable bytes on disk: `core/log.ts`'s append
3
+ * (APRV-440) and `core/payload-store.ts`'s atomic write (APRV-457). They are
4
+ * kept in their own module so that `core/log.ts`'s public surface stays what
5
+ * `tests/log.test.ts` pins: no mutation, no reorder, no truncate, and no way to
6
+ * change how its bytes reach the disk.
7
+ *
8
+ * Why a seam at all. The durability claim is "fsync after the write and before
9
+ * ok", and a page cache that was flushed reads back exactly like one that was
10
+ * not until the machine dies. Only a layer that records the ORDER of the calls
11
+ * can prove the claim, so `tests/log-fsync.test.ts` and
12
+ * `tests/payload-fsync.test.ts` install one that records every call and then
13
+ * delegates to the real one, which keeps each log they build a real log built
14
+ * through the real append path.
15
+ *
16
+ * One seam for both writers, deliberately. A log record is only as durable as
17
+ * the payload it binds: a record that survives a crash while its payload comes
18
+ * back NUL-filled verifies and points at nothing (APRV-457). Proving the two
19
+ * through one layer keeps one model of "what reached the disk" for both.
20
+ *
21
+ * Nothing in `src/` calls {@link setAppendWriteLayerForTests}. It changes how
22
+ * the bytes reach the disk and nothing about which bytes, so every check before
23
+ * the write (lock, tail, compare-and-append, schema, payload hashing) runs as in
24
+ * production.
25
+ */
26
+ export interface AppendWriteLayer {
27
+ /** `open(2)` with numeric flags; throws like `fs.openSync`. */
28
+ open(path: string, flags: number, mode: number): number;
29
+ /** One `write(2)` of the whole buffer; returns the bytes written. */
30
+ write(fd: number, data: Buffer): number;
31
+ /** `fsync(2)` of a file or directory descriptor; throws on failure. */
32
+ fsync(fd: number): void;
33
+ close(fd: number): void;
34
+ /**
35
+ * `rename(2)`; throws like `fs.renameSync`. Used by the payload store's
36
+ * temp-then-rename write (APRV-457); the log append never renames.
37
+ */
38
+ rename(from: string, to: string): void;
39
+ }
40
+ /** The layer the next append (or payload store write) uses. */
41
+ export declare function appendWriteLayer(): AppendWriteLayer;
42
+ /**
43
+ * Replace the append's write layer, or restore Node's with `null`. For tests
44
+ * and the opt-in benchmarks only.
45
+ */
46
+ export declare function setAppendWriteLayerForTests(layer: AppendWriteLayer | null): void;
47
+ /**
48
+ * fsync a directory through the current layer, so a name created or renamed
49
+ * into it survives a crash. Skipped on Windows (a directory cannot be opened
50
+ * for this there, and its filesystems journal names themselves) and tolerated
51
+ * where the filesystem reports directory fsync as unsupported; every other
52
+ * failure throws for the caller to report. The read-only handle is closed even
53
+ * when the fsync throws.
54
+ */
55
+ export declare function fsyncDirectory(dir: string): void;