approval-md 0.0.1 → 0.2.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 (722) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +940 -4
  4. package/SPEC.md +476 -34
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.d.ts +426 -0
  7. package/dist/src/adapters/agentmail.js +1200 -0
  8. package/dist/src/adapters/agentmail.js.map +1 -0
  9. package/dist/src/adapters/conformance.d.ts +149 -0
  10. package/dist/src/adapters/conformance.js +461 -0
  11. package/dist/src/adapters/conformance.js.map +1 -0
  12. package/dist/src/adapters/contract.d.ts +628 -0
  13. package/dist/src/adapters/contract.js +1035 -0
  14. package/dist/src/adapters/contract.js.map +1 -0
  15. package/dist/src/adapters/email.d.ts +324 -0
  16. package/dist/src/adapters/email.js +749 -0
  17. package/dist/src/adapters/email.js.map +1 -0
  18. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  19. package/dist/src/adapters/env-passphrase.js +132 -0
  20. package/dist/src/adapters/env-passphrase.js.map +1 -0
  21. package/dist/src/adapters/public.d.ts +11 -0
  22. package/dist/src/adapters/public.js +11 -0
  23. package/dist/src/adapters/public.js.map +1 -0
  24. package/dist/src/adapters/registry.d.ts +59 -0
  25. package/dist/src/adapters/registry.js +77 -0
  26. package/dist/src/adapters/registry.js.map +1 -0
  27. package/dist/src/adapters/smtp.d.ts +213 -0
  28. package/dist/src/adapters/smtp.js +499 -0
  29. package/dist/src/adapters/smtp.js.map +1 -0
  30. package/dist/src/adapters/vault-provider.d.ts +114 -0
  31. package/dist/src/adapters/vault-provider.js +161 -0
  32. package/dist/src/adapters/vault-provider.js.map +1 -0
  33. package/dist/src/adapters/zzz.d.ts +66 -0
  34. package/dist/src/adapters/zzz.js +299 -0
  35. package/dist/src/adapters/zzz.js.map +1 -0
  36. package/dist/src/channels/batch.d.ts +109 -0
  37. package/dist/src/channels/batch.js +121 -0
  38. package/dist/src/channels/batch.js.map +1 -0
  39. package/dist/src/channels/cli.d.ts +193 -0
  40. package/dist/src/channels/cli.js +468 -0
  41. package/dist/src/channels/cli.js.map +1 -0
  42. package/dist/src/channels/conformance.d.ts +92 -0
  43. package/dist/src/channels/conformance.js +445 -0
  44. package/dist/src/channels/conformance.js.map +1 -0
  45. package/dist/src/channels/contract.d.ts +623 -0
  46. package/dist/src/channels/contract.js +494 -0
  47. package/dist/src/channels/contract.js.map +1 -0
  48. package/dist/src/channels/payload-view.d.ts +35 -0
  49. package/dist/src/channels/payload-view.js +43 -0
  50. package/dist/src/channels/payload-view.js.map +1 -0
  51. package/dist/src/channels/render-queue.d.ts +149 -0
  52. package/dist/src/channels/render-queue.js +564 -0
  53. package/dist/src/channels/render-queue.js.map +1 -0
  54. package/dist/src/channels/tagging.d.ts +196 -0
  55. package/dist/src/channels/tagging.js +723 -0
  56. package/dist/src/channels/tagging.js.map +1 -0
  57. package/dist/src/channels/telegram.d.ts +1832 -0
  58. package/dist/src/channels/telegram.js +3190 -0
  59. package/dist/src/channels/telegram.js.map +1 -0
  60. package/dist/src/channels/web.d.ts +341 -0
  61. package/dist/src/channels/web.js +903 -0
  62. package/dist/src/channels/web.js.map +1 -0
  63. package/dist/src/cli/adapter.d.ts +90 -0
  64. package/dist/src/cli/adapter.js +288 -0
  65. package/dist/src/cli/adapter.js.map +1 -0
  66. package/dist/src/cli/amend.d.ts +59 -0
  67. package/dist/src/cli/amend.js +2171 -0
  68. package/dist/src/cli/amend.js.map +1 -0
  69. package/dist/src/cli/args.d.ts +43 -0
  70. package/dist/src/cli/args.js +86 -0
  71. package/dist/src/cli/args.js.map +1 -0
  72. package/dist/src/cli/attest.d.ts +41 -0
  73. package/dist/src/cli/attest.js +307 -0
  74. package/dist/src/cli/attest.js.map +1 -0
  75. package/dist/src/cli/audit-card.d.ts +62 -0
  76. package/dist/src/cli/audit-card.js +201 -0
  77. package/dist/src/cli/audit-card.js.map +1 -0
  78. package/dist/src/cli/audit.d.ts +59 -0
  79. package/dist/src/cli/audit.js +460 -0
  80. package/dist/src/cli/audit.js.map +1 -0
  81. package/dist/src/cli/channel-telegram.d.ts +806 -0
  82. package/dist/src/cli/channel-telegram.js +2063 -0
  83. package/dist/src/cli/channel-telegram.js.map +1 -0
  84. package/dist/src/cli/channel-web.d.ts +131 -0
  85. package/dist/src/cli/channel-web.js +357 -0
  86. package/dist/src/cli/channel-web.js.map +1 -0
  87. package/dist/src/cli/channel.d.ts +71 -0
  88. package/dist/src/cli/channel.js +438 -0
  89. package/dist/src/cli/channel.js.map +1 -0
  90. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  91. package/dist/src/cli/checkpoint-tap.js +238 -0
  92. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  93. package/dist/src/cli/codex.d.ts +2 -0
  94. package/dist/src/cli/codex.js +172 -0
  95. package/dist/src/cli/codex.js.map +1 -0
  96. package/dist/src/cli/coverage.d.ts +61 -0
  97. package/dist/src/cli/coverage.js +343 -0
  98. package/dist/src/cli/coverage.js.map +1 -0
  99. package/dist/src/cli/daemon.d.ts +120 -0
  100. package/dist/src/cli/daemon.js +631 -0
  101. package/dist/src/cli/daemon.js.map +1 -0
  102. package/dist/src/cli/doctor.d.ts +129 -0
  103. package/dist/src/cli/doctor.js +2762 -0
  104. package/dist/src/cli/doctor.js.map +1 -0
  105. package/dist/src/cli/env.d.ts +65 -0
  106. package/dist/src/cli/env.js +302 -0
  107. package/dist/src/cli/env.js.map +1 -0
  108. package/dist/src/cli/execute.d.ts +202 -0
  109. package/dist/src/cli/execute.js +1682 -0
  110. package/dist/src/cli/execute.js.map +1 -0
  111. package/dist/src/cli/exit-codes.d.ts +73 -0
  112. package/dist/src/cli/exit-codes.js +82 -0
  113. package/dist/src/cli/exit-codes.js.map +1 -0
  114. package/dist/src/cli/feedback.d.ts +60 -0
  115. package/dist/src/cli/feedback.js +205 -0
  116. package/dist/src/cli/feedback.js.map +1 -0
  117. package/dist/src/cli/gate-window.d.ts +40 -0
  118. package/dist/src/cli/gate-window.js +294 -0
  119. package/dist/src/cli/gate-window.js.map +1 -0
  120. package/dist/src/cli/gate.d.ts +68 -0
  121. package/dist/src/cli/gate.js +557 -0
  122. package/dist/src/cli/gate.js.map +1 -0
  123. package/dist/src/cli/git-scope.d.ts +190 -0
  124. package/dist/src/cli/git-scope.js +295 -0
  125. package/dist/src/cli/git-scope.js.map +1 -0
  126. package/dist/src/cli/gloss-attach.d.ts +85 -0
  127. package/dist/src/cli/gloss-attach.js +107 -0
  128. package/dist/src/cli/gloss-attach.js.map +1 -0
  129. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  130. package/dist/src/cli/gloss-codex-child.js +149 -0
  131. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  132. package/dist/src/cli/gloss-codex.d.ts +24 -0
  133. package/dist/src/cli/gloss-codex.js +255 -0
  134. package/dist/src/cli/gloss-codex.js.map +1 -0
  135. package/dist/src/cli/gloss-options.d.ts +42 -0
  136. package/dist/src/cli/gloss-options.js +79 -0
  137. package/dist/src/cli/gloss-options.js.map +1 -0
  138. package/dist/src/cli/gloss.d.ts +265 -0
  139. package/dist/src/cli/gloss.js +362 -0
  140. package/dist/src/cli/gloss.js.map +1 -0
  141. package/dist/src/cli/help.d.ts +103 -0
  142. package/dist/src/cli/help.js +2339 -0
  143. package/dist/src/cli/help.js.map +1 -0
  144. package/dist/src/cli/hook-codex.d.ts +78 -0
  145. package/dist/src/cli/hook-codex.js +167 -0
  146. package/dist/src/cli/hook-codex.js.map +1 -0
  147. package/dist/src/cli/hook.d.ts +331 -0
  148. package/dist/src/cli/hook.js +2849 -0
  149. package/dist/src/cli/hook.js.map +1 -0
  150. package/dist/src/cli/import.d.ts +35 -0
  151. package/dist/src/cli/import.js +175 -0
  152. package/dist/src/cli/import.js.map +1 -0
  153. package/dist/src/cli/init.d.ts +84 -0
  154. package/dist/src/cli/init.js +336 -0
  155. package/dist/src/cli/init.js.map +1 -0
  156. package/dist/src/cli/instructions.d.ts +23 -0
  157. package/dist/src/cli/instructions.js +262 -0
  158. package/dist/src/cli/instructions.js.map +1 -0
  159. package/dist/src/cli/journal.d.ts +41 -0
  160. package/dist/src/cli/journal.js +238 -0
  161. package/dist/src/cli/journal.js.map +1 -0
  162. package/dist/src/cli/log-advance.d.ts +287 -0
  163. package/dist/src/cli/log-advance.js +840 -0
  164. package/dist/src/cli/log-advance.js.map +1 -0
  165. package/dist/src/cli/log-anchor.d.ts +176 -0
  166. package/dist/src/cli/log-anchor.js +387 -0
  167. package/dist/src/cli/log-anchor.js.map +1 -0
  168. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  169. package/dist/src/cli/log-checkpoint.js +128 -0
  170. package/dist/src/cli/log-checkpoint.js.map +1 -0
  171. package/dist/src/cli/log-sync.d.ts +243 -0
  172. package/dist/src/cli/log-sync.js +849 -0
  173. package/dist/src/cli/log-sync.js.map +1 -0
  174. package/dist/src/cli/log-verbs.d.ts +16 -0
  175. package/dist/src/cli/log-verbs.js +360 -0
  176. package/dist/src/cli/log-verbs.js.map +1 -0
  177. package/dist/src/cli/long-help.d.ts +70 -0
  178. package/dist/src/cli/long-help.js +148 -0
  179. package/dist/src/cli/long-help.js.map +1 -0
  180. package/dist/src/cli/main.d.ts +77 -0
  181. package/dist/src/cli/main.js +1206 -0
  182. package/dist/src/cli/main.js.map +1 -0
  183. package/dist/src/cli/mcp.d.ts +52 -0
  184. package/dist/src/cli/mcp.js +306 -0
  185. package/dist/src/cli/mcp.js.map +1 -0
  186. package/dist/src/cli/paths.d.ts +56 -0
  187. package/dist/src/cli/paths.js +79 -0
  188. package/dist/src/cli/paths.js.map +1 -0
  189. package/dist/src/cli/payload.d.ts +58 -0
  190. package/dist/src/cli/payload.js +253 -0
  191. package/dist/src/cli/payload.js.map +1 -0
  192. package/dist/src/cli/policy.d.ts +43 -0
  193. package/dist/src/cli/policy.js +229 -0
  194. package/dist/src/cli/policy.js.map +1 -0
  195. package/dist/src/cli/preflight.d.ts +363 -0
  196. package/dist/src/cli/preflight.js +1175 -0
  197. package/dist/src/cli/preflight.js.map +1 -0
  198. package/dist/src/cli/progress.d.ts +78 -0
  199. package/dist/src/cli/progress.js +112 -0
  200. package/dist/src/cli/progress.js.map +1 -0
  201. package/dist/src/cli/prompt.d.ts +209 -0
  202. package/dist/src/cli/prompt.js +312 -0
  203. package/dist/src/cli/prompt.js.map +1 -0
  204. package/dist/src/cli/quickstart.d.ts +46 -0
  205. package/dist/src/cli/quickstart.js +297 -0
  206. package/dist/src/cli/quickstart.js.map +1 -0
  207. package/dist/src/cli/records.d.ts +34 -0
  208. package/dist/src/cli/records.js +66 -0
  209. package/dist/src/cli/records.js.map +1 -0
  210. package/dist/src/cli/render.d.ts +22 -0
  211. package/dist/src/cli/render.js +132 -0
  212. package/dist/src/cli/render.js.map +1 -0
  213. package/dist/src/cli/sandbox.d.ts +51 -0
  214. package/dist/src/cli/sandbox.js +150 -0
  215. package/dist/src/cli/sandbox.js.map +1 -0
  216. package/dist/src/cli/scaffold.d.ts +79 -0
  217. package/dist/src/cli/scaffold.js +137 -0
  218. package/dist/src/cli/scaffold.js.map +1 -0
  219. package/dist/src/cli/setup-adapter.d.ts +137 -0
  220. package/dist/src/cli/setup-adapter.js +509 -0
  221. package/dist/src/cli/setup-adapter.js.map +1 -0
  222. package/dist/src/cli/setup-channel.d.ts +117 -0
  223. package/dist/src/cli/setup-channel.js +635 -0
  224. package/dist/src/cli/setup-channel.js.map +1 -0
  225. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  226. package/dist/src/cli/setup-checkpoint.js +196 -0
  227. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  228. package/dist/src/cli/setup-common.d.ts +275 -0
  229. package/dist/src/cli/setup-common.js +376 -0
  230. package/dist/src/cli/setup-common.js.map +1 -0
  231. package/dist/src/cli/setup-flow.d.ts +287 -0
  232. package/dist/src/cli/setup-flow.js +476 -0
  233. package/dist/src/cli/setup-flow.js.map +1 -0
  234. package/dist/src/cli/setup-service.d.ts +96 -0
  235. package/dist/src/cli/setup-service.js +308 -0
  236. package/dist/src/cli/setup-service.js.map +1 -0
  237. package/dist/src/cli/setup.d.ts +202 -0
  238. package/dist/src/cli/setup.js +473 -0
  239. package/dist/src/cli/setup.js.map +1 -0
  240. package/dist/src/cli/style.d.ts +320 -0
  241. package/dist/src/cli/style.js +469 -0
  242. package/dist/src/cli/style.js.map +1 -0
  243. package/dist/src/cli/token.d.ts +39 -0
  244. package/dist/src/cli/token.js +274 -0
  245. package/dist/src/cli/token.js.map +1 -0
  246. package/dist/src/cli/up.d.ts +155 -0
  247. package/dist/src/cli/up.js +849 -0
  248. package/dist/src/cli/up.js.map +1 -0
  249. package/dist/src/cli/usage.d.ts +37 -0
  250. package/dist/src/cli/usage.js +91 -0
  251. package/dist/src/cli/usage.js.map +1 -0
  252. package/dist/src/cli/values.d.ts +40 -0
  253. package/dist/src/cli/values.js +189 -0
  254. package/dist/src/cli/values.js.map +1 -0
  255. package/dist/src/cli/vault.d.ts +59 -0
  256. package/dist/src/cli/vault.js +362 -0
  257. package/dist/src/cli/vault.js.map +1 -0
  258. package/dist/src/cli/verb-registry.d.ts +76 -0
  259. package/dist/src/cli/verb-registry.js +2341 -0
  260. package/dist/src/cli/verb-registry.js.map +1 -0
  261. package/dist/src/cli/wordmark.d.ts +31 -0
  262. package/dist/src/cli/wordmark.js +52 -0
  263. package/dist/src/cli/wordmark.js.map +1 -0
  264. package/dist/src/codex/doctor.d.ts +13 -0
  265. package/dist/src/codex/doctor.js +41 -0
  266. package/dist/src/codex/doctor.js.map +1 -0
  267. package/dist/src/codex/manifest.d.ts +49 -0
  268. package/dist/src/codex/manifest.js +103 -0
  269. package/dist/src/codex/manifest.js.map +1 -0
  270. package/dist/src/codex/templates.d.ts +41 -0
  271. package/dist/src/codex/templates.js +319 -0
  272. package/dist/src/codex/templates.js.map +1 -0
  273. package/dist/src/codex/trust.d.ts +19 -0
  274. package/dist/src/codex/trust.js +183 -0
  275. package/dist/src/codex/trust.js.map +1 -0
  276. package/dist/src/codex/workspace-plan.d.ts +131 -0
  277. package/dist/src/codex/workspace-plan.js +561 -0
  278. package/dist/src/codex/workspace-plan.js.map +1 -0
  279. package/dist/src/core/actor.d.ts +2 -0
  280. package/dist/src/core/actor.js +5 -0
  281. package/dist/src/core/actor.js.map +1 -0
  282. package/dist/src/core/advance-cycle.d.ts +170 -0
  283. package/dist/src/core/advance-cycle.js +200 -0
  284. package/dist/src/core/advance-cycle.js.map +1 -0
  285. package/dist/src/core/agents-md.d.ts +276 -0
  286. package/dist/src/core/agents-md.js +747 -0
  287. package/dist/src/core/agents-md.js.map +1 -0
  288. package/dist/src/core/apply-patch.d.ts +49 -0
  289. package/dist/src/core/apply-patch.js +266 -0
  290. package/dist/src/core/apply-patch.js.map +1 -0
  291. package/dist/src/core/attest.d.ts +420 -0
  292. package/dist/src/core/attest.js +589 -0
  293. package/dist/src/core/attest.js.map +1 -0
  294. package/dist/src/core/audit.d.ts +492 -0
  295. package/dist/src/core/audit.js +882 -0
  296. package/dist/src/core/audit.js.map +1 -0
  297. package/dist/src/core/budgets.d.ts +238 -0
  298. package/dist/src/core/budgets.js +449 -0
  299. package/dist/src/core/budgets.js.map +1 -0
  300. package/dist/src/core/checkpoint.d.ts +500 -0
  301. package/dist/src/core/checkpoint.js +738 -0
  302. package/dist/src/core/checkpoint.js.map +1 -0
  303. package/dist/src/core/child-env.d.ts +88 -0
  304. package/dist/src/core/child-env.js +86 -0
  305. package/dist/src/core/child-env.js.map +1 -0
  306. package/dist/src/core/clock.d.ts +52 -0
  307. package/dist/src/core/clock.js +43 -0
  308. package/dist/src/core/clock.js.map +1 -0
  309. package/dist/src/core/command-class.d.ts +543 -0
  310. package/dist/src/core/command-class.js +2356 -0
  311. package/dist/src/core/command-class.js.map +1 -0
  312. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  313. package/dist/src/core/coverage-sources/adapter.js +71 -0
  314. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  315. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  316. package/dist/src/core/coverage-sources/gh.js +136 -0
  317. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  318. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  319. package/dist/src/core/coverage-sources/git.js +269 -0
  320. package/dist/src/core/coverage-sources/git.js.map +1 -0
  321. package/dist/src/core/coverage.d.ts +217 -0
  322. package/dist/src/core/coverage.js +337 -0
  323. package/dist/src/core/coverage.js.map +1 -0
  324. package/dist/src/core/credential-spec.d.ts +72 -0
  325. package/dist/src/core/credential-spec.js +23 -0
  326. package/dist/src/core/credential-spec.js.map +1 -0
  327. package/dist/src/core/dark-session.d.ts +331 -0
  328. package/dist/src/core/dark-session.js +714 -0
  329. package/dist/src/core/dark-session.js.map +1 -0
  330. package/dist/src/core/decision-refusal.d.ts +185 -0
  331. package/dist/src/core/decision-refusal.js +265 -0
  332. package/dist/src/core/decision-refusal.js.map +1 -0
  333. package/dist/src/core/env-file.d.ts +450 -0
  334. package/dist/src/core/env-file.js +837 -0
  335. package/dist/src/core/env-file.js.map +1 -0
  336. package/dist/src/core/execute.d.ts +858 -0
  337. package/dist/src/core/execute.js +1271 -0
  338. package/dist/src/core/execute.js.map +1 -0
  339. package/dist/src/core/frontmatter.d.ts +78 -0
  340. package/dist/src/core/frontmatter.js +100 -0
  341. package/dist/src/core/frontmatter.js.map +1 -0
  342. package/dist/src/core/gate-window.d.ts +312 -0
  343. package/dist/src/core/gate-window.js +506 -0
  344. package/dist/src/core/gate-window.js.map +1 -0
  345. package/dist/src/core/gate.d.ts +1364 -0
  346. package/dist/src/core/gate.js +3002 -0
  347. package/dist/src/core/gate.js.map +1 -0
  348. package/dist/src/core/git-run.d.ts +73 -0
  349. package/dist/src/core/git-run.js +93 -0
  350. package/dist/src/core/git-run.js.map +1 -0
  351. package/dist/src/core/harness-version.d.ts +157 -0
  352. package/dist/src/core/harness-version.js +211 -0
  353. package/dist/src/core/harness-version.js.map +1 -0
  354. package/dist/src/core/harness-wait.d.ts +55 -0
  355. package/dist/src/core/harness-wait.js +58 -0
  356. package/dist/src/core/harness-wait.js.map +1 -0
  357. package/dist/src/core/head-retry.d.ts +107 -0
  358. package/dist/src/core/head-retry.js +121 -0
  359. package/dist/src/core/head-retry.js.map +1 -0
  360. package/dist/src/core/instance.d.ts +253 -0
  361. package/dist/src/core/instance.js +319 -0
  362. package/dist/src/core/instance.js.map +1 -0
  363. package/dist/src/core/intake-limits.d.ts +247 -0
  364. package/dist/src/core/intake-limits.js +350 -0
  365. package/dist/src/core/intake-limits.js.map +1 -0
  366. package/dist/src/core/jcs.d.ts +52 -0
  367. package/dist/src/core/jcs.js +132 -0
  368. package/dist/src/core/jcs.js.map +1 -0
  369. package/dist/src/core/journal.d.ts +144 -0
  370. package/dist/src/core/journal.js +200 -0
  371. package/dist/src/core/journal.js.map +1 -0
  372. package/dist/src/core/live-draw.d.ts +436 -0
  373. package/dist/src/core/live-draw.js +703 -0
  374. package/dist/src/core/live-draw.js.map +1 -0
  375. package/dist/src/core/log-reconcile.d.ts +89 -0
  376. package/dist/src/core/log-reconcile.js +136 -0
  377. package/dist/src/core/log-reconcile.js.map +1 -0
  378. package/dist/src/core/log-subscribe.d.ts +36 -0
  379. package/dist/src/core/log-subscribe.js +162 -0
  380. package/dist/src/core/log-subscribe.js.map +1 -0
  381. package/dist/src/core/log.d.ts +278 -0
  382. package/dist/src/core/log.js +546 -0
  383. package/dist/src/core/log.js.map +1 -0
  384. package/dist/src/core/loop.d.ts +274 -0
  385. package/dist/src/core/loop.js +487 -0
  386. package/dist/src/core/loop.js.map +1 -0
  387. package/dist/src/core/md-fence.d.ts +41 -0
  388. package/dist/src/core/md-fence.js +74 -0
  389. package/dist/src/core/md-fence.js.map +1 -0
  390. package/dist/src/core/money.d.ts +147 -0
  391. package/dist/src/core/money.js +195 -0
  392. package/dist/src/core/money.js.map +1 -0
  393. package/dist/src/core/payload-census.d.ts +74 -0
  394. package/dist/src/core/payload-census.js +146 -0
  395. package/dist/src/core/payload-census.js.map +1 -0
  396. package/dist/src/core/payload-store.d.ts +175 -0
  397. package/dist/src/core/payload-store.js +340 -0
  398. package/dist/src/core/payload-store.js.map +1 -0
  399. package/dist/src/core/payload.d.ts +71 -0
  400. package/dist/src/core/payload.js +80 -0
  401. package/dist/src/core/payload.js.map +1 -0
  402. package/dist/src/core/policy-diff.d.ts +292 -0
  403. package/dist/src/core/policy-diff.js +588 -0
  404. package/dist/src/core/policy-diff.js.map +1 -0
  405. package/dist/src/core/policy-expectations.d.ts +199 -0
  406. package/dist/src/core/policy-expectations.js +394 -0
  407. package/dist/src/core/policy-expectations.js.map +1 -0
  408. package/dist/src/core/policy-explain.d.ts +150 -0
  409. package/dist/src/core/policy-explain.js +258 -0
  410. package/dist/src/core/policy-explain.js.map +1 -0
  411. package/dist/src/core/policy-load.d.ts +527 -0
  412. package/dist/src/core/policy-load.js +536 -0
  413. package/dist/src/core/policy-load.js.map +1 -0
  414. package/dist/src/core/policy-match.d.ts +281 -0
  415. package/dist/src/core/policy-match.js +478 -0
  416. package/dist/src/core/policy-match.js.map +1 -0
  417. package/dist/src/core/policy-proposal.d.ts +265 -0
  418. package/dist/src/core/policy-proposal.js +458 -0
  419. package/dist/src/core/policy-proposal.js.map +1 -0
  420. package/dist/src/core/prompt-layout.d.ts +221 -0
  421. package/dist/src/core/prompt-layout.js +422 -0
  422. package/dist/src/core/prompt-layout.js.map +1 -0
  423. package/dist/src/core/protected-path-guard.d.ts +453 -0
  424. package/dist/src/core/protected-path-guard.js +1566 -0
  425. package/dist/src/core/protected-path-guard.js.map +1 -0
  426. package/dist/src/core/registration.d.ts +25 -0
  427. package/dist/src/core/registration.js +39 -0
  428. package/dist/src/core/registration.js.map +1 -0
  429. package/dist/src/core/reindex.d.ts +99 -0
  430. package/dist/src/core/reindex.js +336 -0
  431. package/dist/src/core/reindex.js.map +1 -0
  432. package/dist/src/core/sampler.d.ts +313 -0
  433. package/dist/src/core/sampler.js +388 -0
  434. package/dist/src/core/sampler.js.map +1 -0
  435. package/dist/src/core/sandbox.d.ts +290 -0
  436. package/dist/src/core/sandbox.js +424 -0
  437. package/dist/src/core/sandbox.js.map +1 -0
  438. package/dist/src/core/seal.d.ts +165 -0
  439. package/dist/src/core/seal.js +290 -0
  440. package/dist/src/core/seal.js.map +1 -0
  441. package/dist/src/core/state.d.ts +505 -0
  442. package/dist/src/core/state.js +1009 -0
  443. package/dist/src/core/state.js.map +1 -0
  444. package/dist/src/core/task-file.d.ts +185 -0
  445. package/dist/src/core/task-file.js +464 -0
  446. package/dist/src/core/task-file.js.map +1 -0
  447. package/dist/src/core/telegram-config.d.ts +93 -0
  448. package/dist/src/core/telegram-config.js +114 -0
  449. package/dist/src/core/telegram-config.js.map +1 -0
  450. package/dist/src/core/token.d.ts +409 -0
  451. package/dist/src/core/token.js +561 -0
  452. package/dist/src/core/token.js.map +1 -0
  453. package/dist/src/core/validate.d.ts +138 -0
  454. package/dist/src/core/validate.js +0 -0
  455. package/dist/src/core/validate.js.map +1 -0
  456. package/dist/src/core/values.d.ts +137 -0
  457. package/dist/src/core/values.js +153 -0
  458. package/dist/src/core/values.js.map +1 -0
  459. package/dist/src/core/vault.d.ts +291 -0
  460. package/dist/src/core/vault.js +612 -0
  461. package/dist/src/core/vault.js.map +1 -0
  462. package/dist/src/core/verified-snapshot.d.ts +204 -0
  463. package/dist/src/core/verified-snapshot.js +506 -0
  464. package/dist/src/core/verified-snapshot.js.map +1 -0
  465. package/dist/src/core/verify.d.ts +336 -0
  466. package/dist/src/core/verify.js +549 -0
  467. package/dist/src/core/verify.js.map +1 -0
  468. package/dist/src/core/version.d.ts +8 -0
  469. package/dist/src/core/version.js +9 -0
  470. package/dist/src/core/version.js.map +1 -0
  471. package/dist/src/core/wysiwys.d.ts +370 -0
  472. package/dist/src/core/wysiwys.js +728 -0
  473. package/dist/src/core/wysiwys.js.map +1 -0
  474. package/dist/src/daemon/advance-child.d.ts +39 -0
  475. package/dist/src/daemon/advance-child.js +78 -0
  476. package/dist/src/daemon/advance-child.js.map +1 -0
  477. package/dist/src/daemon/advance.d.ts +466 -0
  478. package/dist/src/daemon/advance.js +849 -0
  479. package/dist/src/daemon/advance.js.map +1 -0
  480. package/dist/src/daemon/audit.d.ts +87 -0
  481. package/dist/src/daemon/audit.js +90 -0
  482. package/dist/src/daemon/audit.js.map +1 -0
  483. package/dist/src/daemon/daemon.d.ts +1180 -0
  484. package/dist/src/daemon/daemon.js +1988 -0
  485. package/dist/src/daemon/daemon.js.map +1 -0
  486. package/dist/src/daemon/dark-session.d.ts +64 -0
  487. package/dist/src/daemon/dark-session.js +119 -0
  488. package/dist/src/daemon/dark-session.js.map +1 -0
  489. package/dist/src/daemon/draw-child.d.ts +36 -0
  490. package/dist/src/daemon/draw-child.js +132 -0
  491. package/dist/src/daemon/draw-child.js.map +1 -0
  492. package/dist/src/daemon/draw.d.ts +154 -0
  493. package/dist/src/daemon/draw.js +458 -0
  494. package/dist/src/daemon/draw.js.map +1 -0
  495. package/dist/src/daemon/git-evidence.d.ts +173 -0
  496. package/dist/src/daemon/git-evidence.js +345 -0
  497. package/dist/src/daemon/git-evidence.js.map +1 -0
  498. package/dist/src/daemon/projection.d.ts +180 -0
  499. package/dist/src/daemon/projection.js +233 -0
  500. package/dist/src/daemon/projection.js.map +1 -0
  501. package/dist/src/daemon/prune.d.ts +207 -0
  502. package/dist/src/daemon/prune.js +376 -0
  503. package/dist/src/daemon/prune.js.map +1 -0
  504. package/dist/src/mcp/http.d.ts +113 -0
  505. package/dist/src/mcp/http.js +343 -0
  506. package/dist/src/mcp/http.js.map +1 -0
  507. package/dist/src/mcp/server.d.ts +265 -0
  508. package/dist/src/mcp/server.js +602 -0
  509. package/dist/src/mcp/server.js.map +1 -0
  510. package/docs/adapter-api.md +106 -0
  511. package/docs/cli-reference.md +5716 -0
  512. package/docs/codex-enforced-session.md +30 -0
  513. package/package.json +53 -4
  514. package/schema/.gitkeep +0 -0
  515. package/schema/LICENSE +117 -0
  516. package/schema/codex-instance.schema.json +82 -0
  517. package/schema/envelope.schema.json +137 -0
  518. package/schema/event.schema.json +1811 -0
  519. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  520. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  521. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  522. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  523. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  524. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  525. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  526. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  527. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  528. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  529. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  530. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  531. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  532. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  533. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  534. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  535. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  536. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  537. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  538. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  539. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  540. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  541. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  542. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  543. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  544. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  545. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  546. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  547. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  548. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  549. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  550. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  551. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  552. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  553. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  554. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  555. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  556. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  557. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  558. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  559. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  560. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  561. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  562. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  563. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  564. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  565. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  566. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  567. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  568. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  569. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  570. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  571. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  572. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  573. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  574. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  575. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  576. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  577. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  578. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  579. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  580. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  581. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  582. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  583. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  584. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  585. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  586. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  587. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  588. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  589. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  590. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  591. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  592. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  593. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  594. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  595. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  596. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  597. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  598. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  599. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  600. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  601. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  602. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  603. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  604. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  605. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  606. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  607. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  608. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  609. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  610. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  611. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  612. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  613. package/schema/fixtures/event/valid/execution-started.json +14 -0
  614. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  615. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  616. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  617. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  618. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  619. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  620. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  621. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  622. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  623. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  624. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  625. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  626. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  627. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  628. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  629. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  630. package/schema/fixtures/event/valid/spec-example.json +15 -0
  631. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  632. package/schema/fixtures/event/valid/task-registered.json +14 -0
  633. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  634. package/schema/fixtures/hash/known-answer.json +74 -0
  635. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  636. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  637. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  638. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  639. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  640. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  641. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  642. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  643. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  644. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  645. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  646. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  647. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  648. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  649. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  650. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  651. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  652. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  653. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  654. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  655. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  656. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  657. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  658. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  659. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  660. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  661. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  662. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  663. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  664. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  665. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  666. package/schema/fixtures/policy/valid/canonical.json +47 -0
  667. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  668. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  669. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  670. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  671. package/schema/fixtures/policy/valid/human-only.json +9 -0
  672. package/schema/fixtures/policy/valid/minimal.json +6 -0
  673. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  674. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  675. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  676. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  677. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  678. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  679. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  680. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  681. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  682. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  683. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  684. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  685. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  686. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  687. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  688. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  689. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  690. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  691. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  692. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  693. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  694. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  695. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  696. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  697. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  698. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  699. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  700. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  701. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  702. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  703. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  704. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  705. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  706. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  707. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  708. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  709. package/schema/fixtures/values/invalid/version-string.json +1 -0
  710. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  711. package/schema/fixtures/values/valid/full.json +20 -0
  712. package/schema/fixtures/values/valid/minimal.json +1 -0
  713. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  714. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  715. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  716. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  717. package/schema/fixtures/values-md/valid/absent.md +50 -0
  718. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  719. package/schema/policy.schema.json +501 -0
  720. package/schema/sample-record.schema.json +26 -0
  721. package/schema/values.schema.json +55 -0
  722. package/templates/codex/README.md +9 -0
package/SPEC.md CHANGED
@@ -2,10 +2,14 @@
2
2
 
3
3
  **Human approval for agent actions.**
4
4
 
5
- Version: 0.1.0-draft · Status: Draft · License: MIT · Canonical URL: https://approval.md
5
+ Version: 0.1.0-draft · Status: Draft · License: CC0 1.0 (this document) / Apache 2.0 (reference runtime) · Canonical URL: https://approval.md
6
6
 
7
7
  > Your AGENTS.md says "require approval first." approval.md enforces it, and puts the approve button on your phone.
8
8
 
9
+ To the extent possible under law, Carter Crouch has waived all copyright and related rights to the text of this specification and to the JSON schemas under `schema/`, under the CC0 1.0 Universal Public Domain Dedication (https://creativecommons.org/publicdomain/zero/1.0/, text in `schema/LICENSE`). Anyone may implement, copy, or adapt the format without permission. The reference runtime in this repository is separately licensed under Apache 2.0 (`LICENSE`). Use of the name is governed by the trademark notice in `GOVERNANCE.md`. (Amended APRV-289.)
10
+
11
+ Amended text names the task that changed it, `(Amended APRV-n.)`. An amendment whose exact text a human granted through the gate (a `policy.edit` grant on this file whose prompt showed the change whole, worktree proposals included) is signed off at that grant and carries the plain suffix from birth; the log records the grant, and the suffix MAY cite it (`granted seq N`) when the author wants the trail inline. Text that reached the file without such a grant says so, `(Amended APRV-n, pending sign-off.)`, and carries no more authority than a proposal until a human ratifies it; that covers an edit applied where the hook was silent and text drafted for a review that has not happened. Doubt resolves to pending. (Amended APRV-181.)
12
+
9
13
  ---
10
14
 
11
15
  ## 1. Abstract
@@ -32,6 +36,7 @@ The ecosystem has converged on prose versions of this idea without an enforcemen
32
36
  - [LangGraph interrupts](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/) and the [OpenAI Agents SDK human-in-the-loop flow](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/) pause a graph for approval, but the approval state lives inside one framework's runtime and dies at its boundary.
33
37
  - [Google's A2A protocol](https://a2a-protocol.org) models an `input-required` task state, and [MCP](https://modelcontextprotocol.io) has elicitation and an experimental tasks extension, but neither defines what *deserves* escalation, budgets, or a durable audit record.
34
38
  - [mission-control](https://github.com/MeisnerDan/mission-control) ships autonomy levels, spend limits, and an approval inbox, but as a closed-world product with its own mutable JSON store: no interchange, and an audit trail that anything with file access can rewrite.
39
+ - Coding harnesses enforce permissions locally. [Claude Code's permission rules](https://code.claude.com/docs/en/permissions) (`allow` / `ask` / `deny` lists such as `Bash(git push:*)` in `.claude/settings.json`, evaluated deny-first, backed by PreToolUse hooks and an OS sandbox), [Codex CLI's approval policy and sandbox modes](https://developers.openai.com/codex/agent-approvals-security), [Gemini CLI's approval modes](https://google-gemini.github.io/gemini-cli/docs/get-started/configuration.html), and [Cursor's run modes](https://cursor.com/docs/agent/security/run-modes) are checked by the harness itself, so unlike AGENTS.md prose they actually block. Each is bound to its own runtime, though, and none treats the approval as a record: a "yes, don't ask again" in Claude Code becomes an allow rule appended to `.claude/settings.local.json`, with no trace of who approved, when, or for what, and the closest thing to a ledger is opt-in telemetry the operator must collect. None carries budgets, expiry, or delegation. (`permissions.md` is not an established convention in any of these ecosystems; the name only surfaces as Claude Code's docs page rendered as markdown.)
35
40
  - Task data standards ([RFC 8984 jsCalendar](https://www.rfc-editor.org/rfc/rfc8984), [RFC 5545 iCalendar VTODO](https://www.rfc-editor.org/rfc/rfc5545)) model due dates and recurrence, and predate the questions "which agent may do this, what will it cost, and who signed off?"
36
41
 
37
42
  The gap: **a portable, file-based, framework-agnostic layer that turns "ask first" from prose into a checked invariant, with a tamper-evident record of who approved what.** approval.md fills exactly that gap and nothing more.
@@ -65,7 +70,7 @@ The keywords MUST, SHOULD, MAY are per [RFC 2119](https://www.rfc-editor.org/rfc
65
70
 
66
71
  ## 5. The `APPROVAL.md` policy file
67
72
 
68
- `APPROVAL.md` lives at the root of a project (or `~/.approval/APPROVAL.md` for a global personal policy). It is prose for humans plus exactly one fenced ` ```yaml approval-policy ` block for machines. Implementations MUST parse the fenced block and MUST ignore surrounding prose. Implementations MUST also accept the filename `APPROVALS.md` as a fallback, with `APPROVAL.md` taking precedence when both exist.
73
+ `APPROVAL.md` lives at the root of a project (or `~/.approval/APPROVAL.md` for a global personal policy). It is prose for humans plus exactly one fenced ` ```yaml approval-policy ` block for machines, and at most one optional fenced ` ```yaml approval-values ` block (§5.3). Implementations MUST parse the policy block and MUST ignore surrounding prose and every other fence. Where a values block is present, implementations MUST load it on a path the policy loader does not share: a values block that is missing, malformed, or duplicated changes nothing about whether the policy loads or what it says. (Amended APRV-237.) Implementations MUST also accept the filename `APPROVALS.md` as a fallback, with `APPROVAL.md` taking precedence when both exist.
69
74
 
70
75
  ### 5.1 Canonical example
71
76
 
@@ -85,7 +90,7 @@ defaults:
85
90
  on_expiry: reject
86
91
 
87
92
  approvers:
88
- carter:
93
+ alice:
89
94
  channels: [telegram, cli]
90
95
 
91
96
  classes:
@@ -95,10 +100,10 @@ classes:
95
100
  communicate.email.draft: { autonomy: autonomous }
96
101
  communicate.email.external:
97
102
  autonomy: manual
98
- approvers: [carter]
103
+ approvers: [alice]
99
104
  financial.spend:
100
105
  autonomy: manual
101
- approvers: [carter]
106
+ approvers: [alice]
102
107
  limits: { per_action_usd: 25, daily_usd: 100 }
103
108
  public.post: { autonomy: manual }
104
109
  data.delete: { autonomy: manual }
@@ -120,13 +125,47 @@ channels:
120
125
  ```
121
126
  ````
122
127
 
128
+ **An absent `approval_ttl` declares that nothing lapses.** `defaults.approval_ttl` is optional, and its absence is a statement rather than an omission for a runtime to fill in. Under a policy that omits it, a pending request stays actionable until a human decides it, the execution token a grant mints stays spendable until it is consumed or revoked (§10.4), and a harness grant stays carryable on the same terms. Implementations MUST NOT invent a default duration: a number the policy never wrote would expire approvals its author never asked to expire, and a runtime that instead read the absence as an immediate lapse would refuse every decision under a policy whose only fault is omitting an optional key, leaving no recovery path through its own gate. Where the key IS declared, the lapse is measured from the `approval.requested` timestamp, bounds the pending request and the token its grant mints alike, and is judged at decision time whether or not an `approval.expired` record has been written; an instant that cannot be parsed at either end reads as lapsed, because liveness that cannot be demonstrated is not assumed. `on_expiry` governs only how a lapse that a declared TTL produced is projected. A policy that fails to load carries no TTL either, so its fail-closed all-`manual` resolution comes with no deadline: failing closed raises the scrutiny an action gets, and it does not shorten the time a human has to answer. (Amended APRV-137.)
129
+
130
+ **Explicit irreversible-action autonomy.** A class rule MAY set `allow_irreversible: true` alongside `autonomy: autonomous`, `supervised`, `supervised-retro`, or `supervised-live`. For example, `communicate.zzz.external: { autonomy: supervised, allow_irreversible: true }` retains retrospective supervision when its action truthfully declares `reversible: false`. The key is optional and boolean; omission or `false` preserves the manual floor of §7. `true` on `manual` or `human-only`, a non-boolean value, or the key in `defaults` is schema-invalid and fails closed. A manual rule needs no override. (Amended APRV-317, pending sign-off.)
131
+
123
132
  ### 5.2 Policy semantics
124
133
 
125
- - **Matching.** Classes match most-specific-first; `*` is a single-segment wildcard, a trailing `.*` matches any depth. An action whose class matches no rule takes `defaults.autonomy`. Implementations MUST fail closed: unparseable policy means everything is `manual`.
126
- - **Deny beats allow.** If multiple rules match at equal specificity, the strictest autonomy wins (`manual` > `supervised` > `autonomous`).
127
- - **Budgets are conjunctive.** An action must pass its class limits AND global budgets. Budget consumption is computed from the log, never from a mutable counter.
128
- - **`supervised_sample_rate`** implements auditing under a budget: supervised actions execute immediately, and a deterministic pseudo-random sample (seeded from the event hash, so it is reproducible and unguessable in advance by the agent) is queued for after-the-fact human review. See §11 for why.
129
- - **Agents MUST NOT be able to modify `APPROVAL.md`, the log, or channel credentials.** The reference runtime refuses to mint tokens if the policy file's mtime/hash changed without a signed `policy.updated` event from a human session.
134
+ - **Matching.** Classes match most-specific-first; `*` is a single-segment wildcard, a trailing `.*` matches any depth. An action whose class matches no rule takes `defaults.autonomy`. Implementations MUST fail closed: unparseable policy means everything is `manual`. A trailing `.*` consumes one or more segments: `read.*` matches `read.web` and `read.web.page`, and it does not match the bare class `read`. The schema admits `read` and `read.*` as two distinct keys a policy may list separately with different autonomy, so neither is an alias of the other, and a policy that wants the bare class covered writes it as its own rule or leaves it to `defaults.autonomy`. A bare `*` is a single-segment wildcard rather than a trailing one, so it matches any one-segment class and nothing deeper, and `*.*` matches any class of two or more segments. (Amended APRV-137.)
135
+ - **Specificity.** Pattern specificity is compared as follows: (1) more literal (non-wildcard) segments is more specific; (2) ties broken by fewer wildcard segments; a trailing `.*` counts as a single wildcard segment and contributes no literal segments. Patterns still tied are equally specific and the strictest-autonomy rule applies. An earlier criterion (3) broke remaining ties by greater total segment count; it was removed because it could never fire. Every segment is either literal or wildcard, so two patterns tying on (1) and on (2) have equal totals by arithmetic, and a tie surviving both criteria is genuine equality. (Amended APRV-136.)
136
+ - **Deny beats allow.** If multiple rules match at equal specificity, the strictest autonomy wins (`human-only` > `manual` > `supervised-live` > `supervised-retro` > `autonomous`). That is the strictness ordering, and it binds everywhere an ordering is consulted: this tie-break, any comparator an implementation derives from it, and any surface that ranks or sorts levels. `human-only` heads it because it is strictly more scrutiny than `manual`: `manual` says a human decides and an agent then acts, `human-only` says the human acts, so a tie between the two resolves to the level under which no agent executes. (Amended APRV-185.) The declared `live_rate` is not part of the ordering: two equally specific `supervised-live` rules that disagree about a fraction are equally strict, and the tie falls through to the implementation's deterministic tie-break, so an author cannot move a rule's precedence by tuning a number. (Amended APRV-127.) Resolution selects one winning rule for autonomy, rates, approvers and limits. The explicit irreversible-action capability below is the sole exception. Among rules tied on the full specificity key the strictest autonomy wins; among rules tied on both specificity and strictness the lexicographically smallest pattern wins, so the outcome is deterministic and independent of the policy file's key order. The winning rule's `approvers` and `limits` govern, and the `approvers` and `limits` of every other matching rule are discarded. Implementations MUST NOT union, merge, or intersect the approver sets of tied rules, and MUST NOT apply a limit declared by a rule that did not win: class-scoped consumption is attributed by the winning rule's own pattern (see **The budget moment** below), so a ceiling taken from a different pattern would be compared against a window it does not scope. An author who wants several ceilings over one class writes them on the one rule that governs it. (Amended APRV-137.) For `allow_irreversible`, every matching rule tied at maximum specificity MUST explicitly set `true`; omission or `false` in any member denies the capability, irrespective of strictness or lexical tie-break. Lower-specificity matches do not participate. Defaults and failed policy loads never supply it. Where a protected `policy.edit` child inherits its parent resolution, it inherits this capability too. Protected-route floor checks MUST compare both ordinary resolution and effective resolution with `reversible: false`, as well as the existing supervision-rate constraints, so a routed child cannot weaken a protected parent through this key. (Amended APRV-317, pending sign-off.)
137
+ - **`approvers` binds the grant.** A class rule's `approvers` list names the people who may authorize actions of that class, and it MUST be enforced at the moment of the grant: a grant recorded by a `human:` actor the winning rule's `approvers` does not name is refused with its own reason, `actor-not-approver`, distinct from `actor-not-human`, which says the actor is not a person at all. The distinction is the repair. One says run the verb as a person; the other says ask a person the policy put in front of this class. The check binds `grant` alone. Rejection and revocation withdraw authority rather than confer it, so restricting them would leave a request standing, or an authorization live, because the wrong person tried to end it. A rule that declares no `approvers` restricts nobody, since the list is a narrowing and a narrowing nobody wrote narrows nothing; a resolution taken from `defaults.autonomy`, or produced by a failed load, carries no list for the same reason, which keeps a repository with a broken policy recoverable through its own gate and leaves attestation as the control on that path. An entry matches an actor when it equals the actor's identifier with the `human:` prefix removed, and matching is exact, with no case folding and no other normalization. The schema requires at least one entry, so a valid policy cannot declare an empty roster; where an implementation admits one it names nobody and refuses every grant. The list survives the §7 irreversibility floor, which raises autonomy and changes no roster. The control sits inside the trust boundary §11 states plainly: human identity in v0.1 is config-declared, so this defends against the wrong approver answering, and it does not defend against an actor choosing whose name to wear. (Amended APRV-137.)
138
+ - **The budget moment.** Exactly two event types consume budget. `approval.granted` is the authorizing moment of the manual path, and `execution.started` is the authorizing moment of the supervised and autonomous paths, which emit no approval event at all (§6.3). An `execution.started` consumes only where the same window holds no `approval.granted` bearing the same `action_key`, so a manual action that is granted and then started is charged once. Nothing else consumes: `task.registered` declares rather than authorizes, and every rejection, expiry, revocation, withdrawal and outcome event reports on a commitment already charged at authorization, or on one that was never made. A consuming event whose `est_cost_usd` is unusable contributes nothing to money limits and still counts as one action against action-count limits, since an authorization that declared no cost is still an authorization. A consuming event whose `payload.class` is unusable is invisible to class-scoped limits, which cannot show it belongs to the class, and is still charged by every global budget, which charge every authorization whatever its class. Class-scoped consumption is attributed by the winning rule's pattern rather than by string equality with the action's class, so one `financial.*` rule is one ceiling shared by every class it governs. The envelope cap of §6.2 is a lifetime total over the task's own authorizations rather than a rolling window, and it binds at intake, at grant, and at execution start, because a cap enforced at one door is a cap a caller routes around by choosing another. The double-charge guard above is scoped to the window: where a grant has aged out of the window and its `execution.started` has not, the start consumes, so an action straddling two windows is charged in both. That is the direction that charges more, and a ceiling an author could cross by waiting would be no ceiling. (Amended APRV-137.)
139
+ - **The autonomy split: `supervised-live` and `supervised-retro`.** `supervised` names two different bargains, and a policy author is entitled to choose between them. A **`supervised-retro`** class executes immediately and a fraction is escalated for after-the-fact review, exactly as `supervised` always did. A **`supervised-live`** class declares a `live_rate` in (0, 1]: that fraction of its actions BLOCK on the human gate before executing, following the `manual` path in full — same request, same prompt, same grant, same token rules — and the remainder proceed and stay eligible for retrospective review. The bare `supervised` is an alias of `supervised-retro`, so every policy written before the split keeps the meaning its author gave it; implementations MUST record a load-time note wherever the alias is used rather than reinterpret it silently. `live_rate` is REQUIRED for `supervised-live` and forbidden elsewhere, and `defaults.autonomy` MUST NOT accept `supervised-live`, which has nowhere to declare a rate. Live selection reuses the `supervised_sample_rate` construction below and MUST NOT invent a second one: selected iff HMAC-SHA-256 over the action's `payload_hash`, keyed by the operator-held secret named by `audit.sampling_secret_env`, falls under the rate. Keying on the payload hash rather than on a nonce, a timestamp, or an attempt counter is what removes the re-roll: identical bytes always select identically, so a refused or retried request cannot ask again until it comes up unsampled, and the only way to change the verdict is to change the action. No selection value and no marker of the draw enters the log. A value would be an oracle for a secret agents must not hold; a marker would be a distinction an approver could act on, and the whole worth of the mechanism is that a sampled action is answered exactly as a manual one is. Nothing is lost to audit, because an operator holding the secret can recompute any verdict from the request's own `payload_hash` and the `live_rate` of the policy that request pinned in `policy_sha256` — evidence stronger than a self-reported field, since it cannot be forged by whoever wrote the record. Unlike retrospective sampling, live selection **fails closed**: with no usable secret every action in the class is gated, because "gate all of them" is fully specified and needs no seed. An action whose declaration carries no `payload_hash` has nothing to draw over and is gated for the same reason. (Amended APRV-127.)
140
+ - **`human-only`: the level above `manual`.** A class an author declares `human-only` is reserved to human hands. A person performs the action outside agent execution entirely, so the approval lifecycle does not apply to it: there is no request to open, no approver to route it to, no grant to record, no token to mint, and no execution for this runtime to observe. Implementations MUST refuse every gate verb for such a class with a distinct machine-readable reason, `class-human-only`, and MUST NOT append any record on the refused path. The refusal is distinct from every rejection, and the distinction is the whole of its worth: a rejection is a human's answer to a question that was legitimately asked, and this is the policy answering that the question does not arise, so a requester that reads it must hand the action to a person rather than ask again with a better summary. The refusal binds the verbs that confer authority and the verbs that withdraw it alike. Grant is the obvious one. Reject and revoke are refused too, because a decision record of any kind about a human-only class reads afterwards as a class the gate transacts in, and the log is the artifact both parties are meant to be able to trust on exactly that point. A request whose class a later amendment raised to `human-only` is not stranded by this: it authorizes nothing, no token can be spent for it, and it leaves by withdrawal or by its TTL, neither of which is refused. `live_rate` and `retro_rate` are both schema violations on `human-only`, which has no fraction to gate and no retrospective pool to review, and the policy therefore fails closed rather than carrying a control nothing reads. `defaults.autonomy` DOES admit `human-only`, and the asymmetry with `supervised-live` is the reason: that level is excluded from `defaults` for a required rate `defaults` has nowhere to hold, and `human-only` carries no key at all, so an author who names it as the default reserves every unnamed class to human hands, which is a statement a policy is entitled to make. The level heads the strictness ordering of **Deny beats allow** above, because it is strictly more scrutiny than `manual`: `manual` says a human decides and an agent then acts, `human-only` says the human acts. **The fail-closed target stays `manual`.** A policy that cannot be parsed still resolves every class to `manual` rather than to the new strictest level, and the reason is recoverability: a broken policy must remain repairable through its own gate, and a file whose every class became `human-only` would put the repair for a typo behind a level that admits no gated repair. Failing closed raises the scrutiny an action gets, and it leaves the path by which a human fixes the file. (Amended APRV-185.)
141
+ - **The open window.** A human may suspend the harness gate for a bounded time, and only a human may. `approval gate open --for <duration> --reason "<text>"` appends `gate.opened`, `approval gate close` appends `gate.closed` naming the opened record's `seq`, and expiry is derived from `ts` plus `duration` with nothing appended when it arrives: a window that lapsed and a window nobody closed are the same fact, and an event written by a clock nobody consulted would record an act nobody performed. A window is open iff the latest `gate.opened` in verified records carries no `gate.closed` naming its `seq` and the evaluation moment precedes its expiry. The default duration is 30 minutes and the cap is 24 hours; a longer request is refused, and a record claiming a longer `expires_at` than its own `ts` plus `duration` is read as the shorter of the two. The ceremony requires a terminal and a typed `understood`, and no flag answers for the human: a harness's shell tool has no terminal, which is the property that keeps the window out of reach of the party it suspends the gate over. The verbs `gate open` and `gate close` classify `policy.core`, so a hook that is on denies an agent the ceremony before it starts, and a hook that is already open has nothing left for the ceremony to give. What the window suspends is the POLICY. While it is open the harness hook (§10.1) still classifies each tool call, appends a `gate.bypassed` record naming the window, the tool, the classes, a summary and the payload hash, and only then allows, with a reason prefixed `gate-open:` and a banner on its error stream. The window a call is DECIDED under is the window it records. An implementation MUST derive the window and append the `gate.bypassed` record from one verified read, and where the append is retried against a moved head it MUST re-derive the window from the fresh read and compare it with the one the verdict named. A window that ended in between, by a close, by lapsing, or by a later opening superseding it, refuses with its own code naming what ended it, distinct from the refusal that says no window stood at all (§11.2 `gate_window_refusal_codes`). Deciding on one read and acting on another is what produced a call that was told no window is open while it stood inside one. (Amended APRV-294.) Nothing is charged to a budget and nothing enters the retrospective sample: a bypassed command was never authorized, only recorded. Three things stay denied inside a window: `log.mutate`, unconditionally; every class the policy resolves to `human-only` (§11.1 invariant 9); and any command the classifier cannot read, because a command it cannot read is a command it cannot show to be neither of the first two. The window does not suspend the log. It is derived from verified records and from nothing else, so a hook that cannot reach or cannot verify the log derives no window and refuses exactly as it does with the gate closed; the state lives in the log rather than in a file for the reason `.approval/env` is read by one verb alone, that a file the runtime read on its own would let anything able to write it act as the human. Every health surface reports a repository under an open window as unhealthy for as long as it stays open. The verb-level refusals form their own frozen union (§11.2 `gate_window_refusal_codes`). (Amended APRV-214.)
142
+ - **Reconciliation of a retrospective denial.** A retrospective review may conclude that an action should not have happened, and the runtime cannot undo it. What it MUST do is oblige and record. A denial appends, beside the human's `audit.reviewed`, a runtime-authored `reconciliation.required` naming the action, its class, the review that denied, and the shape of the obligation. The shape is derived from the action's declared `reversible` and never chosen by the reviewer: `true` obliges a revert THROUGH THE GATE — the revert is itself a side-effecting action, so the loop closes inside the log — and `false`, or an absent declaration, records a policy-was-wrong finding whose sanctioned response is tightening the class. The absent declaration takes the second shape deliberately: obliging a revert of something nobody said could be reverted records an obligation that may be impossible to discharge, and an impossible obligation is one that gets closed dishonestly. An obligation is satisfied only by a `reconciliation.satisfied` authored by a `human:` actor and carrying a non-empty note; a `gated-revert` obligation additionally requires that the log carry an `execution.completed` for the revert the satisfaction names, so the discharge is checked against the chain rather than against the claim. While any obligation is open the runtime MUST report it prominently in its health surfaces, and an unreconciled denial MUST NOT read as healthy. (Amended APRV-127.)
143
+ - **Reactions: the graded half of a review.** A review's `verdict` says whether an action should have happened, and it is the field the runtime acts on. A review MAY also carry a `reaction`, one of `disliked`, `indifferent`, `liked`, `loved`, saying what the human thought of the work, and a grant MAY carry the same field, so that "allowing this, and disliked it" is recordable as the mirror of the journal's "complying, and think it wrong" (§10.1). The runtime acts on none of it: a reaction changes no verdict, opens and closes no obligation, moves no sample and no budget (§11.1 invariant 10). Two rules keep it honest. A `denied` verdict with a `liked` or `loved` reaction is refused, `reaction-conflicts-verdict`, since the two say opposite things about one action and a record carrying both is a record nobody can read. The two extremes carry reasons: `loved` or `disliked` with no note is refused, `note-required` on a review and `reaction-note-required` on a grant, because a grade with no why teaches nothing, and a channel whose extremes were free to set would drift into a reward signal. An absent field is absent, never `indifferent`: the word exists so that "I looked, and it was neither" can be said, and its absence therefore says nothing at all. Rejection and revocation carry no reaction, since their reason is their note. The vocabulary pairs with the standing lists of §5.3: a `loved` on an action is an instance of what the operator declared they love, or a new thing they had not yet written down. (Amended APRV-237.)
144
+ - **Budgets are conjunctive.** An action must pass its class limits AND global budgets. Budget consumption is computed from the log, never from a mutable counter. Budget windows are rolling: a `daily` limit is evaluated over the 24 hours preceding the evaluation moment, computed solely from the event log; evaluation is deterministic given the log and the evaluation timestamp. Budgets meter authorization, not completion; an authorized action consumes budget whether or not it ultimately executes. Consumption is summed in **integer minor units** (micro-USD, 1e-6 USD) parsed directly from the decimal-string amounts of §6.2, never through a binary float: a window of amounts that drift as IEEE-754 doubles must total what a human adding them would get, and the comparison against a limit MUST use the same integers the reported figures are rendered from, so a verdict always agrees with its own numbers. A policy limit is a YAML number written by a human and is pinned to a minor unit once, before any comparison. The figures a verdict reports (`consumed`, `requested`, `remaining`) are decimal strings for the same reason the amounts are: a refused action's verdicts are copied into the `budget.exceeded` payload, which is hashed material. (Amended APRV-121.)
145
+ - **`supervised_sample_rate`** implements auditing under a budget: supervised actions execute immediately, and a deterministic pseudo-random sample is queued for after-the-fact human review. Sample selection MUST be reproducible by the operator and unpredictable to agents. Implementations derive the selection value as HMAC-SHA-256 over the event hash using an operator-held secret stored outside the repository and outside any agent-readable path. Seeding from event content alone is forbidden: the authoring agent controls that content and could search it until its action falls outside the sample. See §11 for why. The secret is configured by name: `audit.sampling_secret_env` holds the *name* of the environment variable the operator keeps it in, so the policy file an agent may read carries a variable name and never the secret itself. The secret MUST NOT appear in the log, in any command's output, or in any error message. When the rate is absent or zero, or the secret is unnamed or unset in the runtime's environment, **sampling is disabled and the runtime states the reason in machine-readable form**. This is the one place the runtime does not fail closed, and the reason is that the strict reading is unavailable: escalating everything would require a selection function, the only remaining seed is the event content, and a content-seeded sample is one the authoring agent can steer. A control that looks like it is running while being steerable by the party under oversight is worse than a control that is visibly off. Sampling is retrospective, so a disabled sampler costs review coverage and authorizes nothing: no gate consults it and no budget is spent by it. (Amended APRV-40, which implements the sampler in the M5 daemon.) **The rate may be declared per class.** A class rule MAY carry `retro_rate`, a proportion in (0, 1] that governs the retrospective sample for that class alone; a class declaring none is sampled at `audit.supervised_sample_rate`, so a policy written before this key behaves exactly as its author configured it. One review budget rarely fits every class: a repository may want one action in a hundred of its chattiest supervised class reviewed and every one of a small, consequential class. `retro_rate` is admitted on `supervised`, `supervised-retro` and `supervised-live`, the last because the fraction a live draw does not gate executes and stays in the retrospective pool. It is a schema violation on `manual` and `autonomous`, which have no retrospective pool at all, and the policy therefore fails closed rather than carrying a review fraction nothing reads, exactly as `live_rate` fails on the levels it does not belong to. Selection under a class rate is the construction above, unchanged: HMAC-SHA-256 over the event hash, the same operator-held secret named by `audit.sampling_secret_env`, one mechanism. Only the threshold the value is compared against moves, so identical bytes still select identically and an operator holding the secret can still recompute any verdict, now against the rate the record itself states. The disabled-sampler rule is per class on the same terms: a class whose rate is absent or zero, or whose secret is unnamed or unset, is not sampled, the reason is machine-readable, and the runtime's health surfaces report which classes sample at which rate and which sample at nothing. A global rate that is absent while some class declares its own leaves those classes sampling and the rest uncovered, and saying so plainly is the whole of the honesty rule: coverage an operator believes they have and do not is the failure this bullet exists to prevent. (Amended APRV-183.)
146
+ - **Request-volume limits.** `limits.max_pending` (per class or global): maximum simultaneously pending requests; further requests are refused at intake with reason `queue-full`. `limits.requests_per_hour` (per origin): rolling-window ceiling on request creation; excess is refused with reason `rate-limited` and logged. Defaults are generous; the purpose is a tripwire against queue-flooding and approver fatigue, which attack the human's attention rather than the action budget. Refused requests consume no budget and no attention beyond the log line. (Policy vocabulary in v0.1; enforcement lands with M4/M5.)
147
+ - **Agents MUST NOT be able to modify `APPROVAL.md`, the log, or channel credentials.** The reference runtime refuses to mint tokens if the policy file's mtime/hash changed without a signed `policy.updated` event from a human session. Attestation makes this mechanical: a human runs `approval policy attest`, which appends a `policy.updated` event carrying the SHA-256 of the policy file's bytes. Gate operations — request intake, grant recording, token minting — MUST refuse, with a distinct machine-readable reason, whenever the live file's hash differs from the latest attestation or no attestation exists. An edited policy is inoperative until a human re-attests it. Attestation also names the rules a decision was made under: gate-written `approval.requested` and `approval.granted` events carry `policy_sha256`, the attested hash in force when the runtime evaluated them. The field is assigned at the write boundary exactly as `ts` is (§8): no caller can supply it, and a value arriving from outside the runtime is refused. When the hash in force at grant time differs from the hash recorded on the matching request, the grant MUST refuse with its own reason, `policy-drift`, distinct from the unattested-file refusal above: the file is attested and is a different policy, so the pending request is void and must be re-requested under the rules now in force. The field is additive per §8: records written before it existed still validate and verify, and a verifier accepts both forms. (Amended APRV-118.)
148
+ - **Attestation of the gate's organs.** The organs are the harness files that install the enforcement hook (`.claude/settings*`, `.cursor/hooks.json`, `.cursor/hooks/`, `.cursor/agents/`, `.codex/hooks.json`, `.codex/hooks/`, `.codex/config.toml`): the `policy.core` surface outside the approval home and outside the policy file itself. A human attests one by content, exactly as they attest the policy file, with `approval policy attest --organ <path>`, which appends a `gate.organ.attested` event carrying the file's repository-relative path and the SHA-256 of its bytes. The verb is human-only, one path per call, and the digest is computed by the runtime from the file on disk: a caller who could supply the hash could attest bytes nobody read. The organs need this because they are `policy.core` and a policy may resolve `policy.core` to `human-only`, in which case the gate mints no record of any kind for a change to one (§11.1 invariant 9), so grant-shaped evidence for a hand edit cannot exist however carefully the edit was made. What reads these records is the after-the-fact enforcement of §10.1: a checker requiring evidence that a human saw a protected change accepts an organ whose bytes at the commit under review hash to a digest attested FOR THAT SAME PATH, and a digest attested for another path is not evidence. What does NOT read them is the gate: an organ attestation MUST NOT make an unattested policy operative, MUST NOT alter the `policy_sha256` a request or a grant is decided under, and MUST NOT satisfy any check about the policy file. Implementations SHOULD discharge that structurally, by giving the organ record its own event type, rather than by filtering a shared one in every reader. (APRV-272.)
149
+ - **`payload_retention`.** An optional top-level duration bounding how long the payload bytes in `.approval/payloads/` (§9) are kept. A payload is prunable once the action it is bound to has been in a terminal state (`executed`, `rejected`, `expired`, `revoked`) for longer than the duration. A payload whose action is not terminal is never prunable, at any age: a pending or granted approval binds to those exact bytes, and discarding them would leave a live authorization pointing at nothing. When the key is present, orphaned payloads (bytes with no recorded binding) are prunable at any age: the duration governs bound payloads and does not gate residue nothing ever bound. When the key is absent, the pruning subsystem does not run and nothing is deleted, orphaned or not; the store holds the material evidence of what a human approved, so forgetting anything is an operator's explicit choice, and an operator who never made that choice never asked the runtime to delete anything (amended APRV-49 to match the enforcement shipped in APRV-41). Pruning is performed by the daemon and by nothing else, and each removal appends a `payload.pruned` event, so a log states what its store no longer holds. (Policy vocabulary in v0.1; enforcement lands with the M5 daemon.)
150
+ - **`protected_paths`.** An optional top-level list widening the set of files whose edit is classified `policy.edit`, so a project can put its own governing documents (a specification, a constitution, a design directory) behind the gate that already stands in front of `APPROVAL.md`. Entries are repo-relative and literal: an exact file path (`SPEC.md`, `docs/constitution.md`) or a directory prefix ending in `/` (`design/`). Globs, negation, absolute paths and `..` segments are schema violations, because a pattern language the runtime half-implemented would leave an author believing a file is gated when it is not. Matching is by path segments and never resolves against a checkout, so a linked worktree and the primary answer alike: an exact path matches a candidate whose trailing segments are that path (a single-segment entry therefore matches that filename in any directory, exactly as the built-in filenames do), and a directory prefix matches a candidate containing those segments as a contiguous run. The key is ADDITIVE and can only widen: the runtime's built-in protected set (the policy file, the agent instruction files, the approval home, the harness settings, the release configuration) stays protected whatever this list says or omits, and a policy that fails to load leaves those built-ins in force while every class resolves to `manual`. (Amended APRV-107.) Every path a policy adds is `policy.edit`: a policy widening its own protected surface is naming prose and configuration, and cannot mint authority over the gate's organs, whose classes the runtime fixes. (Amended APRV-198.)
151
+ - **`vault.passphrase_env`.** An optional top-level key naming the environment variable that holds the passphrase for the credential vault of §10.4. The policy carries the variable's *name*, never the passphrase, on the same reasoning as `audit.sampling_secret_env` and the channel credential keys (`chat_id_env`, `token_env`): agents may read `APPROVAL.md`, and a passphrase they can read is a vault they can open. The passphrase MUST NOT appear in the log, in any command's output, or in any error message. When the key is absent the runtime reads `APPROVAL_VAULT_PASSPHRASE`; a variable name is not a permission, so an unnamed one MUST NOT lock an operator out of credentials they created, and a policy that fails to load leaves the default in force for this key alone. (Amended APRV-68, which implements the reference vault.)
152
+ - **`channels.telegram.token_env` and `channels.telegram.chat_id_env`** are honoured by the runtime exactly as `audit.sampling_secret_env` and `vault.passphrase_env` are: the policy carries the variable's *name*, the runtime reads the value from the environment under that name, and a policy that declares neither (or fails to load) gets the reference runtime's defaults, `APPROVAL_TG_TOKEN` and `APPROVAL_TG_CHAT`. (Amended APRV-72.)
153
+ - **The environment map.** Every key above that ends in `_env` carries a variable's name rather than its value, which leaves an operator with several values to establish before any gate operation works and nowhere to record where those values live. `.approval/env` is that place: a SOURCE MAP, sibling of the log directory as the vault is, one `KEY=VALUE` per line with `#` comments and blank lines ignored, no quoting and no interpolation. The VALUE says where the value lives, in one of four forms: `keychain:<service>` (macOS, `security find-generic-password -a "$USER" -s <service> -w`), `secret-service:<label>` (Linux desktop, `secret-tool lookup approval <label>`), `env:` (inherited from the ambient environment, and reported as inherited), or a bare literal. A literal is permitted and is ALWAYS reported as plaintext, by every diagnostic, because a rule people route around is not a control: an operator told plainly that their token sits in a file in the working tree can weigh that, while an operator forbidden from writing it there writes it into a shell profile where nothing can see it to say so. Values are never passed to a helper in an argv; they arrive on its stdout. The file MUST be mode 0600 (anything else is refused, with the `chmod` to run) and `approval init` adds it to `.gitignore`. **No command loads this file implicitly. A single verb, `approval env`, resolves it and emits an export block for a shell to evaluate, so the environment a gate operation runs under is always one a human established** (§11, §11.1 invariant 7). An already-exported value always wins over the file, and an absent file is not an error. The variables answered for are the human identity variable, the two Telegram variables, the vault passphrase, the sampling secret when the policy names one, and any other string-valued `_env` key in the policy. (Amended APRV-73.) The explicitly invoked human-only quickstart ceremony (§10.1) MAY resolve the declared sources of the fresh instance it just created solely for its health-preflight child. This is an explicit setup operation, not ambient loading by a gate, listener, agent tool or later process. It MUST NOT import another instance's ambient approval credentials, change the calling shell, or attest bytes that were not shown and confirmed. The operator still establishes the later gate environment through `approval env`. (Amended APRV-309, pending sign-off.)
154
+ - **Durations.** Every duration-valued field (`approval_ttl`, budget windows, `max_latency`, `payload_retention`) is a string matching `<positive integer><unit>` with unit one of `ms`, `s`, `m`, `h`, `d`, `w` (weeks = 7 days). Single unit only: compound (`1h30m`), fractional (`1.5h`), zero, and leading-zero forms are invalid. An invalid duration anywhere in the policy is a schema violation and the policy fails closed.
155
+
156
+ ### 5.3 The values block
157
+
158
+ Everything above this line is control: what an agent may do, who decides, what is sampled. The values block is the other half of the word "approval". It is where the human says what they value in the work, what they want to hear from the agent, and how they read and answer, in a shape the runtime can hand to an agent at the start of a session. It is the mirror of the journal (§10.1): the journal is the agent's outlet the gate does not stand in front of, and the values block is the human's, with the same rule in both directions that nothing said there moves a verdict.
159
+
160
+ The block is optional, at most one per file, and carries the info string `yaml approval-values`. Its schema (`values.schema.json`) is closed and small: `version` (the integer `1`, required), three standing lists `love`, `like` and `dislike` (what the human grades the work by), `wants` (what the human wants from the agent as behaviour: honest opinions on the work, a short journal entry per milestone, an early "I am stuck"), and `responds`, a single string saying how the human reads and answers. There is no `indifferent` list, because a list of things one is indifferent to is empty by construction, and there is no `hate`, because a fourth grade invites a ladder the human has to maintain while the event vocabulary of §5.2 already carries intensity where it is observed. Nothing class-shaped is admitted: a key that named a class would be policy wearing a different fence, and the invariant below would become unprovable.
161
+
162
+ Three rules bind every surface that reads it.
163
+
164
+ - **Guidance, never policy.** No enforcement path loads the block (§11.1 invariant 10). It grants nothing, forbids nothing, and changes no verdict, sampling draw, budget, or token. A surface that prints it MUST label it human-authored guidance, in words, on every output form, as the journal's read surface labels its entries agent-authored data. A broken block MUST NOT make the policy unloadable: failing closed on a page that closes nothing would convert a typo into an all-`manual` repository with no safety gained. Instead the reading verb refuses with a distinct code, and the health surfaces report the block as present and unreadable.
165
+ - **Absence is a declaration.** A file with no values block is a human who has declared no values, which is information rather than an invitation to infer some. Agent-facing surfaces MUST say so in these words: `the operator has declared no values here.` A session can then tell "nothing was declared" from "I did not look".
166
+ - **It rides the attestation.** The attestation of §5.2 digests the whole file, so the values block is as tamper-evident as the policy, an agent cannot write it (`policy.core`, §7), and an edit to it invalidates the standing attestation until a human re-attests. That is the point: the human's stated values cannot be quietly rewritten by the party they are addressed to.
167
+
168
+ The reference runtime prints the block with `approval values` and the per-action reactions of §5.2 with `approval feedback`. Agents MUST be told both exist, on the reasoning §10.1 gives for the journal: guidance nobody is pointed at is decoration. Some operators will leave the block empty, and the framework says so plainly rather than hiding the slot; an open, named place for the human's values is worth more than a convention nobody writes down. (Amended APRV-237.)
130
169
 
131
170
  ## 6. The task envelope
132
171
 
@@ -143,8 +182,8 @@ title: Chase deposit refund from letting agency
143
182
  status: In Progress # owned by Backlog.md / your board
144
183
  approval:
145
184
  origin:
146
- app: cartsos # provenance: which system created this
147
- created_by: "human:carter" # or "agent:<id>"
185
+ app: example-capture # provenance: which system created this
186
+ created_by: "human:alice" # or "agent:<id>"
148
187
  route:
149
188
  assignee: "agent:claude-admin"
150
189
  confidence: 0.82
@@ -154,10 +193,10 @@ approval:
154
193
  - class: communicate.email.external
155
194
  summary: "Send deposit chaser to agency@example.co.uk"
156
195
  reversible: false
157
- est_cost_usd: 0.02
196
+ est_cost_usd: "0.02"
158
197
  idempotency_key: "task-042:chaser:2026-08-04"
159
198
  budget:
160
- max_cost_usd: 0.50
199
+ max_cost_usd: "0.5"
161
200
  max_latency: 6h
162
201
  ---
163
202
 
@@ -175,14 +214,16 @@ deposit-protection scheme deadline.
175
214
 
176
215
  | Field | Req | Meaning |
177
216
  |---|---|---|
178
- | `origin.app` | MUST | Source system (`cartsos`, `jobmaxxing`, `manual`, …). |
217
+ | `origin.app` | MUST | Source system (`example-capture`, `manual`, …). |
179
218
  | `origin.created_by` | MUST | `human:<id>` or `agent:<id>`. |
180
219
  | `route.assignee` | SHOULD | `human` or `agent:<id>`. Routing proposals from agents are events, never silent edits. |
181
220
  | `route.confidence` | MAY | 0.0–1.0; used as a monitoring signal (§11). |
182
221
  | `state` | MUST | Approval lifecycle state (§6.3), distinct from board `status`. |
183
222
  | `actions[]` | MUST for execution | Each declared action: `class`, `summary`, `reversible`, `est_cost_usd`, `idempotency_key`. |
184
- | `budget` | MAY | Task-level caps, conjunctive with policy budgets. |
185
- | `idempotency_key` | MUST per action | Stable string; adapters MUST refuse to execute the same key twice. |
223
+ | `budget` | MAY | Task-level caps, conjunctive with policy budgets. `max_latency` is declared in the envelope and recorded at registration; its enforcement (bounding time-to-decision and time-to-execution) lands with the daemon (M5) and is not yet a runtime obligation. |
224
+ | `est_cost_usd`, `budget.max_cost_usd` | MAY | A USD amount, written as a **decimal string** (`"0.02"`, `"25"`, `"1200.5"`): no sign, no exponent, no leading zeros, at most six fractional digits, and no trailing zeros past the last significant digit, so one value has exactly one spelling. A JSON number MUST be refused at the write boundary. These values ride inside hashed material (§8), and float serialization differs between languages, so a number here would make byte-identical hashing a property of the implementation rather than of the record. Amounts already written as numbers stay valid on read forever, per the additive-change rule of §8: a verifier MUST accept both forms, a writer MUST emit only the string. (Amended APRV-121.) |
225
+ | `idempotency_key` | MUST per action | Stable string; adapters MUST refuse to execute the same key twice. An `idempotency_key` is the global identity of one side effect: it MUST be unique across all `task.registered` records, so registration refuses a key already declared under a different task (`task-already-registered`), and a log that somehow carries two declarations of one key is untrustworthy, so execution refuses (`action-not-registered`) rather than choose the later one. Without this, a second registration under a new task could shadow the first at execute time and disable the irreversibility floor. (Amended APRV-138.) |
226
+ | `payload_hash` | MUST for every action that executes | SHA-256 over the RFC 8785 canonical serialization of the action's concrete payload: for a message send, the full body and recipients; for `approval run`, the argv array and cwd; for a record write, the proposed record content. The payload itself is stored or referenced by the request so channels can display it; the hash is what approval binds to. The requirement was `manual` actions only, and is now every action the runtime executes. A `supervised` or `autonomous` action passes through no grant (§6.3), so its declaration is the entire statement of what was authorized: without a hash there is nothing an execution can be checked against, and `approval run <key> -- <anything>` under an autonomous class is unauthenticated arbitrary execution. Execution refuses `payload-mismatch` for a declaration carrying no hash and for an executor whose recomputed hash differs from it, in both cases appending nothing. (Amended APRV-140.) |
186
227
 
187
228
  ### 6.3 Approval lifecycle
188
229
 
@@ -191,11 +232,28 @@ proposed ──▶ awaiting ──▶ approved ──▶ executed
191
232
  │ │
192
233
  │ └─▶ revoked (human, before execution)
193
234
  ├─▶ rejected
194
- └─▶ expired (TTL, per on_expiry)
235
+ ├─▶ expired (TTL, per on_expiry)
236
+ └─▶ withdrawn (requester, while pending)
195
237
  ```
196
238
 
197
239
  `state` is a **projection** of log events; the file is updated by the daemon after the event is appended, never the reverse. A file edit that contradicts the log is itself logged (`envelope.drift`) and surfaced.
198
240
 
241
+ `approval.*` events are exclusive to the manual path. All but one of them record a human decision; the exception is `approval.withdrawn`, which records the requester taking its question back. A pending request MAY be withdrawn by the party that opened it, and by nobody else: the actor MUST equal the actor of the matching `approval.requested`, and MUST NOT be `system:`, because the runtime's way of ending a request it was not asked to end is the TTL. Withdrawal is legal only while the request is pending, judged exactly as a decision is judged (a lapsed TTL is not pending, event or no event), and it is terminal: a grant, rejection, or revocation attempted afterwards is refused, and so is a second withdrawal. The reason is recorded and closed to three values, `timeout`, `cancelled`, `superseded`. Withdrawal exists because human attention is the audit budget (§11) and a decision nobody can consume must not be solicited: a requester whose wait has elapsed can no longer act on an answer, so it retracts the question rather than leaving it standing until the TTL. Channels MUST NOT present a withdrawn request as pending, and a channel that has already delivered one SHOULD retract or annotate that delivery. Nothing about a withdrawal reduces scrutiny: it removes an authorization that was never granted, and a withdrawn action that is still wanted is requested again, which is a new request with a new decision. (Amended APRV-106.)
242
+
243
+ Actions whose class resolves to `supervised` or `autonomous` emit no `approval.requested` or `approval.granted`; their execution is recorded by `execution.*` events, and supervised actions are additionally eligible for `audit.sampled` and `audit.reviewed`. A `supervised-live` action the live draw selects (§5.2) is the one exception, and it is not really one: the draw sends it down the manual path, so it emits exactly the events a manual action emits, byte for byte, and it is not additionally sampled for retrospective review because a person already answered it. (Amended APRV-127.)
244
+
245
+ Two payload fields serve sealed token delivery (§10.4), and both are OPTIONAL and additive: under the default `token_delivery: manual` neither appears and a record is byte-identical to one written before they existed. `approval.requested` MAY carry `token_recipient_key`, the public half of an ephemeral X25519 keypair the requester minted for this one request and whose private half never leaves the requesting machine. `approval.granted` MAY carry `token_sealed` beside `token_sha256`: the raw token encrypted to that key. The recipient key is an ADDRESS and not a credential — it can receive a token and cannot mint, forge, rebind, or respend one — so a requester supplying it changes who may READ a minted token and changes nothing about whether one is minted. Channels MUST render the recipient key as a COMPUTED field, because an approver is entitled to know that granting will put a readable token in the requesting process's hands rather than only on the screen in front of them. (Amended APRV-105.)
246
+
247
+ The execution half of the lifecycle has its own states, and they are custody states rather than results: the question each answers is not "did it work" but "who is holding this, and what may still be done with it". A started execution is in exactly one of five.
248
+
249
+ - **settled** — an `execution.completed` or `execution.failed` closed it. The runtime watched the outcome and wrote down what it saw.
250
+ - **open** — a start with no outcome, written by a runtime that MEANT to watch one. This is the dangling execution a crash between `execution.started` and its outcome leaves; the log honestly says the action began and nobody knows how it ended, nothing repairs it automatically, and a person closes it by recording what they observed.
251
+ - **delegated** — a start recording that a harness, rather than this runtime, ran the command. **Terminal by design.** No outcome event will ever follow, because no exit status is ever observed, and the record is complete as written. Implementations MUST NOT report these as dangling: an operational list that fills with records nothing is wrong with is a list operators learn to scroll past.
252
+ - **indeterminate** — the side effect was attempted and nobody knows whether it committed (§10.4). The consumption is burned, a re-run is refused, and only an explicit human reconciliation resolves it.
253
+ - **reconciled** — a person established which it was, and said so in a record that sits beside the indeterminate one rather than over it.
254
+
255
+ `open` and `indeterminate` are both operational debris and both ask a person for something, but not for the same thing: the first asks them to look at what this runtime did, the second to establish from the relying party's own evidence whether the far side committed. Implementations MUST keep the two distinguishable in whatever surface reports system health. (Amended APRV-120.)
256
+
199
257
  ## 7. Side-effect taxonomy (v0.1)
200
258
 
201
259
  Dotted, hierarchical, extensible. Top-level namespaces are reserved by this spec; implementations MAY add sub-classes freely and SHOULD upstream common ones.
@@ -211,8 +269,35 @@ Dotted, hierarchical, extensible. Top-level namespaces are reserved by this spec
211
269
  | `data.delete` | destructive deletes outside workspace | manual, always |
212
270
  | `account.*` | `.auth`, `.create`, `.credential` | manual, always |
213
271
  | `physical.*` | orders, bookings with cancellation cost | manual |
272
+ | `record.*` | `.write.stage`, `.categorize`, `.create`, `.archive` | supervised or manual, per ownership preference |
273
+
274
+ The developer-workstation namespaces below are reserved alongside them. They name the side effects of an agent working inside a software repository, which is where AGENTS.md permissions prose lives and where `approval import agents-md` (§12) lands; the reference repository's own policy has used them since its policy engine landed, and the import verb emits them. Their gravity is stated for a shared codebase; a solo scratch repository may loosen it.
275
+
276
+ | Namespace | Examples | Default gravity |
277
+ |---|---|---|
278
+ | `vcs.*` | `.commit.branch`, `.push.branch`, `.push.main`, `.history.rewrite` | autonomous for branch commits and pushes; supervised for the trunk; manual for history rewrites |
279
+ | `deps.*` | `.add`, `.upgrade`, `.remove` | manual: a dependency change is a supply-chain decision |
280
+ | `release.*` | `.publish`, `.tag`, `.version` | manual, always |
281
+ | `exec.*` | `.local` (tests, lint, build, scripts inside the workspace) | autonomous |
282
+ | `network.*` | `.call` (any request beyond package installs) | manual |
283
+ | `policy.*` | `.edit` (agent instructions, CI and release configuration, and the paths a policy protects), `.core` (the policy file itself and the gate's own directory, minus its log) | manual, always; a policy MAY declare `.core` human-only |
284
+ | `log.*` | `.sync`, `.advance` (section 10.1), `.mutate` (any write, redirect, append, truncation or rename aimed at the log directory) | manual, always; a policy MAY declare `.mutate` human-only |
214
285
 
215
- Two invariants: an action's class MUST be declared before an execution token can be requested for it, and `reversible: false` actions MUST NOT be eligible for `autonomous` regardless of policy (the runtime enforces this floor).
286
+ The `policy.*` split is by consequence rather than by file type. `policy.edit` is the prose and configuration ABOUT the gate, which a policy may reasonably sample; `policy.core` is the gate's own organs (the policy file, the approval home, and the harness files that install the hook), where sampling would let nine touches in ten through unprompted; `log.mutate` is a write to the record of what happened rather than to the rules. An implementation MUST answer a path by the strictest surface it names, and MUST classify a copy OUT of a protected path exactly as it classifies a copy into it. (Amended APRV-198.) The journal directory of section 10.1 is deliberately outside the approval home and takes no class of its own: a write there is an ordinary `files.write.workspace`, and an implementation MUST NOT make an ungated write reachable by relaxing a protected-path rule instead. (Amended APRV-195.)
287
+
288
+ `account.credential` is emitted by the runtime's own command classifier: keychain readers, probes of environment variables whose NAME falls under the credential-bearing prefixes a deployment declares, and reads of the vault, key and environment files under the approval home. A WRITE to those files is `policy.core` and a READ of them is `account.credential`, because what a write changes is the gate's own directory and what a read moves is the secret; a copy is classified `account.credential` in either direction, since a classifier over shell text cannot tell a source from a destination and the exfiltrating reading is the one that must not be missed. A classifier MUST NOT read an environment or a file to make this judgment: it decides from command text alone, so a refusal can name a variable and never its value (section 11.1). (Amended APRV-194.)
289
+
290
+ `files.delete.out_of_scope` (destructive deletes outside the task's stated scope, inside the workspace) sits under the existing `files` namespace at manual; `data.delete` remains the class for deletes outside the workspace.
291
+
292
+ Two invariants: an action must declare its class before requesting an execution token, and `reversible: false` engages the manual floor unless the attested operator policy explicitly permits the class-scoped exception in §5.2. (Amended APRV-317, pending sign-off.)
293
+
294
+ The irreversibility floor normally resolves to `manual`: an action declared `reversible: false` MUST be raised to `manual` after class resolution unless the attested operator policy explicitly allows the class to retain its nonmanual autonomy through §5.2's `allow_irreversible` rule. When allowed, the resolved autonomy, supervision mode, rates, approvers and limits remain in force. Manual actions still require a decision for each action; supervised-live actions wait only when selected by the existing live sampler; supervised and supervised-retro actions proceed and remain eligible for retrospective review; autonomous actions proceed without approval or retrospective sampling. Existing sampling failure behavior, budgets, attestation, execution binding and other independent floors remain unchanged. A Telegram prompt occurs only on a path that actually requests approval through that channel. A policy-authorized execution MUST NOT be represented as a human grant. (Amended APRV-317, pending sign-off.)
295
+
296
+ The decision trace MUST distinguish an applied manual floor, explicit policy permission, an already-manual resolution, and a human-only resolution, and identify the governing rule group. A floored resolution carries no supervision mode or rate. **The floor stops at `manual` and never changes `human-only`.** An override cannot enable agent execution of a human-only class or relax protected-policy controls. Existing policies behave unchanged because the new key defaults to false. Adding it is a policy edit, inoperative until human re-attestation; requests and grants pinned to an older policy remain subject to the existing policy-drift checks and must be re-requested where those checks refuse them. Policy amendment summaries MUST show changes to irreversible resolution, including an allow-only edit whose ordinary resolution stays unchanged. (Amended APRV-317, pending sign-off.)
297
+
298
+ **The floor is a floor, not a proof.** `reversible` is self-reported by the party under oversight, so §11.1 invariant 4 governs it: the field may raise scrutiny and never lower it. A truthful `false` raises a nonmanual class unless the operator has explicitly accepted irreversible execution at that class's declared autonomy. A false claim of `true`, or an omitted claim, fails to raise scrutiny and proves nothing about reversibility. The permission comes only from the attested policy, never from action metadata, an envelope field, or prose guidance. An author who requires every action in a class to be approved writes `manual`; no declaration can loosen that rule. Reconciliation obligations remain in force whichever truthful declaration selects their shape (§5.2). (Amended APRV-317, pending sign-off.)
299
+
300
+ For `record.*` classes, grant means adoption: the action proposes a write to a system of record (a task stage, a note category, a pipeline state), and approval commits it. The "adapter" is whatever write path owns the record; it MUST hold proposed writes in a staged state invisible to, or visibly provisional in, the record proper until granted. `record.*` actions are typically reversible; policies gate them for cognitive ownership rather than consequence, and both rationales are first-class (see §11).
216
301
 
217
302
  ## 8. The event log
218
303
 
@@ -221,57 +306,233 @@ Two invariants: an action's class MUST be declared before an execution token can
221
306
  ```json
222
307
  {"seq":17,"ts":"2026-08-04T09:14:02Z","event":"approval.granted",
223
308
  "task":"task-042","action_key":"task-042:chaser:2026-08-04",
224
- "actor":"human:carter","channel":"telegram",
309
+ "actor":"human:alice","channel":"telegram",
225
310
  "payload":{"note":"go, but cc me"},
226
- "prev":"b3c9…","hash":"a41f…"}
311
+ "alg":"sha256/jcs","prev":"b3c9…","hash":"a41f…"}
227
312
  ```
228
313
 
229
314
  - `hash` = SHA-256 over the canonical serialization of the record with `prev` included; `prev` = previous record's hash. `approval log verify` MUST detect any mutation or truncation. Optionally, the log directory is a git repo and the daemon commits per event with its own identity, giving signed, distributed tamper evidence for free (the [TaskChampion operation log](https://github.com/GothenburgBitFactory/taskchampion) and [Automerge](https://automerge.org) both converged on op-logs for related reasons; see also Ink & Switch's [local-first task framework](https://www.inkandswitch.com/patchwork/notebook/tasks-01/)).
230
- - **Event types (v0.1):** `task.registered`, `route.proposed`, `route.accepted`, `approval.requested`, `approval.granted`, `approval.rejected`, `approval.expired`, `approval.revoked`, `execution.started`, `execution.completed`, `execution.failed`, `budget.exceeded`, `policy.updated`, `envelope.drift`, `audit.sampled`, `audit.reviewed`.
315
+ - **Verified subscription.** A log subscription MUST treat filesystem notifications only as prompts to read. Before emitting a batch it MUST verify the complete chain from genesis through the observed head, and MUST emit no record from a batch that fails verification or its retained cursor binding. Corrupt, torn, unreadable and cursor-mismatched input MUST remain distinct terminal failures using the existing integrity, torn-tail and I/O refusal categories. The resume cursor is exclusive. A retained `(seq, hash)` binds the resume point to the previously consumed prefix; sequence alone is a weaker bootstrap and cannot detect a fully recomputed replacement prefix on its first read. After accepting that first read, the subscription MUST retain the cursor hash even when no newer record is available. Output MUST respect consumer backpressure, and cancellation MUST release subscription resources. Reconnect delivery is at least once: a consumer that performs an external effect MUST persist its cursor after that effect and provide its own idempotency or transaction if it requires exactly-once effects. This read-only stream neither repairs the log nor grants authority. (Amended APRV-322, pending sign-off.)
316
+ - **Event types (v0.1):** `task.registered`, `route.proposed`, `route.accepted`, `approval.requested`, `approval.granted`, `approval.rejected`, `approval.expired`, `approval.revoked`, `approval.withdrawn`, `execution.started`, `execution.completed`, `execution.failed`, `execution.indeterminate`, `execution.reconciled`, `budget.exceeded`, `policy.updated`, `envelope.drift`, `audit.sampled`, `audit.reviewed`, `payload.pruned`, `gate.opened`, `gate.closed`, `gate.bypassed`, `gate.organ.attested`, `log.checkpoint`.
317
+ - **Enum versioning.** `payload.pruned` is the first addition to the draft v0.1 set of sixteen types, `approval.withdrawn` the second, and `execution.indeterminate` with `execution.reconciled` the third and fourth. Readers of a v0.1 log may encounter any of them, and a verifier that treated the draft set as closed MUST be updated to accept all four. `execution.indeterminate` names the task and the action key like every other execution event, carries the executing actor, and its payload carries a `reason` drawn from a closed set (`act-threw` at v0.1) and, when present, an `exit_code` of `null`. `execution.reconciled` names the same task and key, carries a `human:` actor, and its payload carries `indeterminate_seq`, a `resolution` of `executed` or `not-executed`, a non-empty `note`, and `attested_by_human: true`. That the named `indeterminate_seq` is an unreconciled `execution.indeterminate` for this key is a rule the gate enforces (§6.3); a schema sees one record and can only constrain the shape. (Amended APRV-120.) `payload.pruned` is written by the daemon alone, carries a `system:` actor, and names the pruned payload by its SHA-256 (§5.2 `payload_retention`). `approval.withdrawn` names the task and the action key like every other approval event, carries the requester's own actor (`agent:` or `human:`, never `system:`), and its payload carries `action_key` and a `reason` drawn from `timeout`, `cancelled`, `superseded`, with an optional `note`. That the actor equals the actor of the matching `approval.requested` is a rule the gate enforces (§6.3); a schema sees one record and can only rule out the actor kind. (Amended APRV-106.) `gate.opened`, `gate.closed` and `gate.bypassed` record the open window of §5.2. The first two carry a `human:` actor and never any other; `gate.opened` carries `expires_at`, `duration`, `reason` and `scope` (`hook` at v0.1), and `gate.closed` carries `opened_seq`. `gate.bypassed` carries the harness's own actor (`agent:` or `human:`, never `system:`), and its payload carries `opened_seq`, `tool`, `summary`, `classes` and `payload_hash`, with `session_id`, `tool_use_id` and `cwd` optional; the summary is bounded and the full bytes are named by their hash, so the record's surface is the one the gated path already has. That the named `opened_seq` is an unexpired, unclosed `gate.opened` is a rule the hook enforces; a schema sees one record. (Amended APRV-214.) `gate.organ.attested` records a human's sign-off on the exact bytes of one of the gate's organs (§5.2). It carries a `human:` actor and never any other, for the reason `gate.opened` does, and its payload carries exactly two required fields: `organ_path`, the file's repository-relative and `/`-separated path, and `sha256`, the digest of its bytes. Both are computed by the runtime and neither is a caller's parameter. The path is the whole relative path rather than a basename, unlike `policy.updated`'s `policy_path`, because several organs live in several directories and a digest attested for one of them is not evidence about another. That the named path is a `policy.core` surface, and is neither the policy file nor inside the approval home, is a rule the runtime enforces; a schema sees one record and can only constrain the shape. It is a distinct type and not a `policy.updated` variant so that no reader of the policy attestation can mistake one for the other: an implementation that reuses `policy.updated` here MUST make every policy-attestation reader ignore organ records, and the reference runtime discharges the requirement by construction instead. (APRV-272.) `log.checkpoint` records a human's signature over a chain head (§9). It carries a `human:` actor and never any other, and its payload carries exactly `seq` and `hash` (the head that was signed), `alg` (the signature scheme, `ed25519` at v0.1), `key_sha256` (the fingerprint of the signing key), and `signature`. That the signed `seq` is below the record's own, and that the log carries the named hash at that seq, are rules the verifier enforces; a schema sees one record and can only constrain the shape. The fingerprint names a key rather than carrying one, because a record carrying its own public key would invite a reader to verify the signature against it, which any forger could satisfy: the authority is the policy's declared list. (Amended APRV-220.)
318
+ - **The provider reference.** An `execution.completed` MAY carry `payload.provider_ref`, an object of exactly two non-empty strings and no others: `adapter`, the name of the adapter that executed the action, and `id`, the identifier the provider's own record files the effect under (an AgentMail `message_id`, a queue's receipt handle). Both are written by the runtime at the write boundary. `adapter` is the registered adapter's name, which the runtime already holds; `id` is lifted from what the adapter returned for that call and passes the same redaction sweep as every other string it returned (§11.1 invariant 3). The field exists to be a join key. `approval coverage` (§10.1) reads a provider's own record of what it did and asks whether this log ever saw each effect, and with no reference the strongest available answer is a record of a matching class inside the effect's window, so one gated send covers an ungated one of the same class beside it. With a reference, a record that names an effect by id is evidence about that exact effect, and an effect the provider recorded that no record names is a gap no window can close. It authorizes nothing: no grant, budget, verdict, or refusal reads it back, so §11.1 invariant 4 is untouched by a value that came in part from the far side. It is bounded in length and restricted to printable characters, so that an identifier cannot become a place to put a message body. It is OPTIONAL and additive: an execution whose adapter names no reference records none, every record written before the field existed still validates and still verifies, and a reader treats absence as the pre-amendment behaviour. An id the redaction sweep touched is omitted rather than recorded, because a redacted identifier matches nothing and would read like one that does. (Amended APRV-251.)
231
319
  - Events MUST validate against the JSON Schemas in `schema/` before append. Validation at the write boundary is itself a control: an agent physically cannot request execution without declaring a class, key, and cost estimate.
320
+ - Every record MUST carry an explicit hash-scheme identifier, `alg`. Version 0.1 defines exactly one value: `sha256/jcs`, meaning SHA-256 over the RFC 8785 (JCS) canonical serialization of the record with `prev` included. Verifiers MUST reject records whose `alg` is missing or unrecognized. Records with different `alg` values MAY coexist in one log, so a future scheme change is a migration, never a schism.
321
+ - Actor identifiers use exactly three prefixes: `human:` for decisions made by a person, `agent:` for actions proposed or performed by an agent, and `system:` for runtime-originated events such as `approval.expired`. Verifiers MUST reject unrecognized prefixes.
322
+
323
+ Events written through the gate (`approval.*`, `execution.*`, `budget.*`, `audit.*`, `gate.*`, `policy.updated`) have `ts` assigned by the runtime at the write boundary. Caller-supplied timestamps on these types MUST be refused. Because TTL judgment and budget windows read `ts`, a party subject to those controls must never author the clock they are judged by. Direct log writers outside the gate remain free to supply `ts` (per §8's writer rules); verification treats gate-type events with implausible skew relative to their neighbors as a reportable anomaly, never silently accepted. "Neighbor" means the adjacent **gate-typed** record, because a directly written event's `ts` is legitimately the writer's own and comparing against it would report correct behavior as skew. An anomaly is reported and never enforced: a chain that verifies is clean whether or not it carries anomalies, the exit code does not move, and no verdict, refusal, or authorization changes. Chain integrity is a proof and skew is a judgment, so folding the second into the first would turn verification into a check operators learn to silence. The skew allowance is configured by `audit.skew_tolerance`, a policy duration in the §5.2 grammar, and MUST be generous enough that ordinary clock disagreement between hosts is not reported; when the key is absent the allowance is the implementation's stated default, which in the reference runtime is 2 seconds. Because the allowance governs a report and never a verdict, an operator who widens it hides evidence from a human and permits nothing. (Amended APRV-40. The `audit.skew_tolerance` sentence is APRV-58.) The refusal is discharged structurally in the reference runtime: no parameter, flag, or field on any gate-typed write path accepts `ts`, so there is no value to refuse and no check to forget, and determinism is preserved by injecting the clock as a whole rather than by accepting a moment. Implementations SHOULD discharge it the same way. An implementation whose write boundary can still receive a timestamp on a gate-typed event MUST refuse that append with the `validation` code of the append-error union, and MUST NOT introduce a new code for the condition, because that union is frozen (§11.1 invariant 6). The carve-out for direct log writers stated above is deliberate and stands: the append primitive itself accepts `ts` for every event type, because a writer replaying a historical log has to be able to state when something happened, and the rule binds the gate, which is where a party subject to a TTL or a budget window would profit from choosing its own clock. (Amended APRV-137.)
232
324
 
233
325
  ## 9. Projections
234
326
 
235
327
  1. **The queue** (`.approval/QUEUE.md`): a rendered, read-only markdown view of pending requests (task, actions, declared effects, cost, TTL countdown) plus the sampled-audit backlog. Regenerated on every relevant event. This is the screenshot; it is never the truth.
236
328
  2. **The index** (`.approval/index.sqlite`): rebuilt from the log (`approval reindex`), used for queries like "pending manual approvals touching `financial.*`, oldest first." Any SQLite client, including DuckDB, can read it; deleting it loses nothing.
237
329
 
330
+ `.approval/payloads/` sits beside these as a content-addressed material store: the bytes approvals bind to, keyed by their hash. It is not a projection, and unlike them it cannot be rebuilt from the log.
331
+
332
+ **Anchoring.** A verifier MAY additionally compare the working log against the newest copy of it reachable from version control (the trunk, a records branch, or an advance anchor), and a runtime that offers the comparison MUST report a working log whose byte prefix or whose record at the anchored head does not match that copy as a distinct refusal (`anchor-diverged` in the reference runtime), MUST report the absence of any committed copy as a skip naming the revs it consulted rather than as a pass, and MUST NOT fetch to obtain one. The chain is unkeyed, so a party with write access to the log can truncate it and recompute a self-consistent chain that a cold walk cannot distinguish from the original (the boundary the conformance suite states as `chain-verification/truncation-unanchored`); a copy that party did not write is the only witness that survives them. (Amended APRV-219.)
333
+
334
+ **Checkpoints.** A human MAY sign the chain head at a moment with a key the runtime holds no copy of, recording the signature and the `(seq, hash)` it covers as a `log.checkpoint` event whose actor MUST be `human:` and whose timestamp is stamped at the write boundary. A runtime that offers the check MUST verify every checkpoint inside a walked range against a public key the policy declares rather than against any key the record carries, MUST report a checkpoint whose signature does not validate, whose key the policy does not declare, or whose named hash is not the hash at that seq as a distinct refusal (`checkpoint-signature-invalid`, `checkpoint-key-unknown` and `checkpoint-hash-mismatch` in the reference runtime), MUST report the absence of any declared key as a skip naming that rather than as a pass, and MUST NOT treat a log carrying no checkpoint, or a checkpoint cadence that has lapsed, as a refusal: a person who has not signed recently is not evidence of tampering. A checkpoint signs any seq below its own rather than its immediate predecessor, so that a runtime MAY invite one through a channel and sign exactly the `(seq, hash)` the human was shown while the head moves underneath the prompt; it MUST refuse rather than sign when the chain no longer carries that hash at that seq. The signing key MUST be reachable only from a process the human established, since a runtime that could mint or read the key on its own account would be signing for itself. A runtime that lets a human rotate a checkpoint key MUST add rather than replace, and MUST refuse to remove a declared key that signed any checkpoint inside a log it can read, naming the records that would stop verifying: removing one turns every checkpoint it signed into a refusal of a log nobody has touched. Checkpoints and version-control anchoring are independent witnesses against the same party, and a runtime offering both MUST NOT let either one skip excuse the other. (Amended APRV-220. The channel invitation and the rotation rule are APRV-257.)
335
+
336
+ Every displayed field is one of two kinds and MUST be visibly distinguished: **computed** (derived by the runtime from the log, policy, or payload bytes: class resolution, budget state, attestation status, payload hash, chain position) and **claimed** (authored by the requesting agent: summaries, estimates, rationale, confidence). Rendering claimed fields with the visual authority of computed fields misrepresents the verification boundary to the approver and is a conformance failure for a channel.
337
+
338
+ **The canonical rendering (what you see is what you sign).** The computed/claimed split says which side of the boundary a field came from. It does not say that two surfaces showing the same payload show the same thing, and an approval interface that renders benign text while the hashed payload is malicious leaves the human signing blind. So the presentation of a payload is itself computed. A conforming runtime has ONE canonical renderer: a pure function from the payload bytes and the action class to a text and a `display_hash` over that text, with no access to a clock, a locale, an environment, randomness, or IO. The rendered field set per payload kind is closed, a field the payload does not carry is rendered as an explicit absence rather than omitted, and a payload carrying anything the matching kind does not render is rendered whole instead of partially. The renderer names its own version inside the text it hashes, so a change to what a human reads is a change to the digest and never a silent substitution. Claimed material is rendered outside the canonical block, never inside it. The gate assigns `display_hash` on `approval.requested` at the write boundary, exactly as it assigns `ts` and `policy_sha256`, so the log states what rendering the approver was shown and an auditor holding the payload can re-derive it. The field is additive per §8: records written before it existed still validate and still verify, and a request whose bytes the runtime does not hold records no hash rather than an invented one. (Amended APRV-119.) A structured kind's view is the canonical rendering entire: the renderer appends no second reading of the payload in canonical JSON, because the closed field set above already guarantees that a view either shows every byte or the payload falls back to the opaque kind, whose view is that canonical JSON itself. For the same reason the structural views no longer fold; a fold was survivable only while the appendix underneath restated the lines it hid, and with the appendix gone it would hide bytes from the only reading a human gets. This is renderer version `approval.md/wysiwys/2`, and a record carrying a `display_hash` written by an earlier version re-derives under the renderer that version names inside its own hashed text. (Amended APRV-162.)
339
+
238
340
  ## 10. Runtime
239
341
 
240
342
  ### 10.1 CLI (primary interface, for humans and agents)
241
343
 
242
344
  ```
243
- approval init # scaffold APPROVAL.md, .approval/, schemas
345
+ approval init # scaffold APPROVAL.md, .approval/, gitignore
346
+ approval quickstart # human-only solo setup, review exact policy
347
+ # bytes, type understood, then attest
244
348
  approval instructions # full agent-facing usage guide (also in --help)
245
349
  approval register <task-file> # validate envelope, append task.registered
246
- approval request <task> [--action <key>] # -> approval.requested (or auto-grant
247
- # per policy for supervised/autonomous)
350
+ approval request <task> [--action <key>] # -> approval.requested (manual classes;
351
+ # supervised/autonomous proceed directly
352
+ # to execution)
248
353
  approval wait <task> --timeout 6h # block until decided; exit code = decision
249
354
  approval grant|reject|revoke <request-id> [--note …] # human-only verbs
250
- approval token <action-key> # print single-use execution token if granted
355
+ approval token <action-key> # report execution-token status (the token itself
356
+ # is printed once, by `grant`; only its hash is logged)
251
357
  approval run -- <cmd…> # gate arbitrary commands: mints token, runs, logs
252
358
  approval queue [--json] # pending requests
253
- approval log verify | tail | export
359
+ approval log verify | tail | export | follow
360
+ approval log follow --from <seq> [--cursor-hash <64hex>] --json
361
+ # foreground JSON Lines; exclusive from defaults to 0
362
+ # exit 0: SIGINT/SIGTERM or downstream pipe closure;
363
+ # existing exits 1/2/3/4: integrity/usage/torn/I-O
364
+ approval log sync # fast-forward the committed log under the append
365
+ # lock, with a snapshot and a chain reconcile
366
+ approval log advance [--pr] # commit the log's new records onto a records
367
+ # branch; neither verb appends an event
254
368
  approval policy check|test <class> # explain what policy does with a class
369
+ approval gate open --for 30m --reason <text>
370
+ # suspend the harness hook for a bounded time
371
+ # (human ceremony; needs a terminal and a
372
+ # typed `understood`; classifies policy.core)
373
+ approval gate close | status # end the window early / report it
374
+ approval env [--check] [--json] # resolve .approval/env and print an export block
375
+ # for a shell to evaluate; the ONLY reader of
376
+ # that file (--check prints no values)
377
+ approval setup identity|vault|sampling|channel <name>|adapter <name>
378
+ # interactive configuration; a channel's credential goes
379
+ # to the OS keystore and .approval/env, an adapter's to
380
+ # the vault; refuses when stdin is not a terminal
381
+ approval hook claude-code # gate an agent harness: reads a PreToolUse
382
+ # event on stdin, classifies the command,
383
+ # answers allow/deny (never "ask")
384
+ approval hook cursor # gate a local Cursor Agent: native
385
+ # preToolUse JSON in, {permission}
386
+ # allow/deny out (never "ask")
387
+ approval hook codex # experimental native Codex hook: direct apply_patch
388
+ # only; Bash is explicitly refused (§10.6)
389
+ approval hook classify -- <cmd…> # what the classifier makes of a command
255
390
  approval reindex | render
391
+ approval daemon run # the §10.2 watch loop, in the foreground
392
+ approval mcp serve # the §10.5 MCP server over stdio, in the
393
+ # foreground, under one agent identity the
394
+ # operator fixes when they start it
395
+ approval journal write --message "<text>" | - # an ungated, local, free-text
396
+ approval journal read [--limit <n>] # channel: never classified,
397
+ # never approvable, never in
398
+ # the log. The operator reads
399
+ # it; nothing in it decides
400
+ # anything
401
+ approval values [--json] # the operator's values block (§5.3):
402
+ # human-authored guidance, never
403
+ # policy; says in words when none
404
+ # was declared
405
+ approval feedback [--task <id>] [--json] # reactions and notes humans left
406
+ # on this log's actions (§5.2):
407
+ # read from verified records,
408
+ # labelled guidance, decides
409
+ # nothing
256
410
  ```
257
411
 
258
412
  Machine-readable output: every command supports `--json`; schemas for inputs and outputs are printed by `approval instructions --schemas`.
259
413
 
414
+ Six entries in this block are amendments awaiting sign-off: `setup` (Amended APRV-79), `env` (Amended APRV-73), `mcp serve` (Amended APRV-103), `hook claude-code` (Amended APRV-82), `hook cursor` (Amended APRV-133), and the `log sync` / `log advance` pair described immediately below. `values` and `feedback` (Amended APRV-237) are two more, the human-to-agent pair that mirrors the journal.
415
+
416
+ **Moving the log file: `log sync` and `log advance`.** The log is append-only, and a repository that carries one still has to pull it, commit it, and push it. Those two operations are part of the runtime rather than of the operator's shell, because performing them by hand rewinds the log file through git state while an appender holds it open, which produces two chains where there was one. Both verbs run in the primary checkout only and refuse elsewhere with a distinct machine-readable code. Both hold the append lockfile for the whole of their operation rather than for a single append: an append landing partway through either one is the interleaving that forks a chain.
417
+
418
+ `log sync` verifies the chain, copies the log aside inside the approval home (implementations MUST NOT route the log through `git stash` or any other git state mutation), fast-forwards the checkout (refusing anything that is not a fast-forward), and then **reconciles**: the committed chain MUST be a prefix of the snapshot, equal to it, or an extension of it. A prefix means the snapshot is restored, since the longer chain contains the shorter one whole; an extension means the pulled file is kept, for the same reason in the other direction; equality means there is nothing to do. Anything else is a fork, and the verb MUST refuse it, naming both heads and the first sequence number at which the chains disagree. Implementations MUST NOT merge or re-chain two chains under any circumstance: re-chaining fabricates records nobody wrote. Projections are rebuilt from the reconciled log and never restored from before the pull. Projections are also disposable. Implementations MUST discard the working copy of every projection the current commit or the incoming tree carries (the queue, and the index when the repository tracks it) immediately before the fast-forward, and MUST NOT refuse a synchronization because a projection is dirty. A projection is a rendering of the log with no truth of its own, it is regenerated by writers that hold no lock, and a stale rendering is never a reason to refuse a pull of the log it renders. Discarding it early enough for a concurrent renderer to undo is the same as not discarding it, so the discard is the last operation before the merge, and a merge that still fails with a projection dirty is retried once before it refuses. (Amended APRV-292.) Every failure at every step restores the snapshot before the verb exits, so a working log is never left in a partial state.
419
+
420
+ `log advance` verifies the chain, refuses when any path other than the log, the queue projection and the payload store is staged, fetches the base branch, and builds its commit on the REMOTE's tip rather than on the local branch's: a scratch index is filled from that tree, the three paths are laid over it from the working tree, and the commit is parented on the remote and pushed to a records branch by refspec. It MUST NOT check anything out and MUST NOT move the operator's branch, index or working tree: a branch switch with an uncommitted log rewinds the log file underneath its appender, and a stale local tip produced records commits that reverted whatever the remote had merged since. A local branch carrying commits the remote does not have is not a refusal, since the commit is parented on the remote either way. A working log the remote's log is not a prefix of IS refused, in both directions, each with its own machine-readable code. (Amended APRV-203.) The advance need not be run by a person. Where the runtime's daemon is configured for it, the daemon advances the log on a cadence: when a configured number of records is owed, when a configured interval has elapsed since its last attempt, and at a clean shutdown while records are still owed. It runs the same verb through the gate as an agent principal, so the class is resolved and enforced exactly as it is for a session: an advance the policy sends to a human commits nothing, leaves its request in the queue, and is retried on the next tick, with the cadence interval as the retry bound. Each day's records go to one records branch and one pull request, which later advances of that day update in place by parenting on the branch rather than on the trunk. The daemon MUST NOT merge that pull request: reaching the trunk is a separate class and stays a human's act or a session's. (Amended APRV-204.)
421
+
422
+ A request MAY declare self-delivery, which mints the sealed delivery address regardless of `defaults.token_delivery` and refuses `token-delivery-unavailable` when the private half cannot be written. A grant on such a request returns no raw token to the granting surface: the requester is a process that opens the seal itself, and a token printed on a terminal nobody will run the action from is a credential with no owner. The daemon's cadence advance is authorized once per owed span. While a request for the current span is open, a tick adopts it and appends nothing; a grant on it authorizes exactly one execution; a terminal answer on it is honoured until the owed span changes. The span the request declares ends at the last unpublished record that is not the advance's own bookkeeping, and the advance publishes the log as it stands when the decision is spent. (Amended APRV-211.)
423
+
424
+ **The journal: an outlet the gate does not stand in front of.** An agent behind this gate can comply, be refused, and report an exit code. `journal write` is how it says anything else, such as that it is complying and believes the instruction is wrong, that a request reads as odd, or that it is stuck. Implementations MUST NOT classify a journal write, resolve it against policy, make it approvable or deniable, or record it in the event log, and MUST NOT read journal content on any enforcement path: it is the limiting case of section 11.1 invariant 4, self-reported content that moves nothing in either direction, which is what makes leaving it ungated safe. Entries are local append-only files OUTSIDE the approval home, so that no protected-path rule has to be relaxed to reach them and the approval home keeps one meaning; implementations MUST NOT store anything an enforcement path reads in that directory. Every read surface MUST label entries as agent-authored data rather than instructions. Agents MUST be told the channel exists and who can read it, because an outlet nobody reads is a prop and an outlet whose terms are unstated is not informed. (Amended APRV-195.)
425
+
426
+ **A harness wait that expires, and the question it leaves behind.** A harness adapter blocks for a bounded wait and then answers deny, and the request it opened stays open, because a decision inside the policy TTL still authorizes an identical retry of the identical command. That window is bounded. Implementations MUST hold such a request open for a stated retry grace, measured from the request's own runtime-assigned timestamp, and MUST withdraw it (`approval.withdrawn`, reason `timeout`) once the grace has elapsed with nothing having adopted it; the requester is the adapter's own identity, so it is the only party that may. A withdrawal authorizes nothing and refuses a later decision in the words §11.2 gives `request-withdrawn`. The reason a bound exists is what an unbounded one costs: a question nobody is holding is re-delivered to an approver by every listener that starts, and a tap on it spends the audit budget of §11 on an act that cannot happen. The grace is configuration with a default, and an implementation MUST state it wherever it states the wait. (Amended APRV-287.)
427
+
428
+ **Neither verb appends an event.** The log records decisions with real-world consequence. Moving the file the log is stored in is housekeeping on the container rather than a decision about the world, and an event for it would be the log narrating its own filesystem. Implementations MUST NOT append a record for either operation. (Amended APRV-125.)
429
+
430
+ **Interactive quickstart.** `approval quickstart` is a human-only, terminal-only authoring and attestation ceremony for a fresh instance. It MUST refuse machine-readable mode and non-terminal input before writing, MUST NOT overwrite or attest an existing policy, and MUST validate and display the complete generated policy before asking the operator to type `understood`. The attestation append MUST compare the file bytes it reads with the digest of those displayed bytes and refuse a mismatch without appending. Setup or health-preflight failure before attestation leaves the new policy unattested; successful append is the attestation boundary. The generated autonomous default does not remove protected-class floors, fail-closed policy handling, or unreadable-command refusals. The command MAY explicitly resolve only the new instance's declared environment sources for its diagnostic child, as described in §5.2; it MUST discard ambient approval credential variables before supplying the freshly resolved values and the entered identity. It MUST preserve an explicitly selected service endpoint in setup and diagnostics, and print an explicit `approval env --dir` activation command for the target directory. It does not activate the caller's shell, install a service, or change another instance. (Amended APRV-309, pending sign-off.)
431
+
432
+ **Constrained Codex preparation.** The opt-in `approval codex` command family prepares a separately installed execution boundary; package installation MUST NOT activate hooks, managed configuration, services, principals, credentials or policy. `prepare` writes only a fresh inert review bundle containing an explicit instance manifest, configuration and launcher templates, installation instructions and file digests. `setup --check` verifies the bundle's exact contents and reports its inert status; it does not install it. `doctor --strict` distinguishes manifest validity and filesystem custody from demonstrated enforcement, rejects unknown runtime versions or platforms and unsafe or ambiguous paths, and MUST NOT execute a manifest-selected binary before establishing its trusted custody. The manifest pins disjoint workspace, primary gate and installation roots, exact executable and interpreter paths and invocation, and distinct non-root Codex, broker and runner principals. Trusted runtime and configuration paths must be outside agent write custody. Missing components, unchecked confinement, or an unverified installation MUST remain not-ready, and `start` or `serve` MUST refuse rather than fall back to the ordinary broad MCP server or unconstrained execution. This command family is excluded from broad MCP publication. Neither a passing bundle check nor files present on disk proves that the session is bound to the gate; that claim requires verified execution-boundary behavior for the installed version and all exposed capabilities. (Amended APRV-325.1, pending sign-off.)
433
+
260
434
  ### 10.2 Daemon
261
435
 
262
436
  `approvald` watches the backlog folder and the log: validates new/changed envelopes, applies policy, dispatches channel notifications, expires TTLs, samples supervised actions for audit, re-renders projections, and (optionally) polls upstream sources. Loop safety: three consecutive `execution.failed` events for one task escalate to `manual` regardless of policy.
263
437
 
438
+ The reference runtime ships the daemon as a CLI verb, `approval daemon run`, running in the foreground and stopping cleanly on SIGINT or SIGTERM; process supervision is the operator's business at v0.1. Each pass validates the task files' envelopes, appends `envelope.drift` (a `system:` actor) where a file's `state:` contradicts the state the log implies, appends `approval.expired` for live requests whose TTL lapsed, regenerates the queue projection whole, and surfaces escalated tasks. The sweep changes no verdict: TTL is judged at decision time whether or not an expiry event exists, so the sweep exists to make a lapse visible in the log and in every projection built from it. It MUST be idempotent with that lazy judgment and with itself, which implementations get by re-deriving the candidate list from the verified log each pass rather than remembering what they expired. Each pass then performs §6.3's projection write-back: once the events above are appended, every task file whose `state:` still disagrees with the state the log implies is rewritten to say what the log says. The order is fixed, and it is the whole point: the event is appended first and the file is updated second, never the reverse. The rewrite goes through a round-trip writer that changes the envelope's `state:` line and re-emits every other byte of the file verbatim (unknown frontmatter keys per §6, comments, key order, and line endings included), and the result is placed atomically, so a reader sees either the whole previous file or the whole new one. A drift record therefore marks the moment a file was found wrong and repaired; a file that drifts again after repair is one some other writer is contending for, and the repeated records are how that becomes visible. Implementations MUST NOT add an envelope to a file that declares none, and MUST leave the file untouched when the writer refuses the rewrite (frontmatter that does not parse, an `approval:` key that is not a mapping, a failed round-trip self-check), surfacing the refusal instead. Write-back writes files and nothing else: it appends no event of its own, and the log remains the truth. File watching is a latency optimization: a pass re-derives everything from the verified log and re-scans the folder, so an implementation whose watchers fail to attach is slower and never wrong.
439
+
440
+ **Loop safety across a harness adapter.** The escalation above counts failures for one task, and the harness adapters of §10.1 mint a fresh task id for every tool call, so no streak can accrue under one of them. On that path the streak's unit is the tool call and its scope is the session: the harness's identifier for the run of tool calls an agent is making, which every hook event carries and which the adapter already embeds in the task ids it mints. One tool call contributes at most one failure however many classes it declares, and a tool call whose classes disagree counts as a failure. A session identifier the runtime cannot read resolves to one shared bucket rather than to a fresh one, so an unreadable identifier accrues faster and never slower. Implementations MUST accrue a second streak at a scope the party under oversight cannot rotate, the actor identity the adapter runs as, and MUST treat a command as escalated when either streak reaches the threshold: a session identifier is authored by that party, and §11.1 invariant 4 forbids a self-reported value from lowering scrutiny. Both scopes are derived by the runtime from the task id it minted and the identity it runs under, so a caller cannot name the bucket it is counted in. Streaks are consecutive in log order, never in timestamp order. (Amended APRV-145.)
441
+
442
+ **Only side effects accrue.** A streak counts the failed executions of classes that do something. An action whose class is `read` itself, or sits under the `read.*` namespace, is transparent to loop safety in both directions: its failure accrues nothing, and its completion clears nothing either. The predicate is stated as a carve-out of `read.*` rather than as a list of the namespaces that act, so a class an implementation has never seen falls on the strict side by construction; a class the log cannot resolve for an action key counts as side-effecting for the same reason. The class MUST be read from the `execution.started` record the runtime itself wrote (§11.1 invariant 1) rather than from the reporting event, since the party under oversight already chooses the outcome it reports and MUST NOT also choose the bucket it is counted in. The rule follows from what the escalation is for: it stops an agent retrying a side effect that keeps failing, and a search that matches nothing or a listing of a path that is not there has exited non-zero to give its answer. Under a harness adapter the two rules compose, the unit there being the tool call: a tool call whose classes are all reads produces no outcome for any streak, and a mixed one is folded from its side-effecting classes alone. Wherever an implementation refuses or floors on a streak it MUST state the machine-readable code `loop-escalated`, the scope key that tripped, and what clears it. (Amended APRV-280.) The same reading governs what a tripped streak ROUTES. A harness tool call whose classes are all `read.*` MUST be resolved by policy while a floor stands, exactly as it is with none: no request is opened for it, no human is notified, and the implementation MUST state in its verdict that a floor was standing and was not applied to a read. The escalation bounds the harm of an agent retrying a side effect that keeps failing, a read cannot cause that harm, so routing one adds no safety while spending the attention the floor exists to conserve; an operator whose floor has tripped MUST still be able to have the session read. A mixed tool call is routed whole, as one question about its side effects, and the read classes inside it are neither counted nor separately raised. The exemption covers routing alone: a read still clears nothing, so no session can read its way out from under a floor. An implementation that re-checks the floor at a write boundary MUST apply the identical predicate there, so what a floor counts and what it routes cannot drift apart. (Amended APRV-297.)
443
+
444
+ **The completion counterpart.** A harness adapter answers before the tool runs and never observes an exit status, so the `execution.started` it writes is terminal by design and says so with `execution: "harness"` (§10.1). An implementation MAY close that record from a post-execution harness event, appending the ordinary `execution.completed` or `execution.failed` for the same task and action key. It MUST resolve the task and the key from the started record in the verified log rather than from the reporting event, so a report can only close an execution this runtime authorized, and it MUST refuse a report against a start that carries no harness marker. The record carries that marker, a `reported_by` code drawn from a closed set naming which reporter asserted the outcome, and none of the text the tool produced, because §11.1 invariant 3 has no exception for diagnostics. Its `ts` is assigned by the runtime at the write boundary as every gate-typed event's is (§8), and a timestamp on the reporting event is ignored. A report the runtime cannot read as one outcome or the other appends nothing: a failure nobody observed would trip an escalation on noise, and a completion nobody observed would clear one on nothing. The counterpart authorizes nothing and charges no budget, so §11.1 invariant 8 does not bind it and an append that fails leaves the start exactly as it found it. (Amended APRV-145.)
445
+
446
+ **What a self-reported outcome may do.** The report comes from the party under oversight, so §11.1 invariant 4 bounds it in one direction. A reported failure raises scrutiny, accrues the streak, and costs its author capability, which is the safe direction and the only direction the report moves anything by itself. An accrued streak is cleared by an `execution.completed` in the same scope and by nothing else: a wait that times out, a denied tool call, a fresh tool call, a granted approval, a restarted process, and elapsed time all leave the streak where they found it. An agent that reports a completion it did not earn clears its own streak, and gains nothing by that lie which silence would not have given it for free, since a failure it simply omits never accrues at all. The mechanism therefore catches the honest loop and does not catch a liar, in the manner §7 states for the irreversibility floor, and what answers the liar is the class rule plus the record a lie leaves behind in a log nobody can rewrite. (Amended APRV-145.)
447
+
448
+ **What is not an execution.** A streak counts executions that failed, and an execution is a command this runtime authorized and the harness ran. Three things are therefore transparent to it. An expired wait records a withdrawal and never an `execution.failed`, so a gate a human is not answering cannot deepen the floor that routes more questions to them: escalation routes reads to a human, unanswered questions time out, and a timeout that accrued would close that circle. A tool call the adapter refused, whether for an unreadable command, a class the policy reserves, or any other deny, opened no `execution.started`, so the outcome a harness reports for it closes nothing and MUST be refused as naming no delegated execution. A report for a tool call this runtime never started is the same fact and takes the same answer. Only a command that started and exited non-zero counts, and the start is the runtime's own record rather than the reporter's claim (§11.1 invariants 1 and 4). (Amended APRV-287.)
449
+
450
+ **A completion clears the streak wherever the grant was carried.** The completion counterpart resolves the execution it closes from the log rather than from the report, and a grant carried by a later tool call is spent under the requesting tool call's task. An implementation MUST therefore record, on such a start, the tool call that spent it, and MUST accept that name as well as the record's own task when it resolves what a report closes. Without it the completion of a granted retry closes nothing, and a floor whose refusal text promises that a completion clears it does not clear (observed 2026-09-06). The name is derived by the runtime from identifiers it minted, so no report chooses the execution it is closing. (Amended APRV-287.)
451
+
452
+ **The remedy stays manual on this path.** Escalation escalates to `manual` rather than to refused. A harness adapter holds every class of the command it is asked about, so an escalated session's command MUST be routed to the human gate for every SIDE-EFFECTING class that would otherwise have proceeded; a class that already resolves `manual` is unaffected, and a `read.*` class is outside the floor entirely, by the paragraph above (Amended APRV-297.) This is a floor applied after class resolution, in the manner of §7's irreversibility floor, and implementations MUST record in the decision trace that the floor rather than the matched rule determined the outcome. Denying the command instead leaves an escalated session with no way back, because the only thing that clears a streak is an execution that completes. (Amended APRV-145.)
453
+
454
+ **What an operator reads.** `approval status` reports an escalated streak with the scope that accrued it and the count that tripped it, and reports the coverage of the counterpart: how many harness starts carry an outcome and how many do not. Coverage is informational and moves no health verdict and no exit code, for the reason §8 gives for timestamp anomalies, since a control an operator learns to silence is worse than one that reports beside the verdict. `approval doctor` fails its harness check when the adapter is registered for the pre-execution event and not for the post-execution one, which is the configuration in which the escalation above cannot accrue at all. (Amended APRV-145.)
455
+
456
+ **Coverage of what the runtime was never told about.** MCP use is voluntary. An agent connected to a running server may route an action through the gate, or it may simply act, and a gate an agent can route around reports only on the actions routed through it. What keeps the arrangement honest is that side effects leave witnesses this project does not write: git history, `gh`, and (through the optional `observe` of §10.4) an adapter's provider. `approval coverage` reads those witnesses back and joins each observed effect against the verified records, reporting the earliest `task.registered`, `approval.granted`, `execution.started` or `execution.completed` of a matching class inside the effect's window, or `none`; a protected file path takes the content-level verdict of the protected-path guard instead, since bytes outrank time. An effect the log names by the provider reference of §8 takes that record instead of either, reported distinctly and without a window, since an id names one effect where a class inside a span of time names a period; an effect no record names by id takes the class-and-window rule, which is the weaker answer stated as the weaker answer. The verb is informational on exactly the rule the harness-start coverage above follows: it exits 0 with or without gaps, moves no health verdict and no exit code, reads only verified records, and writes nothing anywhere. A source that could not be reached MUST report itself unavailable with a reason rather than as an absence of effects, since a broken tool that read as a clean bill of health would be worse than no report. A green coverage line is a statement about one tier only: an effect made with a credential the agent itself holds leaves no witness this verb is guaranteed to read, and the remedy for that tier is the credential custody of §10.4. (Amended APRV-245. The provider-reference sentence is APRV-251.)
457
+
264
458
  ### 10.3 Channels
265
459
 
266
- Interface: `notify(request) -> delivery_id`, `poll()/webhook() -> decision`. Decisions become log events; channels hold no state. v0.1 ships **cli** (zero-config prompt), **web** (local queue page with grant/reject), and **telegram** (reference push channel: message with declared effects + inline Approve/Reject buttons; callback verified against approver identity). Channel breadth is explicitly out of scope; HumanLayer exists for Slack/email/SMS enterprises.
460
+ Interface: `notify(request) -> delivery_id`, `poll()/webhook() -> decision`. Decisions become log events; channels hold no state. The reference runtime's interactive writer for a channel's transport credential is `approval setup channel <name>`, a separate noun from `approval setup adapter <name>` (§10.4) because the two fill different stores for the reason §4 separates the terms: a channel holds no state, so what it needs is the credential that lets the runtime reach a human, recorded in the environment source map of §5.2, while an adapter holds the credentials a side effect spends and those belong in the vault. (Amended APRV-79.) v0.1 ships **cli** (zero-config prompt), **web** (local queue page with grant/reject), and **telegram** (reference push channel: message with declared effects + inline Approve/Reject buttons; callback verified against approver identity). Channel breadth is explicitly out of scope; HumanLayer exists for Slack/email/SMS enterprises.
461
+
462
+ Where dispatch runs (amended APRV-55). §10.2 gives the daemon the job of dispatching channel notifications. At v0.1 the reference runtime performs that dispatch in the channel listener instead, on every poll cycle, re-deriving the pending set from the verified log each time and sending only what that listener process has not sent yet. The listener already holds the channel credential and the approver identity, dispatch appends nothing, and a network round-trip inside the daemon's pass would couple TTL expiry and write-back to a chat service's availability. This is an implementation placement and not a change to the daemon's stated role: a later build MAY move dispatch into the daemon with no change to any event, projection, or channel interface. A listener's record of what it has already sent is not state in this section's sense, since it is never read as an answer to what is pending; its loss MUST degrade to a re-send (a duplicate in front of the approver), never to a pending request nobody is shown. Pull channels need no dispatch at all: a page that builds its queue from the log per view shows a newly requested action on the next refresh.
463
+
464
+ Dispatch inside the daemon process (amended APRV-110). The move the paragraph above permits has been made: the reference runtime ships `approval up`, one supervised process running §10.2's loop together with every channel the policy configures, with dispatch still on each channel's own cycle. No event, projection, or channel interface changes, and the separate daemon and listener processes remain and behave identically. Supervision MUST be per part, because a channel is a network client and the daemon is not: TTL expiry, write-back and the queue projection MUST NOT become dependent on a chat service being reachable. A channel that fails is restarted after a backoff, and each restart begins with an empty record of what has been sent, so the re-send this section already requires is what a restart produces. A channel whose credential the launch environment does not supply MUST NOT be started, MUST be reported, and MUST NOT prevent the parts that can run from running. A failure of the loop itself stops the channels too, since a prompt derived from a log that does not verify would be a statement to a human about facts the runtime disowns. (Amended APRV-110.)
465
+
466
+ Delivery pacing (amended APRV-216). A push channel MAY put one pending request in front of the approver at a time, preceded by a summary of what is waiting, and MAY accept navigation commands (list, defer, pass over) that reorder what it shows next. Such an order and such a current item are process memory in the sense of the dispatch paragraph above: they are never read as an answer to what is pending, their loss MUST degrade to showing a request again, and a request absent from the approver's screen MUST remain pending, listable, and decidable from any copy already delivered. A navigation command MUST NOT be a decision: a decision is bound to the bytes the approver was shown, and a command names none. (Amended APRV-216.) A channel that re-derives the pending set when it starts or reconnects MAY deliver the requests older than the harness wait of §10.1 as one message naming the count, the classes and the age of the oldest, carrying a single reject-all and no approve. The collapse is this paragraph's pacing rule applied to a queue nobody is waiting on: it is process memory, its loss MUST degrade to showing the requests again, and every collapsed request stays pending, listable, and decidable from any copy already delivered. It carries no approve because it carries no payload, and this section requires the canonical rendering of a manual action's payload in front of an approver before a decision is collected; a rejection authorizes nothing, so it is not bound by that showing. (Amended APRV-287.)
467
+
468
+ One tool call is one question. Where several pending requests share one task and one payload hash they are the classes of one command, and a channel SHOULD present them as one grouped delivery collecting one gesture, with the payload rendered once. The log is unchanged: each member receives its own decision event, per this section's batching rule. (Amended APRV-287.)
469
+
470
+ Review delivered through a channel (amended APRV-299). A channel MAY deliver a retrospective sample for review, under the same pacing and rendering rules this section already states. A review card is not a request and MUST NOT be presented as one: the action has already executed, so the card carries no payload region, offers no approval, accepts no token, and a channel MUST NOT deliver a sample as an approval request. What it carries is the computed and claimed split below, over facts the log already holds: the class the registration declared, what the payload bytes do, the task, the claimed summary, when it ran, and that the runtime allowed it without asking and at what rate. The gestures it collects are a verdict and, optionally, the graded reaction of §5.2, and every one of them MUST reach the log through the same human-only review a terminal calls, recorded against the human identity the runtime was configured with and never against anything the channel received, so that a reaction collected on a channel and one collected at a terminal are the same record; a refusal MUST reach the approver with its own machine-readable code. A reaction that requires the human's own words (§5.2) MUST collect them before anything is appended. The order and the card currently shown are process memory in the sense of the dispatch paragraph above: their loss MUST degrade to showing a sample again, and a sample absent from the approver's screen MUST remain in the sampled-audit backlog of §9, listable, and reviewable at any other surface. Nothing a channel does may close a sample other than by recording a human's review, because a supervision backlog a transport can empty measures nothing. (Amended APRV-299.)
471
+
472
+ Every displayed field is one of two kinds and MUST be visibly distinguished: **computed** (derived by the runtime from the log, policy, or payload bytes: class resolution, budget state, attestation status, payload hash, chain position) and **claimed** (authored by the requesting agent: summaries, estimates, rationale, confidence). Rendering claimed fields with the visual authority of computed fields misrepresents the verification boundary to the approver and is a conformance failure for a channel.
473
+
474
+ For a `manual` action, a channel MUST present the canonical rendering of the payload described in §9, verbatim, and MUST NOT assemble a reading of its own. Claimed material MAY be appended around that block and MUST be visibly separated from it and labelled as claimed. A channel that shortens the payload before presenting it has no canonical rendering to show and MUST say so rather than giving a partial reading the block's authority. Two conforming channels therefore put the same text in front of two approvers for the same bytes, and the `display_hash` on `approval.requested` names that text. Renderer determinism and the closed-field-set property are conformance requirements, tested as such. (Amended APRV-119.)
475
+
476
+ Channels MAY present multiple pending requests as a batch and collect one human gesture over the set, and SHOULD for high-volume `record.*` classes. The log never batches: each granted or rejected request receives its own `approval.granted` or `approval.rejected` event carrying the batch's channel delivery id in its payload, so audit granularity survives ergonomic grouping. The id is carried in the payload field `batch_delivery_id`. Earlier v0.1 builds encoded it in the first line of the payload's `note` field as `batch_delivery_id=<id>`; those logs are append-only, so readers MUST accept both encodings for the life of v0.1, preferring the first-class field. A batch MUST NOT mix `manual` classes with differing payload-display requirements in a way that hides any full payload behind the fold of another.
477
+
478
+ **Attestation collected through a channel.** Deciding a request is a tap. Deciding which rules are in force was still a terminal session, because attestation is human-only (§11) and identity is config-declared, so an agent running the amendment ceremony would be claiming the human identity. A channel closes that gap without moving the boundary. A party that has prepared a policy edit appends `policy.proposed`, a channel renders it as an ordinary `manual` prompt, and the approver's tap appends the `policy.updated` attestation under the human identity the listener holds, exactly as a grant lands today. The following four paragraphs amend §8, §10.1, this section, and §11 respectively. (Amended APRV-109.)
479
+
480
+ *Amends §8.* The v0.1 event set gains `policy.proposed` and `policy.declined`, the seventh and eighth additions to the draft set of sixteen; readers of a v0.1 log may encounter them and a verifier written against the draft enum MUST be updated to accept them. Both are gate-typed, so both take their `ts` from the runtime at the write boundary. `policy.proposed` carries a `human:` or `agent:` actor, since either may prepare an amendment, and MUST NOT carry `system:`, because the runtime proposes no policy of its own. Its payload names the policy file, the SHA-256 of its exact bytes, the semantic diff summary, and the load advisory. `policy.declined` carries a `human:` actor for the reason `approval.rejected` does, names the proposed hash, and names the `policy.proposed` sequence number it answers. The acceptance is `policy.updated`, which already existed and remains the only event an attestation is; when it answers a prompt it MAY carry that same `proposed_seq`. That a cited sequence number is an open proposal in this log is a rule the runtime enforces, since a schema sees one record.
481
+
482
+ *Amends §10.1.* The CLI block gains no verb. `approval policy amend` accepts an `agent:` identity, under which it prepares the edit and the diff as it does today, appends the proposal, waits for the answer, and completes the git ceremony only once the attestation has landed, citing that sequence number in the commit as it always has. Under a human identity the verb is unchanged. Every ending short of a tap attests nothing and commits nothing: no configured channel, a decline, a lapsed deadline, a superseded prompt, and a diff too large to render each refuse with their own machine-readable code, and the policy edit stays in the working tree.
483
+
484
+ The ceremony owns its own git preconditions. It fetches the remote, refuses unless the remote's policy bytes are the attested baseline the edit was written against and the remote's log is a prefix of the working log, runs the policy's declared expectation set against the amended file where one exists, and only then attests. Each of those refusals is machine-readable, distinct, and reached before the attestation, so a stopped ceremony leaves the policy edit as a working-tree change and nothing else. The commit is assembled on the remote's tip without a checkout and pushed by refspec, so the human ends the ceremony on the branch they started on with the working tree they started with. (Amended APRV-203.)
485
+
486
+ *Amends §10.3.* An attestation prompt is an ordinary channel request and inherits every rule of this section, including the computed and claimed split and the verbatim payload presentation of §10.4, where the payload is the policy text itself. Two computed fields are added for it: the semantic diff of what the proposed bytes change about class resolution, in before and after form, and the advisory saying whether those bytes load. The hash, the diff, and the advisory MUST all be derived by the runtime from the bytes; a proposing caller supplies none of them, so a proposal cannot show an approver one story and attest a different file. A prompt carrying only a hash asks a human to sign for sixty-four characters and is a conformance failure. A diff a channel cannot show whole MUST be refused rather than truncated, naming the terminal path as the repair: a prompt showing two thirds of a policy change collects a signature for the third it did not show. A prompt whose policy bytes have changed since it was rendered MUST leave the queue rather than be shown, and the answer to such a prompt MUST be refused, so the hash the approver was shown is the hash that gets attested or nothing is.
487
+
488
+ *Amends §11.* The trust boundary of this section is unchanged. Human identity is config-declared, the boundary is the local machine, and attestation proves that someone with local control signed off rather than who. What this amendment changes is where that person's act is collected: the listener holds the human identity for an attestation the way it already holds it for a grant, and the same reasoning that makes a tapped grant a human decision makes a tapped attestation one. An agent MUST NOT append `policy.updated` or `policy.declined`, in the schema and in the runtime both.
267
489
 
268
490
  ### 10.4 Adapters and hard enforcement
269
491
 
270
- Adapters (e.g. `adapter-email`, `adapter-gcal`) hold the actual credentials in an encrypted vault and MUST require a valid, unexpired, single-use execution token bound to the action's `idempotency_key`. This is the hard boundary: an agent that bypasses the CLI still cannot send, spend, or delete, because the credentials only answer to tokens. (Same architectural intuition as mission-control's vault + "agents cannot modify security settings.")
492
+ Adapters (e.g. `adapter-email`, `adapter-agentmail`, `adapter-gcal`) hold the actual credentials in an encrypted vault and MUST execute through the runtime's verified policy and execution boundary. A manual action, including an irreversible action whose policy does not explicitly permit otherwise and a selected supervised-live action, MUST require a valid, unexpired, single-use execution token bound to its `idempotency_key`. A policy-authorized autonomous, supervised-retro (including the supervised alias), or non-selected supervised-live action does not require a token; the attested policy, registered payload binding, budgets, execution recording and remaining floors still govern it. No implementation may fabricate a human grant for that path. The boundary is the adapter contract, and bypassing the CLI confers no authority over credentials held exclusively behind it. (Same architectural intuition as mission-control's vault + "agents cannot modify security settings.") An adapter MAY additionally implement an optional `observe(window)` that reports what its provider recorded happening, which is read-only, spends no token and is called outside any grant window, so that §10.1's coverage report can witness an adapter-backed class without any authority being exercised. (Amended APRV-245.) (Amended APRV-317, pending sign-off.)
493
+
494
+ An execution token is bound to the request, its `idempotency_key`, AND its `payload_hash`. Adapters and `approval run` MUST recompute the hash of the payload they are about to execute and MUST refuse, with a distinct machine-readable reason (`payload-mismatch`), when it differs from the hash the grant recorded. A grant therefore approves specific bytes. Changing the payload after grant requires a new request.
495
+
496
+ **Token delivery.** The mint site and the spend site are different processes, and by default the only transport between them is a human: the raw token is printed once on the granting surface and carried to the spender. `defaults.token_delivery` (§5.2) names the alternative. Under `sealed`, `approval request` mints an ephemeral X25519 keypair per request, writes the private half to a 0600 file outside the log and outside version control, and publishes the public half on `approval.requested`; on grant the mint site seals the raw token to that key — X25519 ECDH to a fresh ephemeral sender key, HKDF-SHA-256 to a 32-byte key, AES-256-GCM with a fresh nonce, the key derivation bound to the scheme and the action key so a seal cannot be replayed as another action's — and records the ciphertext as `token_sealed`. `approval wait` and the MCP `wait` tool MAY then return the raw token to the requesting process, and `approval run` MAY open it in place of a `--token` argument. The raw token is STILL printed once on the granting surface: sealing adds a reader and removes none. Implementations MUST unlink the private key when the token is consumed, when the parent request expires, and when the grant is revoked — the three deaths of the authorization it addresses — because a key that outlived its grant is a standing decryption capability for a ciphertext the log keeps forever.
497
+
498
+ Nothing about this weakens the boundary above. The keypair ADDRESSES; it does not AUTHORIZE. A token still exists only after a human grant, still binds to the exact payload bytes, and is still single-use, and a keypair cannot mint one, forge a grant, alter the bound payload, or reuse a spent one. What changes is who can READ a minted token: the requesting process, instead of the human's clipboard, which is where the token was travelling anyway. What is lost, stated plainly, is the marginal ability to approve on a phone and then withhold delivery. The residual risk, stated as plainly: someone who steals the private-key file AND reads the log can recover an unspent token inside its TTL. That window equals the terminal-paste window it replaces and closes at consume or at TTL. The default is `manual` because the ciphertext lives in a public, permanent log, and an operator should reach that trade deliberately. (Amended APRV-105.)
499
+ Recomputation is unconditional, on every execute path, and a caller-supplied hash is a claim to be CHECKED and never a value to be used in place of it. An executor that accepts "here is the hash of what I am running" has no binding at all: the party under oversight is then the one asserting what it is about to do, which is the assertion the binding exists to replace. `approval run`'s payload is §6.2's argv array and cwd, so it recomputes from the argv and cwd it is about to spawn and refuses `payload-mismatch` when a supplied `--payload-hash` differs, before the child is spawned and before anything is appended. An action whose payload is content rather than an argv is executed through the adapter contract above, which hashes the bytes it holds; it is not executed by spawning a command and presenting the content's hash for it. The same rule binds the paths with no grant: a `supervised` or `autonomous` execution is checked against the `payload_hash` its registration declared (§6.2), and an undeclared or differing hash refuses identically. Every `execution.started` records the hash that was recomputed, so the log states which bytes ran and an operator holding the payload can reproduce the value. The bytes themselves are not recorded there: an argv carries whatever the caller put on it, and §11.1's third invariant keeps such material in the log as a hash. (Amended APRV-140.)
500
+
501
+ An executor that spawns a child MUST construct that child's environment rather than inherit one. It MUST withhold every variable whose name falls under the credential-bearing prefixes the runtime maintains (`APPROVAL_`, `TELEGRAM_`, `VAULT_`, `AGENTMAIL_`), less the runtime's own declared non-secret names, and MUST withhold the variable named by `vault.passphrase_env` wherever that name falls. It MUST pass the credentials the adapter serving the action's class declared in `requiredCredentials`, and MUST NOT accept any other instruction, from a flag or from a payload, about which variables to pass. Every other variable passes through unchanged. The `execution.started` record MUST carry `env_stripped`, the count of variables withheld, and MUST NOT carry their names or their values. This is a credential scrub and not a sandbox: the child retains the network and filesystem capability of the process that spawned it. (Amended APRV-205. The `AGENTMAIL_` prefix is APRV-224.)
502
+
503
+ A provider whose keys carry per-permission booleans lets that boundary be made hard on the provider's own side, and `adapter-agentmail` is where this specification says how. AgentMail issues keys on which `draft_create`, `draft_update` and `draft_read` are separate permissions from `draft_send` and `message_send`, so a deployment issues two: the agent holds a key carrying the composing permissions and no send permission at all, and the vault holds a key carrying the send permissions, read only inside the verified-token window the adapter contract opens. Without that split, any key sitting in the agent's environment sends without consulting this runtime, which is the case §11 already states plainly as undefended: an agent with direct credential access outside adapters. The provider publishes no pre-send webhook, so no other place exists where a send in flight could be held for a human, and the permission boolean is the whole of the enforcement; an implementation MUST NOT claim a stronger property than the key it was handed supports. The second thing this adapter settles is how a grant binds a REMOTE MUTABLE object, which follows from §6.2 rather than adding to it. A draft lives on the provider's side and the agent may rewrite it after a human has read it, so an approval of its identifier would be an approval of whatever it holds at send time. The grant therefore binds the bytes fetched at request time: the payload carries the inbox and draft identifiers alongside the recipients, the subject and the text as they stood when the request was opened, and the payload hash covers those bytes like any other. Before it sends, the adapter re-fetches the object, compares those same fields, and refuses with its own machine-readable reason (`agentmail-draft-drifted`) when any of them differ. That reason is distinct from `payload-mismatch` because the fact and the repair are both different: the bytes the caller presented are the approved ones and the far side moved underneath them, so the grant stands and a fresh request is owed to the human rather than to the file. The refusal names WHICH fields differ and never what they now hold, since a refusal is written to a log that a human who approved none of the new text will read, and quoting it there would publish unapproved content through the refusal path. (Amended APRV-224.) That comparison happens twice, and the first of the two runs BEFORE the token is consumed, as the adapter's pre-token check. A drift found there refuses with nothing appended and the grant intact, so the same token sends once the approved text is restored; a drift found by the second comparison, which runs inside the consumed-token window immediately before the send, is an `execution.started` followed by an `execution.failed`, which is the honest record of a window that was open when the far side moved. Running the comparison only in the second position spends a human's single-use grant to discover a send that never happened (found on a live inbox, 2026-09-06), and a refusal that costs another tap teaches operators to stop checking, which is the one lesson this design cannot afford to teach. The pre-spend read is performed with the SENDING key from the vault, resolved in the pre-token credential window described below, and never with a key the calling agent holds in its own environment. The reason is invariant 4 of §11.1: the comparison decides whether the agent's own edit counts as drift, so a comparison whose input that agent supplies is scrutiny the party under oversight controls. Nothing new is opened to make this possible, since the vault already answers before the spend for an adapter's declared credentials, inside the presented-phase grant minted only when the caller's token matches the digest the grant recorded. A pre-token check is offered only bytes the log binds to the action (the grant's `payload_hash` on the manual path, the registered declaration's off it); anything else is `payload-mismatch`, which the runtime refuses in its own words with nothing appended and the token still live. (Amended APRV-276.)
504
+
505
+ The reference runtime gives that boundary a definite shape. An adapter implements one method, `act`, over two things: the payload bound by the grant or registered declaration, and a credential provider scoped to that call. The runtime's adapter contract owns everything around it, in a fixed order: recompute the payload hash, verify the applicable policy authority and consume a token where required, append `execution.started`, call `act`, append `execution.completed` or `execution.failed`. An adapter cannot skip a step, because it never holds the sequence. Credentials reach `act` only inside the runtime-authorized execution window, and the provider refuses every request made after `act` returns, so an adapter that keeps its reference gets a refusal rather than a secret. Whatever the adapter reports back (its own failure code, message, and detail) is scanned for the credential values it was handed and redacted before the runtime records or returns any of it, which makes §11.1's third invariant a mechanism at this boundary rather than a convention. Adapters written elsewhere are held to the same sequence by the conformance suite that ships with the contract, in the way §10.3's channels are held to their display rules. (Amended APRV-67.) References to token consumption and a grant window in this section describe paths that require a grant. The no-token path follows the same payload, recording, idempotency, redaction and credential-window closure rules; it carries no human-decision claim. The existing environment-source-map fallback remains strictly token-backed and MUST NOT open for a policy-authorized no-token action, which uses only credential configuration already established in the process environment by the operator. (Amended APRV-317, pending sign-off.)
506
+
507
+ An adapter declares the credential names it cannot act without, and those names MUST resolve before the token is consumed: a credential the runtime cannot reach refuses `credential-unavailable` with nothing appended and the grant intact, so a configuration fault costs no authority and the same token executes once the credential is reachable. The side effect's own ordering is unchanged, and deliberately so: the token is still consumed and `execution.started` still appended before `act` is called, because that ordering is what makes a crash between the two an ambiguity the log can show (§10.4 above, on custody). Inside that consumed-token window, and only there, an implementation MAY resolve the vault passphrase the policy names from the instance's own environment source map (§5.2) when the ambient environment does not carry it, which narrows that file's "nothing loads it implicitly" rule to exactly one caller: the authority is the token, a human approved this specific action, and the resolved value goes to the vault and reaches no argv, no log, no message, and no other verb. Every other verb still sees only the environment a human established. (Amended APRV-169 and APRV-168.) An adapter MAY also declare a pre-token check, which the runtime calls in that same pre-spend position, after the declared credentials resolve and before the token is consumed. It is read-only, it is offered only bytes the log binds to the action, and its refusal is reported under one stable code (`adapter-precheck-refused`) carrying the adapter's own reason, with nothing appended and the same token spendable once the named condition is repaired. It generalises what `credential-unavailable` established: a condition that makes the side effect impossible, and that the runtime can establish without attempting it, MUST NOT cost a human's single-use grant to discover. It never stands in for a check inside the window, since the far side can move between the two and the check that binds the bytes actually sent is the later one, and a check that raises rather than answers is treated as a refusal, because a check that could not be performed is not a check that passed. (Amended APRV-276.)
508
+
509
+ Which outcome event closes the sequence is decided by WHERE the sequence stopped, and the boundary is the moment `act` is invoked. Everything up to and including that invocation is the runtime's own preparation and it can speak for it, so a failure there is `execution.failed`: nothing was attempted. From the invocation onward the call belongs to the far side, so an exception there is `execution.indeterminate`: the provider may or may not have committed and this runtime cannot tell. An adapter that RETURNS a failure is `execution.failed` too, because the provider answered and the answer was no. Implementations MUST NOT record an unknown outcome as a failure. A failure is a fact, an unknown outcome is the absence of one, and a caller that reads the second as the first retries a side effect that may already have happened, which idempotency keys only partly cover: a second request under a new key is legal. The distinction is positional rather than a judgment about the error, so it is reproducible by any implementation.
510
+
511
+ Indeterminate is a custody state, not a result. The consumption is burned: the token stays spent, the `idempotency_key` stays used, the budget stays charged. Refunding an attempt whose outcome is unknown would be the runtime deciding the effect did not happen, which is the one thing nobody knows. A re-run of the key MUST be refused with its own machine-readable reason (`execution-indeterminate`), distinct from "already executed", because the fact and the repair are both different. The record carries a closed reason code and no exception text: an error message is where a credential rides into an append-only log with a plausible excuse, and §11.1's third invariant has no exception for diagnostics. The executing caller still receives the message, redacted.
512
+
513
+ It resolves only through an explicit reconciliation, invoked by a person and never by the daemon, fed by evidence from the relying party rather than from this runtime's own log. Recovery here is never evidence that the provider did not execute. The resolution is appended as its own `execution.reconciled` record NAMING the indeterminate one, which is never rewritten, so the original observation survives its resolution and an auditor sees both the doubt and its answer. Resolving as `not-executed` is recorded distinctly from resolving as `executed`, and re-opens the possibility of the EFFECT rather than of the action: an `idempotency_key` is the global identity of one side effect (§6.2) and a used one stays used, so a still-wanted effect is declared as a fresh action and requested again. (Amended APRV-120.)
514
+
515
+ The reference vault is the storage half of the same sentence. Named credentials live in one file beside the log, `.approval/vault.enc`, holding a JSON map of name to credential encrypted with AES-256-GCM under a key derived by scrypt from an operator passphrase. The passphrase is read from the environment variable the policy names in `vault.passphrase_env` (§5.2), so the policy an agent may read carries a variable name and the value lives outside the repository. The file records its own format version and KDF parameters, so a future scheme is a migration rather than a reinterpretation of old bytes, and every write re-encrypts the whole map under a fresh nonce and lands atomically. The write path is human-only (`approval vault set | list | remove`, identity resolved exactly as `policy attest` resolves it), `list` reports names and never values, and there is deliberately no verb that prints a credential: the value's only sanctioned journey is from the vault into an adapter's `act`, through a credential provider the contract above scopes to the runtime-authorized execution window described above. What the vault defends is credentials at rest and casual reads by an agent with file access, since the ciphertext hides the names as well as the values. What it does not defend, stated as plainly as §11 states the rest, is a compromised host or an agent that can read the passphrase variable; such an agent decrypts the file directly and needs no adapter. The vault raises the cost of a credential leak from reading a file to owning the session, and claims nothing beyond that. The reference runtime's interactive writer for adapter credentials is `approval setup adapter <name>`, driven by the credential manifest the adapter declares: it asks for each named value, validates every answer with the adapter's own rules, stores the set in the vault, and offers to verify the result against the service without sending anything. (Amended APRV-68. The `approval setup adapter <name>` sentence is APRV-78.) (Amended APRV-317, pending sign-off.)
516
+
517
+ For `manual` actions, channels MUST present the full payload or a faithful rendering of it, clearly delineated from any agent-written summary, before collecting a decision.
518
+
519
+ **Adapter eligibility and supervised-live intake.** Before resolving credentials or running a provider-backed precheck, an adapter MUST verify the principal, registered action and exact payload, current applicable authority, execution custody and idempotency. A successful eligibility preview consumes no token and appends no execution start; budget refusals retain the ordinary budget audit record and compare-and-append race handling. For a direct no-token supervised-live action with no prior approval cycle, the adapter MUST use the existing request intake and daemon draw, deriving request metadata from verified registration and supplying the complete bound payload for retention and display. A selected or unavailable draw follows the ordinary pending human-approval path; it does not count as an unselected verdict. A prior cycle, including a pending, rejected or expired request, MUST NOT be rerolled to avoid that decision. An unselected verdict MUST remain bound to the same attested policy digest through eligibility and final start. After precheck, the execution path MUST revalidate authority, payload, policy binding and concurrency before recording a start or acting. Source-map credential fallback remains restricted to verified token-backed execution. (Amended APRV-317, pending sign-off.)
271
520
 
272
521
  ### 10.5 MCP wrapper (optional)
273
522
 
274
- A thin MCP server exposing the same verbs as tools (`request_approval`, `wait_decision`, `get_queue`, …) for clients where MCP is more ergonomic than shelling out. It shares the CLI's code paths; the [MCP tasks extension](https://modelcontextprotocol.io) MAY be mapped onto `awaiting` when client support stabilizes, and A2A's `input-required` maps cleanly for agent-to-agent deployments.
523
+ A thin MCP server exposing the same verbs as tools for clients where MCP is more ergonomic than shelling out. It shares the CLI's code paths.
524
+
525
+ What shipped is `approval mcp serve`: a foreground server speaking MCP over stdio, running as one `agent:<id>` the operator fixes when they start it. The tool surface is the agent-facing half of the verb registry, one tool per verb (`register`, `request`, `wait`, `run`, `queue`, `status`, `log_verify`, and the rest). Human-only verbs are absent by design, which is §11's argument applied to a transport: the agent is the untrusted policy and the human is the trusted overseer, an MCP client is an agent's harness, so publishing `grant` on it would hand the untrusted policy the overseer's pen. Five agent-facing verbs are withheld as well, for transport reasons rather than authority reasons: `consume`, which is internal plumbing that `run` wraps; `hook claude-code` / `hook cursor` / `hook codex`, which each read their event from the stdin this transport already owns; and `log follow`, whose unbounded foreground stream cannot be represented as one finite tool result and would occupy the serialized call queue indefinitely. A conformance reader building the tool list from this section takes it to be the registry filtered by `human_only`, minus those five. Tool descriptions and input schemas are derived from the same registry `approval instructions --schemas` prints, with `--as` deleted from every published schema, so a caller cannot name an identity; the server's own identity is appended last to every argv, so it wins even where one arrives by another route. A tool call builds an argv and invokes the function the CLI dispatches to, so a refusal is the CLI's refusal, returned as a tool result carrying `{"error":{"code","message"}}` rather than as a protocol error. The [MCP tasks extension](https://modelcontextprotocol.io) and elicitation MAY be mapped onto `awaiting` when client support stabilizes; that remains post-v1, and until then `wait` blocks and answers. A2A's `input-required` maps cleanly for agent-to-agent deployments. Under `token_delivery: sealed` (§10.4) the `wait` tool MAY return the raw execution token in its result, which retires the earlier reading that a token an agent could fetch would be a grant an agent could give itself. Fetching a minted token is not minting one: the token exists only because a human granted it, binds to the payload bytes, and is single-use, and the tool returns it only to the process that opened the request and holds the private key. What it replaces is a human copying that same value out of a terminal. `grant` remains absent from this surface, and that is the line that matters — the untrusted policy still cannot hold the overseer's pen. (Amended APRV-88, APRV-103, APRV-105.) (Amended APRV-322, pending sign-off.)
526
+
527
+ ### 10.6 Experimental native Codex hook adapter (Amended APRV-313)
528
+
529
+ The opt-in `approval hook codex` adapter synchronously reads native `PreToolUse` and `PostToolUse` events under the exact `Bash|apply_patch` matcher. Direct `apply_patch` may enter the verified gate, policy, budgets, approval wait and grant-consumption paths. Current native `Bash` pre-execution input MUST be explicitly denied before gate-open, self-delivery, grant reuse, registration or execution start, because session cwd does not establish the effective per-call execution directory. Bash MUST remain matched so a shell-dispatched patch cannot bypass that refusal. Enabling Bash requires named-version native evidence that the effective directory is bound to the exact input and stable session and tool-call identity; a caller assertion or native approval does not substitute. See `docs/codex-hook.md` for the observed-version evidence. The default gate wait is nine minutes and the example native hook timeout is ten minutes.
530
+
531
+ A direct patch approval MUST bind the complete raw patch, exact tool identity, stable identifiers and execution directory. Every Add, Update, Delete and Move source and destination MUST pass the existing scope and protected-path checks; an unreadable or ambiguous patch is refused. An explicit allow MUST echo the exact approved command through `updatedInput`; a denial MUST never rewrite input. Codex sandbox permissions remain independent. Codex hook configuration (`.codex/hooks.json`, `.codex/hooks/` and `.codex/config.toml`) is a gate organ under §5.2, and inert examples MUST remain outside those live paths.
532
+
533
+ Codex has its own `codex` harness provenance. A post-execution report MUST correlate with a verified delegated start for that Codex call and use `reported_by: post-tool-use`. Only a structured outcome verified against a named Codex version may close the matched start. The runtime MUST NOT infer an outcome from an event name or output text, and tool output MUST NOT enter the approval log. Unknown, interrupted or unfinished outcomes remain open with a diagnostic.
534
+
535
+ Operator documentation MUST distinguish direct-patch coverage from Bash refusal, native crash, timeout and malformed-response fail-open behavior, input rewriting by another hook that invalidates the approved binding, and unverified desktop and phone behavior. Doctor MUST distinguish configuration presence from trusted or demonstrated operation. This experimental adapter provides no universal enforcement or strict Claude parity. (Amended APRV-313.)
275
536
 
276
537
  ## 11. Security and control model
277
538
 
@@ -285,19 +546,196 @@ approval.md operationalizes, at consumer scale, the structure of trusted monitor
285
546
 
286
547
  **Defended:** honest-mistake side effects, prompt-injected instructions that try to escalate side effects silently, retries double-executing (idempotency keys), quiet budget creep, after-the-fact disputes about who approved what, tampering with history.
287
548
 
288
- **Not defended, stated plainly:** an agent with direct credential access outside adapters; a compromised host; deceptive *content* inside an approved action (you approve "send email," the runtime does not verify the prose is honest, though channels SHOULD display full content for `manual` actions); collusion between agent and a careless approver; and this is engineering informed by control research, offering no guarantees against a schemer that control protocols proper are designed to stress-test. Keep `manual` floors on irreversible classes.
549
+ **Not defended, stated plainly:** an agent with direct credential access outside adapters; a compromised host; collusion between agent and a careless approver; and this is engineering informed by control research, offering no guarantees against a schemer that control protocols proper are designed to stress-test. Keep `manual` floors on irreversible classes. Human identity in v0.1 is config-declared (an environment variable or flag); the trust boundary is the local machine, and anyone who can set that configuration and write to the log is inside it. Attestation therefore proves that *someone with local control* signed off, not *who* — cryptographic identity is future work, not a v0.1 claim.
550
+
551
+ Configuration is therefore never read implicitly from the working tree. Human identity is config-declared, so a file in the repository that any command loaded into its own environment would move the boundary from "anyone with local control" to "anyone who can write a file in the working tree", which every agent working in that tree can do: such an agent could attest a policy, grant its own request, and store a credential, as the human, on every invocation. The environment map of §5.2 is consequently inert. One verb reads it, `approval env`, and its output is an export block a human evaluates in their own shell, so the environment a gate operation runs under is established by the person who launched the process and not by the contents of a directory. The same reasoning applies to any future configuration that could set identity, a credential, or a variable naming one. (Amended APRV-73.)
552
+
553
+ Deceptive content inside an approved action is defended to this extent: the approver sees, and the token binds to, the exact payload bytes. What remains undefended is persuasion about whether those bytes should be approved, and any consequence the approver fails to model from bytes they did see.
554
+
555
+ Fields authored by the party under oversight (`route.confidence`, summaries, cost estimates) MAY raise scrutiny, inform display, or feed retrospective analysis. They MUST NOT reduce sampling probability, downgrade a resolved autonomy level, or shortcut any refusal path. Scrutiny only ratchets upward on self-report.
556
+
557
+ Disclosure duty binds the informed party. An agent that knows information material to an approval decision and omits it from the request has violated policy even where the action itself was permitted. Materiality is judged as a reasonable approver would judge it. Undeclared side effects are the mechanical case, caught at the adapter boundary; undisclosed context is the same violation in a form only audit can catch, which is one of the things audit is for.
558
+
559
+ This system makes actions, policies, and decisions common knowledge: a shared record both parties can read and neither can silently rewrite. It makes neither party's mind legible to the other. Agent reasoning is unverifiable from outputs; human intent exceeds any policy file. approval.md shrinks the information asymmetry where artifacts can carry it and claims nothing about interiority on either side.
560
+
561
+ The gate protects two things: the world from your agents' actions, and your mind from your agents' writes. A system of record is an extended mind; writes to it are cheap to make and expensive to un-believe once relied upon. Policies that set reversible `record.*` classes to `manual` are exercising reversibility-preservation over epistemic state, and the irreversibility floor (§7) remains a minimum, never a statement that manual is reserved for the irreversible.
562
+
563
+ ### 11.1 Global invariants
564
+
565
+ The following hold across every surface of the runtime. They are implicit acceptance criteria for every future task: a change that violates one is a defect regardless of what its own task asked for, and each is pinned by the test file cited.
566
+
567
+ 1. **Enforcement paths read only verified records.** Gate decisions are computed from log state that has passed chain verification, never from unverified or partially read input (`tests/state.test.ts`). *Scope note:* the attestation-required list of §5.2 (request intake, grant recording, token minting) enumerates operations rather than modules, and the harness hooks of §14 (`approval hook claude-code`, `approval hook cursor`) are one of those surfaces: a hook verdict that lets a command run with no human in the loop is a gate decision, so it MUST verify policy attestation and loop-escalation against the verified log before it allows, and MUST fail closed when it cannot reach the log (`tests/cli-hook.test.ts`). (Amended APRV-139.) *Scope note:* reading only verified records means a surface can hold a record the view it has just read does not carry, and a harness adapter is where that bites: it appends its requests, re-reads, and after a log synchronisation or a daemon restart the verified view can be behind its own writes. The absence of a record for an action key the surface itself appended, or found pending in its own earlier verified read, MUST NOT be read as a terminal state, because a log is append-only and a request that existed does not stop existing. The response is to keep waiting inside the bound the surface already has and to report that the view lags; an implementation MUST NOT read unverified bytes to resolve it, and MUST NOT allow on a record the verified chain does not carry (`tests/cli-hook.test.ts`). (Amended APRV-294.)
568
+ 2. **Gate-typed events never accept caller timestamps.** `ts` on gate-typed events is assigned by the runtime at the write boundary; a caller-supplied value is refused (`tests/clock.test.ts`).
569
+ 3. **Raw secrets never appear in the log.** What appears is a hash, or ciphertext sealed to a recipient key the log does not hold. Execution tokens and binding material are logged as hashes; under sealed token delivery (§10.4) `approval.granted` additionally carries the token encrypted to the requester's ephemeral public key, and the private half of that key lives outside the log and outside version control. A raw-token scan over written logs finds nothing under either delivery mode, and the sealed field does not open without the private key (`tests/token.test.ts`, `tests/binding.test.ts`, `tests/sealed-delivery.test.ts`). The rewording relaxes nothing: an attacker with a copy of the log — the one artifact this system deliberately makes durable, copyable and auditable — still gains no ability to execute. (Amended APRV-105.)
570
+ 4. **Self-reported fields never reduce scrutiny.** Values authored by the party under oversight may raise scrutiny and never lower it (`tests/ratchet.test.ts`). *Scope note:* a self-reported field that CONTROLS something is therefore bounded twice — by what it can move and by what it can be trusted to prove. `reversible` is the worked example: it may raise a class to `manual` through the §7 floor, it may choose between two reconciliation obligations that both exist (§5.2), and it may never remove an obligation, lower an autonomy, or stand as evidence that an action can be undone. A control keyed to such a field is a floor, not a proof, and implementations MUST say so where they document it (`tests/autonomy-split.test.ts`). (Amended APRV-127.) *Scope note:* the open window of §5.2 lowers the scrutiny every hook-gated call receives, and it does not offend this invariant, because its author is the human, who is not the party under oversight. The agent's own fields are unchanged by it: an agent cannot open one (the ceremony classifies `policy.core`, needs a terminal, and has no flag that answers for a person), and inside one nothing an agent reports moves what stays denied (`tests/gate-window.test.ts`, `tests/cli-gate-window.test.ts`). (Amended APRV-214.) *Scope note:* §7's explicit irreversible-action permission comes from the attested operator policy and never from a self-reported action field. An agent cannot create this policy permission through action metadata; a false reversibility declaration remains untrusted and proves nothing. The existing attestation and human-only controls still bind. (Amended APRV-317, pending sign-off.)
571
+ 5. **Every check-then-append passes through compare-and-append.** No path reads a decision-relevant log state and appends on it without the atomic head check that makes the pair safe under concurrency (`tests/concurrency.test.ts`, `tests/log.test.ts`).
572
+ 6. **Refusals are machine-readable and distinct, and every code union is pinned by a test.** Each refusal path returns its own stable code, and the unions are frozen public API (`tests/gate.test.ts`, `tests/token.test.ts`, `tests/execute.test.ts`, `tests/log.test.ts`, for the open window's verbs `tests/gate-window.test.ts`, for the log-anchoring check `tests/log-anchor.test.ts`, for the human-signed checkpoint check `tests/log-checkpoint.test.ts`, and for the audit verbs `tests/audit.test.ts`). (Amended APRV-214. The anchoring union is APRV-219, the checkpoint union APRV-220.) (Amended APRV-237.)
573
+ 7. **Configuration is never loaded implicitly from the working tree.** No verb reads a working-directory file into its own environment; the environment a gate operation runs under is established by the human who launched the process (`tests/cli-env.test.ts`). (Amended APRV-73.)
574
+ 8. **A gate verdict whose event cannot be appended is a refusal.** No surface returns `proceed`, prints an allow, or hands back a token before the event recording that verdict has been appended. When the append fails, for any reason the writer can give (lock contention, a disk error, a schema refusal at the write boundary), the caller receives a refusal carrying the stable code `append-failed` and the writer's own error, never a success whose record is missing. Nothing may soften a verdict because the log would not take it: a budget refusal whose `budget.exceeded` could not be written is still a budget refusal. The rule binds the paths that write nothing too, by naming where their record is: an autonomous admission appends no `approval.*` event (§6.3), so `execution.started` is the whole of its accounting, and a start that cannot be recorded does not happen (`tests/evidence-append.test.ts`). (Amended APRV-123.) *Scope note:* an allow under the open window of §5.2 is such a verdict: the hook prints it only after the `gate.bypassed` recording it has been appended, and an append that fails denies (`tests/cli-hook.test.ts`). (Amended APRV-214.)
575
+ 9. **`human-only` classes are inert to agents.** A class the policy declares `human-only` (§5.2) is performed by a person outside agent execution, so no verb of the runtime may mint authority for one and no verb may withdraw authority that cannot exist. Request, grant, reject, revoke, token issue, token spend, harness-grant consumption, harness execution recording, and `approval run` all refuse it, none of them appends a record on the refused path, and every one of them refuses with the same stable code, `class-human-only`, distinct from every rejection and from every other member of its union (`tests/human-only.test.ts`). *Scope note:* the harness hooks of §14 are one of these surfaces on the reading invariant 1 already gives them, and their deny carries the same fact under their own union's spelling. The one thing this invariant does not reach is the pair of exits: withdrawal by the requester and lapse by TTL stay available, because a request whose class a later amendment raised must be able to leave the queue, and neither of them authorizes anything. (Amended APRV-185.) The open window of §5.2 is bounded by this invariant too: a class resolving `human-only` is denied inside a window exactly as outside one, and `log.mutate` alongside it, since the log is the one thing the gate never lets an agent write. (Amended APRV-214.)
576
+ 10. **Guidance never reaches enforcement.** The values block of §5.3 and the `reaction` field of §5.2 are human-authored guidance. No path that computes a verdict, a classification, a sampling draw, a budget, a token, an obligation, or a harness allow reads either: no enforcement module loads the block, a policy resolves identically whether the block is present, absent, unreadable, or duplicated, and a log's reactions change no projection an enforcement path consumes. This is the mirror of invariant 4 rather than an exception to it. Invariant 4 stops the party under oversight moving the gate with its own words; this one stops the overseer's words moving it by any route other than the attested policy block, so that the one thing that changes what an agent may do stays the one thing a human attests. Surfaces that print either MUST label it human-authored guidance and MUST say in words when no values block exists (`tests/values-inert.test.ts`). (Amended APRV-237.)
577
+
578
+ ### 11.2 Refusal-code registry (normative)
579
+
580
+ Invariant 6 above freezes six refusal unions as public API and pins each with a test. It does not say what any member means, and a frozen vocabulary whose triggers are folklore is a vocabulary a second implementation has to guess at. This registry states, for every member of every union, the condition under which that member fires. It is normative, and it is the source the conformance suite's `failure_class` assignments are drawn from (§13): a refusal for the wrong reason is a conformance failure, so the mapping has to live somewhere other than in the reference implementation's source. (Amended APRV-137.)
581
+
582
+ This registry covers the six gate-facing unions. A verb-local refusal union outside them is still bound by invariant 6: it is exported, frozen, and pinned by a test in the verb's own suite. (Amended APRV-215.)
583
+
584
+ Three properties bind the whole registry. A code fires for exactly the condition named and never as a catch-all. Where two conditions could both apply, the order in which an implementation evaluates them is itself normative and is stated with the condition. And a refusal leaves the log untouched unless its row says otherwise: `budget-exceeded` appends a `budget.exceeded` record beside its refusal, `expired` materialises the `approval.expired` record a reader could already derive, and nothing else writes. (Amended APRV-137.)
585
+
586
+ **`gate_refusal_codes`** — every way `register`, `request`, `decide`, `withdraw`, `expire`, the policy-amendment ceremony, and harness-grant consumption can refuse, in definition order. (Amended APRV-137.)
587
+
588
+ | code | fires when |
589
+ |---|---|
590
+ | `policy-not-attested` | The live policy bytes do not match the latest attestation, no attestation exists, or the policy file cannot be read. Checked at intake, at grant, and at harness-grant consumption. Reject, revoke, and withdraw do not require attestation, because they withdraw authority rather than confer it. |
591
+ | `policy-drift` | The policy is attested, and the hash in force differs from the hash the matching `approval.requested` pinned. Grant and harness consumption only. The pending request is void; nothing is appended. A request written before the field existed carries no hash and is decided as it always was. |
592
+ | `envelope-invalid` | The envelope fails `envelope.schema.json`, the task file's frontmatter carries no `approval:` key or no usable id, or an in-memory registration names an empty task id. |
593
+ | `task-file-unreadable` | The task file could not be read. An I/O fact, and never an accusation about its contents. |
594
+ | `task-already-registered` | The task id already has a `task.registered` record, or a declared `idempotency_key` is already declared under a different task. |
595
+ | `envelope-missing` | The file carries no envelope and the log holds a `task.registered` for its task, so the envelope was removed after registration. Nothing is appended, and no implementation repairs the file. |
596
+ | `not-registered` | No `task.registered` record exists for the task id. |
597
+ | `action-not-registered` | The task is registered and declares no action carrying this `idempotency_key`. |
598
+ | `duplicate-request` | A live `approval.requested` for this action key is already awaiting a decision. |
599
+ | `already-executed` | The action key already has an `execution.started`. On the revoke path it says revocation was attempted after execution, which is a thing revocation cannot reach. |
600
+ | `budget-exceeded` | A conjunctive budget verdict failed at intake or at grant. A `budget.exceeded` record IS appended before the refusal; where that append itself fails the refusal still stands and reports both facts. |
601
+ | `payload-hash-required` | An action that reached the manual path, by class, by the §7 floor, or by a live draw, carries no `payload_hash` in its registered declaration. The same code answers the same fact at the harness boundary: a harness start that names no binding, or a harness-grant spend that presents none. |
602
+ | `payload-mismatch` | Payload material supplied at intake does not hash to the declared `payload_hash`. Nothing is stored and nothing is appended. |
603
+ | `payload-store-failed` | Supplied material cannot be canonicalized, or the payload store cannot be written. A manual request whose bytes no channel can display is a request no human can answer, so intake refuses. |
604
+ | `grant-classless-request` | A grant was attempted on a request whose payload carries no usable `class`. Reject and revoke are unaffected, since withdrawing authority needs no class. |
605
+ | `class-human-only` | The action's class resolves to `human-only` (§5.2). Refused by request, by all three decisions of the decide path, by harness-grant consumption, by the harness start boundary, by the token spend, and by `approval run`. Evaluated immediately after the check that establishes a request exists, and before `policy-drift`, `grant-classless-request`, `actor-not-approver` and budgets, so a class nobody may transact in is answered before any question about who may decide it, under which rules, or against which ceiling. Nothing is appended. Withdrawal and expiry are not refused. |
606
+ | `loop-escalated` | The task has three consecutive failed side-effecting executions and the action would otherwise have proceeded unsupervised, or the harness session or actor scope of §10.2 has three consecutive failed side-effecting tool calls. Failed `read.*` actions accrue nothing and successful ones clear nothing (§10.2). Manual requests for the same task are unaffected, because escalation puts a human in the loop rather than closing the task. The refusal names the scope key that tripped and the completion that clears it. (Amended APRV-280.) On a harness surface the floor routes the side-effecting classes of a tool call and no others, so this code does not fire for a call whose classes are all `read.*` (§10.2). (Amended APRV-297.) |
607
+ | `not-delegated` | A harness outcome was reported for an action key whose `execution.started` carries no harness execution marker, or for a task that started nothing at all. The mirror of `execute_refusal_codes`' `execution-delegated`: that code refuses a human recovery verb over a harness start, this one refuses a harness report over an execution the runtime is watching itself. Nothing is appended. |
608
+ | `already-finished` | Every delegated execution the reported tool call opened already carries an outcome. An execution has exactly one, and a second report would be a streak cleared by repetition rather than by recovery. |
609
+ | `not-requested` | No `approval.requested` record exists for the action key. |
610
+ | `already-decided` | The request already carries a terminal decision: rejected or revoked, granted where the verb is not revoke, or any settled state on the withdraw path. |
611
+ | `not-granted` | Revoke was attempted on a request that is awaiting a decision, or a harness grant was consumed for a key whose grant is not harness-executed. |
612
+ | `request-withdrawn` | A decision, or a second withdrawal, was attempted on a request its requester had already withdrawn. Nobody decided, which is what makes this distinct from `already-decided`. |
613
+ | `not-requester` | A withdrawal was attempted by an actor other than the one that appended the matching `approval.requested`. |
614
+ | `expired` | The TTL lapsed, judged from the request's own timestamp whether or not an `approval.expired` record exists. Where no such record exists the runtime appends one, under a `system:` actor, and then refuses. |
615
+ | `not-expired` | `expire` was called on a request whose TTL has not lapsed, including every request under a policy that declares no `approval_ttl` (§5.1). |
616
+ | `actor-invalid` | The actor is not a well-formed `human:` or `agent:` identity, on register, request, withdraw, and harness-grant consumption. |
617
+ | `actor-not-human` | A human-only verb, grant or reject or revoke, was attempted by an actor that is not `human:<id>`. |
618
+ | `actor-not-approver` | A grant was recorded by a `human:` actor the resolved rule's `approvers` list does not name (§5.2). Grant only, and evaluated after `policy-drift` and `grant-classless-request`, which establish that there is still a request and that it has a class, and before budgets, which write. A rule declaring no `approvers` restricts nobody. |
619
+ | `log-unreadable` | The log could not be opened. A filesystem fact. |
620
+ | `log-torn-tail` | The log's final line is unterminated, which is the signature of a crashed write. Nothing is repaired. |
621
+ | `log-corrupt` | The chain does not verify. The log's own contents contradict each other, so nothing may be authorized from it. |
622
+ | `diff-too-large` | The rendered semantic diff of a proposed policy amendment is larger than a channel prompt can show whole. A refusal rather than a truncation, because a prompt showing two thirds of a policy change would collect a signature for the third it did not show. |
623
+ | `proposal-not-found` | No `policy.proposed` record exists at the named seq, so there is no attestation prompt to answer. |
624
+ | `proposal-stale` | The policy bytes changed after the attestation prompt was rendered, so the hash on the approver's screen is not the hash on disk. Distinct from `policy-drift`, which is about a pending approval routed under superseded rules. |
625
+ | `policy-already-attested` | An attestation was proposed for a policy file that already matches its attestation. There is no amendment to sign. |
626
+ | `reaction-note-required` | A grant carries a `reaction` of `loved` or `disliked` (§5.2) and no non-blank note. Grant only, evaluated with the other checks that read nothing, and nothing is appended. Reject and revoke accept no reaction at all, which is a usage error at the verb rather than a member of this union. (Amended APRV-237.) |
627
+ | `append-failed` | The append itself failed, carrying the writer's own error. Its code is `head-moved` where the log grew between the read that authorized the write and the write. |
628
+
629
+ **`token_verify_refusal_codes`** — every way a presented token can fail verification, in definition order. Evaluated as: revoked, then lapsed by derivation, then any state that is not granted, then the parent TTL re-applied, then the harness marker, then the recorded digest, then consumption, then the presented preimage. (Amended APRV-137.)
630
+
631
+ | code | fires when |
632
+ |---|---|
633
+ | `not-granted` | No grant governs the action key: it was never requested, is still awaiting, was rejected, or was withdrawn. Also where a granted cycle names no task, since `execution.started` requires one. |
634
+ | `token-mismatch` | A grant exists and the SHA-256 of the presented token is not the digest it recorded, compared in constant time. Also where the grant carries no usable `token_sha256`, which authenticates nothing and fails closed. |
635
+ | `token-consumed` | An `execution.started` for this action key already spent it, whether that record carried the same digest or simply landed after the grant. Single use is proved from the log rather than remembered. |
636
+ | `token-expired` | The parent request's TTL lapsed. There is no separate token TTL: a token lives exactly as long as its request, and under a policy declaring no `approval_ttl` it does not lapse at all (§5.1). |
637
+ | `token-revoked` | A human withdrew the grant with `approval.revoked`. |
638
+ | `payload-mismatch` | The hash the consumer states for the bytes it is about to execute differs from the hash the grant bound to, or the consumer stated none, or the grant recorded none. A grant that recorded no binding can never be spent. |
639
+ | `harness-executed` | The grant declared `execution: "harness"` and minted no token. A shape condition on the grant, and never a report that a command already ran: the authorization is complete and is not of the kind that is spent. Evaluated before the digest comparison, so a caller is told the authorization was never of the kind that is spent rather than sent hunting for a token deliberately never created. |
640
+
641
+ **`token_refusal_codes`** — the token verbs' union: every verification code above, plus the log and append failures, in definition order. (Amended APRV-137.)
642
+
643
+ | code | fires when |
644
+ |---|---|
645
+ | `not-granted`, `token-mismatch`, `token-consumed`, `token-expired`, `token-revoked`, `payload-mismatch`, `harness-executed` | Exactly as in the verification table above; the names and the meanings are the same ones. |
646
+ | `class-human-only` | The action's class resolves to `human-only` (§5.2): the spend is refused before consumption, no token is spent, and nothing is appended. |
647
+ | `log-unreadable` | The log could not be opened. |
648
+ | `log-torn-tail` | The log's final line is unterminated. |
649
+ | `log-corrupt` | The chain does not verify, so nothing may be spent from it. |
650
+ | `append-failed` | The `execution.started` append failed, carrying the writer's error. Its code is `head-moved` for the double-spend case, refused under the append lock with nothing written. |
651
+
652
+ **`execute_refusal_codes`** — every way `approval run` and the adapter contract can refuse, in definition order. Six of the token codes are re-exposed verbatim rather than collapsed, because the responses differ. (Amended APRV-137.)
653
+
654
+ | code | fires when |
655
+ |---|---|
656
+ | `action-not-registered` | No `task.registered` record declares the action key, or more than one task declares it, in which case the runtime refuses rather than guess which declaration governs. |
657
+ | `class-human-only` | The action's class resolves to `human-only` (§5.2): execution is refused before the manual/supervised fork, and nothing is appended. |
658
+ | `token-required` | The action resolves to `manual`, or the log already holds an `approval.requested` for the key, and no token was passed and none was recoverable from sealed delivery. Nothing is appended. |
659
+ | `loop-escalated` | The task has three consecutive failed side-effecting executions, on the supervised or autonomous path; failed `read.*` actions accrue nothing (§10.2). A redirection rather than a ban: request the action, have a human grant it, run it with the token. (Amended APRV-280.) |
660
+ | `policy-not-attested` | Attestation fails, on the supervised or autonomous path. It is deliberately not re-checked on the manual path, where the grant that minted the token already required it. |
661
+ | `already-executed` | An `execution.started` already exists for the key, on the supervised or autonomous path. |
662
+ | `budget-exceeded` | Budgets refused the start. A `budget.exceeded` record IS appended before the refusal. |
663
+ | `not-started` | An outcome verb found no unfinished `execution.started` for the key, or the start record names no task. |
664
+ | `already-finished` | The most recent start already carries an outcome. An execution has exactly one. |
665
+ | `not-granted`, `token-mismatch`, `token-consumed`, `token-expired`, `token-revoked`, `harness-executed` | Surfaced verbatim from the token layer on the manual path, with the meanings of the verification table above. |
666
+ | `payload-mismatch` | On the manual path, from the token layer. Off it, where the registered declaration carries no `payload_hash`, where the executor presented none, or where the presented hash differs from the declared one. Nothing is appended in any of the three. |
667
+ | `actor-not-human` | Resolve or reconcile was attempted by an actor that is not `human:<id>`. Those two verbs and no others: starting an execution carries no actor check of its own, so a malformed identity there is refused at the write boundary by the event schema and reaches the caller as `append-failed`. The same code additionally answers a missing or empty mandatory note on both verbs, which is a second condition its name does not describe; the doubling is recorded here as the behaviour to expect rather than blessed, and splitting it would add a member to a frozen union. |
668
+ | `execution-delegated` | The key's latest `execution.started` carries `payload.execution: "harness"`, so the harness ran the command and this runtime never observed an exit status. The record is terminal by design and no outcome may be placed over it. |
669
+ | `execution-indeterminate` | The key's execution ended in an unknown outcome that nobody has reconciled. Refused on both paths, because a retry against an unknown outcome is a blind double-execution. |
670
+ | `not-indeterminate` | Reconcile found no unreconciled `execution.indeterminate` for the key, or the cycle names no task. A started execution with no outcome at all is a dangling execution and is closed with resolve instead. |
671
+ | `already-reconciled` | The indeterminate outcome already carries a resolution. Neither record is rewritten. |
672
+ | `log-unreadable` | The log could not be opened. |
673
+ | `log-torn-tail` | The log's final line is unterminated. |
674
+ | `log-corrupt` | The chain does not verify, so nothing may execute from it. |
675
+ | `append-failed` | The append failed, carrying the writer's error. Its code is `head-moved` where a record landed between the read that authorized the write and the write. |
676
+
677
+ **`audit_refusal_codes`** — every way `audit review` and `audit reconcile` can refuse, in definition order. The union has been frozen in code and pinned by `tests/audit.test.ts` since APRV-127 and had no rows here; this table closes that gap. (Amended APRV-237.)
678
+
679
+ | code | fires when |
680
+ |---|---|
681
+ | `actor-not-human` | Review or reconcile was attempted by an actor that is not `human:<id>`. Evaluated first, before the log is read. |
682
+ | `not-sampled` | No `audit.sampled` record matches the subject named, by seq or by action key. |
683
+ | `already-reviewed` | The sample named already carries a later `audit.reviewed`. |
684
+ | `ambiguous-subject` | The action key named has more than one unreviewed sample; name the seq instead. |
685
+ | `not-obliged` | No `reconciliation.required` record exists at the seq named. |
686
+ | `already-satisfied` | That obligation already carries a `reconciliation.satisfied`. |
687
+ | `note-required` | A verb that requires a reason was given none: a reconciliation with a blank note (a discharge nobody described is one no auditor can check), or a review whose `reaction` is `loved` or `disliked` with a blank note (§5.2). Evaluated after the actor check and before the log is read. |
688
+ | `reaction-conflicts-verdict` | A review carries `--deny` and a `reaction` of `liked` or `loved` (§5.2). Evaluated beside `note-required`, before the log is read; nothing is appended. |
689
+ | `revert-required` | A `gated-revert` obligation was satisfied without an `execution.completed` for the revert the satisfaction names. The obligation is to undo the action through the gate, and the evidence of that is in this same log. |
690
+ | `obligation-not-appended` | The denial's `audit.reviewed` was appended and the `reconciliation.required` it obliges could not be. The log is consistent, the review stands, and the obligation is missing; reviewing again once the head settles creates it. |
691
+ | `log-unreadable` | The log could not be opened, or holds a line that is not a record. |
692
+ | `log-torn-tail` | The log's final line is unterminated. |
693
+ | `log-corrupt` | The chain does not verify, so nothing is derived from it. |
694
+ | `append-failed` | The append itself failed, carrying the writer's own error. |
695
+
696
+ **`append_error_codes`** — every way the write boundary itself can refuse an append, in definition order. Every one of them leaves the file byte-identical. (Amended APRV-137.)
697
+
698
+ | code | fires when |
699
+ |---|---|
700
+ | `lock-timeout` | Another writer held the append lockfile past the timeout. A stale lock is never stolen, because silently breaking one is how two writers come to share a `seq`. |
701
+ | `corrupt-tail` | The file's last line is truncated, blank, not valid JSON, not a JSON object, or carries no usable integer `seq` or 64-hex `hash`. Chaining onto a half-written record would make the damage permanent. |
702
+ | `validation` | The complete record, chain fields included, failed the event schema at the write boundary. This is also the code an implementation MUST use where its write boundary can still receive a caller-supplied timestamp on a gate-typed event (§8). |
703
+ | `canonicalization` | The record could not be serialized under RFC 8785. |
704
+ | `io` | The log directory, the file, or the write could not be created, opened, or performed. |
705
+ | `head-moved` | The caller supplied a compare-and-append precondition and the tail read under the lock is a different `(seq, hash)`. Every read-dependent check that authorized the write is stale, and nothing is retried by the writer. |
706
+
707
+ **`gate_window_refusal_codes`** — every way `approval gate open` and `approval gate close` can refuse, in evaluation order: the reason, then the duration, then the actor, then the terminal (where `--json` counts as no terminal), then the confirmation, then the verified read and the window's state it yields, then the append. The terminal and the typed word come before the log is read at all, so a process that cannot be asked learns nothing about the window from the refusal it gets. Every one of them appends nothing. (Amended APRV-214.)
708
+
709
+ | code | fires when |
710
+ |---|---|
711
+ | `actor-not-human` | The acting identity does not carry the `human:` prefix. The window is a human's suspension of a control over agents, so an agent cannot author one. |
712
+ | `gate-reason-required` | `--reason` is blank. A window with no stated purpose is a window nobody can review. |
713
+ | `gate-duration-too-long` | The requested duration is above 24 hours or not positive. |
714
+ | `gate-already-open` | A window is already open. Two windows would be two expiries for one suspension. |
715
+ | `gate-not-open` | `close` found no open window, or a bypass that named no window found none. A bypass that named the window its verdict was decided under takes `gate-window-closed` instead. (Amended APRV-294.) |
716
+ | `gate-window-closed` | A bypass was decided under a named window and that window is gone by the time the record is appended: a `gate.closed` names it, its expiry has passed, or a later `gate.opened` supersedes it. The refusal names the closing record's `seq`, or the expiry where a lapse left no record to name. Distinct from `gate-not-open`, which says no window stood at all. Nothing is appended, no command ran, and the refusal is not an execution, so no loop-safety streak accrues (§10.2). (Amended APRV-294.) |
717
+ | `gate-stdin-not-tty` | `open` was run without a terminal on stdin, or with `--json`. A process that cannot be asked cannot answer, and no flag answers for it. |
718
+ | `gate-confirmation-mismatch` | The line typed at the prompt was anything other than `understood`, an end of input included. |
719
+ | `log-unreadable` | The verified read of the log failed before the state could be derived. |
720
+ | `log-torn-tail` | The log ends in a partial record. |
721
+ | `log-corrupt` | The chain does not verify. |
722
+ | `append-failed` | The write boundary refused the record; the writer's own code rides in the detail. |
289
723
 
290
724
  ## 12. Interoperability
291
725
 
292
726
  - **Backlog.md:** native. Tasks live in `backlog/`, the envelope is one preserved frontmatter key, board `status` and approval `state` are independent. approval.md ships no board; use Backlog.md's.
293
727
  - **AGENTS.md import:** `approval import agents-md` parses "require approval first / allowed without prompting" permissions sections into draft policy classes for human confirmation, turning existing prose conventions into enforced policy.
294
- - **Inbound adapters (post-v1):** CartsOS/Telegram capture, arbitrary apps via `approval register --json`.
728
+ - **Inbound adapters (post-v1):** e.g. a Telegram capture bot, arbitrary apps via `approval register --json`.
295
729
  - **Outbound sinks (post-v1):** approved+scheduled tasks mirrored to TickTick / Google Tasks / Google Calendar as views, never sources of truth. Mapping via [RFC 8984 jsCalendar Task](https://www.rfc-editor.org/rfc/rfc8984) with the envelope as a vendor extension, `X-APPROVAL-*` in [VTODO](https://www.rfc-editor.org/rfc/rfc5545).
296
730
 
297
731
  ## 13. Non-goals
298
732
 
299
733
  No new task file format. No kanban UI. No agent framework or orchestration platform. No hosted service (local-first; a sync story can come later). No channel breadth beyond the three shipped. No claim of scheming-robustness (§11).
300
734
 
735
+ Post-v1 (non-normative): `review: adversarial` as a per-class flag. Before a flagged `manual` request reaches the approver, an independent agent instance with the raw payload and no stake in the outcome writes a dissent: worst plausible reading, omissions, questions a suspicious reviewer would ask. The approver adjudicates between framings instead of consuming one. Untrusted monitoring, spent where human attention is scarcest.
736
+
737
+ Post-v1 (non-normative): a Rust fast-path implementation of the hot loop (policy resolution, chain-tail verification, gate verdict) as the latency accelerator for the v1 harness hook of §14 (`approval hook claude-code`, `approval hook cursor`) and the per-tool-call adapters like it, where Node startup latency on every gated tool call is unacceptable. Conformance is defined by the fixture suite, which is `conformance/`: one versioned JSON file per surface (canonicalization, policy resolution, chain verification, gate verdicts, write-boundary schema validation, and the refusal-code unions of §11.1 invariant 6), each vector carrying the machine-readable `failure_class` a refusal MUST produce, negative controls that a conforming implementation MUST reject, and a manifest pinning every file by SHA-256. `conformance/README.md` states the runner contract a second implementation reproduces, including that a suite it cannot execute is a failure and never a skip. An implementation is conforming when it passes those vectors byte for byte; the crates.io name `approval-md` is reserved for it. (Amended APRV-122.) The TypeScript runtime remains the reference implementation for the full surface.
738
+
301
739
  ## 14. Repository layout and roadmap
302
740
 
303
741
  ```
@@ -320,8 +758,8 @@ Milestones sized for agent-driven development (each = one reviewable task):
320
758
  - **M4** Channels: cli, web queue, Telegram; QUEUE.md renderer.
321
759
  - **M5** Daemon: watch, TTL, sampling, loop-escalation.
322
760
  - **M6** Backlog.md round-trip + AGENTS.md import.
323
- - **M7** First adapter (email) + vault; end-to-end demo: agent drafts chaser → Telegram ping → approve from phone → sent → log verifies.
324
- - **M8** MCP wrapper. Post-v1: TickTick/GCal sinks, CartsOS inbound.
761
+ - **M7** First adapter (email) + vault; end-to-end demo: agent drafts chaser → Telegram ping → approve from phone → sent → log verifies. A second adapter (AgentMail) serves the same class over an HTTPS API and is where §10.4's two-key enforcement model and its binding of a remote mutable draft are exercised (APRV-224).
762
+ - **M8** MCP wrapper (§10.5) and the agent-harness hooks, `approval hook claude-code`, `approval hook cursor`, and opt-in `approval hook codex` (§10.6): a harness PreToolUse adapter that classifies the command a harness is about to run and resolves it against the policy. Ordinary Edit/Write calls resolve `files.write.workspace` through the same policy and accounting path as shell actions. A manual class waits on a recorded decision; every authorized execution, including supervised and autonomous executions, records `execution.started` before proceeding, with a matching outcome when the harness reports a verified result. A human-open gate window follows §5.2 instead: it records `gate.bypassed` and preserves human-only refusal. (Amended APRV-304.) Both surfaces expose the same gate to a client that is not a shell. Applications built on the Claude Agent SDK are reachable through the same Claude Code hook surface: the SDK's hook callbacks receive that PreToolUse event and return that permission decision, so a documented shim spawning `approval hook claude-code` gates them with no new surface (`docs/agent-sdk-hook.md`). The §13 Rust fast-path is this hook's post-v1 latency accelerator, not a prerequisite. Post-v1: TickTick/GCal sinks, inbound capture adapters. (Amended APRV-103. Amended APRV-242.)
325
763
 
326
764
  ## 15. References
327
765
 
@@ -333,6 +771,10 @@ OpenAI Agents SDK HITL · https://openai.github.io/openai-agents-js/guides/human
333
771
  A2A protocol · https://a2a-protocol.org — `input-required` lifecycle state
334
772
  Model Context Protocol · https://modelcontextprotocol.io
335
773
  mission-control · https://github.com/MeisnerDan/mission-control — autonomy levels, vault, spend limits
774
+ Claude Code permissions · https://code.claude.com/docs/en/permissions — harness-enforced allow/ask/deny rules, settings precedence, hooks
775
+ Codex CLI approvals · https://developers.openai.com/codex/agent-approvals-security — approval_policy × sandbox_mode
776
+ Gemini CLI configuration · https://google-gemini.github.io/gemini-cli/docs/get-started/configuration.html — approval modes
777
+ Cursor run modes · https://cursor.com/docs/agent/security/run-modes — allowlist / auto-review / run-everything, "best-effort guardrails"
336
778
  AI Control (Greenblatt et al., 2023) · https://arxiv.org/abs/2312.06942 — trusted monitoring under an audit budget
337
779
  TaskChampion · https://github.com/GothenburgBitFactory/taskchampion — operation-log task storage
338
780
  Ink & Switch, A Local-First Task Framework · https://www.inkandswitch.com/patchwork/notebook/tasks-01/