approval-md 0.3.0 → 0.4.1

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 (270) hide show
  1. package/README.md +83 -914
  2. package/SPEC.md +101 -13
  3. package/cli.js +134 -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 +72 -0
  8. package/dist/src/channels/contract.js +161 -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.d.ts +25 -0
  17. package/dist/src/channels/tagging.js +63 -6
  18. package/dist/src/channels/tagging.js.map +1 -1
  19. package/dist/src/channels/telegram-minimal.d.ts +251 -0
  20. package/dist/src/channels/telegram-minimal.js +618 -0
  21. package/dist/src/channels/telegram-minimal.js.map +1 -0
  22. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  23. package/dist/src/channels/telegram-webhook.js +536 -0
  24. package/dist/src/channels/telegram-webhook.js.map +1 -0
  25. package/dist/src/channels/telegram.d.ts +725 -20
  26. package/dist/src/channels/telegram.js +1332 -123
  27. package/dist/src/channels/telegram.js.map +1 -1
  28. package/dist/src/cli/amend.js +358 -5
  29. package/dist/src/cli/amend.js.map +1 -1
  30. package/dist/src/cli/attest.js +43 -6
  31. package/dist/src/cli/attest.js.map +1 -1
  32. package/dist/src/cli/audit-card.d.ts +1 -1
  33. package/dist/src/cli/audit-card.js +40 -1
  34. package/dist/src/cli/audit-card.js.map +1 -1
  35. package/dist/src/cli/audit.d.ts +7 -5
  36. package/dist/src/cli/audit.js +22 -10
  37. package/dist/src/cli/audit.js.map +1 -1
  38. package/dist/src/cli/channel-relay.d.ts +108 -0
  39. package/dist/src/cli/channel-relay.js +334 -0
  40. package/dist/src/cli/channel-relay.js.map +1 -0
  41. package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
  42. package/dist/src/cli/channel-telegram-webhook.js +716 -0
  43. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  44. package/dist/src/cli/channel-telegram.d.ts +347 -28
  45. package/dist/src/cli/channel-telegram.js +648 -56
  46. package/dist/src/cli/channel-telegram.js.map +1 -1
  47. package/dist/src/cli/channel.d.ts +1 -1
  48. package/dist/src/cli/channel.js +5 -1
  49. package/dist/src/cli/channel.js.map +1 -1
  50. package/dist/src/cli/daemon.js +37 -5
  51. package/dist/src/cli/daemon.js.map +1 -1
  52. package/dist/src/cli/doctor.d.ts +63 -0
  53. package/dist/src/cli/doctor.js +856 -111
  54. package/dist/src/cli/doctor.js.map +1 -1
  55. package/dist/src/cli/execute.d.ts +29 -1
  56. package/dist/src/cli/execute.js +387 -21
  57. package/dist/src/cli/execute.js.map +1 -1
  58. package/dist/src/cli/exit-codes.d.ts +12 -0
  59. package/dist/src/cli/exit-codes.js +13 -0
  60. package/dist/src/cli/exit-codes.js.map +1 -1
  61. package/dist/src/cli/gate.d.ts +2 -0
  62. package/dist/src/cli/gate.js +233 -2
  63. package/dist/src/cli/gate.js.map +1 -1
  64. package/dist/src/cli/help.d.ts +10 -1
  65. package/dist/src/cli/help.js +283 -54
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook.d.ts +485 -6
  68. package/dist/src/cli/hook.js +2370 -70
  69. package/dist/src/cli/hook.js.map +1 -1
  70. package/dist/src/cli/init.js +16 -7
  71. package/dist/src/cli/init.js.map +1 -1
  72. package/dist/src/cli/instructions.js +24 -3
  73. package/dist/src/cli/instructions.js.map +1 -1
  74. package/dist/src/cli/log-anchor.d.ts +55 -0
  75. package/dist/src/cli/log-anchor.js +199 -60
  76. package/dist/src/cli/log-anchor.js.map +1 -1
  77. package/dist/src/cli/log-unlock.d.ts +23 -0
  78. package/dist/src/cli/log-unlock.js +109 -0
  79. package/dist/src/cli/log-unlock.js.map +1 -0
  80. package/dist/src/cli/main.d.ts +0 -12
  81. package/dist/src/cli/main.js +72 -4
  82. package/dist/src/cli/main.js.map +1 -1
  83. package/dist/src/cli/muse.d.ts +2 -0
  84. package/dist/src/cli/muse.js +62 -0
  85. package/dist/src/cli/muse.js.map +1 -0
  86. package/dist/src/cli/payload.js +79 -1
  87. package/dist/src/cli/payload.js.map +1 -1
  88. package/dist/src/cli/scaffold.js +6 -0
  89. package/dist/src/cli/scaffold.js.map +1 -1
  90. package/dist/src/cli/serve.d.ts +53 -0
  91. package/dist/src/cli/serve.js +270 -0
  92. package/dist/src/cli/serve.js.map +1 -0
  93. package/dist/src/cli/up.js +26 -2
  94. package/dist/src/cli/up.js.map +1 -1
  95. package/dist/src/cli/verb-registry.d.ts +2 -0
  96. package/dist/src/cli/verb-registry.js +338 -39
  97. package/dist/src/cli/verb-registry.js.map +1 -1
  98. package/dist/src/cli/wordmark.d.ts +2 -2
  99. package/dist/src/cli/wordmark.js +2 -2
  100. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  101. package/dist/src/core/attest-bootstrap.js +135 -0
  102. package/dist/src/core/attest-bootstrap.js.map +1 -0
  103. package/dist/src/core/attest.d.ts +20 -3
  104. package/dist/src/core/attest.js +25 -0
  105. package/dist/src/core/attest.js.map +1 -1
  106. package/dist/src/core/audit.d.ts +153 -12
  107. package/dist/src/core/audit.js +415 -20
  108. package/dist/src/core/audit.js.map +1 -1
  109. package/dist/src/core/channel-lease.d.ts +322 -0
  110. package/dist/src/core/channel-lease.js +663 -0
  111. package/dist/src/core/channel-lease.js.map +1 -0
  112. package/dist/src/core/command-class.d.ts +51 -0
  113. package/dist/src/core/command-class.js +766 -8
  114. package/dist/src/core/command-class.js.map +1 -1
  115. package/dist/src/core/daemon-host.d.ts +178 -0
  116. package/dist/src/core/daemon-host.js +228 -0
  117. package/dist/src/core/daemon-host.js.map +1 -0
  118. package/dist/src/core/daemon-identity.d.ts +238 -0
  119. package/dist/src/core/daemon-identity.js +252 -0
  120. package/dist/src/core/daemon-identity.js.map +1 -0
  121. package/dist/src/core/gate.d.ts +364 -6
  122. package/dist/src/core/gate.js +802 -27
  123. package/dist/src/core/gate.js.map +1 -1
  124. package/dist/src/core/harness-version.d.ts +128 -4
  125. package/dist/src/core/harness-version.js +179 -12
  126. package/dist/src/core/harness-version.js.map +1 -1
  127. package/dist/src/core/harness-wait.d.ts +82 -3
  128. package/dist/src/core/harness-wait.js +89 -3
  129. package/dist/src/core/harness-wait.js.map +1 -1
  130. package/dist/src/core/log-lock.d.ts +435 -0
  131. package/dist/src/core/log-lock.js +1093 -0
  132. package/dist/src/core/log-lock.js.map +1 -0
  133. package/dist/src/core/log-subscribe.d.ts +21 -0
  134. package/dist/src/core/log-subscribe.js +16 -6
  135. package/dist/src/core/log-subscribe.js.map +1 -1
  136. package/dist/src/core/log-write-layer.d.ts +55 -0
  137. package/dist/src/core/log-write-layer.js +91 -0
  138. package/dist/src/core/log-write-layer.js.map +1 -0
  139. package/dist/src/core/log.d.ts +130 -22
  140. package/dist/src/core/log.js +473 -50
  141. package/dist/src/core/log.js.map +1 -1
  142. package/dist/src/core/payload-census.d.ts +51 -0
  143. package/dist/src/core/payload-census.js +125 -2
  144. package/dist/src/core/payload-census.js.map +1 -1
  145. package/dist/src/core/payload-store.d.ts +31 -0
  146. package/dist/src/core/payload-store.js +113 -8
  147. package/dist/src/core/payload-store.js.map +1 -1
  148. package/dist/src/core/payload.d.ts +18 -3
  149. package/dist/src/core/payload.js +20 -4
  150. package/dist/src/core/payload.js.map +1 -1
  151. package/dist/src/core/policy-diff.d.ts +10 -0
  152. package/dist/src/core/policy-diff.js +10 -2
  153. package/dist/src/core/policy-diff.js.map +1 -1
  154. package/dist/src/core/policy-explain.d.ts +7 -1
  155. package/dist/src/core/policy-explain.js +16 -1
  156. package/dist/src/core/policy-explain.js.map +1 -1
  157. package/dist/src/core/policy-load.d.ts +24 -0
  158. package/dist/src/core/policy-load.js.map +1 -1
  159. package/dist/src/core/policy-match.d.ts +38 -0
  160. package/dist/src/core/policy-match.js +35 -0
  161. package/dist/src/core/policy-match.js.map +1 -1
  162. package/dist/src/core/policy-proposal.d.ts +40 -0
  163. package/dist/src/core/policy-proposal.js +37 -16
  164. package/dist/src/core/policy-proposal.js.map +1 -1
  165. package/dist/src/core/prompt-layout.d.ts +186 -1
  166. package/dist/src/core/prompt-layout.js +345 -3
  167. package/dist/src/core/prompt-layout.js.map +1 -1
  168. package/dist/src/core/read-scope.d.ts +10 -0
  169. package/dist/src/core/read-scope.js +4 -0
  170. package/dist/src/core/read-scope.js.map +1 -1
  171. package/dist/src/core/run-payload.d.ts +160 -0
  172. package/dist/src/core/run-payload.js +304 -0
  173. package/dist/src/core/run-payload.js.map +1 -0
  174. package/dist/src/core/sender-identity.d.ts +59 -11
  175. package/dist/src/core/sender-identity.js +79 -11
  176. package/dist/src/core/sender-identity.js.map +1 -1
  177. package/dist/src/core/state.d.ts +52 -1
  178. package/dist/src/core/state.js +52 -6
  179. package/dist/src/core/state.js.map +1 -1
  180. package/dist/src/core/telegram-config.d.ts +79 -0
  181. package/dist/src/core/telegram-config.js +108 -0
  182. package/dist/src/core/telegram-config.js.map +1 -1
  183. package/dist/src/core/validate.d.ts +20 -6
  184. package/dist/src/core/validate.js +0 -0
  185. package/dist/src/core/validate.js.map +1 -1
  186. package/dist/src/core/verify.d.ts +23 -1
  187. package/dist/src/core/verify.js +20 -2
  188. package/dist/src/core/verify.js.map +1 -1
  189. package/dist/src/daemon/audit.d.ts +80 -5
  190. package/dist/src/daemon/audit.js +126 -6
  191. package/dist/src/daemon/audit.js.map +1 -1
  192. package/dist/src/daemon/daemon.d.ts +174 -2
  193. package/dist/src/daemon/daemon.js +298 -7
  194. package/dist/src/daemon/daemon.js.map +1 -1
  195. package/dist/src/daemon/git-evidence.d.ts +2 -2
  196. package/dist/src/daemon/git-evidence.js +1 -1
  197. package/dist/src/daemon/projection.d.ts +13 -2
  198. package/dist/src/daemon/projection.js +39 -6
  199. package/dist/src/daemon/projection.js.map +1 -1
  200. package/dist/src/mcp/server.d.ts +39 -3
  201. package/dist/src/mcp/server.js +22 -3
  202. package/dist/src/mcp/server.js.map +1 -1
  203. package/dist/src/muse/server.d.ts +16 -0
  204. package/dist/src/muse/server.js +238 -0
  205. package/dist/src/muse/server.js.map +1 -0
  206. package/dist/src/serve/archive.d.ts +166 -0
  207. package/dist/src/serve/archive.js +363 -0
  208. package/dist/src/serve/archive.js.map +1 -0
  209. package/dist/src/serve/arguments.d.ts +93 -0
  210. package/dist/src/serve/arguments.js +228 -0
  211. package/dist/src/serve/arguments.js.map +1 -0
  212. package/dist/src/serve/credentials.d.ts +91 -0
  213. package/dist/src/serve/credentials.js +143 -0
  214. package/dist/src/serve/credentials.js.map +1 -0
  215. package/dist/src/serve/follow.d.ts +86 -0
  216. package/dist/src/serve/follow.js +101 -0
  217. package/dist/src/serve/follow.js.map +1 -0
  218. package/dist/src/serve/hook-thread.d.ts +134 -0
  219. package/dist/src/serve/hook-thread.js +333 -0
  220. package/dist/src/serve/hook-thread.js.map +1 -0
  221. package/dist/src/serve/hook-worker.d.ts +134 -0
  222. package/dist/src/serve/hook-worker.js +173 -0
  223. package/dist/src/serve/hook-worker.js.map +1 -0
  224. package/dist/src/serve/server.d.ts +412 -0
  225. package/dist/src/serve/server.js +1251 -0
  226. package/dist/src/serve/server.js.map +1 -0
  227. package/docs/README-extended.md +768 -0
  228. package/docs/cli-reference.md +2139 -67
  229. package/package.json +4 -2
  230. package/schema/event.schema.json +413 -30
  231. package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
  232. package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
  233. package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
  234. package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
  235. package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
  236. package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
  237. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
  238. package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
  239. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
  240. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
  241. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
  242. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
  243. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
  244. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
  245. package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
  246. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  247. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  248. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  249. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  250. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  251. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  252. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  253. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  254. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  255. package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
  256. package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
  257. package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
  258. package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
  259. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
  260. package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
  261. package/schema/fixtures/event/valid/audit-sampled.json +2 -1
  262. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  263. package/schema/fixtures/event/valid/daemon-identity.json +16 -0
  264. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  265. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  266. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  267. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  268. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  269. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  270. package/schema/policy.schema.json +102 -6
@@ -14,7 +14,10 @@
14
14
  * and subtracts the subjects already sampled. A tick that samples nothing new is
15
15
  * the normal case; a restarted daemon, a second daemon, and an operator running
16
16
  * a sweep by hand all converge on exactly the same set. This module therefore
17
- * holds no state at all — no seen-set, no cursor, no cache.
17
+ * holds no state that any APPEND depends on: no seen-set, no cursor, no cache.
18
+ * The two process-lifetime sets below are output bookkeeping (which notice has
19
+ * been printed, which deferral was promised), and every decision about what to
20
+ * append is made from the log on every call.
18
21
  *
19
22
  * ## Reporting, and why it goes through `warning`
20
23
  *
@@ -27,6 +30,58 @@
27
30
  * its `sampled` line. Both channels remain the caller's: this module names no
28
31
  * event and writes to no stream.
29
32
  *
33
+ * ## A deferred sample is not a lost one (APRV-381)
34
+ *
35
+ * On 2026-09-19 a tick printed `append-refused audit sampling: append-failed:
36
+ * audit.sampled for hook:…:vcs.push.main was not appended (lock-timeout)` while a
37
+ * lane's hook append and a records advance were writing the same log. Nothing was
38
+ * lost, and the next ticks appended the sample; the operator reading the window
39
+ * had no way to know that, because the line said an audit record had not been
40
+ * written and stopped there.
41
+ *
42
+ * So the two outcomes are now said differently. `core/audit.ts` classifies
43
+ * `lock-timeout` and `head-moved` as transient (see its `TRANSIENT_APPEND_CODES`)
44
+ * and hands them back on `deferred`; this module reports each on {@link
45
+ * AuditSweepOptions.defer} with the action key and the fact that the next tick
46
+ * retries it, and reports everything else on {@link AuditSweepOptions.warn} in
47
+ * the form it always had. When the caller passes no `defer` sink the deferral
48
+ * goes to `warn` instead: a deferral nobody prints is indistinguishable, to the
49
+ * person who has to trust this log, from a sample that was dropped.
50
+ *
51
+ * The retry is then named as one. `pendingSamples` re-derives pendency from the
52
+ * verified log, so the retry needs no memory to HAPPEN; saying "this is the retry
53
+ * of what I deferred" is a fact about this process's own output, and it is kept
54
+ * here as such (see {@link rememberDeferral}).
55
+ *
56
+ * ## The lock wait: skip and retry, at `core/log.ts`'s own 2000 ms (APRV-381)
57
+ *
58
+ * The daemon is the one writer that could afford to wait longer, and it
59
+ * deliberately does not. Four reasons, recorded here because the number itself is
60
+ * a one-line change and the reasoning is not:
61
+ *
62
+ * 1. A tick is serial. This sweep sits between the TTL sweep and the queue
63
+ * render, so every millisecond it blocks is a millisecond an expiry line, a
64
+ * `state:` write-back and `QUEUE.md` wait. A longer wait spends latency a
65
+ * person notices on a record that costs nothing to postpone.
66
+ * 2. The contenders hold the lock for spans no polite wait covers. `approval log
67
+ * advance` holds it across a whole verify-and-commit, `approval log sync`
68
+ * across a baseline move. A wait long enough to win those races reliably is a
69
+ * wait long enough to stall the loop for seconds, and it would still time out
70
+ * sometimes, so the transient path has to exist either way.
71
+ * 3. Deferring loses nothing, provably. The sample's pendency is a property of
72
+ * the log (eligible, selected, no `audit.sampled` yet), not of this process, so
73
+ * the retry bound is the tick interval, and an append by anyone else also wakes
74
+ * the log watcher, which usually makes it the next debounce.
75
+ * 4. The appends this sweep contends with are the ones somebody is waiting on: a
76
+ * hook's gate verdict, a lane's execution record, an advance's commit. The
77
+ * writer with nobody blocked on it is the writer that should yield.
78
+ *
79
+ * "Skip" here means letting the append refuse and classifying the refusal. It
80
+ * explicitly does NOT mean peeking at `events.jsonl.lock` before sweeping: that
81
+ * read can be wrong in both directions (a lock released a microsecond later, a
82
+ * lock taken a microsecond after the peek), and `appendEvent`'s own acquisition
83
+ * under the lock is the only authority on whether an append can be made.
84
+ *
30
85
  * ## A disabled sampler is not a failure
31
86
  *
32
87
  * When no sampling secret is configured, `core/sampler.ts` returns a disabled
@@ -39,7 +94,7 @@
39
94
  * operator goes to ask what is switched on. See `core/sampler.ts` for why an
40
95
  * unconfigured sampler disables sampling rather than escalating everything.
41
96
  */
42
- import { sampleSupervised } from "../core/audit.js";
97
+ import { sampleSupervised, } from "../core/audit.js";
43
98
  /**
44
99
  * Disablement reasons already reported in this process, so a standing
45
100
  * configuration fact is stated once rather than every tick. Process-lifetime and
@@ -47,9 +102,52 @@ import { sampleSupervised } from "../core/audit.js";
47
102
  * losing it costs one extra warning line and nothing else.
48
103
  */
49
104
  const reported = new Set();
50
- /** Reset the once-per-process notice. Exported for tests, used nowhere else. */
105
+ /**
106
+ * Samples this process deferred and has not yet seen appended (APRV-381), keyed
107
+ * by log path and subject hash.
108
+ *
109
+ * Exactly the same standing as {@link reported}: process-lifetime, memory-only,
110
+ * and a de-duplicator for OUTPUT. Nothing here is consulted before an append,
111
+ * gates nothing, and suppresses no line. Losing it (a restart, a second daemon,
112
+ * an operator sweeping by hand) costs the word "retry" on one line and nothing
113
+ * else, which is why the retry itself is derived from the log and never from
114
+ * here.
115
+ *
116
+ * Bounded so a long run cannot accumulate: an entry is dropped when its sample
117
+ * lands, and the oldest is evicted past {@link DEFERRAL_MEMORY_LIMIT} for the
118
+ * candidate that is deferred and then stops being a candidate (a policy edit that
119
+ * drops its class, a rate that no longer selects it). A `Set` iterates in
120
+ * insertion order, so "the oldest" needs no timestamp.
121
+ */
122
+ const deferrals = new Set();
123
+ /** How many deferred samples this process will remember for the retry wording. */
124
+ const DEFERRAL_MEMORY_LIMIT = 256;
125
+ function deferralKey(logPath, subjectHash) {
126
+ return JSON.stringify([logPath, subjectHash]);
127
+ }
128
+ function rememberDeferral(logPath, subjectHash) {
129
+ const key = deferralKey(logPath, subjectHash);
130
+ if (deferrals.has(key))
131
+ return;
132
+ if (deferrals.size >= DEFERRAL_MEMORY_LIMIT) {
133
+ const oldest = deferrals.values().next();
134
+ if (!oldest.done)
135
+ deferrals.delete(oldest.value);
136
+ }
137
+ deferrals.add(key);
138
+ }
139
+ /** Whether this process deferred this sample, consuming the memory of it. */
140
+ function takeDeferral(logPath, subjectHash) {
141
+ return deferrals.delete(deferralKey(logPath, subjectHash));
142
+ }
143
+ /**
144
+ * Reset the once-per-process notice and the deferral memory. Exported for tests,
145
+ * used nowhere else: both are output bookkeeping, so a suite that shares a
146
+ * process needs to be able to start from nothing.
147
+ */
51
148
  export function resetAuditSweepNotices() {
52
149
  reported.clear();
150
+ deferrals.clear();
53
151
  }
54
152
  /** One sampling sweep. Appends through `core/audit.ts` and decides nothing. */
55
153
  export function sweepAuditSampling(options) {
@@ -80,11 +178,33 @@ export function sweepAuditSampling(options) {
80
178
  for (const refusal of result.refusals) {
81
179
  options.warn(`audit sampling: ${refusal.code}: ${refusal.message}`);
82
180
  }
181
+ // The transient half (APRV-381). Said BEFORE the successes of this same sweep
182
+ // are said, because the sweep stops at the first deferral: anything it did
183
+ // append, it appended before the contention, and the reading order should match
184
+ // the order it happened in.
185
+ for (const entry of result.deferred) {
186
+ rememberDeferral(options.logPath, entry.candidate.hash);
187
+ (options.defer ?? options.warn)(deferralLine(entry));
188
+ }
83
189
  const sampled = options.sampled;
84
- if (sampled !== undefined) {
85
- for (const entry of result.appended)
86
- sampled(entry);
190
+ for (const entry of result.appended) {
191
+ // Consumed whether or not anyone is listening, so a caller with no `sampled`
192
+ // sink cannot leave a stale deferral behind to mislabel a later sample.
193
+ const retry = takeDeferral(options.logPath, entry.candidate.hash);
194
+ if (sampled !== undefined)
195
+ sampled(entry, retry);
87
196
  }
88
197
  return { sampled: result.appended.length, disabled: null };
89
198
  }
199
+ /**
200
+ * The deferral line: what was not appended, why, and that it is coming.
201
+ *
202
+ * "retries it on the next tick" is load-bearing wording. The line an
203
+ * operator read before APRV-381 ended at "was not appended", which is true and
204
+ * reads as a loss; this one has to answer the question that sentence provokes
205
+ * before they go looking for a missing audit record.
206
+ */
207
+ function deferralLine(entry) {
208
+ return `audit sampling deferred ${entry.candidate.actionKey} (${entry.append.code}): ${entry.append.message} The sample is NOT lost: it is still pending in the log's own terms (eligible, drawn, no audit.sampled yet), and the sweep retries it on the next tick.`;
209
+ }
90
210
  //# sourceMappingURL=audit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/daemon/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,gBAAgB,EAA0C,MAAM,kBAAkB,CAAC;AA0C5F;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;AAEnC,gFAAgF;AAChF,MAAM,UAAU,sBAAsB;IACpC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAC,OAA0B;IAC3D,MAAM,YAAY,GAAiB,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;IAC9D,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;QAAE,YAAY,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IAChF,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,YAAY,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IACpE,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS;QAAE,YAAY,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IAE9D,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAE5E,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,4EAA4E;QAC5E,2EAA2E;QAC3E,4EAA4E;QAC5E,OAAO,CAAC,IAAI,CACV,gDAAgD,MAAM,CAAC,IAAI,MAAM,MAAM,CAAC,OAAO,EAAE,CAClF,CAAC;QACF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACxC,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClB,MAAM,CACJ,0BAA0B,MAAM,CAAC,OAAO,CAAC,MAAM,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,2KAA2K,CACvP,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IACzD,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,mBAAmB,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/daemon/audit.ts"],"names":[],"mappings":"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"}
@@ -104,6 +104,12 @@ import { type PruneReason } from "./prune.js";
104
104
  export declare const DAEMON_ACTOR = "system:daemon";
105
105
  /** Backlog.md's conventional task folder, relative to the working directory. */
106
106
  export { DEFAULT_TASKS_DIR } from "../core/registration.js";
107
+ /**
108
+ * Forget every deferral. Exported for tests, used nowhere else: the memory is
109
+ * output bookkeeping, so a suite that shares a process needs to be able to start
110
+ * from nothing and to exercise what a restarted daemon does.
111
+ */
112
+ export declare function resetDriftDeferrals(): void;
107
113
  /** How often the daemon looks, absent any watcher event. */
108
114
  export declare const DEFAULT_INTERVAL_MS = 30000;
109
115
  /** How long a burst of watcher events is allowed to settle before a tick. */
@@ -203,6 +209,18 @@ export type DaemonEvent = {
203
209
  * a person.
204
210
  */
205
211
  dangling_advances?: string[];
212
+ /**
213
+ * Which daemon instance this run is (APRV-383), and `null` when the id it
214
+ * was launched with is unusable. Additive, like every other growth of this
215
+ * union.
216
+ *
217
+ * On the FIRST line for the reason `read_proof` and `draw` are: it is the
218
+ * name every record this run appends will carry, a tenant reading a hosted
219
+ * log identifies the process by it, and it is not something an operator
220
+ * should have to ask the process about. `null` is the state in which this
221
+ * run will refuse every append, which the warning beside it says in words.
222
+ */
223
+ daemon?: string | null;
206
224
  } | {
207
225
  event: "drift";
208
226
  task: string;
@@ -216,6 +234,18 @@ export type DaemonEvent = {
216
234
  * appeared on all of them would change a shape supervisors already parse.
217
235
  */
218
236
  reason?: "envelope-missing";
237
+ /**
238
+ * This append is the one an earlier tick of THIS run deferred (APRV-403).
239
+ * Present only when it is true, so the shape a supervisor already parses is
240
+ * unchanged for every other drift record.
241
+ *
242
+ * It closes a `drift-deferred` warning, exactly as `sampled.retry` closes a
243
+ * `sample-deferred` one: the pair is what tells an operator that the
244
+ * transient failure they read resolved, and which record resolved it. A
245
+ * fresh process makes the same append and claims no retry, because this
246
+ * loop says only what it witnessed.
247
+ */
248
+ retry?: true;
219
249
  } | {
220
250
  /**
221
251
  * A task file's `state:` was rewritten to match the log (SPEC.md §6.3).
@@ -252,6 +282,18 @@ export type DaemonEvent = {
252
282
  seq: number;
253
283
  /** `seq` of the `execution.started` record the sample named. */
254
284
  subject_seq: number;
285
+ /**
286
+ * This append is the one an earlier tick of THIS run deferred (APRV-381).
287
+ * Present only when it is true, so the shape a supervisor already parses is
288
+ * unchanged for every other sample.
289
+ *
290
+ * It closes a `sample-deferred` warning: the pair is what tells an operator
291
+ * that the transient failure they read resolved, and which sample resolved
292
+ * it. Absent on a run that did not itself defer the sample, including a
293
+ * fresh process picking up a deferral the previous one printed, because this
294
+ * loop says only what it witnessed.
295
+ */
296
+ retry?: true;
255
297
  } | {
256
298
  /**
257
299
  * A payload's bytes were removed under `payload_retention` (APRV-41,
@@ -551,6 +593,19 @@ export declare const DAEMON_WARNING_CODES: readonly [
551
593
  * repair, and the next tick asks again.
552
594
  */
553
595
  "anchor-behind",
596
+ /**
597
+ * The working log changed between this tick's opening read and the anchor
598
+ * check's own read of the file, so the check read it again (APRV-389).
599
+ *
600
+ * A line rather than a stop, and rather than a silence. `approval log sync`
601
+ * and a records pull both rewrite `events.jsonl` under a running daemon
602
+ * (baseline, fast-forward, restore), and this loop reads without the append
603
+ * lock, so a tick can hold a view the file has already moved past. That is a
604
+ * fact about reads, never a fork: the anchor comparison is made again from the
605
+ * file and the verdict beside this line is the second read's. The line is the
606
+ * one an operator acts on when the writer was not a sync they ran.
607
+ */
608
+ "anchor-reread",
554
609
  /**
555
610
  * `audit.checkpoint_every` says a human-signed checkpoint is due and the log
556
611
  * carries none that recent (APRV-220). A WARNING and never a stop, at every
@@ -566,7 +621,54 @@ export declare const DAEMON_WARNING_CODES: readonly [
566
621
  * while it stands, and that is a thing to know rather than to discover from a
567
622
  * month of taps.
568
623
  */
569
- "draw-unavailable"];
624
+ "draw-unavailable",
625
+ /**
626
+ * This daemon's own id is not one the attested policy's `daemons` list admits,
627
+ * or the id it was launched with is not a usable id at all (APRV-383).
628
+ *
629
+ * A warning rather than a stop, and the distinction matters: the refusal itself
630
+ * happens at the write boundary, one append at a time, so the loop keeps
631
+ * reading, keeps rendering the queue and keeps reporting, and writes nothing.
632
+ * The line exists because an operator should learn that from the first tick
633
+ * rather than from a day of `append-refused` messages. The repair is a line in
634
+ * a policy a human re-attests, or a launch environment fixed, and neither is
635
+ * something this process can do for itself.
636
+ */
637
+ "daemon-identity-refused",
638
+ /**
639
+ * An `audit.sampled` append met a held lock or a moved head, so the sample was
640
+ * postponed to the next tick (APRV-381). Appended to this union, so no existing
641
+ * entry changed meaning.
642
+ *
643
+ * Distinct from `append-refused` because the operator's next move is different:
644
+ * `append-refused` on a sample is a fact about the record or the file and wants
645
+ * a person, and this is contention between writers that the next sweep resolves
646
+ * by re-deriving the sample from the log. It is still a warning rather than a
647
+ * silence, because a tick that keeps deferring is a log under contention and
648
+ * that is a thing to know, and the `sampled` line that closes it carries
649
+ * `retry`.
650
+ */
651
+ "sample-deferred",
652
+ /**
653
+ * An `envelope.drift` append met a held lock or a moved head, so the record was
654
+ * postponed to the next tick (APRV-403). Appended to this union, so no existing
655
+ * entry changed meaning.
656
+ *
657
+ * The same split APRV-381 made for `audit.sampled`, for the same reason and
658
+ * from the same closed list of transient codes. Found while landing that task:
659
+ * the end-to-end test that holds the append lockfile for a whole tick showed
660
+ * the drift scan printing `append-refused … (lock-timeout)`, which is true and
661
+ * reads as a lost record, while the next tick re-derived the drift from the
662
+ * verified log and appended it. An operator's next move differs between the
663
+ * two: `append-refused` on a drift record is a fact about the record or the
664
+ * file and wants a person, and this is contention between writers that the
665
+ * scan resolves by itself.
666
+ *
667
+ * Still a warning rather than a silence: a scan that keeps deferring is a log
668
+ * under contention, which is a thing to know. The `drift` line that closes it
669
+ * carries `retry`.
670
+ */
671
+ "drift-deferred"];
570
672
  export type DaemonWarningCode = (typeof DAEMON_WARNING_CODES)[number];
571
673
  /** Where daemon output goes. Injected, so the loop itself writes to nothing. */
572
674
  export interface DaemonSink {
@@ -838,6 +940,16 @@ export declare class Daemon {
838
940
  private previousSelfWrites;
839
941
  private settle;
840
942
  private finished;
943
+ /**
944
+ * What this process resolved as its own daemon id (APRV-383), kept so the
945
+ * `started` line can name it and the startup check can say what is wrong with
946
+ * it. The id in FORCE lives in `core/daemon-identity.ts`'s process state, which
947
+ * is what the write boundary reads; this field is the resolution that produced
948
+ * it, and nothing reads it to decide anything.
949
+ */
950
+ private readonly identity;
951
+ /** Has the unlisted-id warning already been said this run? */
952
+ private reportedIdentityRefusal;
841
953
  /** Whether {@link DaemonOptions.draw} actually bound (APRV-208). */
842
954
  private drawServing;
843
955
  constructor(options: DaemonOptions);
@@ -1025,6 +1137,23 @@ export declare class Daemon {
1025
1137
  */
1026
1138
  private read;
1027
1139
  private readOnce;
1140
+ /** Where this daemon's policy is, in `loadPolicy`'s own vocabulary. */
1141
+ private policyWhere;
1142
+ /**
1143
+ * Put the attested policy's `daemons` allowlist in force for this tick
1144
+ * (APRV-383).
1145
+ *
1146
+ * Called once per tick, immediately after the opening verified read and before
1147
+ * any sweep that appends. It never widens: a resolution that fails sets nothing,
1148
+ * so whatever was last resolved stays in force, and a run whose policy was never
1149
+ * attested is unrestricted exactly as every run before this key existed was.
1150
+ *
1151
+ * The warning is said ONCE per run. A line every thirty seconds forever stops
1152
+ * being read (the `reportedDangling` set above exists for the same reason), and
1153
+ * the write boundary is where the refusal actually happens: every refused append
1154
+ * carries its own `append-refused` line with the reason in it.
1155
+ */
1156
+ private refreshIdentity;
1028
1157
  /** The TTL in force right now, re-read every pass: policy files change. */
1029
1158
  private ttlMs;
1030
1159
  private gateOptions;
@@ -1045,8 +1174,46 @@ export declare class Daemon {
1045
1174
  * malformed file is not a *contradiction* of the log, it is a file the runtime
1046
1175
  * cannot read a claim out of at all, and inventing a `declared_state` for it
1047
1176
  * would put a fact in the log that nobody wrote.
1177
+ *
1178
+ * ## A deferred drift record is not a lost one (APRV-403)
1179
+ *
1180
+ * An append here can fail two ways that call for different things. A held
1181
+ * lockfile or a head that moved under the compare-and-append means the log was
1182
+ * not written and nothing about the file changed; the next tick re-reads the
1183
+ * folder, re-derives the disagreement from the verified log, and appends it.
1184
+ * A validation refusal, a canonicalization failure, a corrupt tail or an I/O
1185
+ * error is a fact about the record or the file that no retry repairs.
1186
+ *
1187
+ * Both used to read as `append-refused … was not appended`, which for the
1188
+ * first is true and reads as a loss. So the transient half now goes out as
1189
+ * {@link deferDrift}'s own `drift-deferred` line, naming the task and saying
1190
+ * that the scan retries, and the `drift` record that closes it carries
1191
+ * `retry`. Everything else keeps the form it had.
1192
+ *
1193
+ * Nothing about the APPEND changed, which is the part that matters for
1194
+ * SPEC.md §11.1 invariant 5: the record is still placed by `appendEvent`
1195
+ * against the `expectedHead` of a read taken immediately before it, and the
1196
+ * whole decision is still remade against that read. A `head-moved` here IS
1197
+ * compare-and-append working — another writer landed a record between this
1198
+ * scan's read and its append — and deferring is the only answer that does not
1199
+ * either write against a head this scan never saw or lose the observation.
1048
1200
  */
1049
1201
  private scanForDrift;
1202
+ /**
1203
+ * Say that an `envelope.drift` append did not happen YET, and remember it
1204
+ * (APRV-403).
1205
+ *
1206
+ * Two things this line has to do that `append-refused` did not. It names the
1207
+ * TASK KEY, so an operator can match the deferral to the record that closes
1208
+ * it, and it says in words that the next tick retries — because the sentence
1209
+ * it replaced ended at "was not appended", which is true and reads as a loss.
1210
+ *
1211
+ * The claim is safe to make because the scan carries nothing between ticks:
1212
+ * the next one re-reads the folder, re-derives the state from the verified
1213
+ * log, and finds the same disagreement, so the retry is a property of how the
1214
+ * scan is written rather than a promise this method is keeping.
1215
+ */
1216
+ private deferDrift;
1050
1217
  /** Every `*.md` under the task folder, sorted, non-recursive. */
1051
1218
  private taskFiles;
1052
1219
  private checkOneFile;
@@ -1116,7 +1283,7 @@ export declare class Daemon {
1116
1283
  * log, and produces bytes only through `core/task-file.ts`. A file can no more
1117
1284
  * teach the log a state than a screenshot can teach a database a row.
1118
1285
  *
1119
- * Four rules, each of which is a way of not making things worse:
1286
+ * Five rules, each of which is a way of not making things worse:
1120
1287
  *
1121
1288
  * 1. **Only files that already have an envelope.** `set-state` refuses
1122
1289
  * `no-envelope`, and that refusal is honoured silently: a task with no
@@ -1138,6 +1305,11 @@ export declare class Daemon {
1138
1305
  * envelope: each was warned about a few milliseconds ago by
1139
1306
  * {@link scanForDrift} over the same folder. Repeating it here would double
1140
1307
  * every line an operator reads without adding a fact.
1308
+ * 5. **No repair for a drift record that was deferred** (APRV-403). See the
1309
+ * check inside the loop: a file whose `envelope.drift` this tick could not
1310
+ * append is left exactly as it is, because repairing it would correct the
1311
+ * disagreement off the record and erase what the retry is supposed to
1312
+ * re-derive.
1141
1313
  *
1142
1314
  * Loop safety comes from the comparison, not from a remembered flag: the next
1143
1315
  * tick derives the same state from the same log, finds the file already