approval-md 0.0.1 → 0.1.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 (522) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +909 -4
  4. package/SPEC.md +445 -32
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.js +1200 -0
  7. package/dist/src/adapters/agentmail.js.map +1 -0
  8. package/dist/src/adapters/conformance.js +461 -0
  9. package/dist/src/adapters/conformance.js.map +1 -0
  10. package/dist/src/adapters/contract.js +941 -0
  11. package/dist/src/adapters/contract.js.map +1 -0
  12. package/dist/src/adapters/email.js +749 -0
  13. package/dist/src/adapters/email.js.map +1 -0
  14. package/dist/src/adapters/env-passphrase.js +132 -0
  15. package/dist/src/adapters/env-passphrase.js.map +1 -0
  16. package/dist/src/adapters/registry.js +76 -0
  17. package/dist/src/adapters/registry.js.map +1 -0
  18. package/dist/src/adapters/smtp.js +499 -0
  19. package/dist/src/adapters/smtp.js.map +1 -0
  20. package/dist/src/adapters/vault-provider.js +161 -0
  21. package/dist/src/adapters/vault-provider.js.map +1 -0
  22. package/dist/src/channels/batch.js +121 -0
  23. package/dist/src/channels/batch.js.map +1 -0
  24. package/dist/src/channels/cli.js +468 -0
  25. package/dist/src/channels/cli.js.map +1 -0
  26. package/dist/src/channels/conformance.js +445 -0
  27. package/dist/src/channels/conformance.js.map +1 -0
  28. package/dist/src/channels/contract.js +494 -0
  29. package/dist/src/channels/contract.js.map +1 -0
  30. package/dist/src/channels/payload-view.js +43 -0
  31. package/dist/src/channels/payload-view.js.map +1 -0
  32. package/dist/src/channels/render-queue.js +564 -0
  33. package/dist/src/channels/render-queue.js.map +1 -0
  34. package/dist/src/channels/tagging.js +723 -0
  35. package/dist/src/channels/tagging.js.map +1 -0
  36. package/dist/src/channels/telegram.js +3190 -0
  37. package/dist/src/channels/telegram.js.map +1 -0
  38. package/dist/src/channels/web.js +903 -0
  39. package/dist/src/channels/web.js.map +1 -0
  40. package/dist/src/cli/adapter.js +278 -0
  41. package/dist/src/cli/adapter.js.map +1 -0
  42. package/dist/src/cli/amend.js +2171 -0
  43. package/dist/src/cli/amend.js.map +1 -0
  44. package/dist/src/cli/args.js +86 -0
  45. package/dist/src/cli/args.js.map +1 -0
  46. package/dist/src/cli/attest.js +307 -0
  47. package/dist/src/cli/attest.js.map +1 -0
  48. package/dist/src/cli/audit-card.js +201 -0
  49. package/dist/src/cli/audit-card.js.map +1 -0
  50. package/dist/src/cli/audit.js +460 -0
  51. package/dist/src/cli/audit.js.map +1 -0
  52. package/dist/src/cli/channel-telegram.js +2063 -0
  53. package/dist/src/cli/channel-telegram.js.map +1 -0
  54. package/dist/src/cli/channel-web.js +357 -0
  55. package/dist/src/cli/channel-web.js.map +1 -0
  56. package/dist/src/cli/channel.js +438 -0
  57. package/dist/src/cli/channel.js.map +1 -0
  58. package/dist/src/cli/checkpoint-tap.js +238 -0
  59. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  60. package/dist/src/cli/coverage.js +343 -0
  61. package/dist/src/cli/coverage.js.map +1 -0
  62. package/dist/src/cli/daemon.js +631 -0
  63. package/dist/src/cli/daemon.js.map +1 -0
  64. package/dist/src/cli/doctor.js +2648 -0
  65. package/dist/src/cli/doctor.js.map +1 -0
  66. package/dist/src/cli/env.js +302 -0
  67. package/dist/src/cli/env.js.map +1 -0
  68. package/dist/src/cli/execute.js +1682 -0
  69. package/dist/src/cli/execute.js.map +1 -0
  70. package/dist/src/cli/exit-codes.js +82 -0
  71. package/dist/src/cli/exit-codes.js.map +1 -0
  72. package/dist/src/cli/feedback.js +205 -0
  73. package/dist/src/cli/feedback.js.map +1 -0
  74. package/dist/src/cli/gate-window.js +294 -0
  75. package/dist/src/cli/gate-window.js.map +1 -0
  76. package/dist/src/cli/gate.js +557 -0
  77. package/dist/src/cli/gate.js.map +1 -0
  78. package/dist/src/cli/git-scope.js +295 -0
  79. package/dist/src/cli/git-scope.js.map +1 -0
  80. package/dist/src/cli/gloss-attach.js +107 -0
  81. package/dist/src/cli/gloss-attach.js.map +1 -0
  82. package/dist/src/cli/gloss-codex-child.js +149 -0
  83. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  84. package/dist/src/cli/gloss-codex.js +255 -0
  85. package/dist/src/cli/gloss-codex.js.map +1 -0
  86. package/dist/src/cli/gloss-options.js +79 -0
  87. package/dist/src/cli/gloss-options.js.map +1 -0
  88. package/dist/src/cli/gloss.js +362 -0
  89. package/dist/src/cli/gloss.js.map +1 -0
  90. package/dist/src/cli/help.js +2217 -0
  91. package/dist/src/cli/help.js.map +1 -0
  92. package/dist/src/cli/hook.js +2743 -0
  93. package/dist/src/cli/hook.js.map +1 -0
  94. package/dist/src/cli/import.js +175 -0
  95. package/dist/src/cli/import.js.map +1 -0
  96. package/dist/src/cli/init.js +336 -0
  97. package/dist/src/cli/init.js.map +1 -0
  98. package/dist/src/cli/instructions.js +262 -0
  99. package/dist/src/cli/instructions.js.map +1 -0
  100. package/dist/src/cli/journal.js +238 -0
  101. package/dist/src/cli/journal.js.map +1 -0
  102. package/dist/src/cli/log-advance.js +749 -0
  103. package/dist/src/cli/log-advance.js.map +1 -0
  104. package/dist/src/cli/log-anchor.js +387 -0
  105. package/dist/src/cli/log-anchor.js.map +1 -0
  106. package/dist/src/cli/log-checkpoint.js +128 -0
  107. package/dist/src/cli/log-checkpoint.js.map +1 -0
  108. package/dist/src/cli/log-sync.js +849 -0
  109. package/dist/src/cli/log-sync.js.map +1 -0
  110. package/dist/src/cli/log-verbs.js +354 -0
  111. package/dist/src/cli/log-verbs.js.map +1 -0
  112. package/dist/src/cli/long-help.js +148 -0
  113. package/dist/src/cli/long-help.js.map +1 -0
  114. package/dist/src/cli/main.js +1056 -0
  115. package/dist/src/cli/main.js.map +1 -0
  116. package/dist/src/cli/mcp.js +306 -0
  117. package/dist/src/cli/mcp.js.map +1 -0
  118. package/dist/src/cli/paths.js +79 -0
  119. package/dist/src/cli/paths.js.map +1 -0
  120. package/dist/src/cli/payload.js +253 -0
  121. package/dist/src/cli/payload.js.map +1 -0
  122. package/dist/src/cli/policy.js +229 -0
  123. package/dist/src/cli/policy.js.map +1 -0
  124. package/dist/src/cli/preflight.js +888 -0
  125. package/dist/src/cli/preflight.js.map +1 -0
  126. package/dist/src/cli/progress.js +112 -0
  127. package/dist/src/cli/progress.js.map +1 -0
  128. package/dist/src/cli/prompt.js +312 -0
  129. package/dist/src/cli/prompt.js.map +1 -0
  130. package/dist/src/cli/records.js +66 -0
  131. package/dist/src/cli/records.js.map +1 -0
  132. package/dist/src/cli/render.js +132 -0
  133. package/dist/src/cli/render.js.map +1 -0
  134. package/dist/src/cli/sandbox.js +150 -0
  135. package/dist/src/cli/sandbox.js.map +1 -0
  136. package/dist/src/cli/scaffold.js +137 -0
  137. package/dist/src/cli/scaffold.js.map +1 -0
  138. package/dist/src/cli/setup-adapter.js +475 -0
  139. package/dist/src/cli/setup-adapter.js.map +1 -0
  140. package/dist/src/cli/setup-channel.js +635 -0
  141. package/dist/src/cli/setup-channel.js.map +1 -0
  142. package/dist/src/cli/setup-checkpoint.js +196 -0
  143. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  144. package/dist/src/cli/setup-common.js +376 -0
  145. package/dist/src/cli/setup-common.js.map +1 -0
  146. package/dist/src/cli/setup-flow.js +476 -0
  147. package/dist/src/cli/setup-flow.js.map +1 -0
  148. package/dist/src/cli/setup-service.js +308 -0
  149. package/dist/src/cli/setup-service.js.map +1 -0
  150. package/dist/src/cli/setup.js +473 -0
  151. package/dist/src/cli/setup.js.map +1 -0
  152. package/dist/src/cli/style.js +469 -0
  153. package/dist/src/cli/style.js.map +1 -0
  154. package/dist/src/cli/token.js +274 -0
  155. package/dist/src/cli/token.js.map +1 -0
  156. package/dist/src/cli/up.js +847 -0
  157. package/dist/src/cli/up.js.map +1 -0
  158. package/dist/src/cli/usage.js +91 -0
  159. package/dist/src/cli/usage.js.map +1 -0
  160. package/dist/src/cli/values.js +189 -0
  161. package/dist/src/cli/values.js.map +1 -0
  162. package/dist/src/cli/vault.js +362 -0
  163. package/dist/src/cli/vault.js.map +1 -0
  164. package/dist/src/cli/verb-registry.js +2173 -0
  165. package/dist/src/cli/verb-registry.js.map +1 -0
  166. package/dist/src/cli/wordmark.js +52 -0
  167. package/dist/src/cli/wordmark.js.map +1 -0
  168. package/dist/src/core/advance-cycle.js +200 -0
  169. package/dist/src/core/advance-cycle.js.map +1 -0
  170. package/dist/src/core/agents-md.js +747 -0
  171. package/dist/src/core/agents-md.js.map +1 -0
  172. package/dist/src/core/attest.js +577 -0
  173. package/dist/src/core/attest.js.map +1 -0
  174. package/dist/src/core/audit.js +882 -0
  175. package/dist/src/core/audit.js.map +1 -0
  176. package/dist/src/core/budgets.js +449 -0
  177. package/dist/src/core/budgets.js.map +1 -0
  178. package/dist/src/core/checkpoint.js +738 -0
  179. package/dist/src/core/checkpoint.js.map +1 -0
  180. package/dist/src/core/child-env.js +86 -0
  181. package/dist/src/core/child-env.js.map +1 -0
  182. package/dist/src/core/clock.js +43 -0
  183. package/dist/src/core/clock.js.map +1 -0
  184. package/dist/src/core/command-class.js +2321 -0
  185. package/dist/src/core/command-class.js.map +1 -0
  186. package/dist/src/core/coverage-sources/adapter.js +71 -0
  187. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  188. package/dist/src/core/coverage-sources/gh.js +136 -0
  189. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  190. package/dist/src/core/coverage-sources/git.js +269 -0
  191. package/dist/src/core/coverage-sources/git.js.map +1 -0
  192. package/dist/src/core/coverage.js +337 -0
  193. package/dist/src/core/coverage.js.map +1 -0
  194. package/dist/src/core/credential-spec.js +23 -0
  195. package/dist/src/core/credential-spec.js.map +1 -0
  196. package/dist/src/core/dark-session.js +714 -0
  197. package/dist/src/core/dark-session.js.map +1 -0
  198. package/dist/src/core/decision-refusal.js +265 -0
  199. package/dist/src/core/decision-refusal.js.map +1 -0
  200. package/dist/src/core/env-file.js +837 -0
  201. package/dist/src/core/env-file.js.map +1 -0
  202. package/dist/src/core/execute.js +1233 -0
  203. package/dist/src/core/execute.js.map +1 -0
  204. package/dist/src/core/frontmatter.js +100 -0
  205. package/dist/src/core/frontmatter.js.map +1 -0
  206. package/dist/src/core/gate-window.js +506 -0
  207. package/dist/src/core/gate-window.js.map +1 -0
  208. package/dist/src/core/gate.js +2947 -0
  209. package/dist/src/core/gate.js.map +1 -0
  210. package/dist/src/core/git-run.js +93 -0
  211. package/dist/src/core/git-run.js.map +1 -0
  212. package/dist/src/core/harness-version.js +210 -0
  213. package/dist/src/core/harness-version.js.map +1 -0
  214. package/dist/src/core/harness-wait.js +58 -0
  215. package/dist/src/core/harness-wait.js.map +1 -0
  216. package/dist/src/core/head-retry.js +121 -0
  217. package/dist/src/core/head-retry.js.map +1 -0
  218. package/dist/src/core/instance.js +319 -0
  219. package/dist/src/core/instance.js.map +1 -0
  220. package/dist/src/core/intake-limits.js +350 -0
  221. package/dist/src/core/intake-limits.js.map +1 -0
  222. package/dist/src/core/jcs.js +132 -0
  223. package/dist/src/core/jcs.js.map +1 -0
  224. package/dist/src/core/journal.js +200 -0
  225. package/dist/src/core/journal.js.map +1 -0
  226. package/dist/src/core/live-draw.js +703 -0
  227. package/dist/src/core/live-draw.js.map +1 -0
  228. package/dist/src/core/log-reconcile.js +136 -0
  229. package/dist/src/core/log-reconcile.js.map +1 -0
  230. package/dist/src/core/log.js +546 -0
  231. package/dist/src/core/log.js.map +1 -0
  232. package/dist/src/core/loop.js +476 -0
  233. package/dist/src/core/loop.js.map +1 -0
  234. package/dist/src/core/md-fence.js +74 -0
  235. package/dist/src/core/md-fence.js.map +1 -0
  236. package/dist/src/core/money.js +195 -0
  237. package/dist/src/core/money.js.map +1 -0
  238. package/dist/src/core/payload-census.js +146 -0
  239. package/dist/src/core/payload-census.js.map +1 -0
  240. package/dist/src/core/payload-store.js +340 -0
  241. package/dist/src/core/payload-store.js.map +1 -0
  242. package/dist/src/core/payload.js +80 -0
  243. package/dist/src/core/payload.js.map +1 -0
  244. package/dist/src/core/policy-diff.js +565 -0
  245. package/dist/src/core/policy-diff.js.map +1 -0
  246. package/dist/src/core/policy-expectations.js +394 -0
  247. package/dist/src/core/policy-expectations.js.map +1 -0
  248. package/dist/src/core/policy-explain.js +230 -0
  249. package/dist/src/core/policy-explain.js.map +1 -0
  250. package/dist/src/core/policy-load.js +524 -0
  251. package/dist/src/core/policy-load.js.map +1 -0
  252. package/dist/src/core/policy-match.js +467 -0
  253. package/dist/src/core/policy-match.js.map +1 -0
  254. package/dist/src/core/policy-proposal.js +458 -0
  255. package/dist/src/core/policy-proposal.js.map +1 -0
  256. package/dist/src/core/prompt-layout.js +422 -0
  257. package/dist/src/core/prompt-layout.js.map +1 -0
  258. package/dist/src/core/protected-path-guard.js +1087 -0
  259. package/dist/src/core/protected-path-guard.js.map +1 -0
  260. package/dist/src/core/registration.js +39 -0
  261. package/dist/src/core/registration.js.map +1 -0
  262. package/dist/src/core/reindex.js +336 -0
  263. package/dist/src/core/reindex.js.map +1 -0
  264. package/dist/src/core/sampler.js +388 -0
  265. package/dist/src/core/sampler.js.map +1 -0
  266. package/dist/src/core/sandbox.js +424 -0
  267. package/dist/src/core/sandbox.js.map +1 -0
  268. package/dist/src/core/seal.js +290 -0
  269. package/dist/src/core/seal.js.map +1 -0
  270. package/dist/src/core/state.js +1009 -0
  271. package/dist/src/core/state.js.map +1 -0
  272. package/dist/src/core/task-file.js +464 -0
  273. package/dist/src/core/task-file.js.map +1 -0
  274. package/dist/src/core/telegram-config.js +114 -0
  275. package/dist/src/core/telegram-config.js.map +1 -0
  276. package/dist/src/core/token.js +578 -0
  277. package/dist/src/core/token.js.map +1 -0
  278. package/dist/src/core/validate.js +0 -0
  279. package/dist/src/core/validate.js.map +1 -0
  280. package/dist/src/core/values.js +153 -0
  281. package/dist/src/core/values.js.map +1 -0
  282. package/dist/src/core/vault.js +612 -0
  283. package/dist/src/core/vault.js.map +1 -0
  284. package/dist/src/core/verified-snapshot.js +506 -0
  285. package/dist/src/core/verified-snapshot.js.map +1 -0
  286. package/dist/src/core/verify.js +549 -0
  287. package/dist/src/core/verify.js.map +1 -0
  288. package/dist/src/core/version.js +9 -0
  289. package/dist/src/core/version.js.map +1 -0
  290. package/dist/src/core/wysiwys.js +728 -0
  291. package/dist/src/core/wysiwys.js.map +1 -0
  292. package/dist/src/daemon/advance-child.js +78 -0
  293. package/dist/src/daemon/advance-child.js.map +1 -0
  294. package/dist/src/daemon/advance.js +849 -0
  295. package/dist/src/daemon/advance.js.map +1 -0
  296. package/dist/src/daemon/audit.js +90 -0
  297. package/dist/src/daemon/audit.js.map +1 -0
  298. package/dist/src/daemon/daemon.js +1988 -0
  299. package/dist/src/daemon/daemon.js.map +1 -0
  300. package/dist/src/daemon/dark-session.js +119 -0
  301. package/dist/src/daemon/dark-session.js.map +1 -0
  302. package/dist/src/daemon/draw-child.js +132 -0
  303. package/dist/src/daemon/draw-child.js.map +1 -0
  304. package/dist/src/daemon/draw.js +458 -0
  305. package/dist/src/daemon/draw.js.map +1 -0
  306. package/dist/src/daemon/git-evidence.js +345 -0
  307. package/dist/src/daemon/git-evidence.js.map +1 -0
  308. package/dist/src/daemon/projection.js +233 -0
  309. package/dist/src/daemon/projection.js.map +1 -0
  310. package/dist/src/daemon/prune.js +376 -0
  311. package/dist/src/daemon/prune.js.map +1 -0
  312. package/dist/src/mcp/http.js +343 -0
  313. package/dist/src/mcp/http.js.map +1 -0
  314. package/dist/src/mcp/server.js +594 -0
  315. package/dist/src/mcp/server.js.map +1 -0
  316. package/docs/cli-reference.md +5363 -0
  317. package/package.json +43 -4
  318. package/schema/.gitkeep +0 -0
  319. package/schema/LICENSE +117 -0
  320. package/schema/envelope.schema.json +137 -0
  321. package/schema/event.schema.json +1810 -0
  322. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  323. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  324. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  325. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  326. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  327. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  328. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  329. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  330. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  331. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  332. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  333. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  334. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  335. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  336. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  337. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  338. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  339. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  340. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  341. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  342. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  343. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  344. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  345. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  346. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  347. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  348. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  349. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  350. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  351. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  352. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  353. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  354. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  355. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  356. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  357. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  358. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  359. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  360. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  361. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  362. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  363. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  364. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  365. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  366. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  367. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  368. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  370. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  371. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  372. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  373. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  374. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  375. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  376. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  377. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  378. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  379. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  380. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  381. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  382. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  383. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  384. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  385. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  386. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  387. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  388. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  389. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  390. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  391. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  392. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  393. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  394. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  395. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  396. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  397. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  398. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  399. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  400. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  401. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  402. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  403. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  404. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  405. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  406. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  407. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  408. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  409. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  410. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  411. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  412. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  413. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  414. package/schema/fixtures/event/valid/execution-started.json +14 -0
  415. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  416. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  417. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  418. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  419. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  420. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  421. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  422. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  423. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  424. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  425. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  426. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  427. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  428. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  429. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  430. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  431. package/schema/fixtures/event/valid/spec-example.json +15 -0
  432. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  433. package/schema/fixtures/event/valid/task-registered.json +14 -0
  434. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  435. package/schema/fixtures/hash/known-answer.json +74 -0
  436. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  437. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  438. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  439. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  440. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  441. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  442. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  443. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  444. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  445. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  446. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  447. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  448. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  449. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  450. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  451. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  452. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  453. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  454. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  455. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  456. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  457. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  458. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  459. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  460. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  461. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  462. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  463. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  464. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  465. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  466. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  467. package/schema/fixtures/policy/valid/canonical.json +47 -0
  468. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  469. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  470. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  471. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  472. package/schema/fixtures/policy/valid/human-only.json +9 -0
  473. package/schema/fixtures/policy/valid/minimal.json +6 -0
  474. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  475. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  476. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  477. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  478. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  479. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  480. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  481. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  482. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  483. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  484. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  485. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  486. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  487. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  488. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  489. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  490. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  491. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  492. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  493. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  494. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  495. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  496. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  497. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  498. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  499. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  500. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  501. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  502. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  503. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  504. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  505. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  506. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  507. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  508. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  509. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  510. package/schema/fixtures/values/invalid/version-string.json +1 -0
  511. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  512. package/schema/fixtures/values/valid/full.json +20 -0
  513. package/schema/fixtures/values/valid/minimal.json +1 -0
  514. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  515. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  516. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  517. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  518. package/schema/fixtures/values-md/valid/absent.md +50 -0
  519. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  520. package/schema/policy.schema.json +481 -0
  521. package/schema/sample-record.schema.json +26 -0
  522. package/schema/values.schema.json +55 -0
@@ -0,0 +1,481 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://approval.md/schema/policy.schema.json",
4
+ "$comment": "CC0 1.0. See schema/LICENSE.",
5
+ "title": "APPROVAL.md policy block",
6
+ "description": "SPEC.md §5: the parsed YAML of the single ```yaml approval-policy``` fenced block inside APPROVAL.md (or APPROVALS.md). Surrounding prose is ignored by the parser and never reaches this schema. SPEC.md §5.2 requires implementations to fail closed: a document rejected here means the runtime treats every class as `manual`, so this schema is deliberately closed (additionalProperties: false at every level) — an unrecognised key is a policy the author believed was in force and the runtime did not understand, which must be a hard error rather than a silent no-op.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["version"],
10
+ "properties": {
11
+ "version": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "description": "SPEC.md §5.1: policy format version, quoted in YAML so \"0.1\" stays a string rather than becoming the float 0.1. The only REQUIRED top-level key: a block with no version cannot be interpreted safely under any future format revision.",
15
+ "examples": ["0.1"]
16
+ },
17
+ "defaults": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "description": "SPEC.md §5.1/§5.2: fallbacks applied to any action whose class matches no rule in `classes`.",
21
+ "properties": {
22
+ "autonomy": {
23
+ "$ref": "#/$defs/defaultAutonomy",
24
+ "description": "SPEC.md §5.2 (amended, APRV-127; widened, APRV-185): autonomy for actions matching no class rule. Fail-closed policies set this to `manual`. `supervised-live` is NOT admitted here: it is meaningless without a `live_rate`, there is no place on `defaults` to declare one, and a default rate this schema invented would be a gating fraction no author ever wrote. A policy that wants live sampling declares it on the class rules it means it for. `human-only` IS admitted (APRV-185), because it carries no key to be missing: an author who writes it here reserves every unnamed class to human hands, which is an explicit choice of maximal strictness rather than a level a runtime fell back to."
25
+ },
26
+ "channel": {
27
+ "type": "string",
28
+ "minLength": 1,
29
+ "description": "SPEC.md §5.1: default channel name used to surface approval requests. Expected to name a key of `channels`; cross-key consistency is a runtime check (SPEC.md §10.3), not a schema constraint.",
30
+ "examples": ["telegram", "web", "cli"]
31
+ },
32
+ "approval_ttl": {
33
+ "$ref": "#/$defs/duration",
34
+ "description": "SPEC.md §5.1: how long a pending request stays actionable before it expires. Duration string, `<positive integer><unit>` with unit in ms|s|m|h|d|w (e.g. \"24h\"). Integer-only and single-unit by design: compound forms (\"1h30m\") and fractional values (\"1.5h\") are rejected so every implementation parses the same string to the same number of milliseconds.",
35
+ "examples": ["24h"]
36
+ },
37
+ "token_delivery": {
38
+ "type": "string",
39
+ "enum": ["manual", "sealed"],
40
+ "description": "SPEC.md §10.4 (amended, APRV-105): how the raw execution token reaches the process that will spend it. `manual` (THE DEFAULT, and what an absent key means): the token is printed once on the granting surface and carried to the spender by a human. `sealed`: `approval request` also mints an ephemeral X25519 keypair per request, keeps the private half 0600 beside the log, and publishes the public half on `approval.requested` as `token_recipient_key`; the grant then seals the raw token to that key and records the ciphertext as `token_sealed`, so `approval wait` can hand the token back to the process that asked for it — across machines, with the log synced through git. The default is `manual` because the fail-closed reading of an absent key is the behaviour that requires a human, and because sealed delivery puts ciphertext in a public, permanent log. The keypair ADDRESSES and does not AUTHORIZE: a token still exists only after a human grant, still binds to the exact payload bytes, and is still single-use. What sealing changes is who can READ a minted token, which is the human's clipboard today.",
41
+ "default": "manual",
42
+ "examples": ["manual", "sealed"]
43
+ },
44
+ "on_expiry": {
45
+ "type": "string",
46
+ "enum": ["reject"],
47
+ "description": "SPEC.md §5.1/§5.2: what happens when `approval_ttl` elapses with no human decision. SPEC.md §5.1 defines exactly one value, `reject`, and §5.2 mandates failing closed — so the enum is closed to `reject` rather than speculatively admitting a permissive value such as `grant`. Widening this enum is a spec amendment, and a policy that names an unknown expiry action fails validation (and therefore falls back to all-`manual`) instead of silently expiring into an unintended behaviour."
48
+ }
49
+ }
50
+ },
51
+ "payload_retention": {
52
+ "$ref": "#/$defs/duration",
53
+ "description": "SPEC.md §5.2 (amended, APRV-38): how long payload bytes are kept in `.approval/payloads/` after the action they bind to reaches a terminal state (`executed`, `rejected`, `expired`, `revoked`). A payload older than this duration in a terminal state MAY be pruned; a payload whose action is still live is never prunable at any age, because the bytes are what a pending or granted approval binds to. Orphaned payloads (bytes with no recorded binding) are prunable regardless of this key. Omitting the key means retain indefinitely: the payload store holds the material evidence of what was approved, so forgetting it is an explicit operator choice rather than a default. Pruning is performed by the daemon and by nothing else, and each removal is recorded as a `payload.pruned` event. Policy vocabulary in v0.1: enforcement lands with the M5 daemon (APRV-41), so a policy may declare it before any runtime reads it.",
54
+ "examples": ["30d", "12w"]
55
+ },
56
+ "protected_paths": {
57
+ "type": "array",
58
+ "uniqueItems": true,
59
+ "description": "SPEC.md §5.2 (amended, APRV-107 and APRV-266): repo-relative paths whose edit the runtime classifies as `policy.edit`, or as a named `policy.edit` sub-class, so an agent editing them must go through the gate. ADDITIVE ONLY: the built-in protected set (APPROVAL.md, APPROVALS.md, CLAUDE.md, AGENTS.md, .npmrc, anything under .approval/, .claude/settings*, .github/workflows/) is protected whatever this list says or omits, because a policy that could shrink the protected surface would be a policy an agent could edit its way out of. Grammar: an entry is either a bare path string (APRV-107, meaning `policy.edit`) or an object `{path, class}` routing that path family to a `policy.edit` sub-class (APRV-266), so a project can give its specification, its CI configuration and its design directory each their own autonomy and live rate. A path is an exact file (`SPEC.md`, `docs/constitution.md`) or a directory prefix ending in `/` (`design/`). No globs, no negation, no absolute paths, no `..` — the classifier is a pure segment matcher, not a shell, and a pattern language it half-implemented would be a protection an author believed was in force and the runtime did not apply. Matching is by path segments and never resolves against a checkout, so it answers the same in a worktree as in the primary: an exact path matches a candidate whose TRAILING segments are that path (`SPEC.md` matches `SPEC.md`, `./SPEC.md` and `/abs/repo/SPEC.md`; a single-segment entry therefore also matches `docs/SPEC.md`, exactly as the built-in filenames match anywhere), and a directory prefix matches a candidate containing those segments as a contiguous run (`design/` matches `design/x.md` and `/abs/repo/design/sub/x.md`). Both directions err wide on purpose: a false positive costs one approval prompt, a false negative costs the property the gate exists to defend. What this schema CANNOT state is the routing floor: a route aimed at a built-in protected path must resolve at least as strictly as the `policy.edit` line itself, and a policy that breaks that is refused at load with the code `protected-route-floor`.",
60
+ "items": {
61
+ "$comment": "A `oneOf` over the two entry shapes, which costs a refusal-code change and is still the right spelling. A malformed path now answers `oneOf` with the `pattern` failure nested under it rather than `pattern` at the top, because every union construct this validator admits reports the union first: `if`/`then`/`else` on the entry type emits an `if` error the same way, and the flat form that would have avoided both — `pattern` and `minLength` at the item level, ignored for an object by JSON Schema's own rules — is refused by Ajv's `strictTypes`, which requires a declared type for those keywords. So the conformance suite's `schema-validation` moves to 2.0.0 for these two vectors; see conformance/README.md.",
62
+ "oneOf": [
63
+ { "$ref": "#/$defs/protectedPath" },
64
+ {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "required": ["path", "class"],
68
+ "description": "SPEC.md §5.2 (amended, APRV-266): one path family routed to a named `policy.edit` sub-class, so that family carries its own autonomy and its own live rate. The sub-class is an ordinary §7 class and takes an ordinary `classes` line; a routed class with no line of its own inherits the `policy.edit` line.",
69
+ "properties": {
70
+ "path": { "$ref": "#/$defs/protectedPath" },
71
+ "class": {
72
+ "type": "string",
73
+ "pattern": "^policy\\.edit\\.[a-z][a-z0-9-]*$",
74
+ "description": "A `policy.edit` sub-class: exactly one extra lowercase segment under `policy.edit`. Four names are reserved with fixed meanings so two policies mean the same thing by them — `policy.edit.spec` (the governing specification), `policy.edit.harness` (agent instruction files and harness configuration that is not the hook itself), `policy.edit.ci` (continuous-integration and release configuration), `policy.edit.design` (design documents and decision records) — and an author may mint any other lowercase word beside them. The namespace is closed on purpose: a policy may not route a path to `policy.core`, to `log.mutate`, or to any class outside `policy.edit`, because those are the gate's own organs and the record of what happened, and a policy widening its own protected surface mints no authority over them (SPEC.md §11.1 invariant 9)."
75
+ }
76
+ }
77
+ }
78
+ ]
79
+ },
80
+ "examples": [
81
+ ["SPEC.md", "design/"],
82
+ [
83
+ { "path": "SPEC.md", "class": "policy.edit.spec" },
84
+ { "path": ".github/workflows/", "class": "policy.edit.ci" }
85
+ ]
86
+ ]
87
+ },
88
+ "approvers": {
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "description": "SPEC.md §5.1: map of approver id -> approver record. Ids are referenced by the `approvers` list on class rules.",
92
+ "propertyNames": { "$ref": "#/$defs/identifier" },
93
+ "patternProperties": {
94
+ "^[a-z0-9][a-z0-9_-]*$": {
95
+ "type": "object",
96
+ "additionalProperties": false,
97
+ "required": ["channels"],
98
+ "description": "SPEC.md §5.1: a human who may decide requests, and the channels they can be reached on.",
99
+ "properties": {
100
+ "channels": {
101
+ "type": "array",
102
+ "minItems": 1,
103
+ "uniqueItems": true,
104
+ "description": "SPEC.md §5.1/§10.3: channel names this approver can decide on, e.g. [telegram, cli]. At least one, since an approver reachable nowhere can never grant.",
105
+ "items": { "type": "string", "minLength": 1 }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "classes": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "description": "SPEC.md §5.1/§5.2 and §7: map of side-effect class pattern -> rule. Keys are dotted class patterns; `*` is a single-segment wildcard and a trailing `.*` matches any depth. Matching is most-specific-first and, at equal specificity, the strictest autonomy wins — both are runtime concerns; the schema only constrains key shape and rule shape.",
115
+ "propertyNames": { "$ref": "#/$defs/classPattern" },
116
+ "patternProperties": {
117
+ "^(?:[a-z0-9][a-z0-9_-]*|\\*)(?:\\.(?:[a-z0-9][a-z0-9_-]*|\\*))*$": {
118
+ "$ref": "#/$defs/classRule"
119
+ }
120
+ }
121
+ },
122
+ "budgets": {
123
+ "type": "object",
124
+ "description": "SPEC.md §5.1/§5.2: named budget scopes. `global` is the scope every action is checked against; budgets are conjunctive (an action must pass its class limits AND the global budget) and consumption is computed from the log, never from a mutable counter. Additional named scopes are permitted and share the `global` shape.",
125
+ "additionalProperties": { "$ref": "#/$defs/budget" },
126
+ "properties": {
127
+ "global": {
128
+ "$ref": "#/$defs/budget",
129
+ "description": "SPEC.md §5.1: the repo-wide budget, e.g. { daily_usd: 100, daily_actions: 200 }."
130
+ }
131
+ }
132
+ },
133
+ "audit": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "description": "SPEC.md §5.2/§11: retrospective review settings for actions that executed without prior sign-off.",
137
+ "properties": {
138
+ "supervised_sample_rate": {
139
+ "type": "number",
140
+ "minimum": 0,
141
+ "maximum": 1,
142
+ "description": "SPEC.md §5.2: fraction of `supervised` actions escalated for after-the-fact human review, as a proportion in [0, 1] (0 = never sample, 1 = review everything). Sample selection is reproducible by the operator and unpredictable to agents: HMAC-SHA-256 over the event hash using an operator-held secret (amended SPEC.md §5.2; enforcement lands with the M5 daemon). Amended APRV-183: this is the FALLBACK rate. A class rule may declare its own `retro_rate`, which governs that class instead; classes declaring none are sampled at this rate, and a policy that declares this key alone behaves exactly as it did before the per-class key existed."
143
+ },
144
+ "sampling_secret_env": {
145
+ "type": "string",
146
+ "minLength": 1,
147
+ "description": "SPEC.md §5.2/§11 (amended, APRV-38): name of the environment variable holding the operator's HMAC sampling secret. Named indirectly for the same reason channel credentials are (`chat_id_env`, `token_env`): agents may read APPROVAL.md, and a secret they can read is a sample they can predict and then steer their actions out of. The secret itself lives outside the repository and outside any agent-readable path; this policy carries the variable name only. Policy vocabulary in v0.1: the sampler that reads it lands with the M5 daemon.",
148
+ "examples": ["APPROVAL_SAMPLING_SECRET"]
149
+ },
150
+ "skew_tolerance": {
151
+ "$ref": "#/$defs/duration",
152
+ "description": "SPEC.md §8 (amended, APRV-58): how far a gate-typed event's `ts` may step backwards from the previous gate-typed event before verification reports a `gate-ts-regression` anomaly. Duration string, `<positive integer><unit>` with unit in ms|s|m|h|d|w, the same grammar `defaults.approval_ttl` uses. Absent means the reference runtime's 2 seconds, which is generous enough that ordinary clock disagreement between NTP-disciplined hosts is not reported and far below the skew a useful lie requires. Operators on one host may tighten it; operators across a WAN with poor time discipline may loosen it. This threshold is REPORT-ONLY: it changes which anomalies a verified log carries and never changes a verdict, an exit code, or an authorization, so widening it hides evidence from a human rather than permitting anything.",
153
+ "examples": ["250ms", "5s"]
154
+ },
155
+ "checkpoint_keys": {
156
+ "type": "array",
157
+ "items": {
158
+ "type": "string",
159
+ "minLength": 1,
160
+ "pattern": "^[A-Za-z0-9+/]+={0,2}$"
161
+ },
162
+ "uniqueItems": true,
163
+ "description": "SPEC.md §9 (amended, APRV-220): the PUBLIC halves of the keys permitted to sign a `log.checkpoint`, base64 DER SPKI, Ed25519. Unlike every other key-shaped field in this file this one holds the material itself rather than the name of a variable, because a public key is not a secret and the point of writing it here is that it is written somewhere a human has committed and attested: an agent that edited this list would leave a visible diff AND de-attest the policy, which stops every gate operation until a human re-attests. The private halves live in the credential vault and never appear in this file, in the log, or in any path an agent may read. A LIST rather than one key so that rotation can retain a retired key: a checkpoint signed by a key this list does not carry is refused (`checkpoint-key-unknown`), so removing a key that signed anything de-verifies the range it signed rather than tidying the file. Absent, empty, or unreadable means verification SKIPS the checkpoint check with a reason and never reports it as a pass.",
164
+ "examples": [["MCowBQYDK2VwAyEA9dqzvJnn0VtpD6ETeDRUsCn4l6MPyfaV9Xx6KN+e678="]]
165
+ },
166
+ "checkpoint_every": {
167
+ "$ref": "#/$defs/duration",
168
+ "description": "SPEC.md §9 (amended, APRV-220): how long a log may go without a human-signed checkpoint before verification says one is due. Duration string, `<positive integer><unit>` with unit in ms|s|m|h|d|w, the same grammar `defaults.approval_ttl` uses. Absent means the cadence is off and nothing is ever reported as due. REPORT-ONLY, and more strongly than `skew_tolerance` is: a lapsed cadence is a WARNING at every layer and there is no path from due to refused anywhere in this runtime. A human who has been away is not a forger, and a gate that refused a log for want of a tap is a gate whose operator turns the check off.",
169
+ "examples": ["24h", "7d"]
170
+ }
171
+ }
172
+ },
173
+ "daemon": {
174
+ "type": "object",
175
+ "additionalProperties": false,
176
+ "description": "APRV-217 (docs/proposals/incremental-prefix-proof.md): how the long-lived readers of this log prove a cached prefix before they reuse it. Read by `approval daemon run` and `approval up` and by nothing else: one-shot processes, the Claude Code hook included, always prove in full, and `approval log verify` and `approval doctor` never use the cache at all. Every key here is a LATENCY setting whose strictest value is the default, so a policy that omits the block, or the whole block, behaves exactly as the runtime did before it existed.",
177
+ "properties": {
178
+ "read_proof": {
179
+ "type": "string",
180
+ "enum": ["full", "incremental"],
181
+ "default": "full",
182
+ "description": "Which proof a repeat read runs over the prefix it already verified. `full` (THE DEFAULT, and what an absent key means): re-hash every byte of the proved prefix on every read and compare the digest, which is the APRV-43 behaviour byte for byte. `incremental`: carry the un-finalised SHA-256 state at the end of the prefix, feed it only the appended bytes, and re-prove the whole prefix on the cadence below. The cheap guards (schema key, file not shrunk, same-size-implies-same-mtime, head line byte-identical at its offset) run in both modes, and the appended tail is parsed, schema-checked and chain-walked in both. What `incremental` gives up, between full re-proofs, is the detection of a rewrite STRICTLY INSIDE the prefix that preserves the file length, the head line and the mtime — an edit only a party with write access to the log can make, who can already recompute a self-consistent forged chain because the chain is unkeyed. The default is `full` because it is the behaviour an operator has attested to, and moving it is a policy amendment like any other.",
183
+ "examples": ["full", "incremental"]
184
+ },
185
+ "full_reproof_every": {
186
+ "type": "integer",
187
+ "minimum": 1,
188
+ "default": 50,
189
+ "description": "APRV-217: how many reads one full re-proof may cover under `read_proof: incremental`, the anchoring read included. `1` means every read hashes the whole prefix, which is `full` reached by another spelling. Ignored under `full`. Bounds the window in which an in-place rewrite of the prefix would be served from cache, together with `full_reproof_after`, whichever comes first.",
190
+ "examples": [50]
191
+ },
192
+ "full_reproof_after": {
193
+ "$ref": "#/$defs/duration",
194
+ "default": "60s",
195
+ "description": "APRV-217: how much wall-clock time one full re-proof may cover under `read_proof: incremental`. Duration string, `<positive integer><unit>` with unit in ms|s|m|h|d|w, the same grammar `defaults.approval_ttl` uses. Ignored under `full`. The binding bound on an idle daemon, where the read count grows slowly: at the default 30 s tick this makes every second tick re-prove in full.",
196
+ "examples": ["60s", "5m"]
197
+ }
198
+ }
199
+ },
200
+ "vault": {
201
+ "type": "object",
202
+ "additionalProperties": false,
203
+ "description": "SPEC.md §5.2/§10.4 (amended, APRV-68): configuration for the encrypted credential vault adapters read from (`.approval/vault.enc`). Exactly one key, and it holds a NAME.",
204
+ "properties": {
205
+ "passphrase_env": {
206
+ "type": "string",
207
+ "minLength": 1,
208
+ "description": "SPEC.md §5.2/§10.4 (amended, APRV-68): name of the environment variable holding the vault passphrase, from which the AES-256-GCM key is derived by scrypt. Named indirectly for the same reason `audit.sampling_secret_env`, `chat_id_env` and `token_env` are: agents may read APPROVAL.md, and a passphrase they can read is a vault they can open. The passphrase itself lives outside the repository and outside any agent-readable path; this policy carries the variable name only. Absent means the runtime reads `APPROVAL_VAULT_PASSPHRASE`, because the variable's NAME is not a permission and an unnamed one must not lock an operator out of credentials they created.",
209
+ "examples": ["APPROVAL_VAULT_PASSPHRASE"]
210
+ }
211
+ }
212
+ },
213
+ "channels": {
214
+ "type": "object",
215
+ "description": "SPEC.md §5.1/§10.3: map of channel name -> channel configuration. Known channels are typed; unknown channel names are accepted as objects so a third-party transport plugin does not invalidate the whole policy (and, per §5.2, force everything to `manual`).",
216
+ "additionalProperties": { "type": "object" },
217
+ "properties": {
218
+ "telegram": {
219
+ "type": "object",
220
+ "additionalProperties": false,
221
+ "description": "SPEC.md §5.1/§11: Telegram channel. Credentials are named indirectly by environment variable — the policy file holds env var NAMES, never token values, because agents may read APPROVAL.md but must never reach channel credentials.",
222
+ "properties": {
223
+ "chat_id_env": {
224
+ "type": "string",
225
+ "minLength": 1,
226
+ "description": "SPEC.md §5.1: name of the environment variable holding the Telegram chat id.",
227
+ "examples": ["APPROVAL_TG_CHAT"]
228
+ },
229
+ "token_env": {
230
+ "type": "string",
231
+ "minLength": 1,
232
+ "description": "SPEC.md §5.1: name of the environment variable holding the Telegram bot token.",
233
+ "examples": ["APPROVAL_TG_TOKEN"]
234
+ },
235
+ "delivery": {
236
+ "type": "string",
237
+ "enum": ["paced", "burst"],
238
+ "default": "paced",
239
+ "description": "SPEC.md §10.3 (APRV-216): how a listener puts the pending set in front of the approver. `paced`, the default and what an absent key means, sends one summary line and the OLDEST pending request, then the next one once that request is decided, skipped, or passed over; the bot commands /queue, /skip and /next drive it. `burst` sends every pending request the process has not sent yet, on every cycle, behind the APRV-196 re-delivery banner. Neither mode changes what is pending: that is re-derived from the verified log on every cycle in both, and the order and the currently shown item are process memory whose loss costs a re-send and nothing else. The enum is closed because an unrecognised mode cannot be ordered against these two; a policy naming one fails validation, which fails closed to all-manual rather than to a guessed delivery.",
240
+ "examples": ["paced", "burst"]
241
+ },
242
+ "prompt": { "$ref": "#/$defs/promptLayout" }
243
+ }
244
+ },
245
+ "web": {
246
+ "type": "object",
247
+ "additionalProperties": false,
248
+ "description": "SPEC.md §5.1/§10.3: local web channel.",
249
+ "properties": {
250
+ "port": {
251
+ "type": "integer",
252
+ "minimum": 1,
253
+ "maximum": 65535,
254
+ "description": "SPEC.md §5.1: TCP port for the local approval UI. Valid port range only; 0 (ephemeral) is excluded because the policy must name a port a human can navigate to.",
255
+ "examples": [4680]
256
+ },
257
+ "prompt": { "$ref": "#/$defs/promptLayout" }
258
+ }
259
+ },
260
+ "cli": {
261
+ "type": "object",
262
+ "additionalProperties": false,
263
+ "description": "SPEC.md §5.1/§10.3: the zero-config terminal channel. It needs no credential and no port, so until APRV-218 it had nothing to configure and the key did not exist; a policy that omits it is the norm and always will be.",
264
+ "properties": {
265
+ "prompt": { "$ref": "#/$defs/promptLayout" }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ },
271
+ "$defs": {
272
+ "promptRow": {
273
+ "type": "string",
274
+ "enum": [
275
+ "action_key",
276
+ "task",
277
+ "class",
278
+ "command_breakdown",
279
+ "protected_path",
280
+ "policy_diff",
281
+ "policy_load",
282
+ "autonomy",
283
+ "provenance",
284
+ "state",
285
+ "requested_ts",
286
+ "waiting",
287
+ "ttl_remaining_ms",
288
+ "payload_hash",
289
+ "attestation",
290
+ "budgets",
291
+ "chain",
292
+ "token_delivery",
293
+ "est_cost_usd",
294
+ "gloss",
295
+ "summary",
296
+ "rationale",
297
+ "confidence"
298
+ ],
299
+ "description": "SPEC.md §5.2/§10.3 (APRV-218): one INFORMATIONAL row of a channel prompt, named by the `ChannelRequest` member it renders. The enum is closed for the reason `channels.telegram.delivery`'s is: a name this runtime cannot place is a row the author believes is in force that nothing reads, so it fails validation, which fails the policy closed to all-`manual` rather than to a silently ignored key. `fullPayload` is deliberately absent — the canonical rendering of SPEC.md §9 is not a row, it is rendered verbatim, and no layout key can reorder, shorten, or remove it."
300
+ },
301
+ "promptRows": {
302
+ "type": "array",
303
+ "uniqueItems": true,
304
+ "items": { "$ref": "#/$defs/promptRow" }
305
+ },
306
+ "promptLayout": {
307
+ "type": "object",
308
+ "additionalProperties": false,
309
+ "description": "SPEC.md §5.2/§10.3 (APRV-218): which rows this channel's prompt shows, and in what order. Absent means the layout the channel ships, so every policy written before this key renders exactly as it did. A layout chooses among rows the approver READS; it cannot touch what the approver SIGNS. Three things stay out of its reach: the canonical payload block (not a row), the buttons (a prompt with no way to answer it is not a prompt), and the computed/claimed split, which is a property of the field rather than of the layout — `rows` reorders, and a channel partitions by kind afterwards, so a claimed line cannot be moved into the computed block.",
310
+ "properties": {
311
+ "rows": {
312
+ "$ref": "#/$defs/promptRows",
313
+ "description": "ORDER ONLY. The rows named here render in this order, ahead of every row not named; those keep their default relative order behind them. It is never a whitelist: a `ChannelRequest` widened by a later task must not lose a field to a list written before that field existed. Use `hide` to remove a row and `always` to add one.",
314
+ "examples": [["class", "command_breakdown", "task", "waiting"]]
315
+ },
316
+ "always": {
317
+ "$ref": "#/$defs/promptRows",
318
+ "description": "Visibility UP: a row this channel renders only when abnormal (Telegram's `autonomy`, `budgets` and `attestation`, per APRV-163), or not at all by default (Telegram's `task`, `state`, `chain`, `provenance`, `requested_ts`, `ttl_remaining_ms` and `payload_hash`, per APRV-143 and APRV-163), renders on every prompt instead. The anomaly mark stays a statement about the VALUE: a row forced on here carries it only when the value is in fact the reason to look.",
319
+ "examples": [["budgets", "task"]]
320
+ },
321
+ "hide": {
322
+ "$ref": "#/$defs/promptRows",
323
+ "description": "Visibility DOWN: the row never renders on this channel. Refused for the rows required for a decision — `action_key`, `class`, `command_breakdown`, `protected_path`, `policy_diff`, `policy_load` — and refused at policy load with the machine-readable keyword `prompt-row-required`, which fails the whole policy closed. `payload_hash` is NOT among them because the bound hash is stated inside the canonical block on every channel, so hiding the row removes a duplicate rather than the binding. Naming one row in both `always` and `hide` is refused rather than resolved by precedence: an author who wrote both believes one of them, and picking for them would be a guess.",
324
+ "examples": [["provenance", "chain", "requested_ts"]]
325
+ }
326
+ }
327
+ },
328
+ "autonomy": {
329
+ "type": "string",
330
+ "enum": ["human-only", "manual", "supervised-live", "supervised", "supervised-retro", "autonomous"],
331
+ "description": "SPEC.md §5.2 (amended, APRV-127; widened, APRV-185): autonomy level, strictest first — `human-only` (reserved to human hands: a person performs the action outside agent execution, so no agent may request it, be granted it, or run it, and every gate verb refuses it with the code `class-human-only`) > `manual` (human sign-off before every action) > `supervised-live` (a policy-declared fraction of actions blocks on the human gate exactly as `manual` does; the rest proceed) > `supervised-retro` (executes immediately, sampled for retrospective review) > `autonomous` (executes freely). `supervised` is the pre-APRV-127 spelling and is an ALIAS OF `supervised-retro`, so every policy written before the split keeps the meaning its author gave it; the runtime records a load-time note naming the alias rather than silently reinterpreting it. The enum is closed: an unrecognised level cannot be ordered against the others, so it must fail validation rather than be guessed at.",
332
+ "examples": ["human-only", "manual", "supervised-live", "supervised-retro"]
333
+ },
334
+ "liveRate": {
335
+ "type": "number",
336
+ "exclusiveMinimum": 0,
337
+ "maximum": 1,
338
+ "description": "SPEC.md §5.2 (amended, APRV-127): the fraction of `supervised-live` actions in a class that BLOCK on the human gate before executing, as a proportion in (0, 1]. Selection is HMAC-SHA-256 over the action's `payload_hash` under the operator-held secret named by `audit.sampling_secret_env` — the same construction `audit.supervised_sample_rate` uses, reused rather than reinvented, so an agent cannot predict its own luck and an operator holding the secret can verify every selection after the fact. Determinism is a required property, not an implementation detail: identical payload bytes always select identically, so a retry cannot re-roll until it comes up unsampled. Zero is excluded because \"never gate live\" is spelled `autonomy: supervised-retro`, and 1 is admitted because \"gate every one of them\" is a legitimate, if verbose, way to say `manual` for a class an author expects to relax later.",
339
+ "examples": [0.01, 0.1]
340
+ },
341
+ "retroRate": {
342
+ "type": "number",
343
+ "exclusiveMinimum": 0,
344
+ "maximum": 1,
345
+ "description": "SPEC.md §5.2 (amended, APRV-183): the fraction of a supervised class's executed actions drawn into the retrospective review backlog, as a proportion in (0, 1]. Overrides `audit.supervised_sample_rate` for this class and for nothing else; an absent key leaves the class on the global rate. Selection is the same construction the global rate uses, HMAC-SHA-256 over the subject record's event hash under the operator-held secret named by `audit.sampling_secret_env`, so there is one mechanism and one secret, and an operator holding that secret can recompute any verdict from the record's own hash. Zero is excluded because \"never review this class\" is a claim a policy makes by raising the class out of supervision, and a rate of 0 written here would read as a review budget the author meant to fill in; 1 is admitted because \"review every one of them\" is a legitimate setting for a small, high-consequence class.",
346
+ "examples": [0.25, 1]
347
+ },
348
+ "defaultAutonomy": {
349
+ "type": "string",
350
+ "enum": ["human-only", "manual", "supervised", "supervised-retro", "autonomous"],
351
+ "description": "SPEC.md §5.2 (amended, APRV-127; widened, APRV-185): the autonomy levels a policy may name as its DEFAULT. Identical to `autonomy` less `supervised-live`, which carries a required `live_rate` that `defaults` has nowhere to declare. `human-only` IS admitted, and the asymmetry with `supervised-live` is the reason: that level is excluded for a key `defaults` cannot hold, and `human-only` carries no key at all, so an author naming it here declares maximal strictness over everything the policy did not name — a statement a policy is entitled to make. It is NOT the fail-closed target. A policy that fails to load still resolves every class to `manual`, because a broken policy must stay recoverable 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."
352
+ },
353
+ "duration": {
354
+ "type": "string",
355
+ "pattern": "^[1-9][0-9]*(?:ms|s|m|h|d|w)$",
356
+ "description": "SPEC.md §5.1: duration string `<positive integer><unit>`, unit in ms|s|m|h|d|w. Leading zeros, zero durations, negatives, fractions, whitespace and compound forms are all rejected so parsing is unambiguous across implementations."
357
+ },
358
+ "identifier": {
359
+ "type": "string",
360
+ "pattern": "^[a-z0-9][a-z0-9_-]*$",
361
+ "description": "SPEC.md §5.1: lowercase identifier used for approver ids — starts alphanumeric, then alphanumerics, `_` or `-`. Case-insensitive collisions are excluded by construction."
362
+ },
363
+ "classPattern": {
364
+ "type": "string",
365
+ "pattern": "^(?:[a-z0-9][a-z0-9_-]*|\\*)(?:\\.(?:[a-z0-9][a-z0-9_-]*|\\*))*$",
366
+ "description": "SPEC.md §5.2 and §7: a side-effect class pattern — one or more dot-separated segments, each either a literal lowercase segment ([a-z0-9] then [a-z0-9_-]) or the wildcard `*`. Covers exact classes (`financial.spend`), trailing-depth wildcards (`read.*`), interior single-segment wildcards (`calendar.*.own`) and the bare `*`. Empty keys, empty segments (`read..write`), leading/trailing dots, uppercase and whitespace are rejected: an unmatchable key would be a rule the author believes is in force that can never fire."
367
+ },
368
+ "classRule": {
369
+ "type": "object",
370
+ "additionalProperties": false,
371
+ "required": ["autonomy"],
372
+ "description": "SPEC.md §5.1: a class rule. The shorthand form is `{ autonomy }` alone; the full form adds `approvers` and `limits`. Both are this one shape — `autonomy` is always REQUIRED, since a rule without it cannot decide anything, and unknown keys are rejected so a typo'd constraint is never silently ignored.",
373
+ "allOf": [
374
+ {
375
+ "$comment": "SPEC.md §5.2 (amended, APRV-127): `live_rate` belongs to `supervised-live` and to nothing else. Required there, because a live mode with no fraction declares a control without saying how much of it runs, and the runtime would have to invent the number an author did not write. Forbidden everywhere else, because a rate sitting on a `manual`, `supervised-retro` or `autonomous` rule is a fraction its author believes is in force that nothing reads — the same silent no-op this schema's closed shape exists to prevent. APRV-185 adds `human-only` to that everywhere-else, and the `else` branch below already carries it with no new clause: a level whose actions no agent may take has no fraction of them to gate, so a `live_rate` written there is a control its author believes is running over a population of size zero.",
376
+ "if": {
377
+ "type": "object",
378
+ "properties": { "autonomy": { "const": "supervised-live" } },
379
+ "required": ["autonomy"]
380
+ },
381
+ "then": {
382
+ "type": "object",
383
+ "properties": { "live_rate": { "$ref": "#/$defs/liveRate" } },
384
+ "required": ["live_rate"]
385
+ },
386
+ "else": {
387
+ "type": "object",
388
+ "properties": { "live_rate": false }
389
+ }
390
+ },
391
+ {
392
+ "$comment": "SPEC.md §5.2 (amended, APRV-183): `retro_rate` is the per-class retrospective sampling rate, and it belongs to the levels that have a retrospective pool. OPTIONAL on `supervised`, `supervised-retro` and `supervised-live` — absent means the class is sampled at `audit.supervised_sample_rate`, so every policy written before this key keeps the rate its author configured. `supervised-live` admits it because the fraction the live draw does NOT gate executes and stays eligible for retrospective review, which is a real pool with a real rate. FORBIDDEN on `manual`, `autonomous` and (APRV-185) `human-only`, and forbidden as a LOAD ERROR rather than as a stated no-op, which is exactly how `live_rate` treats the levels it does not belong to: a `manual` class has no retrospective pool because every one of its actions was decided in advance, an `autonomous` class is not supervised at all, a `human-only` class has no agent execution to review, and a rate sitting on any of them is a review fraction its author believes is running that nothing reads. The `else` branch carries all three with no new clause, which is why the `if` enumerates the supervised levels rather than the forbidden ones.",
393
+ "if": {
394
+ "type": "object",
395
+ "properties": {
396
+ "autonomy": { "enum": ["supervised", "supervised-retro", "supervised-live"] }
397
+ },
398
+ "required": ["autonomy"]
399
+ },
400
+ "then": {
401
+ "type": "object",
402
+ "properties": { "retro_rate": { "$ref": "#/$defs/retroRate" } }
403
+ },
404
+ "else": {
405
+ "type": "object",
406
+ "properties": { "retro_rate": false }
407
+ }
408
+ }
409
+ ],
410
+ "properties": {
411
+ "autonomy": {
412
+ "$ref": "#/$defs/autonomy",
413
+ "description": "SPEC.md §5.2: autonomy for actions matching this class pattern."
414
+ },
415
+ "live_rate": { "$ref": "#/$defs/liveRate" },
416
+ "retro_rate": { "$ref": "#/$defs/retroRate" },
417
+ "approvers": {
418
+ "type": "array",
419
+ "minItems": 1,
420
+ "uniqueItems": true,
421
+ "description": "SPEC.md §5.1: approver ids permitted to decide requests for this class. Ids are expected to be keys of the top-level `approvers` map; that cross-reference is a runtime check, not a schema constraint.",
422
+ "items": { "$ref": "#/$defs/identifier" }
423
+ },
424
+ "limits": {
425
+ "type": "object",
426
+ "minProperties": 1,
427
+ "description": "SPEC.md §5.1/§5.2: per-class ceilings, e.g. { per_action_usd: 25, daily_usd: 100 }. Limit names are open-ended (the taxonomy in §7 grows), but every value MUST be a positive number: zero or negative would express 'no action may ever pass', which is spelled `autonomy: manual` instead, and a non-numeric limit cannot participate in budget arithmetic. Two request-volume names are documented explicitly (SPEC.md §5.2) and are counts, hence integers: `max_pending` and `requests_per_hour`.",
428
+ "additionalProperties": { "$ref": "#/$defs/positiveNumber" },
429
+ "propertyNames": { "$ref": "#/$defs/identifier" },
430
+ "properties": {
431
+ "max_pending": {
432
+ "$ref": "#/$defs/positiveInteger",
433
+ "description": "SPEC.md §5.2: maximum simultaneously pending requests for this class; further requests are refused at intake with reason `queue-full`. A count, hence an integer. Policy vocabulary in v0.1: enforcement lands with M4/M5, so a policy may declare it before any runtime reads it."
434
+ },
435
+ "requests_per_hour": {
436
+ "$ref": "#/$defs/positiveInteger",
437
+ "description": "SPEC.md §5.2: rolling-window ceiling on request creation, evaluated per origin; excess is refused with reason `rate-limited` and logged. A count, hence an integer. The per-origin windowing (and enforcement generally) lands with M4/M5; v0.1 only accepts the vocabulary."
438
+ }
439
+ }
440
+ }
441
+ }
442
+ },
443
+ "budget": {
444
+ "type": "object",
445
+ "additionalProperties": false,
446
+ "minProperties": 1,
447
+ "description": "SPEC.md §5.1/§5.2: a budget scope. Budgets are conjunctive with class limits and are computed from the append-only log.",
448
+ "properties": {
449
+ "daily_usd": {
450
+ "$ref": "#/$defs/positiveNumber",
451
+ "description": "SPEC.md §5.1: maximum spend in USD per rolling day for this scope."
452
+ },
453
+ "daily_actions": {
454
+ "type": "integer",
455
+ "exclusiveMinimum": 0,
456
+ "description": "SPEC.md §5.1: maximum number of side-effecting actions per rolling day for this scope. A count, hence an integer."
457
+ },
458
+ "max_pending": {
459
+ "$ref": "#/$defs/positiveInteger",
460
+ "description": "SPEC.md §5.2: maximum simultaneously pending requests across this scope (the global counterpart of `limits.max_pending`); further requests are refused at intake with reason `queue-full`. A count, hence an integer. Policy vocabulary in v0.1: enforcement lands with M4/M5."
461
+ }
462
+ }
463
+ },
464
+ "positiveInteger": {
465
+ "type": "integer",
466
+ "exclusiveMinimum": 0,
467
+ "description": "A strictly positive whole number, used for counts (pending requests, requests per hour). Zero and negatives are rejected for the same reason as `positiveNumber`: an unreachable ceiling is spelled `autonomy: manual`, not `0`."
468
+ },
469
+ "positiveNumber": {
470
+ "type": "number",
471
+ "exclusiveMinimum": 0,
472
+ "description": "A strictly positive number. Zero and negatives are rejected: budget and limit arithmetic treats them as unreachable ceilings, which policy authors express with `autonomy: manual`."
473
+ },
474
+ "protectedPath": {
475
+ "type": "string",
476
+ "minLength": 1,
477
+ "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?(?:/|$))(?!.*//)[^*?\\[\\]{}\\\\\\s]+$",
478
+ "description": "A repo-relative path: exact file (`SPEC.md`) or directory prefix with a trailing `/` (`design/`). Leading `/`, `.`/`..` segments, whitespace and the glob characters `* ? [ ] { }` are rejected — each would be a path the matcher cannot honour literally, and silently ignoring one would leave the author believing a file is gated when it is not. Named as a `$def` since APRV-266 so the bare-string entry and the `path` of a routed entry are held to one grammar and cannot drift apart."
479
+ }
480
+ }
481
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://approval.md/schema/sample-record.schema.json",
4
+ "$comment": "CC0 1.0. See schema/LICENSE.",
5
+ "title": "Sample record",
6
+ "description": "Exercise schema for the write-boundary validation harness. Not a runtime record type: it exists so the harness proves additionalProperties rejection and format enforcement end to end.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["id", "ts"],
10
+ "properties": {
11
+ "id": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "description": "Opaque record identifier."
15
+ },
16
+ "ts": {
17
+ "type": "string",
18
+ "format": "date-time",
19
+ "description": "RFC 3339 timestamp."
20
+ },
21
+ "note": {
22
+ "type": "string",
23
+ "description": "Optional free-text note."
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://approval.md/schema/values.schema.json",
4
+ "$comment": "CC0 1.0. See schema/LICENSE.",
5
+ "title": "APPROVAL.md values block",
6
+ "description": "SPEC.md §5.3: the parsed YAML of the OPTIONAL second fenced block inside APPROVAL.md, marked ```yaml approval-values```. It is the human-to-agent direction of a file that until now carried control in one direction only: the policy block says what an agent may do, and this block says what the operator values and how they read what comes back. It is human-authored GUIDANCE and it is never policy. No enforcement path reads it — not routing, not policy matching, not the sampler, not budgets, not token minting, not execution — which is SPEC.md §11.1 invariant 10 (\"guidance never reaches enforcement\"), pinned by tests/values-inert.test.ts. A values block that fails this schema never makes the policy block unloadable; the two are parsed and judged independently, and the failure of guidance can never widen or narrow a class. Because the block lives inside APPROVAL.md, editing it changes the file's bytes and therefore invalidates the whole-file attestation, so a values edit goes through the same attestation ceremony as a policy edit (SPEC.md §5.3). Absence is a declaration, never a default: a file with no values block means the operator has declared no values here, and an agent-facing surface must render it as absence rather than inventing a neutral middle.\n\nThe key set is closed (additionalProperties: false at every level) for the same reason the policy block is: an unrecognised key is something the author believed was in force and the runtime did not understand. Keys deliberately REJECTED, recorded here so the rejection is reviewable rather than forgotten: (1) anything class-shaped or override-shaped — a `classes:` map, an `autonomy:`, a `defaults:`, an `overrides:` — because that is policy, and admitting a policy-shaped key into the guidance block would make invariant 10 unprovable: a static guard cannot show that guidance never reaches enforcement once guidance is allowed to LOOK like enforcement; (2) `priority` and `weight`, because a number invites arithmetic and there is no arithmetic to do here — nothing ranks, sums or thresholds these entries, and a field that suggests it does would be read as a knob; (3) `hate`, because a fourth standing grade invites an escalation ladder, and the standing lists are deliberately three (love/like/dislike). Intensity is carried where it is actually observed, by the event vocabulary's `reaction` (disliked|indifferent|liked|loved, SPEC.md §5.2), not by an ever-longer list of nouns in a static file.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["version"],
10
+ "properties": {
11
+ "version": {
12
+ "type": "integer",
13
+ "const": 1,
14
+ "description": "SPEC.md §5.3: values format version. The only REQUIRED key: a block with no version cannot be interpreted safely under a future format revision. It is an INTEGER here, unlike the policy block's quoted string version (\"0.1\"), and deliberately so. The policy version is quoted because it is a dotted format identifier that YAML would otherwise coerce to the float 0.1, losing the distinction between \"0.10\" and \"0.1\". This block has no dotted versions and never will: it is a small, closed key set that either is revision 1 or is a revision this runtime does not know, so a bare integer is the honest type and leaves nothing for YAML to coerce.",
15
+ "examples": [1]
16
+ },
17
+ "love": {
18
+ "$ref": "#/$defs/valueList",
19
+ "description": "SPEC.md §5.3: what the operator loves, in their own words. The strongest of the three standing grades. Guidance for an agent reading the file, never an input to any decision the runtime makes."
20
+ },
21
+ "like": {
22
+ "$ref": "#/$defs/valueList",
23
+ "description": "SPEC.md §5.3: what the operator likes. The middle standing grade, and the one that carries most of the ordinary content."
24
+ },
25
+ "dislike": {
26
+ "$ref": "#/$defs/valueList",
27
+ "description": "SPEC.md §5.3: what the operator dislikes. The negative standing grade. It is NOT a prohibition: a prohibition belongs in the policy block, where it is enforced; an entry here is a preference an agent should weigh in how it works, and a runtime that refused an action because of it would be enforcing guidance."
28
+ },
29
+ "wants": {
30
+ "$ref": "#/$defs/valueList",
31
+ "description": "SPEC.md §5.3: what the operator wants FROM the agent, as behaviour rather than as taste. Where love/like/dislike describe the operator, this describes the working relationship they are asking for. Examples: \"honest opinions on the work, including when you think a task is wrong\"; \"a journal entry of about five points per milestone\". Still guidance: nothing here is checked, counted, or enforced, and an agent that does not do these things is not refused by anything."
32
+ },
33
+ "responds": {
34
+ "type": "string",
35
+ "maxLength": 500,
36
+ "description": "SPEC.md §5.3: how the operator reads and answers — the shape of the human end of the loop, so an agent can read silence, terseness or delay correctly. Deliberately a SINGLE SCALAR rather than a list: a list here would grow into a second rules document sitting beside the policy block, and the one thing this block must never become is a place where rules live outside the enforced file. One sentence or two, and no more room than that.",
37
+ "examples": [
38
+ "Usually within the hour on the phone; a bare 'ok' means yes and is not curtness."
39
+ ]
40
+ }
41
+ },
42
+ "$defs": {
43
+ "valueList": {
44
+ "type": "array",
45
+ "uniqueItems": true,
46
+ "maxItems": 20,
47
+ "items": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "maxLength": 200
51
+ },
52
+ "description": "SPEC.md §5.3: a standing list of short human-authored statements. Entries are unique (a repeated entry is an editing accident, and there is no counting here for a repeat to mean anything to) and capped at twenty of at most 200 characters each. The caps are not storage limits: this block is read by a person and by an agent's prompt, and a list long enough to need scrolling is one nobody reads, which is the failure mode guidance has instead of a validation error. An empty array is valid and says the operator considered this grade and had nothing to put in it."
53
+ }
54
+ }
55
+ }