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
@@ -0,0 +1,252 @@
1
+ /**
2
+ * WHICH daemon wrote this record, and the check the write boundary runs on it
3
+ * (APRV-383).
4
+ *
5
+ * ## The fact this module is about
6
+ *
7
+ * `core/daemon-actor.ts` answers "is this process the daemon". Every record the
8
+ * daemon appends carried that and nothing more: a generic `system:daemon` actor,
9
+ * one string, identical on every machine that ever ran one. That is enough while
10
+ * a gate's daemon is the operator's own process in the operator's own checkout,
11
+ * and it stops being enough the moment one party HOSTS a daemon for another.
12
+ * Three properties of that arrangement already hold: authority comes from the
13
+ * tenant's attested policy and never from the host (§5.2, APRV-324 and 356),
14
+ * credentials come from the launch environment so one process holds one tenant's
15
+ * token, and a decision arrives only through the tenant's own sender mapping so
16
+ * the host cannot mint one. What was missing is the smallest of the four and the
17
+ * one a tenant reads first: opening their own log and seeing which daemon acted
18
+ * on their behalf.
19
+ *
20
+ * So a daemon-written record carries a `daemon` field naming the instance that
21
+ * wrote it, and an attested policy MAY list the ids permitted to write
22
+ * (`daemons`, read by `core/daemon-host.ts`). An id the list does not name is
23
+ * refused at the write boundary.
24
+ *
25
+ * ## What the id is NOT, and this is the important half
26
+ *
27
+ * It is a SELF-REPORTED field, so SPEC.md §11.1 invariant 4 governs it
28
+ * completely: it may raise scrutiny and may never lower it. Concretely, in this
29
+ * runtime:
30
+ *
31
+ * - a listed id gains NOTHING. No verdict, no autonomy, no budget, no floor, no
32
+ * sampling draw, no token and no TTL reads this field or the list. Being named
33
+ * in `daemons` buys a daemon exactly the ability it had before the key existed;
34
+ * - an unlisted id is REFUSED, which is the only direction the field moves
35
+ * anything, and it moves it strictly towards writing less;
36
+ * - the id never enters a payload hash and never enters a token. It is a
37
+ * top-level record field, so the record's own chain hash covers it the way it
38
+ * covers `actor` (a record's provenance has to be part of what the chain
39
+ * protects, or it could be edited without breaking the chain), and no bound
40
+ * payload, no `payload_hash` and no `token_sha256` is computed over it.
41
+ *
42
+ * And it is ATTRIBUTION rather than authorization. A hostile host process holds
43
+ * the tenant's log handle and can call anything in this module the daemon calls,
44
+ * exactly as it can call {@link markDaemonProcess}; `core/daemon-actor.ts` states
45
+ * that boundary and this module inherits it word for word. What the allowlist
46
+ * catches is the likelier failure and the one nothing caught before: a daemon
47
+ * started against the wrong tenant's log, a second daemon nobody meant to leave
48
+ * running, a container rebuilt with a new id, each of which used to write records
49
+ * indistinguishable from the intended daemon's.
50
+ *
51
+ * ## Why the state is module state and not an argument
52
+ *
53
+ * For the reason `core/daemon-actor.ts` gives for taking no arguments: a field a
54
+ * caller passes is a field a caller chooses, and an `appendEvent` option naming
55
+ * the writing daemon would let any process in this runtime claim to be one. The
56
+ * daemon runtime declares its own identity once, at construction, through
57
+ * `core/daemon-host.ts`, and `core/log.ts` READS it. `EventInput` deliberately
58
+ * has no `daemon` member, and `buildRecord` composes a record field by field, so
59
+ * a caller that puts the property on its input object anyway is ignored rather
60
+ * than obeyed.
61
+ *
62
+ * ## Which processes declare, and why the stamp does not ask for the mark
63
+ *
64
+ * Three processes append on a gate's behalf without being a session, and since
65
+ * APRV-448 all three declare the same identity through the same two calls in
66
+ * `core/daemon-host.ts` (`declareDaemonIdentityFor` or
67
+ * `declareResolvedDaemonIdentity`, then `refreshDaemonAllowlist*`): the daemon
68
+ * loop (`daemon/daemon.ts`, at construction), `approval serve` (its verbs on the
69
+ * listener's thread and its hook calls on their worker threads), and `approval
70
+ * channel telegram webhook`. In a co-located deployment, where a facade, a
71
+ * daemon and a channel run in one process per tenant, a tenant reading their
72
+ * own log therefore sees one id on the sweeps and on the actions.
73
+ *
74
+ * The stamp is keyed on the DECLARATION, not on `core/daemon-actor.ts`'s mark.
75
+ * The two answer different questions: the mark says "this process is the daemon
76
+ * loop" and is what routes an advance under the autonomous
77
+ * `log.advance.daemon` class (APRV-382), while the declaration says "records
78
+ * this process writes name this instance". `approval serve` dispatches verbs an
79
+ * agent asked for, so marking it the daemon to get the stamp would hand its
80
+ * process the daemon's advance route, a widening nothing here needs. Declaring
81
+ * without marking stamps and restricts, and widens nothing.
82
+ *
83
+ * ## Why this module imports almost nothing
84
+ *
85
+ * `core/log.ts` imports it, and `core/log.ts` is the bottom of this codebase's
86
+ * import graph: `core/instance.ts` (where the derived id comes from) reaches
87
+ * `core/attest.ts` through `core/env-file.ts`, and `core/attest.ts` spreads
88
+ * `APPEND_ERROR_CODES` at module scope, so importing the
89
+ * derivation here would close a cycle whose evaluation order throws before any
90
+ * test ran. The split is also the right seam on its own terms: what the write
91
+ * boundary needs is a grammar, a state and one question, and everything that
92
+ * reads a policy or a filesystem to ANSWER that question lives in
93
+ * `core/daemon-host.ts`.
94
+ */
95
+ /**
96
+ * The environment variable a launch may declare the daemon's id in.
97
+ *
98
+ * Beside `APPROVAL_HUMAN` and the credential variables, and in the launch
99
+ * environment for the same reason they are: SPEC.md §11.1 invariant 7 keeps this
100
+ * runtime from reading configuration out of the working tree, and a hosted
101
+ * daemon's id is configuration its OPERATOR sets, never something the tenant's
102
+ * repository tells the host about itself.
103
+ */
104
+ export const DAEMON_ID_ENV = "APPROVAL_DAEMON_ID";
105
+ /** The longest id this runtime writes or admits. */
106
+ export const DAEMON_ID_MAX_LENGTH = 64;
107
+ /**
108
+ * The shape an id takes: lowercase alphanumerics, dots, dashes and underscores,
109
+ * starting with an alphanumeric.
110
+ *
111
+ * Narrow on purpose, and for the reason `payload.harness_version` is narrow
112
+ * (APRV-227): the value is a string from a launch environment being written into
113
+ * an append-only log, so a banner, a path, a newline or a shell fragment must not
114
+ * be able to arrive through it. It is also a value an operator COMPARES by eye
115
+ * against `approval doctor` and against the `daemons` list in their policy, and
116
+ * two ids differing only in case are two ids nobody can tell apart on a phone.
117
+ */
118
+ const DAEMON_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
119
+ /** Is `value` a well-formed daemon id? */
120
+ export function isDaemonId(value) {
121
+ return value.length <= DAEMON_ID_MAX_LENGTH && DAEMON_ID_PATTERN.test(value);
122
+ }
123
+ /**
124
+ * The two ways an append can be refused for the identity of the daemon making
125
+ * it, spread into `APPEND_ERROR_CODES` by `core/log.ts`.
126
+ *
127
+ * Two codes rather than one, because the repairs have nothing in common and
128
+ * SPEC.md §11.1 invariant 6 is about a caller being able to act on the answer:
129
+ * one is a line to add to an attested policy, the other is a variable to fix in
130
+ * the launch environment of a process that should not have started.
131
+ *
132
+ * Declared here rather than in `core/log.ts`'s literal list so that the codes and
133
+ * the logic that emits them cannot drift apart; the union there spreads this one,
134
+ * exactly as `ATTEST_ERROR_CODES` spreads it.
135
+ */
136
+ export const DAEMON_APPEND_REFUSAL_CODES = [
137
+ /**
138
+ * The daemon declared an id through {@link DAEMON_ID_ENV} that is not a
139
+ * well-formed id at all, so no record it wrote could be attributed to anything.
140
+ *
141
+ * Refused rather than silently falling back to the derived id, which is the
142
+ * fail-closed direction twice over: an operator who set the variable meant the
143
+ * value they set, and a runtime that quietly used a different one would write
144
+ * records under a name nobody chose, against an allowlist nobody wrote for it.
145
+ */
146
+ "daemon-id-invalid",
147
+ /**
148
+ * The attested policy lists the daemon ids that may write to this log, and this
149
+ * daemon's id is not among them.
150
+ *
151
+ * An ABSENT list is no restriction, not an empty one: a policy that never
152
+ * declares the key behaves exactly as every policy written before it existed.
153
+ */
154
+ "daemon-not-allowed",
155
+ ];
156
+ /** The declaring process's own state. Never set on another process's behalf. */
157
+ let identity = null;
158
+ /**
159
+ * The pid that made the declaration, for the reason `core/daemon-actor.ts` keeps
160
+ * one: the stamp answers only while it is still this process's own. Kept beside
161
+ * {@link identity} rather than inside it, so the reported state is unchanged.
162
+ */
163
+ let declaredPid = null;
164
+ /**
165
+ * Declare the id this process's records carry. Called through
166
+ * `core/daemon-host.ts` by the processes the header names (the daemon loop,
167
+ * `approval serve` and its hook threads, the Telegram webhook listener), and by
168
+ * nothing else.
169
+ *
170
+ * Takes the RESOLVED values rather than resolving them, because resolution reads
171
+ * the environment and the filesystem and this module is the one `core/log.ts`
172
+ * imports (see the header). Idempotent, and a second call replaces the first: a
173
+ * runtime that re-resolves its identity is reporting a fact about itself, and the
174
+ * newest reading is the one in force.
175
+ */
176
+ export function declareDaemonIdentity(state) {
177
+ identity = {
178
+ id: state.id,
179
+ declared: state.declared ?? null,
180
+ source: state.source,
181
+ // A fresh declaration carries no allowlist. The daemon resolves one from its
182
+ // attested policy and sets it separately, so an identity re-declared mid-run
183
+ // cannot silently drop a restriction that was in force.
184
+ allowed: identity === null ? null : identity.allowed,
185
+ };
186
+ declaredPid = process.pid;
187
+ }
188
+ /**
189
+ * Record which daemon ids this log's ATTESTED policy admits.
190
+ *
191
+ * Called only after a successful resolution: the daemon loaded its policy, read
192
+ * the verified log, and the attestation matched (`core/daemon-host.ts`). A
193
+ * resolution that FAILED calls nothing, so the restriction last resolved stays in
194
+ * force rather than lapsing — a policy that becomes unreadable must not be a way
195
+ * to escape the list it carried a moment ago.
196
+ *
197
+ * `null` means the attested policy declares none, which is no restriction.
198
+ */
199
+ export function setDaemonAllowlist(allowed) {
200
+ if (identity === null)
201
+ return;
202
+ identity = { ...identity, allowed: allowed === null ? null : [...allowed] };
203
+ }
204
+ /** Forget the declaration. For tests, and for a runtime that stops being one. */
205
+ export function clearDaemonIdentity() {
206
+ identity = null;
207
+ declaredPid = null;
208
+ }
209
+ /** What this process has declared about itself, or `null`. */
210
+ export function daemonIdentity() {
211
+ return identity;
212
+ }
213
+ /**
214
+ * The write boundary's question, answered from this process's own state
215
+ * (APRV-383).
216
+ *
217
+ * Both halves of the answer are here rather than in two functions, because they
218
+ * are one decision made in one order: a process that cannot name itself is
219
+ * refused before anything asks whether its name is allowed.
220
+ *
221
+ * A process that declared NO identity gets `absent`, whether or not it marked
222
+ * itself the daemon. That is every session, every hook process and the
223
+ * pre-APRV-383 daemon, and the records they write are the records they always
224
+ * wrote; turning an undeclared mark into a dead log would make an unrelated
225
+ * module's `markDaemonProcess` call a stop-the-world bug. The answer is keyed on
226
+ * the declaration alone (APRV-448, see the header): the daemon loop marks and
227
+ * declares together, while `approval serve` and the webhook listener declare
228
+ * without marking, because the mark carries the daemon's advance route and the
229
+ * declaration carries nothing but a name and a restriction.
230
+ */
231
+ export function daemonStampForAppend() {
232
+ const state = identity;
233
+ if (state === null || declaredPid !== process.pid)
234
+ return { kind: "absent" };
235
+ if (state.id === null) {
236
+ return {
237
+ kind: "refuse",
238
+ code: "daemon-id-invalid",
239
+ message: `this daemon declared ${DAEMON_ID_ENV}=${JSON.stringify(state.declared ?? "")}, which is not a usable daemon id (lowercase letters, digits, \`.\`, \`-\` and \`_\`, starting with a letter or digit, at most ${String(DAEMON_ID_MAX_LENGTH)} characters), so no record it wrote could name which daemon wrote it. Nothing was written: fix ${DAEMON_ID_ENV} in this process's launch environment, or unset it and let the id derive from this instance`,
240
+ };
241
+ }
242
+ const allowed = state.allowed;
243
+ if (allowed !== null && !allowed.includes(state.id)) {
244
+ return {
245
+ kind: "refuse",
246
+ code: "daemon-not-allowed",
247
+ message: `this daemon's id is ${state.id} and the attested policy's \`daemons\` list admits ${allowed.length === 0 ? "no daemon at all" : allowed.join(", ")}, so it may not write to this log. Nothing was written: add ${state.id} to \`daemons\` and re-attest the policy, or run the daemon whose id the list already names`,
248
+ };
249
+ }
250
+ return { kind: "stamp", id: state.id };
251
+ }
252
+ //# sourceMappingURL=daemon-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-identity.js","sourceRoot":"","sources":["../../../src/core/daemon-identity.ts"],"names":[],"mappings":"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"}