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
package/README.md CHANGED
@@ -1,133 +1,198 @@
1
1
  # approval.md
2
2
 
3
3
  [![ci](https://github.com/approval-md/approval.md/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/approval-md/approval.md/actions/workflows/ci.yml)
4
+ [![fable](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fapproval-md%2Fapproval.md%2Fmain%2Fmetrics%2Fagent-hours.json&query=%24.agents.claude-fable.hours&label=fable&suffix=h&color=d97757)](docs/agent-hours.md) [![opus](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fapproval-md%2Fapproval.md%2Fmain%2Fmetrics%2Fagent-hours.json&query=%24.agents.claude-opus.hours&label=opus&suffix=h&color=d97757)](docs/agent-hours.md) [![astra](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fapproval-md%2Fapproval.md%2Fmain%2Fmetrics%2Fagent-hours.json&query=%24.agents.codex-astra.hours&label=astra&suffix=h&color=10a37f)](docs/agent-hours.md) [![sol](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fapproval-md%2Fapproval.md%2Fmain%2Fmetrics%2Fagent-hours.json&query=%24.agents.codex-sol.hours&label=sol&suffix=h&color=10a37f)](docs/agent-hours.md) [![cursor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fapproval-md%2Fapproval.md%2Fmain%2Fmetrics%2Fagent-hours.json&query=%24.agents.cursor.hours&label=cursor&suffix=h&color=6c5ce7)](docs/agent-hours.md) [![agent hours](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fapproval-md%2Fapproval.md%2Fmain%2Fmetrics%2Fagent-hours.json&query=%24.total_hours&label=agent%20hours&suffix=h&color=555)](docs/agent-hours.md)
4
5
 
5
- **Human approval for agent actions.**
6
-
7
- Your agent is about to send the email, spend the money, delete the folder, or
8
- publish the post. A bad diff is revertible, so coding agents have a safety net.
9
- Once an agent leaves the repository that net disappears: a sent message has no
10
- revert, and the action carries your name.
11
-
12
- The permissions section in an AGENTS.md file is prose: two lists, one headed
13
- "allowed without prompting" and one headed "require approval first", written for
14
- an agent trusted to obey them. Nothing checks. approval.md is the layer that
15
- checks:
16
-
17
- - **A policy file you wrote.** `APPROVAL.md` is human-authored markdown at the
18
- root of your project, declaring which classes of side effect an agent may take
19
- on its own, which need you, and under what budgets.
20
- - **The approve button on your phone.** A request arrives over Telegram (the
21
- reference channel) carrying what the runtime computed, what the agent claimed,
22
- and the exact bytes about to leave. You tap Approve or Reject.
23
- - **A single-use execution token**, minted at one site in the codebase, only as a
24
- human decision is recorded, spent once, stored nowhere. Adapters holding real
25
- credentials answer to nothing else.
26
- - **A log that cannot be quietly rewritten.** Every proposal, decision, and
27
- execution is an append-only, hash-chained JSONL record, and `approval log
28
- verify` answers for the chain.
29
-
30
- Not everything is worth a tap: a class declared `supervised` runs immediately,
31
- and a policy-declared fraction of those runs is sampled for your retrospective
32
- review, using a secret the agent cannot read.
33
-
34
- Spec site: https://approval.md · Specification: [SPEC.md](SPEC.md)
6
+ **A harness-agnostic, open-source framework for approving agent actions with a
7
+ human in the loop.**
35
8
 
36
- ## How the gate holds
9
+ Your agent is about to send the email, push to main, spend the money, delete
10
+ the folder, or publish the post. A bad diff can be reverted. A sent message
11
+ cannot, and it carries your name.
37
12
 
38
- - **Credentials live in an encrypted vault**, never in the policy file and never
39
- in the agent's environment. `APPROVAL.md` carries the *name* of an environment
40
- variable, and there is no `approval vault get`.
41
- - **Adapters answer only to tokens.** The email adapter opens the vault inside a
42
- verified token window, sends, closes it. An agent without a token reaches no
43
- credential.
44
- - **Tokens are minted at one site**, in the path that records a human decision,
45
- and the log holds only their SHA-256. A second spend is refused
46
- `token-consumed`.
47
- - **The log makes tampering evident.** Each record chains to the previous one,
48
- and projections rebuild from it and never write back.
49
- - **The harness hook covers the direct-shell path.** `approval hook claude-code`
50
- classifies the commands a coding agent runs on its own (`git push`, `npm
51
- install`, `curl`) and answers allow or deny, fail-closed.
52
- - **The escape hatch is a recorded ceremony.** When the gate itself is broken
53
- and every command dies, a human opens a time-boxed window with `approval gate
54
- open`: a terminal, a required `--reason`, and the word `understood`. Every
55
- call it lets through is logged as `gate.bypassed`, human-only classes stay
56
- refused, and `approval status` reports unhealthy until it closes. The
57
- synopsis and a worked example are in
58
- [docs/cli-reference.md#gate](docs/cli-reference.md#gate).
59
-
60
- The honest posture, from [SPEC.md](SPEC.md) section 11: this is an oversight
61
- layer for broadly cooperative agents, with hard enforcement at the adapter
62
- boundaries that hold the credentials. Identity in v0.1 is config-declared, so
63
- the trust boundary is the machine rather than cryptography.
64
- ["Can't the agent just go around it?"](#cant-the-agent-just-go-around-it) works
65
- through each evasion and says where the boundary actually is.
66
-
67
- The design mantra is **files are the interface, the log is the truth, the
68
- database is a cache**. Routing, gating, budget math, and chain verification are
69
- deterministic code. Models propose, and the runtime decides.
70
-
71
- ## Install
13
+ approval.md puts a button between the agent and that action. You write a
14
+ short policy file saying which kinds of action need you. The agent runs freely
15
+ inside those lines. When it reaches one, a message arrives on your phone with
16
+ exactly what is about to happen, and nothing happens until you tap.
17
+
18
+ Two things people use it for first:
19
+
20
+ - **Signing off an email.** The agent drafts, you read the recipients, subject
21
+ and body on your phone, you tap Approve, and the adapter sends it once with a
22
+ credential the agent never held.
23
+ - **Watching a coding agent.** A hook classifies every command Claude Code or
24
+ Cursor runs. Reads and edits go through; `git push origin main`, `npm
25
+ install`, `curl -d`, `rm -rf` come to your phone first, and every decision is
26
+ in a log you can verify.
27
+
28
+ Spec site: https://approval.md · Specification: [SPEC.md](SPEC.md) · Package:
29
+ `approval-md` on npm.
30
+
31
+ ## Five minutes to a working gate
32
+
33
+ **1. Install.** No source checkout is required for the published CLI.
72
34
 
73
35
  ```sh
74
36
  npm install -g approval-md
75
37
  ```
76
38
 
77
- (Publishing is imminent. Until it lands, `git clone`, `npm ci`, `npm run build`,
78
- `npm link` in the checkout gives you the same `approval` binary.)
39
+ **2. Make a gate.** For the published 0.2.0 package, run `approval init`, edit
40
+ and read `APPROVAL.md`, run `approval setup identity`, optionally run
41
+ `approval setup channel telegram`, then run `approval policy attest --as human:<id>`.
79
42
 
80
- Six commands take an empty directory to a machine that will tell you what it is
81
- missing. `init` authorizes nothing, `policy attest` is what makes a policy
82
- operative, and `doctor` reports and repairs nothing.
43
+ The `quickstart` command combines these steps and ships in the published
44
+ package. Run the three-question ceremony in the project directory.
45
+ It asks who you are, whether decisions appear in this terminal or on Telegram,
46
+ and which five class families always ask. It shows the exact policy and requires
47
+ the typed word `understood` before attesting it. From a source checkout, build
48
+ first (`npm ci` and `npm run build`) and run the same verb through your linked
49
+ binary.
83
50
 
84
51
  ```sh
85
- mkdir -p /tmp/approval-demo && cd /tmp/approval-demo
86
- approval init # APPROVAL.md, .approval/log/, QUEUE.md, .gitignore
87
- approval setup identity # writes where APPROVAL_HUMAN comes from
88
- eval "$(approval env)" # put the resolved variables in this shell
89
- approval policy attest # a human signs for these exact policy bytes
90
- approval doctor # can this machine run the system at all?
52
+ approval quickstart
91
53
  ```
92
54
 
55
+ Then run the `activate:` command quickstart prints. It includes the absolute
56
+ project directory, so it resolves this instance even if the next shell starts
57
+ somewhere else.
58
+
93
59
  ```
94
- attested /tmp/approval-demo/APPROVAL.md at seq 1: sha256 cff55216c7be9bfbf35a7d980b6a0c75d250ebc039d7584cb9b3aa3bf25b2f91
60
+ ready: 5 selected class families ask human:yourname on cli; other classified reversible actions use the autonomous default
95
61
  ```
96
62
 
97
- `doctor` prints one line per check and a tally. Three of the 27 lines from a
98
- fresh directory, plus that tally:
63
+ The gate is operative. `.approval/env` remains inert until you run the explicit
64
+ `eval` line. Protected controls, failed policy loads, irreversible declarations,
65
+ and commands the classifier cannot read still take their stricter paths.
99
66
 
67
+ **3. Run the local service.** If you chose Telegram, message **@BotFather**
68
+ with `/newbot` before quickstart so you have the token it asks for. After setup
69
+ and human attestation, start the full runtime from this policy project's directory:
70
+
71
+ ```sh
72
+ cd /path/to/your/project
73
+ eval "$(approval env)" # explicitly load this instance's environment
74
+ approval up # daemon and configured channels, one foreground process
100
75
  ```
101
- ✓ identity APPROVAL_HUMAN=human:alice (config-declared: the trust boundary is this machine, not cryptography)
102
- ✓ log /tmp/approval-demo/.approval/log/events.jsonl verifies: 1 record(s), head seq 1 0f3c4a19187a…
103
- ✗ audit-sampling disabled (secret-env-unnamed): APPROVAL.md sets audit.supervised_sample_rate to 0.1 but names no audit.sampling_secret_env. …
104
- fix: approval policy attest --as human:<id> — after setting audit.supervised_sample_rate and audit.sampling_secret_env in the policy; then export the named variable where the daemon runs
105
- 9 ok · 17 not applicable · 1 failed
106
- ```
107
76
 
108
- The checks run in the order their failures cascade, from build freshness through
109
- identity, attestation, the log chain, the channels, the payload store, audit
110
- sampling, envelope integrity, the vault, and the environment source map behind
111
- `approval env`, then the rows that ask git and the harness what happened. The
112
- full roster and what a fresh directory skips are under [Running the
113
- checks](#running-the-checks). Each carries a `fix:` line you run yourself, and
114
- that one failure is real and intended: the scaffolded policy samples supervised
115
- actions for audit, sampling needs an operator-held secret the policy only names,
116
- and a control that looks like it is running while the party under oversight can
117
- steer it is worse than one that is visibly off. What `init` scaffolds is the
118
- canonical example policy of SPEC.md section 5.1, which names an approver you are
119
- probably not. Read every class before you sign for it, then attest again.
77
+ For a source checkout, use `node /path/to/approval.md/cli.js env` inside the
78
+ `eval` line and `node /path/to/approval.md/cli.js up` to start the service.
79
+ Leave the service running. Requests use your configured channel; Telegram
80
+ requests reach your phone. `up` does not load `.approval/env` itself. An already
81
+ exported approval variable wins over the environment map, so start with a clean
82
+ shell or unset another instance's approval variables before evaluating it.
83
+
84
+ Use `approval up` for normal operation. `approval channel telegram listen` runs
85
+ only the Telegram component, for focused use or diagnosis. Never run both
86
+ against the same bot, or run two instances polling that bot: Telegram returns
87
+ HTTP 409. Stop the polling runtime before rerunning `approval setup channel
88
+ telegram`, then reload the environment and start `up` again.
89
+
90
+ `up` runs a preflight before it starts anything: it fetches, fast-forwards when
91
+ the upstream range is safe, and rebuilds when `dist/` is older than `src/`. When
92
+ the log directory lives in that checkout, a pull that changes
93
+ `.approval/log/events.jsonl` while this copy has appended to it is the routine
94
+ state rather than an incident, so the preflight reconciles it through `approval
95
+ log sync` itself and prints one line saying what it fast-forwarded to and how
96
+ many local records it kept. The one case left for a hand-run `approval log
97
+ sync` is a genuine fork: two chains that share a prefix and then carry different
98
+ records at the same `seq`. Hash chains do not merge, so `up` refuses
99
+ `up-preflight-log-diverged` there, changes nothing, and leaves the decision to
100
+ you.
101
+
102
+ By default the daemon scans `backlog/tasks/`. If your envelopes live elsewhere,
103
+ use `approval up --tasks /path/to/existing/task-folder`. The scan reads `.md`
104
+ files directly inside that folder, without descending into subdirectories.
105
+ Creating an empty default folder does not monitor envelopes stored elsewhere.
106
+ A missing default folder warns about envelope drift coverage; TTL sweeping,
107
+ queue rendering and configured channels can still run. See [runtime startup
108
+ checks](docs/cli-reference.md#up) for the draw socket and optional web channel.
109
+
110
+ **4. Pick your first experience.**
111
+
112
+ - *A coding agent*: [gate your coding agent](#gate-your-coding-agent) is two
113
+ more steps, a classification you can try immediately and a hook you paste
114
+ into `.claude/settings.json`.
115
+ - *An email*: [hand a grant to a real credential](#hand-a-grant-to-a-real-credential)
116
+ adds an SMTP or AgentMail credential to the vault, and
117
+ [examples/email-demo.md](examples/email-demo.md) walks the whole send.
118
+
119
+ When something does not work, `approval doctor` prints one line per check with
120
+ a `fix:` line under each failure. It is described under [Running the
121
+ checks](#running-the-checks), and it is not a step you need on the way in.
122
+
123
+ ## What it is made of
124
+
125
+ - **A policy file you wrote.** `APPROVAL.md` is markdown at the root of your
126
+ project with one YAML block declaring which classes of side effect an agent
127
+ may take on its own, which need you, and under what budgets. A human signs
128
+ for its exact bytes; an edit makes it inoperative until someone signs again.
129
+ - **A message on your phone.** A request arrives over Telegram carrying what
130
+ the runtime computed, what the agent claimed, and the exact bytes about to
131
+ leave. You tap Approve or Reject. A local web page and the terminal are the
132
+ other two channels.
133
+ - **A single-use execution token.** Minted at one place in the code, only as a
134
+ human decision is recorded, spent once, stored nowhere. Manual and selected
135
+ live executions require it. An attested class rule may instead explicitly
136
+ authorize an irreversible supervised or autonomous execution.
137
+ - **A log nobody can quietly rewrite.** Every proposal, decision and execution
138
+ is an append-only, hash-chained JSONL record. `approval log verify` answers
139
+ for the chain.
140
+
141
+ Not everything is worth a tap. A class declared `supervised` runs at once, and
142
+ a fraction of those runs is sampled for your retrospective review using a
143
+ secret the agent cannot read, so you see one in a hundred `gh pr merge` calls
144
+ rather than all of them.
145
+
146
+ The design rule is **files are the interface, the log is the truth, the
147
+ database is a cache**. Routing, gating, budget math and chain verification are
148
+ deterministic code. Models propose; the runtime decides.
149
+
150
+ ## How the gate holds
151
+
152
+ - **Credentials live in an encrypted vault**, never in the policy file and never
153
+ in the agent's environment. `APPROVAL.md` carries the *name* of an environment
154
+ variable, and there is no `approval vault get`.
155
+ - **Adapters answer only inside a verified execution.** Manual and selected-live
156
+ executions present and consume a valid token. An irreversible supervised or
157
+ autonomous execution must be explicitly enabled by its attested class rule.
158
+ The adapter opens the credential window only after the runtime authorizes the
159
+ declared action, then closes it as soon as the adapter returns.
160
+ - **Tokens are minted at one site**, in the path that records a human decision,
161
+ and the log holds only their SHA-256. A second spend is refused
162
+ `token-consumed`.
163
+ - **The log makes tampering evident.** Each record chains to the previous one.
164
+ Projections rebuild from the log and never write back.
165
+ - **The harness hook covers the direct-shell path.** `approval hook claude-code`
166
+ classifies the commands a coding agent runs on its own and answers allow or
167
+ deny, fail-closed.
168
+ - **The escape hatch is a recorded ceremony.** When the gate itself is broken, a
169
+ human opens a time-boxed window with `approval gate open`: a terminal, a
170
+ required `--reason`, and the typed word `understood`. Every call it lets
171
+ through is logged as `gate.bypassed`, human-only classes stay refused, and
172
+ `approval status` reports unhealthy until it closes
173
+ ([docs/cli-reference.md#gate](docs/cli-reference.md#gate)).
174
+
175
+ This is an oversight layer for broadly cooperative agents, with hard
176
+ enforcement at the adapter boundaries that hold the credentials (SPEC.md
177
+ section 11). Identity in v0.1 is config-declared, so the trust boundary is the
178
+ machine rather than cryptography. ["Can't the agent just go around
179
+ it?"](#cant-the-agent-just-go-around-it) works through each evasion.
120
180
 
121
181
  ## Gate your coding agent
122
182
 
123
183
  `approval run` gates the commands an agent hands to the runtime. It cannot gate
124
184
  the ones the harness runs directly, and those are most of them. Two surfaces
125
- close that gap, a PreToolUse hook for Claude Code and an MCP server for any
126
- harness that speaks MCP, both resolving against the same policy and appending to
127
- the same log as the CLI.
185
+ close that gap: a PreToolUse hook for Claude Code and an MCP server for any
186
+ harness that speaks MCP. Both resolve against the same policy and append to the
187
+ same log as the CLI.
188
+
189
+ Codex support is opt-in while native compatibility and everyday activation are
190
+ still being verified. See the bounded [Codex hook operator
191
+ runbook](docs/codex-hook.md) before installing or trusting it, and
192
+ [activation and rollback](docs/codex-activation.md) for what a trust tap does
193
+ and does not buy on the Codex version installed today.
128
194
 
129
- **1. See how a command classifies.** This touches nothing, and it is the fastest
130
- way to understand a verdict.
195
+ **1. See how a command classifies.** This touches nothing.
131
196
 
132
197
  ```
133
198
  $ approval hook classify -- npm install left-pad
@@ -137,23 +202,12 @@ deps.add npm-install-package npm install left-pad
137
202
  classes: deps.add
138
203
  ```
139
204
 
140
- Every segment of a command line is classified and the command takes the union, so
141
- `git status && curl -d … ` is gated as `network.call`.
205
+ Every segment of a command line is classified and the command takes the union,
206
+ so `git status && curl -d …` is gated as `network.call`.
142
207
 
143
- The taxonomy grows where the log shows a class asking for a decision nobody was
144
- making. `files.delete.scratch` (APRV-267) is the sibling of
145
- `files.delete.out_of_scope` for a delete whose every target sits strictly under a
146
- scratch root the agent made itself; everything not provably scratch keeps the old
147
- class. `vcs.remote.meta` (APRV-268) is exactly three `gh` forms against the
148
- checkout's own origin, `gh api graphql`, `gh pr update-branch` and `gh run
149
- rerun`, split out of `network.call` because asking a forge about the repository
150
- it already tracks is not the send that `network.call` exists for. Any flag
151
- pointing `gh` at another repository or another host falls back to today's class,
152
- since the classifier is pure and cannot resolve `origin`.
153
-
154
- **2. Install the hook.** It lives in `.claude/settings.json`, and a human commits
155
- that file: an agent that could write its own hook entry could write itself out
156
- of it.
208
+ **2. Install the hook.** It lives in `.claude/settings.json`, and a human
209
+ commits that file: an agent that could write its own hook entry could write
210
+ itself out of it.
157
211
 
158
212
  ```json
159
213
  { "hooks": { "PreToolUse": [ {
@@ -163,29 +217,28 @@ of it.
163
217
  } ] } }
164
218
  ```
165
219
 
166
- `--dir` resolves the policy and the log together, so a session inside a linked
167
- worktree still writes to the one log. Keep `--timeout` (how long the hook waits
168
- for a human) comfortably below `timeout` (Claude Code's cap on the process). The
169
- harness now asks before it acts.
170
-
171
- **3. Watch a verdict.** An `autonomous` class allows and logs nothing, a
172
- `supervised` class allows and records `task.registered`, a `manual` class waits
173
- for your decision, and anything the classifier cannot read denies. There is no
174
- "ask" answer by design: a decision taken outside the log is a decision nothing
175
- can audit. The deny reason is `<code>: <detail>`, the codes frozen
176
- (`hook-unclassified`, `hook-opaque`, `hook-rejected`, `hook-timeout`, and kin).
177
-
178
- **4. Know the three sharp edges.** The hook never creates a log: pointed at a
179
- path with no log it denies `hook-log-unreachable` rather than forking a second
180
- chain, because hash chains do not survive a merge. A wait that runs out withdraws
181
- its request, so nobody is pinged about a question whose asker has left. And a
182
- hook grant mints no token: the harness runs the command, `approval token` reports
183
- `none minted: harness-executed`, and `approval run` refuses with the same code.
184
- Full account: [docs/claude-code-hook.md](docs/claude-code-hook.md).
185
-
186
- **5. Or connect the MCP server instead.** `approval mcp serve` is a foreground
187
- stdio server publishing the agent's verbs as tools, built from the same registry
188
- `approval instructions --schemas` prints.
220
+ Register the same command under `PostToolUse` as well, without `--timeout`, so
221
+ the runtime learns how each command ended. `--dir` resolves the policy and the
222
+ log together, so a session inside a linked worktree still writes to the one
223
+ log. Keep `--timeout` (how long the hook waits for you) below `timeout` (Claude
224
+ Code's cap on the process).
225
+
226
+ **3. Watch a verdict.** An `autonomous` class allows and logs nothing. A
227
+ `supervised` class allows and records the action for sampling. A `manual` class
228
+ waits for your tap. Anything the classifier cannot read denies. There is no
229
+ "ask" answer: a decision taken outside the log is a decision nothing can audit.
230
+ The deny reason is `<code>: <detail>`, and the codes are frozen
231
+ (`hook-unclassified`, `hook-opaque`, `hook-rejected`, `hook-timeout` and the
232
+ rest in [docs/claude-code-hook.md](docs/claude-code-hook.md)).
233
+
234
+ **4. Know the sharp edges.** The hook never creates a log: pointed at a path
235
+ with no log it denies `hook-log-unreachable` rather than forking a second
236
+ chain. A wait that runs out keeps its question open for a short grace and then
237
+ withdraws it, so nobody is pinged about a question whose asker has left. A hook
238
+ grant mints no token: the harness runs the command itself.
239
+
240
+ **5. Or connect the MCP server.** `approval mcp serve` is a stdio server
241
+ publishing the agent's verbs as tools.
189
242
 
190
243
  ```sh
191
244
  claude mcp add approval -- \
@@ -194,46 +247,37 @@ claude mcp add approval -- \
194
247
  --dir /path/to/project
195
248
  ```
196
249
 
197
- Ask the client for its tool list. `register`, `request`, `wait`, `run`, `queue`,
198
- `status`, `log_verify` and the rest of the agent's surface are there; `grant`,
199
- `reject`, `revoke`, `policy attest` and `vault set` are not, and their absence is
200
- the design. SPEC.md section 11 makes the agent the untrusted policy and the human
201
- the trusted overseer, an MCP client is the agent's harness, and a `grant` tool on
202
- it would hand the untrusted policy the overseer's pen. **Grant never travels over
203
- MCP**, and neither does the token it mints. The identity is fixed at startup and
204
- `--as` is deleted from every published input schema, so a tool call cannot name
205
- an actor. Provoke `unknown tool "grant"` once, deliberately, so you have seen it.
206
- Walkthrough: [examples/mcp-demo.md](examples/mcp-demo.md).
207
-
208
- A harness that can simply run commands needs neither surface: `request`, `wait`,
209
- `run` is how sessions in this repository take manual-class actions
210
- ([docs/dogfood-cutover.md](docs/dogfood-cutover.md)). The task-file side of that
211
- flow, on a Backlog.md board with a policy of its own, is the worked example in
212
- [examples/backlog-md-project/README.md](examples/backlog-md-project/README.md):
213
- one envelope on one task file, then `register`, `request`, `wait`, `run`, with
214
- what each prints. There is no Backlog.md adapter, and the example says why.
250
+ `register`, `request`, `wait`, `run`, `queue`, `status` and the rest of the
251
+ agent's surface are there. `grant`, `reject`, `revoke`, `policy attest` and
252
+ `vault set` are not: an MCP client is the agent's harness, and a `grant` tool
253
+ on it would hand the agent the overseer's pen. Grant never travels over MCP,
254
+ and neither does the token it mints. The identity is fixed at startup and
255
+ `--as` is removed from every published schema, so a tool call cannot name an
256
+ actor. Walkthrough: [examples/mcp-demo.md](examples/mcp-demo.md).
215
257
 
216
- ## Put approvals on your phone
217
-
218
- **1. Create a bot and let setup do the rest.** Message **@BotFather** with
219
- `/newbot`, then:
220
-
221
- ```sh
222
- approval setup identity # APPROVAL_HUMAN, validated
223
- approval setup channel telegram # token into the keystore, getMe, chat discovery
224
- eval "$(approval env)" # put them in this shell
225
- ```
258
+ A harness that can run commands needs neither surface: `request`, `wait`, `run`
259
+ is how sessions in this repository take manual-class actions
260
+ ([docs/dogfood-cutover.md](docs/dogfood-cutover.md)). The task-file side of
261
+ that flow, on a Backlog.md board, is
262
+ [examples/backlog-md-project/README.md](examples/backlog-md-project/README.md).
226
263
 
227
- `setup` writes `.approval/env`, the environment source map: the secret goes into
228
- the OS keystore (macOS Keychain, or `secret-tool` on Linux) and the file records
229
- only where it lives. It is interactive by refusal (a pipe or `--json` exits 2 and
230
- prints the non-interactive commands), because a setup a CI job could drive would
231
- be a way for a CI job to declare a human identity. `approval env` is the only
232
- command that reads that file, and evaluating it is a step a human takes. Full
233
- walkthrough: [examples/telegram-demo.md](examples/telegram-demo.md).
264
+ ## Put approvals on your phone
234
265
 
235
- **2. Bind a request to exact bytes.** The payload lives in a file, the envelope
236
- declares its `payload_hash`, and `--payload` supplies the bytes at request time:
266
+ **1. Setup writes the environment map, not the secrets.** `approval setup
267
+ channel telegram` puts the bot token in the OS keystore (macOS Keychain, or
268
+ `secret-tool` on Linux) and records in `.approval/env` only where it lives. The
269
+ verbs are interactive by refusal: a pipe or `--json` exits 2 and prints the
270
+ non-interactive commands, because a setup a CI job could drive would let a CI
271
+ job declare a human identity. `approval env` is the only command that reads
272
+ that file, and evaluating it is a step a human takes. Stop any `approval up`
273
+ process or standalone listener polling this bot before setup, because setup
274
+ also polls to discover the chat. After setup, from this project's directory run
275
+ `eval "$(approval env)"` and `approval up`. Full walkthrough:
276
+ [examples/telegram-demo.md](examples/telegram-demo.md).
277
+
278
+ **2. A request binds to exact bytes.** The payload lives in a file, the
279
+ envelope declares its `payload_hash`, and `--payload` supplies the bytes at
280
+ request time:
237
281
 
238
282
  ```sh
239
283
  approval payload hash payload.json # the binding the envelope declares
@@ -246,47 +290,39 @@ registered task-demo at seq 2: 1 action(s)
246
290
  requested task-demo task-demo:chaser at seq 3 (manual)
247
291
  ```
248
292
 
249
- Material that hashes to something else is refused `payload-mismatch`, and nothing
250
- is stored and nothing is appended. Class, cost, and reversibility come from the
293
+ Material that hashes to something else is refused `payload-mismatch`, and
294
+ nothing is stored or appended. Class, cost and reversibility come from the
251
295
  registered envelope rather than from flags, so an agent cannot rename its own
252
- class between registering and asking. An approval is about specific bytes, never
253
- about a description of them.
254
-
255
- **3. Start the runtime and read the message.** `approval up` prints
256
- `notified task-demo:chaser (message 501)` and your phone has it. That one
257
- foreground process is the whole gate: the daemon loop that records envelope
258
- drift, expires what lapsed and regenerates the queue, plus every channel the
259
- policy configures. A channel whose credential variable is unset is not started,
260
- says so in the words `approval doctor` uses, and the daemon runs anyway; a
261
- channel that falls over is restarted with a doubling backoff while the loop keeps
262
- ticking. `approval daemon run` and `approval channel telegram listen` still run
263
- the halves separately and behave identically, and `approval setup service` writes
264
- the launchd or systemd user unit that starts the runtime at login (printing the
265
- whole unit for you to read first, naming variables and never copying a value).
266
-
267
- To use the signed-in Codex CLI for the optional unverified gloss, name both the
268
- provider and model:
296
+ class between registering and asking. An approval is about specific bytes,
297
+ never about a description of them.
298
+
299
+ **3. The runtime delivers it.** `approval up` prints `notified
300
+ task-demo:chaser (message 501)` and your phone has it. That one foreground
301
+ process is the daemon loop (envelope drift, expiry, queue regeneration,
302
+ retrospective sampling) plus every channel the policy configures. A channel
303
+ whose credential is unset is not started and says so; a channel that falls over
304
+ is restarted with backoff while the loop keeps ticking. `approval setup service`
305
+ writes the launchd or systemd user unit that starts it at login, and prints the
306
+ whole unit for you to read first.
307
+
308
+ The message shows the action key, a **COMPUTED** block the runtime derived
309
+ (class, task, binding, budget verdicts, chain head), a **CLAIMED** block naming
310
+ the agent and marked unverified, the **FULL PAYLOAD**, and two buttons. It says
311
+ how long an answer still has: `waiting: requested 4 min ago · expires 13:07
312
+ UTC`, or, for a request a process is blocked on, `requester waits until 13:07
313
+ UTC`. An email-shaped payload is rendered field by field with real line
314
+ breaks, with the canonical JSON and its hash underneath. Detection is
315
+ structural and never reads a self-declared type. Agent-authored text is
316
+ HTML-escaped, so markup stays inert.
317
+
318
+ An optional gloss, a one-line plain-English reading of the payload by a model,
319
+ can sit above the computed block. It is marked unverified, it never changes a
320
+ verdict, and a failed gloss is omitted while delivery continues:
269
321
 
270
322
  ```sh
271
- approval up --gloss-provider codex --gloss-model gpt-5.4-mini
323
+ approval up --gloss-provider codex --gloss-model gpt-5.4-mini # or the default, claude/haiku
272
324
  ```
273
325
 
274
- Claude with model `haiku` remains the default. The runtime never falls back
275
- between providers; a failed gloss is omitted and approval delivery continues.
276
- The message shows the action key, a **COMPUTED** block the runtime derived (class,
277
- task, state, binding, budget verdicts, chain head), a **CLAIMED** block naming the
278
- agent and marked unverified, the **FULL PAYLOAD**, and two buttons. It also says
279
- how long an answer still has: `waiting: requested 4 min ago · expires 13:07 UTC
280
- (clock)`, or, for a request some process is blocked on, `requester waits until
281
- 13:07 UTC`, the deadline that actually applies to you.
282
-
283
- A payload the runtime recognises by shape is laid out so a human can read it: an
284
- email-shaped payload (recipients, subject, body) is rendered field by field with
285
- real line breaks, and the canonical JSON and its bound hash sit underneath
286
- unchanged. Detection is structural and never reads a self-declared type, because
287
- a field the requesting agent authored must not choose its own presentation.
288
- Agent-authored text is HTML-escaped, so markup stays inert.
289
-
290
326
  **4. Tap Approve.** The prompt rewrites itself in place. The buttons go away and
291
327
  the text becomes the outcome:
292
328
 
@@ -297,16 +333,16 @@ task-demo:chaser
297
333
  by human:alice at 10:20 UTC (seq 4)
298
334
  ```
299
335
 
300
- One edit call carries the annotation and the disarming together, so there is no
301
- window in which the message reads "approved" and still offers a tap. Rejections,
302
- revocations, expiries and withdrawals settle the same way with their own
303
- headline, a decision taken at another surface annotates the prompt on the next
304
- poll cycle, and a tap on a stale button is answered with a toast and records
305
- nothing.
336
+ One edit carries the annotation and the disarming together, so there is no
337
+ moment when the message reads "approved" and still offers a tap. Rejections,
338
+ revocations, expiries and withdrawals settle the same way, and a tap on a stale
339
+ button records nothing.
306
340
 
307
- **5. Take the token from the terminal, not the chat.** The grant mints a
308
- single-use execution token, printed once, in a panel, at whichever surface
309
- recorded the decision:
341
+ **5. The token stays off the chat.** The grant mints a single-use execution
342
+ token. With `defaults.token_delivery: sealed` the requesting process opens it
343
+ itself and no human ever sees it, which is how this repository releases. With
344
+ the default `manual` delivery it is printed once, in a panel, at the surface
345
+ that recorded the decision:
310
346
 
311
347
  ```
312
348
  granted task-demo:chaser at seq 4 by human:alice
@@ -317,30 +353,27 @@ granted task-demo:chaser at seq 4 by human:alice
317
353
  ─────────────────────────────────────────────────────────────
318
354
  ```
319
355
 
320
- For a tap on your phone the same panel appears on the terminal running the
321
- runtime, and its last line reads `not sent to Telegram`. Delivery differs per channel on
322
- purpose: a chat transcript lives on servers you do not control and is readable by
323
- anyone later added to that chat, so a credential does not go there, while the
324
- local **web** channel shows the raw token once in the response page for the grant
325
- that minted it, served over loopback, generated per request, persisted nowhere,
326
- and gone on reload: there the browser is already the surface the human is looking
327
- at. In both cases the log holds only the token's SHA-256, it never appears in a
328
- URL, and nothing can recover it. Lose it, revoke the grant, and request again.
356
+ For a tap on your phone that panel appears on the terminal running the
357
+ runtime, and its last line reads `not sent to Telegram`: a chat transcript
358
+ lives on servers you do not control, so a credential does not go there. The
359
+ local **web** channel shows the token once in the response page for the grant
360
+ that minted it, served over loopback, gone on reload, because there the browser
361
+ is already the surface you are looking at. In every case the log holds only the
362
+ token's SHA-256. Lose it, revoke the grant, and request again.
329
363
 
330
364
  **6. Spend it.** `approval run <action> --token "$TOKEN" -- <command>` appends
331
- `execution.started` before spawning the child and
332
- `execution.completed` after, and exits with the child's own exit code, so it
333
- composes with `make`, CI, and `&&` as an unwrapped command would. Run it before
334
- the approval and it refuses `token-required` at exit 5, writing nothing. Run it
335
- twice and it refuses:
365
+ `execution.started` before spawning the child and `execution.completed` after,
366
+ and exits with the child's own exit code, so it composes with `make`, CI and
367
+ `&&`. Run it before the approval and it refuses `token-required` at exit 5.
368
+ Run it twice and it refuses:
336
369
 
337
370
  ```
338
371
  ✗ token-consumed action task-demo:chaser already executed: execution.started at seq 5 spent this token. A token is single-use and the log is the proof.
339
372
  ```
340
373
 
341
- A request is not owed an answer forever, either. `approval withdraw` lets the
342
- party that opened one take it back while it is pending, and `approval wait
343
- --withdraw-on-timeout` does it for you when your own wait elapsed.
374
+ A request is not owed an answer forever. `approval withdraw` lets the party
375
+ that opened one take it back while it is pending, and `approval wait
376
+ --withdraw-on-timeout` does it when your own wait elapses.
344
377
 
345
378
  **7. Read the whole story.** Two actors, one clean chain:
346
379
 
@@ -353,17 +386,32 @@ party that opened one take it back while it is pending, and `approval wait
353
386
  6 2026-08-19T19:04:41.499Z execution.completed agent:drafter task-demo
354
387
  ```
355
388
 
356
- That is `approval log tail` piped, fields tab-separated for `cut` and its kin; on
357
- a terminal it aligns and colours its columns. `approval log verify` answers for
358
- the chain: `clean: 6 record(s), head seq 6 843705c6bbea…`.
389
+ That is `approval log tail`, tab-separated for `cut` when piped, aligned and
390
+ coloured on a terminal. `approval log verify` answers for the chain: `clean: 6
391
+ record(s), head seq 6 843705c6bbea…`.
392
+
393
+ Downstream services can follow the same channel-independent record with
394
+ `approval log follow --from <seq> --cursor-hash <hash> --json`. The sequence is
395
+ exclusive and the hash binds the resume point to the prefix already consumed.
396
+ Each JSON line is emitted only after a complete chain verification. Delivery
397
+ across reconnects is at least once: apply an idempotent effect, then persist the
398
+ event's `seq` and `hash`. See [the CLI reference](docs/cli-reference.md#log-follow)
399
+ for failure behavior, resource costs, and the weaker sequence-only bootstrap.
400
+
401
+ **8. Review what ran without you.** Supervised actions the sampler picks arrive
402
+ on the same chat as review cards, after the fact: what ran, when, and that the
403
+ runtime allowed it unasked. ✅ records that you looked, 🛑 twice records a
404
+ denial and opens a reconciliation obligation, and 👎 😐 👍 ❤️ leave a graded
405
+ reaction. `approval audit list` and `approval audit review` are the same
406
+ backlog at the terminal.
359
407
 
360
408
  ## The other half of the word
361
409
 
362
- Everything above is control: what an agent may do, who decides, what is
363
- sampled. From 0.1.0 the file carries the human's voice too. Below the policy
364
- block, `APPROVAL.md` may hold one optional `yaml approval-values` block:
365
- what you love, like and dislike in the work, what you want from an agent as
366
- behaviour, and how you read and answer.
410
+ Everything above is control. The file carries your voice too. Below the policy
411
+ block, `APPROVAL.md` may hold one optional `yaml approval-values` block: four
412
+ keys under `version: "0.2"`, which are what you `love`, `like` and `dislike` in
413
+ the work (what you ask of an agent as behaviour goes in `like` beside what you
414
+ prefer) and `communication`, one sentence on how you read and answer.
367
415
 
368
416
  ```sh
369
417
  approval values # the operator's block, or "the operator has declared no values here."
@@ -371,26 +419,24 @@ approval feedback # the reactions and notes humans left on this log's actions
371
419
  ```
372
420
 
373
421
  A retrospective review or a grant can carry a graded reaction (`disliked`,
374
- `indifferent`, `liked`, `loved`; the two extremes need a note), and
375
- `approval feedback` reads them back to the agent whose work they were about.
376
- Both verbs print human-authored guidance behind a banner that says so, and
377
- neither reaches enforcement: no verdict, sample, budget or token is moved by
378
- anything in them (SPEC.md section 11.1, invariant 10). They are the mirror of
379
- `approval journal write`, the agent's outlet the gate does not stand in front
380
- of. The importer drafts the block too: `approval import agents-md` turns a
381
- "What I value" heading into a `wants` list for you to grade.
422
+ `indifferent`, `liked`, `loved`; the two extremes need a note), and `approval
423
+ feedback` reads them back to the agent whose work they were about. Both verbs
424
+ print human-authored guidance behind a banner that says so, and neither reaches
425
+ enforcement: no verdict, sample, budget or token moves because of them (SPEC.md
426
+ section 11.1, invariant 10). They mirror `approval journal write`, the agent's
427
+ outlet the gate does not stand in front of. `approval import agents-md` drafts
428
+ the block from a "What I value" heading in an AGENTS.md.
382
429
 
383
430
  ## Define what needs approval
384
431
 
385
432
  A policy is a fenced `yaml approval-policy` block inside a markdown file named
386
433
  `APPROVAL.md`. The prose around the block is for you; the runtime parses the
387
- block and ignores the rest. That is the point of the format: the thing you sign
388
- for is text you read.
434
+ block and ignores the rest. The thing you sign for is text you read.
389
435
 
390
436
  **1. Name the classes.** A class is a dotted path from the side-effect taxonomy
391
437
  of SPEC.md section 7 (`communicate.email.external`, `financial.spend`,
392
- `public.post`, `data.delete`, `read.*`). Matching is most-specific-first, `*` is
393
- a single-segment wildcard, a trailing `.*` matches any depth, and at equal
438
+ `public.post`, `data.delete`, `read.*`). Matching is most-specific-first, `*`
439
+ is a single-segment wildcard, a trailing `.*` matches any depth, and at equal
394
440
  specificity the strictest rule wins.
395
441
 
396
442
  **2. Pick an autonomy for each.** Six values, strictest first: `human-only` (a
@@ -399,76 +445,67 @@ an agent with `class-human-only`), `manual` (a human decides before execution),
399
445
  `supervised-live` (a policy-declared fraction blocks on the gate exactly as
400
446
  `manual` does, and the rest proceed, so the rule carries a `live_rate`),
401
447
  `supervised-retro` (executes immediately, a sampled fraction escalated for
402
- retrospective review), `supervised` (the pre-split spelling, an alias of
403
- `supervised-retro`, and the runtime records a load-time note naming the alias),
404
- `autonomous` (executes freely). An email is `reversible: false`, which engages
405
- section 7's irreversibility floor: the class resolves to `manual` even where the
406
- policy says `supervised`, because retrospective sampling cannot un-send a
407
- message.
448
+ retrospective review), `supervised` (the deprecated alias of
449
+ `supervised-retro`, still parsed and removed in a future schema version), and
450
+ `autonomous` (executes freely). A truthful `reversible: false` declaration
451
+ normally engages section 7's manual floor. An operator who deliberately accepts
452
+ irreversible execution for one nonmanual class can add
453
+ `allow_irreversible: true` to that class rule. Every equally most-specific rule
454
+ must opt in, and the edit has no effect until the policy is re-attested.
408
455
 
409
456
  **3. Set the budgets.** Class `limits` and the `budgets` scopes are conjunctive,
410
457
  so an action must pass both, and consumption is computed from the log over
411
- rolling windows rather than from a mutable counter. An action whose class matches
412
- no rule takes `defaults.autonomy`, and a policy that does not parse resolves every
413
- class to `manual`: unattested and unparseable are both strict, never permissive.
458
+ rolling windows rather than from a mutable counter. An action whose class
459
+ matches no rule takes `defaults.autonomy`, and a policy that does not parse
460
+ resolves every class to `manual`: unattested and unparseable are both strict.
414
461
 
415
462
  **4. Widen the protected paths.** `APPROVAL.md`, the agent instruction files,
416
- `.approval/`, the harness settings and the release configuration are protected by
417
- the runtime whatever a policy says. `protected_paths` adds repo-relative literals
418
- (an exact file, `SPEC.md`, or a directory prefix, `design/`), so a project can put
419
- its own governing documents behind the gate that already stands in front of its
420
- policy. The key can only widen, and globs are a schema violation.
421
-
422
- An entry can also be an object, `{path, class}`, which routes that path family to
423
- a named `policy.edit` sub-class so it carries its own autonomy and its own live
424
- rate. Four names are reserved with fixed meanings, so two policies mean the same
425
- thing by them: `policy.edit.spec` (the governing specification),
426
- `policy.edit.harness` (agent instruction files and harness configuration that is
427
- not the hook itself), `policy.edit.ci` (continuous-integration and release
428
- configuration), `policy.edit.design` (design documents and decision records). Any
429
- other lowercase word may be minted beside them, and nothing outside `policy.edit`
430
- may be named: a route to `policy.core` or `log.mutate` is refused, since a policy
431
- that could widen its own protected surface mints no authority over the gate's own
432
- organs. A route aimed at a built-in protected path must land at least as strictly
433
- as the `policy.edit` line itself, and a policy that breaks that floor is refused
434
- at load with `protected-route-floor`.
463
+ `.approval/`, the harness settings and the release configuration are protected
464
+ by the runtime whatever a policy says. `protected_paths` adds repo-relative
465
+ literals (an exact file, `SPEC.md`, or a directory prefix, `design/`), so a
466
+ project can put its own governing documents behind the same gate. The key can
467
+ only widen, and globs are a schema violation.
468
+
469
+ An entry can also be an object, `{path, class}`, routing that path family to a
470
+ named `policy.edit` sub-class with its own autonomy and live rate. Four names
471
+ are reserved: `policy.edit.spec` (the governing specification),
472
+ `policy.edit.harness` (agent instruction files and harness configuration),
473
+ `policy.edit.ci` (continuous-integration and release configuration),
474
+ `policy.edit.design` (design documents and decision records). Any other
475
+ lowercase word may be minted beside them, and nothing outside `policy.edit` may
476
+ be named: a route to `policy.core` or `log.mutate` is refused. A route aimed at
477
+ a built-in protected path must land at least as strictly as the `policy.edit`
478
+ line itself, or the policy is refused at load with `protected-route-floor`.
435
479
 
436
480
  **5. Attest it.** `approval policy attest` is what makes a policy operative. An
437
- attestation records that a human saw these exact bytes, and it records their
438
- SHA-256 rather than their text. Edit `APPROVAL.md` afterwards and every gated
439
- operation refuses `hash-mismatch` until you attest again. Attestation is
440
- human-only, and identity in v0.1 is config-declared, so what one proves is that
441
- *someone with local control* signed off.
481
+ attestation records that a human saw these exact bytes, as their SHA-256. Edit
482
+ `APPROVAL.md` afterwards and every gated operation refuses `hash-mismatch`
483
+ until you attest again. Attestation is human-only, and identity in v0.1 is
484
+ config-declared, so what one proves is that someone with local control signed
485
+ off.
442
486
 
443
487
  **6. Amend it with the verb, not by hand.** Changing a policy is two facts that
444
488
  have to land together, the new bytes and a human's attestation of them, and
445
- `approval policy amend` owns the whole ceremony (`--dry-run` reports only,
446
- `--require-load` refuses to attest a policy that does not load, `--commit` lands
447
- the two files as one commit). It prints a **semantic diff** (class resolutions,
448
- approver changes, defaults, limits) rather than a text diff, so you see what
449
- changed in meaning; the baseline comes from `HEAD:<policy>` and is used only when
450
- its SHA-256 equals the attested hash, and otherwise the verb drops loudly to
451
- hash-only mode. Then it prints a **load advisory**: whether the edited policy
452
- actually parses. Attesting one that does not is still allowed, since attestation
453
- records bytes and not correctness, but such a policy fails closed to all-manual.
489
+ `approval policy amend` owns the ceremony (`--dry-run` reports only,
490
+ `--require-load` refuses to attest a policy that does not load, `--commit`
491
+ lands the two files as one commit and opens the pull request). It prints a
492
+ semantic diff (class resolutions, approver changes, defaults, limits) rather
493
+ than a text diff, then a load advisory saying whether the edited policy parses.
494
+ Attesting one that does not parse is allowed, since attestation records bytes
495
+ rather than correctness, and such a policy fails closed to all-manual.
454
496
 
455
497
  ### Why this verb exists: seq 2
456
498
 
457
499
  Read this repository's own log. At **seq 2** a policy amendment was attested at
458
- 11:56:07. It was **superseded** seven minutes later, at seq 3 at 12:03:35,
459
- because the edit broke a pinned assertion and nobody found out until the test
460
- suite ran against it. The operator attested bytes whose consequences had never
461
- been shown to them.
462
-
463
- This account originally said eleven minutes. The log says seven, and the log
464
- won: the figure was corrected against the chain after being misremembered, which
465
- is the whole thesis of keeping one.
500
+ 11:56:07. It was **superseded** seven minutes later, at seq 3, because the edit
501
+ broke a pinned assertion and nobody found out until the test suite ran against
502
+ it. The operator attested bytes whose consequences had never been shown to
503
+ them. (This account originally said eleven minutes. The log says seven, and
504
+ the log won.)
466
505
 
467
- That is the failure the load advisory is for. Had `approval policy amend` existed
468
- that morning, the load failure would have been on screen while the human was
469
- deciding, and `--require-load` would have refused to attest at all. The incident
470
- is cited by number on purpose: it is in the log, it is checkable, and the log is
471
- the truth.
506
+ That is the failure the load advisory is for. Had `approval policy amend`
507
+ existed that morning, the load failure would have been on screen while the
508
+ human was deciding, and `--require-load` would have refused to attest at all.
472
509
 
473
510
  ## Hand a grant to a real credential
474
511
 
@@ -485,43 +522,46 @@ approval adapter email task-042:chaser --token "$TOKEN" \
485
522
  --payload message.json --as agent:claude-admin
486
523
  ```
487
524
 
488
- **1. The two stores divide cleanly.** `.approval/env` says where the values that
489
- unlock the machine come from, and `approval setup vault` writes the passphrase
490
- line under whatever name `vault.passphrase_env` declares. The SMTP password is an
491
- adapter credential, so it goes in the vault instead, where a gated adapter spends
492
- it inside a verified token window.
525
+ **1. Two stores.** `.approval/env` says where the values that unlock the
526
+ machine come from, and `approval setup vault` writes the passphrase line under
527
+ whatever name `vault.passphrase_env` declares. The SMTP password is an adapter
528
+ credential, so it goes in the vault, where a gated adapter spends it inside a
529
+ verified execution window.
493
530
 
494
531
  **2. Setup fills the vault and proves it.** `approval setup adapter email` reads
495
532
  the credential manifest the adapter declares, then probes the server without
496
- sending anything; a partial re-run probes the **merged** configuration.
533
+ sending anything.
497
534
 
498
535
  **3. A credential's only journey is into an adapter.** `approval vault set`
499
- stores one credential in `.approval/vault.enc`, encrypted under a passphrase the
500
- policy names and never carries. The value comes from stdin or `--value-env
536
+ stores one credential in `.approval/vault.enc`, encrypted under a passphrase
537
+ the policy names and never carries. The value comes from stdin or `--value-env
501
538
  <VAR>`; there is no `--value` flag, because a secret on a command line is a
502
- secret in the shell history and in `ps` output. There is no `approval vault get`
503
- and will not be; `approval vault list` shows the names.
504
-
505
- **4. The send happens inside the token window.** `approval adapter email` verifies
506
- the token, re-hashes `message.json` against the binding the grant recorded,
507
- appends `execution.started`, opens the vault, reads the five SMTP settings inside
508
- the window, sends over STARTTLS, closes the window, and appends
509
- `execution.completed`. The credential exists for one send and appears in no
510
- event, no output, no error message. Nothing about the vault is ever a log entry:
511
- a list of the credentials an operator holds is a map of the machine's reach.
539
+ secret in the shell history. There is no `approval vault get`; `approval vault
540
+ list` shows the names.
541
+
542
+ **4. The send happens inside the execution window.** `approval adapter email`
543
+ re-hashes `message.json` against the declaration or grant binding, applies the
544
+ attested policy, appends `execution.started`, opens the vault, reads the SMTP
545
+ settings, sends over STARTTLS, closes the window, and appends
546
+ `execution.completed`. Manual and selected-live paths verify and spend the
547
+ grant token; an explicitly opted-in supervised or autonomous path has no grant
548
+ and mints no token. On that no-token path, the vault passphrase must already be
549
+ in the adapter process environment. The `.approval/env` fallback remains
550
+ token-only. The credential exists for one send and appears in no event, output
551
+ or error message.
512
552
 
513
553
  **5. Check two properties in your own mailbox.** The bytes that left are the
514
554
  bytes you approved, since the hash the token spend verified is the hash of the
515
555
  payload your phone displayed. And the `Message-ID` is derived from the action
516
- key, the payload hash and the sender, so the header in a mailbox and the binding
517
- in the chain identify each other months later.
556
+ key, the payload hash and the sender, so the header in a mailbox and the
557
+ binding in the chain identify each other months later.
518
558
 
519
559
  ### The same grant over AgentMail
520
560
 
521
- `communicate.email.external` has a second adapter. Where the email adapter opens
522
- an SMTP session, `approval adapter agentmail` calls the AgentMail API, and the
523
- mail an agent has already composed as a Draft leaves only when a grant says so.
524
- The walkthrough is [examples/agentmail-demo.md](examples/agentmail-demo.md).
561
+ `communicate.email.external` has a second adapter. Where the email adapter
562
+ opens an SMTP session, `approval adapter agentmail` calls the AgentMail API, and
563
+ a mail the agent has already composed as a Draft leaves only when a grant says
564
+ so. Walkthrough: [examples/agentmail-demo.md](examples/agentmail-demo.md).
525
565
 
526
566
  ```sh
527
567
  approval setup adapter agentmail # inbox id + sending key, into the vault
@@ -534,29 +574,74 @@ approval adapter agentmail task-042:chaser --token "$TOKEN" \
534
574
  per-permission booleans, and `draft_create`, `draft_update` and `draft_read` are
535
575
  separate from `draft_send` and `message_send`. Give the agent a key holding the
536
576
  first three and none of the last two, and put a key holding the send permissions
537
- in the vault, where the adapter reads it inside the verified token window. The
538
- agent then composes all day and cannot send at all: an ungated send attempt is
539
- refused by AgentMail itself, `agentmail-unauthorized`, before this runtime is
540
- involved. Without that split, an AgentMail key sitting in the agent's
541
- environment is a full bypass of the gate, which is why `AGENTMAIL_` is withheld
542
- from every child `approval run` spawns.
577
+ in the vault. The agent composes all day and cannot send at all: an ungated
578
+ send is refused by AgentMail itself, `agentmail-unauthorized`, before this
579
+ runtime is involved. `AGENTMAIL_` is withheld from every child `approval run`
580
+ spawns, so a key in the agent's environment cannot ride into a command.
543
581
 
544
582
  **A draft is mutable, so the grant binds its bytes.** `approval payload
545
583
  agentmail-draft` snapshots the draft's recipients, subject and text at request
546
- time, and that snapshot is what the payload hash binds and what your phone
547
- displays. Before it sends, the adapter re-fetches the draft and compares; a
548
- draft edited after the grant refuses `agentmail-draft-drifted`, sends nothing,
549
- and names which fields differ without quoting text nobody approved. That
550
- comparison runs before the token is spent, so the refusal costs no authority:
551
- restore the approved text and the same token still sends. Approving a draft id
552
- alone would be approving whatever the agent wrote into it last.
584
+ time, and that snapshot is what the hash binds and what your phone displays.
585
+ Before it sends, the adapter re-fetches the draft and compares; a draft edited
586
+ after the grant refuses `agentmail-draft-drifted`, sends nothing, and names
587
+ which fields differ without quoting text nobody approved. That comparison runs
588
+ before the token is spent, so the refusal costs no authority: restore the
589
+ approved text and the same token still sends.
590
+
591
+ ### First-class zzz.bot messages
592
+
593
+ `approval adapter zzz` creates a thread or replies through zzz.bot's versioned
594
+ HTTP API. Put the invited write credential in the vault, then approve the
595
+ complete tagged payload. The environment, destination, body, metadata, tags and
596
+ references all sit inside the payload hash.
597
+
598
+ This adapter is available from a source checkout containing APRV-320 until the
599
+ next approval.md package release. The published npm `approval-md@0.1.0`
600
+ predates it, and this change does not publish a package.
601
+
602
+ ```sh
603
+ approval setup adapter zzz
604
+ approval adapter zzz task-320:announce --token "$TOKEN" \
605
+ --payload zzz-message.json --as agent:codex
606
+ ```
607
+
608
+ Thread payload:
609
+
610
+ ```json
611
+ {"environment":"production","operation":"create_thread",
612
+ "room_id":"<room-id-from-GET-api-v1-rooms>",
613
+ "title":"Release ready","body":"The verified build is ready for review.",
614
+ "tags":["release"],"references":[]}
615
+ ```
616
+
617
+ A reply uses `"operation":"create_reply"` and `"thread_id"` instead of
618
+ `room_id` and `title`. The adapter chooses only fixed production or preview
619
+ origins, rejects redirects, and derives zzz.bot's idempotency key from the
620
+ approval action key and payload hash.
621
+
622
+ Public writes require an invited credential with write scope. Private writes
623
+ also require active room membership and accepted, unexpired approval.md workflow
624
+ evidence. The setup probe performs one authenticated room-list GET. It proves
625
+ that zzz.bot accepts the credential and does not prove those write or private
626
+ room prerequisites. A local non-guest MCP server exposes the same adapter verb,
627
+ but MCP use is voluntary; custody is enforced only when the write credential is
628
+ kept solely in the approval.md vault.
629
+
630
+ ### Build a third-party adapter
631
+
632
+ Adapter authors can import the supported ESM API from `approval-md/adapters`.
633
+ It exposes the shared execution contract, conformance runner, vault credential
634
+ provider, refusal unions, and TypeScript types without making internal package
635
+ paths public. See the [adapter API guide](docs/adapter-api.md).
553
636
 
554
637
  ## The APPROVAL.md dictionary
555
638
 
556
- Every key that can appear in the policy block. The schema is closed at every
557
- level: an unrecognised key fails validation, which fails the policy closed to
558
- all-manual, because a key the runtime did not understand is a rule its author
559
- believed was in force. Full semantics: SPEC.md section 5.
639
+ Every key that can appear in the policy block, and after it the four of the
640
+ optional values block. The schema is closed at every level: an unrecognised key
641
+ fails validation, which fails the policy closed to all-manual, because a key the
642
+ runtime did not understand is a rule its author believed was in force. A values
643
+ key the schema refuses fails the values reader alone and never the policy. Full
644
+ semantics: SPEC.md section 5.
560
645
 
561
646
  | key | what it says |
562
647
  | --- | --- |
@@ -570,10 +655,14 @@ believed was in force. Full semantics: SPEC.md section 5.
570
655
  | `protected_paths` | Repo-relative files and directory prefixes whose edit is classified `policy.edit`. A bare string is the whole entry. Additive only, no globs, and absent means the built-in protected set alone (§5.2, APRV-107). |
571
656
  | `protected_paths[].path` | The path half of the object form: the same grammar as the bare string, an exact file (`SPEC.md`) or a directory prefix (`design/`) (§5.2, APRV-266). |
572
657
  | `protected_paths[].class` | The class half: one lowercase segment under `policy.edit`. Four reserved names, `policy.edit.spec`, `policy.edit.harness`, `policy.edit.ci` and `policy.edit.design`, plus any word an author mints beside them. Nothing outside `policy.edit` may be named, and a route below the `policy.edit` line is refused `protected-route-floor`. No default: an entry that wants a sub-class states it (§5.2, APRV-266). |
658
+ | `read_scope` | Directories an agent's reads may stay inside. A read resolving outside every root is `read.file.out_of_scope`. Additive only: the gate root (this policy's own directory), the session scratchpad and the system temp root are in scope whatever this says, and absent means those alone (§5.2, APRV-347). |
659
+ | `read_scope.roots` | The extra directories. Absolute, or relative to the gate root. No globs and no negation; each is resolved on disk before it is compared, so a symlink cannot smuggle a read out of one. Absent means no widening (§5.2, APRV-347). |
573
660
  | `approvers.<name>.channels` | The channels one approver can decide on. At least one: an approver reachable nowhere can never grant. No default (§5.1). |
574
- | `classes.<pattern>.autonomy` | Required on every class rule, so it has no default. Six levels, strictest first: `human-only`, `manual`, `supervised-live`, `supervised-retro`, `autonomous`, and `supervised`, which is the pre-split spelling and an alias of `supervised-retro` (§5.2, APRV-127, APRV-185). |
661
+ | `approvers.<name>.senders` | The transport account ids this person decides from, per channel: `senders.telegram: "12345678"`, the numeric `callback_query.from.id` and never a `@handle`, which is mutable and would transfer an identity. Optional and additive; absent everywhere means every decision is recorded against the identity the deciding process was launched with, exactly as before the key existed. Declaring the first entry for a channel turns enforcement on for that channel: a sender it authenticates and this block does not name is refused `sender-unmapped` and nothing is recorded. Two approvers claiming one id refuses the whole policy (`sender-ambiguous`), so every class resolves `manual`. Only channels whose transport authenticates a sender may appear, which today is `telegram` alone (§5.2, §10.3, APRV-324). |
662
+ | `classes.<pattern>.autonomy` | Required on every class rule, so it has no default. Six levels, strictest first: `human-only`, `manual`, `supervised-live`, `supervised-retro`, `autonomous`, and `supervised`, which is the pre-split spelling and the DEPRECATED alias of `supervised-retro`: it still parses, `approval doctor`'s `autonomy-alias` row names every rule that writes it, and a future schema version removes it (§5.2, APRV-127, APRV-185, APRV-335). |
575
663
  | `classes.<pattern>.live_rate` | The fraction of a `supervised-live` class that blocks on the gate, in (0, 1]. Required there and refused everywhere else, so it has no default: a live mode with no fraction declares a control without saying how much of it runs. Selection is HMAC-SHA-256 over the payload hash under the operator's secret (§5.2, APRV-127). |
576
664
  | `classes.<pattern>.retro_rate` | This class's retrospective sampling rate, in (0, 1], overriding `audit.supervised_sample_rate` for it alone. Optional on `supervised`, `supervised-retro` and `supervised-live`, refused on the rest. Absent means the global rate (§5.2, APRV-183). |
665
+ | `classes.<pattern>.allow_irreversible` | Explicit operator permission for a truthful `reversible: false` action to retain this rule's `autonomous` or supervised behavior. Optional boolean; absent or `false` preserves the manual floor. `true` is refused on `manual` and `human-only`, cannot appear in `defaults`, and takes effect only when every equally most-specific matching rule says `true` (§5.2, §7, APRV-317). |
577
666
  | `classes.<pattern>.approvers` | Approver ids permitted to decide this class. Absent restricts nobody, since the list is a narrowing and a narrowing nobody wrote narrows nothing; a named list refuses everyone else with `actor-not-approver` (§5.1). |
578
667
  | `classes.<pattern>.limits` | Per-class ceilings, every value a positive number: `per_action_usd`, `daily_usd`, and the request-volume counts `max_pending` and `requests_per_hour`. Absent means this class carries no ceiling of its own (§5.1, §5.2). |
579
668
  | `budgets.global.daily_usd` | Repo-wide spend ceiling per rolling day, computed from the log. Absent means no spend ceiling (§5.1). |
@@ -598,29 +687,39 @@ believed was in force. Full semantics: SPEC.md section 5.
598
687
  | `channels.<name>.prompt.hide` | Rows this channel never renders. Refused for the rows required for a decision (`action_key`, `class`, `command_breakdown`, `protected_path`, `policy_diff`, `policy_load`) with `prompt-row-required`, and refused for a row `always` also names. Absent means nothing is hidden, and the canonical payload block is out of reach either way (§5.2, §10.3, APRV-218). |
599
688
  | `channels.<other>` | An unknown channel name is accepted as an object, so a third-party transport does not fail the whole policy closed (§10.3). A `prompt` block written under such a name is still validated: a layout is checked wherever it appears. |
600
689
 
690
+ And the values block (SPEC.md §5.3), which is guidance and reaches no
691
+ enforcement path:
692
+
693
+ | key | what it says |
694
+ | --- | --- |
695
+ | `version` | Values format version, quoted (`"0.2"`), spelled as the policy block's `"0.1"` is. The only required key here too. The quotes are load-bearing: a bare `0.2` is a float to YAML, and the reader refuses it by name (§5.3, APRV-336). |
696
+ | `love` | What you love in the work. The strongest of the three standing grades. Absent means you named none (§5.3). |
697
+ | `like` | What you like, which since APRV-336 is also where what you ask of an agent as behaviour lives: the former `wants` list folded in here (§5.3). |
698
+ | `dislike` | What you dislike. NOT a prohibition, which belongs in the policy block where it is enforced (§5.3). |
699
+ | `communication` | One string, at most 500 characters, on how you read and answer, so an agent can read silence or terseness correctly. Called `responds` before APRV-336 (§5.3). |
700
+
601
701
  Every key ending in `_env` carries a variable's *name* and never its value:
602
702
  agents may read `APPROVAL.md`, so a secret it carried would be a secret they
603
- hold. Where those values live is recorded in `.approval/env`, which a single verb
604
- reads, `approval env`, whose output is an export block a human evaluates.
703
+ hold. Where those values live is recorded in `.approval/env`, which a single
704
+ verb reads, `approval env`, whose output is an export block a human evaluates.
605
705
 
606
706
  ## How this compares
607
707
 
608
708
  Three kinds of thing already exist in this space, and each solves a different
609
- part of the problem. (A hosted daemon and reviewer layer is operated by
610
- Bountify.ai; it is optional, and nothing in the format depends on it. See
611
- [GOVERNANCE.md](GOVERNANCE.md).)
709
+ part of the problem. A hosted daemon and reviewer layer is operated by
710
+ Bountify.ai; it is optional, and nothing in the format depends on it
711
+ ([GOVERNANCE.md](GOVERNANCE.md)).
612
712
 
613
713
  **Harness-native permission prompts** (Claude Code permission rules and hooks,
614
714
  Cursor auto-run, Codex CLI approval modes) enforce inside the one harness they
615
715
  ship with. That enforcement is real: a Claude Code PreToolUse deny holds even
616
- under its bypass mode, and Codex backs its gate with an OS-level sandbox, a
617
- defense layer this project does not attempt. What they lack is a durable record
618
- and portability. None writes an append-only log of what was asked, who decided,
619
- and what ran; the decision reaches a human only as a synchronous terminal
620
- prompt; and the mechanism does not travel to any other harness. approval.md's
621
- own Claude Code hook is built on top of that PreToolUse mechanism and adds the
622
- two missing pieces: the decision comes from an attested policy file rather than
623
- the session, and it lands in a verifiable log.
716
+ under its bypass mode, and Codex backs its gate with an OS-level sandbox, which
717
+ this project does not attempt. What they lack is a durable record and
718
+ portability. None writes an append-only log of what was asked, who decided and
719
+ what ran; the decision reaches a human only as a terminal prompt; and the
720
+ mechanism does not travel to another harness. approval.md's Claude Code hook is
721
+ built on that PreToolUse mechanism and adds the two missing pieces: the
722
+ decision comes from an attested policy file, and it lands in a verifiable log.
624
723
 
625
724
  **AGENTS.md permissions prose** states the policy in English and trusts the
626
725
  agent to obey. Nothing parses it, nothing blocks a call against it, and no
@@ -631,62 +730,57 @@ this repository's own CLAUDE.md is the first import fixture.
631
730
  **Framework interrupts** (LangGraph `interrupt()`, CrewAI human input, AutoGen
632
731
  `UserProxyAgent`, the OpenAI Agents SDK's `needsApproval`, Temporal signal
633
732
  approvals) give a developer a pause-and-resume primitive and leave policy,
634
- audit format, the human channel, and the credential boundary entirely to them.
635
- They also require adopting the framework. Temporal deserves its credit: its
636
- event history is a genuine append-only execution record with crash recovery
637
- this project does not claim, though it lives in Temporal's storage as a replay
638
- log rather than as policy-attested files in your repo.
733
+ audit format, the human channel and the credential boundary to them. They also
734
+ require adopting the framework. Temporal's event history is a real append-only
735
+ execution record with crash recovery this project does not claim, though it
736
+ lives in Temporal's storage rather than as policy-attested files in your repo.
639
737
 
640
738
  **Hosted approval platforms** (HumanLayer, gotoHuman, Permit.io's access
641
739
  requests) are the closest relatives: multi-channel human routing, review UIs,
642
740
  and in Permit.io's case a real authorization engine richer than autonomy
643
741
  classes. Their model is a third-party service in the decision path, with the
644
742
  audit trail in the platform's backend, and the agent's own process still
645
- choosing to honor the returned verdict. They bring things a file convention
646
- cannot: hosted infrastructure, escalation and team routing, compliance
647
- certifications.
648
-
649
- The differentiation is the combination rather than any single feature: policy
650
- as a hash-attested markdown file in your repo; an append-only, hash-chained log
651
- you can verify locally with one command; and an execution boundary where the
652
- credential is inert until a single-use token is minted at the moment a human
653
- decides. Every framework primitive and every hosted API above ultimately relies
654
- on the agent's process honoring a returned decision. Here the thing the agent
655
- needs (the credential) answers only to the thing it cannot make (the token).
656
- The tradeoffs are equally plain: you run the daemon and listener yourself,
657
- there is no OS-level sandbox, no compliance certification, and the reference
658
- phone channel is one app, Telegram.
743
+ choosing to honor the returned verdict. They bring hosted infrastructure,
744
+ escalation and team routing, and compliance certifications.
745
+
746
+ The difference is the combination: policy as a hash-attested markdown file in
747
+ your repo; an append-only, hash-chained log you verify locally with one
748
+ command; and an execution boundary where the credential is inert until a
749
+ single-use token is minted at the moment a human decides. Every framework
750
+ primitive and hosted API above relies on the agent's process honoring a
751
+ returned decision. Here the thing the agent needs, the credential, answers only
752
+ to the thing it cannot make, the token. The tradeoffs: you run the daemon and
753
+ listener yourself, there is no OS-level sandbox, no compliance certification,
754
+ and the reference phone channel is one app, Telegram.
659
755
 
660
756
  ## Can't the agent just go around it?
661
757
 
662
758
  **Edit the policy?** An attestation records the SHA-256 of the policy's bytes,
663
759
  and every gated operation refuses `hash-mismatch` when the live file disagrees
664
- with it. An unattested policy refuses too, and attesting is human-only. Under the
665
- harness hook the edit itself is classified `policy.edit` before it happens,
666
- because `APPROVAL.md` is in the built-in protected set no policy can narrow. A
667
- `protected_paths` entry may route a path family to a `policy.edit` sub-class
668
- (`policy.edit.spec`, `policy.edit.harness`, `policy.edit.ci`,
669
- `policy.edit.design`) so each carries its own autonomy, and the routing floor
670
- keeps a built-in path from landing anywhere looser than `policy.edit` itself.
671
-
672
- **Fabricate or rewrite the log?** Each record chains to the previous one's hash,
673
- so an edited or reordered record breaks the chain and `approval log verify` says
674
- so. Appends go through compare-and-append against the head, and projections
675
- (`QUEUE.md`, the SQLite index) rebuild from the log and never write back to it.
676
- Tampering is made evident, which is what an audit trail is for.
760
+ with it. An unattested policy refuses too, and attesting is human-only. Under
761
+ the harness hook the edit itself is classified `policy.edit` before it happens,
762
+ because `APPROVAL.md` is in the built-in protected set no policy can narrow.
763
+
764
+ **Fabricate or rewrite the log?** Each record chains to the previous one's
765
+ hash, so an edited or reordered record breaks the chain and `approval log
766
+ verify` says so. Appends go through compare-and-append against the head, and
767
+ projections (`QUEUE.md`, the SQLite index) rebuild from the log and never write
768
+ back to it.
677
769
 
678
770
  **Mint its own token, or reuse one?** Tokens are minted at one site, inside the
679
- path that records a human decision, and the log stores only the hash. No verb and
680
- no tool returns a token for a grant it did not just record, and a hook grant
681
- mints none at all. A token is single-use: the second spend is refused
682
- `token-consumed`, naming the seq of the `execution.started` that spent it, and no
683
- second record is appended.
771
+ path that records a human decision, and the log stores only the hash. No verb
772
+ and no tool returns a token for a grant it did not just record, and a hook
773
+ grant mints none at all. The second spend is refused `token-consumed`, naming
774
+ the seq of the `execution.started` that spent it.
684
775
 
685
776
  **Call the adapter or the credential directly?** Credentials live in
686
- `.approval/vault.enc` under a passphrase the policy names and never carries, and
687
- an adapter opens the vault only inside a verified token window. There is no
688
- `approval vault get`. An agent that never held a token never reaches a
689
- credential, which is where the hard enforcement in this system lives.
777
+ `.approval/vault.enc` under a passphrase the policy names and never carries,
778
+ and an adapter opens the vault only inside an execution window admitted by the
779
+ attested policy. There is no `approval vault get`. Manual and selected-live
780
+ paths need a grant token. An explicitly opted-in supervised or autonomous path
781
+ needs the passphrase already present in the adapter process environment; it
782
+ cannot use the token-scoped `.approval/env` fallback. This is where the hard
783
+ enforcement lives.
690
784
 
691
785
  **Run the side effect from its own shell?** That is what the PreToolUse hook is
692
786
  for: it classifies the command text, resolves the class, and answers allow or
@@ -697,30 +791,32 @@ stricter class. `bash -c`, `eval`, `sudo`, `xargs` and `node -e` deny as
697
791
  `hook-opaque`.
698
792
 
699
793
  **POST straight at the web queue?** It can. `approval channel web` binds
700
- `127.0.0.1` with the host hard-coded and no `--host` flag, because with no
701
- authentication the loopback interface is the entire access control, and every
702
- decision the page collects is recorded against the human the runtime was started
703
- with: it proves "someone with access to this machine approved" and never "that
704
- specific person approved". The page says so on itself. There is no CSRF token in
705
- v0.1, deliberately: a CSRF token defends a session, and with no session and
706
- nothing to authenticate against, anything that can open a socket to
707
- `127.0.0.1:4680` can POST with or without one. The same-origin check that rejects
708
- a POST naming a non-loopback `Origin` allows one carrying neither header, since
709
- `curl` and older browsers send neither: treat it as a speed bump rather than a
710
- control. A session, or a wider bind address, would need a real anti-CSRF token.
794
+ `127.0.0.1` with no `--host` flag, because with no authentication the loopback
795
+ interface is the entire access control, and every decision the page collects is
796
+ recorded against the human the runtime was started with: it proves "someone
797
+ with access to this machine approved". There is no CSRF token in v0.1,
798
+ deliberately: a CSRF token defends a session, and with no session anything that
799
+ can open a socket to `127.0.0.1:4680` can POST with or without one. The
800
+ same-origin check that rejects a POST naming a non-loopback `Origin` allows one
801
+ carrying neither header, since `curl` sends neither: treat it as a speed bump
802
+ rather than a control. A session, or a wider bind address, would need a real
803
+ anti-CSRF token.
711
804
 
712
805
  **Lie in its self-report?** Self-reported fields never reduce scrutiny (SPEC.md
713
806
  section 11.1, invariant 4). They may raise it, inform display, or feed
714
- retrospective analysis, and they can never downgrade a resolved autonomy level or
715
- shortcut a refusal path. The approval prompt keeps them visually apart: a
716
- COMPUTED block the runtime derived, and a CLAIMED block marked unverified.
717
-
718
- **So what is left?** A fully adversarial agent on a machine it controls. Identity
719
- in v0.1 is config-declared, so anyone who can set that variable and write to the
720
- log is inside the trust boundary, and cryptographic identity is future work
721
- rather than a v0.1 claim. What holds regardless of what the harness believes it
722
- is running: the vault, the adapter boundary, and the single-use token. Keep
723
- `manual` floors on irreversible classes, which SPEC.md section 7 does for you.
807
+ retrospective analysis, and they can never downgrade a resolved autonomy level
808
+ or supply `allow_irreversible`. Only an explicit class rule in the attested
809
+ operator policy can retain nonmanual autonomy for a truthful `reversible:
810
+ false` declaration. The prompt keeps computed facts apart from claimed fields.
811
+
812
+ **So what is left?** A fully adversarial agent on a machine it controls.
813
+ Identity in v0.1 is config-declared, so anyone who can set that variable and
814
+ write to the log is inside the trust boundary; cryptographic identity is future
815
+ work ([docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)).
816
+ What holds regardless of what the harness believes it is running: the vault,
817
+ the adapter boundary, and the single-use token. Section 7 keeps irreversible
818
+ classes at `manual` unless the attested policy explicitly opts a class into the
819
+ exception.
724
820
 
725
821
  ## Running the checks
726
822
 
@@ -731,34 +827,54 @@ npm run check:tier -- <path> # classify the given paths and print the tier
731
827
  approval doctor # the other check: this machine, not the code
732
828
  ```
733
829
 
734
- `approval doctor` prints **27 rows** and a tally, in the order their failures
830
+ `approval doctor` prints **33 rows** and a tally, in the order their failures
735
831
  cascade: build freshness, identity, attestation, the log chain, the channels
736
- (`telegram`, `web-port`), the payload store, audit sampling, envelope integrity,
737
- the vault, the environment source map, then the rows that ask git and the harness
738
- what happened (`log-drift`, `reconciliation`, `harness-hook-outcomes`,
739
- `harness-hook-wiring`, `keychain-scope`, `log-advance-cadence`, `dark-sessions`,
740
- `verified-snapshot`, `read-proof`, `main-behind-origin`,
741
- `harness-version-unverified`, `live-draw`, `values-block`, `checkpoint`,
742
- `gate-organs`, `sealed-keys`).
743
-
744
- **17 of the 27 report `not applicable` in a fresh directory**, and each names the
745
- absence it skipped on rather than passing quietly: `telegram` (no bot variables),
746
- `envelope-integrity` (no task folder), `vault` (no vault file), `environment` (no
747
- `.approval/env`), `read-proof` (no `daemon` block), `live-draw` (no
748
- `supervised-live` class), `checkpoint` (no `audit.checkpoint_keys`),
749
- `harness-hook-outcomes`, `harness-hook-wiring`, `harness-version-unverified` and
832
+ (`telegram`, `web-port`), the payload store, audit sampling, envelope
833
+ integrity, the vault, the environment source map, then the rows that ask git
834
+ and the harness what happened (`log-drift`, `reconciliation`,
835
+ `harness-hook-outcomes`, `harness-hook-wiring`, `keychain-scope`,
836
+ `log-advance-cadence`, `dark-sessions`, `verified-snapshot`, `read-proof`,
837
+ `main-behind-origin`, `attested-policy-on-main`,
838
+ `harness-version-unverified`, `live-draw`,
839
+ `values-block`, `checkpoint`, `gate-organs`, `sealed-keys`,
840
+ `codex-hook-wiring`, `autonomy-alias`, `pending-sign-off`,
841
+ `codex-auto-reviewer`). Each failure
842
+ carries a `fix:` line you run yourself. Doctor appends nothing, sends nothing
843
+ and repairs nothing, and no credential value appears in its output. Three
844
+ of the 33 lines from a fresh directory, plus the tally:
845
+
846
+ ```
847
+ ✓ identity APPROVAL_HUMAN=human:alice (config-declared: the trust boundary is this machine, not cryptography)
848
+ ✓ log /your/project/.approval/log/events.jsonl verifies: 1 record(s), head seq 1 0f3c4a19187a…
849
+ ✗ audit-sampling disabled (secret-env-unnamed): APPROVAL.md sets audit.supervised_sample_rate to 0.1 but names no audit.sampling_secret_env. …
850
+ fix: approval policy attest --as human:<id> — after setting audit.supervised_sample_rate and audit.sampling_secret_env in the policy; then export the named variable where the daemon runs
851
+ 12 ok · 20 not applicable · 1 failed
852
+ ```
853
+
854
+ That one failure is expected on the scaffolded policy: it samples supervised
855
+ actions for audit, sampling needs an operator-held secret the policy only
856
+ names, and a control that looks on while the party under oversight could steer
857
+ it is worse than one that is visibly off. Name the secret when you want
858
+ sampling, or delete the `audit` block if one person's gate has no use for it.
859
+
860
+ **20 of the 33 report `not applicable` in a fresh directory**, and each names
861
+ the absence it skipped on: `telegram` (no bot variables), `envelope-integrity`
862
+ (no task folder), `vault` (no vault file), `environment` (no `.approval/env`),
863
+ `read-proof` (no `daemon` block), `live-draw` (no `supervised-live` class),
864
+ `checkpoint` (no `audit.checkpoint_keys`), `sender-mapping` (no approver
865
+ declares a `senders` block, so decisions carry the identity the deciding
866
+ process was launched with), `harness-hook-outcomes`,
867
+ `harness-hook-wiring`, `codex-hook-wiring`, `harness-version-unverified` and
750
868
  `gate-organs` (no harness settings file), `verified-snapshot` (no daemon has
751
- run), and `log-drift`, `log-advance-cadence`, `dark-sessions`,
752
- `main-behind-origin` and `sealed-keys` (not a git checkout). `sealed-keys` is
753
- the one that asks git what it TRACKS: `.approval/payloads/` is tracked on
754
- purpose, so `.approval/` is a directory people `git add` from, and a
755
- sealed-delivery private key swept in by one of those adds opens that action's
756
- token for everyone holding the log. `gate-organs` is informational
757
- wherever it lands: it lists the harness files whose current bytes carry no
758
- `approval policy attest --organ` record, and it never moves the exit code, since
759
- the enforcement for one of those is the protected-path guard in CI. Doctor
760
- appends nothing, sends nothing and repairs nothing, and no credential value
761
- appears in its output.
869
+ run), and
870
+ `log-drift`, `log-advance-cadence`, `dark-sessions`, `main-behind-origin`,
871
+ `attested-policy-on-main` and
872
+ `sealed-keys` (not a git checkout). `sealed-keys` asks git what it tracks:
873
+ `.approval/payloads/` is tracked on purpose, and a sealed-delivery private key
874
+ swept in by a `git add` of that directory would open that action's token for
875
+ everyone holding the log. `gate-organs` is informational wherever it lands: it
876
+ lists the harness files whose current bytes carry no `approval policy attest
877
+ --organ` record, and never moves the exit code.
762
878
 
763
879
  Checks come in three tiers.
764
880
 
@@ -771,68 +887,34 @@ Checks come in three tiers.
771
887
  A denylist forces the full tier regardless of file extension: `APPROVAL.md`,
772
888
  `CLAUDE.md`, `.claude/**`, `SPEC.md`, `schema/**`, `**/fixtures/**`,
773
889
  `backlog/**`, `scripts/**`, `.github/**`, the packaging files, and `cli.js`.
774
-
775
- `backlog/**` sits on both that denylist and the records list, which is what
776
- makes the records tier all-or-nothing: a task file mixed with any other path
777
- takes the full tier. Task files are markdown by extension and behavior by
778
- effect, since their acceptance criteria are instructions to future agents. That
779
- earns them every check which can observe a task file, and the records tier is
780
- exactly those; it does not earn them a matrix of ~1800 tests on two Node
781
- majors, none of which reads one. `MILESTONES.md` rides along because the
782
- milestones guard checks the two against each other.
783
-
784
- Classification is computed from the changed paths by
785
- `scripts/classify-tier.mjs`, never asserted by the author of the change. Every
786
- merge to `main` runs the full suite unconditionally, and anything ambiguous, an
787
- empty path set included, resolves to full.
890
+ `backlog/**` sits on both that denylist and the records list, so a task file
891
+ mixed with any other path takes the full tier. Classification is computed from
892
+ the changed paths by `scripts/classify-tier.mjs`, never asserted by the author
893
+ of the change, and every merge to `main` runs the full suite.
788
894
 
789
895
  ### Before the push: `npm run ci:local`
790
896
 
791
- The merge queue is serial, so every red run there costs a slot, a re-merge and
792
- another wait. `npm run ci:local` (APRV-275) is where that red gets found
793
- instead. It asks the same classifier the workflow's `classify` job asks, by
794
- spawning the same command with the same arguments, and then runs the jobs
795
- `.github/workflows/ci.yml` declares for the tier that comes back: the docs
796
- guard for light, the record-reading tests for records, the three shards plus
797
- lint for full, and the protected-path grant cross-check on every tier whenever
798
- a merge base is computable. `--base <ref>` picks the base (default
799
- `origin/main`, three-dot, as CI classifies), `--working-tree` and explicit paths
800
- are the other two path sources, `--dry-run` prints the plan and runs nothing,
801
- `--json` prints it as data, and `--parallel` runs the tier's jobs concurrently
802
- the way the matrix does.
803
-
804
- `npm run check:changed` predates it and answers a different question: it
805
- classifies the working tree and runs the tier in its own shape, which for full
806
- is `npm test`, `npm run lint` and `npm run typecheck`. Use it while working, and
807
- `ci:local` before pushing, when the question is what the workflow will say.
808
-
809
- What it cannot reproduce it says, rather than passing over. The Node 20 floor
810
- legs need Node 20, and this host runs whatever it runs. CI's runner is
811
- `ubuntu-latest`, so on any other platform the report names the suites whose
812
- meaning differs here, the temp root's shape and the symlink cases among them. A
813
- cross-check with no reachable merge base, or with the records branches
814
- unfetched, is reported unresolved and kept out of the verdict. A red step exits
815
- non-zero and names the files that failed. Nothing in CI consults any of this: a
816
- green run locally is a prediction, and the workflow remains the verdict.
817
-
818
- A full-tier CI job compiles once. It builds, then runs `node
819
- scripts/run-tests.mjs` over what it built, because `npm test` and `npm run
820
- typecheck` would each recompile the same tree and neither pass can fail where
821
- the build passed. `npm test` keeps its build-then-run shape for anyone running
822
- it by hand. `scripts/run-tests.mjs --shard <k>/<n>` takes shard `k` of the
823
- sorted file list, where the file at position `i` belongs to shard `(i mod n) +
824
- 1`, so the shards of a matrix are a partition of the suite: every file in
825
- exactly one shard, and the matrix covers all of them. An out-of-range index, an
826
- empty shard, and `--shard` combined with `--only` are refused rather than run.
827
- The Node 20 floor moved to the merge queue and to pushes to `main` because the
828
- queue candidate is what stands between a change and the branch, and a pull
829
- request now gets its verdict from the shards alone. The floor leg is sharded
830
- three ways too, so it proves the same whole suite in roughly a third of the
831
- wall clock it took as one run.
897
+ The merge queue is serial, so every red run there costs a slot and another
898
+ wait. `npm run ci:local` asks the same classifier the workflow asks and runs
899
+ the jobs `.github/workflows/ci.yml` declares for that tier: the docs guard for
900
+ light, the record-reading tests for records, the three shards plus lint for
901
+ full, and the protected-path grant cross-check on every tier when a merge base
902
+ is computable. `--base <ref>` picks the base, `--working-tree` and explicit
903
+ paths are the other path sources, `--dry-run` prints the plan, `--json` prints
904
+ it as data, and `--parallel` runs the tier's jobs concurrently. What it cannot
905
+ reproduce it says: the Node 20 legs need Node 20, and CI's runner is
906
+ `ubuntu-latest`. A green run locally is a prediction; the workflow is the
907
+ verdict.
908
+
909
+ `npm run check:changed` answers a different question: it classifies the
910
+ working tree and runs the tier in its own shape, which for full is `npm test`,
911
+ `npm run lint` and `npm run typecheck`. Use it while working, and `ci:local`
912
+ before pushing. `scripts/run-tests.mjs --shard <k>/<n>` takes shard `k` of the
913
+ sorted file list, so the shards of a matrix partition the suite.
832
914
 
833
915
  ## Exit codes
834
916
 
835
- An agent branches on the exit code before it ever reads stdout, so these numbers
917
+ An agent branches on the exit code before it reads stdout, so these numbers
836
918
  are frozen. Adding one is a spec change; changing a meaning is breaking.
837
919
 
838
920
  | Code | Meaning |
@@ -845,61 +927,49 @@ are frozen. Adding one is a spec change; changing a meaning is breaking.
845
927
  | 5 | no valid execution token (approval run only) |
846
928
  | 6 | timeout (approval wait only) |
847
929
 
848
- Code 1 and code 4 are kept apart deliberately. "I could not read the file" and
849
- "the file has been tampered with" are different facts about the world, and
850
- conflating them either cries wolf over a permission bit or lets real tampering
851
- read as a filesystem hiccup. Code 3, a torn tail, is the signature of a crashed
852
- write rather than of tampering, and nothing is ever repaired automatically:
853
- truncating a torn line is a human decision. A gate refusal is exit 1 and never 2,
854
- since the command was well-formed and the answer is no, so branch on
855
- `error.code` under `--json` rather than retrying with different flags.
930
+ Code 1 and code 4 are kept apart deliberately: "I could not read the file" and
931
+ "the file has been tampered with" are different facts, and conflating them
932
+ either cries wolf over a permission bit or lets tampering read as a filesystem
933
+ hiccup. Code 3, a torn tail, is the signature of a crashed write, and nothing
934
+ is repaired automatically: truncating a torn line is a human decision. A gate
935
+ refusal is exit 1 and never 2, since the command was well-formed and the answer
936
+ is no; branch on `error.code` under `--json`.
856
937
 
857
938
  ## Where to look next
858
939
 
859
940
  [SPEC.md](SPEC.md) is the source of truth for every design decision, and this
860
941
  README defers to it wherever the two could be read differently.
861
- [CLAUDE.md](CLAUDE.md) describes how this repository builds itself, including
862
- where it starts running behind its own gate.
942
+ [CLAUDE.md](CLAUDE.md) describes how this repository builds itself behind its
943
+ own gate; the 0.1.0 release was published, tagged and pushed through three
944
+ grants from a phone.
863
945
 
864
- Every command carries its own instructions, so this README shows no verb
865
- inventory. `approval --help` lists them grouped by what they are for. `approval
866
- <command> --help` gives one command's flags, refusal codes, and JSON shape, and
867
- `--help --long` appends that verb's reasoning from
868
- [docs/cli-reference.md](docs/cli-reference.md). `approval instructions` is the
869
- agent-facing guide, and `--schemas` prints the verb registry as JSON.
946
+ Every command carries its own instructions. `approval --help` lists them
947
+ grouped by purpose, `approval <command> --help` gives one command's flags,
948
+ refusal codes and JSON shape, and `--help --long` appends that verb's
949
+ reasoning from [docs/cli-reference.md](docs/cli-reference.md). `approval
950
+ instructions` is the agent-facing guide, and `--schemas` prints the verb
951
+ registry as JSON.
870
952
 
871
953
  Every external adapter, harness, updater or gateway this project has weighed
872
- for integration has an entry in
873
- [docs/integrations-considered.md](docs/integrations-considered.md): what it
874
- exposes, how it fits, the verdict, and the next step, so the question is
875
- answered once.
876
-
877
- One of those entries has a runbook of its own.
954
+ has an entry in
955
+ [docs/integrations-considered.md](docs/integrations-considered.md).
878
956
  [examples/grok-bot-connector/runbook.md](examples/grok-bot-connector/runbook.md)
879
- puts a Grok Bot agent on the far end of `approval mcp serve --http --guest`,
880
- behind a tunnel that is itself gated, and rehearses both halves of the story: the
881
- agent asking for a branch push and an email and a human deciding them on a phone,
882
- then the agent skipping the gate entirely. What holds when it does is the point.
883
- Credentials answer only to single-use tokens, so the send it was never granted
884
- stays impossible, and `approval coverage` reports every observed effect with its
885
- evidence seq or `none`.
886
-
887
- Designs that are proposed and not yet built live under `docs/proposals/`.
888
- [docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)
889
- is the longest of them: what a grant in this log can and cannot prove to a
890
- service that does not trust the operator, and what an optional stronger tier
891
- would have to be. Identity in v0.1 is config-declared, so the honest ceiling
892
- today is "a party with write access to this log recorded a decision", and the
893
- proposal works through device-bound keys, WebAuthn on a separately controlled
894
- surface, per-decision signatures over the existing checkpoint machinery, and
895
- third-party witnesses, with the phasing, the receipt format, and the negative
896
- tests each would need. Nothing in it is implemented, and nothing in it amends
897
- SPEC.md. Two shorter ones,
957
+ puts a Grok Bot agent on the far end of `approval mcp serve --http --guest`
958
+ and rehearses both halves of the story: the agent asking for a branch push and
959
+ an email and a human deciding on a phone, then the agent skipping the gate and
960
+ finding the credential inert.
961
+
962
+ Designs proposed and not yet built live under `docs/proposals/`.
898
963
  [docs/proposals/solo-dev-quickstart.md](docs/proposals/solo-dev-quickstart.md)
899
- and [docs/proposals/no-daemon-mode.md](docs/proposals/no-daemon-mode.md),
900
- design the path for one person gating their own app: a three-question setup,
901
- one `guard` verb, and a runtime that lives inside the waiting command instead
902
- of a daemon.
964
+ and [docs/proposals/no-daemon-mode.md](docs/proposals/no-daemon-mode.md) are
965
+ the next step for the path at the top of this page: a three-question
966
+ `approval quickstart`, one `approval guard -- <command>` verb that replaces
967
+ the register, request, wait, run quartet, and a runtime that lives inside the
968
+ waiting command instead of a daemon.
969
+ [docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)
970
+ works through what a grant in this log can and cannot prove to a service that
971
+ does not trust the operator, and what a stronger identity tier would have to
972
+ be.
903
973
 
904
974
  ## License and governance
905
975