approval-md 0.2.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 (378) hide show
  1. package/README.md +85 -877
  2. package/SPEC.md +150 -19
  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 +53 -1
  8. package/dist/src/channels/contract.js +304 -7
  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 +394 -12
  22. package/dist/src/channels/telegram.js +621 -68
  23. package/dist/src/channels/telegram.js.map +1 -1
  24. package/dist/src/channels/web.d.ts +9 -0
  25. package/dist/src/channels/web.js +17 -0
  26. package/dist/src/channels/web.js.map +1 -1
  27. package/dist/src/cli/amend.js +564 -34
  28. package/dist/src/cli/amend.js.map +1 -1
  29. package/dist/src/cli/attest.d.ts +9 -0
  30. package/dist/src/cli/attest.js +177 -13
  31. package/dist/src/cli/attest.js.map +1 -1
  32. package/dist/src/cli/channel-relay.d.ts +108 -0
  33. package/dist/src/cli/channel-relay.js +334 -0
  34. package/dist/src/cli/channel-relay.js.map +1 -0
  35. package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
  36. package/dist/src/cli/channel-telegram-webhook.js +709 -0
  37. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  38. package/dist/src/cli/channel-telegram.d.ts +347 -28
  39. package/dist/src/cli/channel-telegram.js +763 -28
  40. package/dist/src/cli/channel-telegram.js.map +1 -1
  41. package/dist/src/cli/channel.d.ts +10 -1
  42. package/dist/src/cli/channel.js +14 -1
  43. package/dist/src/cli/channel.js.map +1 -1
  44. package/dist/src/cli/codex-bridge.d.ts +819 -0
  45. package/dist/src/cli/codex-bridge.js +1607 -0
  46. package/dist/src/cli/codex-bridge.js.map +1 -0
  47. package/dist/src/cli/codex.d.ts +1 -1
  48. package/dist/src/cli/codex.js +304 -7
  49. package/dist/src/cli/codex.js.map +1 -1
  50. package/dist/src/cli/daemon.js +41 -6
  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 +1322 -122
  54. package/dist/src/cli/doctor.js.map +1 -1
  55. package/dist/src/cli/execute.d.ts +17 -1
  56. package/dist/src/cli/execute.js +366 -22
  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 +14 -2
  65. package/dist/src/cli/help.js +395 -82
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +49 -1
  68. package/dist/src/cli/hook-codex.js +60 -1
  69. package/dist/src/cli/hook-codex.js.map +1 -1
  70. package/dist/src/cli/hook.d.ts +828 -4
  71. package/dist/src/cli/hook.js +2972 -142
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.js +1 -1
  74. package/dist/src/cli/import.js.map +1 -1
  75. package/dist/src/cli/init.js +16 -7
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.js +24 -3
  78. package/dist/src/cli/instructions.js.map +1 -1
  79. package/dist/src/cli/log-anchor.d.ts +55 -0
  80. package/dist/src/cli/log-anchor.js +199 -60
  81. package/dist/src/cli/log-anchor.js.map +1 -1
  82. package/dist/src/cli/main.d.ts +0 -12
  83. package/dist/src/cli/main.js +56 -3
  84. package/dist/src/cli/main.js.map +1 -1
  85. package/dist/src/cli/muse.d.ts +2 -0
  86. package/dist/src/cli/muse.js +62 -0
  87. package/dist/src/cli/muse.js.map +1 -0
  88. package/dist/src/cli/payload.js +79 -1
  89. package/dist/src/cli/payload.js.map +1 -1
  90. package/dist/src/cli/policy-apply.d.ts +195 -0
  91. package/dist/src/cli/policy-apply.js +573 -0
  92. package/dist/src/cli/policy-apply.js.map +1 -0
  93. package/dist/src/cli/policy.js +14 -1
  94. package/dist/src/cli/policy.js.map +1 -1
  95. package/dist/src/cli/preflight.d.ts +151 -13
  96. package/dist/src/cli/preflight.js +398 -41
  97. package/dist/src/cli/preflight.js.map +1 -1
  98. package/dist/src/cli/sandbox.js +17 -1
  99. package/dist/src/cli/sandbox.js.map +1 -1
  100. package/dist/src/cli/scaffold.d.ts +1 -1
  101. package/dist/src/cli/scaffold.js +7 -1
  102. package/dist/src/cli/scaffold.js.map +1 -1
  103. package/dist/src/cli/serve.d.ts +53 -0
  104. package/dist/src/cli/serve.js +270 -0
  105. package/dist/src/cli/serve.js.map +1 -0
  106. package/dist/src/cli/setup-channel.d.ts +9 -0
  107. package/dist/src/cli/setup-channel.js +28 -1
  108. package/dist/src/cli/setup-channel.js.map +1 -1
  109. package/dist/src/cli/setup-common.d.ts +3 -1
  110. package/dist/src/cli/setup-common.js +3 -2
  111. package/dist/src/cli/setup-common.js.map +1 -1
  112. package/dist/src/cli/setup.d.ts +2 -0
  113. package/dist/src/cli/setup.js +94 -2
  114. package/dist/src/cli/setup.js.map +1 -1
  115. package/dist/src/cli/up.js +134 -51
  116. package/dist/src/cli/up.js.map +1 -1
  117. package/dist/src/cli/values.js +3 -4
  118. package/dist/src/cli/values.js.map +1 -1
  119. package/dist/src/cli/verb-registry.d.ts +2 -0
  120. package/dist/src/cli/verb-registry.js +510 -47
  121. package/dist/src/cli/verb-registry.js.map +1 -1
  122. package/dist/src/cli/wordmark.d.ts +2 -2
  123. package/dist/src/cli/wordmark.js +2 -2
  124. package/dist/src/codex/broker.d.ts +229 -0
  125. package/dist/src/codex/broker.js +548 -0
  126. package/dist/src/codex/broker.js.map +1 -0
  127. package/dist/src/codex/runner.d.ts +178 -0
  128. package/dist/src/codex/runner.js +231 -0
  129. package/dist/src/codex/runner.js.map +1 -0
  130. package/dist/src/codex/serve.d.ts +56 -0
  131. package/dist/src/codex/serve.js +98 -0
  132. package/dist/src/codex/serve.js.map +1 -0
  133. package/dist/src/codex/workspace-commit.d.ts +219 -0
  134. package/dist/src/codex/workspace-commit.js +549 -0
  135. package/dist/src/codex/workspace-commit.js.map +1 -0
  136. package/dist/src/core/advance-cycle.d.ts +51 -0
  137. package/dist/src/core/advance-cycle.js +66 -2
  138. package/dist/src/core/advance-cycle.js.map +1 -1
  139. package/dist/src/core/agents-md.d.ts +20 -18
  140. package/dist/src/core/agents-md.js +33 -31
  141. package/dist/src/core/agents-md.js.map +1 -1
  142. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  143. package/dist/src/core/attest-bootstrap.js +135 -0
  144. package/dist/src/core/attest-bootstrap.js.map +1 -0
  145. package/dist/src/core/attest.d.ts +234 -2
  146. package/dist/src/core/attest.js +342 -7
  147. package/dist/src/core/attest.js.map +1 -1
  148. package/dist/src/core/audit.d.ts +73 -3
  149. package/dist/src/core/audit.js +65 -5
  150. package/dist/src/core/audit.js.map +1 -1
  151. package/dist/src/core/channel-lease.d.ts +322 -0
  152. package/dist/src/core/channel-lease.js +663 -0
  153. package/dist/src/core/channel-lease.js.map +1 -0
  154. package/dist/src/core/channel-owner.d.ts +213 -0
  155. package/dist/src/core/channel-owner.js +358 -0
  156. package/dist/src/core/channel-owner.js.map +1 -0
  157. package/dist/src/core/command-class.d.ts +205 -0
  158. package/dist/src/core/command-class.js +1433 -28
  159. package/dist/src/core/command-class.js.map +1 -1
  160. package/dist/src/core/commit-guard.d.ts +272 -0
  161. package/dist/src/core/commit-guard.js +424 -0
  162. package/dist/src/core/commit-guard.js.map +1 -0
  163. package/dist/src/core/daemon-actor.d.ts +45 -0
  164. package/dist/src/core/daemon-actor.js +54 -0
  165. package/dist/src/core/daemon-actor.js.map +1 -0
  166. package/dist/src/core/daemon-host.d.ts +178 -0
  167. package/dist/src/core/daemon-host.js +228 -0
  168. package/dist/src/core/daemon-host.js.map +1 -0
  169. package/dist/src/core/daemon-identity.d.ts +238 -0
  170. package/dist/src/core/daemon-identity.js +252 -0
  171. package/dist/src/core/daemon-identity.js.map +1 -0
  172. package/dist/src/core/dark-session.d.ts +109 -8
  173. package/dist/src/core/dark-session.js +266 -82
  174. package/dist/src/core/dark-session.js.map +1 -1
  175. package/dist/src/core/decision-refusal.d.ts +23 -2
  176. package/dist/src/core/decision-refusal.js +24 -2
  177. package/dist/src/core/decision-refusal.js.map +1 -1
  178. package/dist/src/core/env-file.d.ts +5 -0
  179. package/dist/src/core/env-file.js +60 -1
  180. package/dist/src/core/env-file.js.map +1 -1
  181. package/dist/src/core/execute.d.ts +15 -2
  182. package/dist/src/core/execute.js +15 -2
  183. package/dist/src/core/execute.js.map +1 -1
  184. package/dist/src/core/gate.d.ts +392 -6
  185. package/dist/src/core/gate.js +852 -24
  186. package/dist/src/core/gate.js.map +1 -1
  187. package/dist/src/core/gesture-refusal.d.ts +166 -0
  188. package/dist/src/core/gesture-refusal.js +188 -0
  189. package/dist/src/core/gesture-refusal.js.map +1 -0
  190. package/dist/src/core/harness-version.d.ts +128 -4
  191. package/dist/src/core/harness-version.js +181 -12
  192. package/dist/src/core/harness-version.js.map +1 -1
  193. package/dist/src/core/harness-wait.d.ts +82 -3
  194. package/dist/src/core/harness-wait.js +89 -3
  195. package/dist/src/core/harness-wait.js.map +1 -1
  196. package/dist/src/core/instance.d.ts +59 -2
  197. package/dist/src/core/instance.js +113 -0
  198. package/dist/src/core/instance.js.map +1 -1
  199. package/dist/src/core/log-subscribe.d.ts +21 -0
  200. package/dist/src/core/log-subscribe.js +16 -6
  201. package/dist/src/core/log-subscribe.js.map +1 -1
  202. package/dist/src/core/log-write-layer.d.ts +55 -0
  203. package/dist/src/core/log-write-layer.js +91 -0
  204. package/dist/src/core/log-write-layer.js.map +1 -0
  205. package/dist/src/core/log.d.ts +89 -22
  206. package/dist/src/core/log.js +197 -19
  207. package/dist/src/core/log.js.map +1 -1
  208. package/dist/src/core/payload-census.d.ts +51 -0
  209. package/dist/src/core/payload-census.js +125 -2
  210. package/dist/src/core/payload-census.js.map +1 -1
  211. package/dist/src/core/payload-store.d.ts +31 -0
  212. package/dist/src/core/payload-store.js +113 -8
  213. package/dist/src/core/payload-store.js.map +1 -1
  214. package/dist/src/core/payload.d.ts +18 -3
  215. package/dist/src/core/payload.js +20 -4
  216. package/dist/src/core/payload.js.map +1 -1
  217. package/dist/src/core/policy-diff.d.ts +10 -0
  218. package/dist/src/core/policy-diff.js +10 -2
  219. package/dist/src/core/policy-diff.js.map +1 -1
  220. package/dist/src/core/policy-explain.d.ts +17 -1
  221. package/dist/src/core/policy-explain.js +48 -1
  222. package/dist/src/core/policy-explain.js.map +1 -1
  223. package/dist/src/core/policy-load.d.ts +65 -1
  224. package/dist/src/core/policy-load.js +21 -3
  225. package/dist/src/core/policy-load.js.map +1 -1
  226. package/dist/src/core/policy-match.d.ts +81 -0
  227. package/dist/src/core/policy-match.js +87 -0
  228. package/dist/src/core/policy-match.js.map +1 -1
  229. package/dist/src/core/policy-proposal.d.ts +75 -0
  230. package/dist/src/core/policy-proposal.js +110 -4
  231. package/dist/src/core/policy-proposal.js.map +1 -1
  232. package/dist/src/core/protected-path-guard.d.ts +117 -4
  233. package/dist/src/core/protected-path-guard.js +362 -48
  234. package/dist/src/core/protected-path-guard.js.map +1 -1
  235. package/dist/src/core/question-preempted.d.ts +141 -0
  236. package/dist/src/core/question-preempted.js +152 -0
  237. package/dist/src/core/question-preempted.js.map +1 -0
  238. package/dist/src/core/read-scope.d.ts +182 -0
  239. package/dist/src/core/read-scope.js +256 -0
  240. package/dist/src/core/read-scope.js.map +1 -0
  241. package/dist/src/core/run-payload.d.ts +160 -0
  242. package/dist/src/core/run-payload.js +304 -0
  243. package/dist/src/core/run-payload.js.map +1 -0
  244. package/dist/src/core/sandbox.d.ts +81 -0
  245. package/dist/src/core/sandbox.js +190 -1
  246. package/dist/src/core/sandbox.js.map +1 -1
  247. package/dist/src/core/sender-identity.d.ts +501 -0
  248. package/dist/src/core/sender-identity.js +609 -0
  249. package/dist/src/core/sender-identity.js.map +1 -0
  250. package/dist/src/core/shlex.d.ts +102 -0
  251. package/dist/src/core/shlex.js +159 -0
  252. package/dist/src/core/shlex.js.map +1 -0
  253. package/dist/src/core/state.d.ts +52 -1
  254. package/dist/src/core/state.js +52 -6
  255. package/dist/src/core/state.js.map +1 -1
  256. package/dist/src/core/telegram-config.d.ts +79 -0
  257. package/dist/src/core/telegram-config.js +108 -0
  258. package/dist/src/core/telegram-config.js.map +1 -1
  259. package/dist/src/core/values.d.ts +18 -8
  260. package/dist/src/core/values.js +36 -1
  261. package/dist/src/core/values.js.map +1 -1
  262. package/dist/src/core/verify.d.ts +23 -1
  263. package/dist/src/core/verify.js +20 -2
  264. package/dist/src/core/verify.js.map +1 -1
  265. package/dist/src/daemon/advance.d.ts +10 -0
  266. package/dist/src/daemon/advance.js +25 -4
  267. package/dist/src/daemon/advance.js.map +1 -1
  268. package/dist/src/daemon/audit.d.ts +80 -5
  269. package/dist/src/daemon/audit.js +126 -6
  270. package/dist/src/daemon/audit.js.map +1 -1
  271. package/dist/src/daemon/daemon.d.ts +174 -2
  272. package/dist/src/daemon/daemon.js +307 -7
  273. package/dist/src/daemon/daemon.js.map +1 -1
  274. package/dist/src/daemon/git-evidence.d.ts +2 -2
  275. package/dist/src/daemon/git-evidence.js +1 -1
  276. package/dist/src/daemon/projection.d.ts +13 -2
  277. package/dist/src/daemon/projection.js +39 -6
  278. package/dist/src/daemon/projection.js.map +1 -1
  279. package/dist/src/mcp/server.d.ts +39 -3
  280. package/dist/src/mcp/server.js +30 -3
  281. package/dist/src/mcp/server.js.map +1 -1
  282. package/dist/src/muse/server.d.ts +16 -0
  283. package/dist/src/muse/server.js +238 -0
  284. package/dist/src/muse/server.js.map +1 -0
  285. package/dist/src/serve/archive.d.ts +158 -0
  286. package/dist/src/serve/archive.js +354 -0
  287. package/dist/src/serve/archive.js.map +1 -0
  288. package/dist/src/serve/arguments.d.ts +93 -0
  289. package/dist/src/serve/arguments.js +228 -0
  290. package/dist/src/serve/arguments.js.map +1 -0
  291. package/dist/src/serve/credentials.d.ts +91 -0
  292. package/dist/src/serve/credentials.js +143 -0
  293. package/dist/src/serve/credentials.js.map +1 -0
  294. package/dist/src/serve/follow.d.ts +86 -0
  295. package/dist/src/serve/follow.js +101 -0
  296. package/dist/src/serve/follow.js.map +1 -0
  297. package/dist/src/serve/hook-thread.d.ts +134 -0
  298. package/dist/src/serve/hook-thread.js +333 -0
  299. package/dist/src/serve/hook-thread.js.map +1 -0
  300. package/dist/src/serve/hook-worker.d.ts +134 -0
  301. package/dist/src/serve/hook-worker.js +173 -0
  302. package/dist/src/serve/hook-worker.js.map +1 -0
  303. package/dist/src/serve/server.d.ts +412 -0
  304. package/dist/src/serve/server.js +1251 -0
  305. package/dist/src/serve/server.js.map +1 -0
  306. package/docs/README-extended.md +766 -0
  307. package/docs/cli-reference.md +2563 -73
  308. package/docs/codex-enforced-session.md +75 -2
  309. package/docs/codex-workspace-broker.md +118 -0
  310. package/package.json +6 -2
  311. package/schema/event.schema.json +595 -9
  312. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  313. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  314. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  315. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  316. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  317. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  318. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  319. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  320. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  321. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  322. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  323. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  324. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  325. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  326. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  327. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  328. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  329. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  330. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  331. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  332. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  333. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  334. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  335. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  336. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  337. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  338. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  339. package/schema/fixtures/event/valid/daemon-identity.json +15 -0
  340. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  341. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  342. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  343. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  344. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  345. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  346. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  347. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  348. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  349. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  350. package/schema/fixtures/policy/valid/canonical.json +1 -1
  351. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  352. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  353. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  354. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  355. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  356. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  357. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  358. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  359. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  360. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  361. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  362. package/schema/fixtures/values/invalid/version-float.json +1 -0
  363. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  364. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  365. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  366. package/schema/fixtures/values/valid/full.json +5 -7
  367. package/schema/fixtures/values/valid/minimal.json +1 -1
  368. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  369. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  370. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  371. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  372. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  373. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  374. package/schema/fixtures/values-md/valid/absent.md +1 -1
  375. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  376. package/schema/policy.schema.json +100 -4
  377. package/schema/values.schema.json +7 -11
  378. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -52,7 +52,7 @@ import { spawnSync } from "node:child_process";
52
52
  import { readdirSync, rmSync } from "node:fs";
53
53
  import { constants as osConstants } from "node:os";
54
54
  import { dirname, isAbsolute, resolve as resolvePathSegments } from "node:path";
55
- import { HUMAN_ACTOR_ENV, checkAttestation, resolveHumanActor } from "../core/attest.js";
55
+ import { HUMAN_ACTOR_ENV, attesterAt, checkAttestation, resolveHumanActor, } from "../core/attest.js";
56
56
  import { RESOLVE_DANGLING_COMMAND, proveDanglingAdvances, } from "../core/advance-cycle.js";
57
57
  import { openObligations } from "../core/audit.js";
58
58
  import { evaluateBudgets } from "../core/budgets.js";
@@ -66,17 +66,21 @@ import { harnessLoopEscalation, harnessOutcomeCoverage, loopClearance } from "..
66
66
  import { isPayloadHash, runPayloadHash } from "../core/payload.js";
67
67
  import { payloadStoreCensus } from "../core/payload-census.js";
68
68
  import { payloadStoreDirFor } from "../core/payload-store.js";
69
- import { withdraw } from "../core/gate.js";
69
+ import { boundScript, describeBoundScript } from "../core/run-payload.js";
70
+ import { attestedPolicySha256, requestStanding, taskRegistration, withdraw, } from "../core/gate.js";
70
71
  import { openGateWindow } from "../core/gate-window.js";
71
72
  import { keyStoreDirFor } from "../core/seal.js";
72
73
  import { readVerifiedRecords, requestState } from "../core/state.js";
73
74
  import { deliveredToken } from "../core/token.js";
74
75
  import { passphraseEnvFor } from "../core/vault.js";
76
+ import { resolveDaemonAllowlist, resolveDaemonId } from "../core/daemon-host.js";
77
+ import { DAEMON_ID_ENV } from "../core/daemon-identity.js";
75
78
  import { loadPolicy, parseDuration, POLICY_FILENAMES } from "../core/policy-load.js";
76
79
  import { verify } from "../core/verify.js";
77
80
  import { boolFlag, parseFlags, stringFlag } from "./args.js";
78
- import { EXIT_INTEGRITY, EXIT_IO, EXIT_NO_TOKEN, EXIT_OK, EXIT_TIMEOUT, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
81
+ import { EXIT_INTEGRITY, EXIT_IO, EXIT_NO_TOKEN, EXIT_OK, EXIT_TIMEOUT, EXIT_VOID, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
79
82
  import { repoRoot } from "./git-scope.js";
83
+ import { resolveReadRoots } from "./hook.js";
80
84
  import { publishedState } from "./log-advance.js";
81
85
  import { confirmUntil, createPrompter } from "./prompt.js";
82
86
  import { EXECUTION_HELP, QUEUE_HELP, RECONCILE_HELP, RESOLVE_HELP, RUN_HELP, STATUS_HELP, WAIT_HELP, } from "./help.js";
@@ -169,6 +173,10 @@ function emitRefusal(streams, json, refusal) {
169
173
  error["seq"] = refusal.seq;
170
174
  if (refusal.record !== undefined)
171
175
  error["event_seq"] = refusal.record.seq;
176
+ // APRV-448: the write-boundary code under `append-failed`, as `approval
177
+ // gate` and the gate verbs report it.
178
+ if (refusal.append !== undefined)
179
+ error["append"] = refusal.append.code;
172
180
  streams.err(`${JSON.stringify({ ok: false, error })}\n`);
173
181
  }
174
182
  else {
@@ -232,15 +240,37 @@ function childEnvFor(logPath, actionKey, flags, cwd) {
232
240
  * and the credential material beside the log unreadable. No flag widens it.
233
241
  * `--no-sandbox` is all or nothing, and it is recorded.
234
242
  */
235
- function wrapExecutable(mechanism, command, args, env, logPath) {
243
+ function wrapExecutable(mechanism, command, args, env, logPath, readJail) {
236
244
  const resolved = resolveExecutable(command, env);
237
245
  if (resolved === null)
238
246
  return null;
239
247
  return wrapForSandbox(mechanism, resolved, args, {
240
248
  loopback: false,
241
249
  denyRead: credentialPathsFor(logPath),
250
+ ...(readJail.length === 0 ? {} : { allowRead: readJail }),
242
251
  });
243
252
  }
253
+ /**
254
+ * The read jail for this execution, or an empty list for "no jail" (APRV-347).
255
+ *
256
+ * Driven by the POLICY and by nothing else. A policy that declares a
257
+ * `read_scope` block — even an empty one — is a human's attested statement that
258
+ * this gate's executions read inside its roots and nowhere else, so the profile
259
+ * denies file reads by default and opens those roots. A policy that declares
260
+ * none gets exactly the profile it got before APRV-347, byte for byte, which is
261
+ * every deployment that has not asked for this.
262
+ *
263
+ * It is not a flag, and deliberately: an agent can pass flags. It is not an
264
+ * environment variable either, for the same reason. The one loosening — no
265
+ * jail — is the absence of a key in a file an agent may not edit.
266
+ */
267
+ function readJailFor(flags, cwd) {
268
+ const location = policyLocation(flags, cwd);
269
+ const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
270
+ if (!load.ok || load.policy.read_scope === undefined)
271
+ return [];
272
+ return resolveReadRoots(cwd, dirname(load.source.path), load.policy.read_scope.roots);
273
+ }
244
274
  /** `defaults.approval_ttl` in force, or `null` when the policy declares none. */
245
275
  function ttlOf(flags, cwd) {
246
276
  const location = policyLocation(flags, cwd);
@@ -348,12 +378,23 @@ export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
348
378
  if (hashFlag !== null && !isPayloadHash(hashFlag)) {
349
379
  return usageError(streams, json, `--payload-hash expects 64 lowercase hex characters (SHA-256 over the RFC 8785 canonical serialization of the payload), got ${JSON.stringify(hashFlag)}`, RUN_HELP);
350
380
  }
381
+ // APRV-401 put the SCRIPT inside that computation: an interpreter followed by
382
+ // a path operand, or a path at argv[0], carries that file's digest, so a
383
+ // script rewritten between the declaration and this moment hashes differently
384
+ // and is refused here rather than executed under a grant nobody gave it. The
385
+ // refusal names what was bound, because "the hash differs" leaves an agent to
386
+ // guess whether the argv moved or the file under it did, and those have
387
+ // different repairs.
351
388
  const payloadHash = runPayloadHash(childArgv, cwd);
352
389
  if (hashFlag !== null && hashFlag !== payloadHash) {
390
+ const script = boundScript(childArgv, cwd);
391
+ const binding = script === null
392
+ ? `The argv names no readable script, so the binding is the argv and the cwd alone (amended SPEC.md §6.2, APRV-401).`
393
+ : `The binding covers the script's bytes as well (amended SPEC.md §6.2, APRV-401): ${describeBoundScript(script)}, so a script edited since the declaration changes this value.`;
353
394
  return emitRefusal(streams, json, {
354
395
  ok: false,
355
396
  code: "payload-mismatch",
356
- message: `--payload-hash ${hashFlag} is not the hash of the command this would spawn: ${JSON.stringify(childArgv[0])} and ${childArgv.length - 1} argument(s) in ${cwd} hash to ${payloadHash}. \`approval run\` recomputes the binding from the argv and cwd it is about to spawn and never accepts a caller's substitute for it (amended SPEC.md §10.4, APRV-140); the flag states what you believe you are running, and this is a refusal to run something else. Nothing was appended.`,
397
+ message: `--payload-hash ${hashFlag} is not the hash of the command this would spawn: ${JSON.stringify(childArgv[0])} and ${childArgv.length - 1} argument(s) in ${cwd} hash to ${payloadHash}. ${binding} \`approval run\` recomputes the binding from the argv, the cwd and those bytes and never accepts a caller's substitute for it (amended SPEC.md §10.4, APRV-140); the flag states what you believe you are running, and this is a refusal to run something else. Nothing was appended.`,
357
398
  });
358
399
  }
359
400
  // APRV-205. The child's environment is built BEFORE `execution.started`,
@@ -417,7 +458,7 @@ export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
417
458
  // command that never ran. A command that does not resolve is left unwrapped
418
459
  // and fails as the ENOENT it is.
419
460
  const wrapped = posture.kind === "apply"
420
- ? wrapExecutable(posture.mechanism, command, childArgv.slice(1), childEnv.env, logPath)
461
+ ? wrapExecutable(posture.mechanism, command, childArgv.slice(1), childEnv.env, logPath, readJailFor(flags, cwd))
421
462
  : null;
422
463
  const child = spawnSync(wrapped?.command ?? command, wrapped?.args ?? childArgv.slice(1), {
423
464
  cwd,
@@ -463,6 +504,8 @@ export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
463
504
  // ===========================================================================
464
505
  // approval wait
465
506
  // ===========================================================================
507
+ /** `wait --timeout 0` (and `0s`, `0ms`, …): read once, never sleep (APRV-445). */
508
+ const ZERO_DURATION = /^0(?:ms|s|m|h|d|w)?$/u;
466
509
  /** Synchronous sleep with no dependency and no busy-spin. */
467
510
  function sleepSync(ms) {
468
511
  if (ms <= 0)
@@ -476,10 +519,28 @@ function sleepSync(ms) {
476
519
  function stateRole(state) {
477
520
  if (state === "granted")
478
521
  return "ok";
479
- if (state === "expired" || state === "requested" || state === "withdrawn")
522
+ if (state === "expired" ||
523
+ state === "requested" ||
524
+ state === "withdrawn" ||
525
+ state === NOTHING_TO_WAIT_FOR) {
480
526
  return "warn";
527
+ }
481
528
  return "fail";
482
529
  }
530
+ /**
531
+ * `approval wait`'s status for a REGISTERED task that holds nothing a wait could
532
+ * resolve (APRV-428): no `approval.requested` at all (no action was declared,
533
+ * none reached the manual path, or none has been requested yet), or every
534
+ * granted action has already started executing, so its grant is spent.
535
+ *
536
+ * Exit 0, because the wait contract has always returned at once and at 0 for a
537
+ * task with no requests, and the supervised path tells an agent its
538
+ * `proceed: true` needs no wait at all. What changes is the WORD: before
539
+ * APRV-428 this was `granted`, and a caller that branched on `status` saw a
540
+ * grant nobody made. Nothing is minted either way, since `approval run` still
541
+ * needs a token on the manual path.
542
+ */
543
+ const NOTHING_TO_WAIT_FOR = "nothing-to-wait-for";
483
544
  /**
484
545
  * `approval wait`'s human answer: the verdict, then one aligned row per action.
485
546
  *
@@ -487,7 +548,11 @@ function stateRole(state) {
487
548
  * (`plainLeft`) and the state beside it carries the colour instead.
488
549
  */
489
550
  export function renderWaitHuman(task, status, actions, st = style()) {
490
- const glyph = status === "granted" ? "ok" : status === "expired" || status === "withdrawn" ? "skip" : "fail";
551
+ const glyph = status === "granted"
552
+ ? "ok"
553
+ : status === "expired" || status === "withdrawn" || status === NOTHING_TO_WAIT_FOR
554
+ ? "skip"
555
+ : "fail";
491
556
  const head = `${st.glyph(glyph)} ${st.key(task)} ${st.paint(stateRole(status), status)}`;
492
557
  if (actions.length === 0)
493
558
  return `${head}\n`;
@@ -498,6 +563,25 @@ export function renderWaitHuman(task, status, actions, st = style()) {
498
563
  }));
499
564
  return `${head}\n${st.table(rows, { indent: 2 })}\n`;
500
565
  }
566
+ /**
567
+ * `approval wait`'s refusal of a task the log has never registered (APRV-428).
568
+ *
569
+ * The frozen `{ok:false,error:{code,message}}` shape on stderr, like every other
570
+ * refusal this verb prints, and exit 1: the same code `approval request` exits
571
+ * for the same `not-registered`, and the meaning wait's exit 1 already carries
572
+ * (not authorized, and waiting again will not change that). A separate writer
573
+ * because the verb's other refusals are the execute union's log-read codes,
574
+ * and `not-registered` is a member of the gate union only.
575
+ */
576
+ function emitWaitRefusal(streams, json, refusal) {
577
+ if (json) {
578
+ streams.err(`${JSON.stringify({ ok: false, error: { code: refusal.code, message: refusal.message } })}\n`);
579
+ }
580
+ else {
581
+ streams.err(`${renderRefusal(style({ json }), refusal.code, refusal.message)}\n`);
582
+ }
583
+ return EXIT_INTEGRITY;
584
+ }
501
585
  /** Every action key of `task` that ever carried an `approval.requested`. */
502
586
  function requestedKeysOf(records, task) {
503
587
  const keys = [];
@@ -537,9 +621,18 @@ export function commandWait(argv, streams, cwd) {
537
621
  if (timeoutText === null) {
538
622
  return usageError(streams, json, "missing --timeout <duration>", WAIT_HELP);
539
623
  }
540
- const timeoutMs = parseDuration(timeoutText);
624
+ // APRV-445. `--timeout 0` is a STATUS READ: one verified read of the log, the
625
+ // current state of every request of the task, and no sleep at all. The
626
+ // duration grammar has no zero (a zero TTL or window in a policy would be a
627
+ // control that never runs), so the spelling is admitted here and only here.
628
+ // It is what a poller on `approval serve` uses: that server runs every verb
629
+ // and every hook call through one queue, and a `wait` that sleeps holds the
630
+ // whole tenant's hook traffic for as long as it sleeps. The result set and
631
+ // the exit codes are unchanged; an undecided request answers `timeout`
632
+ // (exit 6) at once, as a wait that ran out would.
633
+ const timeoutMs = ZERO_DURATION.test(timeoutText) ? 0 : parseDuration(timeoutText);
541
634
  if (timeoutMs === null) {
542
- return usageError(streams, json, `--timeout expects a duration like 30s, 10m, 6h, got ${JSON.stringify(timeoutText)}`, WAIT_HELP);
635
+ return usageError(streams, json, `--timeout expects a duration like 30s, 10m, 6h, or 0 to read the current state without waiting, got ${JSON.stringify(timeoutText)}`, WAIT_HELP);
543
636
  }
544
637
  const intervalText = stringFlag(flags, "--interval");
545
638
  const intervalMs = intervalText === null ? DEFAULT_WAIT_INTERVAL_MS : parseDuration(intervalText);
@@ -553,6 +646,12 @@ export function commandWait(argv, streams, cwd) {
553
646
  // after a nine-minute wait, and failing then, would leave exactly the stale
554
647
  // request the flag exists to prevent.
555
648
  const withdrawOnTimeout = boolFlag(flags, "--withdraw-on-timeout");
649
+ // APRV-445 (L1). A zero wait is a status read, and a status read that
650
+ // withdraws whatever is still pending would retract every question a poller
651
+ // looked at, on the first look.
652
+ if (withdrawOnTimeout && timeoutMs === 0) {
653
+ return usageError(streams, json, "--withdraw-on-timeout cannot be combined with --timeout 0: a zero wait reads the current state, and withdrawing on it would retract every pending request the moment it is polled", WAIT_HELP);
654
+ }
556
655
  const asFlag = stringFlag(flags, "--as");
557
656
  const withdrawActor = asFlag ?? resolveHumanActor();
558
657
  if (withdrawOnTimeout && (withdrawActor === null || !PRINCIPAL_ACTOR.test(withdrawActor))) {
@@ -569,33 +668,72 @@ export function commandWait(argv, streams, cwd) {
569
668
  message: read.message,
570
669
  });
571
670
  }
671
+ // APRV-428. A task the log has never registered is refused, not answered.
672
+ // Before this check the loop below found no requests and reported the task
673
+ // `granted` with no actions: a typo read as a grant, and a shim polling
674
+ // until granted would have proceeded on a request it never opened. Checked
675
+ // on every pass rather than once, because it costs one walk of records
676
+ // already in hand and a registration cannot disappear from an append-only
677
+ // log, so the answer only ever moves from refusal to not.
678
+ const registered = taskRegistration(read.records, task);
679
+ if (!registered.ok)
680
+ return emitWaitRefusal(streams, json, registered);
572
681
  const ts = now();
573
682
  const actions = [];
574
683
  let pending = false;
684
+ // APRV-445 refutation (B1): the state a waiter acts on is what the record
685
+ // still AUTHORIZES. A grant whose window lapsed reads `expired`, and a
686
+ // pending request, or a harness grant (every proposal), pinned to a
687
+ // superseded policy reads `void` (a token grant `run` still spends stays
688
+ // `granted`), so
689
+ // a poller asks again instead of being told `granted` about an answer the
690
+ // spend will refuse.
691
+ const attestedSha = attestedPolicySha256(read.records, { policy: policyLocation(flags, cwd) });
692
+ // A granted action whose grant nothing has spent yet: the one thing a
693
+ // `granted` status can honestly promise (APRV-428). Judged on the
694
+ // standing, so a lapsed or voided grant never counts as one.
695
+ let unspentGrant = false;
575
696
  for (const key of requestedKeysOf(read.records, task)) {
576
697
  const derivation = requestState(read.records, key, ts, ttlMs);
698
+ const standing = requestStanding(read.records, key, ts, ttlMs, attestedSha);
699
+ // An executed key reads as its recorded decision when a grant authorized
700
+ // it (the long-standing `granted`), and as `executed` when it ran on the
701
+ // policy's own authority after its request ended (APRV-445 recheck L-a):
702
+ // a policy-path `start` after a withdrawal is not a `withdrawn` action.
703
+ const state = standing === "executed"
704
+ ? derivation.state === "granted"
705
+ ? "granted"
706
+ : "executed"
707
+ : standing;
577
708
  // APRV-105. The token, when this machine can open it: the grant sealed it
578
709
  // to the ephemeral public key this action's request published, and the
579
710
  // private half is in the key store beside the log. Attached only to a
580
711
  // GRANTED action, and only in `--json` below — a `null` on every other
581
712
  // state would be a field consumers have to ignore, and a token on a
582
713
  // rejected action would be a value with nothing behind it.
583
- const token = json && derivation.state === "granted"
714
+ const token = json && state === "granted"
584
715
  ? deliveredToken(read.records, key, keyStoreDirFor(logPath))
585
716
  : null;
586
717
  actions.push({
587
718
  action_key: key,
588
- state: derivation.state,
719
+ state,
589
720
  seq: derivation.decisionSeq ?? derivation.requestSeq,
590
721
  ...(token === null ? {} : { token }),
591
722
  });
592
- if (derivation.state === "requested")
723
+ if (state === "requested")
593
724
  pending = true;
725
+ if (state === "granted" &&
726
+ derivation.state === "granted" &&
727
+ derivation.execution.started === null) {
728
+ unspentGrant = true;
729
+ }
594
730
  }
595
731
  if (!pending) {
596
732
  // Precedence, documented in --help: a human's "no" outranks a lapse, and
597
- // both outrank "everything was granted". A task with no requests at all
598
- // has nothing to wait for and is granted vacuously.
733
+ // both outrank "everything was granted". A registered task with no
734
+ // requests at all, or whose every grant has already been spent by an
735
+ // execution, has nothing to wait for and says so (APRV-428); it was
736
+ // "granted vacuously" before, which is a grant nobody made.
599
737
  //
600
738
  // APRV-106 puts `withdrawn` between the two, and REUSES exit 1 rather
601
739
  // than adding a code. The table in `cli/exit-codes.ts` is frozen public
@@ -607,15 +745,31 @@ export function commandWait(argv, streams, cwd) {
607
745
  // the JSON and printed beside the action in the human render.
608
746
  const rejected = actions.some((action) => action.state === "rejected" || action.state === "revoked");
609
747
  const withdrawn = actions.some((action) => action.state === "withdrawn");
748
+ const voided = actions.some((action) => action.state === "void");
610
749
  const expired = actions.some((action) => action.state === "expired");
750
+ // APRV-445: `void` sits below a human's no and a requester's withdrawal
751
+ // (both final) and above a lapse, because its repair is the most
752
+ // specific: the same question, asked again under the policy in force.
611
753
  const status = rejected
612
754
  ? "rejected"
613
755
  : withdrawn
614
756
  ? "withdrawn"
757
+ : voided
758
+ ? "void"
759
+ : expired
760
+ ? "expired"
761
+ : unspentGrant
762
+ ? "granted"
763
+ : actions.length > 0 && actions.every((action) => action.state === "executed")
764
+ ? "executed"
765
+ : NOTHING_TO_WAIT_FOR;
766
+ const code = rejected || withdrawn
767
+ ? EXIT_INTEGRITY
768
+ : voided
769
+ ? EXIT_VOID
615
770
  : expired
616
- ? "expired"
617
- : "granted";
618
- const code = rejected || withdrawn ? EXIT_INTEGRITY : expired ? EXIT_TORN_TAIL : EXIT_OK;
771
+ ? EXIT_TORN_TAIL
772
+ : EXIT_OK;
619
773
  if (json)
620
774
  emitJson(streams, { ok: true, task, status, actions });
621
775
  else
@@ -684,11 +838,18 @@ function pendingRequests(records, ts, ttlMs) {
684
838
  const derivation = requestState(records, key, ts, ttlMs);
685
839
  if (derivation.state !== "requested")
686
840
  continue;
841
+ // APRV-423 (second review pass): the window is the derivation's, never a
842
+ // second copy of the arithmetic here. `requestState` already took the
843
+ // minimum of the policy's TTL and the declared harness cap less its margin;
844
+ // reading `ttlMs` again would show a capped request the policy's hour while
845
+ // the gate refuses it after four minutes, and would show a capped request
846
+ // under a policy with no TTL as "no TTL" when it has one.
847
+ const windowMs = derivation.effectiveTtlMs;
687
848
  const requestedAt = Date.parse(derivation.requestTs ?? "");
688
849
  const asked = Date.parse(ts);
689
- const remaining = ttlMs === null || Number.isNaN(requestedAt) || Number.isNaN(asked)
850
+ const remaining = windowMs === null || Number.isNaN(requestedAt) || Number.isNaN(asked)
690
851
  ? null
691
- : requestedAt + ttlMs - asked;
852
+ : requestedAt + windowMs - asked;
692
853
  entries.push({
693
854
  action_key: key,
694
855
  task: derivation.task,
@@ -697,6 +858,7 @@ function pendingRequests(records, ts, ttlMs) {
697
858
  requested_ts: derivation.requestTs,
698
859
  seq: derivation.requestSeq,
699
860
  ttl_remaining_ms: remaining,
861
+ ttl_ms: windowMs,
700
862
  });
701
863
  }
702
864
  return entries.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
@@ -750,7 +912,13 @@ export function renderQueueHuman(pending, ttlMs, st = style()) {
750
912
  entry.class ?? "-",
751
913
  `$${String(entry.est_cost_usd ?? 0)}`,
752
914
  entry.requested_ts ?? "-",
753
- { text: ttlText(entry.ttl_remaining_ms), role: ttlRole(entry.ttl_remaining_ms, ttlMs) },
915
+ // The fraction is of the entry's OWN window (APRV-423): a capped request's
916
+ // four minutes, or the policy's TTL for one nothing narrowed. The `ttlMs`
917
+ // parameter stays as the fallback for an entry that carries no window.
918
+ {
919
+ text: ttlText(entry.ttl_remaining_ms),
920
+ role: ttlRole(entry.ttl_remaining_ms, entry.ttl_ms ?? ttlMs),
921
+ },
754
922
  ]);
755
923
  return `${table(st, rows, {
756
924
  header: ["action", "task", "class", "cost", "requested", "ttl"],
@@ -927,6 +1095,101 @@ function gitCoverageSummary(records, flags, cwd) {
927
1095
  covered: report.covered,
928
1096
  };
929
1097
  }
1098
+ /**
1099
+ * The two refusal records `status` counts, and the key each is reported under
1100
+ * (APRV-376).
1101
+ *
1102
+ * The event NAMES are matched here rather than imported from
1103
+ * `core/decision-refusal.ts` and `core/gesture-refusal.ts`, and that is
1104
+ * deliberate: both modules are writers on the append path, and the whole safety
1105
+ * argument for those records is that nothing reads them from anywhere that
1106
+ * decides. A report that filters on a string pulls in no writer, and the two
1107
+ * suites that pin the module graph stay true without an exemption for this file.
1108
+ */
1109
+ const REFUSAL_FAMILIES = {
1110
+ decision: "audit.decision_refused",
1111
+ gesture: "audit.gesture_refused",
1112
+ };
1113
+ /**
1114
+ * How many of the newest refusals each family lists.
1115
+ *
1116
+ * Small and fixed on purpose. The COUNT answers "is this happening", and the
1117
+ * point of the listing beside it is to hand an operator a seq to paste into
1118
+ * `approval log tail` and an account to recognize. A row that grew with the log
1119
+ * would push the rest of the report off a terminal for a fact that is
1120
+ * informational, and the whole history is `approval log export` away.
1121
+ */
1122
+ const RECENT_REFUSALS = 5;
1123
+ /** One record, read the way `status` reports it: verbatim, and nothing derived. */
1124
+ function refusalEntry(record) {
1125
+ const payload = record.payload ?? {};
1126
+ // The event schema requires `code` on both types, so the fallback is for a
1127
+ // reader of a log written by something else: a report that threw on a record
1128
+ // it could not read would take the whole health report down with it.
1129
+ const code = typeof payload["code"] === "string" ? payload["code"] : "(no code)";
1130
+ const sender = payload["sender"];
1131
+ if (sender === null || typeof sender !== "object")
1132
+ return { seq: record.seq, code };
1133
+ const observed = sender;
1134
+ const channel = observed["channel"];
1135
+ const id = observed["id"];
1136
+ if (typeof channel !== "string" || typeof id !== "string")
1137
+ return { seq: record.seq, code };
1138
+ return {
1139
+ seq: record.seq,
1140
+ code,
1141
+ sender: {
1142
+ channel,
1143
+ id,
1144
+ // APRV-370's marker, carried only when the record carries it: a keyed
1145
+ // digest and a raw account id are different things to go looking for.
1146
+ ...(observed["hashed"] === true ? { hashed: true } : {}),
1147
+ },
1148
+ };
1149
+ }
1150
+ /**
1151
+ * The refused decisions and refused gestures in this log (APRV-376).
1152
+ *
1153
+ * Both families were visible only through `approval log tail` and `approval log
1154
+ * export` until this row existed. The fact an operator needs is small and the
1155
+ * log is the wrong place to read it from: told that taps from an account they
1156
+ * did not map are being refused, they want to know how many and from which
1157
+ * account, and the chain is a poor answer to a question that size.
1158
+ *
1159
+ * INFORMATIONAL, on the terms `harness outcomes` and `git coverage` set: it
1160
+ * moves neither `healthy` nor the exit code. A refusal is the gate having
1161
+ * worked, and a report that went red because the gate refused a stranger would
1162
+ * teach an operator to stop reading it. It reads only verified records (SPEC.md
1163
+ * §11.1 invariant 1) because its caller passes only those, and it reads them
1164
+ * only to report: nothing here authorizes, settles, charges or samples
1165
+ * anything.
1166
+ *
1167
+ * A family with no records is ABSENT rather than zero, and the object is empty
1168
+ * when both are, so a log with no refusals emits the object it always emitted.
1169
+ * A zero here would be a row an operator scrolls past on every healthy
1170
+ * repository, which is how the rows that matter stop being read.
1171
+ */
1172
+ function refusalSummaries(records) {
1173
+ const summaries = {};
1174
+ for (const family of Object.keys(REFUSAL_FAMILIES)) {
1175
+ const seen = records.filter((record) => record.event === REFUSAL_FAMILIES[family]);
1176
+ if (seen.length === 0)
1177
+ continue;
1178
+ summaries[family] = {
1179
+ count: seen.length,
1180
+ // Newest first: the reason to read this row at all is what just happened.
1181
+ recent: seen.slice(-RECENT_REFUSALS).reverse().map(refusalEntry),
1182
+ };
1183
+ }
1184
+ return summaries;
1185
+ }
1186
+ /** How the human rendering spells one entry's account. */
1187
+ function refusalSenderText(entry) {
1188
+ if (entry.sender === undefined)
1189
+ return "";
1190
+ const keyed = entry.sender.hashed === true ? " (keyed)" : "";
1191
+ return ` ${entry.sender.channel}:${entry.sender.id}${keyed}`;
1192
+ }
930
1193
  export function commandStatus(argv, streams, cwd) {
931
1194
  const outcome = front(argv, { ...COMMON_FLAGS, ...POLICY_FLAGS, "--verbose": "boolean" }, STATUS_HELP, streams, cwd);
932
1195
  if (outcome.kind === "handled")
@@ -954,6 +1217,13 @@ export function commandStatus(argv, streams, cwd) {
954
1217
  // anything from one.
955
1218
  const records = read.ok ? read.records : [];
956
1219
  const attestation = checkAttestation(records, policyPathFor(flags, cwd));
1220
+ // APRV-449: who made the attestation the state names, read from the verified
1221
+ // record at that seq, never from the file. On `hash-mismatch` it is the
1222
+ // attester of the bytes the log last vouched for, which is the policy a
1223
+ // re-attestation would replace.
1224
+ const attestedBy = attestation.status === "attested" || attestation.status === "hash-mismatch"
1225
+ ? attesterAt(records, attestation.seq)
1226
+ : null;
957
1227
  // APRV-120. `dangling` is now the `open` custody state only: a harness
958
1228
  // execution is terminal by design and never gains an outcome, so listing it
959
1229
  // as debris trained operators to ignore this list (the reference repository's
@@ -1047,6 +1317,28 @@ export function commandStatus(argv, streams, cwd) {
1047
1317
  status: verification.status,
1048
1318
  records: verification.status === "corrupt" ? null : verification.records,
1049
1319
  };
1320
+ // APRV-383. The daemon identity this instance resolves, and the allowlist in
1321
+ // force for it. `status` is not the daemon, so what it reports is the id a
1322
+ // daemon started HERE would write: that is the fact an operator wiring up a
1323
+ // hosted deployment needs, and the one a tenant needs to look up a `daemon`
1324
+ // field they have just read in a record.
1325
+ //
1326
+ // The allowlist comes from `resolveDaemonAllowlist`, so it is reported only
1327
+ // when the policy loads AND is attested — the same bytes the write boundary
1328
+ // would refuse on. An unattested policy reports `null`, no restriction, which
1329
+ // is exactly what the write boundary would do with it.
1330
+ const daemonId = resolveDaemonId(logPath);
1331
+ const daemonAllowlist = resolveDaemonAllowlist(records, loadPolicy(policyLocation(flags, cwd)));
1332
+ const daemonReport = {
1333
+ id: daemonId.ok ? daemonId.id : null,
1334
+ source: daemonId.ok ? daemonId.source : null,
1335
+ allowed: daemonAllowlist.ok ? daemonAllowlist.allowed : null,
1336
+ };
1337
+ // APRV-376, and informational for the reason stated where it is computed: the
1338
+ // two refusal families, counted, with the newest few of each. Neither the
1339
+ // health line below nor the exit code reads it.
1340
+ const refusals = refusalSummaries(records);
1341
+ const refusalFamilies = Object.keys(refusals);
1050
1342
  // APRV-40. Timestamp anomalies (SPEC.md §8) are informational and deliberately
1051
1343
  // outside `healthy`: they are a judgment, not an integrity verdict. `verify`
1052
1344
  // already declined to refuse on them, and `status` does not get to overrule it
@@ -1083,6 +1375,9 @@ export function commandStatus(argv, streams, cwd) {
1083
1375
  seq: attestation.status === "attested" || attestation.status === "hash-mismatch"
1084
1376
  ? attestation.seq
1085
1377
  : null,
1378
+ // APRV-449, additive and always present: the identity the attesting
1379
+ // record declared, `null` when there is no attestation to name.
1380
+ attested_by: attestedBy,
1086
1381
  },
1087
1382
  verification: verificationSummary,
1088
1383
  dangling,
@@ -1104,7 +1399,16 @@ export function commandStatus(argv, streams, cwd) {
1104
1399
  },
1105
1400
  reconciliation: obligations,
1106
1401
  payload_store: payloadStore,
1402
+ // APRV-383. Always present, for the reason `coverage` is: three keys every
1403
+ // consumer sees on every log, whether or not a daemon has ever run here and
1404
+ // whether or not the policy restricts one. `allowed: null` is no restriction
1405
+ // and is not the same fact as an empty list, which admits nobody.
1406
+ daemon: daemonReport,
1107
1407
  ...(anomalies.length === 0 ? {} : { anomalies }),
1408
+ // APRV-376. Present only when the log carries a refusal of either family,
1409
+ // and each family present only when that family has one, so a repository
1410
+ // where nothing has been refused emits the object it always emitted.
1411
+ ...(refusalFamilies.length === 0 ? {} : { refusals }),
1108
1412
  // Present only while a window stands, exactly as `anomalies` and
1109
1413
  // `indeterminate` are: a repository with no window emits the object it
1110
1414
  // has always emitted, byte for byte.
@@ -1136,7 +1440,7 @@ export function commandStatus(argv, streams, cwd) {
1136
1440
  // a bug report, so APRV-102 took the `muted` dressing off it. The state
1137
1441
  // beside it still carries the colour.
1138
1442
  right: `${st.paint(attestation.status === "attested" ? "ok" : "warn", attestation.status)}${attestation.status === "attested" || attestation.status === "hash-mismatch"
1139
- ? ` (seq ${attestation.seq})`
1443
+ ? ` (seq ${attestation.seq}${attestedBy === null ? "" : `, by ${attestedBy}`})`
1140
1444
  : ""}`,
1141
1445
  },
1142
1446
  {
@@ -1231,6 +1535,26 @@ export function commandStatus(argv, streams, cwd) {
1231
1535
  ? `${String(coverage.covered)} of ${String(coverage.observed)} effects carry evidence`
1232
1536
  : (coverage.reason ?? "unavailable")),
1233
1537
  },
1538
+ // APRV-376. Its own INFORMATIONAL row, and printed only when the log
1539
+ // carries a refusal of either family: a repository where nothing has been
1540
+ // refused reads exactly as it always did, and a row saying "none" on every
1541
+ // healthy repository is a row an operator learns to skip. The counts say
1542
+ // whether this is happening; the lines under them hand over a seq to paste
1543
+ // into `approval log tail` and an account to recognize.
1544
+ ...(refusalFamilies.length === 0
1545
+ ? []
1546
+ : [
1547
+ {
1548
+ left: "refusals",
1549
+ right: st.muted(`${refusalFamilies
1550
+ .map((family) => `${String(refusals[family]?.count ?? 0)} ${family}`)
1551
+ .join(", ")} (reported; health unaffected)`),
1552
+ under: refusalFamilies.flatMap((family) => (refusals[family]?.recent ?? []).map((entry) =>
1553
+ // The family is padded to the longer of the two names, so the
1554
+ // seqs line up under each other when both are listed.
1555
+ `${family.padEnd("decision".length)} seq ${String(entry.seq)} ${entry.code}${refusalSenderText(entry)}`)),
1556
+ },
1557
+ ]),
1234
1558
  {
1235
1559
  // APRV-214. Its own row and not a footnote: while this says OPEN, the
1236
1560
  // policy is deciding nothing for the harness, and the person reading
@@ -1259,6 +1583,26 @@ export function commandStatus(argv, streams, cwd) {
1259
1583
  under: obligations.map((item) => `seq ${item.seq} ${item.action_key} ${item.class} ${item.obligation} — close with \`approval audit reconcile ${item.seq}\``),
1260
1584
  }),
1261
1585
  },
1586
+ {
1587
+ // APRV-383. The id a daemon started against THIS log writes onto every
1588
+ // record it appends, and whether the attested policy admits it. Printed
1589
+ // by a verb that is not the daemon on purpose: an operator setting up a
1590
+ // hosted deployment needs the id before anything is running, and a tenant
1591
+ // reading a record's `daemon` field needs somewhere to look it up.
1592
+ // Informational: it moves neither `healthy` nor the exit code, because
1593
+ // nothing here is a fact about the log's integrity.
1594
+ left: "daemon identity",
1595
+ right: daemonReport.id === null
1596
+ ? st.fail(`UNUSABLE (${DAEMON_ID_ENV} is set to something that is not an id)`)
1597
+ : `${daemonReport.id} ${st.muted(`(${daemonReport.source})`)}`,
1598
+ under: daemonReport.allowed === null
1599
+ ? [st.muted("the policy lists no allowed daemon ids, so any daemon may write")]
1600
+ : [
1601
+ daemonReport.id !== null && daemonReport.allowed.includes(daemonReport.id)
1602
+ ? st.muted(`allowed by the policy's daemons list (${daemonReport.allowed.join(", ")})`)
1603
+ : st.warn(`NOT in the policy's daemons list (${daemonReport.allowed.length === 0 ? "empty" : daemonReport.allowed.join(", ")}) — a daemon started here would be refused every append`),
1604
+ ],
1605
+ },
1262
1606
  { left: "log", right: relPath(logPath, cwd) },
1263
1607
  ];
1264
1608
  streams.out(`${st.table(rows)}\n`);