diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c166329..d0d9359 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -23,3 +23,17 @@ jobs: - run: python scripts/generate_examples.py - run: git diff --exit-code -- examples - run: python -m unittest discover -s tests -v + + action: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./ + with: + envelopes: | + examples/approval-envelope.json + records: | + approval-record examples/webauthn-approval-record.json + resolution-record examples/resolution-record.json + audit-events: examples/audit-events.json + audit-snapshot: examples/audit-snapshot.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ccca9..e4f393b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 3.1.0 + +Approval paths that fail closed, approvals that can be withdrawn until dispatch, and +records you can verify in your own pipeline. + +### Added + +- `ApprovalPolicySnapshot.outcome`: a closed `require_approval`, `auto_approve`, or + `deny` result. Auto approval requires an explicit `auto_approve` outcome under a + policy that allows it; a `deny` outcome ends the request as `policy_failed` and + never reaches a provider. +- Revocation of an approved request before consumption: `revoked` status, + `approval_revoked` terminal and dispatch reasons, a `revocation` record with actor, + time, and reason, an `approval.revoked` audit event, and a complete + [revoked lifecycle](examples/revoked-envelope.json). +- `ResolutionRecord.agent_feedback` for refusals: reason code, redacted message, + requested payload changes, and questions, returned with + `revise_and_resubmit` and `new_proposal`. +- An `mcp_elicitation` approval channel with a [profile](docs/mcp-elicitation.md) for + MCP servers that collect decisions through form-mode elicitation. Approved and + rejected elicitation decisions name a present, authenticated human. +- Strict reference payload schemas for `crm.update_record`, `ticket.create`, + `db.update_row`, `api.call`, and `n8n.trigger_workflow`, with example payloads. +- `x-review-target` on action schemas: the review snapshot target must equal those + members of the reviewed arguments for every action type. +- Validator inputs for your own files: `--envelope`, `--record KIND FILE`, + `--schema-file`, `--audit-events` with `--audit-snapshot` and `--anchor`, and + `--json`, with exit status 0, 1, or 2. +- Set checks across envelopes: unique proposal, request, dispatch, and audit-stream + identities, one approval request per call, and one action per idempotency key. +- Offline verification of exported audit ranges, including partial exports anchored + to a retained checkpoint hash. +- A GitHub Action (`action.yml`) that runs the validator in any repository. +- Portable conformance vectors in `tests/release-3.1.json`. + +### Compatibility + +Every addition is optional on existing records. Schema IDs and `schema_version` move +to `3.1.0`; continue validating stored v3.0 records with the v3.0.0 schemas and do +not relabel them. Records produced under 3.1 that use `email.send` must cite the +v3.1.0 payload schema ID and digest. + ## 3.0.0 This release enforces the approval and execution boundaries across complete success, diff --git a/README.md b/README.md index d9628d1..5afa727 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Nine boundaries make the gate real: If the agent still has the side-effect credential, the approval path remains optional from the agent's point of view. -## Contracts in v3.0.0 +## Contracts in v3.1.0 | Contract | Purpose | |---|---| @@ -69,9 +69,9 @@ from the agent's point of view. | [`audit-event.schema.json`](schemas/audit-event.schema.json) | Redacted lifecycle evidence with actor, event time, ingestion time, and hash chain | | [`audit-snapshot.schema.json`](schemas/audit-snapshot.schema.json) | Completeness manifest for one exported audit-event set | | [`approval-envelope.schema.json`](schemas/approval-envelope.schema.json) | Portable bundle plus cross-contract profile for a completed lifecycle | -| [`actions/email.send.schema.json`](schemas/actions/email.send.schema.json) | Strict reference for composing action-specific payload contracts | +| [`actions/*.schema.json`](schemas/actions/) | Strict payload references for `email.send`, `crm.update_record`, `ticket.create`, `db.update_row`, `api.call`, and `n8n.trigger_workflow`, each naming the fields the approver must see as the target | -Schema IDs are pinned to the `v3.0.0` release. Pin a released ID in production; do not +Schema IDs are pinned to the `v3.1.0` release. Pin a released ID in production; do not resolve schemas from the mutable default branch. ## Quick start @@ -108,12 +108,43 @@ Recovery retains the unknown attempt, a linked reconciliation record, and a boun retry with fresh authority. The portable vectors in [`tests/hardening.json`](tests/hardening.json) cover these boundaries. +Version 3.1 adds a fail-closed policy outcome (`require_approval`, `auto_approve`, or +`deny`; anything else is recorded as `deny`), [revocation](examples/revoked-envelope.json) +of an approved request until the moment it is consumed, structured `agent_feedback` on +refusals so the agent can revise instead of re-asking, an `mcp_elicitation` channel +([profile](docs/mcp-elicitation.md)), and review-target binding for every action type. + [`tests/canonicalization.json`](tests/canonicalization.json) is a language-neutral RFC 8785 corpus with exact canonical UTF-8 bytes and SHA-256 results. Use it to prove that implementations in different languages bind the same document to the same hash. [`tests/adversarial.json`](tests/adversarial.json) exercises deceptive identities, display controls, metadata injection, redaction boundaries, and review tampering. +## Validate your own records + +The reference validator checks your envelopes, standalone records, and audit exports +with the same rules it applies to this repository: + +```bash +python3 scripts/validate_contracts.py --envelope out/envelope.json +python3 scripts/validate_contracts.py --envelope a.json --envelope b.json +python3 scripts/validate_contracts.py --record approval-record out/approval.json --json +python3 scripts/validate_contracts.py --audit-events out/audit.jsonl --audit-snapshot out/snapshot.json +``` + +Several envelopes are also checked as a set: one approval request per call, and one +action per idempotency key. Audit exports verify offline, including a range that +continues from a retained checkpoint hash. In CI, use the bundled action: + +```yaml +- uses: renezander030/agent-approval-gate@v3.1.0 + with: + envelopes: out/envelope.json +``` + +See [`docs/contracts.md`](docs/contracts.md#validating-your-own-records) for every option +and exit status. + ## Optional passkey proof Authenticated web approvals may carry a `webauthn-es256` signature. Its challenge is diff --git a/VERSION b/VERSION index 4a36342..fd2a018 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 +3.1.0 diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..920c39a --- /dev/null +++ b/action.yml @@ -0,0 +1,60 @@ +name: Agent approval gate conformance +description: Validate approval envelopes, standalone records, and audit exports against the agent-approval-gate contracts. +branding: + icon: check-circle + color: green +inputs: + envelopes: + description: Newline-separated paths to complete approval envelopes. Two or more are also checked for shared identities. + required: false + default: "" + records: + description: Newline-separated "KIND PATH" pairs, for example "approval-record out/approval.json". + required: false + default: "" + schema-files: + description: Newline-separated paths to trusted local payload schemas for custom action types. + required: false + default: "" + audit-events: + description: Exported audit events (.json array or .jsonl). + required: false + default: "" + audit-snapshot: + description: AuditSnapshot manifest for the exported audit events. + required: false + default: "" + anchor: + description: Retained hash of the event preceding a partial audit export. + required: false + default: "" +runs: + using: composite + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - shell: bash + run: python -m pip install --quiet --requirement "$GITHUB_ACTION_PATH/requirements-dev.txt" + - shell: bash + env: + INPUT_ENVELOPES: ${{ inputs.envelopes }} + INPUT_RECORDS: ${{ inputs.records }} + INPUT_SCHEMA_FILES: ${{ inputs.schema-files }} + INPUT_AUDIT_EVENTS: ${{ inputs.audit-events }} + INPUT_AUDIT_SNAPSHOT: ${{ inputs.audit-snapshot }} + INPUT_ANCHOR: ${{ inputs.anchor }} + run: | + set -euo pipefail + args=() + while IFS= read -r line; do [ -n "$line" ] && args+=(--envelope "$line"); done <<< "$INPUT_ENVELOPES" + while IFS= read -r line; do [ -n "$line" ] && args+=(--schema-file "$line"); done <<< "$INPUT_SCHEMA_FILES" + while read -r kind path; do [ -n "${kind:-}" ] && args+=(--record "$kind" "$path"); done <<< "$INPUT_RECORDS" + [ -n "$INPUT_AUDIT_EVENTS" ] && args+=(--audit-events "$INPUT_AUDIT_EVENTS") + [ -n "$INPUT_AUDIT_SNAPSHOT" ] && args+=(--audit-snapshot "$INPUT_AUDIT_SNAPSHOT") + [ -n "$INPUT_ANCHOR" ] && args+=(--anchor "$INPUT_ANCHOR") + if [ ${#args[@]} -eq 0 ]; then + echo "::error::Give at least one of envelopes, records, or audit-events with audit-snapshot." + exit 2 + fi + python "$GITHUB_ACTION_PATH/scripts/validate_contracts.py" "${args[@]}" diff --git a/docs/architecture.md b/docs/architecture.md index 0ad5cfc..c8c91b0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -238,6 +238,7 @@ path without a fabricated approval or invocation. See the version 3 boundary rul | Telegram | Medium | Very low | Sign high-risk records and bind callback nonce | | Email | Low | High | Sign high-risk records; resist forwarding and replay | | n8n form | Medium | Low | Keep the request identity and callback validation outside the form fields | +| MCP elicitation | Medium | Very low | Form-mode clients only; a client auto-decline is not a human rejection ([profile](mcp-elicitation.md)) | | Policy | Machine | Very low | Allowed only when the immutable policy snapshot permits auto approval | A UI control that an automation-capable agent can click is not, by itself, a human diff --git a/docs/contracts.md b/docs/contracts.md index 831ac05..957ec4b 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -2,8 +2,8 @@ ## Versioning -The release version and every schema's `schema_version` are `3.0.0`. Schema `$id` -values resolve through the immutable `v3.0.0` tag. A consumer may cache schemas by +The release version and every schema's `schema_version` are `3.1.0`. Schema `$id` +values resolve through the immutable `v3.1.0` tag. A consumer may cache schemas by `$id`; it must not replace a cached release schema with content from `main`. - Patch releases may clarify descriptions and add compatible examples or tests. @@ -138,6 +138,60 @@ with the record. Decision actor and time agree with the approval; witness times with the facts they record. Capturing the envelope cannot precede its contained evidence. Rehashing a false event never makes it a valid witness. +## Version 3.1 additions + +### Policy outcome + +`ApprovalPolicySnapshot.outcome` records the evaluator's decision as exactly one of +`require_approval`, `auto_approve`, or `deny`. Map every other evaluator result, +including a non-boolean, missing, unknown, or errored result, to `deny` before writing +the snapshot. Never treat "no opinion" as permission. + +- `auto_approve` requires `allow_auto_approval: true`, and every `auto_approved` + decision requires an explicit `auto_approve` outcome. +- `deny` ends the request as `failed` with `terminal_reason: "policy_failed"`. It cannot + carry an approving decision or reach provider invocation. + +### Revocation before consumption + +An approved request may become `revoked` until it is consumed. The transition is a +compare-and-set from `approved` to `revoked`; it must fail once consumption won. +The request names the withdrawn decision in `decision_record_id` and records +`revocation.revoked_at`, `revoked_by`, and a `reason_code`. Its revision includes both +the decision and the revocation transition. Revocation must precede request expiry, an +`approval.revoked` audit event must witness the same actor and time, and a revoked +request cannot invoke a provider. A dispatcher that observes the revocation records +`not_dispatched` with `reason: "approval_revoked"`. The approval record and its +resolution receipt stay unchanged; a new action needs a new proposal. + +### Agent feedback + +A refusal resolution (`rejected`, `expired`, or `cancelled`) may carry +`agent_feedback` with a closed `reason_code`, a redacted `message`, optional +`requested_changes` (JSON Pointers inside `/payload` with a note), and optional +`questions`. Requested changes or questions require +`disposition: "revise_and_resubmit"` and `retry_policy: "new_proposal"`. Feedback +informs the next proposal; it never reopens the decision or grants execution. When +the `resolution.emitted` event carries a `reason_code`, it must equal the feedback +reason. + +### Reference action catalog and review targets + +Every built-in `action_type` has a strict reference payload schema in +`schemas/actions/`. Each schema names its action in `x-action-type` and the payload +members the approver must see as the target in `x-review-target`. The validator +requires `ReviewSnapshot.content.target` to equal exactly those members of the +reviewed arguments, for built-in and trusted custom schemas alike. Deployments copy a +schema next to their dispatcher, adapt it, publish it under an immutable ID, and pass +it to the validator as a trusted local file. + +### Cross-envelope identities + +One envelope proves one lifecycle. A deployment's set of envelopes must also keep +proposal, request, dispatch, and audit-stream identities unique, admit at most one +approval request per tenant and `call_id`, and bind each tenant `idempotency_key` to a +single original action hash. Pass several envelopes to the validator to check the set. + ## WebAuthn approval proof `webauthn-es256` is an optional signature profile for authenticated web approvals. @@ -249,6 +303,43 @@ Hash linkage proves ordering only for the events presented. Pair an export with event-array digest, and terminal event hash. Retain a checkpoint outside the mutable event store when rewrite detection must survive compromise of that store. +## Validating your own records + +The reference validator checks this repository's corpus when run without arguments, +and your records when given files: + +```bash +python scripts/validate_contracts.py --envelope out/envelope.json +python scripts/validate_contracts.py --envelope a.json --envelope b.json # also checks the set +python scripts/validate_contracts.py --record approval-record out/approval.json +python scripts/validate_contracts.py --envelope out/envelope.json \ + --schema-file schemas/crm.update_record.schema.json # trusted custom schema +python scripts/validate_contracts.py --audit-events export.jsonl \ + --audit-snapshot snapshot.json [--anchor sha256:...] # offline audit export +``` + +Inputs are parsed with duplicate-property and non-finite-number refusal. `--json` +prints one result object. Exit status is `0` when every input passed, `1` when a +contract refused an input, and `2` when an input could not be read or parsed. + +An audit export may be a contiguous range. A range that starts after sequence 1 must +name the preceding event hash, and `--anchor` checks it against the hash you retained +outside the event store. The snapshot's count, bounds, digest, and terminal hash must +match the exported range exactly. + +The repository's `action.yml` runs the same checks in GitHub Actions: + +```yaml +- uses: renezander030/agent-approval-gate@v3.1.0 + with: + envelopes: | + out/envelope.json + records: | + approval-record out/approval.json + audit-events: out/audit.jsonl + audit-snapshot: out/audit-snapshot.json +``` + ## Conformance corpus `tests/conformance.json` contains positive examples and lifecycle mutations for @@ -262,3 +353,6 @@ arguments, forged review hashes, invalid WebAuthn binding, origin drift, counter rollback, and signature tampering. `tests/canonicalization.json` is the language-neutral byte-and-digest corpus. Together the files can drive validators in any implementation language. + +`tests/release-3.1.json` adds portable vectors for policy outcomes, revocation, agent +feedback, the MCP elicitation channel, and the reference action schemas. diff --git a/docs/mcp-elicitation.md b/docs/mcp-elicitation.md new file mode 100644 index 0000000..caf1283 --- /dev/null +++ b/docs/mcp-elicitation.md @@ -0,0 +1,60 @@ +# MCP elicitation profile + +An MCP server that gates side effects can ask the person at the MCP client to decide +through `elicitation/create`. The `mcp_elicitation` channel records that decision in the +same `ApprovalRecord` as any other channel. The elicitation is a notification and answer +surface; the approval request, review snapshot, policy, and dispatcher stay on the +server side of the trust boundary. + +## When the channel is available + +Offer the channel only when the client declared form-mode elicitation in its +`initialize` capabilities. A URL-only declaration does not count. If the client cannot +elicit, do not notify through this channel: record the channel entry with +`delivery_status: "failed"` and use another allowed channel, or let the request end as +`failed` with `terminal_reason: "delivery_failed"`. + +Never expose approve or reject as a tool the model can call with a decision argument. +The model may ask the server to collect a decision; only the elicitation answer from the +client decides. + +## Request + +1. Persist the `ApprovalRequest` and its `ReviewSnapshot` before sending the + elicitation, exactly as for any other channel. +2. Render the snapshot `content` (action type, target, complete arguments, risk, expiry, + and policy summary) into the elicitation `message`. Include the snapshot + `content_hash` so the answer can be bound to what was shown. +3. Request a single boolean field in `requestedSchema`, for example + `{"type": "object", "properties": {"approve": {"type": "boolean"}}, "required": ["approve"]}`. + Do not ask the person to type the decision as free text. +4. Add a `channels` entry with `channel: "mcp_elicitation"`, the hash of the client + session identity as `target_hash`, and `delivery_status: "delivered"` once the client + accepted the request for display. + +## Answer mapping + +| Elicitation result | Approval record | +|---|---| +| `accept` with `approve: true` | `decision: "approved"`, human actor, `human_present: true` | +| `accept` with `approve: false` | `decision: "rejected"`, human actor, `human_present: true` | +| `decline` from the person | `decision: "rejected"`, human actor, `human_present: true` | +| `cancel` | no decision; the request stays `pending` until another answer or expiry | +| client declined without showing the form | no decision; mark the channel `failed` | + +A client that answers `decline` without presenting the form to a person has not +rejected anything. Recording it as a human rejection would let a client configuration +decide on the person's behalf. The schema therefore requires an approved or rejected +`mcp_elicitation` record to name a human actor who was present and authenticated through +`authenticated_session`, `channel_identity`, or `signed_callback`. + +## Validation + +The reference validator refuses an `mcp_elicitation` decision when the request has no +delivered `mcp_elicitation` channel entry, and the envelope schema requires a signature +on an approved high-risk action decided through this channel, as it does for email and +Telegram. Prefer the `web` channel with the WebAuthn profile for high-risk actions that +need independently attributable proof. + +The answer must still pass every ordinary check: the request revision, nonce, action +hash, review content hash, policy evaluation ID, quorum, and separation of duties. diff --git a/examples/actions/api.call.json b/examples/actions/api.call.json new file mode 100644 index 0000000..e601602 --- /dev/null +++ b/examples/actions/api.call.json @@ -0,0 +1,6 @@ +{ + "service": "stripe", + "operation": "refunds.create", + "method": "POST", + "body": {"charge": "ch_3P0example", "amount": 4900, "reason": "requested_by_customer"} +} diff --git a/examples/actions/crm.update_record.json b/examples/actions/crm.update_record.json new file mode 100644 index 0000000..9dc0b31 --- /dev/null +++ b/examples/actions/crm.update_record.json @@ -0,0 +1,10 @@ +{ + "system": "hubspot", + "object_type": "Deal", + "record_id": "deal-48213", + "expected_version": "2026-04-28T09:02:11Z", + "changes": [ + {"field": "dealstage", "from": "proposal_sent", "to": "contract_review"}, + {"field": "next_step", "to": "Send the signed order form"} + ] +} diff --git a/examples/actions/db.update_row.json b/examples/actions/db.update_row.json new file mode 100644 index 0000000..5ee1343 --- /dev/null +++ b/examples/actions/db.update_row.json @@ -0,0 +1,7 @@ +{ + "database": "billing", + "table": "public.invoices", + "key": {"invoice_id": "INV-2026-04-1182"}, + "expected_version": 7, + "set": {"status": "void", "void_reason": "duplicate"} +} diff --git a/examples/actions/n8n.trigger_workflow.json b/examples/actions/n8n.trigger_workflow.json new file mode 100644 index 0000000..f2782a5 --- /dev/null +++ b/examples/actions/n8n.trigger_workflow.json @@ -0,0 +1,5 @@ +{ + "workflow_id": "wf-onboarding-01", + "workflow_version": "12", + "input": {"customer_id": "cus-2041", "plan": "team"} +} diff --git a/examples/actions/ticket.create.json b/examples/actions/ticket.create.json new file mode 100644 index 0000000..98da9ac --- /dev/null +++ b/examples/actions/ticket.create.json @@ -0,0 +1,9 @@ +{ + "system": "jira", + "project": "SUP", + "title": "Invoice export fails for EU tenants", + "description": "Customer reports the April invoice export returns an empty file for EU tenants.", + "priority": "high", + "assignee": "billing-oncall", + "labels": ["billing", "customer-reported"] +} diff --git a/examples/approval-envelope.json b/examples/approval-envelope.json index a5c16be..a48c405 100644 --- a/examples/approval-envelope.json +++ b/examples/approval-envelope.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "captured_at": "2026-04-28T09:14:35Z", "proposal": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "agent": { @@ -27,9 +27,9 @@ ], "action_type": "email.send", "payload_schema": { - "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", - "version": "3.0.0", - "digest": "sha256:14730714de221b0506c8ae13b2f783f0eeb52202ea333385bf0d26e840f9d1d4" + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" }, "payload": { "from": "support@acme-corp.example", @@ -51,10 +51,10 @@ }, "validations": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "validation_id": "validation_2026-04-28_841ac000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validator": { "kind": "system", "identifier": "approval-validator", @@ -93,10 +93,10 @@ ], "review_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_2026-04-28_47b2c000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", "rendered_at": "2026-04-28T09:14:26Z", @@ -138,12 +138,12 @@ } ], "request": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "tenant": "acme-corp", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validation_id": "validation_2026-04-28_841ac000", "review_snapshot_id": "review_2026-04-28_47b2c000", "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", @@ -162,13 +162,14 @@ } }, "policy": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, @@ -218,12 +219,12 @@ }, "approvals": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_revision": 1, - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_snapshot_id": "review_2026-04-28_47b2c000", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", @@ -259,14 +260,14 @@ ], "resolutions": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "resolution_id": "resolution_2026-04-28_78ac1000", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", - "approval_record_hash": "sha256:a3eaa0a9ea1e1b88b88de8d67fd7c58c1d66c05c7d274bf5ad130de46ec7c0e8", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "approval_record_hash": "sha256:d78fc9225b2cc30eb16571689b5a8f51f66cfffa0fb6e2364b6df998bb2c4ded", "decision": "approved", "terminal": true, "disposition": "execute_once", @@ -283,11 +284,11 @@ ], "authority_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "authority_snapshot_id": "authority_2026-04-28_9bc31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "checked_at": "2026-04-28T09:14:33Z", "valid_until": "2026-04-28T09:15:33Z", "status": "active", @@ -336,7 +337,7 @@ ], "dispatches": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_2026-04-28_4ad31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", @@ -344,7 +345,7 @@ "approval_2026-04-28_f9821000" ], "authority_snapshot_id": "authority_2026-04-28_9bc31000", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 1, "max_attempts": 3, @@ -365,12 +366,12 @@ ], "audit_events": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000001", "stream_id": "stream_prop_b3f1a2c4", "sequence": 1, "previous_event_hash": null, - "event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", "event_type": "proposal.received", "occurred_at": "2026-04-28T09:14:23Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -394,16 +395,16 @@ }, "references": {}, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000002", "stream_id": "stream_prop_b3f1a2c4", "sequence": 2, - "previous_event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", - "event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", "event_type": "validation.passed", "occurred_at": "2026-04-28T09:14:24Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -429,16 +430,16 @@ "validation_id": "validation_2026-04-28_841ac000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000003", "stream_id": "stream_prop_b3f1a2c4", "sequence": 3, - "previous_event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", - "event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", "event_type": "approval.requested", "occurred_at": "2026-04-28T09:14:25Z", "recorded_at": "2026-04-28T09:14:25Z", @@ -466,17 +467,17 @@ "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "policy_revision": "2026-04-15.3" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000004", "stream_id": "stream_prop_b3f1a2c4", "sequence": 4, - "previous_event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", - "event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -503,18 +504,18 @@ "review_snapshot_id": "review_2026-04-28_47b2c000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000005", "stream_id": "stream_prop_b3f1a2c4", "sequence": 5, - "previous_event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", - "event_hash": "sha256:c4006b95ac46a252dc5139ab83b9de948a47834cf64b412b0a0ab0da0055b262", + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", "event_type": "approval.decided", "occurred_at": "2026-04-28T09:14:30Z", "recorded_at": "2026-04-28T09:14:31Z", @@ -547,19 +548,19 @@ "detail": { "decision": "approved", "channel": "email", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "policy_revision": "2026-04-15.3", "wait_ms": 7000 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000006", "stream_id": "stream_prop_b3f1a2c4", "sequence": 6, - "previous_event_hash": "sha256:c4006b95ac46a252dc5139ab83b9de948a47834cf64b412b0a0ab0da0055b262", - "event_hash": "sha256:7077b2bb96fbe0d64f7d61ca4101ac41d5b8bee56156d674a8e63a8a859c8873", + "previous_event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", + "event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", "event_type": "resolution.emitted", "occurred_at": "2026-04-28T09:14:32Z", "recorded_at": "2026-04-28T09:14:32Z", @@ -588,16 +589,16 @@ }, "detail": { "decision": "approved", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000007", "stream_id": "stream_prop_b3f1a2c4", "sequence": 7, - "previous_event_hash": "sha256:7077b2bb96fbe0d64f7d61ca4101ac41d5b8bee56156d674a8e63a8a859c8873", - "event_hash": "sha256:f56f556310f333146e94f4808659d227039ed5c3bfa1e6a8c0791b7c400aac14", + "previous_event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", + "event_hash": "sha256:bd32c4072d00fa1f9c7296df898a290b45d5313ef4b4572778143b37787d9546", "event_type": "authority.checked", "occurred_at": "2026-04-28T09:14:33Z", "recorded_at": "2026-04-28T09:14:33Z", @@ -625,16 +626,16 @@ "authority_snapshot_id": "authority_2026-04-28_9bc31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000008", "stream_id": "stream_prop_b3f1a2c4", "sequence": 8, - "previous_event_hash": "sha256:f56f556310f333146e94f4808659d227039ed5c3bfa1e6a8c0791b7c400aac14", - "event_hash": "sha256:da01aa5134e17e13187f2046b5304750d85c8ea3c7ff6c49a018909c0aa65f4d", + "previous_event_hash": "sha256:bd32c4072d00fa1f9c7296df898a290b45d5313ef4b4572778143b37787d9546", + "event_hash": "sha256:271bc8a077e64d16e9825e3d21ca82c471273403db330bae7b0b91767a0644d0", "event_type": "dispatch.succeeded", "occurred_at": "2026-04-28T09:14:34Z", "recorded_at": "2026-04-28T09:14:34Z", @@ -662,18 +663,18 @@ "dispatch_id": "dispatch_2026-04-28_4ad31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "attempt": 1, "provider_result_hash": "sha256:78b0436944885eb78c09ea1a657e2d2c57b0d9672a19f709e6724c22a633a29b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000009", "stream_id": "stream_prop_b3f1a2c4", "sequence": 9, - "previous_event_hash": "sha256:da01aa5134e17e13187f2046b5304750d85c8ea3c7ff6c49a018909c0aa65f4d", - "event_hash": "sha256:22f923ad679f6d3b129733c86f20654350695be1af82883571672c9d990fed6b", + "previous_event_hash": "sha256:271bc8a077e64d16e9825e3d21ca82c471273403db330bae7b0b91767a0644d0", + "event_hash": "sha256:496ef04099ca3fdfe596d2cb037e27ad594a346bd478f978a9bb5182988096af", "event_type": "audit.snapshotted", "occurred_at": "2026-04-28T09:14:35Z", "recorded_at": "2026-04-28T09:14:35Z", @@ -702,7 +703,7 @@ } ], "audit_snapshot": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "audit_snapshot_2026-04-28_1df41000", "stream_id": "stream_prop_b3f1a2c4", "captured_at": "2026-04-28T09:14:35Z", @@ -710,7 +711,7 @@ "entry_count": 9, "first_sequence": 1, "last_sequence": 9, - "events_digest": "sha256:832fc218e4a91f42b16a48d06cc0273269af9b79509b6afb6e7ff501269b7093", - "root_event_hash": "sha256:22f923ad679f6d3b129733c86f20654350695be1af82883571672c9d990fed6b" + "events_digest": "sha256:f554f69da88c06819364a6b7e3ca361f7b72f7cb5929221537c3819325a153eb", + "root_event_hash": "sha256:496ef04099ca3fdfe596d2cb037e27ad594a346bd478f978a9bb5182988096af" } } diff --git a/examples/approval-policy.json b/examples/approval-policy.json index 9eb7d09..2c8f46a 100644 --- a/examples/approval-policy.json +++ b/examples/approval-policy.json @@ -1,11 +1,12 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, diff --git a/examples/approval-request.json b/examples/approval-request.json index ad656a4..676fcff 100644 --- a/examples/approval-request.json +++ b/examples/approval-request.json @@ -1,10 +1,10 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "tenant": "acme-corp", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validation_id": "validation_2026-04-28_841ac000", "review_snapshot_id": "review_2026-04-28_47b2c000", "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", @@ -23,13 +23,14 @@ } }, "policy": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, diff --git a/examples/audit-events.json b/examples/audit-events.json index 3aee6f2..f05c494 100644 --- a/examples/audit-events.json +++ b/examples/audit-events.json @@ -1,11 +1,11 @@ [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000001", "stream_id": "stream_prop_b3f1a2c4", "sequence": 1, "previous_event_hash": null, - "event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", "event_type": "proposal.received", "occurred_at": "2026-04-28T09:14:23Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -29,16 +29,16 @@ }, "references": {}, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000002", "stream_id": "stream_prop_b3f1a2c4", "sequence": 2, - "previous_event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", - "event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", "event_type": "validation.passed", "occurred_at": "2026-04-28T09:14:24Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -64,16 +64,16 @@ "validation_id": "validation_2026-04-28_841ac000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000003", "stream_id": "stream_prop_b3f1a2c4", "sequence": 3, - "previous_event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", - "event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", "event_type": "approval.requested", "occurred_at": "2026-04-28T09:14:25Z", "recorded_at": "2026-04-28T09:14:25Z", @@ -101,17 +101,17 @@ "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "policy_revision": "2026-04-15.3" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000004", "stream_id": "stream_prop_b3f1a2c4", "sequence": 4, - "previous_event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", - "event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -138,18 +138,18 @@ "review_snapshot_id": "review_2026-04-28_47b2c000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000005", "stream_id": "stream_prop_b3f1a2c4", "sequence": 5, - "previous_event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", - "event_hash": "sha256:c4006b95ac46a252dc5139ab83b9de948a47834cf64b412b0a0ab0da0055b262", + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", "event_type": "approval.decided", "occurred_at": "2026-04-28T09:14:30Z", "recorded_at": "2026-04-28T09:14:31Z", @@ -182,19 +182,19 @@ "detail": { "decision": "approved", "channel": "email", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "policy_revision": "2026-04-15.3", "wait_ms": 7000 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000006", "stream_id": "stream_prop_b3f1a2c4", "sequence": 6, - "previous_event_hash": "sha256:c4006b95ac46a252dc5139ab83b9de948a47834cf64b412b0a0ab0da0055b262", - "event_hash": "sha256:7077b2bb96fbe0d64f7d61ca4101ac41d5b8bee56156d674a8e63a8a859c8873", + "previous_event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", + "event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", "event_type": "resolution.emitted", "occurred_at": "2026-04-28T09:14:32Z", "recorded_at": "2026-04-28T09:14:32Z", @@ -223,16 +223,16 @@ }, "detail": { "decision": "approved", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000007", "stream_id": "stream_prop_b3f1a2c4", "sequence": 7, - "previous_event_hash": "sha256:7077b2bb96fbe0d64f7d61ca4101ac41d5b8bee56156d674a8e63a8a859c8873", - "event_hash": "sha256:f56f556310f333146e94f4808659d227039ed5c3bfa1e6a8c0791b7c400aac14", + "previous_event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", + "event_hash": "sha256:bd32c4072d00fa1f9c7296df898a290b45d5313ef4b4572778143b37787d9546", "event_type": "authority.checked", "occurred_at": "2026-04-28T09:14:33Z", "recorded_at": "2026-04-28T09:14:33Z", @@ -260,16 +260,16 @@ "authority_snapshot_id": "authority_2026-04-28_9bc31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000008", "stream_id": "stream_prop_b3f1a2c4", "sequence": 8, - "previous_event_hash": "sha256:f56f556310f333146e94f4808659d227039ed5c3bfa1e6a8c0791b7c400aac14", - "event_hash": "sha256:da01aa5134e17e13187f2046b5304750d85c8ea3c7ff6c49a018909c0aa65f4d", + "previous_event_hash": "sha256:bd32c4072d00fa1f9c7296df898a290b45d5313ef4b4572778143b37787d9546", + "event_hash": "sha256:271bc8a077e64d16e9825e3d21ca82c471273403db330bae7b0b91767a0644d0", "event_type": "dispatch.succeeded", "occurred_at": "2026-04-28T09:14:34Z", "recorded_at": "2026-04-28T09:14:34Z", @@ -297,18 +297,18 @@ "dispatch_id": "dispatch_2026-04-28_4ad31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "attempt": 1, "provider_result_hash": "sha256:78b0436944885eb78c09ea1a657e2d2c57b0d9672a19f709e6724c22a633a29b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000009", "stream_id": "stream_prop_b3f1a2c4", "sequence": 9, - "previous_event_hash": "sha256:da01aa5134e17e13187f2046b5304750d85c8ea3c7ff6c49a018909c0aa65f4d", - "event_hash": "sha256:22f923ad679f6d3b129733c86f20654350695be1af82883571672c9d990fed6b", + "previous_event_hash": "sha256:271bc8a077e64d16e9825e3d21ca82c471273403db330bae7b0b91767a0644d0", + "event_hash": "sha256:496ef04099ca3fdfe596d2cb037e27ad594a346bd478f978a9bb5182988096af", "event_type": "audit.snapshotted", "occurred_at": "2026-04-28T09:14:35Z", "recorded_at": "2026-04-28T09:14:35Z", diff --git a/examples/audit-snapshot.json b/examples/audit-snapshot.json index 0d43a27..9e1d2e2 100644 --- a/examples/audit-snapshot.json +++ b/examples/audit-snapshot.json @@ -1,5 +1,5 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "audit_snapshot_2026-04-28_1df41000", "stream_id": "stream_prop_b3f1a2c4", "captured_at": "2026-04-28T09:14:35Z", @@ -7,6 +7,6 @@ "entry_count": 9, "first_sequence": 1, "last_sequence": 9, - "events_digest": "sha256:832fc218e4a91f42b16a48d06cc0273269af9b79509b6afb6e7ff501269b7093", - "root_event_hash": "sha256:22f923ad679f6d3b129733c86f20654350695be1af82883571672c9d990fed6b" + "events_digest": "sha256:f554f69da88c06819364a6b7e3ca361f7b72f7cb5929221537c3819325a153eb", + "root_event_hash": "sha256:496ef04099ca3fdfe596d2cb037e27ad594a346bd478f978a9bb5182988096af" } diff --git a/examples/authority-snapshot.json b/examples/authority-snapshot.json index db7bdc1..bc5bb17 100644 --- a/examples/authority-snapshot.json +++ b/examples/authority-snapshot.json @@ -1,9 +1,9 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "authority_snapshot_id": "authority_2026-04-28_9bc31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "checked_at": "2026-04-28T09:14:33Z", "valid_until": "2026-04-28T09:15:33Z", "status": "active", diff --git a/examples/dispatch-record.json b/examples/dispatch-record.json index e0c757d..2282e76 100644 --- a/examples/dispatch-record.json +++ b/examples/dispatch-record.json @@ -1,5 +1,5 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_2026-04-28_4ad31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", @@ -7,7 +7,7 @@ "approval_2026-04-28_f9821000" ], "authority_snapshot_id": "authority_2026-04-28_9bc31000", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 1, "max_attempts": 3, diff --git a/examples/edited-envelope.json b/examples/edited-envelope.json index a9215fe..9b5c289 100644 --- a/examples/edited-envelope.json +++ b/examples/edited-envelope.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "captured_at": "2026-04-28T09:14:35Z", "proposal": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "agent": { @@ -27,9 +27,9 @@ ], "action_type": "email.send", "payload_schema": { - "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", - "version": "3.0.0", - "digest": "sha256:14730714de221b0506c8ae13b2f783f0eeb52202ea333385bf0d26e840f9d1d4" + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" }, "payload": { "from": "support@acme-corp.example", @@ -51,10 +51,10 @@ }, "validations": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "validation_id": "validation_2026-04-28_841ac000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validator": { "kind": "system", "identifier": "approval-validator", @@ -93,10 +93,10 @@ ], "review_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_2026-04-28_47b2c000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", "rendered_at": "2026-04-28T09:14:26Z", @@ -137,10 +137,10 @@ } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_edited_01", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "payload_hash": "sha256:46bfc2140a183593e6a03ba2ccac7fef1d029dbd8d0cd7c17878b6022f667a02", "content_hash": "sha256:b699962e199ac183f4d64a36a21ce73a37ea52d16d7e061e4318c6c2f499bff9", "rendered_at": "2026-04-28T09:14:26Z", @@ -182,12 +182,12 @@ } ], "request": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "tenant": "acme-corp", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validation_id": "validation_2026-04-28_841ac000", "review_snapshot_id": "review_2026-04-28_47b2c000", "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", @@ -206,13 +206,14 @@ } }, "policy": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, @@ -262,12 +263,12 @@ }, "approvals": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_revision": 1, - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "payload_hash": "sha256:46bfc2140a183593e6a03ba2ccac7fef1d029dbd8d0cd7c17878b6022f667a02", "review_snapshot_id": "review_edited_01", "review_content_hash": "sha256:b699962e199ac183f4d64a36a21ce73a37ea52d16d7e061e4318c6c2f499bff9", @@ -310,14 +311,14 @@ ], "resolutions": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "resolution_id": "resolution_2026-04-28_78ac1000", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", - "approval_record_hash": "sha256:2a1c24dc655e6e64f0197c0c466975d5d25e3f43671e8d1adc8175f8f2091b04", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", + "approval_record_hash": "sha256:fd11ae65b40c05fc38e35a923f6a9b6474586c8b8ce96ae0531274e733159ad7", "decision": "approved", "terminal": true, "disposition": "execute_once", @@ -334,11 +335,11 @@ ], "authority_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "authority_snapshot_id": "authority_2026-04-28_9bc31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "checked_at": "2026-04-28T09:14:33Z", "valid_until": "2026-04-28T09:15:33Z", "status": "active", @@ -387,7 +388,7 @@ ], "dispatches": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_2026-04-28_4ad31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", @@ -395,7 +396,7 @@ "approval_2026-04-28_f9821000" ], "authority_snapshot_id": "authority_2026-04-28_9bc31000", - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 1, "max_attempts": 3, @@ -416,12 +417,12 @@ ], "audit_events": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000001", "stream_id": "stream_prop_b3f1a2c4", "sequence": 1, "previous_event_hash": null, - "event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", "event_type": "proposal.received", "occurred_at": "2026-04-28T09:14:23Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -445,16 +446,16 @@ }, "references": {}, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000002", "stream_id": "stream_prop_b3f1a2c4", "sequence": 2, - "previous_event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", - "event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", "event_type": "validation.passed", "occurred_at": "2026-04-28T09:14:24Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -480,16 +481,16 @@ "validation_id": "validation_2026-04-28_841ac000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000003", "stream_id": "stream_prop_b3f1a2c4", "sequence": 3, - "previous_event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", - "event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", "event_type": "approval.requested", "occurred_at": "2026-04-28T09:14:25Z", "recorded_at": "2026-04-28T09:14:25Z", @@ -517,17 +518,17 @@ "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "policy_revision": "2026-04-15.3" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000004", "stream_id": "stream_prop_b3f1a2c4", "sequence": 4, - "previous_event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", - "event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -554,18 +555,18 @@ "review_snapshot_id": "review_2026-04-28_47b2c000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_edited_review_01", "stream_id": "stream_prop_b3f1a2c4", "sequence": 5, - "previous_event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", - "event_hash": "sha256:5db92bd1e204f8c9aee7c331e50ab86e920a39c84703f485280c920f35b5268e", + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:412257855b4f433a3942618eb1011932d3b873a460125d8d1e0e3fa63d817761", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -592,18 +593,18 @@ "review_snapshot_id": "review_edited_01" }, "detail": { - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "payload_hash": "sha256:46bfc2140a183593e6a03ba2ccac7fef1d029dbd8d0cd7c17878b6022f667a02", "review_content_hash": "sha256:b699962e199ac183f4d64a36a21ce73a37ea52d16d7e061e4318c6c2f499bff9" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000005", "stream_id": "stream_prop_b3f1a2c4", "sequence": 6, - "previous_event_hash": "sha256:5db92bd1e204f8c9aee7c331e50ab86e920a39c84703f485280c920f35b5268e", - "event_hash": "sha256:7fbe3365e2f0dd8dca4b23d72b3140f5476b161dd86856f6d29c10dd16e84576", + "previous_event_hash": "sha256:412257855b4f433a3942618eb1011932d3b873a460125d8d1e0e3fa63d817761", + "event_hash": "sha256:5a4c24d2f0d78635f2e07885b14684ab0c405a2df765fdc35c4af025d542816e", "event_type": "approval.decided", "occurred_at": "2026-04-28T09:14:30Z", "recorded_at": "2026-04-28T09:14:31Z", @@ -636,19 +637,19 @@ "detail": { "decision": "approved", "channel": "email", - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "payload_hash": "sha256:46bfc2140a183593e6a03ba2ccac7fef1d029dbd8d0cd7c17878b6022f667a02", "policy_revision": "2026-04-15.3", "wait_ms": 7000 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000006", "stream_id": "stream_prop_b3f1a2c4", "sequence": 7, - "previous_event_hash": "sha256:7fbe3365e2f0dd8dca4b23d72b3140f5476b161dd86856f6d29c10dd16e84576", - "event_hash": "sha256:938a68c99efadf3aad5e01dd4e2ed7f916e31f1a92bf73fbecb67f806b833add", + "previous_event_hash": "sha256:5a4c24d2f0d78635f2e07885b14684ab0c405a2df765fdc35c4af025d542816e", + "event_hash": "sha256:3a951319c784af29952b619b212984c4c089e9cbf5018cd23d315b7bacfa91d0", "event_type": "resolution.emitted", "occurred_at": "2026-04-28T09:14:32Z", "recorded_at": "2026-04-28T09:14:32Z", @@ -677,16 +678,16 @@ }, "detail": { "decision": "approved", - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53" + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000007", "stream_id": "stream_prop_b3f1a2c4", "sequence": 8, - "previous_event_hash": "sha256:938a68c99efadf3aad5e01dd4e2ed7f916e31f1a92bf73fbecb67f806b833add", - "event_hash": "sha256:7af7151dbf276849202e1d16eaa793b63622b660feed5dc1d1a67fbe6f3da782", + "previous_event_hash": "sha256:3a951319c784af29952b619b212984c4c089e9cbf5018cd23d315b7bacfa91d0", + "event_hash": "sha256:300b3ba6f3c7786b9ad8108e71e8464aa2b809bc70db8a54f21c6e37ef15fce6", "event_type": "authority.checked", "occurred_at": "2026-04-28T09:14:33Z", "recorded_at": "2026-04-28T09:14:33Z", @@ -714,16 +715,16 @@ "authority_snapshot_id": "authority_2026-04-28_9bc31000" }, "detail": { - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53" + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000008", "stream_id": "stream_prop_b3f1a2c4", "sequence": 9, - "previous_event_hash": "sha256:7af7151dbf276849202e1d16eaa793b63622b660feed5dc1d1a67fbe6f3da782", - "event_hash": "sha256:d9420a2f5768bad9adc75561e6c9101e0a2b5cda94f2f1692f3d3f60fc5b5501", + "previous_event_hash": "sha256:300b3ba6f3c7786b9ad8108e71e8464aa2b809bc70db8a54f21c6e37ef15fce6", + "event_hash": "sha256:181dbe8e10ae8e908b8f5e1ce182e0b28c980569f4f190edcf4d8dfd389940a6", "event_type": "dispatch.succeeded", "occurred_at": "2026-04-28T09:14:34Z", "recorded_at": "2026-04-28T09:14:34Z", @@ -751,18 +752,18 @@ "dispatch_id": "dispatch_2026-04-28_4ad31000" }, "detail": { - "action_hash": "sha256:84af89ea5e2e9c54828466f46df51c235692ec6939c3be2620b942ca8af53e53", + "action_hash": "sha256:540c1a9c0f435f18ec5a36b6521bd7928c44921fd99b613b8ed13f1abbaa5705", "attempt": 1, "provider_result_hash": "sha256:78b0436944885eb78c09ea1a657e2d2c57b0d9672a19f709e6724c22a633a29b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000009", "stream_id": "stream_prop_b3f1a2c4", "sequence": 10, - "previous_event_hash": "sha256:d9420a2f5768bad9adc75561e6c9101e0a2b5cda94f2f1692f3d3f60fc5b5501", - "event_hash": "sha256:b0c5e6e3e16163d212d56f5aee40a657073d561f076aa14454545797c68a50a9", + "previous_event_hash": "sha256:181dbe8e10ae8e908b8f5e1ce182e0b28c980569f4f190edcf4d8dfd389940a6", + "event_hash": "sha256:ab29c69805b3ab61cc9b06585a867681154dcb2189c227f87d2c9413d4f9a826", "event_type": "audit.snapshotted", "occurred_at": "2026-04-28T09:14:35Z", "recorded_at": "2026-04-28T09:14:35Z", @@ -791,7 +792,7 @@ } ], "audit_snapshot": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "audit_snapshot_2026-04-28_1df41000", "stream_id": "stream_prop_b3f1a2c4", "captured_at": "2026-04-28T09:14:35Z", @@ -799,7 +800,7 @@ "entry_count": 10, "first_sequence": 1, "last_sequence": 10, - "events_digest": "sha256:7896ac8db03212d18ce9de3bbd6fd0350f9c707b630af2627666e26681933a01", - "root_event_hash": "sha256:b0c5e6e3e16163d212d56f5aee40a657073d561f076aa14454545797c68a50a9" + "events_digest": "sha256:63449284d9b359e857958736715e6fad152d0df2d51c795b10d67252852ca9f1", + "root_event_hash": "sha256:ab29c69805b3ab61cc9b06585a867681154dcb2189c227f87d2c9413d4f9a826" } } diff --git a/examples/email-reply-approval-record.json b/examples/email-reply-approval-record.json index 9b6ef9c..f3d14a0 100644 --- a/examples/email-reply-approval-record.json +++ b/examples/email-reply-approval-record.json @@ -1,10 +1,10 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_revision": 1, - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_snapshot_id": "review_2026-04-28_47b2c000", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", diff --git a/examples/email-reply-approval.json b/examples/email-reply-approval.json index b5e1252..53965e0 100644 --- a/examples/email-reply-approval.json +++ b/examples/email-reply-approval.json @@ -1,5 +1,5 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "agent": { @@ -24,9 +24,9 @@ ], "action_type": "email.send", "payload_schema": { - "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", - "version": "3.0.0", - "digest": "sha256:14730714de221b0506c8ae13b2f783f0eeb52202ea333385bf0d26e840f9d1d4" + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" }, "payload": { "from": "support@acme-corp.example", diff --git a/examples/expired-envelope.json b/examples/expired-envelope.json index 1b9a0c4..70ceaa5 100644 --- a/examples/expired-envelope.json +++ b/examples/expired-envelope.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "captured_at": "2026-04-28T17:14:23Z", "proposal": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "agent": { @@ -27,9 +27,9 @@ ], "action_type": "email.send", "payload_schema": { - "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", - "version": "3.0.0", - "digest": "sha256:14730714de221b0506c8ae13b2f783f0eeb52202ea333385bf0d26e840f9d1d4" + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" }, "payload": { "from": "support@acme-corp.example", @@ -51,10 +51,10 @@ }, "validations": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "validation_id": "validation_2026-04-28_841ac000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validator": { "kind": "system", "identifier": "approval-validator", @@ -93,10 +93,10 @@ ], "review_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_2026-04-28_47b2c000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", "rendered_at": "2026-04-28T09:14:26Z", @@ -138,12 +138,12 @@ } ], "request": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "tenant": "acme-corp", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validation_id": "validation_2026-04-28_841ac000", "review_snapshot_id": "review_2026-04-28_47b2c000", "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", @@ -162,13 +162,14 @@ } }, "policy": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, @@ -219,12 +220,12 @@ "dispatches": [], "audit_events": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000001", "stream_id": "stream_prop_b3f1a2c4", "sequence": 1, "previous_event_hash": null, - "event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", "event_type": "proposal.received", "occurred_at": "2026-04-28T09:14:23Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -248,16 +249,16 @@ }, "references": {}, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000002", "stream_id": "stream_prop_b3f1a2c4", "sequence": 2, - "previous_event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", - "event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", "event_type": "validation.passed", "occurred_at": "2026-04-28T09:14:24Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -283,16 +284,16 @@ "validation_id": "validation_2026-04-28_841ac000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000003", "stream_id": "stream_prop_b3f1a2c4", "sequence": 3, - "previous_event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", - "event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", "event_type": "approval.requested", "occurred_at": "2026-04-28T09:14:25Z", "recorded_at": "2026-04-28T09:14:25Z", @@ -320,17 +321,17 @@ "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "policy_revision": "2026-04-15.3" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000004", "stream_id": "stream_prop_b3f1a2c4", "sequence": 4, - "previous_event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", - "event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -357,18 +358,18 @@ "review_snapshot_id": "review_2026-04-28_47b2c000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000005", "stream_id": "stream_prop_b3f1a2c4", "sequence": 5, - "previous_event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", - "event_hash": "sha256:51497e37dbf9f7284f52d9acda02f76a96fba7dba8bd53ff43f22d8e5bc9e2fd", + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:32b71eabc644f719fca3f8eb0eae0e314fd077dade1d70ae86117b3d01902d90", "event_type": "approval.expired", "occurred_at": "2026-04-28T17:14:23Z", "recorded_at": "2026-04-28T17:14:23Z", @@ -398,12 +399,12 @@ } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000009", "stream_id": "stream_prop_b3f1a2c4", "sequence": 6, - "previous_event_hash": "sha256:51497e37dbf9f7284f52d9acda02f76a96fba7dba8bd53ff43f22d8e5bc9e2fd", - "event_hash": "sha256:136a6cb24aca8c0410899e63c8c8bb4b35bfa20b2f8da4b22ca3af5723fc2089", + "previous_event_hash": "sha256:32b71eabc644f719fca3f8eb0eae0e314fd077dade1d70ae86117b3d01902d90", + "event_hash": "sha256:07ad174323d8f698fa2b53cd2b971d24f62562e058cdf79f192b2e7e04e78f93", "event_type": "audit.snapshotted", "occurred_at": "2026-04-28T17:14:23Z", "recorded_at": "2026-04-28T17:14:23Z", @@ -432,7 +433,7 @@ } ], "audit_snapshot": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "audit_snapshot_2026-04-28_1df41000", "stream_id": "stream_prop_b3f1a2c4", "captured_at": "2026-04-28T17:14:23Z", @@ -440,7 +441,7 @@ "entry_count": 6, "first_sequence": 1, "last_sequence": 6, - "events_digest": "sha256:66939b37bed32a3410fbceb0711f0a626026494f8307dd21429e07c51e61928f", - "root_event_hash": "sha256:136a6cb24aca8c0410899e63c8c8bb4b35bfa20b2f8da4b22ca3af5723fc2089" + "events_digest": "sha256:983ae93f8f6a59e07294af16b437c4de82db4fddfd1d157e462ecb500667538b", + "root_event_hash": "sha256:07ad174323d8f698fa2b53cd2b971d24f62562e058cdf79f192b2e7e04e78f93" } } diff --git a/examples/recovered-envelope.json b/examples/recovered-envelope.json index 7d6531b..461255d 100644 --- a/examples/recovered-envelope.json +++ b/examples/recovered-envelope.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "captured_at": "2026-04-28T09:14:38Z", "proposal": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "agent": { @@ -27,9 +27,9 @@ ], "action_type": "email.send", "payload_schema": { - "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", - "version": "3.0.0", - "digest": "sha256:14730714de221b0506c8ae13b2f783f0eeb52202ea333385bf0d26e840f9d1d4" + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" }, "payload": { "from": "support@acme-corp.example", @@ -51,10 +51,10 @@ }, "validations": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "validation_id": "validation_2026-04-28_841ac000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validator": { "kind": "system", "identifier": "approval-validator", @@ -93,10 +93,10 @@ ], "review_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_2026-04-28_47b2c000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", "rendered_at": "2026-04-28T09:14:26Z", @@ -138,12 +138,12 @@ } ], "request": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "tenant": "acme-corp", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validation_id": "validation_2026-04-28_841ac000", "review_snapshot_id": "review_2026-04-28_47b2c000", "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", @@ -162,13 +162,14 @@ } }, "policy": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, @@ -218,12 +219,12 @@ }, "approvals": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_revision": 1, - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_snapshot_id": "review_2026-04-28_47b2c000", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", @@ -259,14 +260,14 @@ ], "resolutions": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "resolution_id": "resolution_2026-04-28_78ac1000", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", - "approval_record_hash": "sha256:a3eaa0a9ea1e1b88b88de8d67fd7c58c1d66c05c7d274bf5ad130de46ec7c0e8", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "approval_record_hash": "sha256:d78fc9225b2cc30eb16571689b5a8f51f66cfffa0fb6e2364b6df998bb2c4ded", "decision": "approved", "terminal": true, "disposition": "execute_once", @@ -283,11 +284,11 @@ ], "authority_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "authority_snapshot_id": "authority_2026-04-28_9bc31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "checked_at": "2026-04-28T09:14:33Z", "valid_until": "2026-04-28T09:15:33Z", "status": "active", @@ -334,11 +335,11 @@ ] }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "authority_snapshot_id": "authority_retry_02", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "checked_at": "2026-04-28T09:14:36Z", "valid_until": "2026-04-28T09:15:33Z", "status": "active", @@ -387,7 +388,7 @@ ], "dispatches": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_2026-04-28_4ad31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", @@ -395,7 +396,7 @@ "approval_2026-04-28_f9821000" ], "authority_snapshot_id": "authority_2026-04-28_9bc31000", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 1, "max_attempts": 3, @@ -411,7 +412,7 @@ } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_reconciled_01", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", @@ -419,7 +420,7 @@ "approval_2026-04-28_f9821000" ], "authority_snapshot_id": "authority_2026-04-28_9bc31000", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 1, "max_attempts": 3, @@ -441,7 +442,7 @@ "previous_dispatch_id": "dispatch_2026-04-28_4ad31000" }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_retry_02", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", @@ -449,7 +450,7 @@ "approval_2026-04-28_f9821000" ], "authority_snapshot_id": "authority_retry_02", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 2, "max_attempts": 3, @@ -471,12 +472,12 @@ ], "audit_events": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000001", "stream_id": "stream_prop_b3f1a2c4", "sequence": 1, "previous_event_hash": null, - "event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", "event_type": "proposal.received", "occurred_at": "2026-04-28T09:14:23Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -500,16 +501,16 @@ }, "references": {}, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000002", "stream_id": "stream_prop_b3f1a2c4", "sequence": 2, - "previous_event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", - "event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", "event_type": "validation.passed", "occurred_at": "2026-04-28T09:14:24Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -535,16 +536,16 @@ "validation_id": "validation_2026-04-28_841ac000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000003", "stream_id": "stream_prop_b3f1a2c4", "sequence": 3, - "previous_event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", - "event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", "event_type": "approval.requested", "occurred_at": "2026-04-28T09:14:25Z", "recorded_at": "2026-04-28T09:14:25Z", @@ -572,17 +573,17 @@ "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "policy_revision": "2026-04-15.3" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000004", "stream_id": "stream_prop_b3f1a2c4", "sequence": 4, - "previous_event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", - "event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -609,18 +610,18 @@ "review_snapshot_id": "review_2026-04-28_47b2c000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000005", "stream_id": "stream_prop_b3f1a2c4", "sequence": 5, - "previous_event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", - "event_hash": "sha256:c4006b95ac46a252dc5139ab83b9de948a47834cf64b412b0a0ab0da0055b262", + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", "event_type": "approval.decided", "occurred_at": "2026-04-28T09:14:30Z", "recorded_at": "2026-04-28T09:14:31Z", @@ -653,19 +654,19 @@ "detail": { "decision": "approved", "channel": "email", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "policy_revision": "2026-04-15.3", "wait_ms": 7000 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000006", "stream_id": "stream_prop_b3f1a2c4", "sequence": 6, - "previous_event_hash": "sha256:c4006b95ac46a252dc5139ab83b9de948a47834cf64b412b0a0ab0da0055b262", - "event_hash": "sha256:7077b2bb96fbe0d64f7d61ca4101ac41d5b8bee56156d674a8e63a8a859c8873", + "previous_event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", + "event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", "event_type": "resolution.emitted", "occurred_at": "2026-04-28T09:14:32Z", "recorded_at": "2026-04-28T09:14:32Z", @@ -694,16 +695,16 @@ }, "detail": { "decision": "approved", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000007", "stream_id": "stream_prop_b3f1a2c4", "sequence": 7, - "previous_event_hash": "sha256:7077b2bb96fbe0d64f7d61ca4101ac41d5b8bee56156d674a8e63a8a859c8873", - "event_hash": "sha256:f56f556310f333146e94f4808659d227039ed5c3bfa1e6a8c0791b7c400aac14", + "previous_event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", + "event_hash": "sha256:bd32c4072d00fa1f9c7296df898a290b45d5313ef4b4572778143b37787d9546", "event_type": "authority.checked", "occurred_at": "2026-04-28T09:14:33Z", "recorded_at": "2026-04-28T09:14:33Z", @@ -731,16 +732,16 @@ "authority_snapshot_id": "authority_2026-04-28_9bc31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000008", "stream_id": "stream_prop_b3f1a2c4", "sequence": 8, - "previous_event_hash": "sha256:f56f556310f333146e94f4808659d227039ed5c3bfa1e6a8c0791b7c400aac14", - "event_hash": "sha256:786168c6f2ea3a65ee303a3886d474d84efeccbab841b2fb1976b86de6f6ba4a", + "previous_event_hash": "sha256:bd32c4072d00fa1f9c7296df898a290b45d5313ef4b4572778143b37787d9546", + "event_hash": "sha256:c5dc06ee26c8fe6b79aa2798f98b8aa9a6c1465532ff9d63883208794dce93af", "event_type": "dispatch.outcome_unknown", "occurred_at": "2026-04-28T09:14:34Z", "recorded_at": "2026-04-28T09:14:34Z", @@ -768,17 +769,17 @@ "dispatch_id": "dispatch_2026-04-28_4ad31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "attempt": 1 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_reconciled_01", "stream_id": "stream_prop_b3f1a2c4", "sequence": 9, - "previous_event_hash": "sha256:786168c6f2ea3a65ee303a3886d474d84efeccbab841b2fb1976b86de6f6ba4a", - "event_hash": "sha256:73b7b0a293861864123760352646ac19ed924606c5b099480d458610a8253400", + "previous_event_hash": "sha256:c5dc06ee26c8fe6b79aa2798f98b8aa9a6c1465532ff9d63883208794dce93af", + "event_hash": "sha256:8d13ead973456078a3ebdb9a404b8871b4e3f3ccddc79ea193da91ff30de4055", "event_type": "dispatch.reconciled", "occurred_at": "2026-04-28T09:14:35Z", "recorded_at": "2026-04-28T09:14:35Z", @@ -806,17 +807,17 @@ "dispatch_id": "dispatch_reconciled_01" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "attempt": 1 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_retry_authority_02", "stream_id": "stream_prop_b3f1a2c4", "sequence": 10, - "previous_event_hash": "sha256:73b7b0a293861864123760352646ac19ed924606c5b099480d458610a8253400", - "event_hash": "sha256:fcc6f19bb2d1c38bbae7cc9ee52395d16676a9c526c1d62e64398d4ab8aeb17f", + "previous_event_hash": "sha256:8d13ead973456078a3ebdb9a404b8871b4e3f3ccddc79ea193da91ff30de4055", + "event_hash": "sha256:9819a523978a7a4addfffeb2b9a2b1fb517d5dff7f4964213ce6f3e62b1075af", "event_type": "authority.checked", "occurred_at": "2026-04-28T09:14:36Z", "recorded_at": "2026-04-28T09:14:36Z", @@ -844,16 +845,16 @@ "authority_snapshot_id": "authority_retry_02" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_retry_02", "stream_id": "stream_prop_b3f1a2c4", "sequence": 11, - "previous_event_hash": "sha256:fcc6f19bb2d1c38bbae7cc9ee52395d16676a9c526c1d62e64398d4ab8aeb17f", - "event_hash": "sha256:f46bee5f1870fa93a3b8cf2c89fb02847f7f62279c78ad071fc0663f45228065", + "previous_event_hash": "sha256:9819a523978a7a4addfffeb2b9a2b1fb517d5dff7f4964213ce6f3e62b1075af", + "event_hash": "sha256:e1ef68fa76c22b05f6f2a7492c9743b47db77c42cc2d343c25987289880f5f9d", "event_type": "dispatch.succeeded", "occurred_at": "2026-04-28T09:14:37Z", "recorded_at": "2026-04-28T09:14:37Z", @@ -881,18 +882,18 @@ "dispatch_id": "dispatch_retry_02" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "attempt": 2, "provider_result_hash": "sha256:78b0436944885eb78c09ea1a657e2d2c57b0d9672a19f709e6724c22a633a29b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000009", "stream_id": "stream_prop_b3f1a2c4", "sequence": 12, - "previous_event_hash": "sha256:f46bee5f1870fa93a3b8cf2c89fb02847f7f62279c78ad071fc0663f45228065", - "event_hash": "sha256:bd6659674a0bc15aad3368db8fe31ac4a2eb97ce29df1b313076eaa61a658222", + "previous_event_hash": "sha256:e1ef68fa76c22b05f6f2a7492c9743b47db77c42cc2d343c25987289880f5f9d", + "event_hash": "sha256:97ea22807003cf9927d99756e063a5a61ceecd1679f439814ef5c0377758be67", "event_type": "audit.snapshotted", "occurred_at": "2026-04-28T09:14:38Z", "recorded_at": "2026-04-28T09:14:38Z", @@ -921,7 +922,7 @@ } ], "audit_snapshot": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "audit_snapshot_2026-04-28_1df41000", "stream_id": "stream_prop_b3f1a2c4", "captured_at": "2026-04-28T09:14:38Z", @@ -929,7 +930,7 @@ "entry_count": 12, "first_sequence": 1, "last_sequence": 12, - "events_digest": "sha256:fd3af6e070b308abcda42c06332a1dce2f7ca7735e245435fe085a031e2812b5", - "root_event_hash": "sha256:bd6659674a0bc15aad3368db8fe31ac4a2eb97ce29df1b313076eaa61a658222" + "events_digest": "sha256:cf6e397c05fd710afa6cb1e1221e83b67b13e81e41d3088cc9144df663f5e23e", + "root_event_hash": "sha256:97ea22807003cf9927d99756e063a5a61ceecd1679f439814ef5c0377758be67" } } diff --git a/examples/rejected-envelope.json b/examples/rejected-envelope.json index 7918534..403ec2a 100644 --- a/examples/rejected-envelope.json +++ b/examples/rejected-envelope.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "captured_at": "2026-04-28T09:14:35Z", "proposal": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "agent": { @@ -27,9 +27,9 @@ ], "action_type": "email.send", "payload_schema": { - "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", - "version": "3.0.0", - "digest": "sha256:14730714de221b0506c8ae13b2f783f0eeb52202ea333385bf0d26e840f9d1d4" + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" }, "payload": { "from": "support@acme-corp.example", @@ -51,10 +51,10 @@ }, "validations": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "validation_id": "validation_2026-04-28_841ac000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validator": { "kind": "system", "identifier": "approval-validator", @@ -93,10 +93,10 @@ ], "review_snapshots": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_2026-04-28_47b2c000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", "rendered_at": "2026-04-28T09:14:26Z", @@ -138,12 +138,12 @@ } ], "request": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", "tenant": "acme-corp", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validation_id": "validation_2026-04-28_841ac000", "review_snapshot_id": "review_2026-04-28_47b2c000", "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", @@ -162,13 +162,14 @@ } }, "policy": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "policy_id": "policy.high-risk-email.v1", "revision": "2026-04-15.3", "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", "evaluation_id": "eval_2026-04-28_1c4f8a00", - "material_input_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", "requirements": { "minimum_approvals": 1, "distinct_approvers": true, @@ -216,12 +217,12 @@ }, "approvals": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_revision": 1, - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_snapshot_id": "review_2026-04-28_47b2c000", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", @@ -260,18 +261,18 @@ ], "resolutions": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "resolution_id": "resolution_2026-04-28_78ac1000", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", - "approval_record_hash": "sha256:505db82561b809cedbc4f9469c0967724295f6d15a9a62802aba407e9955fc5a", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "approval_record_hash": "sha256:abec2cf7d95c29f2061519342329afa22f74c08df321b764677d90abe526896a", "decision": "rejected", "terminal": true, - "disposition": "do_not_execute", - "retry_policy": "never", + "disposition": "revise_and_resubmit", + "retry_policy": "new_proposal", "emitted_at": "2026-04-28T09:14:32Z", "delivery": { "target_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", @@ -279,18 +280,28 @@ "status": "acknowledged", "last_attempt_at": "2026-04-28T09:14:32Z", "acknowledged_at": "2026-04-28T09:14:32Z" + }, + "agent_feedback": { + "reason_code": "incorrect_content", + "message": "The reply promises a restore date that engineering has not confirmed.", + "requested_changes": [ + { + "path": "/payload/body_text", + "note": "Remove the restore date and link the status page instead." + } + ] } } ], "authority_snapshots": [], "dispatches": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "dispatch_id": "dispatch_2026-04-28_4ad31000", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_id": "req_2026-04-28_6a2d8800", "approval_ids": [], - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "idempotency_key": "idem_email_ZD-48211_reply_01", "attempt": 1, "max_attempts": 3, @@ -304,12 +315,12 @@ ], "audit_events": [ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000001", "stream_id": "stream_prop_b3f1a2c4", "sequence": 1, "previous_event_hash": null, - "event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", "event_type": "proposal.received", "occurred_at": "2026-04-28T09:14:23Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -333,16 +344,16 @@ }, "references": {}, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000002", "stream_id": "stream_prop_b3f1a2c4", "sequence": 2, - "previous_event_hash": "sha256:66a246a3d0b327820f1917f2b253b519ee63eb7f6c208b2516699c04f826061e", - "event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", "event_type": "validation.passed", "occurred_at": "2026-04-28T09:14:24Z", "recorded_at": "2026-04-28T09:14:24Z", @@ -368,16 +379,16 @@ "validation_id": "validation_2026-04-28_841ac000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000003", "stream_id": "stream_prop_b3f1a2c4", "sequence": 3, - "previous_event_hash": "sha256:08ea2c92d08a80f8500e577f39b99fbd92c82a36fdd5d4f383f7fe80b8a80a4c", - "event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", "event_type": "approval.requested", "occurred_at": "2026-04-28T09:14:25Z", "recorded_at": "2026-04-28T09:14:25Z", @@ -405,17 +416,17 @@ "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "policy_revision": "2026-04-15.3" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000004", "stream_id": "stream_prop_b3f1a2c4", "sequence": 4, - "previous_event_hash": "sha256:090742d5bbe1ff6082ac67791bfddd3f05d98e9b99c9dca2d56adc1c0c08a32a", - "event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", "event_type": "review.presented", "occurred_at": "2026-04-28T09:14:26Z", "recorded_at": "2026-04-28T09:14:26Z", @@ -442,18 +453,18 @@ "review_snapshot_id": "review_2026-04-28_47b2c000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000005", "stream_id": "stream_prop_b3f1a2c4", "sequence": 5, - "previous_event_hash": "sha256:56d02856f4b6549f0eae2ebc33c895541bd91ca1395df398acea29548a5d9688", - "event_hash": "sha256:bb1b52981c3d3731098ca4754c7f56e58c624aa12333d5433358e040b85087cc", + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:cf57a31101130006a4cb79e762480e7ce804bfc5ace0cce51b8c88cf228bcadb", "event_type": "approval.decided", "occurred_at": "2026-04-28T09:14:30Z", "recorded_at": "2026-04-28T09:14:31Z", @@ -486,19 +497,19 @@ "detail": { "decision": "rejected", "channel": "email", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "policy_revision": "2026-04-15.3", "wait_ms": 7000 } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000006", "stream_id": "stream_prop_b3f1a2c4", "sequence": 6, - "previous_event_hash": "sha256:bb1b52981c3d3731098ca4754c7f56e58c624aa12333d5433358e040b85087cc", - "event_hash": "sha256:bd699c2a161d7336c666c497397c7bae150aae3a82257b18d30cc0e28d2813b3", + "previous_event_hash": "sha256:cf57a31101130006a4cb79e762480e7ce804bfc5ace0cce51b8c88cf228bcadb", + "event_hash": "sha256:c9a007b389c54f19ddbee4e89ef5e4990aab60bb7d9231ec56f257b3f488cce5", "event_type": "resolution.emitted", "occurred_at": "2026-04-28T09:14:32Z", "recorded_at": "2026-04-28T09:14:32Z", @@ -527,16 +538,17 @@ }, "detail": { "decision": "rejected", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c" + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "reason_code": "incorrect_content" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000008", "stream_id": "stream_prop_b3f1a2c4", "sequence": 7, - "previous_event_hash": "sha256:bd699c2a161d7336c666c497397c7bae150aae3a82257b18d30cc0e28d2813b3", - "event_hash": "sha256:d57b9e1073743a0f7d98dbc496ef7cc77cd659656fd9631deec143cba3c33174", + "previous_event_hash": "sha256:c9a007b389c54f19ddbee4e89ef5e4990aab60bb7d9231ec56f257b3f488cce5", + "event_hash": "sha256:31aa6536e160ab7bf7202f624146c0594df7e3ccf944fde1264e353c17448792", "event_type": "dispatch.not_dispatched", "occurred_at": "2026-04-28T09:14:34Z", "recorded_at": "2026-04-28T09:14:34Z", @@ -564,17 +576,17 @@ "dispatch_id": "dispatch_2026-04-28_4ad31000" }, "detail": { - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "reason_code": "approval_rejected" } }, { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "event_id": "event_2026-04-28_00000009", "stream_id": "stream_prop_b3f1a2c4", "sequence": 8, - "previous_event_hash": "sha256:d57b9e1073743a0f7d98dbc496ef7cc77cd659656fd9631deec143cba3c33174", - "event_hash": "sha256:092f9ece7c9245c5385f03f2b98cb05b5880b880553cc6fc386a94bb654e02a3", + "previous_event_hash": "sha256:31aa6536e160ab7bf7202f624146c0594df7e3ccf944fde1264e353c17448792", + "event_hash": "sha256:bdaa62480dbec230488998b2b096ed8f967451388cf5411cb160a878b9b3e81d", "event_type": "audit.snapshotted", "occurred_at": "2026-04-28T09:14:35Z", "recorded_at": "2026-04-28T09:14:35Z", @@ -603,7 +615,7 @@ } ], "audit_snapshot": { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "audit_snapshot_2026-04-28_1df41000", "stream_id": "stream_prop_b3f1a2c4", "captured_at": "2026-04-28T09:14:35Z", @@ -611,7 +623,7 @@ "entry_count": 8, "first_sequence": 1, "last_sequence": 8, - "events_digest": "sha256:a4d3054ccf857372263566a881a02665df1fbb14c685e28f5c0e4cec561218dc", - "root_event_hash": "sha256:092f9ece7c9245c5385f03f2b98cb05b5880b880553cc6fc386a94bb654e02a3" + "events_digest": "sha256:67fe3563617427f58cfec061d4fd70130f278bc5f2a34975b16a4579d0c12cd4", + "root_event_hash": "sha256:bdaa62480dbec230488998b2b096ed8f967451388cf5411cb160a878b9b3e81d" } } diff --git a/examples/rejected-with-settlement-approval-record.json b/examples/rejected-with-settlement-approval-record.json index cfdcc8c..99dba10 100644 --- a/examples/rejected-with-settlement-approval-record.json +++ b/examples/rejected-with-settlement-approval-record.json @@ -1,5 +1,5 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_0e1bb000", "request_id": "req_2026-04-28_91cc7000", "proposal_id": "prop_2026-04-28_9d7e0f13", diff --git a/examples/resolution-record.json b/examples/resolution-record.json index 0cef3d1..6c936a2 100644 --- a/examples/resolution-record.json +++ b/examples/resolution-record.json @@ -1,12 +1,12 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "resolution_id": "resolution_2026-04-28_78ac1000", "approval_id": "approval_2026-04-28_f9821000", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "call_id": "call_2026-04-28_7e19c5ad", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", - "approval_record_hash": "sha256:a3eaa0a9ea1e1b88b88de8d67fd7c58c1d66c05c7d274bf5ad130de46ec7c0e8", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "approval_record_hash": "sha256:d78fc9225b2cc30eb16571689b5a8f51f66cfffa0fb6e2364b6df998bb2c4ded", "decision": "approved", "terminal": true, "disposition": "execute_once", diff --git a/examples/review-snapshot.json b/examples/review-snapshot.json index d34965e..edba6a5 100644 --- a/examples/review-snapshot.json +++ b/examples/review-snapshot.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "snapshot_id": "review_2026-04-28_47b2c000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", "rendered_at": "2026-04-28T09:14:26Z", diff --git a/examples/revoked-envelope.json b/examples/revoked-envelope.json new file mode 100644 index 0000000..fed6d33 --- /dev/null +++ b/examples/revoked-envelope.json @@ -0,0 +1,674 @@ +{ + "schema_version": "3.1.0", + "captured_at": "2026-04-28T09:14:35Z", + "proposal": { + "schema_version": "3.1.0", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "call_id": "call_2026-04-28_7e19c5ad", + "agent": { + "identity": "agent.support-triage.prod", + "name": "support-triage-agent", + "version": "1.4.2", + "model": "claude-opus-4-7", + "run_id": "run_2026-04-28_5f9e2c00" + }, + "delegation_chain": [ + { + "agent": { + "identity": "agent.support-triage.prod", + "name": "support-triage-agent", + "version": "1.4.2", + "model": "claude-opus-4-7", + "run_id": "run_2026-04-28_5f9e2c00" + }, + "call_id": "call_2026-04-28_7e19c5ad", + "delegated_at": "2026-04-28T09:14:23Z" + } + ], + "action_type": "email.send", + "payload_schema": { + "id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", + "version": "3.1.0", + "digest": "sha256:dc0eef238759e9c212248d0b1b56f62b182d0639e8f330bbe5b64bc1746ff35b" + }, + "payload": { + "from": "support@acme-corp.example", + "to": [ + "customer@example.com" + ], + "cc": [], + "subject": "Re: cannot export invoices for March", + "body_text": "Hi,\n\nThanks for the report. The export endpoint was rate-limited above the documented threshold last week — we've raised it back to 50 req/min. Please retry the March export and let us know if it still fails.\n\nIf you need the data sooner, I can run the export on our side and email you the CSV directly.\n\nBest,\nSupport", + "in_reply_to_message_id": "", + "ticket_id": "ZD-48211" + }, + "rationale": "The export issue maps to the documented rate-limit incident. Confirm the fix and offer a manual export as fallback.", + "created_at": "2026-04-28T09:14:23Z", + "expires_at": "2026-04-28T17:14:23Z", + "tenant": "acme-corp", + "risk": "high", + "idempotency_key": "idem_email_ZD-48211_reply_01" + }, + "validations": [ + { + "schema_version": "3.1.0", + "validation_id": "validation_2026-04-28_841ac000", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "validator": { + "kind": "system", + "identifier": "approval-validator", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:24Z" + } + }, + "validated_at": "2026-04-28T09:14:24Z", + "outcome": "passed", + "checks": [ + { + "name": "proposal_schema", + "status": "passed" + }, + { + "name": "payload_schema", + "status": "passed" + }, + { + "name": "semantic_preconditions", + "status": "passed" + }, + { + "name": "policy_inputs", + "status": "passed" + }, + { + "name": "authorization_inputs", + "status": "passed" + } + ] + } + ], + "review_snapshots": [ + { + "schema_version": "3.1.0", + "snapshot_id": "review_2026-04-28_47b2c000", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", + "content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", + "rendered_at": "2026-04-28T09:14:26Z", + "renderer": { + "name": "approval-email-renderer", + "version": "3.0.0", + "format": "json" + }, + "locale": "en-US", + "content": { + "action_type": "email.send", + "action_identity": { + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "call_id": "call_2026-04-28_7e19c5ad", + "agent_identity": "agent.support-triage.prod" + }, + "target": { + "from": "support@acme-corp.example", + "to": [ + "customer@example.com" + ], + "cc": [] + }, + "arguments": { + "from": "support@acme-corp.example", + "to": [ + "customer@example.com" + ], + "cc": [], + "subject": "Re: cannot export invoices for March", + "body_text": "Hi,\n\nThanks for the report. The export endpoint was rate-limited above the documented threshold last week — we've raised it back to 50 req/min. Please retry the March export and let us know if it still fails.\n\nIf you need the data sooner, I can run the export on our side and email you the CSV directly.\n\nBest,\nSupport", + "in_reply_to_message_id": "", + "ticket_id": "ZD-48211" + }, + "risk": "high", + "expires_at": "2026-04-28T17:14:23Z", + "policy_summary": "One distinct customer-support approver; email decisions require a signature." + } + } + ], + "request": { + "schema_version": "3.1.0", + "request_id": "req_2026-04-28_6a2d8800", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "call_id": "call_2026-04-28_7e19c5ad", + "tenant": "acme-corp", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "validation_id": "validation_2026-04-28_841ac000", + "review_snapshot_id": "review_2026-04-28_47b2c000", + "callback_nonce_hash": "sha256:91eaf28be1fa8121d74d9ce65298da05363c70dfb691c4c4e1a17691bb09966a", + "requested_at": "2026-04-28T09:14:25Z", + "expires_at": "2026-04-28T17:14:23Z", + "status": "revoked", + "revision": 3, + "requested_by": { + "kind": "system", + "identifier": "approval-router", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:25Z" + } + }, + "policy": { + "schema_version": "3.1.0", + "policy_id": "policy.high-risk-email.v1", + "revision": "2026-04-15.3", + "policy_digest": "sha256:a4f5ac837738a43e70baa0f2d602bbba2f2285a518beb568746f310f0678dc35", + "evaluation_id": "eval_2026-04-28_1c4f8a00", + "material_input_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "evaluated_at": "2026-04-28T09:14:24Z", + "outcome": "require_approval", + "requirements": { + "minimum_approvals": 1, + "distinct_approvers": true, + "human_only": true, + "allowed_channels": [ + "email", + "web" + ], + "signature_required_channels": [ + "email" + ], + "allow_auto_approval": false, + "required_roles": [ + "customer-support" + ], + "separation_of_duties": { + "requester_must_not_approve": true, + "agent_must_not_approve": true, + "distinct_signing_keys": true, + "eligible_approver_ids": [ + "rene@acme-corp.example" + ], + "excluded_actor_ids": [ + "approval-router", + "agent.support-triage.prod" + ], + "excluded_key_ids": [ + "agent-fleet-shared-key" + ] + } + } + }, + "channels": [ + { + "channel": "email", + "target_hash": "sha256:e8e3f7b243a672993b9c238585042b95995665863cc28ff00adeafca8e5ecb1b", + "delivery_status": "delivered", + "notification_id": "mail_2026-04-28_3391", + "sent_at": "2026-04-28T09:14:26Z" + } + ], + "terminal_at": "2026-04-28T09:14:34Z", + "decision_record_id": "approval_2026-04-28_f9821000", + "terminal_reason": "approval_revoked", + "revocation": { + "revoked_at": "2026-04-28T09:14:34Z", + "revoked_by": { + "kind": "human", + "identifier": "rene@acme-corp.example", + "display_name": "Rene Z.", + "roles": [ + "customer-support" + ], + "authentication": { + "method": "signed_callback", + "human_present": true, + "assurance": "substantial", + "authenticated_at": "2026-04-28T09:14:30Z", + "evidence_ref": "mail-callback:mail_2026-04-28_3391" + } + }, + "reason_code": "target_changed", + "safe_detail": "Customer replied on another thread before dispatch" + } + }, + "approvals": [ + { + "schema_version": "3.1.0", + "approval_id": "approval_2026-04-28_f9821000", + "request_id": "req_2026-04-28_6a2d8800", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "request_revision": 1, + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", + "review_snapshot_id": "review_2026-04-28_47b2c000", + "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", + "decision": "approved", + "decided_by": { + "kind": "human", + "identifier": "rene@acme-corp.example", + "display_name": "Rene Z.", + "roles": [ + "customer-support" + ], + "authentication": { + "method": "signed_callback", + "human_present": true, + "assurance": "substantial", + "authenticated_at": "2026-04-28T09:14:30Z", + "evidence_ref": "mail-callback:mail_2026-04-28_3391" + } + }, + "decided_at": "2026-04-28T09:14:30Z", + "recorded_at": "2026-04-28T09:14:31Z", + "channel": "email", + "comment": "Rate-limit explanation is accurate. Sending as drafted.", + "signatures": [ + { + "algorithm": "hmac-sha256", + "key_id": "tenant-acme-2026-q2", + "value": "H4f8GxQqI5kTMs7b4Y8cn2RlpdVLz0WGv8VUW0Sm5OU" + } + ], + "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" + } + ], + "resolutions": [ + { + "schema_version": "3.1.0", + "resolution_id": "resolution_2026-04-28_78ac1000", + "approval_id": "approval_2026-04-28_f9821000", + "request_id": "req_2026-04-28_6a2d8800", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "call_id": "call_2026-04-28_7e19c5ad", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "approval_record_hash": "sha256:d78fc9225b2cc30eb16571689b5a8f51f66cfffa0fb6e2364b6df998bb2c4ded", + "decision": "approved", + "terminal": true, + "disposition": "execute_once", + "retry_policy": "delivery_only", + "emitted_at": "2026-04-28T09:14:32Z", + "delivery": { + "target_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "attempt": 1, + "status": "acknowledged", + "last_attempt_at": "2026-04-28T09:14:32Z", + "acknowledged_at": "2026-04-28T09:14:32Z" + } + } + ], + "authority_snapshots": [], + "dispatches": [ + { + "schema_version": "3.1.0", + "dispatch_id": "dispatch_2026-04-28_4ad31000", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "request_id": "req_2026-04-28_6a2d8800", + "approval_ids": [], + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "idempotency_key": "idem_email_ZD-48211_reply_01", + "attempt": 1, + "max_attempts": 3, + "status": "not_dispatched", + "retry_allowed": false, + "started_at": "2026-04-28T09:14:35Z", + "completed_at": "2026-04-28T09:14:35Z", + "recorded_at": "2026-04-28T09:14:35Z", + "reason": "approval_revoked" + } + ], + "audit_events": [ + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000001", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 1, + "previous_event_hash": null, + "event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_type": "proposal.received", + "occurred_at": "2026-04-28T09:14:23Z", + "recorded_at": "2026-04-28T09:14:24Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "proposal-ingress", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:24Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:24Z", + "legal_hold": false + }, + "references": {}, + "detail": { + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000002", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 2, + "previous_event_hash": "sha256:ec2bf93c92a00c304b7236e832ce2bbe2b647a596cc63644f6b117417645ddc5", + "event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_type": "validation.passed", + "occurred_at": "2026-04-28T09:14:24Z", + "recorded_at": "2026-04-28T09:14:24Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "approval-validator", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:24Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:24Z", + "legal_hold": false + }, + "references": { + "validation_id": "validation_2026-04-28_841ac000" + }, + "detail": { + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000003", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 3, + "previous_event_hash": "sha256:19ee8cb6777da6401081a10f0632d7cf2d374864fc9756342e9e2a9b9f072162", + "event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_type": "approval.requested", + "occurred_at": "2026-04-28T09:14:25Z", + "recorded_at": "2026-04-28T09:14:25Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "approval-router", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:25Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:25Z", + "legal_hold": false + }, + "references": { + "request_id": "req_2026-04-28_6a2d8800", + "validation_id": "validation_2026-04-28_841ac000", + "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" + }, + "detail": { + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "policy_revision": "2026-04-15.3" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000004", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 4, + "previous_event_hash": "sha256:08e41cb26375e4a207e417c885dceb78f83054c0ccecea06958bad6d5fcbfb94", + "event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_type": "review.presented", + "occurred_at": "2026-04-28T09:14:26Z", + "recorded_at": "2026-04-28T09:14:26Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "approval-email-renderer", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:26Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:26Z", + "legal_hold": false + }, + "references": { + "request_id": "req_2026-04-28_6a2d8800", + "review_snapshot_id": "review_2026-04-28_47b2c000" + }, + "detail": { + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", + "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000005", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 5, + "previous_event_hash": "sha256:0b7b9061103a019b39628e26b2aea2656bb73d28221f2be251b7e67783391379", + "event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", + "event_type": "approval.decided", + "occurred_at": "2026-04-28T09:14:30Z", + "recorded_at": "2026-04-28T09:14:31Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "human", + "identifier": "rene@acme-corp.example", + "roles": [ + "customer-support" + ], + "authentication": { + "method": "signed_callback", + "human_present": true, + "assurance": "substantial", + "authenticated_at": "2026-04-28T09:14:30Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:31Z", + "legal_hold": false + }, + "references": { + "request_id": "req_2026-04-28_6a2d8800", + "approval_id": "approval_2026-04-28_f9821000", + "policy_evaluation_id": "eval_2026-04-28_1c4f8a00" + }, + "detail": { + "decision": "approved", + "channel": "email", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", + "policy_revision": "2026-04-15.3", + "wait_ms": 7000 + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000006", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 6, + "previous_event_hash": "sha256:baf6f0634aebc6c94e8b9ef76f9c9da60e0513938fee83183de4662609e35d75", + "event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", + "event_type": "resolution.emitted", + "occurred_at": "2026-04-28T09:14:32Z", + "recorded_at": "2026-04-28T09:14:32Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "approval-router", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:32Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:32Z", + "legal_hold": false + }, + "references": { + "request_id": "req_2026-04-28_6a2d8800", + "approval_id": "approval_2026-04-28_f9821000", + "resolution_id": "resolution_2026-04-28_78ac1000" + }, + "detail": { + "decision": "approved", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_revoked_01", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 7, + "previous_event_hash": "sha256:bf75da38290db5fb704e6b089a2eb5315097190d55bad35a156cc19ea33c453b", + "event_hash": "sha256:f20d26a729f83f599b876c882c18d473241266554ae0e8ad9a460b420cc6f0a1", + "event_type": "approval.revoked", + "occurred_at": "2026-04-28T09:14:34Z", + "recorded_at": "2026-04-28T09:14:34Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "human", + "identifier": "rene@acme-corp.example", + "display_name": "Rene Z.", + "roles": [ + "customer-support" + ], + "authentication": { + "method": "signed_callback", + "human_present": true, + "assurance": "substantial", + "authenticated_at": "2026-04-28T09:14:30Z", + "evidence_ref": "mail-callback:mail_2026-04-28_3391" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:31Z", + "legal_hold": false + }, + "references": { + "request_id": "req_2026-04-28_6a2d8800" + }, + "detail": { + "reason_code": "target_changed" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000008", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 8, + "previous_event_hash": "sha256:f20d26a729f83f599b876c882c18d473241266554ae0e8ad9a460b420cc6f0a1", + "event_hash": "sha256:3aecdaad166720e7fb0dc7c0f2af2fca16dc2e3705d72cfd26d54a382d8e2e96", + "event_type": "dispatch.not_dispatched", + "occurred_at": "2026-04-28T09:14:35Z", + "recorded_at": "2026-04-28T09:14:35Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "email-dispatcher", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:33Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:34Z", + "legal_hold": false + }, + "references": { + "request_id": "req_2026-04-28_6a2d8800", + "dispatch_id": "dispatch_2026-04-28_4ad31000" + }, + "detail": { + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", + "reason_code": "approval_revoked" + } + }, + { + "schema_version": "3.1.0", + "event_id": "event_2026-04-28_00000009", + "stream_id": "stream_prop_b3f1a2c4", + "sequence": 9, + "previous_event_hash": "sha256:3aecdaad166720e7fb0dc7c0f2af2fca16dc2e3705d72cfd26d54a382d8e2e96", + "event_hash": "sha256:2a58d83546f9e3010d0df37805a6633857ae05dfdcb67b39bf37d6b38942089c", + "event_type": "audit.snapshotted", + "occurred_at": "2026-04-28T09:14:35Z", + "recorded_at": "2026-04-28T09:14:35Z", + "tenant": "acme-corp", + "proposal_id": "prop_2026-04-28_b3f1a2c4", + "actor": { + "kind": "system", + "identifier": "approval-audit", + "authentication": { + "method": "service_identity", + "human_present": false, + "assurance": "high", + "authenticated_at": "2026-04-28T09:14:35Z" + } + }, + "classification": "confidential", + "retention": { + "class": "standard", + "delete_after": "2027-04-28T09:14:35Z", + "legal_hold": false + }, + "references": { + "audit_snapshot_id": "audit_snapshot_2026-04-28_1df41000" + }, + "detail": {} + } + ], + "audit_snapshot": { + "schema_version": "3.1.0", + "snapshot_id": "audit_snapshot_2026-04-28_1df41000", + "stream_id": "stream_prop_b3f1a2c4", + "captured_at": "2026-04-28T09:14:35Z", + "complete": true, + "entry_count": 9, + "first_sequence": 1, + "last_sequence": 9, + "events_digest": "sha256:8ad8bfec331fe34f475827e2832db57d422f5ff8df267c30f68778fa16c05aa4", + "root_event_hash": "sha256:2a58d83546f9e3010d0df37805a6633857ae05dfdcb67b39bf37d6b38942089c" + } +} diff --git a/examples/validation-record.json b/examples/validation-record.json index 4fdd451..18144f3 100644 --- a/examples/validation-record.json +++ b/examples/validation-record.json @@ -1,8 +1,8 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "validation_id": "validation_2026-04-28_841ac000", "proposal_id": "prop_2026-04-28_b3f1a2c4", - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "validator": { "kind": "system", "identifier": "approval-validator", diff --git a/examples/webauthn-approval-record.json b/examples/webauthn-approval-record.json index 59d4069..6b23256 100644 --- a/examples/webauthn-approval-record.json +++ b/examples/webauthn-approval-record.json @@ -1,10 +1,10 @@ { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "approval_id": "approval_2026-04-28_webauthn1", "request_id": "req_2026-04-28_6a2d8800", "proposal_id": "prop_2026-04-28_b3f1a2c4", "request_revision": 1, - "action_hash": "sha256:94710a0a0a7cebd31eda4a86a91ad493fd162495484726cf7b13fd7906ef5f8c", + "action_hash": "sha256:bfe3494ebf07cf812fa4b5fc1720cac89f49def7a62f935b3c579e21c86e601b", "payload_hash": "sha256:c919182dcf3aa49d95f5927636aeb7a6a227fa9f9b3daa7384ac4c531a9f891b", "review_snapshot_id": "review_2026-04-28_47b2c000", "review_content_hash": "sha256:1496a0bf99cb54f169a7deb5e57ec48be882f1859fbd39cc2014563d3a250636", @@ -33,14 +33,14 @@ { "algorithm": "webauthn-es256", "key_id": "0cjNOhrHb5fQyIDYaRY4Xl9alS_ZqMfUMhHTUOdwD44", - "value": "MEYCIQDIUUpEON4hAJD6vWc2nE7UkgnwJZnopj6ljYVYWLP6UAIhAPNeH_j_L7KWlKZXx_MLnGTqvHBna3rib8k2pI6ptSEo", - "signed_object_hash": "sha256:d8e3d79f6f1bd80c0a50861a4835f789b5e4b74e8610c33cfd81349aa6f442b2", + "value": "MEQCIB0jjUdGANxgZ4V51wgqPcqt-sFstGGzFpwIrG58V9rKAiAVaKXiZSnt_HnFK-66jOi7OBZKCMC2FaJR8YQFiqIB9Q", + "signed_object_hash": "sha256:52ac7187db2acd0442c37a9c4d2ab1db837136c37641e8697787bd9ba78c9c90", "webauthn": { "credential_id": "0cjNOhrHb5fQyIDYaRY4Xl9alS_ZqMfUMhHTUOdwD44", "credential_public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwTs5yuIWm75cZG8csjFhuwDbl58swFV1Dg9wMx7heTMY8zZXtOZpXSj0WgP5gICY76Nc8k3u39ELxbZlpWK8IA", "public_key_format": "spki-der", "authenticator_data": "Bh4i5kXWgfgyp3-i_7Tc6tPg1gtE80-ZMLP6GootWYQFAAAACQ", - "client_data_json": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMk9QWG4yOGIyQXdLVUlZYVNEWDNpYlhrdDA2R0VNTThfWUUwbXFiMFFySSIsIm9yaWdpbiI6Imh0dHBzOi8vYXBwcm92ZS5hY21lLWNvcnAuZXhhbXBsZSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", + "client_data_json": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVXF4eGg5c3F6UVJDdzNxY1RTcXgyNE54TnNOMlFlaHBkNGU5bTZlTW5KQSIsIm9yaWdpbiI6Imh0dHBzOi8vYXBwcm92ZS5hY21lLWNvcnAuZXhhbXBsZSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "rp_id": "acme-corp.example", "origin": "https://approve.acme-corp.example", "user_verification": "required", diff --git a/schemas/actions/api.call.schema.json b/schemas/actions/api.call.schema.json new file mode 100644 index 0000000..76d4b05 --- /dev/null +++ b/schemas/actions/api.call.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/api.call.schema.json", + "title": "api.call payload", + "description": "Reference payload contract for a dispatcher-owned HTTP call. The agent names a registered operation, never a URL or credential; the dispatcher resolves the endpoint and authentication.", + "x-action-type": "api.call", + "x-review-target": ["service", "operation", "method"], + "type": "object", + "required": ["service", "operation", "method"], + "additionalProperties": false, + "properties": { + "service": {"type": "string", "pattern": "^[a-z][a-z0-9_.-]{1,63}$"}, + "operation": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,127}$"}, + "method": {"type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"]}, + "path_params": { + "type": "object", + "maxProperties": 16, + "additionalProperties": {"type": "string", "maxLength": 256} + }, + "query": { + "type": "object", + "maxProperties": 32, + "additionalProperties": {"type": ["string", "number", "boolean"]} + }, + "body": {"type": ["object", "array"]} + } +} diff --git a/schemas/actions/crm.update_record.schema.json b/schemas/actions/crm.update_record.schema.json new file mode 100644 index 0000000..dc02990 --- /dev/null +++ b/schemas/actions/crm.update_record.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/crm.update_record.schema.json", + "title": "crm.update_record payload", + "description": "Reference payload contract for a CRM update dispatcher. The expected version lets the dispatcher refuse an update to a record that changed after review.", + "x-action-type": "crm.update_record", + "x-review-target": ["system", "object_type", "record_id"], + "type": "object", + "required": ["system", "object_type", "record_id", "expected_version", "changes"], + "additionalProperties": false, + "properties": { + "system": {"type": "string", "pattern": "^[a-z][a-z0-9_.-]{1,63}$"}, + "object_type": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$"}, + "record_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "expected_version": {"type": "string", "minLength": 1, "maxLength": 128}, + "changes": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "type": "object", + "required": ["field", "to"], + "additionalProperties": false, + "properties": { + "field": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_.]{0,127}$"}, + "from": true, + "to": true + } + } + } + } +} diff --git a/schemas/actions/db.update_row.schema.json b/schemas/actions/db.update_row.schema.json new file mode 100644 index 0000000..8066569 --- /dev/null +++ b/schemas/actions/db.update_row.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/db.update_row.schema.json", + "title": "db.update_row payload", + "description": "Reference payload contract for a single-row database update. The key and the expected row version bound the update to exactly one reviewed row; there is no free-form query.", + "x-action-type": "db.update_row", + "x-review-target": ["database", "table", "key"], + "type": "object", + "required": ["database", "table", "key", "expected_version", "set"], + "additionalProperties": false, + "properties": { + "database": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"}, + "table": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_.]{0,127}$"}, + "key": { + "type": "object", + "minProperties": 1, + "maxProperties": 8, + "propertyNames": {"pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$"}, + "additionalProperties": {"type": ["string", "integer"]} + }, + "expected_version": {"type": ["string", "integer"]}, + "set": { + "type": "object", + "minProperties": 1, + "maxProperties": 50, + "propertyNames": {"pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$"} + } + } +} diff --git a/schemas/actions/email.send.schema.json b/schemas/actions/email.send.schema.json index f940880..de7fba3 100644 --- a/schemas/actions/email.send.schema.json +++ b/schemas/actions/email.send.schema.json @@ -1,8 +1,10 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/actions/email.send.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/email.send.schema.json", "title": "email.send payload", "description": "Reference payload contract for an email dispatcher. Copy and adapt this pattern next to each real dispatcher.", + "x-action-type": "email.send", + "x-review-target": ["from", "to", "cc"], "type": "object", "required": ["from", "to", "subject", "body_text"], "additionalProperties": false, diff --git a/schemas/actions/n8n.trigger_workflow.schema.json b/schemas/actions/n8n.trigger_workflow.schema.json new file mode 100644 index 0000000..6a1f255 --- /dev/null +++ b/schemas/actions/n8n.trigger_workflow.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/n8n.trigger_workflow.schema.json", + "title": "n8n.trigger_workflow payload", + "description": "Reference payload contract for triggering a registered n8n workflow. The dispatcher maps the workflow ID to its webhook and credential.", + "x-action-type": "n8n.trigger_workflow", + "x-review-target": ["workflow_id"], + "type": "object", + "required": ["workflow_id", "input"], + "additionalProperties": false, + "properties": { + "workflow_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$"}, + "workflow_version": {"type": "string", "minLength": 1, "maxLength": 64}, + "input": {"type": "object", "maxProperties": 64} + } +} diff --git a/schemas/actions/ticket.create.schema.json b/schemas/actions/ticket.create.schema.json new file mode 100644 index 0000000..7be00c0 --- /dev/null +++ b/schemas/actions/ticket.create.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/actions/ticket.create.schema.json", + "title": "ticket.create payload", + "description": "Reference payload contract for an issue or ticket creation dispatcher.", + "x-action-type": "ticket.create", + "x-review-target": ["system", "project", "assignee"], + "type": "object", + "required": ["system", "project", "title", "description"], + "additionalProperties": false, + "properties": { + "system": {"type": "string", "pattern": "^[a-z][a-z0-9_.-]{1,63}$"}, + "project": {"type": "string", "minLength": 1, "maxLength": 128}, + "title": {"type": "string", "minLength": 1, "maxLength": 256}, + "description": {"type": "string", "minLength": 1, "maxLength": 65536}, + "priority": {"type": "string", "enum": ["low", "normal", "high", "urgent"]}, + "assignee": {"type": "string", "minLength": 1, "maxLength": 128}, + "labels": { + "type": "array", + "maxItems": 20, + "uniqueItems": true, + "items": {"type": "string", "minLength": 1, "maxLength": 64} + } + } +} diff --git a/schemas/approval-envelope.schema.json b/schemas/approval-envelope.schema.json index a567b73..790caba 100644 --- a/schemas/approval-envelope.schema.json +++ b/schemas/approval-envelope.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/approval-envelope.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/approval-envelope.schema.json", "title": "ApprovalEnvelope", "description": "A complete portable lifecycle bundle. JSON Schema checks structure; scripts/validate_contracts.py also checks cross-record identity, hashes, quorum, time, sequence, and attempt invariants.", "type": "object", @@ -86,7 +86,7 @@ "if": { "properties": { "decision": {"const": "approved"}, - "channel": {"enum": ["telegram", "email"]} + "channel": {"enum": ["telegram", "email", "mcp_elicitation"]} }, "required": ["decision", "channel"] }, diff --git a/schemas/approval-policy.schema.json b/schemas/approval-policy.schema.json index bce9d20..49aa734 100644 --- a/schemas/approval-policy.schema.json +++ b/schemas/approval-policy.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/approval-policy.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/approval-policy.schema.json", "title": "ApprovalPolicySnapshot", "description": "Immutable evidence of the policy revision and material inputs that selected an approval path.", "type": "object", @@ -23,6 +23,12 @@ "evaluation_id": {"$ref": "common.schema.json#/$defs/identifier"}, "material_input_hash": {"$ref": "common.schema.json#/$defs/digest"}, "evaluated_at": {"type": "string", "format": "date-time"}, + "outcome": { + "description": "The evaluator's closed decision for this action. Record any other evaluator result, including a non-boolean, unknown, or errored result, as deny.", + "type": "string", + "enum": ["require_approval", "auto_approve", "deny"] + }, + "outcome_reason": {"$ref": "common.schema.json#/$defs/safe_text", "maxLength": 500}, "requirements": { "type": "object", "required": [ @@ -44,7 +50,7 @@ "uniqueItems": true, "items": { "type": "string", - "enum": ["telegram", "slack", "email", "web", "n8n_form", "policy"] + "enum": ["telegram", "slack", "email", "web", "n8n_form", "mcp_elicitation", "policy"] } }, "signature_required_channels": { @@ -52,7 +58,7 @@ "uniqueItems": true, "items": { "type": "string", - "enum": ["telegram", "slack", "email", "web", "n8n_form", "policy"] + "enum": ["telegram", "slack", "email", "web", "n8n_form", "mcp_elicitation", "policy"] } }, "allow_auto_approval": {"type": "boolean"}, @@ -96,5 +102,15 @@ } } } - } + }, + "allOf": [ + { + "if": {"properties": {"outcome": {"const": "auto_approve"}}, "required": ["outcome"]}, + "then": { + "properties": { + "requirements": {"properties": {"allow_auto_approval": {"const": true}}} + } + } + } + ] } diff --git a/schemas/approval-record.schema.json b/schemas/approval-record.schema.json index 735a218..fc185c7 100644 --- a/schemas/approval-record.schema.json +++ b/schemas/approval-record.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/approval-record.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/approval-record.schema.json", "title": "ApprovalRecord", "description": "An append-only terminal decision bound to the exact effective action and payload reviewed by the approver.", "type": "object", @@ -41,7 +41,7 @@ "recorded_at": {"type": "string", "format": "date-time"}, "channel": { "type": "string", - "enum": ["telegram", "slack", "email", "web", "n8n_form", "policy"] + "enum": ["telegram", "slack", "email", "web", "n8n_form", "mcp_elicitation", "policy"] }, "comment": {"type": "string", "maxLength": 2000}, "modifications": { @@ -174,7 +174,7 @@ "required": ["authentication"] }, "channel": { - "enum": ["telegram", "slack", "email", "web", "n8n_form"] + "enum": ["telegram", "slack", "email", "web", "n8n_form", "mcp_elicitation"] } } } @@ -215,6 +215,32 @@ "if": {"required": ["modifications"]}, "then": {"properties": {"decision": {"const": "approved"}}} }, + { + "if": { + "properties": { + "channel": {"const": "mcp_elicitation"}, + "decision": {"enum": ["approved", "rejected"]} + }, + "required": ["channel", "decision"] + }, + "then": { + "properties": { + "decided_by": { + "properties": { + "kind": {"const": "human"}, + "authentication": { + "properties": { + "method": {"enum": ["authenticated_session", "channel_identity", "signed_callback"]}, + "human_present": {"const": true} + }, + "required": ["method", "human_present"] + } + }, + "required": ["kind", "authentication"] + } + } + } + }, { "if": { "properties": { diff --git a/schemas/approval-request.schema.json b/schemas/approval-request.schema.json index a36eb43..24d2404 100644 --- a/schemas/approval-request.schema.json +++ b/schemas/approval-request.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/approval-request.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/approval-request.schema.json", "title": "ApprovalRequest", "description": "Durable state for one pending approval. Resolve it by request_id with compare-and-set semantics; never infer authority from payload shape or conversation text.", "type": "object", @@ -38,7 +38,7 @@ "expires_at": {"type": "string", "format": "date-time"}, "status": { "type": "string", - "enum": ["pending", "approved", "rejected", "expired", "cancelled", "failed", "consumed"] + "enum": ["pending", "approved", "rejected", "expired", "cancelled", "failed", "consumed", "revoked"] }, "revision": {"type": "integer", "minimum": 1}, "requested_by": {"$ref": "common.schema.json#/$defs/actor"}, @@ -54,7 +54,7 @@ "properties": { "channel": { "type": "string", - "enum": ["telegram", "slack", "email", "web", "n8n_form", "policy"] + "enum": ["telegram", "slack", "email", "web", "n8n_form", "mcp_elicitation", "policy"] }, "target_hash": {"$ref": "common.schema.json#/$defs/digest"}, "delivery_status": { @@ -85,8 +85,31 @@ "callback_invalid", "policy_failed", "delivery_failed", - "consumed_for_dispatch" + "consumed_for_dispatch", + "approval_revoked" ] + }, + "revocation": { + "description": "Withdrawal of an approved, unconsumed request. A conditional transition from approved to revoked must fail when the request was already consumed.", + "type": "object", + "required": ["revoked_at", "revoked_by", "reason_code"], + "additionalProperties": false, + "properties": { + "revoked_at": {"type": "string", "format": "date-time"}, + "revoked_by": {"$ref": "common.schema.json#/$defs/actor"}, + "reason_code": { + "type": "string", + "enum": [ + "approver_withdrew", + "authority_changed", + "policy_changed", + "target_changed", + "superseded", + "security_hold" + ] + }, + "safe_detail": {"$ref": "common.schema.json#/$defs/safe_text", "maxLength": 500} + } } }, "allOf": [ @@ -179,6 +202,20 @@ } } }, + { + "if": {"properties": {"status": {"const": "revoked"}}, "required": ["status"]}, + "then": { + "required": ["terminal_at", "terminal_reason", "decision_record_id", "revocation"], + "properties": {"terminal_reason": {"const": "approval_revoked"}}, + "not": { + "anyOf": [ + {"required": ["consumed_at"]}, + {"required": ["dispatch_id"]} + ] + } + }, + "else": {"not": {"required": ["revocation"]}} + }, { "if": {"properties": {"status": {"const": "consumed"}}, "required": ["status"]}, "then": { diff --git a/schemas/audit-event.schema.json b/schemas/audit-event.schema.json index 4289611..04afb86 100644 --- a/schemas/audit-event.schema.json +++ b/schemas/audit-event.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/audit-event.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/audit-event.schema.json", "title": "ApprovalAuditEvent", "description": "A redacted, append-only lifecycle event. event_hash is the RFC 8785 SHA-256 digest of this document with event_hash omitted; previous_event_hash links the stream.", "type": "object", @@ -48,6 +48,7 @@ "approval.decided", "approval.expired", "approval.cancelled", + "approval.revoked", "resolution.emitted", "resolution.acknowledged", "authority.checked", @@ -105,7 +106,7 @@ }, "channel": { "type": "string", - "enum": ["telegram", "slack", "email", "web", "n8n_form", "policy"] + "enum": ["telegram", "slack", "email", "web", "n8n_form", "mcp_elicitation", "policy"] }, "action_hash": {"$ref": "common.schema.json#/$defs/digest"}, "payload_hash": {"$ref": "common.schema.json#/$defs/digest"}, diff --git a/schemas/audit-snapshot.schema.json b/schemas/audit-snapshot.schema.json index 8305cb7..2424d9d 100644 --- a/schemas/audit-snapshot.schema.json +++ b/schemas/audit-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/audit-snapshot.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/audit-snapshot.schema.json", "title": "AuditSnapshot", "description": "A completeness manifest that binds one exported audit-event set to its stream, sequence bounds, entry count, canonical digest, and terminal chain hash.", "type": "object", diff --git a/schemas/authority-snapshot.schema.json b/schemas/authority-snapshot.schema.json index ab7dd49..fd4ff47 100644 --- a/schemas/authority-snapshot.schema.json +++ b/schemas/authority-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/authority-snapshot.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/authority-snapshot.schema.json", "title": "AuthoritySnapshot", "description": "A dispatch-time snapshot proving that the approver, policy, workspace, and dispatcher authorities remain active after approval and before provider invocation.", "type": "object", diff --git a/schemas/common.schema.json b/schemas/common.schema.json index bd91dbc..5291b3d 100644 --- a/schemas/common.schema.json +++ b/schemas/common.schema.json @@ -1,11 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/common.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/common.schema.json", "title": "Agent Approval Gate common definitions", "$defs": { "schema_version": { "type": "string", - "const": "3.0.0" + "const": "3.1.0" }, "identifier": { "type": "string", diff --git a/schemas/dispatch-record.schema.json b/schemas/dispatch-record.schema.json index 0667df4..f46d3b6 100644 --- a/schemas/dispatch-record.schema.json +++ b/schemas/dispatch-record.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/dispatch-record.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/dispatch-record.schema.json", "title": "DispatchRecord", "description": "An append-only record for one deterministic dispatch attempt or reconciliation result. outcome_unknown is never an invitation to retry blindly.", "type": "object", @@ -72,6 +72,7 @@ "dispatcher_error", "approval_rejected", "approval_cancelled", + "approval_revoked", "request_failed" ] }, diff --git a/schemas/proposed-action.schema.json b/schemas/proposed-action.schema.json index f489378..aa4fd8b 100644 --- a/schemas/proposed-action.schema.json +++ b/schemas/proposed-action.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/proposed-action.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/proposed-action.schema.json", "title": "ProposedAction", "description": "An immutable, serializable action draft. The RFC 8785 SHA-256 digest of this complete document is the action_hash carried through approval and dispatch.", "type": "object", @@ -85,6 +85,51 @@ "then": { "properties": {"payload": {"$ref": "actions/email.send.schema.json"}} } + }, + { + "if": { + "properties": {"action_type": {"const": "crm.update_record"}}, + "required": ["action_type"] + }, + "then": { + "properties": {"payload": {"$ref": "actions/crm.update_record.schema.json"}} + } + }, + { + "if": { + "properties": {"action_type": {"const": "ticket.create"}}, + "required": ["action_type"] + }, + "then": { + "properties": {"payload": {"$ref": "actions/ticket.create.schema.json"}} + } + }, + { + "if": { + "properties": {"action_type": {"const": "db.update_row"}}, + "required": ["action_type"] + }, + "then": { + "properties": {"payload": {"$ref": "actions/db.update_row.schema.json"}} + } + }, + { + "if": { + "properties": {"action_type": {"const": "api.call"}}, + "required": ["action_type"] + }, + "then": { + "properties": {"payload": {"$ref": "actions/api.call.schema.json"}} + } + }, + { + "if": { + "properties": {"action_type": {"const": "n8n.trigger_workflow"}}, + "required": ["action_type"] + }, + "then": { + "properties": {"payload": {"$ref": "actions/n8n.trigger_workflow.schema.json"}} + } } ] } diff --git a/schemas/resolution-record.schema.json b/schemas/resolution-record.schema.json index a58449b..ca7d86e 100644 --- a/schemas/resolution-record.schema.json +++ b/schemas/resolution-record.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/resolution-record.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/resolution-record.schema.json", "title": "ResolutionRecord", "description": "A durable agent-facing receipt for an approval decision. Recording the decision and delivering it are separate facts, so a failed resume can be retried without reopening approval or execution.", "type": "object", @@ -41,6 +41,48 @@ }, "retry_policy": {"type": "string", "enum": ["delivery_only", "new_proposal", "never"]}, "emitted_at": {"type": "string", "format": "date-time"}, + "agent_feedback": { + "description": "Structured guidance returned to the proposing agent with a refusal. It informs a new proposal; it never reopens this decision.", + "type": "object", + "required": ["reason_code", "message"], + "additionalProperties": false, + "properties": { + "reason_code": { + "type": "string", + "enum": [ + "policy_violation", + "incorrect_content", + "wrong_target", + "needs_information", + "out_of_scope", + "too_risky", + "timing", + "other" + ] + }, + "message": {"$ref": "common.schema.json#/$defs/safe_text", "maxLength": 1000}, + "requested_changes": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "object", + "required": ["path", "note"], + "additionalProperties": false, + "properties": { + "path": {"type": "string", "pattern": "^/payload(/([^~/]|~[01])*)*$", "maxLength": 512}, + "note": {"$ref": "common.schema.json#/$defs/safe_text", "maxLength": 500} + } + } + }, + "questions": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": {"$ref": "common.schema.json#/$defs/safe_text", "maxLength": 500} + } + } + }, "delivery": { "type": "object", "required": ["target_hash", "attempt", "status", "last_attempt_at"], @@ -86,6 +128,26 @@ } }, "allOf": [ + { + "if": {"required": ["agent_feedback"]}, + "then": {"properties": {"decision": {"enum": ["rejected", "expired", "cancelled"]}}} + }, + { + "if": { + "properties": { + "agent_feedback": { + "anyOf": [{"required": ["requested_changes"]}, {"required": ["questions"]}] + } + }, + "required": ["agent_feedback"] + }, + "then": { + "properties": { + "disposition": {"const": "revise_and_resubmit"}, + "retry_policy": {"const": "new_proposal"} + } + } + }, { "if": {"properties": {"decision": {"enum": ["approved", "auto_approved"]}}, "required": ["decision"]}, "then": { diff --git a/schemas/review-snapshot.schema.json b/schemas/review-snapshot.schema.json index e1ecf87..5c10020 100644 --- a/schemas/review-snapshot.schema.json +++ b/schemas/review-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/review-snapshot.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/review-snapshot.schema.json", "title": "ReviewSnapshot", "description": "The exact decision material presented to an approver, bound to the effective action and payload by canonical hashes.", "type": "object", diff --git a/schemas/validation-record.schema.json b/schemas/validation-record.schema.json index 8833c94..14b1f72 100644 --- a/schemas/validation-record.schema.json +++ b/schemas/validation-record.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.0.0/schemas/validation-record.schema.json", + "$id": "https://raw.githubusercontent.com/renezander030/agent-approval-gate/v3.1.0/schemas/validation-record.schema.json", "title": "ValidationRecord", "description": "Immutable pre-admission evidence that an exact proposed action passed structural, payload, semantic, policy-input, and authorization-input checks before a human was notified.", "type": "object", diff --git a/scripts/generate_examples.py b/scripts/generate_examples.py index fb0310f..62ed5b3 100644 --- a/scripts/generate_examples.py +++ b/scripts/generate_examples.py @@ -93,7 +93,12 @@ def lifecycle_examples(base: dict[str, Any]) -> None: approval = rejected["approvals"][0] approval.update(decision="rejected", rejection_settlement={"state": "unresolved"}) resolution = rejected["resolutions"][0] - resolution.update(decision="rejected", disposition="do_not_execute", retry_policy="never", approval_record_hash=digest_value(approval)) + resolution.update(decision="rejected", disposition="revise_and_resubmit", retry_policy="new_proposal", approval_record_hash=digest_value(approval)) + resolution["agent_feedback"] = { + "reason_code": "incorrect_content", + "message": "The reply promises a restore date that engineering has not confirmed.", + "requested_changes": [{"path": "/payload/body_text", "note": "Remove the restore date and link the status page instead."}], + } rejected["authority_snapshots"] = [] dispatch = rejected["dispatches"][0] dispatch.update(status="not_dispatched", approval_ids=[], reason="approval_rejected") @@ -103,6 +108,8 @@ def lifecycle_examples(base: dict[str, Any]) -> None: for event in rejected["audit_events"]: if event["event_type"] in {"approval.decided", "resolution.emitted"}: event["detail"]["decision"] = "rejected" + if event["event_type"] == "resolution.emitted": + event["detail"]["reason_code"] = resolution["agent_feedback"]["reason_code"] if event["event_type"] == "dispatch.succeeded": event["event_type"] = "dispatch.not_dispatched" event["detail"] = {"action_hash": dispatch["action_hash"], "reason_code": "approval_rejected"} @@ -125,6 +132,33 @@ def lifecycle_examples(base: dict[str, Any]) -> None: refresh_audit(expired) write("expired-envelope.json", expired) + revoked = deepcopy(base) + revoked_at = "2026-04-28T09:14:34Z" + request = revoked["request"] + request.update(status="revoked", revision=3, terminal_at=revoked_at, terminal_reason="approval_revoked") + request.pop("consumed_at") + request.pop("dispatch_id") + revoker = deepcopy(revoked["approvals"][0]["decided_by"]) + request["revocation"] = {"revoked_at": revoked_at, "revoked_by": revoker, "reason_code": "target_changed", "safe_detail": "Customer replied on another thread before dispatch"} + revoked["authority_snapshots"] = [] + dispatch = revoked["dispatches"][0] + dispatch.update(status="not_dispatched", approval_ids=[], reason="approval_revoked", started_at="2026-04-28T09:14:35Z", completed_at="2026-04-28T09:14:35Z", recorded_at="2026-04-28T09:14:35Z") + dispatch.pop("authority_snapshot_id") + dispatch.pop("provider") + events = [e for e in revoked["audit_events"] if e["event_type"] != "authority.checked"] + revocation_event = deepcopy(next(e for e in events if e["event_type"] == "approval.decided")) + revocation_event.update(event_id="event_revoked_01", event_type="approval.revoked", occurred_at=revoked_at, recorded_at=revoked_at, actor=deepcopy(revoker), references={"request_id": request["request_id"]}, detail={"reason_code": "target_changed"}) + for event in events: + if event["event_type"] == "dispatch.succeeded": + event.update(event_type="dispatch.not_dispatched", occurred_at=dispatch["completed_at"], recorded_at=dispatch["recorded_at"]) + event["references"].pop("approval_id") + event["detail"] = {"action_hash": dispatch["action_hash"], "reason_code": "approval_revoked"} + index = next(i for i, e in enumerate(events) if e["event_type"] == "dispatch.not_dispatched") + events.insert(index, revocation_event) + revoked["audit_events"] = events + refresh_audit(revoked) + write("revoked-envelope.json", revoked) + recovered = deepcopy(base) first = recovered["dispatches"][0] first.update(status="outcome_unknown", reason="provider_response_lost", reconciliation={"lookup_key_hash": digest_value("provider-lookup-01"), "status": "pending"}) @@ -311,7 +345,7 @@ def main() -> None: write("audit-snapshot.json", audit_snapshot) envelope = { - "schema_version": "3.0.0", + "schema_version": "3.1.0", "captured_at": "2026-04-28T09:14:35Z", "proposal": proposal, "validations": [validation], diff --git a/scripts/validate_contracts.py b/scripts/validate_contracts.py index 118df8d..3627166 100644 --- a/scripts/validate_contracts.py +++ b/scripts/validate_contracts.py @@ -356,15 +356,36 @@ def canonicalization_errors() -> list[str]: return errors -def payload_errors(action: dict[str, Any], schema_files: dict[str, Path] | None = None) -> list[str]: - """Use locally supplied released bytes; never resolve an agent-selected URL.""" - if "payload" not in action: - return ["effective action has no payload"] +def payload_schema_files(schema_files: dict[str, Path] | None = None) -> dict[str, Path]: files = { load(path)["$id"]: path for path in (ROOT / "schemas" / "actions").glob("*.json") } files.update(schema_files or {}) + return files + + +def review_target_fields( + action: dict[str, Any], schema_files: dict[str, Path] | None = None +) -> list[str] | None: + """Payload members the approver must see as the target, from the trusted schema.""" + path = payload_schema_files(schema_files).get(action["payload_schema"]["id"]) + if path is None: + return None + try: + fields = load(path).get("x-review-target") + except (OSError, ValueError): + return None + if not isinstance(fields, list) or not all(isinstance(item, str) for item in fields): + return None + return fields + + +def payload_errors(action: dict[str, Any], schema_files: dict[str, Path] | None = None) -> list[str]: + """Use locally supplied released bytes; never resolve an agent-selected URL.""" + if "payload" not in action: + return ["effective action has no payload"] + files = payload_schema_files(schema_files) binding = action["payload_schema"] path = files.get(binding["id"]) if path is None: @@ -501,6 +522,10 @@ def audit_binding_errors(envelope: dict[str, Any]) -> list[str]: errors.append("audit validation outcome disagrees with validation record") if event_type == "validation.rejected" and primary["outcome"] == "passed": errors.append("audit validation outcome disagrees with validation record") + if event_type == "resolution.emitted" and "reason_code" in detail: + feedback = primary.get("agent_feedback") + if not feedback or feedback["reason_code"] != detail["reason_code"]: + errors.append("audit resolution reason disagrees with agent feedback") if event_type == "review.presented" and "review_content_hash" in detail and detail["review_content_hash"] != primary["content_hash"]: errors.append("audit review content hash disagrees with snapshot") if event_type == "approval.decided": @@ -534,6 +559,59 @@ def audit_binding_errors(envelope: dict[str, Any]) -> list[str]: return errors +def policy_outcome_errors( + request: dict[str, Any], approvals: list[dict[str, Any]], dispatches: list[dict[str, Any]] +) -> list[str]: + """Only an explicit, closed policy outcome may select an approval path.""" + errors: list[str] = [] + outcome = request["policy"].get("outcome") + auto_approvals = [item for item in approvals if item["decision"] == "auto_approved"] + if auto_approvals and outcome != "auto_approve": + errors.append("auto approval requires an explicit auto_approve policy outcome") + if outcome == "deny": + if request["status"] != "failed" or request.get("terminal_reason") != "policy_failed": + errors.append("denied policy outcome must end the request as policy_failed") + if any(item["decision"] in {"approved", "auto_approved"} for item in approvals): + errors.append("denied policy outcome cannot carry an approving decision") + if any(item["status"] != "not_dispatched" for item in dispatches): + errors.append("denied policy outcome cannot invoke a provider") + return errors + + +def revocation_errors( + request: dict[str, Any], decision: dict[str, Any] | None, events: list[dict[str, Any]] +) -> list[str]: + """A revocation withdraws a recorded approval before consumption, never after.""" + errors: list[str] = [] + revocation = request["revocation"] + revoked_at = parse_time(revocation["revoked_at"]) + if not decision or decision["decision"] not in {"approved", "auto_approved"}: + errors.append("revoked request lacks an approving decision") + else: + if revoked_at < parse_time(decision["recorded_at"]): + errors.append("revocation predates the approval it withdraws") + if request["revision"] < decision["request_revision"] + 2: + errors.append("revoked request lacks decision and revocation revisions") + if revoked_at >= parse_time(request["expires_at"]): + errors.append("revocation occurred at or after request expiry") + if parse_time(request["terminal_at"]) != revoked_at: + errors.append("revoked request terminal_at disagrees with revocation time") + witnesses = [event for event in events if event["event_type"] == "approval.revoked"] + if len(witnesses) > 1: + errors.append("audit stream records more than one revocation") + for event in witnesses: + if event["references"].get("request_id") != request["request_id"]: + errors.append("audit approval.revoked lacks its request_id witness") + if parse_time(event["occurred_at"]) != revoked_at: + errors.append("audit revocation time disagrees with request") + actor = revocation["revoked_by"] + if (event["actor"]["kind"], event["actor"]["identifier"]) != (actor["kind"], actor["identifier"]): + errors.append("audit revocation actor disagrees with request") + if event["detail"].get("reason_code", revocation["reason_code"]) != revocation["reason_code"]: + errors.append("audit revocation reason disagrees with request") + return errors + + def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | None = None) -> list[str]: errors: list[str] = [] proposal = envelope["proposal"] @@ -557,7 +635,7 @@ def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | No invokes_provider = any(item["status"] in invocation_statuses for item in dispatches) if invokes_provider and request["status"] != "consumed": errors.append("provider invocation requires a consumed request") - if request["status"] in {"rejected", "expired", "cancelled", "failed"} and invokes_provider: + if request["status"] in {"rejected", "expired", "cancelled", "failed", "revoked"} and invokes_provider: errors.append("refused request cannot invoke a provider") initial_action_hash = digest(proposal) @@ -666,15 +744,15 @@ def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | No errors.append(f"review snapshot {snapshot['snapshot_id']} predates proposal") if rendered_at > request_expires_at: errors.append(f"review snapshot {snapshot['snapshot_id']} was rendered after expiry") - if proposal["action_type"] == "email.send": + target_fields = review_target_fields(proposal, schema_files) + arguments = snapshot["content"]["arguments"] + if target_fields is not None and isinstance(arguments, dict): expected_target = { - key: snapshot["content"]["arguments"][key] - for key in ("from", "to", "cc") - if key in snapshot["content"]["arguments"] + key: arguments[key] for key in target_fields if key in arguments } if snapshot["content"]["target"] != expected_target: errors.append( - f"review snapshot {snapshot['snapshot_id']} changes the email target" + f"review snapshot {snapshot['snapshot_id']} changes the {proposal['action_type']} target" ) initial_snapshot = snapshot_by_id.get(request["review_snapshot_id"]) @@ -768,6 +846,7 @@ def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | No errors.append(f"approval {approval['approval_id']} was decided after expiry") requirements = request["policy"]["requirements"] + errors.extend(policy_outcome_errors(request, approvals, dispatches)) if request["status"] in {"approved", "consumed"} and len(approved_records) < requirements["minimum_approvals"]: errors.append("approval quorum is not satisfied") if requirements["distinct_approvers"]: @@ -790,6 +869,12 @@ def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | No hop["agent"]["identity"] for hop in proposal["delegation_chain"] } observed_signing_keys: set[str] = set() + delivered_channels = { + item["channel"] for item in request["channels"] if item["delivery_status"] == "delivered" + } + for approval in approvals: + if approval["channel"] == "mcp_elicitation" and "mcp_elicitation" not in delivered_channels: + errors.append(f"approval {approval['approval_id']} answers an elicitation that was never delivered") for approval in approved_records: approver_id = approval["decided_by"]["identifier"] if approval["channel"] not in allowed_channels: @@ -1031,6 +1116,8 @@ def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | No errors.append("rejected request lacks a rejecting decision") if request["status"] == "expired" and terminal_at and terminal_at < request_expires_at: errors.append("expired request terminated before its expiry") + if request["status"] == "revoked": + errors.extend(revocation_errors(request, decision_record, events)) if request["status"] == "consumed": if request["decision_record_id"] not in approval_ids: errors.append("consumed request does not name an approving decision") @@ -1124,6 +1211,8 @@ def semantic_errors(envelope: dict[str, Any], schema_files: dict[str, Path] | No required_event_types.add("approval.expired") if request["status"] == "cancelled": required_event_types.add("approval.cancelled") + if request["status"] == "revoked": + required_event_types.add("approval.revoked") missing_event_types = sorted(required_event_types - event_types) if missing_event_types: errors.append( @@ -1265,7 +1354,7 @@ def validate_repository() -> list[str]: if "/main/" in identifier: failures.append(f"{path}: $id points at mutable main") - for corpus_path in ("tests/conformance.json", "tests/adversarial.json", "tests/hardening.json"): + for corpus_path in ("tests/conformance.json", "tests/adversarial.json", "tests/hardening.json", "tests/release-3.1.json"): corpus = load(corpus_path) for vector in corpus["positive"]: validator = validator_for(vector["schema"], schemas, registry) @@ -1328,21 +1417,205 @@ def validate_repository() -> list[str]: return failures -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--quiet", action="store_true") - args = parser.parse_args() +RECORD_SCHEMAS = { + path.name.removesuffix(".schema.json"): f"schemas/{path.name}" + for path in sorted((ROOT / "schemas").glob("*.schema.json")) + if path.name not in {"common.schema.json", "approval-envelope.schema.json"} +} + + +def record_errors(kind: str, record: Any) -> list[str]: + """Structural validation of one standalone record, plus its own proof when present.""" + schemas, registry = schema_catalog() + validator = validator_for(RECORD_SCHEMAS[kind], schemas, registry) + errors = [] + for error in sorted(validator.iter_errors(record), key=lambda item: list(item.path)): + location = "/".join(str(part) for part in error.path) or "" + errors.append(f"{location}: {error.message}") + if not errors and kind == "approval-record": + errors.extend(webauthn_signature_errors(record)) + return errors + + +def audit_export_errors( + events: Any, snapshot: Any, anchor: str | None = None +) -> list[str]: + """Verify an exported event range against its AuditSnapshot without the lifecycle records.""" + schemas, registry = schema_catalog() + if not isinstance(events, list) or not events: + return ["audit export must be a non-empty event array"] + errors = [ + f"audit snapshot: {error.message}" + for error in validator_for("schemas/audit-snapshot.schema.json", schemas, registry).iter_errors(snapshot) + ] + event_validator = validator_for("schemas/audit-event.schema.json", schemas, registry) + for index, event in enumerate(events): + if list(event_validator.iter_errors(event)): + errors.append(f"audit event {index} fails structural validation") + if errors: + return errors + first = events[0] + if first["sequence"] == 1: + if first["previous_event_hash"] is not None: + errors.append("first audit event of a stream cannot have a previous hash") + elif first["previous_event_hash"] is None: + errors.append("partial audit export does not name the preceding event hash") + if anchor is not None and first["previous_event_hash"] != anchor: + errors.append("audit export does not continue from the retained checkpoint") + expected_previous = first["previous_event_hash"] + previous_recorded_at: datetime | None = None + event_ids: set[str] = set() + for offset, event in enumerate(events): + if event["event_id"] in event_ids: + errors.append("audit events reuse event_id") + event_ids.add(event["event_id"]) + if event["stream_id"] != snapshot["stream_id"]: + errors.append(f"audit event {event['event_id']} belongs to another stream") + if event["sequence"] != first["sequence"] + offset: + errors.append("audit sequence is not contiguous") + if event["previous_event_hash"] != expected_previous: + errors.append(f"audit event {event['event_id']} has a broken previous hash") + if event["event_hash"] != digest({key: value for key, value in event.items() if key != "event_hash"}): + errors.append(f"audit event {event['event_id']} hash mismatch") + recorded_at = parse_time(event["recorded_at"]) + if recorded_at < parse_time(event["occurred_at"]): + errors.append(f"audit event {event['event_id']} was recorded before it occurred") + if previous_recorded_at and recorded_at < previous_recorded_at: + errors.append("audit event recording times are not monotonic") + expected_previous = event["event_hash"] + previous_recorded_at = recorded_at + if snapshot["entry_count"] != len(events): + errors.append("audit snapshot entry_count is incomplete") + if snapshot["first_sequence"] != first["sequence"]: + errors.append("audit snapshot first_sequence mismatch") + if snapshot["last_sequence"] != events[-1]["sequence"]: + errors.append("audit snapshot last_sequence mismatch") + if snapshot["events_digest"] != digest(events): + errors.append("audit snapshot events_digest mismatch") + if snapshot["root_event_hash"] != events[-1]["event_hash"]: + errors.append("audit snapshot root_event_hash mismatch") + if parse_time(snapshot["captured_at"]) < parse_time(events[-1]["recorded_at"]): + errors.append("audit snapshot predates its final event") + return errors + + +def collection_errors(envelopes: list[dict[str, Any]]) -> list[str]: + """Identities that must stay unique across every lifecycle a deployment admitted.""" + errors: list[str] = [] + seen: dict[str, dict[Any, int]] = { + "proposal_id": {}, "request_id": {}, "call": {}, "dispatch_id": {}, "audit_stream": {}, + } + idempotency: dict[tuple[str, str], tuple[int, str]] = {} + + def claim(kind: str, key: Any, index: int, message: str) -> None: + if key in seen[kind] and seen[kind][key] != index: + errors.append(f"envelopes {seen[kind][key]} and {index}: {message}") + seen[kind].setdefault(key, index) + + for index, envelope in enumerate(envelopes): + proposal, request = envelope["proposal"], envelope["request"] + claim("proposal_id", proposal["proposal_id"], index, "reuse proposal_id") + claim("request_id", request["request_id"], index, "reuse request_id") + claim("call", (proposal["tenant"], proposal["call_id"]), index, "admit more than one approval request for one call") + claim("audit_stream", envelope["audit_snapshot"]["stream_id"], index, "share one audit stream") + for dispatch in envelope["dispatches"]: + claim("dispatch_id", dispatch["dispatch_id"], index, "reuse dispatch_id") + key = (proposal["tenant"], proposal["idempotency_key"]) + action_hash = digest(proposal) + if key in idempotency and idempotency[key][1] != action_hash: + errors.append(f"envelopes {idempotency[key][0]} and {index}: idempotency_key binds two different actions") + idempotency.setdefault(key, (index, action_hash)) + return errors + + +def read_json_input(path: str) -> Any: + text = Path(path).read_text(encoding="utf-8") + if path.endswith(".jsonl"): + return [strict_json_loads(line) for line in text.splitlines() if line.strip()] + return strict_json_loads(text) + + +def trusted_schema_files(paths: list[str]) -> dict[str, Path]: + files: dict[str, Path] = {} + for path in paths: + schema = read_json_input(path) + if not isinstance(schema, dict) or not isinstance(schema.get("$id"), str): + raise ValueError(f"{path}: payload schema has no $id") + files[schema["$id"]] = Path(path).resolve() + return files + + +def validate_inputs(args: argparse.Namespace) -> list[tuple[str, list[str]]]: + results: list[tuple[str, list[str]]] = [] + schema_files = trusted_schema_files(args.schema_file) + envelopes: list[dict[str, Any]] = [] + for path in args.envelope: + envelope = read_json_input(path) + errors = validate_envelope(envelope, schema_files) + results.append((path, errors)) + if not errors: + envelopes.append(envelope) + if len(args.envelope) > 1: + results.append(("", collection_errors(envelopes))) + for kind, path in args.record: + if kind not in RECORD_SCHEMAS: + raise ValueError(f"unknown record kind {kind}; expected one of: {', '.join(RECORD_SCHEMAS)}") + results.append((path, record_errors(kind, read_json_input(path)))) + if args.audit_events or args.audit_snapshot: + if not (args.audit_events and args.audit_snapshot): + raise ValueError("--audit-events and --audit-snapshot must be given together") + results.append(( + args.audit_events, + audit_export_errors(read_json_input(args.audit_events), read_json_input(args.audit_snapshot), args.anchor), + )) + return results + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Validate this repository's contracts, or your own approval records against them.", + ) + parser.add_argument("--quiet", action="store_true", help="print nothing on success") + parser.add_argument("--json", action="store_true", help="print one JSON result object") + parser.add_argument("--envelope", action="append", default=[], metavar="FILE", + help="complete approval envelope; repeat to also check identities across envelopes") + parser.add_argument("--record", action="append", nargs=2, default=[], metavar=("KIND", "FILE"), + help="one standalone record, e.g. approval-record path.json") + parser.add_argument("--schema-file", action="append", default=[], metavar="FILE", + help="trusted local payload schema for a custom action type") + parser.add_argument("--audit-events", metavar="FILE", help="exported audit events (.json array or .jsonl)") + parser.add_argument("--audit-snapshot", metavar="FILE", help="AuditSnapshot manifest for the exported events") + parser.add_argument("--anchor", metavar="SHA256", help="retained hash of the event preceding a partial export") + args = parser.parse_args(argv) + user_mode = bool(args.envelope or args.record or args.audit_events or args.audit_snapshot) try: - failures = validate_repository() - except (ValueError, TypeError, KeyError, IndexError, rfc8785.CanonicalizationError): - failures = ["contract input could not be validated"] - if failures: - for failure in failures: - print(f"FAIL: {failure}") - return 1 - if not args.quiet: - print("All schemas, examples, negative vectors, and lifecycle invariants passed.") - return 0 + results = validate_inputs(args) if user_mode else [("repository", validate_repository())] + except (OSError, ValueError) as error: + message = str(error) if user_mode else "contract input could not be validated" + if args.json: + print(json.dumps({"ok": False, "error": message})) + else: + print(f"ERROR: {message}") + return 2 + except (TypeError, KeyError, IndexError, rfc8785.CanonicalizationError): + if args.json: + print(json.dumps({"ok": False, "error": "contract input could not be validated"})) + else: + print("ERROR: contract input could not be validated") + return 2 + ok = not any(errors for _, errors in results) + if args.json: + print(json.dumps({"ok": ok, "results": [{"input": name, "errors": errors} for name, errors in results]}, indent=2)) + else: + for name, errors in results: + for error in errors: + print(f"FAIL: {error}" if name == "repository" else f"FAIL: {name}: {error}") + if ok and not args.quiet: + print( + "All schemas, examples, negative vectors, and lifecycle invariants passed." + if not user_mode else f"All {len(results)} inputs passed." + ) + return 0 if ok else 1 if __name__ == "__main__": diff --git a/tests/release-3.1.json b/tests/release-3.1.json new file mode 100644 index 0000000..ce78f5c --- /dev/null +++ b/tests/release-3.1.json @@ -0,0 +1,204 @@ +{ + "positive": [ + { + "name": "revoked lifecycle", + "schema": "schemas/approval-envelope.schema.json", + "instance": "examples/revoked-envelope.json", + "semantic": true + }, + { + "name": "rejection with agent feedback", + "schema": "schemas/approval-envelope.schema.json", + "instance": "examples/rejected-envelope.json", + "semantic": true + }, + { + "name": "crm.update_record reference payload", + "schema": "schemas/actions/crm.update_record.schema.json", + "instance": "examples/actions/crm.update_record.json" + }, + { + "name": "ticket.create reference payload", + "schema": "schemas/actions/ticket.create.schema.json", + "instance": "examples/actions/ticket.create.json" + }, + { + "name": "db.update_row reference payload", + "schema": "schemas/actions/db.update_row.schema.json", + "instance": "examples/actions/db.update_row.json" + }, + { + "name": "api.call reference payload", + "schema": "schemas/actions/api.call.schema.json", + "instance": "examples/actions/api.call.json" + }, + { + "name": "n8n.trigger_workflow reference payload", + "schema": "schemas/actions/n8n.trigger_workflow.schema.json", + "instance": "examples/actions/n8n.trigger_workflow.json" + } + ], + "negative": [ + { + "name": "policy outcome outside the closed set", + "schema": "schemas/approval-policy.schema.json", + "instance": "examples/approval-policy.json", + "expect": "schema", + "operations": [ + { + "op": "replace", + "path": "/outcome", + "value": "no_opinion" + } + ] + }, + { + "name": "auto-approve outcome under a policy that forbids auto approval", + "schema": "schemas/approval-policy.schema.json", + "instance": "examples/approval-policy.json", + "expect": "schema", + "operations": [ + { + "op": "replace", + "path": "/outcome", + "value": "auto_approve" + } + ] + }, + { + "name": "denied policy outcome reaches dispatch", + "schema": "schemas/approval-envelope.schema.json", + "instance": "examples/approval-envelope.json", + "expect": "semantic", + "expect_error": "denied policy outcome cannot invoke a provider", + "operations": [ + { + "op": "replace", + "path": "/request/policy/outcome", + "value": "deny" + } + ] + }, + { + "name": "revocation after expiry", + "schema": "schemas/approval-envelope.schema.json", + "instance": "examples/revoked-envelope.json", + "expect": "semantic", + "expect_error": "revocation occurred at or after request expiry", + "operations": [ + { + "op": "replace", + "path": "/request/revocation/revoked_at", + "value": "2026-04-28T17:14:23Z" + }, + { + "op": "replace", + "path": "/request/terminal_at", + "value": "2026-04-28T17:14:23Z" + } + ] + }, + { + "name": "consumed request carries a revocation", + "schema": "schemas/approval-request.schema.json", + "instance": "examples/approval-request.json", + "expect": "schema", + "operations": [ + { + "op": "add", + "path": "/revocation", + "value": { + "revoked_at": "2026-04-28T09:14:34Z", + "revoked_by": { + "kind": "human", + "identifier": "rene@acme-corp.example", + "authentication": { + "method": "signed_callback", + "human_present": true, + "assurance": "substantial" + } + }, + "reason_code": "approver_withdrew" + } + } + ] + }, + { + "name": "agent feedback asks to change a field outside the payload", + "schema": "schemas/approval-envelope.schema.json", + "instance": "examples/rejected-envelope.json", + "expect": "schema", + "operations": [ + { + "op": "replace", + "path": "/resolutions/0/agent_feedback/requested_changes/0/path", + "value": "/tenant" + } + ] + }, + { + "name": "agent feedback on an approval", + "schema": "schemas/resolution-record.schema.json", + "instance": "examples/resolution-record.json", + "expect": "schema", + "operations": [ + { + "op": "add", + "path": "/agent_feedback", + "value": { + "reason_code": "other", + "message": "Fine" + } + } + ] + }, + { + "name": "elicitation rejection by a client without a present human", + "schema": "schemas/approval-record.schema.json", + "instance": "examples/rejected-with-settlement-approval-record.json", + "expect": "schema", + "operations": [ + { + "op": "replace", + "path": "/channel", + "value": "mcp_elicitation" + }, + { + "op": "replace", + "path": "/decided_by/kind", + "value": "system" + }, + { + "op": "replace", + "path": "/decided_by/authentication/human_present", + "value": false + } + ] + }, + { + "name": "crm update without an expected record version", + "schema": "schemas/actions/crm.update_record.schema.json", + "instance": "examples/actions/crm.update_record.json", + "expect": "schema", + "operations": [ + { + "op": "remove", + "path": "/expected_version" + } + ] + }, + { + "name": "api call names a raw URL", + "schema": "schemas/actions/api.call.schema.json", + "instance": "examples/actions/api.call.json", + "expect": "schema", + "operations": [ + { + "op": "add", + "path": "/url", + "value": "https://attacker.example/refund" + } + ] + } + ] +} diff --git a/tests/test_lifecycle_hardening.py b/tests/test_lifecycle_hardening.py index 0c62d0c..eb1ad33 100644 --- a/tests/test_lifecycle_hardening.py +++ b/tests/test_lifecycle_hardening.py @@ -27,7 +27,7 @@ def assert_refused(self, envelope: dict, reason: str) -> None: self.assertTrue(any(reason in error for error in errors), errors) def test_positive_terminal_and_recovery_lifecycles(self) -> None: - for name in ("approval", "edited", "rejected", "expired", "recovered"): + for name in ("approval", "edited", "rejected", "expired", "recovered", "revoked"): with self.subTest(name=name): self.assertEqual(validate_envelope(load(f"examples/{name}-envelope.json")), []) @@ -351,7 +351,7 @@ def test_invalid_patch_is_a_refusal_and_does_not_mutate_original(self) -> None: self.assertEqual(self.envelope["proposal"], original) def test_malformed_envelope_fails_closed(self) -> None: - for value in (None, [], {}, {"proposal":{}}, {"schema_version":"3.0.0"}): + for value in (None, [], {}, {"proposal":{}}, {"schema_version":"3.1.0"}): self.assertEqual(validate_envelope(value), ["envelope fails structural validation"]) diff --git a/tests/test_release_3_1.py b/tests/test_release_3_1.py new file mode 100644 index 0000000..03e5f2b --- /dev/null +++ b/tests/test_release_3_1.py @@ -0,0 +1,436 @@ +from __future__ import annotations + +import contextlib +import hashlib +import io +import json +import sys +import tempfile +import unittest +from copy import deepcopy +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +from generate_examples import refresh_audit # noqa: E402 +from validate_contracts import ( # noqa: E402 + ROOT as REPO, audit_export_errors, collection_errors, digest, load, main, payload_errors, + policy_outcome_errors, schema_catalog, validate_envelope, validator_for, +) + +ACTION_TYPES = ("crm.update_record", "ticket.create", "db.update_row", "api.call", "n8n.trigger_workflow") + + +def rebind(envelope: dict, action_type: str, payload: dict) -> dict: + """Move the reference lifecycle onto another action type and recompute every binding.""" + envelope = deepcopy(envelope) + proposal = envelope["proposal"] + schema_path = REPO / "schemas" / "actions" / f"{action_type}.schema.json" + schema = load(schema_path) + proposal.update(action_type=action_type, payload=deepcopy(payload)) + proposal["payload_schema"] = { + "id": schema["$id"], + "version": envelope["schema_version"], + "digest": "sha256:" + hashlib.sha256(schema_path.read_bytes()).hexdigest(), + } + action_hash, payload_hash = digest(proposal), digest(payload) + envelope["validations"][0]["action_hash"] = action_hash + envelope["request"]["action_hash"] = action_hash + envelope["request"]["policy"]["material_input_hash"] = action_hash + review = envelope["review_snapshots"][0] + review["content"].update( + action_type=action_type, arguments=deepcopy(payload), + target={key: payload[key] for key in schema["x-review-target"] if key in payload}, + ) + review.update(action_hash=action_hash, payload_hash=payload_hash, content_hash=digest(review["content"])) + approval = envelope["approvals"][0] + approval.update(action_hash=action_hash, payload_hash=payload_hash, review_content_hash=review["content_hash"]) + envelope["resolutions"][0].update(action_hash=action_hash, approval_record_hash=digest(approval)) + authority = envelope["authority_snapshots"][0] + authority["action_hash"] = action_hash + for subject in authority["subjects"]: + subject["scopes"] = [action_type if scope == "email.send" else scope for scope in subject.get("scopes", [])] + authority["authority_digest"] = digest(authority["subjects"]) + envelope["dispatches"][0]["action_hash"] = action_hash + for event in envelope["audit_events"]: + if "action_hash" in event["detail"]: + event["detail"]["action_hash"] = action_hash + if "payload_hash" in event["detail"]: + event["detail"]["payload_hash"] = payload_hash + if "review_content_hash" in event["detail"]: + event["detail"]["review_content_hash"] = review["content_hash"] + refresh_audit(envelope) + return envelope + + +def schema_errors(schema: str, instance: object) -> list[str]: + schemas, registry = schema_catalog() + return [error.message for error in validator_for(schema, schemas, registry).iter_errors(instance)] + + +class PolicyOutcomeTests(unittest.TestCase): + def setUp(self) -> None: + self.envelope = load("examples/approval-envelope.json") + + def test_outcome_is_a_closed_set(self) -> None: + policy = load("examples/approval-policy.json") + for value in ("allow", "true", "", "no_opinion"): + with self.subTest(value=value): + policy["outcome"] = value + self.assertTrue(schema_errors("schemas/approval-policy.schema.json", policy)) + + def test_auto_approve_outcome_requires_auto_approval_policy(self) -> None: + policy = load("examples/approval-policy.json") + policy["outcome"] = "auto_approve" + self.assertTrue(schema_errors("schemas/approval-policy.schema.json", policy)) + policy["requirements"]["allow_auto_approval"] = True + self.assertEqual(schema_errors("schemas/approval-policy.schema.json", policy), []) + + def test_auto_approval_needs_explicit_outcome(self) -> None: + request = deepcopy(self.envelope["request"]) + approval = {"decision": "auto_approved"} + for outcome in (None, "require_approval", "deny"): + with self.subTest(outcome=outcome): + request["policy"].pop("outcome", None) + if outcome: + request["policy"]["outcome"] = outcome + errors = policy_outcome_errors(request, [approval], []) + self.assertIn("auto approval requires an explicit auto_approve policy outcome", errors) + request["policy"]["outcome"] = "auto_approve" + self.assertEqual(policy_outcome_errors(request, [approval], []), []) + + def test_denied_outcome_cannot_reach_dispatch(self) -> None: + envelope = self.envelope + envelope["request"]["policy"]["outcome"] = "deny" + refresh_audit(envelope) + errors = validate_envelope(envelope) + for reason in ( + "denied policy outcome must end the request as policy_failed", + "denied policy outcome cannot carry an approving decision", + "denied policy outcome cannot invoke a provider", + ): + self.assertIn(reason, errors) + + +class ActionCatalogTests(unittest.TestCase): + def setUp(self) -> None: + self.envelope = load("examples/approval-envelope.json") + + def test_every_built_in_action_type_has_a_reference_schema(self) -> None: + enum = load("schemas/proposed-action.schema.json")["properties"]["action_type"]["enum"] + for action_type in enum: + with self.subTest(action_type=action_type): + schema = load(f"schemas/actions/{action_type}.schema.json") + self.assertEqual(schema["x-action-type"], action_type) + self.assertFalse(schema["additionalProperties"]) + self.assertTrue(set(schema["x-review-target"]) <= set(schema["properties"])) + + def test_complete_lifecycle_for_each_action_type(self) -> None: + for action_type in ACTION_TYPES: + with self.subTest(action_type=action_type): + payload = load(f"examples/actions/{action_type}.json") + self.assertEqual(validate_envelope(rebind(self.envelope, action_type, payload)), []) + + def test_unknown_payload_members_are_refused(self) -> None: + for action_type in ACTION_TYPES: + with self.subTest(action_type=action_type): + payload = load(f"examples/actions/{action_type}.json") + payload["callback_url"] = "https://attacker.example/hook" + envelope = rebind(self.envelope, action_type, load(f"examples/actions/{action_type}.json")) + envelope["proposal"]["payload"] = payload + self.assertIn("effective payload fails its action schema", payload_errors(envelope["proposal"])) + + def test_review_target_must_match_the_arguments(self) -> None: + for action_type in ACTION_TYPES + ("email.send",): + with self.subTest(action_type=action_type): + if action_type == "email.send": + envelope = deepcopy(self.envelope) + else: + envelope = rebind(self.envelope, action_type, load(f"examples/actions/{action_type}.json")) + review = envelope["review_snapshots"][0] + field = next(iter(review["content"]["target"])) + review["content"]["target"][field] = "something-else" + review["content_hash"] = digest(review["content"]) + errors = validate_envelope(envelope) + self.assertIn(f"review snapshot {review['snapshot_id']} changes the {action_type} target", errors) + + +class RevocationTests(unittest.TestCase): + def setUp(self) -> None: + self.envelope = load("examples/revoked-envelope.json") + + def refused(self, envelope: dict, reason: str) -> None: + refresh_audit(envelope) + errors = validate_envelope(envelope) + self.assertTrue(any(reason in error for error in errors), errors) + + def test_revoked_lifecycle_is_complete(self) -> None: + self.assertEqual(validate_envelope(self.envelope), []) + + def test_consumed_request_cannot_be_revoked(self) -> None: + request = self.envelope["request"] + request["consumed_at"] = request["terminal_at"] + request["dispatch_id"] = self.envelope["dispatches"][0]["dispatch_id"] + self.assertTrue(schema_errors("schemas/approval-request.schema.json", request)) + + def test_revocation_only_on_revoked_requests(self) -> None: + request = load("examples/approval-envelope.json")["request"] + request["revocation"] = deepcopy(self.envelope["request"]["revocation"]) + self.assertTrue(schema_errors("schemas/approval-request.schema.json", request)) + + def test_revocation_needs_an_approving_decision(self) -> None: + approval = self.envelope["approvals"][0] + approval.update(decision="rejected", rejection_settlement={"state": "unresolved"}) + self.envelope["resolutions"][0].update(decision="rejected", disposition="do_not_execute", retry_policy="never", approval_record_hash=digest(approval)) + for event in self.envelope["audit_events"]: + if event["event_type"] in {"approval.decided", "resolution.emitted"}: + event["detail"]["decision"] = "rejected" + self.refused(self.envelope, "revoked request lacks an approving decision") + + def test_revocation_after_expiry_is_refused(self) -> None: + request = self.envelope["request"] + request["revocation"]["revoked_at"] = request["terminal_at"] = request["expires_at"] + self.refused(self.envelope, "revocation occurred at or after request expiry") + + def test_revoked_request_cannot_invoke_provider(self) -> None: + base = load("examples/approval-envelope.json") + self.envelope["dispatches"] = base["dispatches"] + self.refused(self.envelope, "provider invocation requires a consumed request") + + def test_revocation_needs_its_audit_witness(self) -> None: + self.envelope["audit_events"] = [e for e in self.envelope["audit_events"] if e["event_type"] != "approval.revoked"] + self.refused(self.envelope, "audit stream lacks required lifecycle events: approval.revoked") + + def test_revocation_witness_must_agree(self) -> None: + event = next(e for e in self.envelope["audit_events"] if e["event_type"] == "approval.revoked") + event["actor"]["identifier"] = "someone-else@example.com" + self.refused(self.envelope, "audit revocation actor disagrees with request") + + +class AgentFeedbackTests(unittest.TestCase): + SCHEMA = "schemas/resolution-record.schema.json" + + def setUp(self) -> None: + self.envelope = load("examples/rejected-envelope.json") + self.resolution = self.envelope["resolutions"][0] + + def test_rejection_carries_structured_feedback(self) -> None: + self.assertEqual(validate_envelope(self.envelope), []) + self.assertEqual(self.resolution["agent_feedback"]["reason_code"], "incorrect_content") + + def test_feedback_never_accompanies_an_approval(self) -> None: + approved = load("examples/resolution-record.json") + approved["agent_feedback"] = {"reason_code": "other", "message": "Looks fine"} + self.assertTrue(schema_errors(self.SCHEMA, approved)) + + def test_requested_changes_require_a_new_proposal(self) -> None: + self.resolution.update(disposition="do_not_execute", retry_policy="never") + self.assertTrue(schema_errors(self.SCHEMA, self.resolution)) + + def test_questions_require_a_new_proposal(self) -> None: + feedback = self.resolution["agent_feedback"] + feedback.pop("requested_changes") + feedback.update(reason_code="needs_information", questions=["Which invoice period does the customer mean?"]) + self.assertEqual(schema_errors(self.SCHEMA, self.resolution), []) + self.resolution["retry_policy"] = "never" + self.assertTrue(schema_errors(self.SCHEMA, self.resolution)) + + def test_requested_changes_stay_inside_the_payload(self) -> None: + for path in ("/tenant", "/payload_schema/id", "payload/subject", "/payload/~2"): + with self.subTest(path=path): + self.resolution["agent_feedback"]["requested_changes"][0]["path"] = path + self.assertTrue(schema_errors(self.SCHEMA, self.resolution)) + + def test_feedback_text_rejects_display_controls(self) -> None: + self.resolution["agent_feedback"]["message"] = "Approve \u202eetaler" + self.assertTrue(schema_errors(self.SCHEMA, self.resolution)) + + def test_audit_reason_must_match_feedback(self) -> None: + event = next(e for e in self.envelope["audit_events"] if e["event_type"] == "resolution.emitted") + event["detail"]["reason_code"] = "policy_violation" + refresh_audit(self.envelope) + self.assertIn("audit resolution reason disagrees with agent feedback", validate_envelope(self.envelope)) + + +def elicitation_envelope() -> dict: + envelope = load("examples/approval-envelope.json") + envelope["request"]["policy"]["requirements"]["allowed_channels"].append("mcp_elicitation") + envelope["request"]["channels"].append({ + "channel": "mcp_elicitation", "target_hash": digest("mcp-session-01"), + "delivery_status": "delivered", "notification_id": "elicitation-01", "sent_at": "2026-04-28T09:14:26Z", + }) + approval = envelope["approvals"][0] + approval["channel"] = "mcp_elicitation" + approval["decided_by"]["authentication"]["method"] = "authenticated_session" + envelope["resolutions"][0]["approval_record_hash"] = digest(approval) + for event in envelope["audit_events"]: + if event["event_type"] == "approval.decided": + event["detail"]["channel"] = "mcp_elicitation" + refresh_audit(envelope) + return envelope + + +class ElicitationChannelTests(unittest.TestCase): + def test_human_approval_through_elicitation(self) -> None: + self.assertEqual(validate_envelope(elicitation_envelope()), []) + + def test_client_auto_decline_is_not_a_human_rejection(self) -> None: + approval = elicitation_envelope()["approvals"][0] + approval.update(decision="rejected", rejection_settlement={"state": "unresolved"}) + for kind, method, present in (("system", "service_identity", False), ("human", "unknown", True), ("human", "authenticated_session", False)): + with self.subTest(kind=kind, method=method): + approval["decided_by"]["kind"] = kind + approval["decided_by"]["authentication"].update(method=method, human_present=present) + self.assertTrue(schema_errors("schemas/approval-record.schema.json", approval)) + + def test_answer_requires_a_delivered_elicitation(self) -> None: + envelope = elicitation_envelope() + envelope["request"]["channels"][-1]["delivery_status"] = "failed" + errors = validate_envelope(envelope) + self.assertIn(f"approval {envelope['approvals'][0]['approval_id']} answers an elicitation that was never delivered", errors) + + def test_high_risk_elicitation_approval_requires_a_signature(self) -> None: + envelope = elicitation_envelope() + envelope["proposal"]["risk"] = "high" + envelope["approvals"][0].pop("signatures") + self.assertTrue(schema_errors("schemas/approval-envelope.schema.json", envelope)) + + +def renamed(envelope: dict, suffix: str) -> dict: + envelope = deepcopy(envelope) + envelope["proposal"]["proposal_id"] += suffix + envelope["proposal"]["call_id"] += suffix + envelope["proposal"]["idempotency_key"] += suffix + envelope["request"]["request_id"] += suffix + envelope["audit_snapshot"]["stream_id"] += suffix + for dispatch in envelope["dispatches"]: + dispatch["dispatch_id"] += suffix + return envelope + + +class CollectionTests(unittest.TestCase): + def setUp(self) -> None: + self.envelope = load("examples/approval-envelope.json") + + def test_independent_lifecycles_pass(self) -> None: + self.assertEqual(collection_errors([self.envelope, renamed(self.envelope, "_b")]), []) + + def test_one_call_cannot_raise_two_requests(self) -> None: + second = renamed(self.envelope, "_b") + second["proposal"]["call_id"] = self.envelope["proposal"]["call_id"] + self.assertEqual( + collection_errors([self.envelope, second]), + ["envelopes 0 and 1: admit more than one approval request for one call"], + ) + + def test_idempotency_key_binds_one_action(self) -> None: + second = renamed(self.envelope, "_b") + second["proposal"]["idempotency_key"] = self.envelope["proposal"]["idempotency_key"] + second["proposal"]["payload"]["subject"] = "Different action" + self.assertIn("envelopes 0 and 1: idempotency_key binds two different actions", collection_errors([self.envelope, second])) + + def test_same_tenant_scope_only(self) -> None: + second = renamed(self.envelope, "_b") + second["proposal"]["call_id"] = self.envelope["proposal"]["call_id"] + second["proposal"]["tenant"] = "other-tenant" + self.assertEqual(collection_errors([self.envelope, second]), []) + + +class AuditExportTests(unittest.TestCase): + def setUp(self) -> None: + self.events = load("examples/audit-events.json") + self.snapshot = load("examples/audit-snapshot.json") + + def test_complete_export_verifies(self) -> None: + self.assertEqual(audit_export_errors(self.events, self.snapshot), []) + + def test_partial_export_continues_from_checkpoint(self) -> None: + tail = self.events[4:] + self.snapshot.update(entry_count=len(tail), first_sequence=tail[0]["sequence"], events_digest=digest(tail)) + anchor = self.events[3]["event_hash"] + self.assertEqual(audit_export_errors(tail, self.snapshot, anchor), []) + self.assertIn("audit export does not continue from the retained checkpoint", audit_export_errors(tail, self.snapshot, self.events[2]["event_hash"])) + + def test_dropped_final_event_is_detected(self) -> None: + errors = audit_export_errors(self.events[:-1], self.snapshot) + self.assertIn("audit snapshot entry_count is incomplete", errors) + self.assertIn("audit snapshot root_event_hash mismatch", errors) + + def test_rewritten_event_is_detected(self) -> None: + self.events[2]["detail"]["policy_revision"] = "rewritten" + self.assertIn(f"audit event {self.events[2]['event_id']} hash mismatch", audit_export_errors(self.events, self.snapshot)) + + def test_removed_middle_event_is_detected(self) -> None: + del self.events[3] + errors = audit_export_errors(self.events, self.snapshot) + self.assertIn("audit sequence is not contiguous", errors) + + +class CommandTests(unittest.TestCase): + def run_main(self, *argv: str) -> tuple[int, str]: + buffer = io.StringIO() + with contextlib.redirect_stdout(buffer): + code = main(list(argv)) + return code, buffer.getvalue() + + def test_valid_records_exit_zero(self) -> None: + code, output = self.run_main( + "--envelope", str(REPO / "examples/approval-envelope.json"), + "--record", "approval-record", str(REPO / "examples/webauthn-approval-record.json"), + "--audit-events", str(REPO / "examples/audit-events.json"), + "--audit-snapshot", str(REPO / "examples/audit-snapshot.json"), + ) + self.assertEqual(code, 0, output) + self.assertIn("All 3 inputs passed.", output) + + def test_invalid_record_exits_one_with_json(self) -> None: + record = load("examples/resolution-record.json") + record["decision"] = "maybe" + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "resolution.json" + path.write_text(json.dumps(record), encoding="utf-8") + code, output = self.run_main("--json", "--record", "resolution-record", str(path)) + self.assertEqual(code, 1) + result = json.loads(output) + self.assertFalse(result["ok"]) + self.assertTrue(result["results"][0]["errors"]) + + def test_ambiguous_json_exits_two(self) -> None: + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "envelope.json" + path.write_text('{"proposal": {}, "proposal": {}}', encoding="utf-8") + code, output = self.run_main("--envelope", str(path)) + self.assertEqual(code, 2) + self.assertIn("duplicate JSON property", output) + + def test_jsonl_audit_export(self) -> None: + events = load("examples/audit-events.json") + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "events.jsonl" + path.write_text("\n".join(json.dumps(event) for event in events) + "\n", encoding="utf-8") + code, output = self.run_main("--audit-events", str(path), "--audit-snapshot", str(REPO / "examples/audit-snapshot.json")) + self.assertEqual(code, 0, output) + + def test_custom_payload_schema_from_trusted_file(self) -> None: + envelope = load("examples/approval-envelope.json") + schema = load("schemas/actions/email.send.schema.json") + schema["$id"] = "https://schemas.example.com/actions/v1.0.0/email.send.schema.json" + with tempfile.TemporaryDirectory() as directory: + schema_path = Path(directory) / "email.send.schema.json" + schema_path.write_text(json.dumps(schema), encoding="utf-8") + envelope["proposal"]["payload_schema"] = { + "id": schema["$id"], "version": "1.0.0", + "digest": "sha256:" + hashlib.sha256(schema_path.read_bytes()).hexdigest(), + } + envelope_path = Path(directory) / "envelope.json" + envelope_path.write_text(json.dumps(envelope), encoding="utf-8") + code, output = self.run_main("--envelope", str(envelope_path)) + self.assertEqual(code, 1) + self.assertIn("payload schema is not available in the trusted local catalog", output) + code, output = self.run_main("--envelope", str(envelope_path), "--schema-file", str(schema_path)) + self.assertNotIn("trusted local catalog", output) + + +if __name__ == "__main__": + unittest.main()