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
@@ -36,6 +36,7 @@
36
36
  * validation. Nothing in this file opens the log itself.
37
37
  */
38
38
  import { type ClockOptions } from "./clock.js";
39
+ import { type ProtectedPathEntry } from "./command-class.js";
39
40
  import { type AppendOptions, type EventRecord } from "./log.js";
40
41
  import type { ValidationError } from "./validate.js";
41
42
  /** Environment variable naming the human on whose behalf the CLI attests. */
@@ -93,7 +94,7 @@ export interface AttestationRefusal {
93
94
  * assignable to {@link AttestationAppendResult}, so nothing downstream is
94
95
  * forced to change, and the CLI adds one case.
95
96
  */
96
- export declare const ATTEST_ERROR_CODES: readonly ["lock-timeout", "corrupt-tail", "validation", "canonicalization", "io", "head-moved", "actor-not-human"];
97
+ export declare const ATTEST_ERROR_CODES: readonly ["lock-timeout", "corrupt-tail", "validation", "canonicalization", "io", "head-moved", "daemon-id-invalid", "daemon-not-allowed", "actor-not-human"];
97
98
  export type AttestErrorCode = (typeof ATTEST_ERROR_CODES)[number];
98
99
  export interface AttestError {
99
100
  code: AttestErrorCode;
@@ -201,7 +202,41 @@ export declare function policyBytesHash(bytes: Uint8Array): string;
201
202
  * moment, and the *latest* attestation is the one `checkAttestation` honors.
202
203
  * Passing a precondition here would only manufacture spurious failures.
203
204
  */
205
+ /**
206
+ * The value a terminal attestation stores for the bytes it attests (APRV-356).
207
+ *
208
+ * One author for the shape, because two parties need it and they must agree:
209
+ * {@link appendAttestation} writes it, and `cli/amend.ts` has to name the file
210
+ * it will land in before the append happens, so the ceremony commit can carry
211
+ * it. A second spelling of `{ text }` anywhere would be a store file the
212
+ * ceremony quietly left behind.
213
+ *
214
+ * Deliberately narrower than `proposalPayloadValue` in `core/policy-proposal.ts`,
215
+ * which carries `policy_path` beside the text: a proposal is a prompt and names
216
+ * the file it is asking about, while an attestation is a binding and the bytes
217
+ * are the whole of it. The two therefore address different files in the store
218
+ * for the same policy, which costs one duplicate and keeps each record's
219
+ * binding meaning exactly one thing.
220
+ */
221
+ export declare function attestedPolicyPayload(text: string): {
222
+ text: string;
223
+ };
224
+ /** The store hash {@link attestedPolicyPayload} addresses for these bytes. */
225
+ export declare function attestedPolicyPayloadHash(text: string): string;
204
226
  export declare function appendAttestation(logPath: string, policyPath: string, actor: string, options?: AttestOptions): AttestationAppendResult;
227
+ /**
228
+ * Is this record an attestation — a `policy.updated` carrying a `sha256`?
229
+ *
230
+ * `policy.updated` records **without** a `payload.sha256` are ignored for
231
+ * attestation purposes. Two reasons, one forward and one backward: the event
232
+ * type predates this verb (SPEC.md §8 has always had it, and the schema keeps
233
+ * it base-only with no required payload fields), so pre-attestation logs and
234
+ * hand-rolled tooling may carry `policy.updated` events that assert nothing
235
+ * about bytes; and treating such a record as an attestation would mean a
236
+ * payload-less event could *satisfy* the guard, which is exactly backwards.
237
+ * Ignoring them means an old log reads as `not-attested` — fail closed.
238
+ */
239
+ export declare function attestationSha256(record: EventRecord): string | null;
205
240
  /**
206
241
  * Compare the live policy file against the latest attestation in `records`.
207
242
  *
@@ -258,6 +293,23 @@ export declare function attestationRefusal(status: AttestationStatus): Attestati
258
293
  export declare function resolveHumanActor(options?: {
259
294
  actor?: string;
260
295
  }): string | null;
296
+ /**
297
+ * Who attested the policy at `seq`: the `actor` of the attestation record a
298
+ * status names, or `null` when no record at that seq is an attestation
299
+ * (APRV-449).
300
+ *
301
+ * `records` are VERIFIED records, as {@link checkAttestation}'s are. The answer
302
+ * is read from the log and never from the policy file: a tenant asking who set
303
+ * their starting policy is asking what was recorded, and an `approvers:` block
304
+ * in the file names who MAY decide, which is a different fact. Records are
305
+ * appended in seq order, so the record at index `seq - 1` is tried first and a
306
+ * scan is the fallback for a slice that does not start at genesis.
307
+ *
308
+ * The identity is config-declared (see {@link resolveHumanActor}), so this
309
+ * reports the identity the attesting process declared. It does not authenticate
310
+ * it, and a caller rendering it should not imply that it does.
311
+ */
312
+ export declare function attesterAt(records: readonly EventRecord[], seq: number): string | null;
261
313
  /**
262
314
  * The event an ORGAN attestation is: a human's sign-off on the exact bytes of
263
315
  * one of the gate's own organ files.
@@ -312,7 +364,7 @@ export declare const ORGAN_PATH_FIELD = "organ_path";
312
364
  * contents are the human's own ceremony surface, and any ordinary file, which is
313
365
  * not part of the gate and needs no attestation to be edited.
314
366
  */
315
- export declare const ORGAN_ATTEST_ERROR_CODES: readonly ["lock-timeout", "corrupt-tail", "validation", "canonicalization", "io", "head-moved", "actor-not-human", "path-not-organ", "path-is-policy"];
367
+ export declare const ORGAN_ATTEST_ERROR_CODES: readonly ["lock-timeout", "corrupt-tail", "validation", "canonicalization", "io", "head-moved", "daemon-id-invalid", "daemon-not-allowed", "actor-not-human", "path-not-organ", "path-is-policy"];
316
368
  export type OrganAttestErrorCode = (typeof ORGAN_ATTEST_ERROR_CODES)[number];
317
369
  export interface OrganAttestError {
318
370
  code: OrganAttestErrorCode;
@@ -418,3 +470,183 @@ export declare function findOrganAttestation(records: readonly EventRecord[], or
418
470
  * it: {@link findOrganAttestation} is the question the guard asks.
419
471
  */
420
472
  export declare function latestOrganAttestation(records: readonly EventRecord[], organPath: string): OrganAttestation | null;
473
+ /**
474
+ * The event a protected-path SIGN-OFF is: a human's statement that they read
475
+ * one `policy.edit` file at these exact bytes and stand behind them.
476
+ *
477
+ * ## The hole this fills
478
+ *
479
+ * SPEC.md's amendment-provenance rule says text that reached a protected file
480
+ * without a grant carries `(Amended APRV-n, pending sign-off.)` until a human
481
+ * ratifies it, and doubt resolves to pending. Nothing recorded the
482
+ * ratification. A human who had read a diff and agreed with it had exactly two
483
+ * ways to get it past the protected-path guard: re-make the edit under a grant,
484
+ * or change the guard. Both are the wrong shape — the first re-does work the
485
+ * human has already done with their eyes, and the second edits an enforcement
486
+ * path to admit one change.
487
+ *
488
+ * ## Why it is not the organ record, and not a grant either
489
+ *
490
+ * It sits between them, and the ordering is the point.
491
+ *
492
+ * A grant binds the exact hunk: `{before, after}` bytes a human saw in a
493
+ * prompt. It is the strongest evidence in the system about a CHANGE, and
494
+ * nothing here weakens it — {@link findPathSignOff} is asked only after the
495
+ * guard's hunk search has failed, so a covered change still passes on its
496
+ * grants and still prints them as its reason.
497
+ *
498
+ * {@link ORGAN_ATTESTATION_EVENT} is the other end. An organ is `policy.core`,
499
+ * a policy may resolve `policy.core` to `human-only`, and the gate mints
500
+ * nothing for a human-only class (§11.1 invariant 9), so for an organ content
501
+ * attestation is not the weaker evidence, it is the only evidence that can
502
+ * exist. A protected path is not in that position: a `policy.edit` grant for it
503
+ * is obtainable, so this record must never be the first thing a reader reaches
504
+ * for. A separate type is what makes that ordering structural rather than a
505
+ * convention every reader has to remember, and it is why the organ verb's
506
+ * refusals and this one's are not merged.
507
+ *
508
+ * The `gate.` prefix is deliberate: SPEC.md §8 keys the write-boundary clock on
509
+ * it, and a signer who could supply the moment could place a sign-off before
510
+ * bytes they had not yet seen.
511
+ */
512
+ export declare const PATH_SIGN_OFF_EVENT: "gate.path.signed_off";
513
+ /**
514
+ * The payload field naming which file was signed off.
515
+ *
516
+ * Repository-relative and `/`-separated, for the reason {@link ORGAN_PATH_FIELD}
517
+ * is: the log is copied and read on other machines, and an absolute path would
518
+ * leak the signer's home directory into a permanent record. Spelled `path`
519
+ * rather than `signed_path` because the record type already says what kind of
520
+ * statement it is, and a reader should not have to learn a second noun.
521
+ */
522
+ export declare const SIGN_OFF_PATH_FIELD = "path";
523
+ /**
524
+ * Why a sign-off was refused: the attestation codes, plus the three path rules
525
+ * this verb has of its own.
526
+ *
527
+ * Three and not one, because the repair differs in each case and a caller has
528
+ * to be able to tell them apart without reading prose:
529
+ *
530
+ * - `path-is-policy` — the policy file, whose sign-off is its attestation and
531
+ * which the gate reads on every operation. A second record that looked like
532
+ * a statement about the policy's bytes is exactly what the organ verb refuses
533
+ * to create, for the same reason.
534
+ * - `path-is-core` — a `policy.core` surface or the log directory. An organ is
535
+ * signed off with `--organ`, which is a different record under different
536
+ * rules; the approval home and the log are the human's own ceremony surface
537
+ * and are not ratified by any verb at all.
538
+ * - `path-not-protected` — an ordinary file, or a path that is not
539
+ * repository-relative. Nothing to ratify: an unprotected file's edits are not
540
+ * gated, so a record about them would assert authority over nothing.
541
+ */
542
+ export declare const PATH_SIGN_OFF_ERROR_CODES: readonly ["lock-timeout", "corrupt-tail", "validation", "canonicalization", "io", "head-moved", "daemon-id-invalid", "daemon-not-allowed", "actor-not-human", "path-not-protected", "path-is-policy", "path-is-core"];
543
+ export type PathSignOffErrorCode = (typeof PATH_SIGN_OFF_ERROR_CODES)[number];
544
+ export interface PathSignOffError {
545
+ code: PathSignOffErrorCode;
546
+ message: string;
547
+ /** Schema errors, present when `code` is "validation". */
548
+ errors?: ValidationError[];
549
+ }
550
+ /** {@link appendPathSignOff}'s result: the append's, widened by three codes. */
551
+ export type PathSignOffAppendResult = {
552
+ ok: true;
553
+ record: EventRecord;
554
+ line: string;
555
+ } | {
556
+ ok: false;
557
+ error: PathSignOffError;
558
+ };
559
+ /**
560
+ * Which file to sign off, in the two spellings that are not the same fact,
561
+ * plus the policy's own protected list.
562
+ *
563
+ * `path` is the identity the record carries and the checker matches on, so it
564
+ * is repository-relative; `root` is where that path is rooted on THIS machine
565
+ * and never reaches the log. `protectedPaths` is `policy.protected_paths`: a
566
+ * project that widened its protected surface may sign off the files it added,
567
+ * and a caller that omits the list gets the built-in set alone, which is the
568
+ * strictly NARROWER answer and therefore the fail-closed one.
569
+ */
570
+ export interface SignOffTarget {
571
+ /** Repository-relative, e.g. `SPEC.md`. */
572
+ path: string;
573
+ /** The checkout `path` is relative to; `join(root, path)` is hashed. */
574
+ root: string;
575
+ /** `policy.protected_paths`, widening which paths are signable. */
576
+ protectedPaths?: readonly ProtectedPathEntry[];
577
+ }
578
+ /** The identity and digest a sign-off carries, or `null`. */
579
+ export interface PathSignOffFields {
580
+ path: string;
581
+ sha256: string;
582
+ }
583
+ /** A sign-off found in the log, with the record it came from. */
584
+ export interface PathSignOff extends PathSignOffFields {
585
+ record: EventRecord;
586
+ }
587
+ /**
588
+ * Append a `gate.path.signed_off` record for `target` to `logPath`.
589
+ *
590
+ * The rules, refused as structured results and never as throws, in the order
591
+ * they are checked — and the order is normative, because a path can break more
592
+ * than one of them and the caller's repair depends on which they are told:
593
+ *
594
+ * 1. `actor` MUST match `^human:.+`. This is the verb an agent must not
595
+ * perform: a record the party under oversight could write would let it
596
+ * ratify its own text, which is the whole of what the pending-sign-off
597
+ * suffix exists to prevent (§11.1 invariants 4 and 9).
598
+ * 2. The path must be repository-relative: absolute paths and `..` are refused,
599
+ * because the recorded identity has to mean the same file on the machine
600
+ * that later reads the log.
601
+ * 3. The policy file is refused with its own code; it has its own attestation.
602
+ * 4. `policy.core` and `log.mutate` surfaces are refused with their own code.
603
+ * 5. What remains must classify `policy.edit` or a `policy.edit.*` sub-class.
604
+ * Anything else is not a protected path, and this verb is not a general
605
+ * "bless these bytes" primitive.
606
+ *
607
+ * Every one of those is decided BEFORE the file is read, so a path this verb
608
+ * would never sign off is refused whether or not it exists.
609
+ *
610
+ * The digest is computed by the runtime from the file's exact bytes. There is
611
+ * no parameter for it and none for `ts`, for the reason
612
+ * {@link appendOrganAttestation} has neither: a caller who could supply the
613
+ * hash could ratify bytes nobody read, and one who could supply the moment
614
+ * could backdate what they had seen when.
615
+ *
616
+ * Like the attestations this append carries no `expectedHead`: it reads nothing
617
+ * from the log, so it has no check-then-act window to close.
618
+ */
619
+ export declare function appendPathSignOff(logPath: string, target: SignOffTarget, actor: string, options?: AttestOptions): PathSignOffAppendResult;
620
+ /**
621
+ * Is this record a protected-path sign-off, and which bytes of which file does
622
+ * it stand behind?
623
+ *
624
+ * A record missing either field is ignored, for the reason a `policy.updated`
625
+ * with no `sha256` is ignored: a record that asserts nothing about bytes must
626
+ * never be able to SATISFY a check about bytes. Fail closed.
627
+ */
628
+ export declare function pathSignOffOf(record: EventRecord): PathSignOffFields | null;
629
+ /**
630
+ * The record in which a human signed off THIS digest FOR THIS path, latest
631
+ * first, or `null`.
632
+ *
633
+ * Both halves are required and that is the whole rule, exactly as it is for an
634
+ * organ: a digest signed for some other file is not evidence about this one,
635
+ * and bytes edited after a sign-off are bytes nobody signed. Two files that
636
+ * happen to hash alike are still two files, and a human who read one has said
637
+ * nothing about the other.
638
+ *
639
+ * There is no "latest wins" supersession. A sign-off stands for the bytes it
640
+ * names: the checker asks about a blob at a commit that may be old, and the
641
+ * newest sign-off of a file says nothing about whether an earlier state was
642
+ * once ratified.
643
+ */
644
+ export declare function findPathSignOff(records: readonly EventRecord[], path: string, sha256: string): EventRecord | null;
645
+ /**
646
+ * The most recent sign-off of `path` at ANY digest, or `null`.
647
+ *
648
+ * What a status surface needs to tell "this file was never signed off" from
649
+ * "this file was signed off and has been edited since". Nothing enforcing reads
650
+ * it: {@link findPathSignOff} is the question the guard asks.
651
+ */
652
+ export declare function latestPathSignOff(records: readonly EventRecord[], path: string): PathSignOff | null;
@@ -39,8 +39,10 @@ import { createHash } from "node:crypto";
39
39
  import { readFileSync } from "node:fs";
40
40
  import { basename, isAbsolute, join } from "node:path";
41
41
  import { tick } from "./clock.js";
42
- import { builtinProtectedPathClass, isGateOrganPath, normalizePathSpelling, } from "./command-class.js";
42
+ import { builtinProtectedPathClass, isGateOrganPath, normalizePathSpelling, POLICY_EDIT_SUBCLASS, protectedPathClass, } from "./command-class.js";
43
43
  import { APPEND_ERROR_CODES, appendEvent, } from "./log.js";
44
+ import { payloadHash } from "./payload.js";
45
+ import { payloadStoreDirFor, storePayload } from "./payload-store.js";
44
46
  /**
45
47
  * Actors permitted to attest. Deliberately narrower than the event schema's
46
48
  * `^(human|agent|system):.+`: attestation is the one verb an agent must not be
@@ -170,6 +172,29 @@ export function policyBytesHash(bytes) {
170
172
  * moment, and the *latest* attestation is the one `checkAttestation` honors.
171
173
  * Passing a precondition here would only manufacture spurious failures.
172
174
  */
175
+ /**
176
+ * The value a terminal attestation stores for the bytes it attests (APRV-356).
177
+ *
178
+ * One author for the shape, because two parties need it and they must agree:
179
+ * {@link appendAttestation} writes it, and `cli/amend.ts` has to name the file
180
+ * it will land in before the append happens, so the ceremony commit can carry
181
+ * it. A second spelling of `{ text }` anywhere would be a store file the
182
+ * ceremony quietly left behind.
183
+ *
184
+ * Deliberately narrower than `proposalPayloadValue` in `core/policy-proposal.ts`,
185
+ * which carries `policy_path` beside the text: a proposal is a prompt and names
186
+ * the file it is asking about, while an attestation is a binding and the bytes
187
+ * are the whole of it. The two therefore address different files in the store
188
+ * for the same policy, which costs one duplicate and keeps each record's
189
+ * binding meaning exactly one thing.
190
+ */
191
+ export function attestedPolicyPayload(text) {
192
+ return { text };
193
+ }
194
+ /** The store hash {@link attestedPolicyPayload} addresses for these bytes. */
195
+ export function attestedPolicyPayloadHash(text) {
196
+ return payloadHash(attestedPolicyPayload(text));
197
+ }
173
198
  export function appendAttestation(logPath, policyPath, actor, options = {}) {
174
199
  if (!HUMAN_ACTOR.test(actor)) {
175
200
  return {
@@ -181,11 +206,14 @@ export function appendAttestation(logPath, policyPath, actor, options = {}) {
181
206
  };
182
207
  }
183
208
  let sha256;
209
+ let text;
184
210
  try {
185
- // One read supplies both the expected-digest check and the recorded digest.
186
- // A later edit naturally leaves the live policy hash-mismatched; it cannot
187
- // make this record attest bytes other than the ones checked here.
188
- sha256 = policyBytesHash(readFileSync(policyPath));
211
+ // One read supplies the expected-digest check, the recorded digest AND the
212
+ // bytes that go to the store (APRV-356). One read and not three, because
213
+ // three could disagree with each other about a file being edited.
214
+ const bytes = readFileSync(policyPath);
215
+ sha256 = policyBytesHash(bytes);
216
+ text = bytes.toString("utf8");
189
217
  }
190
218
  catch (cause) {
191
219
  return {
@@ -205,11 +233,40 @@ export function appendAttestation(logPath, policyPath, actor, options = {}) {
205
233
  },
206
234
  };
207
235
  }
236
+ // APRV-356. The attested BYTES go to the content-addressed store and the
237
+ // record binds their hash, so the policy in force is recoverable from any
238
+ // chain attested after this lands.
239
+ //
240
+ // Before it, only an attestation that answered a phone PROPOSAL left
241
+ // recoverable bytes (`policy.proposed` binds the whole text, SPEC.md §10.4),
242
+ // and a chain only ever attested at a terminal — this repository included —
243
+ // had none. `inForcePolicyText` then failed, and the privileged-gesture rule
244
+ // of APRV-324 fell back to refusing, which is safe and leaves the residual
245
+ // that an amendment REMOVING a sender mapping is indistinguishable from a
246
+ // policy that never had one.
247
+ //
248
+ // A store that cannot be written REFUSES the attestation, and nothing is
249
+ // appended. That is the fail-closed direction and it is deliberate: a record
250
+ // claiming a binding whose bytes are absent would be a worse artifact than no
251
+ // record, and the store sits beside the log, which this verb is about to
252
+ // write anyway. The alternative considered — append without the binding, as
253
+ // before — was rejected because it would make the recoverability guarantee
254
+ // conditional on a condition no reader of the record can see.
255
+ const stored = storePayload(payloadStoreDirFor(logPath), attestedPolicyPayload(text));
256
+ if (!stored.ok) {
257
+ return {
258
+ ok: false,
259
+ error: {
260
+ code: "io",
261
+ message: `the attested bytes of ${basename(policyPath)} could not be stored beside the log (${stored.message}); the log was left unchanged, because an attestation whose bytes are unrecoverable is the state this verb exists to end`,
262
+ },
263
+ };
264
+ }
208
265
  return appendEvent(logPath, {
209
266
  ts: tick(options),
210
267
  event: "policy.updated",
211
268
  actor,
212
- payload: { policy_path: basename(policyPath), sha256 },
269
+ payload: { policy_path: basename(policyPath), sha256, payload_hash: stored.hash },
213
270
  }, options);
214
271
  }
215
272
  /**
@@ -224,7 +281,7 @@ export function appendAttestation(logPath, policyPath, actor, options = {}) {
224
281
  * payload-less event could *satisfy* the guard, which is exactly backwards.
225
282
  * Ignoring them means an old log reads as `not-attested` — fail closed.
226
283
  */
227
- function attestationSha256(record) {
284
+ export function attestationSha256(record) {
228
285
  if (record.event !== "policy.updated")
229
286
  return null;
230
287
  const payload = record.payload;
@@ -353,6 +410,31 @@ export function resolveHumanActor(options = {}) {
353
410
  return fromEnv;
354
411
  return null;
355
412
  }
413
+ /**
414
+ * Who attested the policy at `seq`: the `actor` of the attestation record a
415
+ * status names, or `null` when no record at that seq is an attestation
416
+ * (APRV-449).
417
+ *
418
+ * `records` are VERIFIED records, as {@link checkAttestation}'s are. The answer
419
+ * is read from the log and never from the policy file: a tenant asking who set
420
+ * their starting policy is asking what was recorded, and an `approvers:` block
421
+ * in the file names who MAY decide, which is a different fact. Records are
422
+ * appended in seq order, so the record at index `seq - 1` is tried first and a
423
+ * scan is the fallback for a slice that does not start at genesis.
424
+ *
425
+ * The identity is config-declared (see {@link resolveHumanActor}), so this
426
+ * reports the identity the attesting process declared. It does not authenticate
427
+ * it, and a caller rendering it should not imply that it does.
428
+ */
429
+ export function attesterAt(records, seq) {
430
+ const direct = records[seq - 1];
431
+ const record = direct !== undefined && direct.seq === seq
432
+ ? direct
433
+ : records.find((candidate) => candidate.seq === seq);
434
+ if (record === undefined || attestationSha256(record) === null)
435
+ return null;
436
+ return record.actor;
437
+ }
356
438
  // ===========================================================================
357
439
  // Organ attestation (APRV-272, amended SPEC.md §5.2 and §8)
358
440
  // ===========================================================================
@@ -586,4 +668,257 @@ export function latestOrganAttestation(records, organPath) {
586
668
  }
587
669
  return null;
588
670
  }
671
+ // ===========================================================================
672
+ // Protected-path sign-off (APRV-338, amended SPEC.md §5.2, §8 and §10.1)
673
+ // ===========================================================================
674
+ /**
675
+ * The event a protected-path SIGN-OFF is: a human's statement that they read
676
+ * one `policy.edit` file at these exact bytes and stand behind them.
677
+ *
678
+ * ## The hole this fills
679
+ *
680
+ * SPEC.md's amendment-provenance rule says text that reached a protected file
681
+ * without a grant carries `(Amended APRV-n, pending sign-off.)` until a human
682
+ * ratifies it, and doubt resolves to pending. Nothing recorded the
683
+ * ratification. A human who had read a diff and agreed with it had exactly two
684
+ * ways to get it past the protected-path guard: re-make the edit under a grant,
685
+ * or change the guard. Both are the wrong shape — the first re-does work the
686
+ * human has already done with their eyes, and the second edits an enforcement
687
+ * path to admit one change.
688
+ *
689
+ * ## Why it is not the organ record, and not a grant either
690
+ *
691
+ * It sits between them, and the ordering is the point.
692
+ *
693
+ * A grant binds the exact hunk: `{before, after}` bytes a human saw in a
694
+ * prompt. It is the strongest evidence in the system about a CHANGE, and
695
+ * nothing here weakens it — {@link findPathSignOff} is asked only after the
696
+ * guard's hunk search has failed, so a covered change still passes on its
697
+ * grants and still prints them as its reason.
698
+ *
699
+ * {@link ORGAN_ATTESTATION_EVENT} is the other end. An organ is `policy.core`,
700
+ * a policy may resolve `policy.core` to `human-only`, and the gate mints
701
+ * nothing for a human-only class (§11.1 invariant 9), so for an organ content
702
+ * attestation is not the weaker evidence, it is the only evidence that can
703
+ * exist. A protected path is not in that position: a `policy.edit` grant for it
704
+ * is obtainable, so this record must never be the first thing a reader reaches
705
+ * for. A separate type is what makes that ordering structural rather than a
706
+ * convention every reader has to remember, and it is why the organ verb's
707
+ * refusals and this one's are not merged.
708
+ *
709
+ * The `gate.` prefix is deliberate: SPEC.md §8 keys the write-boundary clock on
710
+ * it, and a signer who could supply the moment could place a sign-off before
711
+ * bytes they had not yet seen.
712
+ */
713
+ export const PATH_SIGN_OFF_EVENT = "gate.path.signed_off";
714
+ /**
715
+ * The payload field naming which file was signed off.
716
+ *
717
+ * Repository-relative and `/`-separated, for the reason {@link ORGAN_PATH_FIELD}
718
+ * is: the log is copied and read on other machines, and an absolute path would
719
+ * leak the signer's home directory into a permanent record. Spelled `path`
720
+ * rather than `signed_path` because the record type already says what kind of
721
+ * statement it is, and a reader should not have to learn a second noun.
722
+ */
723
+ export const SIGN_OFF_PATH_FIELD = "path";
724
+ /**
725
+ * Why a sign-off was refused: the attestation codes, plus the three path rules
726
+ * this verb has of its own.
727
+ *
728
+ * Three and not one, because the repair differs in each case and a caller has
729
+ * to be able to tell them apart without reading prose:
730
+ *
731
+ * - `path-is-policy` — the policy file, whose sign-off is its attestation and
732
+ * which the gate reads on every operation. A second record that looked like
733
+ * a statement about the policy's bytes is exactly what the organ verb refuses
734
+ * to create, for the same reason.
735
+ * - `path-is-core` — a `policy.core` surface or the log directory. An organ is
736
+ * signed off with `--organ`, which is a different record under different
737
+ * rules; the approval home and the log are the human's own ceremony surface
738
+ * and are not ratified by any verb at all.
739
+ * - `path-not-protected` — an ordinary file, or a path that is not
740
+ * repository-relative. Nothing to ratify: an unprotected file's edits are not
741
+ * gated, so a record about them would assert authority over nothing.
742
+ */
743
+ export const PATH_SIGN_OFF_ERROR_CODES = [
744
+ ...ATTEST_ERROR_CODES,
745
+ "path-not-protected",
746
+ "path-is-policy",
747
+ "path-is-core",
748
+ ];
749
+ /**
750
+ * Append a `gate.path.signed_off` record for `target` to `logPath`.
751
+ *
752
+ * The rules, refused as structured results and never as throws, in the order
753
+ * they are checked — and the order is normative, because a path can break more
754
+ * than one of them and the caller's repair depends on which they are told:
755
+ *
756
+ * 1. `actor` MUST match `^human:.+`. This is the verb an agent must not
757
+ * perform: a record the party under oversight could write would let it
758
+ * ratify its own text, which is the whole of what the pending-sign-off
759
+ * suffix exists to prevent (§11.1 invariants 4 and 9).
760
+ * 2. The path must be repository-relative: absolute paths and `..` are refused,
761
+ * because the recorded identity has to mean the same file on the machine
762
+ * that later reads the log.
763
+ * 3. The policy file is refused with its own code; it has its own attestation.
764
+ * 4. `policy.core` and `log.mutate` surfaces are refused with their own code.
765
+ * 5. What remains must classify `policy.edit` or a `policy.edit.*` sub-class.
766
+ * Anything else is not a protected path, and this verb is not a general
767
+ * "bless these bytes" primitive.
768
+ *
769
+ * Every one of those is decided BEFORE the file is read, so a path this verb
770
+ * would never sign off is refused whether or not it exists.
771
+ *
772
+ * The digest is computed by the runtime from the file's exact bytes. There is
773
+ * no parameter for it and none for `ts`, for the reason
774
+ * {@link appendOrganAttestation} has neither: a caller who could supply the
775
+ * hash could ratify bytes nobody read, and one who could supply the moment
776
+ * could backdate what they had seen when.
777
+ *
778
+ * Like the attestations this append carries no `expectedHead`: it reads nothing
779
+ * from the log, so it has no check-then-act window to close.
780
+ */
781
+ export function appendPathSignOff(logPath, target, actor, options = {}) {
782
+ if (!HUMAN_ACTOR.test(actor)) {
783
+ return {
784
+ ok: false,
785
+ error: {
786
+ code: "actor-not-human",
787
+ message: `signing off a protected path requires a human actor matching ^human:.+, got ${JSON.stringify(actor)}; this record is what resolves the pending-sign-off suffix, so an agent that could write one could ratify its own text, and the log was left unchanged`,
788
+ },
789
+ };
790
+ }
791
+ const refusal = signOffPathRefusal(target.path, target.protectedPaths ?? []);
792
+ if (refusal !== null)
793
+ return { ok: false, error: refusal };
794
+ const path = normalizePathSpelling(target.path);
795
+ let sha256;
796
+ try {
797
+ sha256 = policyFileHash(join(target.root, path));
798
+ }
799
+ catch (cause) {
800
+ return {
801
+ ok: false,
802
+ error: {
803
+ code: "io",
804
+ message: `protected path ${path} could not be read under ${target.root} for sign-off: ${detail(cause)}`,
805
+ },
806
+ };
807
+ }
808
+ return appendEvent(logPath, {
809
+ ts: tick(options),
810
+ event: PATH_SIGN_OFF_EVENT,
811
+ actor,
812
+ payload: { [SIGN_OFF_PATH_FIELD]: path, sha256 },
813
+ }, options);
814
+ }
815
+ /** Rules 2 to 5 of {@link appendPathSignOff}, or `null` when the path passes. */
816
+ function signOffPathRefusal(candidate, extra) {
817
+ const normalized = normalizePathSpelling(candidate);
818
+ if (normalized.length === 0 || isAbsolute(candidate) || normalized.split("/").includes("..")) {
819
+ return {
820
+ code: "path-not-protected",
821
+ message: `${JSON.stringify(candidate)} is not a repository-relative path; a sign-off records the path itself, so it must name the same file on every machine that later reads the log`,
822
+ };
823
+ }
824
+ const builtin = builtinProtectedPathClass(normalized);
825
+ if (builtin === "policy.core") {
826
+ const last = normalized.split("/").pop() ?? normalized;
827
+ if (POLICY_FILE_NAMES.includes(last)) {
828
+ return {
829
+ code: "path-is-policy",
830
+ message: `${normalized} is the policy file, whose sign-off is its attestation: \`approval policy attest\` with no --path, which the gate reads on every operation. A second record about those bytes would be a thing that looks like an attestation and is not one`,
831
+ };
832
+ }
833
+ return {
834
+ code: "path-is-core",
835
+ message: isGateOrganPath(normalized)
836
+ ? `${normalized} is one of the gate's organs, which is signed off by content with \`approval policy attest --organ ${normalized}\`; that is a different record under different rules, because an organ is policy.core and can have no grant at all, while a protected path can`
837
+ : `${normalized} is inside the approval home, which is the human's own ceremony surface and is ratified by no verb`,
838
+ };
839
+ }
840
+ if (builtin === "log.mutate") {
841
+ return {
842
+ code: "path-is-core",
843
+ message: `${normalized} is inside the log directory, which no verb of this runtime signs off: the log is append-only and its contents are evidence rather than text anyone ratifies`,
844
+ };
845
+ }
846
+ const routed = protectedPathClass(normalized, extra);
847
+ if (routed === "policy.edit" || (routed !== null && POLICY_EDIT_SUBCLASS.test(routed))) {
848
+ return null;
849
+ }
850
+ return {
851
+ code: "path-not-protected",
852
+ message: `${normalized} does not classify policy.edit or a policy.edit.* sub-class${extra.length === 0 ? " under the built-in protected set (no policy.protected_paths were supplied, which is the narrower reading)" : " under the built-in protected set widened by policy.protected_paths"}, so it is not a protected path and nothing about it is gated; a sign-off on it would assert authority over nothing`,
853
+ };
854
+ }
855
+ /**
856
+ * Is this record a protected-path sign-off, and which bytes of which file does
857
+ * it stand behind?
858
+ *
859
+ * A record missing either field is ignored, for the reason a `policy.updated`
860
+ * with no `sha256` is ignored: a record that asserts nothing about bytes must
861
+ * never be able to SATISFY a check about bytes. Fail closed.
862
+ */
863
+ export function pathSignOffOf(record) {
864
+ if (record.event !== PATH_SIGN_OFF_EVENT)
865
+ return null;
866
+ const payload = record.payload;
867
+ if (payload === undefined)
868
+ return null;
869
+ const path = payload[SIGN_OFF_PATH_FIELD];
870
+ const sha256 = payload["sha256"];
871
+ if (typeof path !== "string" || typeof sha256 !== "string")
872
+ return null;
873
+ const normalized = normalizePathSpelling(path);
874
+ if (normalized.length === 0)
875
+ return null;
876
+ return { path: normalized, sha256 };
877
+ }
878
+ /**
879
+ * The record in which a human signed off THIS digest FOR THIS path, latest
880
+ * first, or `null`.
881
+ *
882
+ * Both halves are required and that is the whole rule, exactly as it is for an
883
+ * organ: a digest signed for some other file is not evidence about this one,
884
+ * and bytes edited after a sign-off are bytes nobody signed. Two files that
885
+ * happen to hash alike are still two files, and a human who read one has said
886
+ * nothing about the other.
887
+ *
888
+ * There is no "latest wins" supersession. A sign-off stands for the bytes it
889
+ * names: the checker asks about a blob at a commit that may be old, and the
890
+ * newest sign-off of a file says nothing about whether an earlier state was
891
+ * once ratified.
892
+ */
893
+ export function findPathSignOff(records, path, sha256) {
894
+ const want = normalizePathSpelling(path);
895
+ for (let index = records.length - 1; index >= 0; index -= 1) {
896
+ const record = records[index];
897
+ const fields = pathSignOffOf(record);
898
+ if (fields === null)
899
+ continue;
900
+ if (fields.path === want && fields.sha256 === sha256)
901
+ return record;
902
+ }
903
+ return null;
904
+ }
905
+ /**
906
+ * The most recent sign-off of `path` at ANY digest, or `null`.
907
+ *
908
+ * What a status surface needs to tell "this file was never signed off" from
909
+ * "this file was signed off and has been edited since". Nothing enforcing reads
910
+ * it: {@link findPathSignOff} is the question the guard asks.
911
+ */
912
+ export function latestPathSignOff(records, path) {
913
+ const want = normalizePathSpelling(path);
914
+ for (let index = records.length - 1; index >= 0; index -= 1) {
915
+ const record = records[index];
916
+ const fields = pathSignOffOf(record);
917
+ if (fields === null)
918
+ continue;
919
+ if (fields.path === want)
920
+ return { ...fields, record };
921
+ }
922
+ return null;
923
+ }
589
924
  //# sourceMappingURL=attest.js.map