approval-md 0.4.0 → 0.4.2

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 (190) hide show
  1. package/SPEC.md +11 -0
  2. package/cli.js +72 -4
  3. package/dist/src/channels/contract.d.ts +53 -0
  4. package/dist/src/channels/contract.js +56 -0
  5. package/dist/src/channels/contract.js.map +1 -1
  6. package/dist/src/channels/tagging.d.ts +25 -0
  7. package/dist/src/channels/tagging.js +57 -5
  8. package/dist/src/channels/tagging.js.map +1 -1
  9. package/dist/src/channels/telegram-minimal.d.ts +251 -0
  10. package/dist/src/channels/telegram-minimal.js +618 -0
  11. package/dist/src/channels/telegram-minimal.js.map +1 -0
  12. package/dist/src/channels/telegram.d.ts +454 -19
  13. package/dist/src/channels/telegram.js +931 -80
  14. package/dist/src/channels/telegram.js.map +1 -1
  15. package/dist/src/cli/amend.js +8 -1
  16. package/dist/src/cli/amend.js.map +1 -1
  17. package/dist/src/cli/attest.js +2 -1
  18. package/dist/src/cli/attest.js.map +1 -1
  19. package/dist/src/cli/audit-card.d.ts +1 -1
  20. package/dist/src/cli/audit-card.js +40 -1
  21. package/dist/src/cli/audit-card.js.map +1 -1
  22. package/dist/src/cli/audit.d.ts +7 -5
  23. package/dist/src/cli/audit.js +25 -12
  24. package/dist/src/cli/audit.js.map +1 -1
  25. package/dist/src/cli/channel-telegram-webhook.d.ts +2 -0
  26. package/dist/src/cli/channel-telegram-webhook.js +7 -0
  27. package/dist/src/cli/channel-telegram-webhook.js.map +1 -1
  28. package/dist/src/cli/channel-telegram.d.ts +75 -2
  29. package/dist/src/cli/channel-telegram.js +172 -16
  30. package/dist/src/cli/channel-telegram.js.map +1 -1
  31. package/dist/src/cli/channel-web.js +2 -1
  32. package/dist/src/cli/channel-web.js.map +1 -1
  33. package/dist/src/cli/channel.js +2 -1
  34. package/dist/src/cli/channel.js.map +1 -1
  35. package/dist/src/cli/doctor.js +1 -1
  36. package/dist/src/cli/doctor.js.map +1 -1
  37. package/dist/src/cli/execute.d.ts +12 -0
  38. package/dist/src/cli/execute.js +50 -4
  39. package/dist/src/cli/execute.js.map +1 -1
  40. package/dist/src/cli/gate.js +2 -1
  41. package/dist/src/cli/gate.js.map +1 -1
  42. package/dist/src/cli/help.d.ts +1 -0
  43. package/dist/src/cli/help.js +39 -18
  44. package/dist/src/cli/help.js.map +1 -1
  45. package/dist/src/cli/hook.d.ts +121 -3
  46. package/dist/src/cli/hook.js +719 -79
  47. package/dist/src/cli/hook.js.map +1 -1
  48. package/dist/src/cli/log-checkpoint.js +2 -1
  49. package/dist/src/cli/log-checkpoint.js.map +1 -1
  50. package/dist/src/cli/log-unlock.d.ts +23 -0
  51. package/dist/src/cli/log-unlock.js +110 -0
  52. package/dist/src/cli/log-unlock.js.map +1 -0
  53. package/dist/src/cli/main.js +23 -6
  54. package/dist/src/cli/main.js.map +1 -1
  55. package/dist/src/cli/setup-common.js +2 -1
  56. package/dist/src/cli/setup-common.js.map +1 -1
  57. package/dist/src/cli/up.js +5 -0
  58. package/dist/src/cli/up.js.map +1 -1
  59. package/dist/src/cli/vault.js +2 -1
  60. package/dist/src/cli/vault.js.map +1 -1
  61. package/dist/src/cli/verb-registry.js +1 -0
  62. package/dist/src/cli/verb-registry.js.map +1 -1
  63. package/dist/src/cli/wordmark.d.ts +2 -2
  64. package/dist/src/cli/wordmark.js +2 -2
  65. package/dist/src/core/attest-bootstrap.js +2 -1
  66. package/dist/src/core/attest-bootstrap.js.map +1 -1
  67. package/dist/src/core/attest.js +5 -4
  68. package/dist/src/core/attest.js.map +1 -1
  69. package/dist/src/core/audit.d.ts +113 -9
  70. package/dist/src/core/audit.js +382 -18
  71. package/dist/src/core/audit.js.map +1 -1
  72. package/dist/src/core/budgets.d.ts +12 -0
  73. package/dist/src/core/budgets.js +54 -1
  74. package/dist/src/core/budgets.js.map +1 -1
  75. package/dist/src/core/checkpoint.js +3 -2
  76. package/dist/src/core/checkpoint.js.map +1 -1
  77. package/dist/src/core/command-class.js +6 -0
  78. package/dist/src/core/command-class.js.map +1 -1
  79. package/dist/src/core/decision-refusal.js +2 -1
  80. package/dist/src/core/decision-refusal.js.map +1 -1
  81. package/dist/src/core/delegation.d.ts +109 -0
  82. package/dist/src/core/delegation.js +244 -0
  83. package/dist/src/core/delegation.js.map +1 -0
  84. package/dist/src/core/execute.js +12 -3
  85. package/dist/src/core/execute.js.map +1 -1
  86. package/dist/src/core/gate-window.js +4 -3
  87. package/dist/src/core/gate-window.js.map +1 -1
  88. package/dist/src/core/gate.d.ts +84 -43
  89. package/dist/src/core/gate.js +114 -6
  90. package/dist/src/core/gate.js.map +1 -1
  91. package/dist/src/core/gesture-refusal.js +2 -1
  92. package/dist/src/core/gesture-refusal.js.map +1 -1
  93. package/dist/src/core/identity.d.ts +105 -0
  94. package/dist/src/core/identity.js +122 -0
  95. package/dist/src/core/identity.js.map +1 -0
  96. package/dist/src/core/log-lock.d.ts +435 -0
  97. package/dist/src/core/log-lock.js +1093 -0
  98. package/dist/src/core/log-lock.js.map +1 -0
  99. package/dist/src/core/log.d.ts +82 -3
  100. package/dist/src/core/log.js +285 -40
  101. package/dist/src/core/log.js.map +1 -1
  102. package/dist/src/core/loop.d.ts +11 -0
  103. package/dist/src/core/loop.js +63 -4
  104. package/dist/src/core/loop.js.map +1 -1
  105. package/dist/src/core/policy-diff.js +20 -1
  106. package/dist/src/core/policy-diff.js.map +1 -1
  107. package/dist/src/core/policy-explain.js +18 -2
  108. package/dist/src/core/policy-explain.js.map +1 -1
  109. package/dist/src/core/policy-load.d.ts +38 -1
  110. package/dist/src/core/policy-load.js +34 -2
  111. package/dist/src/core/policy-load.js.map +1 -1
  112. package/dist/src/core/policy-match.js +30 -5
  113. package/dist/src/core/policy-match.js.map +1 -1
  114. package/dist/src/core/policy-proposal.d.ts +17 -0
  115. package/dist/src/core/policy-proposal.js +32 -16
  116. package/dist/src/core/policy-proposal.js.map +1 -1
  117. package/dist/src/core/prompt-layout.d.ts +186 -1
  118. package/dist/src/core/prompt-layout.js +345 -3
  119. package/dist/src/core/prompt-layout.js.map +1 -1
  120. package/dist/src/core/sender-identity.d.ts +24 -1
  121. package/dist/src/core/sender-identity.js +31 -0
  122. package/dist/src/core/sender-identity.js.map +1 -1
  123. package/dist/src/core/token.js +11 -0
  124. package/dist/src/core/token.js.map +1 -1
  125. package/dist/src/core/tool-map.d.ts +125 -0
  126. package/dist/src/core/tool-map.js +179 -0
  127. package/dist/src/core/tool-map.js.map +1 -0
  128. package/dist/src/core/validate.d.ts +20 -6
  129. package/dist/src/core/validate.js +0 -0
  130. package/dist/src/core/validate.js.map +1 -1
  131. package/dist/src/daemon/git-evidence.d.ts +2 -2
  132. package/dist/src/daemon/git-evidence.js +1 -1
  133. package/dist/src/serve/archive.d.ts +8 -0
  134. package/dist/src/serve/archive.js +11 -2
  135. package/dist/src/serve/archive.js.map +1 -1
  136. package/docs/README-extended.md +15 -0
  137. package/docs/cli-reference.md +571 -28
  138. package/package.json +2 -2
  139. package/schema/event.schema.json +387 -29
  140. package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
  141. package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
  142. package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
  143. package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
  144. package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
  145. package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
  146. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
  147. package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
  148. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
  149. package/schema/fixtures/event/invalid/audit-reviewed-model-actor.json +20 -0
  150. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
  151. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
  152. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
  153. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
  154. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
  155. package/schema/fixtures/event/invalid/audit-reviewed-verdict-source-model.json +20 -0
  156. package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
  157. package/schema/fixtures/event/invalid/execution-started-harness-tool-space.json +18 -0
  158. package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
  159. package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
  160. package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
  161. package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
  162. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
  163. package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
  164. package/schema/fixtures/event/valid/audit-sampled.json +2 -1
  165. package/schema/fixtures/event/valid/daemon-identity.json +2 -1
  166. package/schema/fixtures/event/valid/execution-started-harness-tool.json +19 -0
  167. package/schema/fixtures/policy/invalid/delegation-advice-string.json +15 -0
  168. package/schema/fixtures/policy/invalid/delegation-class-duplicate.json +18 -0
  169. package/schema/fixtures/policy/invalid/delegation-class-wildcard.json +17 -0
  170. package/schema/fixtures/policy/invalid/delegation-daily-cap-fractional.json +15 -0
  171. package/schema/fixtures/policy/invalid/delegation-daily-cap-negative.json +15 -0
  172. package/schema/fixtures/policy/invalid/delegation-daily-cap-too-high.json +15 -0
  173. package/schema/fixtures/policy/invalid/delegation-escalate-duplicate.json +18 -0
  174. package/schema/fixtures/policy/invalid/delegation-escalate-unknown.json +17 -0
  175. package/schema/fixtures/policy/invalid/delegation-max-autonomy-alias.json +15 -0
  176. package/schema/fixtures/policy/invalid/delegation-max-autonomy-autonomous.json +15 -0
  177. package/schema/fixtures/policy/invalid/delegation-model-no-version.json +15 -0
  178. package/schema/fixtures/policy/invalid/delegation-model-provider-form.json +15 -0
  179. package/schema/fixtures/policy/invalid/delegation-null.json +7 -0
  180. package/schema/fixtures/policy/invalid/delegation-reviewer-agent.json +17 -0
  181. package/schema/fixtures/policy/invalid/delegation-reviewer-bare-id.json +17 -0
  182. package/schema/fixtures/policy/invalid/delegation-unknown-key.json +16 -0
  183. package/schema/fixtures/policy/invalid/tools-class-wildcard.json +9 -0
  184. package/schema/fixtures/policy/invalid/tools-match-double-star.json +9 -0
  185. package/schema/fixtures/policy/invalid/unmapped-tool-unknown.json +7 -0
  186. package/schema/fixtures/policy/valid/delegation-empty.json +7 -0
  187. package/schema/fixtures/policy/valid/delegation-grammar-full.json +41 -0
  188. package/schema/fixtures/policy/valid/delegation-off.json +15 -0
  189. package/schema/fixtures/policy/valid/tools-mapping.json +17 -0
  190. package/schema/policy.schema.json +178 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "approval-md",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Human approval for agent actions. CLI runtime for the approval.md convention (pre-release).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,5 +59,5 @@
59
59
  "better-sqlite3": "12.11.1",
60
60
  "yaml": "2.9.0"
61
61
  },
62
- "gitHead": "fb0cf987985c2b9884ebe46b7b3b4419147d057e"
62
+ "gitHead": "9f796b03f184f139d4cccde09cea4a098951631d"
63
63
  }
@@ -54,6 +54,7 @@
54
54
  "audit.decision_refused",
55
55
  "audit.gesture_refused",
56
56
  "audit.question_preempted",
57
+ "audit.lock_reclaimed",
57
58
  "reconciliation.required",
58
59
  "reconciliation.satisfied",
59
60
  "payload.pruned",
@@ -64,12 +65,12 @@
64
65
  "gate.path.signed_off",
65
66
  "log.checkpoint"
66
67
  ],
67
- "description": "SPEC.md §8 'Event types (v0.1)': the closed set of thirty-four event types. Closed by design — an unrecognized type fails closed at the write boundary rather than entering the log unvalidated. `payload.pruned` (APRV-38) is the first addition after the v0.1 draft set of sixteen, `approval.withdrawn` (APRV-106) the second, `execution.indeterminate` with `execution.reconciled` (APRV-120) the third and fourth, `reconciliation.required` / `reconciliation.satisfied` (APRV-127) the fifth and sixth, `policy.proposed` / `policy.declined` (APRV-109) the seventh and eighth, `audit.dark_session` (APRV-192) the ninth, `gate.opened` / `gate.closed` / `gate.bypassed` (APRV-214) the tenth, eleventh and twelfth, `log.checkpoint` (APRV-220) the thirteenth, `audit.decision_refused` (APRV-235) the fourteenth, `gate.organ.attested` (APRV-272) the fifteenth, `gate.path.signed_off` (APRV-338) the sixteenth, `audit.gesture_refused` (APRV-355) the seventeenth, and `audit.question_preempted` (APRV-378) the eighteenth: a reader of a v0.1 log may encounter any of them, and a verifier written against the draft enum must be updated to accept them all."
68
+ "description": "SPEC.md §8 'Event types (v0.1)': the closed set of thirty-five event types. Closed by design — an unrecognized type fails closed at the write boundary rather than entering the log unvalidated. `payload.pruned` (APRV-38) is the first addition after the v0.1 draft set of sixteen, `approval.withdrawn` (APRV-106) the second, `execution.indeterminate` with `execution.reconciled` (APRV-120) the third and fourth, `reconciliation.required` / `reconciliation.satisfied` (APRV-127) the fifth and sixth, `policy.proposed` / `policy.declined` (APRV-109) the seventh and eighth, `audit.dark_session` (APRV-192) the ninth, `gate.opened` / `gate.closed` / `gate.bypassed` (APRV-214) the tenth, eleventh and twelfth, `log.checkpoint` (APRV-220) the thirteenth, `audit.decision_refused` (APRV-235) the fourteenth, `gate.organ.attested` (APRV-272) the fifteenth, `gate.path.signed_off` (APRV-338) the sixteenth, `audit.gesture_refused` (APRV-355) the seventeenth, `audit.question_preempted` (APRV-378) the eighteenth, and `audit.lock_reclaimed` (APRV-479) the nineteenth: a reader of a v0.1 log may encounter any of them, and a verifier written against the draft enum must be updated to accept them all."
68
69
  },
69
70
  "actor": {
70
71
  "type": "string",
71
72
  "pattern": "^(human|agent|system):.+",
72
- "description": "SPEC.md §8: who caused the event, as `<kind>:<id>` (the example record uses `human:carter`). Three kinds: `human:` for a person acting through a channel or the CLI, `agent:` for an agent-originated event, and `system:` for events the daemon originates with no external prompt (for example `approval.expired`, which fires on TTL lapse rather than on any actor's action). Approval decisions are restricted to `human:` — see the per-type rules below."
73
+ "description": "SPEC.md §8: who caused the event, as `<kind>:<id>` (the example record uses `human:carter`). Three kinds: `human:` for a person acting through a channel or the CLI, `agent:` for an agent-originated event, and `system:` for events the daemon originates with no external prompt (for example `approval.expired`, which fires on TTL lapse rather than on any actor's action). Approval decisions are restricted to `human:` — see the per-type rules below. A fourth kind, `model:<name>@<version>` (the judge, a pinned model reviewer), is RESERVED by APRV-500 (amended SPEC.md §5.2 proposed) and deliberately absent from this pattern: no record written by this release carries it, so a `model:` actor is refused here at the write boundary until a later core admits it on named events."
73
74
  },
74
75
  "alg": {
75
76
  "const": "sha256/jcs",
@@ -240,6 +241,156 @@
240
241
  { "type": "number", "minimum": 0 }
241
242
  ],
242
243
  "description": "The read form: the decimal string, or the pre-APRV-121 JSON number. The log is append-only, so a verifier must accept every shape the write boundary ever admitted. Used only in historical validation mode."
244
+ },
245
+ "audit_reviewed_record": {
246
+ "$comment": "SPEC.md §5.2/§8 (amended, APRV-481): the write-boundary form of an `audit.reviewed` record. A review must name the sample it answers (`subject_seq`), the execution that sample named (`sampled_subject_hash`), and what it concluded (`verdict`), because the follower and the research models read reviews as approvals and a review that does not say which action it judged, or what it concluded, is one nobody can count. `validate(..., { mode: \"historical\" })` replaces this definition with `audit_reviewed_record_historical`, so a review written before APRV-481 still validates and verifies unchanged.",
247
+ "type": "object",
248
+ "properties": {
249
+ "actor": {
250
+ "type": "string",
251
+ "pattern": "^human:"
252
+ },
253
+ "payload": {
254
+ "type": "object",
255
+ "properties": {
256
+ "subject_seq": {
257
+ "type": "integer",
258
+ "minimum": 1,
259
+ "description": "SPEC.md §5.2/§8 (amended, APRV-481): the `seq` of the `audit.sampled` record this review answers. Not the execution's seq: the sample is what was offered for review, and the execution it names is joined through `sampled_subject_hash`. REQUIRED on new records."
260
+ },
261
+ "sampled_subject_hash": {
262
+ "type": "string",
263
+ "pattern": "^[a-f0-9]{64}$",
264
+ "description": "SPEC.md §5.2/§8 (amended, APRV-481): the `hash` of the `execution.started` record the sample named (the sample's own `subject_hash`), copied by the runtime from the verified log and never supplied by a caller. The join key from a review to the execution it judged. REQUIRED on new records."
265
+ },
266
+ "verdict": {
267
+ "type": "string",
268
+ "enum": [
269
+ "ok",
270
+ "denied"
271
+ ],
272
+ "description": "SPEC.md §5.2 (amended, APRV-127): what the reviewer concluded. `ok`: the action should have happened — the spelling the v0.1 fixtures already used, kept rather than renamed, because a second word for a verdict already in logs is the drift this vocabulary exists to avoid. `denied`: it should not have, and the runtime appends a `reconciliation.required` naming the obligation the denial creates. REQUIRED on every record written since APRV-481, because a review that does not say what it concluded cannot count as anybody's approval; a review written before then may carry none and reads as the observation it was (the historical form of this definition), which is why the ABSENCE of this field on an old record is never read as a denial."
273
+ },
274
+ "verdict_source": {
275
+ "type": "string",
276
+ "const": "explicit",
277
+ "description": "SPEC.md §5.2 (amended, PR #614 refutation F5): how the verdict was given. `explicit` is the only value: since APRV-482 a verdict is said by the reviewer (`--ok`/`--deny`, a card's OK or second Deny) and never defaulted. A review written before then may carry `verdict: \"ok\"` that the runtime filled in for a grade-only tap or a bare terminal review, and nothing else in the record tells the two apart, so this field is the discriminator a follower reads: present means explicit, absent (on an old record) means the verdict may have been defaulted. REQUIRED on new records. A second value, `model` (a review given by the judge, `model:<name>@<version>`), is RESERVED by APRV-500 (amended SPEC.md §10.3 proposed): registered so the word means nothing else, never written by this release, and refused here by the `const`. A follower that meets it may ignore it, and then never counts a model review as a human approval."
278
+ },
279
+ "payload_hash": {
280
+ "type": "string",
281
+ "pattern": "^[a-f0-9]{64}$",
282
+ "description": "SPEC.md §5.2/§10.3 (amended, APRV-481): the SHA-256/JCS binding of the bytes the sampled execution ran, present ONLY when the surface that collected this review rendered those bytes whole in front of the reviewer (a review card's `bytes` view). Absent when the reviewer saw a hash, saw nothing, or reviewed at a terminal, so the field never claims more than the reviewer read. The runtime checks it equals the execution's recorded binding before appending."
283
+ },
284
+ "reaction": {
285
+ "type": "string",
286
+ "enum": [
287
+ "disliked",
288
+ "indifferent",
289
+ "liked",
290
+ "loved"
291
+ ],
292
+ "description": "SPEC.md §5.2 (amended, APRV-237): what the human thought of the action, graded. `verdict` remains the ENFORCEMENT field and is the only one any enforcement path reads; `reaction` is guidance travelling the human-to-agent direction and no routing, matching, sampling, budgeting or token decision consults it (SPEC.md §11.1 invariant 10). Optional and additive: a review written before the field existed still validates and still verifies, and the ABSENCE of the field is absence — it is never read as `indifferent`, which is a thing a person had to actually say. The four words are the whole vocabulary; a fifth is refused here rather than left to accumulate synonyms across surfaces."
293
+ }
294
+ },
295
+ "required": [
296
+ "subject_seq",
297
+ "sampled_subject_hash",
298
+ "verdict",
299
+ "verdict_source"
300
+ ]
301
+ }
302
+ },
303
+ "required": [
304
+ "actor",
305
+ "payload"
306
+ ]
307
+ },
308
+ "audit_reviewed_record_historical": {
309
+ "$comment": "The read form of `audit_reviewed_record` (APRV-481): every constraint a review has carried since APRV-56 and APRV-237, without the three fields APRV-481 made required or the `verdict_source` PR #614's F5 fix made required. The log is append-only, so a verifier must accept every review the write boundary ever admitted. Used only in historical validation.",
310
+ "type": "object",
311
+ "properties": {
312
+ "actor": {
313
+ "type": "string",
314
+ "pattern": "^human:"
315
+ },
316
+ "payload": {
317
+ "type": "object",
318
+ "properties": {
319
+ "subject_seq": {
320
+ "type": "integer",
321
+ "minimum": 1
322
+ },
323
+ "sampled_subject_hash": {
324
+ "type": "string",
325
+ "pattern": "^[a-f0-9]{64}$"
326
+ },
327
+ "verdict": {
328
+ "type": "string",
329
+ "enum": [
330
+ "ok",
331
+ "denied"
332
+ ]
333
+ },
334
+ "verdict_source": {
335
+ "type": "string",
336
+ "const": "explicit"
337
+ },
338
+ "payload_hash": {
339
+ "type": "string",
340
+ "pattern": "^[a-f0-9]{64}$"
341
+ },
342
+ "reaction": {
343
+ "type": "string",
344
+ "enum": [
345
+ "disliked",
346
+ "indifferent",
347
+ "liked",
348
+ "loved"
349
+ ]
350
+ }
351
+ }
352
+ }
353
+ },
354
+ "required": [
355
+ "actor"
356
+ ]
357
+ },
358
+ "audit_sampled_record": {
359
+ "$comment": "SPEC.md §5.2/§8 (amended, PR #614 refutation F2, recheck NF-3): the write-boundary form of an `audit.sampled` record. A sample pins the attested policy its execution ran under (`policy_sha256`: the latest attestation before the sampled `execution.started`), because a retrospective review is held to the approvers roster of THAT policy: read from whatever policy is in force at review or sampling time, a re-attestation after the run could re-roster the review or leave the sample with no roster at all. `validate(..., { mode: \"historical\" })` replaces this definition with `audit_sampled_record_historical`, so a sample written before the field existed still validates and verifies, and reads as not pinned.",
360
+ "type": "object",
361
+ "properties": {
362
+ "payload": {
363
+ "type": "object",
364
+ "properties": {
365
+ "policy_sha256": {
366
+ "type": "string",
367
+ "pattern": "^[a-f0-9]{64}$",
368
+ "description": "SPEC.md §5.2/§8 (amended, PR #614 refutation F2): the SHA-256 of the policy bytes the latest attestation named when this sample was taken, copied by the runtime from the verified log. A review of this sample reads its roster only from bytes that hash to this value. REQUIRED on new records."
369
+ }
370
+ },
371
+ "required": [
372
+ "policy_sha256"
373
+ ]
374
+ }
375
+ },
376
+ "required": [
377
+ "payload"
378
+ ]
379
+ },
380
+ "audit_sampled_record_historical": {
381
+ "$comment": "The read form of `audit_sampled_record` (PR #614 refutation F2): the same field shape without the requirement, so a sample written before samples pinned their policy still verifies. Used only in historical validation.",
382
+ "type": "object",
383
+ "properties": {
384
+ "payload": {
385
+ "type": "object",
386
+ "properties": {
387
+ "policy_sha256": {
388
+ "type": "string",
389
+ "pattern": "^[a-f0-9]{64}$"
390
+ }
391
+ }
392
+ }
393
+ }
243
394
  }
244
395
  },
245
396
  "allOf": [
@@ -322,39 +473,24 @@
322
473
  ]
323
474
  },
324
475
  "then": {
476
+ "$ref": "#/$defs/audit_reviewed_record"
477
+ }
478
+ },
479
+ {
480
+ "$comment": "SPEC.md §5.2/§8 (amended, PR #614 refutation F2): a sample names the attested policy it was taken under, so its review can be held to that policy's roster.",
481
+ "if": {
325
482
  "type": "object",
326
483
  "properties": {
327
- "actor": {
328
- "type": "string",
329
- "pattern": "^human:"
330
- },
331
- "payload": {
332
- "type": "object",
333
- "properties": {
334
- "verdict": {
335
- "type": "string",
336
- "enum": [
337
- "ok",
338
- "denied"
339
- ],
340
- "description": "SPEC.md §5.2 (amended, APRV-127): what the reviewer concluded. `ok`: the action should have happened — the spelling the v0.1 fixtures already used, kept rather than renamed, because a second word for a verdict already in logs is the drift this vocabulary exists to avoid. `denied`: it should not have, and the runtime appends a `reconciliation.required` naming the obligation the denial creates. Optional and additive — a review written before the split carries none and reads as the observation it was, which is why the ABSENCE of this field is never read as a denial."
341
- },
342
- "reaction": {
343
- "type": "string",
344
- "enum": [
345
- "disliked",
346
- "indifferent",
347
- "liked",
348
- "loved"
349
- ],
350
- "description": "SPEC.md §5.2 (amended, APRV-237): what the human thought of the action, graded. `verdict` remains the ENFORCEMENT field and is the only one any enforcement path reads; `reaction` is guidance travelling the human-to-agent direction and no routing, matching, sampling, budgeting or token decision consults it (SPEC.md §11.1 invariant 10). Optional and additive: a review written before the field existed still validates and still verifies, and the ABSENCE of the field is absence — it is never read as `indifferent`, which is a thing a person had to actually say. The four words are the whole vocabulary; a fifth is refused here rather than left to accumulate synonyms across surfaces."
351
- }
352
- }
484
+ "event": {
485
+ "const": "audit.sampled"
353
486
  }
354
487
  },
355
488
  "required": [
356
- "actor"
489
+ "event"
357
490
  ]
491
+ },
492
+ "then": {
493
+ "$ref": "#/$defs/audit_sampled_record"
358
494
  }
359
495
  },
360
496
  {
@@ -871,6 +1007,38 @@
871
1007
  }
872
1008
  }
873
1009
  },
1010
+ {
1011
+ "$comment": "SPEC.md §8 (APRV-499, proposed): a harness `execution.started` whose class came from the policy's tool mapping (a `tools` entry, or `harness.tool.unmapped` under `defaults.unmapped_tool`) carries `harness_tool`, the harness tool NAME the call was made under, because several tools share one such class and the class alone cannot say which ran. The name only: the call's arguments are bound by `payload_hash` and are never written to the log. OPTIONAL and additive: every record written before the field existed still validates and still verifies, and a start on any other path records none. Only the SHAPE is constrained here: the characters a `tools` match may name (letters, digits, `_`, `.`, `:`, `-`), at most 256 of them, so a name outside that set refuses the append rather than writing bytes no mapping could have named.",
1012
+ "if": {
1013
+ "type": "object",
1014
+ "properties": {
1015
+ "event": {
1016
+ "const": "execution.started"
1017
+ }
1018
+ },
1019
+ "required": [
1020
+ "event"
1021
+ ]
1022
+ },
1023
+ "then": {
1024
+ "type": "object",
1025
+ "properties": {
1026
+ "payload": {
1027
+ "type": "object",
1028
+ "properties": {
1029
+ "harness_tool": {
1030
+ "type": "string",
1031
+ "minLength": 1,
1032
+ "maxLength": 256,
1033
+ "pattern": "^[A-Za-z0-9_.:-]+$",
1034
+ "description": "SPEC.md §8 (APRV-499, proposed): the harness tool name a policy-mapped or unmapped harness execution was made under.",
1035
+ "examples": ["mcp__contextsling__publish"]
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ },
874
1042
  {
875
1043
  "$comment": "SPEC.md §10.3 (amended, APRV-38): a decision collected as part of a batch gesture carries the batch's channel delivery id as a first-class payload field. Optional — a unit decision has no batch — and constrained only when present, so the field can never arrive as an empty string or a number that audit would have to guess at.",
876
1044
  "if": {
@@ -2045,6 +2213,196 @@
2045
2213
  }
2046
2214
  }
2047
2215
  },
2216
+ {
2217
+ "$comment": "SPEC.md §8 (proposed, APRV-479, pending sign-off): `<log>.lock` was taken over from a holder that is gone, and this is the first record under the lock that was taken. A `system:log` actor is this runtime's log writer, which proved the holder gone after waiting out its whole lock timeout: its process has exited in the writer's own pid namespace and boot (Linux: `kill(pid, 0)` answers ESRCH with `/proc/<pid>` absent or a zombie, or the pid names a process that started later; elsewhere the same host and boot and `kill(pid, 0)` answering ESRCH), or the lockfile names no holder and is older than the reclaim age. A `human:` actor is a person's `approval log unlock`, for a lock no writer can judge (another container or boot): the reason is then that person's assertion, not a proof. `core/log-lock.ts` states the rules.\n\nNo existing type carries it. The audit records before it are about a gesture on a request, a question answered elsewhere, or a sweep over activity; this is about the log's own lock and nothing else.\n\nAUDIT TIER, on the strict terms `audit.decision_refused` set: it grants nothing, mints no token, settles no request, charges no budget, is not sampled, and NO ENFORCEMENT PATH READS IT. Its `ts` is the write boundary's, like every audit record's.\n\nThe payload names the lockfile by its base name, the reason from a closed set, how long the lock had been held, and, when the lockfile carried a holder record, the holder's pid, kind and start of hold, strictly parsed: nothing else is taken from the file. A start of hold that is not plausible (before the log's last record by more than 24 hours, or after the taker's clock by more than 5 minutes) is recorded as null, with no age. The holder's host is deliberately not recorded: a log is often committed, and a machine name is not the log's business.",
2218
+ "if": {
2219
+ "type": "object",
2220
+ "properties": {
2221
+ "event": {
2222
+ "const": "audit.lock_reclaimed"
2223
+ }
2224
+ },
2225
+ "required": [
2226
+ "event"
2227
+ ]
2228
+ },
2229
+ "then": {
2230
+ "type": "object",
2231
+ "required": [
2232
+ "actor",
2233
+ "payload"
2234
+ ],
2235
+ "allOf": [
2236
+ {
2237
+ "$comment": "`age_ms` is required, and is absent exactly when `holder.created` is null: an implausible `created` is not recorded, and neither is the age derived from it (APRV-479 R3-7).",
2238
+ "if": {
2239
+ "type": "object",
2240
+ "properties": {
2241
+ "payload": {
2242
+ "type": "object",
2243
+ "properties": {
2244
+ "holder": {
2245
+ "type": "object",
2246
+ "properties": {
2247
+ "created": {
2248
+ "type": "null"
2249
+ }
2250
+ },
2251
+ "required": [
2252
+ "created"
2253
+ ]
2254
+ }
2255
+ },
2256
+ "required": [
2257
+ "holder"
2258
+ ]
2259
+ }
2260
+ },
2261
+ "required": [
2262
+ "payload"
2263
+ ]
2264
+ },
2265
+ "then": {
2266
+ "type": "object",
2267
+ "properties": {
2268
+ "payload": {
2269
+ "type": "object",
2270
+ "not": {
2271
+ "properties": {
2272
+ "age_ms": {}
2273
+ },
2274
+ "required": [
2275
+ "age_ms"
2276
+ ]
2277
+ }
2278
+ }
2279
+ }
2280
+ },
2281
+ "else": {
2282
+ "type": "object",
2283
+ "properties": {
2284
+ "payload": {
2285
+ "type": "object",
2286
+ "properties": {
2287
+ "age_ms": {}
2288
+ },
2289
+ "required": [
2290
+ "age_ms"
2291
+ ]
2292
+ }
2293
+ }
2294
+ }
2295
+ },
2296
+ {
2297
+ "$comment": "A person's assertion is never recorded as the writer's proof, nor the reverse: `operator-cleared` if and only if the actor is `human:`.",
2298
+ "if": {
2299
+ "type": "object",
2300
+ "properties": {
2301
+ "payload": {
2302
+ "type": "object",
2303
+ "properties": {
2304
+ "reason": {
2305
+ "const": "operator-cleared"
2306
+ }
2307
+ },
2308
+ "required": [
2309
+ "reason"
2310
+ ]
2311
+ }
2312
+ },
2313
+ "required": [
2314
+ "payload"
2315
+ ]
2316
+ },
2317
+ "then": {
2318
+ "type": "object",
2319
+ "properties": {
2320
+ "actor": {
2321
+ "type": "string",
2322
+ "pattern": "^human:"
2323
+ }
2324
+ }
2325
+ },
2326
+ "else": {
2327
+ "type": "object",
2328
+ "properties": {
2329
+ "actor": {
2330
+ "type": "string",
2331
+ "pattern": "^system:"
2332
+ }
2333
+ }
2334
+ }
2335
+ }
2336
+ ],
2337
+ "properties": {
2338
+ "actor": {
2339
+ "type": "string",
2340
+ "pattern": "^(system|human):",
2341
+ "description": "`system:log`: the runtime's log writer, which proved the holder gone. `human:<id>`: the person who ran `approval log unlock`, and only with reason `operator-cleared`. An `agent:` actor is refused: no agent takes over the log's lock."
2342
+ },
2343
+ "payload": {
2344
+ "type": "object",
2345
+ "additionalProperties": false,
2346
+ "required": [
2347
+ "lockfile",
2348
+ "reason"
2349
+ ],
2350
+ "properties": {
2351
+ "lockfile": {
2352
+ "type": "string",
2353
+ "minLength": 1,
2354
+ "pattern": "^[^/\\\\]+$",
2355
+ "description": "The lockfile's base name (`events.jsonl.lock`), never a path."
2356
+ },
2357
+ "reason": {
2358
+ "enum": [
2359
+ "holder-dead",
2360
+ "legacy-aged",
2361
+ "operator-cleared"
2362
+ ],
2363
+ "description": "Why the holder counts as gone. CLOSED, additive-only. `holder-dead`: the writer proved it: no process has the holder's pid in the writer's pid namespace and boot, or the pid names a later process. `legacy-aged`: the writer found the lockfile naming no holder and older than the reclaim age. `operator-cleared`: a person ran `approval log unlock` and asserted the holder gone, which is not a proof; it is the only reason a `human:` actor may carry, and a `system:` actor may never carry it."
2364
+ },
2365
+ "age_ms": {
2366
+ "type": "integer",
2367
+ "minimum": 0,
2368
+ "description": "How long the lock had been held when it was taken over, in milliseconds, by the taker's clock: from the holder record's `created`, or from the lockfile's mtime when it had none. Required, except that it is absent exactly when `holder.created` is null (its `created` was implausible, so no age derived from it is recorded)."
2369
+ },
2370
+ "holder": {
2371
+ "type": "object",
2372
+ "additionalProperties": false,
2373
+ "required": [
2374
+ "pid",
2375
+ "op",
2376
+ "created"
2377
+ ],
2378
+ "description": "The holder record the lockfile carried, strictly parsed. Absent exactly when it carried none.",
2379
+ "properties": {
2380
+ "pid": {
2381
+ "type": "integer",
2382
+ "minimum": 1
2383
+ },
2384
+ "op": {
2385
+ "enum": [
2386
+ "append",
2387
+ "hold"
2388
+ ],
2389
+ "description": "`append`: one record. `hold`: a whole operation under the lock (`log sync`, `log advance`, an export, an unlock)."
2390
+ },
2391
+ "created": {
2392
+ "type": [
2393
+ "string",
2394
+ "null"
2395
+ ],
2396
+ "format": "date-time",
2397
+ "description": "When the holder took the lock, by its own clock. `null` when the value the lockfile carried was not plausible: earlier than the log's last record's `ts` minus 24 hours, or later than the taker's clock plus 5 minutes (a value the file chose, recorded as nothing)."
2398
+ }
2399
+ }
2400
+ }
2401
+ }
2402
+ }
2403
+ }
2404
+ }
2405
+ },
2048
2406
  {
2049
2407
  "$comment": "SPEC.md §5.2 (amended, APRV-214), 'The open window': a person opening the harness gate on themselves so the gate can be debugged from inside. The actor MUST be `^human:`, in the schema as well as in `core/gate-window.ts`, for the reason attestation refuses an agent: this is the one act that suspends the policy, and an agent able to author it could authorize its own next command. The window's state lives here and in no file, so what a reader derives is derived from a verified chain and nothing else. Expiry is `ts` plus `duration`, and a record claiming an `expires_at` beyond that reads as the shorter of the two: the field is a convenience for a human reading the log, never the authority.",
2050
2408
  "if": {
@@ -0,0 +1,19 @@
1
+ {
2
+ "seq": 217,
3
+ "ts": "2026-10-05T07:41:12.408Z",
4
+ "event": "audit.lock_reclaimed",
5
+ "actor": "system:log",
6
+ "payload": {
7
+ "lockfile": "events.jsonl.lock",
8
+ "reason": "holder-dead",
9
+ "age_ms": 63958419351790,
10
+ "holder": {
11
+ "pid": 48213,
12
+ "op": "append",
13
+ "created": null
14
+ }
15
+ },
16
+ "alg": "sha256/jcs",
17
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
18
+ "hash": "2e9b7c1a4d6f08213b5c7e9a0d2f416385a7c9e1b3d5f70921a3c5e7092b4d6f"
19
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "seq": 214,
3
+ "ts": "2026-10-05T07:41:12.408Z",
4
+ "event": "audit.lock_reclaimed",
5
+ "actor": "agent:claude-code",
6
+ "payload": {
7
+ "lockfile": "events.jsonl.lock",
8
+ "reason": "holder-dead",
9
+ "age_ms": 734211
10
+ },
11
+ "alg": "sha256/jcs",
12
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
13
+ "hash": "2e9b7c1a4d6f08213b5c7e9a0d2f416385a7c9e1b3d5f70921a3c5e7092b4d6f"
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "seq": 214,
3
+ "ts": "2026-10-05T07:41:12.408Z",
4
+ "event": "audit.lock_reclaimed",
5
+ "actor": "system:log",
6
+ "payload": {
7
+ "lockfile": "/home/tenant/.approval/log/events.jsonl.lock",
8
+ "reason": "holder-dead",
9
+ "age_ms": 734211
10
+ },
11
+ "alg": "sha256/jcs",
12
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
13
+ "hash": "2e9b7c1a4d6f08213b5c7e9a0d2f416385a7c9e1b3d5f70921a3c5e7092b4d6f"
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "seq": 216,
3
+ "ts": "2026-10-05T11:02:40.115Z",
4
+ "event": "audit.lock_reclaimed",
5
+ "actor": "system:log",
6
+ "payload": {
7
+ "lockfile": "events.jsonl.lock",
8
+ "reason": "operator-cleared",
9
+ "age_ms": 4021877
10
+ },
11
+ "alg": "sha256/jcs",
12
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
13
+ "hash": "2e9b7c1a4d6f08213b5c7e9a0d2f416385a7c9e1b3d5f70921a3c5e7092b4d6f"
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "seq": 214,
3
+ "ts": "2026-10-05T07:41:12.408Z",
4
+ "event": "audit.lock_reclaimed",
5
+ "actor": "system:log",
6
+ "payload": {
7
+ "lockfile": "events.jsonl.lock",
8
+ "reason": "holder-looked-old",
9
+ "age_ms": 734211
10
+ },
11
+ "alg": "sha256/jcs",
12
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
13
+ "hash": "2e9b7c1a4d6f08213b5c7e9a0d2f416385a7c9e1b3d5f70921a3c5e7092b4d6f"
14
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "seq": 215,
3
+ "ts": "2026-10-05T07:41:12.408Z",
4
+ "event": "audit.lock_reclaimed",
5
+ "actor": "system:log",
6
+ "payload": {
7
+ "lockfile": "events.jsonl.lock",
8
+ "reason": "holder-dead"
9
+ },
10
+ "alg": "sha256/jcs",
11
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
12
+ "hash": "2e9b7c1a4d6f08213b5c7e9a0d2f416385a7c9e1b3d5f70921a3c5e7092b4d6f"
13
+ }
@@ -7,7 +7,12 @@
7
7
  "actor": "agent:chaser",
8
8
  "channel": "cli",
9
9
  "payload": {
10
- "verdict": "ok"
10
+ "subject_seq": 15,
11
+ "subject_event": "audit.sampled",
12
+ "reviewed": true,
13
+ "sampled_subject_hash": "6c1d3f0b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817060504",
14
+ "verdict": "ok",
15
+ "verdict_source": "explicit"
11
16
  },
12
17
  "alg": "sha256/jcs",
13
18
  "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
@@ -0,0 +1,21 @@
1
+ {
2
+ "seq": 16,
3
+ "ts": "2026-08-04T09:26:02Z",
4
+ "event": "audit.reviewed",
5
+ "task": "task-049",
6
+ "action_key": "task-049:archive:2026-08-04",
7
+ "actor": "human:carter",
8
+ "channel": "cli",
9
+ "payload": {
10
+ "subject_seq": 15,
11
+ "subject_event": "audit.sampled",
12
+ "reviewed": true,
13
+ "sampled_subject_hash": "6c1d3f0b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817060504",
14
+ "verdict": "ok",
15
+ "verdict_source": "explicit",
16
+ "payload_hash": "6C1D3F0B"
17
+ },
18
+ "alg": "sha256/jcs",
19
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
20
+ "hash": "6752e6617b2b810c1cc0cf09c51678c78891ff6fdfd8bb9399aecbdfca00dd5b"
21
+ }
@@ -7,7 +7,12 @@
7
7
  "actor": "human:carter",
8
8
  "channel": "cli",
9
9
  "payload": {
10
+ "subject_seq": 15,
11
+ "subject_event": "audit.sampled",
12
+ "reviewed": true,
13
+ "sampled_subject_hash": "6c1d3f0b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817060504",
10
14
  "verdict": "ok",
15
+ "verdict_source": "explicit",
11
16
  "reaction": "loved"
12
17
  },
13
18
  "alg": "sha256/jcs",
@@ -0,0 +1,20 @@
1
+ {
2
+ "seq": 16,
3
+ "ts": "2026-08-04T09:26:02Z",
4
+ "event": "audit.reviewed",
5
+ "task": "task-049",
6
+ "action_key": "task-049:archive:2026-08-04",
7
+ "actor": "model:judge@0.3.0",
8
+ "channel": "cli",
9
+ "payload": {
10
+ "subject_seq": 15,
11
+ "subject_event": "audit.sampled",
12
+ "reviewed": true,
13
+ "sampled_subject_hash": "6c1d3f0b9a8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817060504",
14
+ "verdict": "ok",
15
+ "verdict_source": "explicit"
16
+ },
17
+ "alg": "sha256/jcs",
18
+ "prev": "7f625051c176f2f260a1f7e25c9a933717ebb49917a8788ac25ae7a4614ebb30",
19
+ "hash": "6752e6617b2b810c1cc0cf09c51678c78891ff6fdfd8bb9399aecbdfca00dd5b"
20
+ }