approval-md 0.1.0 → 0.3.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 (416) hide show
  1. package/README.md +629 -559
  2. package/SPEC.md +99 -24
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +656 -0
  27. package/dist/src/channels/contract.js +200 -7
  28. package/dist/src/channels/contract.js.map +1 -1
  29. package/dist/src/channels/payload-view.d.ts +35 -0
  30. package/dist/src/channels/render-queue.d.ts +149 -0
  31. package/dist/src/channels/tagging.d.ts +196 -0
  32. package/dist/src/channels/telegram.d.ts +1944 -0
  33. package/dist/src/channels/telegram.js +218 -23
  34. package/dist/src/channels/telegram.js.map +1 -1
  35. package/dist/src/channels/web.d.ts +350 -0
  36. package/dist/src/channels/web.js +17 -0
  37. package/dist/src/channels/web.js.map +1 -1
  38. package/dist/src/cli/adapter.d.ts +90 -0
  39. package/dist/src/cli/adapter.js +25 -15
  40. package/dist/src/cli/adapter.js.map +1 -1
  41. package/dist/src/cli/amend.d.ts +59 -0
  42. package/dist/src/cli/amend.js +214 -30
  43. package/dist/src/cli/amend.js.map +1 -1
  44. package/dist/src/cli/args.d.ts +43 -0
  45. package/dist/src/cli/attest.d.ts +50 -0
  46. package/dist/src/cli/attest.js +134 -7
  47. package/dist/src/cli/attest.js.map +1 -1
  48. package/dist/src/cli/audit-card.d.ts +62 -0
  49. package/dist/src/cli/audit.d.ts +59 -0
  50. package/dist/src/cli/channel-telegram.d.ts +879 -0
  51. package/dist/src/cli/channel-telegram.js +311 -13
  52. package/dist/src/cli/channel-telegram.js.map +1 -1
  53. package/dist/src/cli/channel-web.d.ts +131 -0
  54. package/dist/src/cli/channel.d.ts +80 -0
  55. package/dist/src/cli/channel.js +9 -0
  56. package/dist/src/cli/channel.js.map +1 -1
  57. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  58. package/dist/src/cli/codex-bridge.d.ts +819 -0
  59. package/dist/src/cli/codex-bridge.js +1607 -0
  60. package/dist/src/cli/codex-bridge.js.map +1 -0
  61. package/dist/src/cli/codex.d.ts +2 -0
  62. package/dist/src/cli/codex.js +469 -0
  63. package/dist/src/cli/codex.js.map +1 -0
  64. package/dist/src/cli/coverage.d.ts +61 -0
  65. package/dist/src/cli/daemon.d.ts +120 -0
  66. package/dist/src/cli/daemon.js +4 -1
  67. package/dist/src/cli/daemon.js.map +1 -1
  68. package/dist/src/cli/doctor.d.ts +129 -0
  69. package/dist/src/cli/doctor.js +586 -17
  70. package/dist/src/cli/doctor.js.map +1 -1
  71. package/dist/src/cli/env.d.ts +65 -0
  72. package/dist/src/cli/execute.d.ts +202 -0
  73. package/dist/src/cli/execute.js +25 -2
  74. package/dist/src/cli/execute.js.map +1 -1
  75. package/dist/src/cli/exit-codes.d.ts +73 -0
  76. package/dist/src/cli/feedback.d.ts +60 -0
  77. package/dist/src/cli/gate-window.d.ts +40 -0
  78. package/dist/src/cli/gate.d.ts +68 -0
  79. package/dist/src/cli/git-scope.d.ts +190 -0
  80. package/dist/src/cli/gloss-attach.d.ts +85 -0
  81. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  82. package/dist/src/cli/gloss-codex.d.ts +24 -0
  83. package/dist/src/cli/gloss-options.d.ts +42 -0
  84. package/dist/src/cli/gloss.d.ts +265 -0
  85. package/dist/src/cli/help.d.ts +107 -0
  86. package/dist/src/cli/help.js +320 -93
  87. package/dist/src/cli/help.js.map +1 -1
  88. package/dist/src/cli/hook-codex.d.ts +126 -0
  89. package/dist/src/cli/hook-codex.js +226 -0
  90. package/dist/src/cli/hook-codex.js.map +1 -0
  91. package/dist/src/cli/hook.d.ts +787 -0
  92. package/dist/src/cli/hook.js +1235 -181
  93. package/dist/src/cli/hook.js.map +1 -1
  94. package/dist/src/cli/import.d.ts +35 -0
  95. package/dist/src/cli/import.js +1 -1
  96. package/dist/src/cli/import.js.map +1 -1
  97. package/dist/src/cli/init.d.ts +84 -0
  98. package/dist/src/cli/init.js +2 -2
  99. package/dist/src/cli/init.js.map +1 -1
  100. package/dist/src/cli/instructions.d.ts +23 -0
  101. package/dist/src/cli/journal.d.ts +41 -0
  102. package/dist/src/cli/log-advance.d.ts +287 -0
  103. package/dist/src/cli/log-advance.js +102 -11
  104. package/dist/src/cli/log-advance.js.map +1 -1
  105. package/dist/src/cli/log-anchor.d.ts +176 -0
  106. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  107. package/dist/src/cli/log-sync.d.ts +243 -0
  108. package/dist/src/cli/log-verbs.d.ts +16 -0
  109. package/dist/src/cli/log-verbs.js +7 -1
  110. package/dist/src/cli/log-verbs.js.map +1 -1
  111. package/dist/src/cli/long-help.d.ts +70 -0
  112. package/dist/src/cli/main.d.ts +77 -0
  113. package/dist/src/cli/main.js +159 -7
  114. package/dist/src/cli/main.js.map +1 -1
  115. package/dist/src/cli/mcp.d.ts +52 -0
  116. package/dist/src/cli/paths.d.ts +56 -0
  117. package/dist/src/cli/payload.d.ts +58 -0
  118. package/dist/src/cli/policy-apply.d.ts +195 -0
  119. package/dist/src/cli/policy-apply.js +573 -0
  120. package/dist/src/cli/policy-apply.js.map +1 -0
  121. package/dist/src/cli/policy.d.ts +43 -0
  122. package/dist/src/cli/policy.js +14 -1
  123. package/dist/src/cli/policy.js.map +1 -1
  124. package/dist/src/cli/preflight.d.ts +501 -0
  125. package/dist/src/cli/preflight.js +689 -45
  126. package/dist/src/cli/preflight.js.map +1 -1
  127. package/dist/src/cli/progress.d.ts +78 -0
  128. package/dist/src/cli/prompt.d.ts +209 -0
  129. package/dist/src/cli/quickstart.d.ts +46 -0
  130. package/dist/src/cli/quickstart.js +297 -0
  131. package/dist/src/cli/quickstart.js.map +1 -0
  132. package/dist/src/cli/records.d.ts +34 -0
  133. package/dist/src/cli/render.d.ts +22 -0
  134. package/dist/src/cli/sandbox.d.ts +51 -0
  135. package/dist/src/cli/sandbox.js +17 -1
  136. package/dist/src/cli/sandbox.js.map +1 -1
  137. package/dist/src/cli/scaffold.d.ts +79 -0
  138. package/dist/src/cli/scaffold.js +1 -1
  139. package/dist/src/cli/setup-adapter.d.ts +137 -0
  140. package/dist/src/cli/setup-adapter.js +38 -4
  141. package/dist/src/cli/setup-adapter.js.map +1 -1
  142. package/dist/src/cli/setup-channel.d.ts +126 -0
  143. package/dist/src/cli/setup-channel.js +28 -1
  144. package/dist/src/cli/setup-channel.js.map +1 -1
  145. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  146. package/dist/src/cli/setup-common.d.ts +277 -0
  147. package/dist/src/cli/setup-common.js +3 -2
  148. package/dist/src/cli/setup-common.js.map +1 -1
  149. package/dist/src/cli/setup-flow.d.ts +287 -0
  150. package/dist/src/cli/setup-service.d.ts +96 -0
  151. package/dist/src/cli/setup.d.ts +204 -0
  152. package/dist/src/cli/setup.js +94 -2
  153. package/dist/src/cli/setup.js.map +1 -1
  154. package/dist/src/cli/style.d.ts +320 -0
  155. package/dist/src/cli/token.d.ts +39 -0
  156. package/dist/src/cli/up.d.ts +155 -0
  157. package/dist/src/cli/up.js +119 -53
  158. package/dist/src/cli/up.js.map +1 -1
  159. package/dist/src/cli/usage.d.ts +37 -0
  160. package/dist/src/cli/values.d.ts +40 -0
  161. package/dist/src/cli/values.js +3 -4
  162. package/dist/src/cli/values.js.map +1 -1
  163. package/dist/src/cli/vault.d.ts +59 -0
  164. package/dist/src/cli/vault.js +2 -2
  165. package/dist/src/cli/vault.js.map +1 -1
  166. package/dist/src/cli/verb-registry.d.ts +76 -0
  167. package/dist/src/cli/verb-registry.js +344 -11
  168. package/dist/src/cli/verb-registry.js.map +1 -1
  169. package/dist/src/cli/wordmark.d.ts +31 -0
  170. package/dist/src/cli/wordmark.js +2 -2
  171. package/dist/src/codex/broker.d.ts +229 -0
  172. package/dist/src/codex/broker.js +548 -0
  173. package/dist/src/codex/broker.js.map +1 -0
  174. package/dist/src/codex/doctor.d.ts +13 -0
  175. package/dist/src/codex/doctor.js +41 -0
  176. package/dist/src/codex/doctor.js.map +1 -0
  177. package/dist/src/codex/manifest.d.ts +49 -0
  178. package/dist/src/codex/manifest.js +103 -0
  179. package/dist/src/codex/manifest.js.map +1 -0
  180. package/dist/src/codex/runner.d.ts +178 -0
  181. package/dist/src/codex/runner.js +231 -0
  182. package/dist/src/codex/runner.js.map +1 -0
  183. package/dist/src/codex/serve.d.ts +56 -0
  184. package/dist/src/codex/serve.js +98 -0
  185. package/dist/src/codex/serve.js.map +1 -0
  186. package/dist/src/codex/templates.d.ts +41 -0
  187. package/dist/src/codex/templates.js +319 -0
  188. package/dist/src/codex/templates.js.map +1 -0
  189. package/dist/src/codex/trust.d.ts +19 -0
  190. package/dist/src/codex/trust.js +183 -0
  191. package/dist/src/codex/trust.js.map +1 -0
  192. package/dist/src/codex/workspace-commit.d.ts +219 -0
  193. package/dist/src/codex/workspace-commit.js +549 -0
  194. package/dist/src/codex/workspace-commit.js.map +1 -0
  195. package/dist/src/codex/workspace-plan.d.ts +131 -0
  196. package/dist/src/codex/workspace-plan.js +561 -0
  197. package/dist/src/codex/workspace-plan.js.map +1 -0
  198. package/dist/src/core/actor.d.ts +2 -0
  199. package/dist/src/core/actor.js +5 -0
  200. package/dist/src/core/actor.js.map +1 -0
  201. package/dist/src/core/advance-cycle.d.ts +221 -0
  202. package/dist/src/core/advance-cycle.js +66 -2
  203. package/dist/src/core/advance-cycle.js.map +1 -1
  204. package/dist/src/core/agents-md.d.ts +278 -0
  205. package/dist/src/core/agents-md.js +33 -31
  206. package/dist/src/core/agents-md.js.map +1 -1
  207. package/dist/src/core/apply-patch.d.ts +49 -0
  208. package/dist/src/core/apply-patch.js +266 -0
  209. package/dist/src/core/apply-patch.js.map +1 -0
  210. package/dist/src/core/attest.d.ts +635 -0
  211. package/dist/src/core/attest.js +326 -4
  212. package/dist/src/core/attest.js.map +1 -1
  213. package/dist/src/core/audit.d.ts +510 -0
  214. package/dist/src/core/audit.js +13 -0
  215. package/dist/src/core/audit.js.map +1 -1
  216. package/dist/src/core/budgets.d.ts +238 -0
  217. package/dist/src/core/channel-owner.d.ts +213 -0
  218. package/dist/src/core/channel-owner.js +358 -0
  219. package/dist/src/core/channel-owner.js.map +1 -0
  220. package/dist/src/core/checkpoint.d.ts +500 -0
  221. package/dist/src/core/child-env.d.ts +88 -0
  222. package/dist/src/core/clock.d.ts +52 -0
  223. package/dist/src/core/command-class.d.ts +697 -0
  224. package/dist/src/core/command-class.js +713 -25
  225. package/dist/src/core/command-class.js.map +1 -1
  226. package/dist/src/core/commit-guard.d.ts +272 -0
  227. package/dist/src/core/commit-guard.js +424 -0
  228. package/dist/src/core/commit-guard.js.map +1 -0
  229. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  230. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  231. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  232. package/dist/src/core/coverage.d.ts +217 -0
  233. package/dist/src/core/credential-spec.d.ts +72 -0
  234. package/dist/src/core/daemon-actor.d.ts +45 -0
  235. package/dist/src/core/daemon-actor.js +54 -0
  236. package/dist/src/core/daemon-actor.js.map +1 -0
  237. package/dist/src/core/dark-session.d.ts +432 -0
  238. package/dist/src/core/dark-session.js +266 -82
  239. package/dist/src/core/dark-session.js.map +1 -1
  240. package/dist/src/core/decision-refusal.d.ts +206 -0
  241. package/dist/src/core/decision-refusal.js +24 -2
  242. package/dist/src/core/decision-refusal.js.map +1 -1
  243. package/dist/src/core/env-file.d.ts +455 -0
  244. package/dist/src/core/env-file.js +60 -1
  245. package/dist/src/core/env-file.js.map +1 -1
  246. package/dist/src/core/execute.d.ts +871 -0
  247. package/dist/src/core/execute.js +59 -8
  248. package/dist/src/core/execute.js.map +1 -1
  249. package/dist/src/core/frontmatter.d.ts +78 -0
  250. package/dist/src/core/gate-window.d.ts +312 -0
  251. package/dist/src/core/gate.d.ts +1449 -0
  252. package/dist/src/core/gate.js +149 -14
  253. package/dist/src/core/gate.js.map +1 -1
  254. package/dist/src/core/gesture-refusal.d.ts +166 -0
  255. package/dist/src/core/gesture-refusal.js +188 -0
  256. package/dist/src/core/gesture-refusal.js.map +1 -0
  257. package/dist/src/core/git-run.d.ts +73 -0
  258. package/dist/src/core/harness-version.d.ts +157 -0
  259. package/dist/src/core/harness-version.js +4 -1
  260. package/dist/src/core/harness-version.js.map +1 -1
  261. package/dist/src/core/harness-wait.d.ts +55 -0
  262. package/dist/src/core/head-retry.d.ts +107 -0
  263. package/dist/src/core/instance.d.ts +310 -0
  264. package/dist/src/core/instance.js +113 -0
  265. package/dist/src/core/instance.js.map +1 -1
  266. package/dist/src/core/intake-limits.d.ts +247 -0
  267. package/dist/src/core/jcs.d.ts +52 -0
  268. package/dist/src/core/journal.d.ts +144 -0
  269. package/dist/src/core/live-draw.d.ts +436 -0
  270. package/dist/src/core/log-reconcile.d.ts +89 -0
  271. package/dist/src/core/log-subscribe.d.ts +36 -0
  272. package/dist/src/core/log-subscribe.js +162 -0
  273. package/dist/src/core/log-subscribe.js.map +1 -0
  274. package/dist/src/core/log.d.ts +316 -0
  275. package/dist/src/core/log.js.map +1 -1
  276. package/dist/src/core/loop.d.ts +274 -0
  277. package/dist/src/core/loop.js +11 -0
  278. package/dist/src/core/loop.js.map +1 -1
  279. package/dist/src/core/md-fence.d.ts +41 -0
  280. package/dist/src/core/money.d.ts +147 -0
  281. package/dist/src/core/payload-census.d.ts +74 -0
  282. package/dist/src/core/payload-store.d.ts +175 -0
  283. package/dist/src/core/payload.d.ts +71 -0
  284. package/dist/src/core/policy-diff.d.ts +292 -0
  285. package/dist/src/core/policy-diff.js +27 -4
  286. package/dist/src/core/policy-diff.js.map +1 -1
  287. package/dist/src/core/policy-expectations.d.ts +199 -0
  288. package/dist/src/core/policy-explain.d.ts +160 -0
  289. package/dist/src/core/policy-explain.js +63 -3
  290. package/dist/src/core/policy-explain.js.map +1 -1
  291. package/dist/src/core/policy-load.d.ts +567 -0
  292. package/dist/src/core/policy-load.js +36 -6
  293. package/dist/src/core/policy-load.js.map +1 -1
  294. package/dist/src/core/policy-match.d.ts +324 -0
  295. package/dist/src/core/policy-match.js +72 -9
  296. package/dist/src/core/policy-match.js.map +1 -1
  297. package/dist/src/core/policy-proposal.d.ts +317 -0
  298. package/dist/src/core/policy-proposal.js +102 -2
  299. package/dist/src/core/policy-proposal.js.map +1 -1
  300. package/dist/src/core/prompt-layout.d.ts +221 -0
  301. package/dist/src/core/protected-path-guard.d.ts +566 -0
  302. package/dist/src/core/protected-path-guard.js +848 -55
  303. package/dist/src/core/protected-path-guard.js.map +1 -1
  304. package/dist/src/core/question-preempted.d.ts +141 -0
  305. package/dist/src/core/question-preempted.js +152 -0
  306. package/dist/src/core/question-preempted.js.map +1 -0
  307. package/dist/src/core/read-scope.d.ts +172 -0
  308. package/dist/src/core/read-scope.js +252 -0
  309. package/dist/src/core/read-scope.js.map +1 -0
  310. package/dist/src/core/registration.d.ts +25 -0
  311. package/dist/src/core/reindex.d.ts +99 -0
  312. package/dist/src/core/sampler.d.ts +313 -0
  313. package/dist/src/core/sandbox.d.ts +371 -0
  314. package/dist/src/core/sandbox.js +190 -1
  315. package/dist/src/core/sandbox.js.map +1 -1
  316. package/dist/src/core/seal.d.ts +165 -0
  317. package/dist/src/core/sender-identity.d.ts +476 -0
  318. package/dist/src/core/sender-identity.js +572 -0
  319. package/dist/src/core/sender-identity.js.map +1 -0
  320. package/dist/src/core/shlex.d.ts +102 -0
  321. package/dist/src/core/shlex.js +159 -0
  322. package/dist/src/core/shlex.js.map +1 -0
  323. package/dist/src/core/state.d.ts +505 -0
  324. package/dist/src/core/task-file.d.ts +185 -0
  325. package/dist/src/core/telegram-config.d.ts +93 -0
  326. package/dist/src/core/token.d.ts +409 -0
  327. package/dist/src/core/token.js +21 -38
  328. package/dist/src/core/token.js.map +1 -1
  329. package/dist/src/core/validate.d.ts +138 -0
  330. package/dist/src/core/values.d.ts +147 -0
  331. package/dist/src/core/values.js +36 -1
  332. package/dist/src/core/values.js.map +1 -1
  333. package/dist/src/core/vault.d.ts +291 -0
  334. package/dist/src/core/verified-snapshot.d.ts +204 -0
  335. package/dist/src/core/verify.d.ts +336 -0
  336. package/dist/src/core/version.d.ts +8 -0
  337. package/dist/src/core/wysiwys.d.ts +370 -0
  338. package/dist/src/daemon/advance-child.d.ts +39 -0
  339. package/dist/src/daemon/advance.d.ts +476 -0
  340. package/dist/src/daemon/advance.js +25 -4
  341. package/dist/src/daemon/advance.js.map +1 -1
  342. package/dist/src/daemon/audit.d.ts +87 -0
  343. package/dist/src/daemon/daemon.d.ts +1180 -0
  344. package/dist/src/daemon/daemon.js +9 -0
  345. package/dist/src/daemon/daemon.js.map +1 -1
  346. package/dist/src/daemon/dark-session.d.ts +64 -0
  347. package/dist/src/daemon/draw-child.d.ts +36 -0
  348. package/dist/src/daemon/draw.d.ts +154 -0
  349. package/dist/src/daemon/git-evidence.d.ts +173 -0
  350. package/dist/src/daemon/git-evidence.js +1 -1
  351. package/dist/src/daemon/projection.d.ts +180 -0
  352. package/dist/src/daemon/prune.d.ts +207 -0
  353. package/dist/src/mcp/http.d.ts +113 -0
  354. package/dist/src/mcp/server.d.ts +265 -0
  355. package/dist/src/mcp/server.js +17 -1
  356. package/dist/src/mcp/server.js.map +1 -1
  357. package/docs/adapter-api.md +106 -0
  358. package/docs/cli-reference.md +1316 -63
  359. package/docs/codex-enforced-session.md +103 -0
  360. package/docs/codex-workspace-broker.md +118 -0
  361. package/package.json +14 -2
  362. package/schema/codex-instance.schema.json +82 -0
  363. package/schema/event.schema.json +539 -9
  364. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  365. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  366. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  367. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  368. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  370. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  371. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  372. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  373. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  374. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  375. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  376. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  377. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  378. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  379. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  380. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  381. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  382. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  383. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  384. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  385. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  386. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  387. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  388. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  389. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  390. package/schema/fixtures/policy/valid/canonical.json +1 -1
  391. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  392. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  393. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  394. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  395. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  396. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  397. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  398. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  399. package/schema/fixtures/values/invalid/version-float.json +1 -0
  400. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  401. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  402. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  403. package/schema/fixtures/values/valid/full.json +5 -7
  404. package/schema/fixtures/values/valid/minimal.json +1 -1
  405. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  406. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  407. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  408. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  409. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  410. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  411. package/schema/fixtures/values-md/valid/absent.md +1 -1
  412. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  413. package/schema/policy.schema.json +75 -3
  414. package/schema/values.schema.json +7 -11
  415. package/templates/codex/README.md +9 -0
  416. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Policy loading: find `APPROVAL.md`, extract its single
3
+ * ` ```yaml approval-policy ` fenced block, parse it, validate it, and resolve
4
+ * its duration strings to milliseconds.
5
+ *
6
+ * SPEC.md §5: the policy file is "prose for humans plus exactly one fenced
7
+ * block for machines". Implementations MUST parse the fenced block, MUST
8
+ * ignore surrounding prose, and MUST accept `APPROVALS.md` as a fallback
9
+ * filename with `APPROVAL.md` winning when both exist.
10
+ *
11
+ * ## Fail-closed contract (SPEC.md §5.2)
12
+ *
13
+ * Every failure mode of this module returns `{ ok: false, code, message }` —
14
+ * nothing throws, and nothing returns a partially-understood policy. A not-ok
15
+ * result places a hard obligation on the consumer (the APRV-11 class matcher
16
+ * and everything downstream of it): **treat every class as `manual`.** There
17
+ * is no "load what we could" path, because a policy the runtime only half
18
+ * understood is a policy whose author believes constraints are in force that
19
+ * are not. A missing file is not permission to run unattended; it is the
20
+ * absence of any grant, which is `manual` for everything.
21
+ *
22
+ * ## YAML version stance
23
+ *
24
+ * The `yaml` package parses **YAML 1.2** by default and this module keeps that
25
+ * default, additionally pinning `schema: "core"` — the YAML 1.2 core schema.
26
+ * Consequences that matter for a policy file:
27
+ *
28
+ * - YAML 1.1-isms are **not** honored. `yes`, `no`, `on`, `off`, `y`, `n` parse
29
+ * as the plain strings `"yes"`, `"no"`, … and **not** as booleans; sexagesimal
30
+ * (`1:30`) is a string, not a number. This is deliberate. A policy file is a
31
+ * permission document: the difference between the string `"no"` and the
32
+ * boolean `false` must never depend on which YAML dialect an implementation
33
+ * happens to ship. Under YAML 1.2 core, only `true`/`false` are booleans, so
34
+ * a policy that writes `autonomy: no` yields the string `"no"`, fails the
35
+ * closed `autonomy` enum in `policy.schema.json`, and the whole policy fails
36
+ * closed to all-`manual` — a loud rejection instead of a silent coercion.
37
+ * - No custom or implicit-typed tags are accepted. Any node carrying an
38
+ * explicit tag (`!!timestamp`, `!!binary`, `!Anything`) is rejected as
39
+ * `yaml-error`, so a policy value can only ever be a string, number, boolean,
40
+ * null, map, or sequence. Tag-driven type coercion (and any tag-driven
41
+ * construction in a future `yaml` release) can therefore never reach the
42
+ * schema validator.
43
+ * - Anchors and aliases are permitted but bounded: {@link MAX_ALIAS_COUNT}
44
+ * caps alias expansion so a billion-laughs document fails closed as
45
+ * `yaml-error` instead of exhausting memory.
46
+ * - Duplicate mapping keys are parse errors (the `yaml` default), not
47
+ * last-one-wins.
48
+ *
49
+ * Any parser error **or warning** fails the load closed. Warnings in `yaml`
50
+ * flag constructs the parser recovered from (unsupported directives, deprecated
51
+ * syntax); "recovered from" is exactly the silent reinterpretation a policy
52
+ * file must not tolerate.
53
+ *
54
+ * ## Determinism
55
+ *
56
+ * `loadPolicy` is a pure function of the file bytes on disk plus the schema
57
+ * directory. No clock, no network, no randomness, no cross-call caching.
58
+ */
59
+ import { type ProtectedPathEntry } from "./command-class.js";
60
+ import type { ReadScope } from "./read-scope.js";
61
+ import { type ValidationError } from "./validate.js";
62
+ /**
63
+ * Upper bound on alias expansions in one policy document.
64
+ *
65
+ * Anchors are legitimate in a hand-written policy (sharing an approver list
66
+ * across a few class rules), so aliases are not banned outright — but alias
67
+ * expansion is exponential in the number of nesting levels, which is the
68
+ * "billion laughs" resource-exhaustion attack. 32 is far above what any
69
+ * plausible hand-written APPROVAL.md needs and far below anything that costs
70
+ * measurable memory. Exceeding it is a hard `yaml-error`.
71
+ */
72
+ export declare const MAX_ALIAS_COUNT = 32;
73
+ /** Info string that marks the machine-readable policy block (SPEC.md §5). */
74
+ export declare const POLICY_INFO_STRING = "yaml approval-policy";
75
+ /** Policy filenames, in precedence order (SPEC.md §5). */
76
+ export declare const POLICY_FILENAMES: readonly ["APPROVAL.md", "APPROVALS.md"];
77
+ /**
78
+ * SPEC.md §5.2 autonomy levels, strictest first — the RESOLVED vocabulary.
79
+ *
80
+ * Deliberately unchanged by APRV-127. The autonomy split is a split of
81
+ * *supervision*, not of autonomy: both supervised modes execute under
82
+ * supervision, both are metered by the same budgets, and both are eligible for
83
+ * the same retrospective review. Every consumer that asks "is this action
84
+ * supervised?" keeps asking one question and getting one answer, and the mode
85
+ * travels beside it as {@link SupervisionMode}. Widening this union instead
86
+ * would have put a third case into every `switch` in the runtime, and a
87
+ * `switch` that forgot it would have failed open.
88
+ *
89
+ * WIDENED by APRV-183's successor, APRV-185, and widened for the opposite
90
+ * reason. `human-only` is not a supervision mode and cannot travel beside
91
+ * anything: it says the action is reserved to human hands and is performed
92
+ * outside agent execution entirely, so there is no gate path, no token, and no
93
+ * record for an agent to produce. A member of this union is exactly what that
94
+ * needs to be, because the failure mode the note above worries about runs the
95
+ * other way here: a `switch` that forgets `human-only` reaches no allow, since
96
+ * every enforcement path in this runtime opens by refusing it and the remaining
97
+ * branches are all keyed to `manual`, `supervised` or `autonomous` by equality.
98
+ */
99
+ export type Autonomy = "human-only" | "manual" | "supervised" | "autonomous";
100
+ /**
101
+ * Amended SPEC.md §5.2 (APRV-127): how far a supervised class is supervised.
102
+ *
103
+ * - `live`: a declared fraction of the class's actions BLOCK on the human gate
104
+ * before executing, exactly as `manual` does. The rest proceed.
105
+ * - `retro`: the pre-split behaviour. Every action proceeds immediately and a
106
+ * fraction is drawn afterwards into a human's review backlog.
107
+ */
108
+ export type SupervisionMode = "live" | "retro";
109
+ /** What a class rule may WRITE. See {@link Autonomy} for what it resolves to. */
110
+ export type DeclaredAutonomy = Autonomy | "supervised-live" | "supervised-retro";
111
+ /**
112
+ * What `defaults.autonomy` may write: {@link DeclaredAutonomy} less
113
+ * `supervised-live`, which is meaningless without a `live_rate` and has nowhere
114
+ * on `defaults` to declare one. Enforced by `policy.schema.json`.
115
+ *
116
+ * `human-only` IS admitted here (APRV-185), and the asymmetry with
117
+ * `supervised-live` is the whole of the reason: `supervised-live` is excluded
118
+ * because it carries a required rate `defaults` cannot hold, and `human-only`
119
+ * carries nothing at all. An author who writes it as the default is declaring
120
+ * maximal strictness for everything the policy did not name, which is a
121
+ * statement a policy is entitled to make.
122
+ *
123
+ * What it is NOT is the fail-closed target. A policy that cannot be parsed
124
+ * still resolves every class to `manual` (see `policy-match.ts`), because a
125
+ * broken policy must stay recoverable through its own gate: an unparseable file
126
+ * whose classes all became `human-only` would leave no gated path to the fix,
127
+ * and the repair for a typo would sit behind a level that admits no repair.
128
+ */
129
+ export type DefaultAutonomy = Exclude<DeclaredAutonomy, "supervised-live">;
130
+ /**
131
+ * Amended SPEC.md §10.4 (APRV-105): how the raw execution token travels from the
132
+ * mint site to the spend site.
133
+ *
134
+ * - `manual` — printed once on the granting surface and carried by a human.
135
+ * THE DEFAULT, and what an absent key means.
136
+ * - `sealed` — additionally sealed to the requester's ephemeral public key and
137
+ * recorded as ciphertext, so `approval wait` can return it to the process
138
+ * that asked, across machines.
139
+ */
140
+ export type TokenDelivery = "manual" | "sealed";
141
+ /** The delivery mode in force. Fail-closed: anything unusable is `manual`. */
142
+ export declare function tokenDeliveryOf(load: PolicyLoadResult): TokenDelivery;
143
+ /** A class rule (SPEC.md §5.1); shape mirrors `policy.schema.json`. */
144
+ export interface PolicyClassRule {
145
+ autonomy: DeclaredAutonomy;
146
+ /**
147
+ * Amended SPEC.md §5.2/§7 (APRV-317): explicit operator permission for a
148
+ * truthful irreversible action to retain this rule's nonmanual autonomy.
149
+ * Absent and false preserve the manual floor. The schema permits true only
150
+ * on autonomous and supervised rules; defaults have no corresponding key.
151
+ */
152
+ allow_irreversible?: boolean;
153
+ /**
154
+ * Amended SPEC.md §5.2 (APRV-127): the fraction of `supervised-live` actions
155
+ * that block on the human gate, in (0, 1]. Required by the schema for
156
+ * `supervised-live` and forbidden for every other level, `human-only`
157
+ * included (APRV-185): a level whose actions no agent may take has no
158
+ * fraction of them to gate.
159
+ */
160
+ live_rate?: number;
161
+ /**
162
+ * Amended SPEC.md §5.2 (APRV-183): the fraction of this class's executed
163
+ * actions drawn into the retrospective review backlog, in (0, 1]. Optional on
164
+ * every supervised level (`supervised`, `supervised-retro`, `supervised-live`)
165
+ * and a schema violation on `manual`, `autonomous` and `human-only`, none of
166
+ * which has a retrospective pool. Absent means the class is sampled at
167
+ * `audit.supervised_sample_rate`.
168
+ */
169
+ retro_rate?: number;
170
+ approvers?: string[];
171
+ limits?: Record<string, number>;
172
+ }
173
+ /**
174
+ * A load-time observation about a policy that PARSED and VALIDATED.
175
+ *
176
+ * Not an error, and never a refusal: a policy carrying notes is fully in force.
177
+ * The notes exist because APRV-127 gave an existing spelling a new name, and a
178
+ * reinterpretation nobody is told about is the failure mode this project exists
179
+ * to prevent. `approval policy check` and `doctor` print them; nothing branches
180
+ * on them.
181
+ */
182
+ export interface PolicyNote {
183
+ /** Machine-readable and closed, so a reader can branch without regex. */
184
+ code: "supervised-alias";
185
+ /** Where the note applies: a class pattern, or `defaults.autonomy`. */
186
+ where: string;
187
+ message: string;
188
+ }
189
+ /** Parsed policy document. Structurally guaranteed by `policy.schema.json`. */
190
+ export interface Policy {
191
+ version: string;
192
+ defaults?: {
193
+ autonomy?: DefaultAutonomy;
194
+ channel?: string;
195
+ approval_ttl?: string;
196
+ /**
197
+ * Amended SPEC.md §10.4 (APRV-105): how the raw execution token reaches the
198
+ * process that will spend it. Absent means `manual`, and under `manual`
199
+ * nothing about the pre-APRV-105 behaviour changes byte for byte.
200
+ */
201
+ token_delivery?: TokenDelivery;
202
+ on_expiry?: "reject";
203
+ };
204
+ /**
205
+ * Amended SPEC.md §5.2 (APRV-38): duration after which a payload whose action
206
+ * reached a terminal state MAY be pruned from `.approval/payloads/`. Absent
207
+ * means retain indefinitely. Read by the M5 daemon; nothing else prunes.
208
+ */
209
+ payload_retention?: string;
210
+ /**
211
+ * Amended SPEC.md §5.2 (APRV-107): repo-relative paths whose edit is
212
+ * `policy.edit` in addition to the runtime's built-in set. Exact file paths
213
+ * (`SPEC.md`) and directory prefixes ending in `/` (`design/`); no globs, no
214
+ * negation. Purely ADDITIVE: the built-ins stay protected whatever this
215
+ * list says, so a policy can widen the protected surface and never narrow
216
+ * it.
217
+ *
218
+ * Amended again (APRV-266): an entry may instead be `{path, class}`, routing
219
+ * that path family to a named `policy.edit` sub-class so each family carries
220
+ * its own autonomy and its own live rate. A bare string keeps its APRV-107
221
+ * meaning exactly. What a routing may not do is weaken a BUILT-IN protected
222
+ * path: {@link checkProtectedRouteFloor} runs at load and makes such a policy
223
+ * inoperative.
224
+ */
225
+ protected_paths?: ProtectedPathEntry[];
226
+ /**
227
+ * Amended SPEC.md §5.2 (APRV-347): directories an agent's reads may stay
228
+ * inside, beyond the built-in roots.
229
+ *
230
+ * ADDITIVE, with the same discipline `protected_paths` has and for a sharper
231
+ * reason: the built-in roots (the gate root, the session scratchpad, the
232
+ * system temp root) are in scope whatever this says, because a runtime denied
233
+ * its own policy and log could not run, and a policy that could NARROW its
234
+ * own read scope would be a policy an agent could edit until nothing was
235
+ * gated. Absent means the built-ins alone, which is the muse jail with no
236
+ * grammar at all: the gate root is the directory holding this file.
237
+ *
238
+ * Nothing here is resolved by the loader. `core/read-scope.ts` turns these
239
+ * into effective roots and `src/cli/hook.ts` resolves them on disk, so a
240
+ * symlink cannot smuggle a read out of one.
241
+ */
242
+ read_scope?: ReadScope;
243
+ /**
244
+ * The approver roster (SPEC.md §5.1), and since APRV-324 the operator's
245
+ * attested statement of which transport account each of them decides from.
246
+ *
247
+ * `senders` is additive and optional, keyed by a channel whose transport
248
+ * authenticates a sender (`schema/policy.schema.json`'s `senderChannel`, which
249
+ * is `telegram` and nothing else today). It is READ by
250
+ * `channels/contract.ts` through `core/sender-identity.ts`, and by nothing on
251
+ * a routing, budget or token path: it decides WHO a decision is recorded as,
252
+ * never what a decision may authorize.
253
+ */
254
+ approvers?: Record<string, {
255
+ channels: string[];
256
+ senders?: Record<string, string>;
257
+ }>;
258
+ classes?: Record<string, PolicyClassRule>;
259
+ /**
260
+ * Named budget scopes (SPEC.md §5.1/§5.2). `max_pending` has been in
261
+ * `policy.schema.json` since v0.1 and was missing from this type until
262
+ * APRV-173, which is what a key nobody read looks like from the inside: the
263
+ * schema accepted it, the loader carried it, and no reader could see it.
264
+ * `core/intake-limits.ts` evaluates it at intake; `core/budgets.ts` skips it.
265
+ */
266
+ budgets?: Record<string, {
267
+ daily_usd?: number;
268
+ daily_actions?: number;
269
+ max_pending?: number;
270
+ }>;
271
+ audit?: {
272
+ supervised_sample_rate?: number;
273
+ /**
274
+ * Amended SPEC.md §5.2 (APRV-38): NAME of the environment variable holding
275
+ * the operator's HMAC sampling secret. The name is what a policy carries;
276
+ * the secret lives outside the repository and outside any agent-readable
277
+ * path, because an agent that can read it can predict the sample.
278
+ */
279
+ sampling_secret_env?: string;
280
+ /**
281
+ * Amended SPEC.md §8 (APRV-58): how far a gate-typed `ts` may step backwards
282
+ * from its gate-typed predecessor before `core/verify.ts` reports a
283
+ * `gate-ts-regression`. Absent means the reference runtime's 2 seconds.
284
+ * Report-only in every direction: it decides what a human is shown about a
285
+ * log that already verified, and no verdict reads it.
286
+ */
287
+ skew_tolerance?: string;
288
+ /**
289
+ * Amended SPEC.md §9 (APRV-220): the PUBLIC halves of the keys permitted to
290
+ * sign a `log.checkpoint`, base64 DER SPKI. The one key-shaped field in
291
+ * this file holding material rather than the NAME of a variable, because a
292
+ * public key is not a secret and the value of writing it here is that this
293
+ * file is committed and attested.
294
+ */
295
+ checkpoint_keys?: string[];
296
+ /**
297
+ * Amended SPEC.md §9 (APRV-220): how long a log may go without a signed
298
+ * checkpoint before verification says one is due. Report-only in every
299
+ * direction; there is no path from due to refused.
300
+ */
301
+ checkpoint_every?: string;
302
+ };
303
+ /**
304
+ * Amended SPEC.md §5.2 (APRV-217): how the long-lived readers of this log
305
+ * prove a cached prefix. Latency only, and its strictest value is the
306
+ * default, so an absent block is the behaviour that existed before it.
307
+ */
308
+ daemon?: {
309
+ read_proof?: "full" | "incremental";
310
+ full_reproof_every?: number;
311
+ full_reproof_after?: string;
312
+ };
313
+ channels?: Record<string, Record<string, unknown>>;
314
+ /**
315
+ * Amended SPEC.md §5.2 (APRV-68): the credential vault's configuration. One
316
+ * key, and it is a NAME: `passphrase_env` holds the name of the environment
317
+ * variable the operator keeps the vault passphrase in, exactly as
318
+ * `channels.telegram.token_env` and `audit.sampling_secret_env` do. A policy
319
+ * that carried the passphrase itself would be a passphrase in a file agents
320
+ * may read, which is the thing the vault exists to prevent.
321
+ */
322
+ vault?: {
323
+ passphrase_env?: string;
324
+ };
325
+ }
326
+ /**
327
+ * Duration-valued policy fields pre-resolved to milliseconds, so callers never
328
+ * re-parse a duration string (and so no two call sites can disagree about what
329
+ * `24h` means). `null` means the field was absent from the policy.
330
+ */
331
+ export interface PolicyDurations {
332
+ /** `defaults.approval_ttl` in milliseconds, or `null` when unset. */
333
+ approvalTtlMs: number | null;
334
+ /**
335
+ * `audit.skew_tolerance` in milliseconds, or `null` when unset (APRV-58).
336
+ * `null` means the verifier's own default, never "no tolerance": a zero
337
+ * allowance would report every healthy fleet's ordinary clock disagreement.
338
+ */
339
+ skewToleranceMs: number | null;
340
+ /**
341
+ * `audit.checkpoint_every` in milliseconds, or `null` when unset (APRV-220).
342
+ * `null` means the cadence is off: nothing is ever reported as due, which is
343
+ * the behaviour of every policy written before the key existed.
344
+ */
345
+ checkpointEveryMs: number | null;
346
+ }
347
+ /**
348
+ * The `daemon` block resolved, with the reference runtime's defaults applied
349
+ * (APRV-217), so no reader re-derives them and no two readers disagree.
350
+ *
351
+ * Every default is the strict end: `full` proves the whole prefix on every
352
+ * read, which is what the runtime did before this block existed. The counts are
353
+ * meaningful only under `incremental`.
354
+ */
355
+ export interface PolicyDaemonRead {
356
+ readProof: "full" | "incremental";
357
+ /** Reads one full re-proof may cover, the anchoring read included. */
358
+ fullReproofEvery: number;
359
+ /** Wall-clock milliseconds one full re-proof may cover. */
360
+ fullReproofAfterMs: number;
361
+ /**
362
+ * Whether the policy declared a `daemon` block at all. Display only — the
363
+ * three fields above are complete either way — and read by `approval doctor`,
364
+ * which skips its row rather than reporting a mode nobody wrote.
365
+ */
366
+ declared: boolean;
367
+ }
368
+ /** The defaults a policy that declares no `daemon` block is read under. */
369
+ export declare const DEFAULT_POLICY_DAEMON_READ: PolicyDaemonRead;
370
+ /** Where the loaded policy came from. */
371
+ export interface PolicySource {
372
+ /** Absolute or caller-relative path actually read. */
373
+ path: string;
374
+ /** Basename of that path, e.g. `APPROVAL.md`. */
375
+ filename: string;
376
+ }
377
+ /** Discrete fail-closed reasons. Every one means "treat all classes manual". */
378
+ export type PolicyLoadErrorCode = "file-missing" | "no-block" | "multiple-blocks" | "yaml-error" | "schema-invalid"
379
+ /**
380
+ * Amended SPEC.md §5.2 (APRV-266): a `protected_paths` routing that a
381
+ * built-in protected path may not take. Distinct from `schema-invalid`
382
+ * because the file IS valid against the schema and the fault is a
383
+ * relationship between two of its parts — the route and the `policy.edit`
384
+ * line — which no JSON Schema can state. See
385
+ * {@link checkProtectedRouteFloor}.
386
+ */
387
+ | "protected-route-floor"
388
+ /**
389
+ * Amended SPEC.md §5.2 (APRV-324): two approvers declare one sender id. Like
390
+ * `protected-route-floor` it is a relationship between two parts of a file
391
+ * that is valid against the schema, which no JSON Schema can state. The
392
+ * consequence is the same and for the same reason: the policy does not load,
393
+ * so every class resolves `manual`, because a file that says one account is
394
+ * two people has no reading under which a decision from that account names
395
+ * anybody. See {@link checkSenderMappings}.
396
+ */
397
+ | "sender-ambiguous";
398
+ /**
399
+ * Result of {@link loadPolicy}.
400
+ *
401
+ * Fail-closed contract: when `ok` is `false` the consumer MUST NOT fall back to
402
+ * any permissive default. Per SPEC.md §5.2 an unparseable, unfindable, or
403
+ * schema-invalid policy means **every class is `manual`** — the `code` is for
404
+ * diagnostics and operator messaging only, never for choosing a softer path.
405
+ */
406
+ export type PolicyLoadResult = {
407
+ ok: true;
408
+ policy: Policy;
409
+ source: PolicySource;
410
+ durations: PolicyDurations;
411
+ /**
412
+ * The `daemon` block resolved (APRV-217), defaults applied. Present for
413
+ * every loaded policy, declared or not, for the reason `durations` is:
414
+ * one parse of the grammar, one number every reader shares.
415
+ */
416
+ daemon: PolicyDaemonRead;
417
+ /**
418
+ * Amended SPEC.md §5.2 (APRV-127): observations about a policy that is in
419
+ * force. Empty for almost every policy. Never a reason to fail closed.
420
+ */
421
+ notes: PolicyNote[];
422
+ } | {
423
+ ok: false;
424
+ code: PolicyLoadErrorCode;
425
+ message: string;
426
+ errors?: ValidationError[];
427
+ /**
428
+ * The parsed-but-rejected YAML value, when the failure happened AFTER a
429
+ * successful parse (APRV-111). It exists for one consumer and one purpose:
430
+ * `core/policy-diff.ts` renders the policy's key vocabulary, and the edit
431
+ * most in need of rendering is the one that made the policy invalid — an
432
+ * unknown top-level key, which this schema rejects outright. Without the
433
+ * rejected value that edit is invisible to every reader, and a differ that
434
+ * cannot see it would report "no semantic change" over bytes that took the
435
+ * whole policy fail-closed.
436
+ *
437
+ * Absent for `file-missing`, `no-block`, `multiple-blocks` and
438
+ * `yaml-error`: there is no value to carry. NOTHING may enforce against
439
+ * it — a failed load is all-manual, full stop, and this field is for
440
+ * DISPLAY only.
441
+ */
442
+ raw?: unknown;
443
+ };
444
+ /** Options accepted by {@link loadPolicy}. */
445
+ export interface LoadPolicyOptions {
446
+ /** Directory to search for `APPROVAL.md` / `APPROVALS.md`. Default: cwd. */
447
+ dir?: string;
448
+ /** Explicit policy file path. Overrides discovery entirely. */
449
+ file?: string;
450
+ /** Schema directory passed through to {@link validate}. Injectable for tests. */
451
+ schemaDir?: string;
452
+ }
453
+ /**
454
+ * Parse a SPEC.md §5.2 duration string to milliseconds.
455
+ *
456
+ * Accepts exactly `<positive integer><unit>` with unit in `ms|s|m|h|d|w`
457
+ * (`w` = 7 days). Returns `null` for **any** deviation: zero, leading zeros,
458
+ * negatives, fractions, compound forms (`1h30m`), surrounding whitespace,
459
+ * empty input, unknown units. Deterministic and total — never throws.
460
+ */
461
+ export declare function parseDuration(text: string): number | null;
462
+ /** How a hardened parse should name itself in its failure messages. */
463
+ export interface HardenedYamlLabels {
464
+ /** Subject of the message, e.g. `"policy YAML"` or `"frontmatter YAML"`. */
465
+ subject: string;
466
+ /** Where a tag was found, e.g. `"a policy block"` / `"a task envelope"`. */
467
+ tagContext: string;
468
+ }
469
+ /** Outcome of {@link parseHardenedYaml}: a value, or one fail-closed message. */
470
+ export type HardenedYamlResult = {
471
+ ok: true;
472
+ value: unknown;
473
+ } | {
474
+ ok: false;
475
+ message: string;
476
+ };
477
+ /**
478
+ * Parse YAML under the hardened settings described in the module header:
479
+ * YAML 1.2 core schema, warnings fatal, duplicate keys fatal, explicitly tagged
480
+ * nodes rejected, alias expansion bounded by {@link MAX_ALIAS_COUNT}.
481
+ *
482
+ * **This is the one implementation.** `core/frontmatter.ts` parses the *other*
483
+ * half of the same permission surface — the task envelope declares the class,
484
+ * cost, and reversibility that policy is matched against — and used to carry a
485
+ * replica of these settings. APRV-20 (finding S5) deleted the replica: a task
486
+ * envelope and a policy block are now hardened by the same code, so the two
487
+ * cannot drift apart, and a hardening fix lands in both at once.
488
+ *
489
+ * Never throws; every failure is a message. Pure function of the source text.
490
+ */
491
+ export declare function parseHardenedYaml(source: string, labels: HardenedYamlLabels): HardenedYamlResult;
492
+ /**
493
+ * The fail-closed result for policy bytes that could not be read at all.
494
+ *
495
+ * Exported for callers that read the file themselves and still need a
496
+ * {@link PolicyLoadResult} to hand `resolve` (APRV-142): a caller must never
497
+ * have to invent one, because an invented result is where a permissive default
498
+ * would creep in. `file-missing` is the same code {@link loadPolicy} reports
499
+ * for the same fact.
500
+ */
501
+ export declare function policyUnreadable(path: string, cause: string): PolicyLoadResult;
502
+ /**
503
+ * Extract, parse, and validate the policy block of an already-read policy file.
504
+ *
505
+ * The bytes-in form of {@link loadPolicy}, split out for APRV-142: a gate
506
+ * operation reads `APPROVAL.md` **once** and feeds the same buffer to both the
507
+ * attestation hash check and this parse, so no mid-operation file swap can
508
+ * attest one policy and enforce another. `path` is used for messages and for
509
+ * {@link PolicySource}; nothing here touches the filesystem.
510
+ *
511
+ * Fails closed on every error path and never throws, exactly as
512
+ * {@link loadPolicy} does — it is the same code.
513
+ */
514
+ export declare function loadPolicyText(path: string, text: string, options?: {
515
+ schemaDir?: string;
516
+ }): PolicyLoadResult;
517
+ /**
518
+ * The routing floor (amended SPEC.md §5.2, APRV-266): a `protected_paths`
519
+ * routing may make a BUILT-IN protected path stricter and never weaker.
520
+ *
521
+ * `protected_paths` has been additive-only since APRV-107 for a reason that
522
+ * routing does not repeal: a policy that could shrink its own protected surface
523
+ * is a policy an agent can edit its way out of. Routing is a new way to try —
524
+ * `{path: CLAUDE.md, class: policy.edit.docs}` with `policy.edit.docs:
525
+ * autonomous` would take the agent instruction files out of the gate without
526
+ * removing a single path from any list, and every surface downstream would
527
+ * report the policy as widened.
528
+ *
529
+ * So: for each entry that routes a path the runtime protects on its own, the
530
+ * sub-class must resolve at least as strictly as the `policy.edit` line does.
531
+ * Strictness is `policy-match.ts`'s own table, so the floor and the resolver
532
+ * cannot come to different conclusions about which of two levels is stricter,
533
+ * and a tie on the level compares the live RATE: `supervised-live 0.01` under a
534
+ * `policy.edit` of `supervised-live 0.1` gates one tenth as often as the line it
535
+ * replaces, which is a weakening whatever the level says.
536
+ *
537
+ * A route aimed at a `policy.core` or `log.mutate` path is refused outright
538
+ * rather than allowed to sit inert. It never fires — `protectedPathClass` tier 2
539
+ * answers before the routed tier, which is §11.1 invariant 9 holding — but an
540
+ * author reading their own file would believe a rule is in force that the
541
+ * runtime will never consult, and a policy nobody can read correctly is the
542
+ * failure this project exists to prevent.
543
+ *
544
+ * The consequence of a failure is that the policy does not load AT ALL, and a
545
+ * policy that does not load resolves every class to `manual`. That is the
546
+ * strictest available answer and the one this file has always given: an author
547
+ * who wrote a weakening they did not intend gets everything gated, plus a
548
+ * message naming the entry, rather than the weakening.
549
+ *
550
+ * `null` when the policy is clear; otherwise the message.
551
+ */
552
+ export declare function checkProtectedRouteFloor(load: PolicyLoadResult): string | null;
553
+ /**
554
+ * Load, extract, parse, and validate the policy block of an `APPROVAL.md`.
555
+ *
556
+ * Discovery: `options.file` when given, otherwise `APPROVAL.md` in
557
+ * `options.dir` (default cwd), falling back to `APPROVALS.md`. `APPROVAL.md`
558
+ * wins when both exist (SPEC.md §5).
559
+ *
560
+ * Fails closed on every error path — see {@link PolicyLoadResult}. Never
561
+ * throws; a thrown error would be a policy bypass.
562
+ *
563
+ * This is discovery plus {@link loadPolicyText}. A caller that has already read
564
+ * the bytes (the gate, since APRV-142) calls the latter directly rather than
565
+ * paying for a second read that could see different bytes.
566
+ */
567
+ export declare function loadPolicy(options?: LoadPolicyOptions): PolicyLoadResult;
@@ -69,6 +69,11 @@ import { scanFences } from "./md-fence.js";
69
69
  // must never have from the resolution it floors.
70
70
  import { resolve, STRICTNESS } from "./policy-match.js";
71
71
  import { promptBlockErrors } from "./prompt-layout.js";
72
+ // APRV-324. Same shape as the matcher import above: this module imports a
73
+ // FUNCTION from `sender-identity.ts`, which imports only this module's TYPES,
74
+ // so the cycle is erased at build and the rule the loader enforces and the rule
75
+ // the decision path applies are one implementation.
76
+ import { checkSenderMappings } from "./sender-identity.js";
72
77
  import { validate } from "./validate.js";
73
78
  /**
74
79
  * Upper bound on alias expansions in one policy document.
@@ -392,6 +397,14 @@ export function loadPolicyText(path, text, options = {}) {
392
397
  daemon: daemonRead,
393
398
  notes: aliasNotes(policy),
394
399
  };
400
+ // APRV-324: one sender id, at most one person. Checked here rather than in
401
+ // the schema because it is a relationship between two approvers' blocks, and
402
+ // checked BEFORE the routing floor below because it needs only the parsed
403
+ // roster, not a resolution.
404
+ const senders = checkSenderMappings(policy.approvers);
405
+ if (senders !== null) {
406
+ return failure("sender-ambiguous", `${resolved.path}: ${senders}`, undefined, parsed.value);
407
+ }
395
408
  // APRV-266: the routing floor is the LAST gate on a load, because it is the
396
409
  // only check here that needs the resolved policy rather than the parsed one.
397
410
  const floor = checkProtectedRouteFloor(loaded);
@@ -456,13 +469,25 @@ export function checkProtectedRouteFloor(load) {
456
469
  if (builtin === null)
457
470
  continue;
458
471
  const routed = resolve(load, parsedEntry.routed);
459
- const weaker = STRICTNESS[routed.declaredAutonomy] > STRICTNESS[line.declaredAutonomy] ||
472
+ const ordinaryWeaker = STRICTNESS[routed.declaredAutonomy] > STRICTNESS[line.declaredAutonomy] ||
460
473
  (STRICTNESS[routed.declaredAutonomy] === STRICTNESS[line.declaredAutonomy] &&
461
474
  routed.liveRate !== null &&
462
475
  line.liveRate !== null &&
463
476
  routed.liveRate < line.liveRate);
464
- if (weaker) {
465
- return `protected_paths entry ${JSON.stringify(entry.path)} routes a built-in policy.edit path to ${JSON.stringify(parsedEntry.routed)}, which resolves ${describeLevel(routed.declaredAutonomy, routed.liveRate)} — weaker than the \`policy.edit\` line's own ${describeLevel(line.declaredAutonomy, line.liveRate)}. \`protected_paths\` is additive: it may widen the protected surface and may never narrow it, and routing a built-in path to a looser sub-class would narrow it without removing anything from any list. Declare ${JSON.stringify(parsedEntry.routed)} at least as strictly as \`policy.edit\`, or route a path the runtime does not already protect.`;
477
+ const irreversibleLine = resolve(load, "policy.edit", { reversible: false });
478
+ const irreversibleRouted = resolve(load, parsedEntry.routed, { reversible: false });
479
+ const irreversibleWeaker = STRICTNESS[irreversibleRouted.declaredAutonomy] >
480
+ STRICTNESS[irreversibleLine.declaredAutonomy] ||
481
+ (STRICTNESS[irreversibleRouted.declaredAutonomy] ===
482
+ STRICTNESS[irreversibleLine.declaredAutonomy] &&
483
+ irreversibleRouted.liveRate !== null &&
484
+ irreversibleLine.liveRate !== null &&
485
+ irreversibleRouted.liveRate < irreversibleLine.liveRate);
486
+ if (ordinaryWeaker || irreversibleWeaker) {
487
+ const comparison = ordinaryWeaker
488
+ ? `${describeLevel(routed.declaredAutonomy, routed.liveRate)} — weaker than the \`policy.edit\` line's own ${describeLevel(line.declaredAutonomy, line.liveRate)}`
489
+ : `${describeLevel(irreversibleRouted.declaredAutonomy, irreversibleRouted.liveRate)} for reversible: false — weaker than the \`policy.edit\` line's effective ${describeLevel(irreversibleLine.declaredAutonomy, irreversibleLine.liveRate)} for reversible: false`;
490
+ return `protected_paths entry ${JSON.stringify(entry.path)} routes a built-in policy.edit path to ${JSON.stringify(parsedEntry.routed)}, which resolves ${comparison}. \`protected_paths\` is additive: it may widen the protected surface and may never narrow it, and routing a built-in path to a looser sub-class would narrow it without removing anything from any list. Declare ${JSON.stringify(parsedEntry.routed)} at least as strictly as \`policy.edit\`, or route a path the runtime does not already protect.`;
466
491
  }
467
492
  }
468
493
  return null;
@@ -472,8 +497,8 @@ function describeLevel(declared, liveRate) {
472
497
  return liveRate === null ? declared : `${declared} at ${String(liveRate)}`;
473
498
  }
474
499
  /**
475
- * Amended SPEC.md §5.2 (APRV-127): one note per place a policy still writes the
476
- * bare `supervised`.
500
+ * Amended SPEC.md §5.2 (APRV-127, APRV-335): one note per place a policy still
501
+ * writes the bare `supervised`.
477
502
  *
478
503
  * The alias keeps every pre-split policy meaning exactly what its author meant:
479
504
  * `supervised` was retrospective sampling, and `supervised-retro` is that same
@@ -482,6 +507,11 @@ function describeLevel(declared, liveRate) {
482
507
  * now means "supervised somehow, possibly live"; the note says, in the one place
483
508
  * a reader is already looking, that it does not.
484
509
  *
510
+ * Since APRV-335 the note leads with the word `deprecated:`, because the alias
511
+ * is now on a path out: it is still parsed, still admitted by the schema, and a
512
+ * future schema version drops it. A reader who sees only the first clause of a
513
+ * long note should still learn the one thing that has a deadline attached.
514
+ *
485
515
  * Pure, total, and ordered: `defaults` first, then class patterns in sorted
486
516
  * order, so the notes of one policy are byte-stable across runs.
487
517
  */
@@ -490,7 +520,7 @@ function aliasNotes(policy) {
490
520
  const say = (where) => ({
491
521
  code: "supervised-alias",
492
522
  where,
493
- message: `${where} declares the bare \`supervised\`, which parses as \`supervised-retro\`: the action executes immediately and is sampled for review AFTERWARDS. Nothing about it changed with the autonomy split — this is the same behaviour under its honest name. Write \`supervised-retro\` to say so explicitly, or \`supervised-live: <rate>\` to have a fraction of the class stop for a human FIRST.`,
523
+ message: `deprecated: ${where} declares the bare \`supervised\`, which parses as \`supervised-retro\`: the action executes immediately and is sampled for review AFTERWARDS. Nothing about it changed with the autonomy split — this is the same behaviour under its honest name. Write \`supervised-retro\` to say so explicitly, or \`supervised-live: <rate>\` to have a fraction of the class stop for a human FIRST. The bare spelling still parses and a future version of the policy schema removes it (APRV-335).`,
494
524
  });
495
525
  if (policy.defaults?.autonomy === "supervised")
496
526
  notes.push(say("defaults.autonomy"));