{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://agenticrail.nz/spec/receipt-schema.json",
  "title": "AgenticRail Pre-Execution Enforcement Receipt",
  "description": "Schema for the cryptographically signed receipt issued by the AgenticRail gate for every pre-execution enforcement decision. Each receipt documents one step in a deterministic sequence: the action proposed, the policy evaluated, the decision reached (ALLOW / DENY / HALT), and the cryptographic proof of integrity. Receipts are chained via prev_receipt_id, forming a tamper-evident audit log for the complete sequence. Published 2026-05-17. Schema version: slp8_receipt_v2.",
  "version": "slp8_receipt_v2",
  "datePublished": "2026-05-17",
  "dateModified": "2026-10-08",
  "changeNote": "2026-06-07: clarified the active signing algorithm (Ed25519, key_id k2_2026-06-07_ed25519) and corrected the signature encoding (Ed25519 is base64, not hex). The receipt structure, required fields, types, and enums are unchanged from the 2026-05-17 publication — this is non-normative errata. The v1.0/v1.1 enforcement-spec fingerprints are at /spec/ ; verification keys at /spec/receipt-public-keys.json. 2026-07-08: added prev_receipt_hash (nullable, required) — sha256 of the immediately preceding receipt's full canonical JSON, a genuine hash chain that catches an in-place edit to any earlier receipt's content. Also corrected prev_receipt_id's description, which previously and inaccurately implied identifier linkage alone caught such tampering — it only proves ordering/reference, not content integrity of the predecessor; that guarantee now belongs to prev_receipt_hash specifically. Additive, same treatment as the 2026-07-05 ARTIFACT_UNBOUND addition — schema version unchanged (slp8_receipt_v2). 2026-07-08 (later same day): removed references to the Hokianga Spine from the function/reasons field descriptions — AgenticRail is a sequence-enforcement and accountability layer only, not a language or data sovereignty product; the Hokianga Spine was never wired into the live enforcement engine and is withdrawn. Non-normative errata — no schema structure change. 2026-09-30: added step_order (optional, nullable array of strings), the step order each call was evaluated against, signed into the receipt so the declared order is part of the evidence. Additive, schema version unchanged (slp8_receipt_v2); not in required, so receipts issued before it remain valid. Same date: the decision field description now says a receipt is issued with the decision before the action executes, rather than written to storage by then; the durable write completes alongside the response. Non-normative errata, no schema structure change. 2026-10-08: added decision_index (optional, nullable integer, minimum 1), a signed count of every decision in the sequence, ALLOW and DENY alike, so a removed receipt of either decision leaves a missing number. Additive, schema version unchanged (slp8_receipt_v2); not in required, so receipts issued before it remain valid.",
  "author": "Kade Cowper — TUARA KURI LIMITED",
  "contact": "hello@agenticrail.nz",
  "specUrl": "https://agenticrail.nz/spec/",
  "copyright": "© 2026 TUARA KURI LIMITED. This document's text is protected by copyright. The schema it describes — and any enforcement mechanism implementing it — may be freely and independently implemented by others; publishing this schema openly is a deliberate choice to support standards engagement and independent verifiability.",

  "type": "object",
  "required": [
    "ts_ms",
    "pack_id",
    "version",
    "decision",
    "reasons",
    "executed",
    "sealed",
    "meta",
    "prev_receipt_id",
    "prev_receipt_hash",
    "key_id",
    "signature_alg",
    "signature"
  ],

  "properties": {

    "ts_ms": {
      "type": "integer",
      "description": "Unix timestamp in milliseconds at which the enforcement decision was made. Used for freshness validation: |ts_ms - now| > 300,000 ms triggers STALE_TIMESTAMP denial.",
      "examples": [1747821600000]
    },

    "pack_id": {
      "type": "string",
      "pattern": "^[0-9a-f]{64}$",
      "description": "SHA-256 hex digest of the canonical pack object (alphabetically sorted keys, no extra whitespace). Serves as the receipt's unique identifier and integrity anchor. The report generator re-derives this hash to verify the receipt has not been altered.",
      "examples": ["e58338a76d00405d2f3a1b9c4e7f8a2d1c6b3e9f0a4d7c2b5e8f1a3d6c9b2e5"]
    },

    "version": {
      "type": "string",
      "const": "slp8_receipt_v2",
      "description": "Receipt format version. slp8_receipt_v2 uses canonical JSON (alphabetically sorted keys) for pack_id derivation and for the signature preimage. The receipt format is independent of the signing algorithm: receipts issued from 2026-06-07 are signed Ed25519 (key_id k2_2026-06-07_ed25519); earlier receipts are HMAC-SHA-256 (key_id k1_2026-02-22_01). Gen-1 receipts used JSON.stringify — both are supported by the verification endpoint. Published keys: /spec/receipt-public-keys.json."
    },

    "decision": {
      "type": "string",
      "enum": ["ALLOW", "DENY"],
      "description": "Enforcement decision. ALLOW: action is authorised to proceed. DENY: action is blocked and the reasons array is populated. Both produce a signed receipt, issued with the decision before the action executes. HALT is NOT a decision and never appears in this field: it is a rejection returned at the boundary when a request is refused before enforcement runs (malformed payload, oversized body, failed auth, poison-pattern match), so nothing reaches enforcement and no receipt is written. Reconciling receipts against calls will therefore show no receipt for a HALTed call. That is correct, not a gap.",
      "examples": ["ALLOW"]
    },

    "reasons": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "SEQUENCE_VIOLATION",
          "REPLAY_NONCE",
          "SEALED_SEQUENCE",
          "UNKNOWN_STEP",
          "FUNCTION_STEP_MISMATCH",
          "ACTION_NOT_ALLOWED",
          "STALE_TIMESTAMP",
          "ARTIFACT_UNBOUND"
        ]
      },
      "description": "Denial or halt reason codes. Empty array when decision is ALLOW. Each code maps to a specific enforcement rule: SEQUENCE_VIOLATION (step submitted out of order), REPLAY_NONCE (nonce already seen — replay attempt blocked), SEALED_SEQUENCE (sequence permanently closed after settle step), UNKNOWN_STEP (step/function value not present in this sequence's own declared step_order — checked at the sequence-enforcement layer; corrected 2026-07-05, supersedes the previously-published but unreachable NO_POLICY_MATCH — an unrecognized name alone does not deny, it falls through to a permissive generic policy so custom step_order sequences work; only a name absent from the declared step_order is rejected), FUNCTION_STEP_MISMATCH (step and function fields differ), ACTION_NOT_ALLOWED (action_type not permitted at this step), STALE_TIMESTAMP (ts_ms outside 300-second freshness window), ARTIFACT_UNBOUND (a RECORD_RESULT at boundary did not supply attestation.witnessed_pack_id matching the actual immediately-preceding receipt in this sequence — the witness must attest to the real prior artifact, not an unverified claim).",
      "examples": [[], ["REPLAY_NONCE"], ["SEQUENCE_VIOLATION"], ["ARTIFACT_UNBOUND"]]
    },

    "executed": {
      "type": "boolean",
      "description": "The enforcement decision was ALLOW — i.e. the step was PERMITTED to proceed. True when decision is ALLOW; false when DENY or HALT. This attests the gate's verdict only. It does NOT assert that the downstream action was carried out or succeeded: AgenticRail is an enforcement layer, not an execution runtime, and the executor's outcome is not part of the signed receipt. (The public API response reports the separate executor outcome under `execution_submitted` / `result`.)",
      "examples": [true]
    },

    "sealed": {
      "type": "boolean",
      "description": "Whether this receipt completes and permanently seals the sequence. True only on the final step (settle). Once sealed, the sequence_id is retired — no further steps are accepted. Sealing is irreversible: no unsealing mechanism exists.",
      "examples": [false, true]
    },

    "meta": {
      "type": "object",
      "description": "Enforcement context captured at decision time. No field is modified after the receipt is written; alteration is detectable via the signature and the prev_receipt_hash chain.",
      "required": [
        "model_id",
        "sequence_id",
        "step",
        "function",
        "action_type",
        "policy_map_ids"
      ],
      "properties": {
        "model_id": {
          "type": ["string", "null"],
          "description": "Agent model identifier. Scopes the Durable Object namespace — sequences with different model_ids are isolated from each other.",
          "examples": ["MSMD"]
        },
        "sequence_id": {
          "type": ["string", "null"],
          "description": "Unique identifier for this decision sequence. All receipts sharing a sequence_id form one chained sequence. Must be unique per agent run; reuse constitutes a replay attempt.",
          "examples": ["whiro-write-f2646de0f129"]
        },
        "step": {
          "type": ["string", "null"],
          "description": "The spine step name submitted by the client. Must equal the function field — divergence triggers FUNCTION_STEP_MISMATCH denial.",
          "examples": ["intake", "execution", "settle"]
        },
        "function": {
          "type": ["string", "null"],
          "description": "The enforcement function resolved from the submitted step. One of the eight canonical MSMD spine functions, or any custom name declared in the request's own step_order — an unrecognized name falls through to a permissive generic policy so custom step_order sequences work (corrected 2026-07-05; not restricted to the eight MSMD names as previously documented). A value absent from the sequence's declared step_order is rejected as UNKNOWN_STEP.",
          "examples": ["intake", "verify_identity"]
        },
        "action_type": {
          "type": ["string", "null"],
          "description": "The type of action proposed at this step. Evaluated against the function's allowed_action_types policy. Disallowed values trigger ACTION_NOT_ALLOWED denial.",
          "examples": ["CHECK_STATE", "RECORD_RESULT", "WAIT_FOR_SIGNAL"]
        },
        "policy_map_ids": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Identifiers of the policy maps evaluated for this function. Used by the report generator to verify receipt integrity across policy versions.",
          "examples": [["policy_state_read_unfamiliar_terrain"], ["policy_settle_cycle_close", "policy_settle_evaluation_after_cycle"]]
        }
      },
      "additionalProperties": false
    },

    "attestation": {
      "type": ["object", "null"],
      "description": "Optional signed evidence attached to this step by the client. Arbitrary key-value pairs that the gate signs into the receipt without interpreting. Enables clients to bind external evidence (test results, sensor readings, human approvals) to a specific enforcement decision. The gate excludes attestation from poison detection.",
      "examples": [null, { "chip_id": "WHIRO-2026-05-21-001", "chip_path": "C:\\MSMD_MASTER\\..." }]
    },

    "payload_hash": {
      "type": ["string", "null"],
      "pattern": "^[0-9a-f]{64}$|^$",
      "description": "SHA-256 hex digest of the raw request payload body. Provides an additional integrity link between the enforcement decision and the exact payload that triggered it. Null in some early receipts.",
      "examples": [null, "a3f1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2"]
    },

    "prev_receipt_id": {
      "type": ["string", "null"],
      "pattern": "^[0-9a-f]{64}$|^null$",
      "description": "pack_id of the immediately preceding receipt in this sequence — an identifier reference forming the chain's order. This proves something with this identifier came before; it does not by itself protect the predecessor's content from a later in-place edit (see prev_receipt_hash for that guarantee). Null for the first step (intake).",
      "examples": [null, "9cc8c77cf469b41dab0511470e94851949d0441ac96e7052c65edbe243d6d32d"]
    },

    "prev_receipt_hash": {
      "type": ["string", "null"],
      "pattern": "^[0-9a-f]{64}$|^null$",
      "description": "SHA-256 hex digest of the immediately preceding receipt's full canonical JSON (the complete stored object, signature included) — not merely its pack_id. Added 2026-07-08. Where prev_receipt_id proves 'something with this identifier came before,' prev_receipt_hash proves 'this exact content did': an in-place edit to any earlier receipt in the chain changes its hash, breaking every subsequent prev_receipt_hash link even if pack_id references still match. Null for the first step (intake), and also null when the anchor this receipt resolved against was written before this field existed — the report generator's chain-integrity check treats that case as 'not verifiable', not 'broken'.",
      "examples": [null, "4c79d63cc16b0475e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4"]
    },

    "step_order": {
      "type": ["array", "null"],
      "items": { "type": "string" },
      "description": "The step order this call was evaluated against, exactly as enforced: the caller's declared step_order, normalised (trimmed, lower-cased), or the default eight-step MSMD spine when the caller declared none. Added 2026-09-30. On every ALLOW it is the order the sequence is locked to, because a call presenting any other order is refused with STEP_ORDER_MISMATCH. It makes the declaration part of the signed evidence: a sequence whose declared order leaves a step out, an approval for example, now shows that omission in its receipts, where previously a receipt named only the step that ran. Covered by the signature like every other receipt field. Not part of the pack, so pack_id derivation is unchanged. Absent from receipts issued before this field was introduced; not in required, so those receipts remain valid.",
      "examples": [
        ["verify_identity", "request_approval", "deploy_production"],
        ["intake", "disruption", "instability", "state_read", "internal_driver", "execution", "boundary", "settle"]
      ]
    },

    "decision_index": {
      "type": ["integer", "null"],
      "minimum": 1,
      "description": "The position of this decision among every decision recorded for this sequence, counted from 1 across ALLOW and DENY alike. Added 2026-10-08. The hash chain (prev_receipt_id, prev_receipt_hash) runs through ALLOWED receipts only, so a refused step points back but nothing points to it; without this field a DENY receipt could be removed with no break in any link. With it, a removed receipt of either decision leaves a missing number, and the numbering cannot be closed up without re-signing every later receipt. A missing number means the receipt is absent: never written or removed, which the report does not attempt to tell apart. It cannot show receipts missing after the highest number present. Null when the index could not be reserved; such a receipt cannot be placed, and is not read as a gap. Covered by the signature like every other receipt field. Not part of the pack, so pack_id derivation is unchanged. Absent from receipts issued before this field was introduced; not in required, so those receipts remain valid, and a sequence open across the change is numbered from 1 at its first receipt after it.",
      "examples": [1, 7, null]
    },

    "key_id": {
      "type": "string",
      "description": "Identifier of the signing key used to produce the signature field. Enables key rotation: verifiers select the correct public key (Ed25519) or HMAC secret by key_id without parsing the signature itself. Resolve against the published keyring at /spec/receipt-public-keys.json. Current: k2_2026-06-07_ed25519 (Ed25519); legacy: k1_2026-02-22_01 (HMAC).",
      "examples": ["k2_2026-06-07_ed25519", "k1_2026-02-22_01"]
    },

    "signature_alg": {
      "type": ["string", "null"],
      "enum": ["hmac-sha256", "Ed25519", null],
      "description": "Signing algorithm. Ed25519 (active since 2026-06-07): EdDSA signature; verifiable offline by anyone using the published public key, no call back to AgenticRail. hmac-sha256 (legacy): HMAC-SHA-256 — symmetric, so not third-party verifiable; verify via the report endpoint. Both are computed over the canonical JSON of the receipt with the signature field removed (alphabetically sorted keys). Null indicates the receipt was written without a signing key configured — should not occur in production.",
      "examples": ["Ed25519", "hmac-sha256"]
    },

    "signature": {
      "type": ["string", "null"],
      "description": "Cryptographic signature of the receipt. For Ed25519: base64-encoded EdDSA signature (64 bytes → 88 base64 chars). For hmac-sha256: hex-encoded HMAC-SHA-256 digest (64 hex chars). Computed over the canonical JSON of all receipt fields excluding the signature field itself. Ed25519 signatures verify against /spec/receipt-public-keys.json; both are verifiable via the report endpoint.",
      "examples": ["3hJ1m0Qm8eYJ0n2k9xqQ2bFaXyWn0r3sKQ5sI9oG1cV6Yc0a8e2tF7uH4wI3rN1pL5dM2bV0xZ9oH7gW4cT8aQ==", "53241ed9add2ca63b6bc0d6865d297b9203101df788f7924a74e1734caf51b13"]
    }

  },

  "additionalProperties": false,

  "$defs": {

    "pack": {
      "title": "AgenticRail Enforcement Pack",
      "description": "The enforcement decision object whose SHA-256 canonical hash becomes the pack_id. The pack is computed before the receipt is written — it is the minimal authoritative record of the enforcement decision, independent of receipt metadata. pack_id = SHA-256(canonicalJson(pack)).",
      "type": "object",
      "required": ["pack_version", "decision", "reasons", "executed", "meta"],
      "properties": {
        "pack_version": {
          "type": "string",
          "const": "slp8_pack_1.0"
        },
        "decision": {
          "type": "string",
          "enum": ["ALLOW", "DENY"]
        },
        "reasons": {
          "type": "array",
          "items": { "type": "string" }
        },
        "executed": {
          "type": "boolean"
        },
        "meta": {
          "type": "object",
          "required": ["model_id", "sequence_id", "step", "function", "action_type", "policy_map_ids"],
          "properties": {
            "model_id":       { "type": ["string", "null"] },
            "sequence_id":    { "type": ["string", "null"] },
            "step":           { "type": ["string", "null"] },
            "function":       { "type": ["string", "null"] },
            "action_type":    { "type": ["string", "null"] },
            "policy_map_ids": { "type": "array", "items": { "type": "string" } }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },

    "request_payload": {
      "title": "AgenticRail Gate Request Payload",
      "description": "The payload submitted by a client to POST /v1/evaluate. Every field is evaluated by the gate; the raw body is hashed to produce receipt.payload_hash.",
      "type": "object",
      "required": ["schema_version", "model_id", "sequence_id", "step", "function", "action_type", "nonce", "ts_ms", "step_order"],
      "properties": {
        "schema_version": {
          "type": "string",
          "const": "1.0"
        },
        "model_id": {
          "type": "string",
          "examples": ["MSMD"]
        },
        "sequence_id": {
          "type": "string",
          "description": "Unique identifier for this agent run. Must be unique per sequence — reuse triggers REPLAY_NONCE or SEQUENCE_VIOLATION."
        },
        "step": {
          "type": "string",
          "enum": ["intake", "disruption", "instability", "state_read", "internal_driver", "execution", "boundary", "settle"],
          "description": "Must equal the function field."
        },
        "function": {
          "type": "string",
          "enum": ["intake", "disruption", "instability", "state_read", "internal_driver", "execution", "boundary", "settle"],
          "description": "Must equal the step field."
        },
        "action_type": {
          "type": "string",
          "description": "Evaluated against the function's allowed_action_types policy."
        },
        "action": {
          "type": "string",
          "description": "Human-readable label for the proposed action. Stored in attestation context; not evaluated by policy."
        },
        "inputs": {
          "type": "object",
          "description": "Arbitrary key-value inputs for the step. Not evaluated by policy; available for attestation and logging."
        },
        "nonce": {
          "type": "string",
          "description": "Unique value per request. The gate rejects any nonce seen before in this sequence — prevents replay attacks. UUID or hex-encoded random bytes recommended."
        },
        "ts_ms": {
          "type": "integer",
          "description": "Client timestamp in Unix milliseconds. Gate enforces |ts_ms - server_now| <= 300,000 ms — prevents delayed replay."
        },
        "step_order": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Complete ordered list of spine steps for this sequence. Must be sent on every request — the gate does not store step_order between calls.",
          "examples": [["intake", "disruption", "instability", "state_read", "internal_driver", "execution", "boundary", "settle"]]
        },
        "attestation": {
          "type": "object",
          "description": "Optional. Arbitrary evidence to be signed into the receipt at this step."
        }
      }
    }

  }
}
