approval-md 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/README.md +629 -559
  2. package/SPEC.md +99 -24
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +656 -0
  27. package/dist/src/channels/contract.js +200 -7
  28. package/dist/src/channels/contract.js.map +1 -1
  29. package/dist/src/channels/payload-view.d.ts +35 -0
  30. package/dist/src/channels/render-queue.d.ts +149 -0
  31. package/dist/src/channels/tagging.d.ts +196 -0
  32. package/dist/src/channels/telegram.d.ts +1944 -0
  33. package/dist/src/channels/telegram.js +218 -23
  34. package/dist/src/channels/telegram.js.map +1 -1
  35. package/dist/src/channels/web.d.ts +350 -0
  36. package/dist/src/channels/web.js +17 -0
  37. package/dist/src/channels/web.js.map +1 -1
  38. package/dist/src/cli/adapter.d.ts +90 -0
  39. package/dist/src/cli/adapter.js +25 -15
  40. package/dist/src/cli/adapter.js.map +1 -1
  41. package/dist/src/cli/amend.d.ts +59 -0
  42. package/dist/src/cli/amend.js +214 -30
  43. package/dist/src/cli/amend.js.map +1 -1
  44. package/dist/src/cli/args.d.ts +43 -0
  45. package/dist/src/cli/attest.d.ts +50 -0
  46. package/dist/src/cli/attest.js +134 -7
  47. package/dist/src/cli/attest.js.map +1 -1
  48. package/dist/src/cli/audit-card.d.ts +62 -0
  49. package/dist/src/cli/audit.d.ts +59 -0
  50. package/dist/src/cli/channel-telegram.d.ts +879 -0
  51. package/dist/src/cli/channel-telegram.js +311 -13
  52. package/dist/src/cli/channel-telegram.js.map +1 -1
  53. package/dist/src/cli/channel-web.d.ts +131 -0
  54. package/dist/src/cli/channel.d.ts +80 -0
  55. package/dist/src/cli/channel.js +9 -0
  56. package/dist/src/cli/channel.js.map +1 -1
  57. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  58. package/dist/src/cli/codex-bridge.d.ts +819 -0
  59. package/dist/src/cli/codex-bridge.js +1607 -0
  60. package/dist/src/cli/codex-bridge.js.map +1 -0
  61. package/dist/src/cli/codex.d.ts +2 -0
  62. package/dist/src/cli/codex.js +469 -0
  63. package/dist/src/cli/codex.js.map +1 -0
  64. package/dist/src/cli/coverage.d.ts +61 -0
  65. package/dist/src/cli/daemon.d.ts +120 -0
  66. package/dist/src/cli/daemon.js +4 -1
  67. package/dist/src/cli/daemon.js.map +1 -1
  68. package/dist/src/cli/doctor.d.ts +129 -0
  69. package/dist/src/cli/doctor.js +586 -17
  70. package/dist/src/cli/doctor.js.map +1 -1
  71. package/dist/src/cli/env.d.ts +65 -0
  72. package/dist/src/cli/execute.d.ts +202 -0
  73. package/dist/src/cli/execute.js +25 -2
  74. package/dist/src/cli/execute.js.map +1 -1
  75. package/dist/src/cli/exit-codes.d.ts +73 -0
  76. package/dist/src/cli/feedback.d.ts +60 -0
  77. package/dist/src/cli/gate-window.d.ts +40 -0
  78. package/dist/src/cli/gate.d.ts +68 -0
  79. package/dist/src/cli/git-scope.d.ts +190 -0
  80. package/dist/src/cli/gloss-attach.d.ts +85 -0
  81. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  82. package/dist/src/cli/gloss-codex.d.ts +24 -0
  83. package/dist/src/cli/gloss-options.d.ts +42 -0
  84. package/dist/src/cli/gloss.d.ts +265 -0
  85. package/dist/src/cli/help.d.ts +107 -0
  86. package/dist/src/cli/help.js +320 -93
  87. package/dist/src/cli/help.js.map +1 -1
  88. package/dist/src/cli/hook-codex.d.ts +126 -0
  89. package/dist/src/cli/hook-codex.js +226 -0
  90. package/dist/src/cli/hook-codex.js.map +1 -0
  91. package/dist/src/cli/hook.d.ts +787 -0
  92. package/dist/src/cli/hook.js +1235 -181
  93. package/dist/src/cli/hook.js.map +1 -1
  94. package/dist/src/cli/import.d.ts +35 -0
  95. package/dist/src/cli/import.js +1 -1
  96. package/dist/src/cli/import.js.map +1 -1
  97. package/dist/src/cli/init.d.ts +84 -0
  98. package/dist/src/cli/init.js +2 -2
  99. package/dist/src/cli/init.js.map +1 -1
  100. package/dist/src/cli/instructions.d.ts +23 -0
  101. package/dist/src/cli/journal.d.ts +41 -0
  102. package/dist/src/cli/log-advance.d.ts +287 -0
  103. package/dist/src/cli/log-advance.js +102 -11
  104. package/dist/src/cli/log-advance.js.map +1 -1
  105. package/dist/src/cli/log-anchor.d.ts +176 -0
  106. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  107. package/dist/src/cli/log-sync.d.ts +243 -0
  108. package/dist/src/cli/log-verbs.d.ts +16 -0
  109. package/dist/src/cli/log-verbs.js +7 -1
  110. package/dist/src/cli/log-verbs.js.map +1 -1
  111. package/dist/src/cli/long-help.d.ts +70 -0
  112. package/dist/src/cli/main.d.ts +77 -0
  113. package/dist/src/cli/main.js +159 -7
  114. package/dist/src/cli/main.js.map +1 -1
  115. package/dist/src/cli/mcp.d.ts +52 -0
  116. package/dist/src/cli/paths.d.ts +56 -0
  117. package/dist/src/cli/payload.d.ts +58 -0
  118. package/dist/src/cli/policy-apply.d.ts +195 -0
  119. package/dist/src/cli/policy-apply.js +573 -0
  120. package/dist/src/cli/policy-apply.js.map +1 -0
  121. package/dist/src/cli/policy.d.ts +43 -0
  122. package/dist/src/cli/policy.js +14 -1
  123. package/dist/src/cli/policy.js.map +1 -1
  124. package/dist/src/cli/preflight.d.ts +501 -0
  125. package/dist/src/cli/preflight.js +689 -45
  126. package/dist/src/cli/preflight.js.map +1 -1
  127. package/dist/src/cli/progress.d.ts +78 -0
  128. package/dist/src/cli/prompt.d.ts +209 -0
  129. package/dist/src/cli/quickstart.d.ts +46 -0
  130. package/dist/src/cli/quickstart.js +297 -0
  131. package/dist/src/cli/quickstart.js.map +1 -0
  132. package/dist/src/cli/records.d.ts +34 -0
  133. package/dist/src/cli/render.d.ts +22 -0
  134. package/dist/src/cli/sandbox.d.ts +51 -0
  135. package/dist/src/cli/sandbox.js +17 -1
  136. package/dist/src/cli/sandbox.js.map +1 -1
  137. package/dist/src/cli/scaffold.d.ts +79 -0
  138. package/dist/src/cli/scaffold.js +1 -1
  139. package/dist/src/cli/setup-adapter.d.ts +137 -0
  140. package/dist/src/cli/setup-adapter.js +38 -4
  141. package/dist/src/cli/setup-adapter.js.map +1 -1
  142. package/dist/src/cli/setup-channel.d.ts +126 -0
  143. package/dist/src/cli/setup-channel.js +28 -1
  144. package/dist/src/cli/setup-channel.js.map +1 -1
  145. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  146. package/dist/src/cli/setup-common.d.ts +277 -0
  147. package/dist/src/cli/setup-common.js +3 -2
  148. package/dist/src/cli/setup-common.js.map +1 -1
  149. package/dist/src/cli/setup-flow.d.ts +287 -0
  150. package/dist/src/cli/setup-service.d.ts +96 -0
  151. package/dist/src/cli/setup.d.ts +204 -0
  152. package/dist/src/cli/setup.js +94 -2
  153. package/dist/src/cli/setup.js.map +1 -1
  154. package/dist/src/cli/style.d.ts +320 -0
  155. package/dist/src/cli/token.d.ts +39 -0
  156. package/dist/src/cli/up.d.ts +155 -0
  157. package/dist/src/cli/up.js +119 -53
  158. package/dist/src/cli/up.js.map +1 -1
  159. package/dist/src/cli/usage.d.ts +37 -0
  160. package/dist/src/cli/values.d.ts +40 -0
  161. package/dist/src/cli/values.js +3 -4
  162. package/dist/src/cli/values.js.map +1 -1
  163. package/dist/src/cli/vault.d.ts +59 -0
  164. package/dist/src/cli/vault.js +2 -2
  165. package/dist/src/cli/vault.js.map +1 -1
  166. package/dist/src/cli/verb-registry.d.ts +76 -0
  167. package/dist/src/cli/verb-registry.js +344 -11
  168. package/dist/src/cli/verb-registry.js.map +1 -1
  169. package/dist/src/cli/wordmark.d.ts +31 -0
  170. package/dist/src/cli/wordmark.js +2 -2
  171. package/dist/src/codex/broker.d.ts +229 -0
  172. package/dist/src/codex/broker.js +548 -0
  173. package/dist/src/codex/broker.js.map +1 -0
  174. package/dist/src/codex/doctor.d.ts +13 -0
  175. package/dist/src/codex/doctor.js +41 -0
  176. package/dist/src/codex/doctor.js.map +1 -0
  177. package/dist/src/codex/manifest.d.ts +49 -0
  178. package/dist/src/codex/manifest.js +103 -0
  179. package/dist/src/codex/manifest.js.map +1 -0
  180. package/dist/src/codex/runner.d.ts +178 -0
  181. package/dist/src/codex/runner.js +231 -0
  182. package/dist/src/codex/runner.js.map +1 -0
  183. package/dist/src/codex/serve.d.ts +56 -0
  184. package/dist/src/codex/serve.js +98 -0
  185. package/dist/src/codex/serve.js.map +1 -0
  186. package/dist/src/codex/templates.d.ts +41 -0
  187. package/dist/src/codex/templates.js +319 -0
  188. package/dist/src/codex/templates.js.map +1 -0
  189. package/dist/src/codex/trust.d.ts +19 -0
  190. package/dist/src/codex/trust.js +183 -0
  191. package/dist/src/codex/trust.js.map +1 -0
  192. package/dist/src/codex/workspace-commit.d.ts +219 -0
  193. package/dist/src/codex/workspace-commit.js +549 -0
  194. package/dist/src/codex/workspace-commit.js.map +1 -0
  195. package/dist/src/codex/workspace-plan.d.ts +131 -0
  196. package/dist/src/codex/workspace-plan.js +561 -0
  197. package/dist/src/codex/workspace-plan.js.map +1 -0
  198. package/dist/src/core/actor.d.ts +2 -0
  199. package/dist/src/core/actor.js +5 -0
  200. package/dist/src/core/actor.js.map +1 -0
  201. package/dist/src/core/advance-cycle.d.ts +221 -0
  202. package/dist/src/core/advance-cycle.js +66 -2
  203. package/dist/src/core/advance-cycle.js.map +1 -1
  204. package/dist/src/core/agents-md.d.ts +278 -0
  205. package/dist/src/core/agents-md.js +33 -31
  206. package/dist/src/core/agents-md.js.map +1 -1
  207. package/dist/src/core/apply-patch.d.ts +49 -0
  208. package/dist/src/core/apply-patch.js +266 -0
  209. package/dist/src/core/apply-patch.js.map +1 -0
  210. package/dist/src/core/attest.d.ts +635 -0
  211. package/dist/src/core/attest.js +326 -4
  212. package/dist/src/core/attest.js.map +1 -1
  213. package/dist/src/core/audit.d.ts +510 -0
  214. package/dist/src/core/audit.js +13 -0
  215. package/dist/src/core/audit.js.map +1 -1
  216. package/dist/src/core/budgets.d.ts +238 -0
  217. package/dist/src/core/channel-owner.d.ts +213 -0
  218. package/dist/src/core/channel-owner.js +358 -0
  219. package/dist/src/core/channel-owner.js.map +1 -0
  220. package/dist/src/core/checkpoint.d.ts +500 -0
  221. package/dist/src/core/child-env.d.ts +88 -0
  222. package/dist/src/core/clock.d.ts +52 -0
  223. package/dist/src/core/command-class.d.ts +697 -0
  224. package/dist/src/core/command-class.js +713 -25
  225. package/dist/src/core/command-class.js.map +1 -1
  226. package/dist/src/core/commit-guard.d.ts +272 -0
  227. package/dist/src/core/commit-guard.js +424 -0
  228. package/dist/src/core/commit-guard.js.map +1 -0
  229. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  230. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  231. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  232. package/dist/src/core/coverage.d.ts +217 -0
  233. package/dist/src/core/credential-spec.d.ts +72 -0
  234. package/dist/src/core/daemon-actor.d.ts +45 -0
  235. package/dist/src/core/daemon-actor.js +54 -0
  236. package/dist/src/core/daemon-actor.js.map +1 -0
  237. package/dist/src/core/dark-session.d.ts +432 -0
  238. package/dist/src/core/dark-session.js +266 -82
  239. package/dist/src/core/dark-session.js.map +1 -1
  240. package/dist/src/core/decision-refusal.d.ts +206 -0
  241. package/dist/src/core/decision-refusal.js +24 -2
  242. package/dist/src/core/decision-refusal.js.map +1 -1
  243. package/dist/src/core/env-file.d.ts +455 -0
  244. package/dist/src/core/env-file.js +60 -1
  245. package/dist/src/core/env-file.js.map +1 -1
  246. package/dist/src/core/execute.d.ts +871 -0
  247. package/dist/src/core/execute.js +59 -8
  248. package/dist/src/core/execute.js.map +1 -1
  249. package/dist/src/core/frontmatter.d.ts +78 -0
  250. package/dist/src/core/gate-window.d.ts +312 -0
  251. package/dist/src/core/gate.d.ts +1449 -0
  252. package/dist/src/core/gate.js +149 -14
  253. package/dist/src/core/gate.js.map +1 -1
  254. package/dist/src/core/gesture-refusal.d.ts +166 -0
  255. package/dist/src/core/gesture-refusal.js +188 -0
  256. package/dist/src/core/gesture-refusal.js.map +1 -0
  257. package/dist/src/core/git-run.d.ts +73 -0
  258. package/dist/src/core/harness-version.d.ts +157 -0
  259. package/dist/src/core/harness-version.js +4 -1
  260. package/dist/src/core/harness-version.js.map +1 -1
  261. package/dist/src/core/harness-wait.d.ts +55 -0
  262. package/dist/src/core/head-retry.d.ts +107 -0
  263. package/dist/src/core/instance.d.ts +310 -0
  264. package/dist/src/core/instance.js +113 -0
  265. package/dist/src/core/instance.js.map +1 -1
  266. package/dist/src/core/intake-limits.d.ts +247 -0
  267. package/dist/src/core/jcs.d.ts +52 -0
  268. package/dist/src/core/journal.d.ts +144 -0
  269. package/dist/src/core/live-draw.d.ts +436 -0
  270. package/dist/src/core/log-reconcile.d.ts +89 -0
  271. package/dist/src/core/log-subscribe.d.ts +36 -0
  272. package/dist/src/core/log-subscribe.js +162 -0
  273. package/dist/src/core/log-subscribe.js.map +1 -0
  274. package/dist/src/core/log.d.ts +316 -0
  275. package/dist/src/core/log.js.map +1 -1
  276. package/dist/src/core/loop.d.ts +274 -0
  277. package/dist/src/core/loop.js +11 -0
  278. package/dist/src/core/loop.js.map +1 -1
  279. package/dist/src/core/md-fence.d.ts +41 -0
  280. package/dist/src/core/money.d.ts +147 -0
  281. package/dist/src/core/payload-census.d.ts +74 -0
  282. package/dist/src/core/payload-store.d.ts +175 -0
  283. package/dist/src/core/payload.d.ts +71 -0
  284. package/dist/src/core/policy-diff.d.ts +292 -0
  285. package/dist/src/core/policy-diff.js +27 -4
  286. package/dist/src/core/policy-diff.js.map +1 -1
  287. package/dist/src/core/policy-expectations.d.ts +199 -0
  288. package/dist/src/core/policy-explain.d.ts +160 -0
  289. package/dist/src/core/policy-explain.js +63 -3
  290. package/dist/src/core/policy-explain.js.map +1 -1
  291. package/dist/src/core/policy-load.d.ts +567 -0
  292. package/dist/src/core/policy-load.js +36 -6
  293. package/dist/src/core/policy-load.js.map +1 -1
  294. package/dist/src/core/policy-match.d.ts +324 -0
  295. package/dist/src/core/policy-match.js +72 -9
  296. package/dist/src/core/policy-match.js.map +1 -1
  297. package/dist/src/core/policy-proposal.d.ts +317 -0
  298. package/dist/src/core/policy-proposal.js +102 -2
  299. package/dist/src/core/policy-proposal.js.map +1 -1
  300. package/dist/src/core/prompt-layout.d.ts +221 -0
  301. package/dist/src/core/protected-path-guard.d.ts +566 -0
  302. package/dist/src/core/protected-path-guard.js +848 -55
  303. package/dist/src/core/protected-path-guard.js.map +1 -1
  304. package/dist/src/core/question-preempted.d.ts +141 -0
  305. package/dist/src/core/question-preempted.js +152 -0
  306. package/dist/src/core/question-preempted.js.map +1 -0
  307. package/dist/src/core/read-scope.d.ts +172 -0
  308. package/dist/src/core/read-scope.js +252 -0
  309. package/dist/src/core/read-scope.js.map +1 -0
  310. package/dist/src/core/registration.d.ts +25 -0
  311. package/dist/src/core/reindex.d.ts +99 -0
  312. package/dist/src/core/sampler.d.ts +313 -0
  313. package/dist/src/core/sandbox.d.ts +371 -0
  314. package/dist/src/core/sandbox.js +190 -1
  315. package/dist/src/core/sandbox.js.map +1 -1
  316. package/dist/src/core/seal.d.ts +165 -0
  317. package/dist/src/core/sender-identity.d.ts +476 -0
  318. package/dist/src/core/sender-identity.js +572 -0
  319. package/dist/src/core/sender-identity.js.map +1 -0
  320. package/dist/src/core/shlex.d.ts +102 -0
  321. package/dist/src/core/shlex.js +159 -0
  322. package/dist/src/core/shlex.js.map +1 -0
  323. package/dist/src/core/state.d.ts +505 -0
  324. package/dist/src/core/task-file.d.ts +185 -0
  325. package/dist/src/core/telegram-config.d.ts +93 -0
  326. package/dist/src/core/token.d.ts +409 -0
  327. package/dist/src/core/token.js +21 -38
  328. package/dist/src/core/token.js.map +1 -1
  329. package/dist/src/core/validate.d.ts +138 -0
  330. package/dist/src/core/values.d.ts +147 -0
  331. package/dist/src/core/values.js +36 -1
  332. package/dist/src/core/values.js.map +1 -1
  333. package/dist/src/core/vault.d.ts +291 -0
  334. package/dist/src/core/verified-snapshot.d.ts +204 -0
  335. package/dist/src/core/verify.d.ts +336 -0
  336. package/dist/src/core/version.d.ts +8 -0
  337. package/dist/src/core/wysiwys.d.ts +370 -0
  338. package/dist/src/daemon/advance-child.d.ts +39 -0
  339. package/dist/src/daemon/advance.d.ts +476 -0
  340. package/dist/src/daemon/advance.js +25 -4
  341. package/dist/src/daemon/advance.js.map +1 -1
  342. package/dist/src/daemon/audit.d.ts +87 -0
  343. package/dist/src/daemon/daemon.d.ts +1180 -0
  344. package/dist/src/daemon/daemon.js +9 -0
  345. package/dist/src/daemon/daemon.js.map +1 -1
  346. package/dist/src/daemon/dark-session.d.ts +64 -0
  347. package/dist/src/daemon/draw-child.d.ts +36 -0
  348. package/dist/src/daemon/draw.d.ts +154 -0
  349. package/dist/src/daemon/git-evidence.d.ts +173 -0
  350. package/dist/src/daemon/git-evidence.js +1 -1
  351. package/dist/src/daemon/projection.d.ts +180 -0
  352. package/dist/src/daemon/prune.d.ts +207 -0
  353. package/dist/src/mcp/http.d.ts +113 -0
  354. package/dist/src/mcp/server.d.ts +265 -0
  355. package/dist/src/mcp/server.js +17 -1
  356. package/dist/src/mcp/server.js.map +1 -1
  357. package/docs/adapter-api.md +106 -0
  358. package/docs/cli-reference.md +1316 -63
  359. package/docs/codex-enforced-session.md +103 -0
  360. package/docs/codex-workspace-broker.md +118 -0
  361. package/package.json +14 -2
  362. package/schema/codex-instance.schema.json +82 -0
  363. package/schema/event.schema.json +539 -9
  364. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  365. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  366. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  367. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  368. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  370. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  371. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  372. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  373. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  374. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  375. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  376. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  377. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  378. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  379. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  380. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  381. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  382. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  383. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  384. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  385. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  386. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  387. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  388. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  389. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  390. package/schema/fixtures/policy/valid/canonical.json +1 -1
  391. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  392. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  393. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  394. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  395. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  396. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  397. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  398. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  399. package/schema/fixtures/values/invalid/version-float.json +1 -0
  400. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  401. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  402. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  403. package/schema/fixtures/values/valid/full.json +5 -7
  404. package/schema/fixtures/values/valid/minimal.json +1 -1
  405. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  406. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  407. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  408. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  409. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  410. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  411. package/schema/fixtures/values-md/valid/absent.md +1 -1
  412. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  413. package/schema/policy.schema.json +75 -3
  414. package/schema/values.schema.json +7 -11
  415. package/templates/codex/README.md +9 -0
  416. package/schema/fixtures/values/invalid/version-string.json +0 -1
package/SPEC.md CHANGED
@@ -8,7 +8,7 @@ Version: 0.1.0-draft · Status: Draft · License: CC0 1.0 (this document) / Apac
8
8
 
9
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
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.)
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.) Ratification is recorded rather than asserted: a `gate.path.signed_off` event over that file's exact bytes clears the suffix for the text those bytes carry, and the runtime's health report names the files still holding it (§5.2, §10.1). (Amended APRV-338.)
12
12
 
13
13
  ---
14
14
 
@@ -61,7 +61,7 @@ The keywords MUST, SHOULD, MAY are per [RFC 2119](https://www.rfc-editor.org/rfc
61
61
  | **Task** | A markdown file describing work; may spawn multiple actions. |
62
62
  | **Side-effect class** | A dotted-namespace label for a category of action, e.g. `communicate.email.external`. |
63
63
  | **Policy** | The rules in `APPROVAL.md` mapping classes to autonomy levels, approvers, and budgets. |
64
- | **Autonomy level** | `manual` (approval required per action), `supervised` (proceed, but sampled for retrospective review), `autonomous` (proceed silently). |
64
+ | **Autonomy level** | How much scrutiny a class gets, strictest first: `human-only` (a person performs the action, and no agent may request it, be granted it, or run it), `manual` (a human decides before every action), `supervised-live` (a declared fraction blocks on the gate exactly as `manual` does, and the rest proceed), `supervised-retro` (proceeds at once, with a sampled fraction reviewed afterwards), `autonomous` (proceeds silently). The bare `supervised` is the deprecated alias of `supervised-retro` (§5.2). |
65
65
  | **Approval** | A recorded human decision (`granted` / `rejected`) on a specific requested action. |
66
66
  | **Execution token** | A single-use token minted on grant, required by adapters to execute. |
67
67
  | **Channel** | A transport plugin that surfaces requests and collects decisions (Telegram, local web, CLI). |
@@ -96,7 +96,7 @@ approvers:
96
96
  classes:
97
97
  read.*: { autonomy: autonomous }
98
98
  files.write.workspace: { autonomy: autonomous }
99
- calendar.write.own: { autonomy: supervised }
99
+ calendar.write.own: { autonomy: supervised-retro }
100
100
  communicate.email.draft: { autonomy: autonomous }
101
101
  communicate.email.external:
102
102
  autonomy: manual
@@ -127,14 +127,19 @@ channels:
127
127
 
128
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
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
+
130
132
  ### 5.2 Policy semantics
131
133
 
132
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.)
133
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.)
134
- - **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 exactly one rule, and that rule contributes the whole of the resolution. 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.)
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.)
135
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
+ - **`approvers[id].senders` binds a decision to an account.** An approver record MAY carry a `senders` map from channel name to the account id that channel's transport attributes a gesture to: `senders: {telegram: "12345678"}`, the numeric `callback_query.from.id` as a string. The key set is closed to channels that authenticate a sender at all (§10.3), which is `telegram` and nothing else in v0.1; `web` and `cli` are schema violations, because a mapping on a surface that can authenticate nobody is a binding the runtime cannot check. The block is OPTIONAL and ADDITIVE, and its absence is the behaviour of every build before it: the decision is recorded against the identity the deciding process was configured with. Declaring the first entry for a channel turns enforcement on for that whole channel — a decision arriving there with a sender the mapping does not name MUST be refused, with its own reason, `sender-unmapped`, and MUST NOT be recorded under the configured identity. Defaulting instead would be the pre-amendment behaviour wearing the amendment's clothes: a stranger in the configured chat approving as the operator. A sender id declared by two approvers is a LOAD failure, `sender-ambiguous`, so the policy does not load and every class resolves `manual` (the fail-closed rule above): one account names at most one person, and a runtime that resolved the operator's ambiguity by picking would be authoring the answer. The mapping is the operator's ASSERTION that an account belongs to a person, and what makes it accountable is that it lives in this file: `APPROVAL.md` is attested, so an agent can no more add itself as an approver's sender than as an approver. It is not, and an implementation MUST NOT present it as, proof of personhood. Matching is exact, on the id the transport reports, and never on a username, which is mutable and reusable, never a key, and never recorded. (Amended APRV-324.)
139
+
140
+ A mapping value MAY instead be written in a KEYED form, `hmac-sha256:<64 lowercase hex>`, the HMAC-SHA-256 of that same account id under an operator-held key the runtime reads from the environment and never from the policy file. It exists for the deployment that PUBLISHES its policy and its log, where the raw form discloses the account once in the file and then on every decision. A plain unkeyed digest MUST NOT be offered for this purpose: a transport account id is a short number, the space is enumerable, and a digest anybody can reverse states a privacy property it does not have. The form is per entry, so one policy MAY carry both while a migration is in progress, and the two cannot collide. Where a channel carries any keyed entry and the deciding process holds no key, every decision on that channel MUST be refused, with its own reason, `sender-key-unavailable` (§11.2), and an implementation MUST NOT fall back to comparing the observed id against the raw entries: without the key it cannot tell whether the account is also claimed by a keyed approver, so it cannot run the ambiguity check this mapping rests on. The key authenticates nothing and no authorization decision reads it; an implementation MUST NOT present losing it as a loss of safety, only as a loss of the ability to resolve accounts. (Amended APRV-370.)
136
141
  - **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.)
137
- - **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.)
142
+ - **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.) **The bare `supervised` is deprecated.** It is still parsed as `supervised-retro`, so no published policy breaks, and the load-time note above is still recorded wherever it appears. Implementations SHOULD surface it in their health report as well, so an operator meets the deprecation while reading the state of their own repository rather than only in a loader note. A future version of the policy schema removes the spelling, and a policy that writes `supervised-retro` today needs no change then. (Amended APRV-335.)
138
143
  - **`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.)
139
144
  - **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.)
140
145
  - **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,19 +148,22 @@ channels:
143
148
  - **`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.)
144
149
  - **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.)
145
150
  - **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.)
146
- - **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/`): 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.)
147
- - **`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.)
151
+ - **An attestation stores the bytes it attests.** An attestation records a digest, and a digest alone cannot produce the file it names, so a reader asking what the policy IN FORCE actually says has nothing to read. That matters wherever a rule has to be decided against the policy in force rather than against the one being proposed, which is exactly the privileged-gesture rule of §10.3: whoever edited a proposed file must not be able to map their own account as the one entitled to attest it. Every attestation — at a terminal and from a channel alike — MUST therefore store the attested text in the payload store of §10.4 and bind its hash on the `policy.updated` record it appends, and an implementation that cannot store those bytes MUST refuse the attestation rather than append a record whose bytes are unrecoverable. A reader recovering the text MUST re-hash it against the attested digest from the verified log and MUST treat a mismatch as unrecoverable, so the store is checked rather than trusted. The bytes of the policy currently in force are not prunable under `payload_retention` at any age, for the reason a live approval's material is not: something in force binds them. A chain attested before an implementation adopted this rule carries no such binding, and a reader MUST keep its fail-closed answer for that case rather than assume. (APRV-356.)
152
+ - **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.)
153
+ - **Sign-off on a protected path.** A protected path is a file whose edit classifies `policy.edit` or one of its sub-classes: the built-in prose and configuration set, plus everything `protected_paths` widens to. A human signs one off by content with `approval policy attest --path <path>`, which appends a `gate.path.signed_off` event carrying the file's repository-relative path and the SHA-256 of its bytes. The verb is human-only, takes one path per call, and the digest is computed by the runtime from the file on disk, for the reasons the organ attestation above carries: a caller who could supply the hash could ratify bytes nobody read. This record is what RESOLVES the amendment-provenance suffix stated at the head of this document. Text that reached a protected file without a grant carries `(Amended APRV-n, pending sign-off.)` and holds no more authority than a proposal until a human ratifies it; a sign-off over that file's exact bytes is the ratification, and until one exists the suffix stands. It is WHOLE-FILE evidence, and therefore weaker than a grant, which binds the exact change a human was shown. Two rules follow, and both are normative. The after-the-fact enforcement of §10.1 MUST consult a sign-off only after its search for authorization covering the change has failed, and MUST state in its finding that the verdict rests on whole-file evidence; a change some authorization does cover passes on that authorization and names it. And a sign-off is appropriate only for ratifying text a human has READ at that commit: it answers for an edit the gate never saw, and it is never a substitute for taking an edit to the gate while one still can be. The three content records stay distinct, on the reasoning the organ bullet gives: the policy file is `policy.updated`, an organ is `gate.organ.attested`, a protected path is `gate.path.signed_off`, no reader of one may be answered by another, and the verb MUST refuse the policy file, every `policy.core` surface and the log directory, each with its own machine-readable code. (APRV-338.)
154
+ - **`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. The bytes an attestation binds (§5.2, §10.4) are never prunable while that attestation is the one in force: they are not bound to an action and have no terminal state, and the policy in force binds them exactly as a live approval binds its own material. (Policy vocabulary in v0.1; enforcement lands with the M5 daemon. The attestation sentence is APRV-356.)
148
155
  - **`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.)
156
+ - **`read_scope`.** An optional top-level block, `{ roots: [...] }`, naming directories an agent's reads may stay inside, beyond the runtime's built-in ones. A read whose resolved target falls outside every root classifies `read.file.out_of_scope` (§7). The BUILT-IN roots are the GATE ROOT (the directory holding the policy file the runtime resolved), the session scratchpad the harness allots, and the system temp root; they stand whatever this block says or omits, because a runtime denied its own policy, log and workspace cannot run, and because a policy able to NARROW its own read scope would be a policy an agent could edit until nothing was gated. The key is therefore ADDITIVE in the same sense `protected_paths` is, in the opposite direction: that one may only widen what is protected, this one may only widen what is permitted, and neither may shrink. An entry is absolute or resolved against the gate root; globs, negation and values requiring expansion are schema violations, for the reason `protected_paths` rejects them. Comparison is by path segment after the runtime RESOLVES the candidate on disk, so a symlink inside a root that points out of it is outside. Omitting the block entirely is the ordinary case and needs no grammar: a session whose policy sits in one project directory is confined to that project. A policy that fails to load leaves the built-in roots in force while every class resolves to `manual`. (Amended APRV-347.)
149
157
  - **`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.)
150
158
  - **`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.)
151
- - **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.)
159
+ - **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.)
152
160
  - **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.
153
161
 
154
162
  ### 5.3 The values block
155
163
 
156
164
  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.
157
165
 
158
- 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.
166
+ 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 quoted string `"0.2"`, required), three standing lists `love`, `like` and `dislike` (what the human grades the work by, and `like` is also where what the human asks of the agent as behaviour lives: honest opinions on the work, a short journal entry per milestone, an early "I am stuck"), and `communication`, a single string saying how the human reads and answers. An earlier revision carried those requests in a separate `wants` list; they are folded into `like` because a request about behaviour and a preference about the output are graded by the same person in the same way, and one list is easier to keep true than two whose boundary has to be re-decided on every edit. The version is spelled as the policy block's `"0.1"` is, and the quotes are required, since YAML reads a bare dotted identifier as a float; it replaces the integer `1` of the first revision, and a block still carrying that integer fails the values loader alone and never the policy loader, exactly as §5 says of every values failure. 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. (Amended APRV-336.)
159
167
 
160
168
  Three rules bind every surface that reads it.
161
169
 
@@ -242,6 +250,8 @@ Actions whose class resolves to `supervised` or `autonomous` emit no `approval.r
242
250
 
243
251
  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.)
244
252
 
253
+ Three fields answer "who decided" on `approval.granted`, `approval.rejected` and `approval.revoked`, and all three are OPTIONAL and additive (amended APRV-324). The record's top-level `channel` (§8) names the surface that collected the decision; the base schema has defined the field since v0.1 and the decision events never set it, so a reader could not tell a tap on a phone from a line typed into a terminal. `payload.sender` is `{channel, id}`, the account the transport authenticated and the record's `actor` was resolved from, plus an optional `hashed: true` saying that `id` is the keyed digest of §5.2 rather than the account (amended APRV-370). The digest recorded is the value the matched `senders` entry carries, so a reader can correlate a log to a policy without holding the key; where nothing matched, a channel with any keyed entry records the digest of the observed account, which is both the value that discloses least and the line an operator would paste to map it. `hashed` is present-and-`true` or ABSENT, never `false`: absent is the raw form, which is what every record written before the amendment says, and a `false` would make all of them read as though they were missing a field. `payload.sender_source` names the kind of evidence, as a closed vocabulary whose only member is `policy`, the attested `approvers[id].senders` mapping of §5.2, so a later source is distinguishable in a log rather than read back as an operator's attestation. The ABSENCE of the sender pair is meaningful and MUST NOT be defaulted away: it says the attribution came from the deciding process's configuration, which is what every record written before this amendment says. What MUST NOT appear is anything the sender authored about themselves, `from.username` included: the mapping key is the transport's own attribution, a username decays into a lie as it is reassigned, and a field that reads like identity and is not one is worse than no field at all.
254
+
245
255
  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.
246
256
 
247
257
  - **settled** — an `execution.completed` or `execution.failed` closed it. The runtime watched the outcome and wrote down what it saw.
@@ -252,6 +262,8 @@ The execution half of the lifecycle has its own states, and they are custody sta
252
262
 
253
263
  `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.)
254
264
 
265
+ **The harness app-server surface.** A harness MAY stop before an action, ask a client over its own protocol, and wait; a client of that protocol decides through this lifecycle exactly as a hook does, and the surface's claim is bounded by what its request BINDS. The Codex app-server is the instance at v0.1 (`docs/codex-app-server-bridge.md`). It binds the command as the words that will run and as the rendering that arrived, the working directory the harness runtime minted rather than the model reported, and a stable call identity, which together are what a classification and a grant need. It does NOT bind three things, and an implementation MUST NOT claim any of them: the content of an item-based file-change request, which arrives on an earlier frame and is referred to by identity, so approving the identifier approves nothing a reader can check; the guarantee that an action produces a question at all, which belongs to the harness's approval policy and sandbox posture and which a client may pin and check but does not attest; and the guarantee that a question reaches this client, which a harness-side auto-reviewer can pre-empt. A client MUST answer only in the vocabulary the request advertised, MUST NOT send a word that converts one decision into standing authority for a session, and MUST NOT send a word meaning "stop the turn" in place of one meaning "no to this action", because recording an interruption as a denial is a false record of a decision. A client's refusals divide into two closed vocabularies, one for declining a single request and one for ending the session, each exported, frozen and pinned as §11.1 invariant 6 requires of a verb-local union; an implementation that published only the first would describe half its boundary, because a reader of one union cannot tell that the other exists unless it says so. Where the client starts the harness process itself over its own pipes, the custody question of who else may connect is answered by the operating system and the claim is scoped to that process and that session; where it does not, the claim is narrower and an implementation MUST say which it is making. (APRV-368.)
266
+
255
267
  ## 7. Side-effect taxonomy (v0.1)
256
268
 
257
269
  Dotted, hierarchical, extensible. Top-level namespaces are reserved by this spec; implementations MAY add sub-classes freely and SHOULD upstream common ones.
@@ -273,25 +285,38 @@ The developer-workstation namespaces below are reserved alongside them. They nam
273
285
 
274
286
  | Namespace | Examples | Default gravity |
275
287
  |---|---|---|
276
- | `vcs.*` | `.commit.branch`, `.push.branch`, `.push.main`, `.history.rewrite` | autonomous for branch commits and pushes; supervised for the trunk; manual for history rewrites |
288
+ | `vcs.*` | `.commit.branch`, `.push.branch`, `.push.main`, `.history.rewrite`, `.ref.delete` | autonomous for branch commits and pushes; supervised for the trunk; manual for history rewrites and for remote ref deletion |
277
289
  | `deps.*` | `.add`, `.upgrade`, `.remove` | manual: a dependency change is a supply-chain decision |
278
290
  | `release.*` | `.publish`, `.tag`, `.version` | manual, always |
279
291
  | `exec.*` | `.local` (tests, lint, build, scripts inside the workspace) | autonomous |
280
292
  | `network.*` | `.call` (any request beyond package installs) | manual |
281
293
  | `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 |
282
294
  | `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 |
295
+ | `harness.launch.*` | `.codex`, `.muse`, `.grok`, `.claude`, `.cursor` (starting an agent harness session) | manual, always; never inferred autonomous; a policy MAY declare a member human-only |
283
296
 
284
297
  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.)
285
298
 
286
299
  `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.)
287
300
 
301
+ `harness.launch.*` is the start of an agent harness session: one member per harness (`harness.launch.codex`, `.muse`, `.grok`, `.claude`, `.cursor`), with the argv bound to the classified action. A grant of a member of this family authorizes THE LAUNCH and authorizes nothing the launched session then does. That is the whole reason the family exists rather than being folded into `exec.*`: a launched harness runs its own tools outside the granting implementation's gate unless that harness's own adapter is installed and attested, so every file it writes, every command it runs and every request it makes happens where the granting implementation cannot see it, and a single grant would otherwise read as permission for all of it. An implementation MUST therefore resolve this family ONLY under an explicit rule of the operator's policy, `harness.launch.*` or the specific member, and MUST refuse a member that no rule names rather than resolving it by `defaults.autonomy`, with a machine-readable refusal distinct from the one it gives an unclassifiable command and from the one it gives a class reserved to human hands (section 11.1). This is the strong reading of "never inferred", and the family needs it: adding the class to a taxonomy would otherwise make every harness launch grantable by a single approval in every deployment whose defaults are manual, the moment it upgraded, which is a capability arriving by upgrade rather than by decision. The same refusal MUST apply wherever a caller DECLARES the class rather than having it classified from a command line, so that the two paths are one door. An implementation MUST therefore never infer an autonomous default for this family, MUST classify a version or help probe (a `--version`, `-V`, `--help`, `-h` or lone `help` argv, which starts no session) under `read.*` rather than here, and MUST resolve an ambiguous invocation toward the launch: a probe word beside other arguments is a launch, because text cannot say which the binary will honour (section 11.1, fail closed). A harness's own `update` verb stays a supply-chain decision under `deps.*` and MUST NOT be relabelled to this family. An implementation offering a confined entry point of its own, as the reference implementation does with `approval codex start` (section 6.3), MUST keep that entry point's existing class: it is the spelling that brings the inner actions back inside the gate, and pricing it as a launch would price the safe spelling above the unsafe one. Where a harness invocation names a model or another capability-bearing argument, an implementation MAY bind it and MAY route it to a stricter outcome, and MUST NOT let any such self-reported value reduce scrutiny below what the same invocation without it receives (section 11.1 invariant 4). (Amended APRV-354.)
302
+
303
+ `vcs.ref.delete` is the deletion of a remote ref: `git push <remote> --delete <ref>`, the `-d` spelling, the colon refspec (`:refs/heads/x`, `:x`), and the bulk forms that mix several. It is separate from `vcs.push.main` because a push and a deletion are different acts, and a policy that sampled trunk pushes retrospectively was thereby sampling irreversible removals: a push adds commits that remain visible, while a deletion removes the only name an unmerged branch had, and the reflog that could recover it sits on a server the acting party cannot reach. It is separate from `vcs.history.rewrite`, which guards shared history that is being moved rather than removed. An implementation that offers the class MUST bind the ref names it matched to the classified action, MUST keep a force push in `vcs.history.rewrite` even when the same command deletes, and MUST NOT use the class to loosen a deletion that already takes a stricter one: a tag deletion stays `release.publish` wherever a tag push does, and a deletion whose refs the text cannot read stays in the class with nothing bound rather than falling back to a push class (§11.1, fail closed). A policy that declares no line for the class resolves it by `defaults.autonomy` like any other, so adopting it is an explicit act. (Amended APRV-352.)
304
+
288
305
  `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.
289
306
 
290
- 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).
307
+ | Namespace | Examples | Default gravity |
308
+ |---|---|---|
309
+ | `read.file.out_of_scope` | a file read whose resolved target falls outside every read root of §5.2 | manual |
310
+
311
+ `read.file.out_of_scope` is the read-side sibling of `files.delete.out_of_scope`: a sub-class of `read.*` rather than a replacement for it, so a read INSIDE the scope keeps whatever autonomy `read.*` carries. An implementation that offers it MUST decide it from the RESOLVED target and MUST resolve ambiguity toward the class: a target whose expansion the classifier cannot see, a target that resolves outside the roots through a symlink, and a target nothing can resolve at all are each out of scope (§11.1, fail closed). A read command naming no target reads the working directory, and the working directory is what MUST be checked in its place. A harness read tool naming a path is classified by the same roots; one naming no path is not a gate question, because a path the harness did not send is not a path an implementation may invent. Implementations MUST NOT resolve the scope against a working directory the party under oversight reports (§11.1 invariant 4). A policy that declares no read line at all resolves this class by `defaults.autonomy` like any other, so adopting the class is an explicit act rather than a silent tightening. (Amended APRV-347.)
312
+
313
+ 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.)
314
+
315
+ 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.)
291
316
 
292
- The irreversibility floor resolves to `manual`: an action declared `reversible: false` MUST NOT execute under `autonomous`, `supervised-retro`, or `supervised-live` at any rate, regardless of policy. Retrospective audit cannot undo an irreversible action, so execute-then-sample is not meaningful oversight for one, and a fraction of an irreversible class is still a fraction that runs unreviewed. Implementations MUST apply the floor after class resolution and record in the decision trace when the floor, rather than the matched rule, determined the outcome; a floored resolution carries no supervision mode and no rate, because it is gated rather than supervised. **The floor stops at `manual` and MUST NOT reach `human-only`** (§5.2), in either direction. It never raises a class TO `human-only`, because a floor is a runtime escalation computed from a self-reported field, while `human-only` is a declaration its author makes about who performs an action; deriving one from the other would let a `reversible: false` on an envelope reserve to human hands a class its author put behind an ordinary gate. It never lowers a `human-only` class either, so an irreversible action in such a class stays `human-only` and the floor records that it decided nothing. (Amended APRV-185.)
317
+ 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.)
293
318
 
294
- **The floor is a floor, not a proof.** `reversible` is self-reported by the party whose action is under oversight, so §11.1 invariant 4 governs it: the field may raise scrutiny and may never lower it. A truthful `false` raises the class to `manual`, which is the safe direction and the only direction this field moves anything; a false claim of `true`, or an omitted claim, simply fails to raise it and leaves the class's declared autonomy in force. So the floor catches the honest declaration of irreversibility and cannot catch a lie, and no implementation may present it as evidence that anything else is reversible. What answers the lie is the class rule: an author who does not trust a class's declarations writes `manual` for the class, which no declaration can loosen. The same reading applies where the declaration selects the shape of a reconciliation obligation (§5.2): it chooses between two obligations that both exist, and cannot remove one. (Amended APRV-127.)
319
+ **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.)
295
320
 
296
321
  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).
297
322
 
@@ -308,8 +333,11 @@ For `record.*` classes, grant means adoption: the action proposes a write to a s
308
333
  ```
309
334
 
310
335
  - `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/)).
311
- - **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`.
312
- - **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.)
336
+ - **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.)
337
+ - **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`, `gate.path.signed_off`, `log.checkpoint`.
338
+ - **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` and `workspace-commit-unknown` at v0.1) and, when present, an `exit_code` of `null`. `workspace-commit-unknown` is the local counterpart of `act-threw`: a transaction over several files in one workspace was applied and the workspace then read back as neither the approved before-state nor the approved after-state, which POSIX permits because it offers no atomic multi-file rename. The two are separate members rather than one, because a reader deciding what to do next needs to know whether the unknown effect was a remote call or a half-written local tree, and the recoveries have nothing in common. An implementation writing this reason MUST retain, outside the log, a durable record naming the before-state and the after-state it was moving between, and MUST NOT resolve the outcome from it: recovery reports which of the two states it can prove and leaves anything else to `execution.reconciled`. (Amended APRV-325.2, pending sign-off.) `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.)
339
+ - **A refused gesture that is not a decision.** `audit.gesture_refused` records a human gesture that a decision surface refused before any verb ran, where the gesture is not a grant, a reject or a revoke and therefore cannot be an `audit.decision_refused`: a checkpoint signature (§9) and a retrospective review (§10.1) are the two at v0.1. The refusal record that exists requires an `action_key` and a `decision`, and neither gesture has either, so an implementation MUST NOT manufacture them to reuse it; without a type of its own, an attempt made from an account the operator did not map spends a person's attention and leaves no trace in the log at all. The record carries a `system:` actor and never any other, for the reason `audit.dark_session` does, and the surface in `channel`; its payload carries `gesture` and `code`, each drawn from a closed set, an optional `message`, the observed `sender` where the surface authenticated one, and `actor` — the person it was refused for, `human:` — which is REQUIRED exactly when no `sender` is present, the same conditional the decision record takes. It is AUDIT TIER on the strict terms that record set: it authorizes nothing, settles no request, charges no budget, is not sampled, and no enforcement path reads it. Refusals handed to agents stay unlogged, as they do for a decision. (APRV-355.)
340
+ - **A question answered by something other than the gate.** `audit.question_preempted` records that another system resolved an approval question this gate exists to ask, before the gate was asked. The first instance is a harness-side auto-reviewer that answers with a model call and discloses it afterwards (`docs/codex-app-server-bridge.md`, question 3). None of the three audit records above it carries the fact: `audit.dark_session` is a sweep over activity with no records beside it, and both refusal records are about a human gesture; here the question existed, the gate would have asked it, and somebody else answered first, so a log that stayed silent would be indistinguishable from a session that never wanted to act. The record carries a `system:` actor and never any other, for the reason `audit.dark_session` does, and both parties to the event here are machines, which makes that rule sharper rather than softer. Its payload carries `source`, drawn from a closed set naming who answered, and `question`, the answering party's OWN identifiers for it (`id` required, with `method`, `thread` and `turn` where the disclosure named them), plus an optional `verdict`, verbatim in the answering party's vocabulary. An implementation MUST NOT default the verdict: a record that stated one because the disclosure named none would be this runtime inventing another party's decision. It is AUDIT TIER on the strict terms `audit.decision_refused` set: it authorizes nothing, settles no request, charges no budget, is not sampled, and no enforcement path reads it. A health check MAY read it, which is a diagnosis rather than an enforcement, and a health row that reports no such record MUST NOT be read as a statement that the other party is disabled. (APRV-378.)
313
341
  - **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.)
314
342
  - 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.
315
343
  - 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.
@@ -338,6 +366,8 @@ Every displayed field is one of two kinds and MUST be visibly distinguished: **c
338
366
 
339
367
  ```
340
368
  approval init # scaffold APPROVAL.md, .approval/, gitignore
369
+ approval quickstart # human-only solo setup, review exact policy
370
+ # bytes, type understood, then attest
341
371
  approval instructions # full agent-facing usage guide (also in --help)
342
372
  approval register <task-file> # validate envelope, append task.registered
343
373
  approval request <task> [--action <key>] # -> approval.requested (manual classes;
@@ -349,7 +379,11 @@ approval token <action-key> # report execution-token status (the token it
349
379
  # is printed once, by `grant`; only its hash is logged)
350
380
  approval run -- <cmd…> # gate arbitrary commands: mints token, runs, logs
351
381
  approval queue [--json] # pending requests
352
- approval log verify | tail | export
382
+ approval log verify | tail | export | follow
383
+ approval log follow --from <seq> [--cursor-hash <64hex>] --json
384
+ # foreground JSON Lines; exclusive from defaults to 0
385
+ # exit 0: SIGINT/SIGTERM or downstream pipe closure;
386
+ # existing exits 1/2/3/4: integrity/usage/torn/I-O
353
387
  approval log sync # fast-forward the committed log under the append
354
388
  # lock, with a snapshot and a chain reconcile
355
389
  approval log advance [--pr] # commit the log's new records onto a records
@@ -373,6 +407,8 @@ approval hook claude-code # gate an agent harness: reads a PreToolUse
373
407
  approval hook cursor # gate a local Cursor Agent: native
374
408
  # preToolUse JSON in, {permission}
375
409
  # allow/deny out (never "ask")
410
+ approval hook codex # experimental native Codex hook: direct apply_patch
411
+ # only; Bash is explicitly refused (§10.6)
376
412
  approval hook classify -- <cmd…> # what the classifier makes of a command
377
413
  approval reindex | render
378
414
  approval daemon run # the §10.2 watch loop, in the foreground
@@ -414,6 +450,12 @@ A request MAY declare self-delivery, which mints the sealed delivery address reg
414
450
 
415
451
  **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.)
416
452
 
453
+ **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.)
454
+
455
+ **Ratifying a protected file's text.** `approval policy attest --path <path>` is the human-only verb that records a sign-off on one protected path's exact bytes (§5.2). It computes the digest itself, and it refuses a non-human actor, the policy file, every `policy.core` surface and the log directory, each with its own machine-readable code and each decided before the file is read, so a path the verb would never sign is refused whether or not it exists. Implementations MUST classify the invocation at the tier their taxonomy gives the gate's other ceremonies, so that a harness adapter denies it to an agent before the verb's own actor rule is reached: a party under oversight able to ratify its own text would make the pending-sign-off suffix decorative. The health report of this section MUST list protected files whose text carries that suffix with no matching record, and it MUST NOT fail on one. Unratified prose breaks nothing on the machine reporting it, the enforcement that does bite is the after-the-fact check on a pull request, and a health check that failed on a suffix would be the runtime grading a human's own sentence about their own text. (Amended APRV-338.)
456
+
457
+ **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.)
458
+
417
459
  ### 10.2 Daemon
418
460
 
419
461
  `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.
@@ -440,7 +482,19 @@ The reference runtime ships the daemon as a CLI verb, `approval daemon run`, run
440
482
 
441
483
  ### 10.3 Channels
442
484
 
443
- 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.
485
+ 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; the callback is verified against the configured chat id, and, where the policy declares a sender mapping, against the account id the transport attributes it to). Channel breadth is explicitly out of scope; HumanLayer exists for Slack/email/SMS enterprises.
486
+
487
+ **What each channel authenticates (amended APRV-324).** The sentence above used to say the Telegram callback was "verified against approver identity". It was not: `routeCallback` compared `message.chat.id` to the configured chat and never read the update's `from` object at all, so the verification was of a CONVERSATION and the identity on the record was the one the listener process was launched with. The correction and the capability arrive together, and the table states what a transport can prove rather than what an implementation would like it to.
488
+
489
+ | channel | what arrives | what it proves | what it does not |
490
+ |---|---|---|---|
491
+ | telegram | `callback_query.from.id`, `from.username`, `message.chat.id` | that Telegram attributes this tap to that account, in a chat the operator configured | that the account belongs to the person the operator thinks; nothing about device possession; nothing if the bot token leaks. `from.username` proves nothing durable at all: usernames are mutable and reusable, so they are never a mapping key and are never recorded |
492
+ | web | an unauthenticated form POST | nothing | no session, no origin binding, no identity of any kind |
493
+ | cli | the process environment | local machine control | who is at the keyboard |
494
+
495
+ **Every gesture a channel collects, not only decisions (amended APRV-324).** A push channel collects four kinds of human act, and a mapping that reached only the first would leave the other three deciding under the listener's own identity — which is the whole of what the mapping removes, kept on the most privileged gestures. So the rule binds all four. **Decisions** (grant, reject) resolve against the policy in force, as above. **Retrospective reviews** resolve the same way: a review confers no authority and is still a human's observation that §5.3's guidance surfaces hand to agents, so a review attributed to the wrong person is guidance in somebody else's name. **Checkpoint signatures** resolve the same way, because a signature says this log's head is what this person saw. **Attestation answers** are the exception, and are stricter rather than looser: the amendment a tap attests may itself add, remove or repoint the sender mapping, so resolving it against the file being attested would let whoever wrote that file name the account that approves their own edit. An attestation answer MUST therefore resolve against the policy **in force** — the bytes the latest attestation names — and never against the bytes it is attesting. Where those bytes cannot be recovered (an attestation records only their digest), or where the amendment changes the mapping and the policy in force maps no sender for that channel, the answer MUST be refused with its own reason, `attest-requires-terminal`: an amendment that introduces the identity system cannot be signed for by the identity system it introduces, and a terminal authenticates no sender and is where a `policy.core` edit happens anyway. A refused gesture of any of these kinds records no authoritative event. A checkpoint signature and a review MUST additionally resolve only against an **attested** policy: a decision is protected afterwards by the grant's own attestation and drift checks, and these two act and append with no such check behind them, so an edited `APPROVAL.md` would otherwise change who may sign or review from a phone before any human had attested it — which §5.2 says a policy cannot do. Where the bytes on disk are not the bytes in force, such a gesture MUST be refused with the existing `policy-not-attested` reason and MUST record nothing; a terminal, which authenticates no sender, is unaffected and remains the repair.
496
+
497
+ So exactly one channel has a sender fact worth mapping, and an implementation MUST treat it as evidence about an account rather than about a person. Where the policy declares `approvers.<id>.senders` for a channel (§5.2), a decision collected on that channel MUST be recorded against the human the attested mapping names, and a sender the mapping does not name MUST be refused rather than attributed to the listener's own identity. Where the policy declares none, the decision is recorded against the configured identity exactly as it was before this amendment, which is the compatibility promise: a deployment that never writes a `senders` key never changes behaviour. The resolution happens at the channel boundary, before the gate's own verbs run, so `approvers`, `actor-not-approver` and every other authorization check of §5.2 apply unchanged to an identity that is now better evidenced. A channel that authenticates no sender MUST NOT supply one, and a `sender` field in a request body, a message, a callback payload or a username is never an identity (§11.1 invariant 4). (Amended APRV-324.)
444
498
 
445
499
  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.
446
500
 
@@ -472,7 +526,7 @@ The ceremony owns its own git preconditions. It fetches the remote, refuses unle
472
526
 
473
527
  ### 10.4 Adapters and hard enforcement
474
528
 
475
- Adapters (e.g. `adapter-email`, `adapter-agentmail`, `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.") 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.)
529
+ 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.)
476
530
 
477
531
  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.
478
532
 
@@ -485,7 +539,7 @@ An executor that spawns a child MUST construct that child's environment rather t
485
539
 
486
540
  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.)
487
541
 
488
- The reference runtime gives that boundary a definite shape. An adapter implements one method, `act`, over two things: the payload the grant bound to, 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 and consume the token, 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 verified-token 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.)
542
+ 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.)
489
543
 
490
544
  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.)
491
545
 
@@ -495,15 +549,27 @@ Indeterminate is a custody state, not a result. The consumption is burned: the t
495
549
 
496
550
  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.)
497
551
 
498
- 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 verified-token window. 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.)
552
+ 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.)
499
553
 
500
554
  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.
501
555
 
556
+ **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.)
557
+
502
558
  ### 10.5 MCP wrapper (optional)
503
559
 
504
560
  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.
505
561
 
506
- 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. Three agent-facing verbs are withheld as well, for transport reasons rather than authority reasons: `consume`, which is internal plumbing that `run` wraps, and `hook claude-code` / `hook cursor`, which each read their event from the stdin this transport already owns. A conformance reader building the tool list from this section takes it to be the registry filtered by `human_only`, minus those three. 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.)
562
+ 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.)
563
+
564
+ ### 10.6 Experimental native Codex hook adapter (Amended APRV-313)
565
+
566
+ 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.
567
+
568
+ 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.
569
+
570
+ 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.
571
+
572
+ 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.)
507
573
 
508
574
  ## 11. Security and control model
509
575
 
@@ -538,7 +604,7 @@ The following hold across every surface of the runtime. They are implicit accept
538
604
  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.)
539
605
  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`).
540
606
  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.)
541
- 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.)
607
+ 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.)
542
608
  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`).
543
609
  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.)
544
610
  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.)
@@ -550,7 +616,7 @@ The following hold across every surface of the runtime. They are implicit accept
550
616
 
551
617
  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.)
552
618
 
553
- 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.)
619
+ 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.) It also covers `channel_decision_refusal_codes`, which is not gate-facing and is registered here anyway: the refusals a decision surface makes are answers a human reads on their phone, and a vocabulary a second implementation has to guess at is exactly what this registry exists to stop. (Amended APRV-324.)
554
620
 
555
621
  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.)
556
622
 
@@ -664,6 +730,15 @@ Three properties bind the whole registry. A code fires for exactly the condition
664
730
  | `log-corrupt` | The chain does not verify, so nothing is derived from it. |
665
731
  | `append-failed` | The append itself failed, carrying the writer's own error. |
666
732
 
733
+ **`channel_decision_refusal_codes`** — every way a decision SURFACE can refuse a human's gesture before the gate is called, in definition order. Its own union rather than two more members of `gate_refusal_codes`, because `decide` emits neither: the sender is resolved against the attested policy at the channel boundary, and a second implementation whose gate emitted one would be describing a different boundary. Both fire only where a channel supplied a sender, so a surface that authenticates nobody can reach neither. (Amended APRV-324.)
734
+
735
+ | code | fires when |
736
+ |---|---|
737
+ | `sender-unmapped` | A channel supplied a sender and the attested policy maps it to no approver, or the policy could not be loaded at all. Evaluated before the gate's verbs, so nothing is decided and no decision record is appended; ONE `audit.decision_refused` IS appended, carrying the observed `payload.sender` and no `payload.actor`, because the runtime cannot name a person and naming the listener would be a false record. A policy that declares no `senders` for the channel does not reach this code: that is the configured-attribution path of §5.2 and it refuses nothing. |
738
+ | `sender-ambiguous` | The policy in force maps the supplied sender to more than one approver. A policy carrying this does not load (§5.2), so the code is reachable only where a caller supplies a policy that never passed a load; the runtime refuses rather than choosing between two people, and appends the same single audit record. |
739
+ | `sender-key-unavailable` | The attested policy maps that channel's senders in the KEYED form of §5.2 and the deciding process holds no key, so no digest can be computed and no account on the channel can be resolved. It refuses the WHOLE channel, including entries written raw beside keyed ones: the roster's ambiguity check cannot be run over a half nobody can read, and ambiguity resolves to the stricter path (§11.1). An implementation MUST NOT fall back to comparing the observed id against the raw entries. Distinct from `sender-unmapped`, which says the policy answered and did not name this account; this says nothing about the account and everything about the process, and its repair is the key in the environment rather than an amendment. The same single `audit.decision_refused` is appended, carrying the observed id in the RAW form, because there is no key to hash it with and a record naming no account at all would leave an operator unable to say who was in front of a gate that had lost its key. (Amended APRV-370.) |
740
+ | `attest-requires-terminal` | An attestation answer arrived with a sender, and the policy IN FORCE cannot say whether that account may sign for this amendment: either its bytes are not recoverable (an attestation records only their digest), or the amendment changes the `senders` mapping and the policy in force maps no sender for this channel. Attestation answers alone; evaluated before the answer is recorded, so nothing is attested and nothing is declined. The repair is a terminal, which supplies no sender. It is distinct from `sender-unmapped`, which says the policy in force answered and did not name this account. |
741
+
667
742
  **`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.)
668
743
 
669
744
  | code | fires when |
@@ -730,7 +805,7 @@ Milestones sized for agent-driven development (each = one reviewable task):
730
805
  - **M5** Daemon: watch, TTL, sampling, loop-escalation.
731
806
  - **M6** Backlog.md round-trip + AGENTS.md import.
732
807
  - **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).
733
- - **M8** MCP wrapper (§10.5) and the agent-harness hooks, `approval hook claude-code` and `approval hook cursor`: a Claude Code PreToolUse adapter that classifies the command a harness is about to run and resolves it against the policy. Allow is recorded only where the class is gated: a manual class waits on a decision the log records, a supervised class appends `task.registered` and proceeds, an autonomous class appends nothing. 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.)
808
+ - **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.)
734
809
 
735
810
  ## 15. References
736
811