approval-md 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/schema/event.schema.json
CHANGED
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"audit.reviewed",
|
|
53
53
|
"audit.dark_session",
|
|
54
54
|
"audit.decision_refused",
|
|
55
|
+
"audit.gesture_refused",
|
|
56
|
+
"audit.question_preempted",
|
|
55
57
|
"reconciliation.required",
|
|
56
58
|
"reconciliation.satisfied",
|
|
57
59
|
"payload.pruned",
|
|
@@ -59,9 +61,10 @@
|
|
|
59
61
|
"gate.closed",
|
|
60
62
|
"gate.bypassed",
|
|
61
63
|
"gate.organ.attested",
|
|
64
|
+
"gate.path.signed_off",
|
|
62
65
|
"log.checkpoint"
|
|
63
66
|
],
|
|
64
|
-
"description": "SPEC.md §8 'Event types (v0.1)': the closed set of thirty-
|
|
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."
|
|
65
68
|
},
|
|
66
69
|
"actor": {
|
|
67
70
|
"type": "string",
|
|
@@ -113,9 +116,11 @@
|
|
|
113
116
|
"enum": [
|
|
114
117
|
"claude-code",
|
|
115
118
|
"cursor",
|
|
116
|
-
"codex"
|
|
119
|
+
"codex",
|
|
120
|
+
"grok",
|
|
121
|
+
"muse"
|
|
117
122
|
],
|
|
118
|
-
"description": "SPEC.md §10.1 (amended, APRV-227): which harness hook wrote this record. A closed set, extended only by a task that adds the case, because it is the discriminator a reader scopes `harness_version` by: one log holds the records of every harness that ever wrote to it, and a version with no binary named beside it is a string nobody can compare against anything. OPTIONAL and additive — every record written before the field existed still validates and still verifies, and the two fields travel together or not at all."
|
|
123
|
+
"description": "SPEC.md §10.1 (amended, APRV-227): which harness hook wrote this record. A closed set, extended only by a task that adds the case, because it is the discriminator a reader scopes `harness_version` by: one log holds the records of every harness that ever wrote to it, and a version with no binary named beside it is a string nobody can compare against anything. OPTIONAL and additive — every record written before the field existed still validates and still verifies, and the two fields travel together or not at all. APRV-358 added `grok`, which APRV-243 shipped an adapter for and never added here: a Grok session's manual-class registration failed validation at the write boundary, so the request never reached a human. Closed does not mean hand-maintained any more. This list is pinned set-equal to `HARNESS_KINDS` in `src/core/harness-version.ts` by `tests/harness-enum.test.ts`, along with every other place a harness name is enumerated, so the next adapter cannot land with the enum left behind."
|
|
119
124
|
},
|
|
120
125
|
"harness_version": {
|
|
121
126
|
"type": "string",
|
|
@@ -184,6 +189,34 @@
|
|
|
184
189
|
}
|
|
185
190
|
},
|
|
186
191
|
"$defs": {
|
|
192
|
+
"senderHashed": {
|
|
193
|
+
"const": true,
|
|
194
|
+
"description": "SPEC.md §5.2/§10.3 (amended, APRV-370): the sibling `id` is the operator's KEYED digest of the account, `hmac-sha256:<64 lowercase hex>` — HMAC-SHA-256 of the transport's own account id under a secret named by `APPROVAL_SENDER_KEY` — rather than the account id itself. It appears exactly when the attested `approvers[id].senders` mapping for that channel is written in the keyed form, which is what a deployment that PUBLISHES its policy and its log uses so that neither discloses the account.\n\n`true` or ABSENT, never `false`, and that asymmetry is deliberate: absent is the raw form, which is the record every build since APRV-324 has written, and a `false` here would make all of those read afterwards as though they were missing something. The digest is not a credential either — it is published in the policy — and it is not an authenticator: nothing in the gate's safety rests on the key's secrecy, and a process that loses the key loses the ability to RESOLVE accounts, never the ability to refuse.",
|
|
195
|
+
"examples": [true]
|
|
196
|
+
},
|
|
197
|
+
"senderHashedImpliesDigest": {
|
|
198
|
+
"$comment": "APRV-370. `hashed` and `id` describe the same fact in two shapes, and two shapes of one fact can disagree. They cannot here: whenever the flag is present the id must be the digest form, so a record claiming to be hashed while carrying a bare account id does not validate, and a reader may branch on the boolean without parsing the string. The converse is deliberately NOT required — an id that merely looks like a digest is not asserted to be one — because the assertion is the flag's to make.",
|
|
199
|
+
"if": {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"hashed": {
|
|
203
|
+
"const": true
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"required": [
|
|
207
|
+
"hashed"
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
"then": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"id": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"pattern": "^hmac-sha256:[0-9a-f]{64}$"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
187
220
|
"usd_amount_string": {
|
|
188
221
|
"type": "string",
|
|
189
222
|
"pattern": "^(0|[1-9][0-9]*)(\\.[0-9]{0,5}[1-9])?$",
|
|
@@ -719,6 +752,11 @@
|
|
|
719
752
|
"type": "integer",
|
|
720
753
|
"minimum": 1,
|
|
721
754
|
"description": "The `policy.proposed` record this attestation answers."
|
|
755
|
+
},
|
|
756
|
+
"payload_hash": {
|
|
757
|
+
"type": "string",
|
|
758
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
759
|
+
"description": "SPEC.md §5.2/§10.4 (APRV-356): the binding for the ATTESTED policy text, stored beside the log, so the policy in force is recoverable and not only nameable. A digest alone cannot produce the file it names, and the privileged-gesture rule of §10.3 has to be decided against the policy in force rather than the one being proposed. Optional and additive: records written before this existed still validate and verify, and a reader treats absence as the pre-amendment state, where the in-force bytes are unrecoverable and the fail-closed fallback applies. A reader that recovers the text MUST re-hash it against `sha256` from the verified log, so the store is checked rather than trusted."
|
|
722
760
|
}
|
|
723
761
|
}
|
|
724
762
|
}
|
|
@@ -828,6 +866,140 @@
|
|
|
828
866
|
}
|
|
829
867
|
}
|
|
830
868
|
},
|
|
869
|
+
{
|
|
870
|
+
"$comment": "SPEC.md §6.3/§10.3 (amended, APRV-324): who decided, as evidence rather than as configuration. `sender` is the account the channel's own transport attributed the gesture to — for Telegram the numeric `callback_query.from.id` — and the record's `actor` is the human the attested `approvers[id].senders` mapping binds that account to. Both optional and both additive: a decision made on a surface that authenticates no sender, or under a policy that maps none, carries neither key and is byte-identical to a decision written before they existed. That ABSENCE is meaningful and is why the fields are not defaulted: it says the attribution came from the identity the deciding process was launched with, which is the whole of what every build before APRV-324 could say. What may never appear here is anything the sender says about themselves: the mapping key is the transport's attribution, never a name, handle or id a message body claims (§11.1 invariant 4), and `from.username` is deliberately absent from the vocabulary because it is mutable and reusable and a log carrying it would grow a field that reads like identity and decays into a lie. `sender_source` names the kind of evidence, as a closed vocabulary, so a later source is distinguishable in a log rather than silently mixed in with policy-attested ones.",
|
|
871
|
+
"if": {
|
|
872
|
+
"type": "object",
|
|
873
|
+
"properties": {
|
|
874
|
+
"event": {
|
|
875
|
+
"enum": [
|
|
876
|
+
"approval.granted",
|
|
877
|
+
"approval.rejected",
|
|
878
|
+
"approval.revoked"
|
|
879
|
+
]
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
"required": [
|
|
883
|
+
"event"
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
"then": {
|
|
887
|
+
"type": "object",
|
|
888
|
+
"properties": {
|
|
889
|
+
"payload": {
|
|
890
|
+
"type": "object",
|
|
891
|
+
"properties": {
|
|
892
|
+
"sender": {
|
|
893
|
+
"type": "object",
|
|
894
|
+
"additionalProperties": false,
|
|
895
|
+
"required": [
|
|
896
|
+
"channel",
|
|
897
|
+
"id"
|
|
898
|
+
],
|
|
899
|
+
"description": "The authenticated sender the `actor` was resolved from. Present only where a transport authenticated one and the attested policy mapped it.",
|
|
900
|
+
"properties": {
|
|
901
|
+
"channel": {
|
|
902
|
+
"type": "string",
|
|
903
|
+
"minLength": 1,
|
|
904
|
+
"description": "The surface that observed the sender: `telegram`. Named separately from the record's own `channel` because a sender id is only meaningful inside the namespace that issued it."
|
|
905
|
+
},
|
|
906
|
+
"id": {
|
|
907
|
+
"type": "string",
|
|
908
|
+
"minLength": 1,
|
|
909
|
+
"description": "The transport's own account id, as a string, OR the operator's keyed digest of it when `hashed` is true. Raw, it is not a credential — a Telegram user id is visible to everyone in a chat — and it is recorded so an operator investigating a decision has the number rather than a name they cannot check."
|
|
910
|
+
},
|
|
911
|
+
"hashed": {
|
|
912
|
+
"$ref": "#/$defs/senderHashed"
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
"allOf": [
|
|
916
|
+
{
|
|
917
|
+
"$ref": "#/$defs/senderHashedImpliesDigest"
|
|
918
|
+
}
|
|
919
|
+
],
|
|
920
|
+
"examples": [
|
|
921
|
+
{ "channel": "telegram", "id": "12345678" },
|
|
922
|
+
{
|
|
923
|
+
"channel": "telegram",
|
|
924
|
+
"id": "hmac-sha256:3f2a9c11b4d7e6085a1c2f9d8e7b6a5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a",
|
|
925
|
+
"hashed": true
|
|
926
|
+
}
|
|
927
|
+
]
|
|
928
|
+
},
|
|
929
|
+
"sender_source": {
|
|
930
|
+
"type": "string",
|
|
931
|
+
"enum": [
|
|
932
|
+
"policy"
|
|
933
|
+
],
|
|
934
|
+
"description": "How the sender became the actor. `policy` is the attested `approvers[id].senders` mapping of SPEC.md §5.2, and it is the only member today. A closed vocabulary on purpose: a future source of the same shape must be distinguishable from an operator's attestation rather than read back as one."
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
"$comment": "SPEC.md §10.3 (amended, APRV-324): the same two fields on the other human acts a channel collects. An attestation decides which rules are in force and a retrospective review is handed to agents as human-authored guidance, so both are recorded as a person's and both are worth attributing to an account rather than to whichever process was listening. Optional and additive on exactly the terms the decision events carry them: absent means the attribution came from the deciding process's configuration. `log.checkpoint` is deliberately NOT in this list — its payload is a signed structure, and an unsigned field sitting beside a signature invites a reader to treat it as covered by one; a checkpoint tap resolves its ACTOR from the sender exactly as these do, and the account it came from is not written next to the signature.",
|
|
943
|
+
"if": {
|
|
944
|
+
"type": "object",
|
|
945
|
+
"properties": {
|
|
946
|
+
"event": {
|
|
947
|
+
"enum": [
|
|
948
|
+
"policy.updated",
|
|
949
|
+
"policy.declined",
|
|
950
|
+
"audit.reviewed"
|
|
951
|
+
]
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
"required": [
|
|
955
|
+
"event"
|
|
956
|
+
]
|
|
957
|
+
},
|
|
958
|
+
"then": {
|
|
959
|
+
"type": "object",
|
|
960
|
+
"properties": {
|
|
961
|
+
"payload": {
|
|
962
|
+
"type": "object",
|
|
963
|
+
"properties": {
|
|
964
|
+
"sender": {
|
|
965
|
+
"type": "object",
|
|
966
|
+
"additionalProperties": false,
|
|
967
|
+
"required": [
|
|
968
|
+
"channel",
|
|
969
|
+
"id"
|
|
970
|
+
],
|
|
971
|
+
"description": "The authenticated sender the `actor` was resolved from, on the same terms as a decision's.",
|
|
972
|
+
"properties": {
|
|
973
|
+
"channel": {
|
|
974
|
+
"type": "string",
|
|
975
|
+
"minLength": 1
|
|
976
|
+
},
|
|
977
|
+
"id": {
|
|
978
|
+
"type": "string",
|
|
979
|
+
"minLength": 1
|
|
980
|
+
},
|
|
981
|
+
"hashed": {
|
|
982
|
+
"$ref": "#/$defs/senderHashed"
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
"allOf": [
|
|
986
|
+
{
|
|
987
|
+
"$ref": "#/$defs/senderHashedImpliesDigest"
|
|
988
|
+
}
|
|
989
|
+
]
|
|
990
|
+
},
|
|
991
|
+
"sender_source": {
|
|
992
|
+
"type": "string",
|
|
993
|
+
"enum": [
|
|
994
|
+
"policy"
|
|
995
|
+
],
|
|
996
|
+
"description": "How the sender became the actor. `policy` is the attested `approvers[id].senders` mapping of SPEC.md §5.2 — and for an attestation specifically, the mapping of the policy IN FORCE, never the one being attested."
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
831
1003
|
{
|
|
832
1004
|
"$comment": "SPEC.md §5.2/§8 (amended, APRV-118): a request and the grant that answers it each pin the SHA-256 of the attested policy the runtime evaluated them under, so a reader can see whether the approver decided by the rules the requester was routed by. The runtime assigns the value at the write boundary from its own attestation check, exactly as it assigns `ts` to a gate-typed event; a grant whose hash differs from its request's is refused `policy-drift` and never reaches this schema. Optional, and deliberately: the field is additive, so records written before it existed still validate and still verify, and only its SHAPE is constrained here — a 64-character lowercase hex digest, never an empty string or a truncated one that audit would have to guess at. Whether the hash names an attestation the log actually carries is not expressible in a schema that sees one record, and is the gate's check.",
|
|
833
1005
|
"if": {
|
|
@@ -1245,9 +1417,10 @@
|
|
|
1245
1417
|
"reason": {
|
|
1246
1418
|
"type": "string",
|
|
1247
1419
|
"enum": [
|
|
1248
|
-
"act-threw"
|
|
1420
|
+
"act-threw",
|
|
1421
|
+
"workspace-commit-unknown"
|
|
1249
1422
|
],
|
|
1250
|
-
"description": "Where the unknowing began. `act-threw`: the adapter's `act` was entered and raised, so the provider call may or may not have committed. Closed, and extended only by a task that adds the case."
|
|
1423
|
+
"description": "Where the unknowing began. `act-threw`: the adapter's `act` was entered and raised, so the provider call may or may not have committed. `workspace-commit-unknown` (APRV-325.2): a multi-file workspace transaction was applied and the workspace then read back as neither the approved before-state nor the approved after-state, because POSIX offers no atomic multi-file rename and a crash, a permission error or a failed rollback can land between two of them; the transaction journal naming both states is retained for a person. Closed, and extended only by a task that adds the case."
|
|
1251
1424
|
},
|
|
1252
1425
|
"exit_code": {
|
|
1253
1426
|
"type": "null",
|
|
@@ -1390,9 +1563,10 @@
|
|
|
1390
1563
|
"type": "string",
|
|
1391
1564
|
"enum": [
|
|
1392
1565
|
"no-records",
|
|
1393
|
-
"no-evidence"
|
|
1566
|
+
"no-evidence",
|
|
1567
|
+
"no-evidence-merged"
|
|
1394
1568
|
],
|
|
1395
|
-
"description": "Which arm found it: `no-records` is git activity with no attributable hook record at all, `no-evidence` is a guarded path changed with no evidence a human decided it (the spelling APRV-151's CI guard already uses)."
|
|
1569
|
+
"description": "Which arm found it: `no-records` is git activity with no attributable hook record at all, `no-evidence` is a guarded path changed with no evidence a human decided it (the spelling APRV-151's CI guard already uses), and `no-evidence-merged` (APRV-369) is that same finding where every failing commit reached the checkout through a merge rather than being authored on it — the same `dark` verdict, reported under its own code because the subject named it the checkout that SYNCED the change and the repair is in the branch the commit came from. A closed set, and `tests/dark-session.test.ts` pins it equal to the codes a `dark` verdict can carry, so a code the sweep can produce cannot be missing here: a record refused at the write boundary is an observation that never reaches a human."
|
|
1396
1570
|
},
|
|
1397
1571
|
"observation_key": {
|
|
1398
1572
|
"type": "string",
|
|
@@ -1464,7 +1638,6 @@
|
|
|
1464
1638
|
"payload": {
|
|
1465
1639
|
"type": "object",
|
|
1466
1640
|
"required": [
|
|
1467
|
-
"actor",
|
|
1468
1641
|
"decision",
|
|
1469
1642
|
"code"
|
|
1470
1643
|
],
|
|
@@ -1472,7 +1645,45 @@
|
|
|
1472
1645
|
"actor": {
|
|
1473
1646
|
"type": "string",
|
|
1474
1647
|
"pattern": "^human:",
|
|
1475
|
-
"description": "The person whose decision was refused, `human:<id>`. Distinct from the record's own `actor`, which is the runtime: this field is the SUBJECT of the observation, and the record's actor is its author. `^human:` because this event exists only for a human decision; a refusal handed to an agent is not recorded at all."
|
|
1648
|
+
"description": "The person whose decision was refused, `human:<id>`. Distinct from the record's own `actor`, which is the runtime: this field is the SUBJECT of the observation, and the record's actor is its author. `^human:` because this event exists only for a human decision; a refusal handed to an agent is not recorded at all. Required unless the refusal is precisely that the runtime could not say who the person was — see the `sender` field and the rule below it.",
|
|
1649
|
+
"$comment": "APRV-324 moved this out of the payload's `required` list and into the conditional rule below, which requires it on every record that does not carry a `sender`."
|
|
1650
|
+
},
|
|
1651
|
+
"sender": {
|
|
1652
|
+
"type": "object",
|
|
1653
|
+
"additionalProperties": false,
|
|
1654
|
+
"required": [
|
|
1655
|
+
"channel",
|
|
1656
|
+
"id"
|
|
1657
|
+
],
|
|
1658
|
+
"description": "SPEC.md §10.3 (amended, APRV-324): the authenticated sender whose decision was refused. Carried on a `sender-unmapped`, `sender-ambiguous` or `sender-key-unavailable` refusal, where it is the only thing the runtime knows about who tapped and the reason the record exists — an operator investigating who tried needs the account — and on any other refusal of a decision whose actor WAS resolved from a sender, where it says which account spent the attention.",
|
|
1659
|
+
"properties": {
|
|
1660
|
+
"channel": {
|
|
1661
|
+
"type": "string",
|
|
1662
|
+
"minLength": 1,
|
|
1663
|
+
"description": "The surface that observed the sender: `telegram`."
|
|
1664
|
+
},
|
|
1665
|
+
"id": {
|
|
1666
|
+
"type": "string",
|
|
1667
|
+
"minLength": 1,
|
|
1668
|
+
"description": "The transport's own account id, as a string, OR the operator's keyed digest of it when `hashed` is true. Raw, a Telegram user id is not a credential: it is visible to everyone in the chat the tap arrived from. Under a keyed mapping the digest is what the operator can act on, because it is exactly the string their `senders` block would carry."
|
|
1669
|
+
},
|
|
1670
|
+
"hashed": {
|
|
1671
|
+
"$ref": "#/$defs/senderHashed"
|
|
1672
|
+
}
|
|
1673
|
+
},
|
|
1674
|
+
"allOf": [
|
|
1675
|
+
{
|
|
1676
|
+
"$ref": "#/$defs/senderHashedImpliesDigest"
|
|
1677
|
+
}
|
|
1678
|
+
],
|
|
1679
|
+
"examples": [{ "channel": "telegram", "id": "12345678" }]
|
|
1680
|
+
},
|
|
1681
|
+
"sender_source": {
|
|
1682
|
+
"type": "string",
|
|
1683
|
+
"enum": [
|
|
1684
|
+
"policy"
|
|
1685
|
+
],
|
|
1686
|
+
"description": "How the sender became the actor, on a refusal whose actor was resolved. Absent where nothing resolved, which is what a `sender-unmapped` refusal is."
|
|
1476
1687
|
},
|
|
1477
1688
|
"decision": {
|
|
1478
1689
|
"type": "string",
|
|
@@ -1507,6 +1718,276 @@
|
|
|
1507
1718
|
}
|
|
1508
1719
|
}
|
|
1509
1720
|
},
|
|
1721
|
+
{
|
|
1722
|
+
"$comment": "SPEC.md §10.3/§11.1 (amended, APRV-324): a refused decision names the person it was refused for, UNLESS naming one is exactly what the runtime could not do. Before the sender mapping, every decision surface held one configured human identity and the answer was always available; `sender-unmapped` is the case where a transport authenticated an account the attested policy binds to nobody, and the honest record of it carries the account and no person. Writing the listener's own identity there would have been the false statement the refusal exists to prevent — that the operator's decision was refused, when the operator did not decide. So the requirement is conditional rather than dropped: a record with no `sender` MUST still name the human, which is every refusal the runtime wrote before this rule existed and every refusal on a surface that authenticates no sender.",
|
|
1723
|
+
"if": {
|
|
1724
|
+
"type": "object",
|
|
1725
|
+
"properties": {
|
|
1726
|
+
"event": {
|
|
1727
|
+
"const": "audit.decision_refused"
|
|
1728
|
+
},
|
|
1729
|
+
"payload": {
|
|
1730
|
+
"type": "object",
|
|
1731
|
+
"not": {
|
|
1732
|
+
"properties": {
|
|
1733
|
+
"sender": {}
|
|
1734
|
+
},
|
|
1735
|
+
"required": [
|
|
1736
|
+
"sender"
|
|
1737
|
+
]
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
"required": [
|
|
1742
|
+
"event",
|
|
1743
|
+
"payload"
|
|
1744
|
+
]
|
|
1745
|
+
},
|
|
1746
|
+
"then": {
|
|
1747
|
+
"type": "object",
|
|
1748
|
+
"properties": {
|
|
1749
|
+
"payload": {
|
|
1750
|
+
"type": "object",
|
|
1751
|
+
"properties": {
|
|
1752
|
+
"actor": {
|
|
1753
|
+
"type": "string",
|
|
1754
|
+
"pattern": "^human:"
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
"required": [
|
|
1758
|
+
"actor"
|
|
1759
|
+
]
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
"$comment": "SPEC.md §8 (amended, APRV-355): a human made a gesture on a decision surface that is NOT a decision — a checkpoint signature, a review — and the surface refused it before any verb ran, so nothing else was appended. Found while landing APRV-324 (PR #427): `audit.decision_refused` requires an `action_key` and a `payload.decision` of grant, reject or revoke, and a signature or a review has neither, so recording one of these there would have meant manufacturing both. The cost of recording nothing is that attention spent, and an attempt made by an account the operator did not map, leave no trace in the log at all — only a terminal line and a chat answer.\n\nAUDIT TIER, in the strict sense `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. It is the runtime's statement about a refusal the runtime made.\n\nThe actor MUST be `^system:`, for the reason `audit.dark_session`'s and `audit.decision_refused`'s are: a record of a refusal authored by either party to it is a record neither party can be held to. The human, when the runtime could name one, is named in the payload; on the refusal where it could not — a transport authenticated an account the attested policy binds to nobody — the payload carries the observed `sender` and no `actor`, on the same conditional terms APRV-324 gave the decision record.\n\nThere is no `action_key` and no `decision`, deliberately. A signature answers for a chain head and a review answers for a sampled record; neither is an action the gate holds a key for, and a field invented to satisfy a schema would be a false statement in a record whose whole purpose is to be true about an attempt.",
|
|
1766
|
+
"if": {
|
|
1767
|
+
"type": "object",
|
|
1768
|
+
"properties": {
|
|
1769
|
+
"event": {
|
|
1770
|
+
"const": "audit.gesture_refused"
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
"required": [
|
|
1774
|
+
"event"
|
|
1775
|
+
]
|
|
1776
|
+
},
|
|
1777
|
+
"then": {
|
|
1778
|
+
"type": "object",
|
|
1779
|
+
"required": [
|
|
1780
|
+
"actor",
|
|
1781
|
+
"channel",
|
|
1782
|
+
"payload"
|
|
1783
|
+
],
|
|
1784
|
+
"properties": {
|
|
1785
|
+
"actor": {
|
|
1786
|
+
"type": "string",
|
|
1787
|
+
"pattern": "^system:",
|
|
1788
|
+
"description": "The runtime. A `human:` or `agent:` actor is refused: the record states that a surface refused a gesture, and the parties to that refusal do not author it."
|
|
1789
|
+
},
|
|
1790
|
+
"channel": {
|
|
1791
|
+
"$ref": "#/properties/channel",
|
|
1792
|
+
"description": "Which surface collected the gesture — `telegram`, `web`, `cli`. Required here for the reason it is required on `audit.decision_refused`: a refusal a reader cannot attribute to a surface is one they cannot go and reproduce, and every decision surface knows its own name."
|
|
1793
|
+
},
|
|
1794
|
+
"payload": {
|
|
1795
|
+
"type": "object",
|
|
1796
|
+
"required": [
|
|
1797
|
+
"gesture",
|
|
1798
|
+
"code"
|
|
1799
|
+
],
|
|
1800
|
+
"properties": {
|
|
1801
|
+
"gesture": {
|
|
1802
|
+
"enum": [
|
|
1803
|
+
"checkpoint-signature",
|
|
1804
|
+
"review",
|
|
1805
|
+
"review-note"
|
|
1806
|
+
],
|
|
1807
|
+
"description": "What was attempted. `checkpoint-signature` is a signature over a chain head (§9, APRV-257); `review` is a retrospective verdict on a sampled record (§10.1, APRV-299); `review-note` is that same gesture carrying a human's own words, distinguished because a note is attention spent writing rather than attention spent tapping, and an operator reading this record to decide whether to map an account wants to know which. CLOSED, and additive-only like every other vocabulary here (§11.1 invariant 6): a kind the write boundary refuses is an observation that never reaches a human."
|
|
1808
|
+
},
|
|
1809
|
+
"code": {
|
|
1810
|
+
"enum": [
|
|
1811
|
+
"sender-unmapped",
|
|
1812
|
+
"sender-ambiguous",
|
|
1813
|
+
"sender-key-unavailable",
|
|
1814
|
+
"policy-not-attested"
|
|
1815
|
+
],
|
|
1816
|
+
"description": "Why the surface refused, verbatim from the resolution that refused. CLOSED, and exactly the refusals sender resolution can reach before a verb runs: the first three are `CHANNEL_DECISION_REFUSAL_CODES` (`core/sender-identity.ts`), and the last is `ATTESTATION_REFUSAL` (`core/attest.ts`), which fires when the policy declaring the mapping is not the attested one and is therefore not in force. `sender-key-unavailable` (APRV-370) is the policy mapping this channel's senders in the keyed form while the process holds no key: the runtime could resolve NO account, which is a statement about the listener rather than about the person who tapped. `attest-requires-terminal` is not here: it belongs to an attestation tap, which is a decision."
|
|
1817
|
+
},
|
|
1818
|
+
"actor": {
|
|
1819
|
+
"type": "string",
|
|
1820
|
+
"pattern": "^human:",
|
|
1821
|
+
"description": "The person whose gesture was refused, `human:<id>`, when the runtime could name one. Distinct from the record's own `actor`, which is the runtime: this field is the SUBJECT of the observation and the record's actor is its author. Required unless the refusal is precisely that the runtime could not say who the person was — see `sender` and the rule below it."
|
|
1822
|
+
},
|
|
1823
|
+
"sender": {
|
|
1824
|
+
"type": "object",
|
|
1825
|
+
"additionalProperties": false,
|
|
1826
|
+
"required": [
|
|
1827
|
+
"channel",
|
|
1828
|
+
"id"
|
|
1829
|
+
],
|
|
1830
|
+
"description": "SPEC.md §10.3: the authenticated sender the gesture arrived from. The transport's own attribution and nothing the message claimed about itself (§11.1 invariant 4). On `sender-unmapped` it is the whole of what is known about who tried, and the reason the record exists: an operator deciding whether to map an account needs the account.",
|
|
1831
|
+
"properties": {
|
|
1832
|
+
"channel": {
|
|
1833
|
+
"type": "string",
|
|
1834
|
+
"minLength": 1,
|
|
1835
|
+
"description": "The surface that observed it. `telegram` at v0.1."
|
|
1836
|
+
},
|
|
1837
|
+
"id": {
|
|
1838
|
+
"type": "string",
|
|
1839
|
+
"minLength": 1,
|
|
1840
|
+
"description": "The transport's account id, as a string, OR the operator's keyed digest of it when `hashed` is true."
|
|
1841
|
+
},
|
|
1842
|
+
"hashed": {
|
|
1843
|
+
"$ref": "#/$defs/senderHashed"
|
|
1844
|
+
}
|
|
1845
|
+
},
|
|
1846
|
+
"allOf": [
|
|
1847
|
+
{
|
|
1848
|
+
"$ref": "#/$defs/senderHashedImpliesDigest"
|
|
1849
|
+
}
|
|
1850
|
+
]
|
|
1851
|
+
},
|
|
1852
|
+
"sender_source": {
|
|
1853
|
+
"enum": [
|
|
1854
|
+
"policy"
|
|
1855
|
+
],
|
|
1856
|
+
"description": "How the sender became the actor, when one did. `policy` at v0.1: the attested policy's `senders` mapping."
|
|
1857
|
+
},
|
|
1858
|
+
"message": {
|
|
1859
|
+
"type": "string",
|
|
1860
|
+
"description": "The refusal message, as the person was shown it. A convenience for a reader; `code` is what anything machine-readable branches on."
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
"$comment": "SPEC.md §10.3/§11.1 (APRV-355), the same conditional APRV-324 gave `audit.decision_refused`: a refused gesture names the person it was refused for, UNLESS naming one is exactly what the runtime could not do. A record with no `sender` MUST still name the human, which is every refusal on a surface that authenticates no sender.",
|
|
1869
|
+
"if": {
|
|
1870
|
+
"type": "object",
|
|
1871
|
+
"properties": {
|
|
1872
|
+
"event": {
|
|
1873
|
+
"const": "audit.gesture_refused"
|
|
1874
|
+
},
|
|
1875
|
+
"payload": {
|
|
1876
|
+
"type": "object",
|
|
1877
|
+
"not": {
|
|
1878
|
+
"properties": {
|
|
1879
|
+
"sender": {}
|
|
1880
|
+
},
|
|
1881
|
+
"required": [
|
|
1882
|
+
"sender"
|
|
1883
|
+
]
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
"required": [
|
|
1888
|
+
"event",
|
|
1889
|
+
"payload"
|
|
1890
|
+
]
|
|
1891
|
+
},
|
|
1892
|
+
"then": {
|
|
1893
|
+
"type": "object",
|
|
1894
|
+
"properties": {
|
|
1895
|
+
"payload": {
|
|
1896
|
+
"type": "object",
|
|
1897
|
+
"properties": {
|
|
1898
|
+
"actor": {
|
|
1899
|
+
"type": "string",
|
|
1900
|
+
"pattern": "^human:"
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1903
|
+
"required": [
|
|
1904
|
+
"actor"
|
|
1905
|
+
]
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
"$comment": "SPEC.md §8 (amended, APRV-378): something other than this gate answered a question this gate exists to ask. The first instance is Codex's server-side auto-reviewer (`docs/codex-app-server-bridge.md`, question 3): a model call can resolve an approval before `approval codex bridge` is asked, and the client is told afterwards through an `item/autoApprovalReview` notification. No existing type carries the fact. `audit.dark_session` is the sweep over activity with no records beside it; `audit.decision_refused` is a human gesture the gate would not take; `audit.gesture_refused` (APRV-355) is a gesture that is not a decision at all. This one is none of those: the question existed, the gate would have asked it, and somebody else answered first.\n\nThe NAME is general because the instance will not be the last. `source` is a closed enum whose first member names the Codex auto-reviewer, and a second party gains a member rather than a type.\n\nAUDIT TIER, on the strict terms `audit.decision_refused` and `audit.gesture_refused` set: it grants nothing, mints no token, settles no request, charges no budget, is not sampled, and NO ENFORCEMENT PATH READS IT. `approval doctor` reads it, which is a diagnosis rather than an enforcement: the row it feeds authorizes nothing and refuses nothing.\n\nThe actor MUST be `^system:`, for the reason `audit.dark_session`'s and `audit.gesture_refused`'s are: a record of an event authored by either party to it is a record neither party can be held to. Here both parties are machines, and neither the harness that pre-empted the question nor the agent whose action it was may author the note that says so.\n\n`verdict` is OPTIONAL and carries the other party's word verbatim when the notification stated one. It is not defaulted: a record that said `accept` because nothing said otherwise would be this runtime inventing another party's decision, which is the one thing a record about another party's decision may not do. Its absence is a fact about the notification.",
|
|
1912
|
+
"if": {
|
|
1913
|
+
"type": "object",
|
|
1914
|
+
"properties": {
|
|
1915
|
+
"event": {
|
|
1916
|
+
"const": "audit.question_preempted"
|
|
1917
|
+
}
|
|
1918
|
+
},
|
|
1919
|
+
"required": [
|
|
1920
|
+
"event"
|
|
1921
|
+
]
|
|
1922
|
+
},
|
|
1923
|
+
"then": {
|
|
1924
|
+
"type": "object",
|
|
1925
|
+
"required": [
|
|
1926
|
+
"actor",
|
|
1927
|
+
"payload"
|
|
1928
|
+
],
|
|
1929
|
+
"properties": {
|
|
1930
|
+
"actor": {
|
|
1931
|
+
"type": "string",
|
|
1932
|
+
"pattern": "^system:",
|
|
1933
|
+
"description": "The runtime. A `human:` or `agent:` actor is refused: the record states that something else answered a question, and neither party to that may author the statement."
|
|
1934
|
+
},
|
|
1935
|
+
"payload": {
|
|
1936
|
+
"type": "object",
|
|
1937
|
+
"required": [
|
|
1938
|
+
"source",
|
|
1939
|
+
"question"
|
|
1940
|
+
],
|
|
1941
|
+
"properties": {
|
|
1942
|
+
"source": {
|
|
1943
|
+
"enum": [
|
|
1944
|
+
"codex-auto-reviewer"
|
|
1945
|
+
],
|
|
1946
|
+
"description": "Who answered instead of the gate. CLOSED, and additive-only like every other vocabulary here (§11.1 invariant 6). `codex-auto-reviewer` is Codex's server-side guardian reviewer, whose answer is a genuine model call (`codex-rs/core/src/guardian/decision.rs`) and which runs before the client path. A second party gains a member here rather than an event type of its own."
|
|
1947
|
+
},
|
|
1948
|
+
"question": {
|
|
1949
|
+
"type": "object",
|
|
1950
|
+
"required": [
|
|
1951
|
+
"id"
|
|
1952
|
+
],
|
|
1953
|
+
"description": "The question that was answered, as the OTHER PARTY identified it. Its own identifiers and nothing this runtime minted: a record about somebody else's decision names it in their terms, or a reader cannot go and find it on their side.",
|
|
1954
|
+
"properties": {
|
|
1955
|
+
"id": {
|
|
1956
|
+
"type": "string",
|
|
1957
|
+
"minLength": 1,
|
|
1958
|
+
"description": "The other party's identifier for the question. Codex's `itemId` on the item-based API."
|
|
1959
|
+
},
|
|
1960
|
+
"method": {
|
|
1961
|
+
"type": "string",
|
|
1962
|
+
"minLength": 1,
|
|
1963
|
+
"description": "The notification that disclosed it, verbatim: `item/autoApprovalReview/started` or `item/autoApprovalReview/completed` at v0.1."
|
|
1964
|
+
},
|
|
1965
|
+
"thread": {
|
|
1966
|
+
"type": "string",
|
|
1967
|
+
"minLength": 1,
|
|
1968
|
+
"description": "The other party's session identifier, where the notification named one. Codex's `threadId`."
|
|
1969
|
+
},
|
|
1970
|
+
"turn": {
|
|
1971
|
+
"type": "string",
|
|
1972
|
+
"minLength": 1,
|
|
1973
|
+
"description": "The other party's turn identifier, where the notification named one. Codex's `turnId`."
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
"verdict": {
|
|
1978
|
+
"type": "string",
|
|
1979
|
+
"minLength": 1,
|
|
1980
|
+
"description": "The verdict the other party reached, verbatim in their own vocabulary and never translated into this project's. Absent when the notification stated none, which is a fact about the notification rather than a default."
|
|
1981
|
+
},
|
|
1982
|
+
"detail": {
|
|
1983
|
+
"type": "string",
|
|
1984
|
+
"description": "The runtime's own one line about what it did next. A convenience for a reader; nothing branches on it."
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
},
|
|
1510
1991
|
{
|
|
1511
1992
|
"$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.",
|
|
1512
1993
|
"if": {
|
|
@@ -1806,6 +2287,54 @@
|
|
|
1806
2287
|
}
|
|
1807
2288
|
}
|
|
1808
2289
|
}
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
"$comment": "SPEC.md §5.2/§8/§10.1 (amended, APRV-338): a human's sign-off on the exact bytes of one PROTECTED PATH whose edits classify `policy.edit` or a `policy.edit.*` sub-class — a specification, an agent instruction file, CI configuration, a document a policy's `protected_paths` widened the surface with. The sibling of `gate.organ.attested` and deliberately a different type, because the two answer about different surfaces under different rules: an organ has no grant available to it at all, while a protected path does, and the after-the-fact checker of §10.1 prefers the grant's hunk evidence and reads this record only when no grant covers the change. Whole-file evidence, and weaker than a hunk grant for exactly that reason: it says a human read this file at these bytes and stands behind them, not that they saw a particular line change. The actor MUST be `^human:`, in the schema as well as in `core/attest.ts`: a record an agent could write would let the party under oversight ratify its own text. Both payload fields are computed by the runtime — the digest from the file's bytes, the path normalized — and the caller supplies neither. Gate-typed (`gate.` prefix), so `ts` is stamped at the write boundary.",
|
|
2293
|
+
"if": {
|
|
2294
|
+
"type": "object",
|
|
2295
|
+
"properties": {
|
|
2296
|
+
"event": {
|
|
2297
|
+
"const": "gate.path.signed_off"
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
"required": [
|
|
2301
|
+
"event"
|
|
2302
|
+
]
|
|
2303
|
+
},
|
|
2304
|
+
"then": {
|
|
2305
|
+
"type": "object",
|
|
2306
|
+
"required": [
|
|
2307
|
+
"actor",
|
|
2308
|
+
"payload"
|
|
2309
|
+
],
|
|
2310
|
+
"properties": {
|
|
2311
|
+
"actor": {
|
|
2312
|
+
"type": "string",
|
|
2313
|
+
"pattern": "^human:"
|
|
2314
|
+
},
|
|
2315
|
+
"payload": {
|
|
2316
|
+
"type": "object",
|
|
2317
|
+
"required": [
|
|
2318
|
+
"path",
|
|
2319
|
+
"sha256"
|
|
2320
|
+
],
|
|
2321
|
+
"properties": {
|
|
2322
|
+
"path": {
|
|
2323
|
+
"type": "string",
|
|
2324
|
+
"minLength": 1,
|
|
2325
|
+
"maxLength": 512,
|
|
2326
|
+
"pattern": "^[^/\\\\][^\\\\]*$",
|
|
2327
|
+
"description": "The signed-off file's REPOSITORY-RELATIVE, `/`-separated path, for example `SPEC.md`. Relative and never absolute, for the reason `organ_path` is: a permanent record must not leak the writer's home directory and must name the same file on the machine that later reads it. That the path actually classifies `policy.edit` or a `policy.edit.*` sub-class, and is neither the policy file nor a `policy.core` surface, is the runtime's check (`core/attest.ts`), as is the refusal of `..`; a schema sees one record and constrains the shape."
|
|
2328
|
+
},
|
|
2329
|
+
"sha256": {
|
|
2330
|
+
"type": "string",
|
|
2331
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
2332
|
+
"description": "SHA-256 (lowercase hex) of the file's exact bytes at the moment of sign-off, computed by the runtime. Bytes, not text, so a file differing by trailing whitespace is a different file and a record about it is not a record about this one. The caller has no parameter for this value — one who could supply it could sign off on bytes nobody read."
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
1809
2338
|
}
|
|
1810
2339
|
]
|
|
1811
2340
|
}
|