Chio/Docs
LOGIN · JOIN

PlatformCapabilities & Receipts

Kernel

Receipts & Audit

The kernel signs a receipt for every mediated tool call, allow or deny, then commits batches of them to a signed Merkle root.

Source

The field, decision, and metadata-key tables render from the receipt-fields dataset, which is read out of crates/core/chio-core-types/src/receipt/ (body.rs, decision.rs, metadata.rs, and signing.rs) at the pinned commit. The rest is verified against crates/core/chio-core-types/src/canonical.rs, crates/kernel/chio-kernel/src/checkpoint.rs, crates/kernel/chio-kernel/src/receipt_query.rs, crates/kernel/chio-kernel/src/receipt_store.rs, and the SQLite bootstrap at crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rs.

ChioReceipt

A receipt is 24 fields under the schema chio.receipt.v1. Three structs share those fields, and the difference between them is what a verifier checks: ChioReceiptIdInput is the 20 fields the content-addressed id is the SHA-256 of; ChioReceiptBody is those 20 plus the id itself; and ChioReceipt adds the three fields that carry the signature and its material.

FieldTypeOn the wireIn the idWhat it records
idStringAlwaysNoContent-addressed receipt ID derived from the canonical receipt body.
timestampu64AlwaysYesUnix timestamp (seconds) when the receipt was created.
capability_idStringAlwaysYesID of the capability token that was exercised (or presented).
tool_serverStringAlwaysYesTool server that handled the invocation.
tool_nameStringAlwaysYesTool that was invoked (or attempted).
actionToolCallActionAlwaysYesThe action that was evaluated.
decisionOption<Decision>Omitted when NoneYesThe Kernel's decision. Present only for mediated decisions.
receipt_kindReceiptKindAlwaysYesSigned receipt semantic kind.
boundary_classBoundaryClassAlwaysYesSigned runtime boundary class.
observation_outcomeOption<ObservationOutcome>Omitted when NoneYesSigned observation outcome for trace and advisory records.
tool_originToolOriginAlwaysYesSigned tool-origin classification.
redaction_modeRedactionModeAlwaysYesSigned redaction mode.
actor_chainVec<ActorRef>Omitted when emptyYesSigned actor attribution chain.
content_hashStringAlwaysYesSHA-256 hash of the evaluated content for this receipt.
policy_hashStringAlwaysYesSHA-256 hash of the policy that was applied.
evidenceVec<GuardEvidence>Omitted when emptyYesPer-guard evidence collected during evaluation.
metadataOption<serde_json::Value>Omitted when NoneYesOptional receipt metadata for stream/accounting details.
trust_levelTrustLevelAlwaysYesStrength of kernel mediation that produced this receipt.
tenant_idOption<String>Omitted when NoneYesMulti-tenant receipt isolation: tenant identifier for multi-tenant deployments. `None` in single-tenant mode; derived from the authenticated session's enterprise identity context and MUST NOT be taken from caller-provided request fields (caller choice would defeat the isolation intent). Serialized only when set.
bbs_projection_versionOption<String>Omitted when NoneYesBBS projection version bound into the receipt id when BBS material is present.
kernel_keyPublicKeyAlwaysYesThe Kernel's public key (for verification without out-of-band lookup).
bbs_signatureOption<BbsReceiptSignature>Omitted when NoneNoOptional BBS material for selective disclosure over this receipt.
algorithmOption<SigningAlgorithm>Omitted when is_default_optional_algorithmNoSigning algorithm used for [`ChioReceipt::signature`]. Informational only: verification dispatches off the self-describing encoding of the signature itself.
signatureSignatureAlwaysNoSignature over canonical JSON of [`ChioReceiptSigningBody`].

9 of the 24 carry a skip_serializing_if, so a receipt JSON blob is not a fixed key set: an absent key means the default, not a missing field. That matters for anyone re-deriving the id, because a key omitted from canonical JSON is a key omitted from the hash input.

decision is an Option: not every receipt carries a mediated verdict. Trace and advisory receipts record an outcome through receipt_kind, boundary_class, and observation_outcome instead, and all three are in the id.

Decision

The receipt's decision is a 4-variant enum, internally tagged verdict with snake_case values, so the wire form is {"verdict": "deny", "reason": "...", "guard": "..."} and never a nested variant object. It differs from the kernel's in-flight Verdict: the receipt records the terminal outcome, which includes cancellation and incompleteness.

VariantWire valuePayloadMeaning
AllowallowNo payloadThe tool call was allowed and executed.
Denydenyreason, guardThe tool call was denied.
CancelledcancelledreasonThe tool call was interrupted by explicit cancellation.
IncompleteincompletereasonThe tool call did not reach a complete terminal result.

Deny.guard names the gate, not the guard

The field's doc comment reads "the guard or validation step that triggered the denial", and on the kernel path it is always the validation step. Every deny site writes a scope literal fixed at that site (kernel, policy.deny, session_roots, finding_status, and their kin), and no value that Guard::name() returns appears there: the runtime names are kebab-case and none of these are. The guard that refused survives in the reason string and in the guard_name of the matching evidence row, so join on evidence.

TrustLevel

Records how the kernel mediated the call. Defaults to Mediated; older receipts without the field deserialize to this default.

crates/core/chio-core-types/src/receipt/kinds.rs11-26rust
pub enum TrustLevel {
    /// Tool invocation was synchronously mediated by the kernel (the
    /// strongest form: kernel observed the call inline and authorized it).
    /// This is the default and the safest baseline.
    #[default]
    Mediated,
    /// Authorization happened inline in the agent process (e.g. a
    /// long-running orchestrator embedded the kernel via FFI). The kernel
    /// observed the call but did not synchronously mediate it through a
    /// separate trust boundary.
    Verified,
    /// Authorization was advisory only -- the kernel evaluated but the
    /// caller may have proceeded regardless. Used for shadow-mode
    /// integrations and observability-only deployments.
    Advisory,
}

GuardEvidence

Each guard that ran during evaluation contributes one record, and the evidence vector is in the receipt id.

FieldTypeOn the wireWhat it records
guard_nameStringAlwaysName of the guard (e.g. "ForbiddenPathGuard").
verdictboolAlwaysWhether the guard passed (true) or denied (false).
detailsOption<String>Omitted when NoneOptional details about the guard's decision.

Read the guard_name doc against the producer rather than at face value. The pipeline writes guard.name().to_string(), and Guard::name() returns the kebab-case runtime name, so the value a receipt carries is forbidden-path, not the PascalCase struct name the doc comment gives as its example. Match on the runtime name.

External-guard adapters serialize their structured evidence (a Bedrock category breakdown, a VirusTotal detection count) into details as a JSON string. The shape of that JSON is owned by the provider crate and versioned there.

Reserved metadata keys

metadata is a free-form JSON object with a reserved namespace inside it. The kernel merges its typed blocks under these keys last and refuses a pre-existing collision from caller or hook metadata, so a block found under a reserved key was written by the kernel and is covered by the receipt signature.

KeyConstantSchemaBlock
admission_operationADMISSION_RECEIPT_METADATA_KEYchio.admission-receipt.v1No doc comment on the constant.
attributionReceiptAttributionMetadataNone pinnedUniversal receipt-side attribution for capability context. This metadata gives downstream analytics a deterministic local join path from a receipt to the capability subject and, when available, the matched grant within the capability scope.
budget_authorityBUDGET_AUTHORITY_METADATA_KEYNone pinnedBudget-authority lineage metadata block key for monetary receipts.
channelCHANNEL_METADATA_KEYNone pinnedStreamed-output channel accounting metadata block key.
chio_receipt_signing_nonceCHIO_RECEIPT_SIGNING_NONCE_METADATA_KEYNone pinnedNo doc comment on the constant.
delivery_contractDELIVERY_CONTRACT_METADATA_KEYchio.delivery-contract.v1Delivery-contract evidence metadata block key (ADR-0018 item 7).
financialFINANCIAL_METADATA_KEYNone pinnedFinancial attribution and settlement metadata block key.
finding_deliveryFINDING_DELIVERY_METADATA_KEYchio.finding.delivery.v1Finding-delivery overlay metadata block key.
finding_recoveryFINDING_RECOVERY_METADATA_KEYchio.finding.recovery.v1Finding-recovery receipt metadata block key.
governed_transactionGOVERNED_TRANSACTION_METADATA_KEYNone pinnedGoverned-transaction intent and approval metadata block key.
original_metadataCHIO_RECEIPT_ORIGINAL_METADATA_KEYNone pinnedNo doc comment on the constant.

Two of these are not application blocks at all. chio_receipt_signing_nonce holds the caller-supplied nonce that bind_receipt_signing_nonce folds into the metadata before the id is computed, which is how a caller nonce reaches the signed bytes; and original_metadata is where that binding parks a caller metadata value that is not a JSON object, so the nonce gets an object to live in without dropping what was there. Both run before chio_receipt_id, so both are inside the hash.


Canonical JSON

Receipts are signed over canonical JSON (RFC 8785 / JCS), not arbitrary serde_json::to_string output. The implementation lives in chio_core_types::canonical. The rules:

  • Object keys are sorted by UTF-16 code unit comparison (not byte or ASCII order).
  • Numbers use the shortest representation matching ECMAScript JSON.stringify().
  • Strings use minimal escaping. Only required characters are escaped.
  • No whitespace between tokens. The output is a single line.

The signature is taken over ChioReceiptSigningBody, which is three things, not a flat struct: the id, the 20-field ChioReceiptIdInput the id was derived from, and the bbs_signature when the receipt carries one. Binding both the id and the exact input means a verifier recomputes the id, compares it, and only then checks the signature, so a receipt whose id does not match its own body fails before any curve arithmetic runs.

The BBS material is inside the signature rather than beside it. from_body_and_bbs takes it as a second argument at signing time, and verify_signature rebuilds the signing body with the receipt's own bbs_signature before verifying, so selective-disclosure material cannot be attached to or stripped from a signed receipt. validate_bbs_receipt_binding runs first and returns Ok(false) from verify_signature whenever bbs_projection_version and bbs_signature disagree about whether BBS material is present, or about which projection version it claims.

Single-tenant receipts, where tenant_id is None, omit that key from canonical JSON entirely rather than writing a null, which is the general rule for the 9 omittable fields and the reason an absent key and a null are not interchangeable here. The swarm artifacts that close a delegation graph reuse this canonical-JSON primitive over a different body rule, dropping only their own signature field, and Join & Terminal Receipts owns that closure.

Do not roll your own canonicalization

TypeScript, Python, Go, and Rust must all produce identical bytes for the same logical receipt body. Use the JCS implementation bundled with each language's chio SDK rather than your runtime's default JSON serializer.

SigningBackend

The kernel uses a trait for receipt signing, allowing FIPS algorithms without changes at call sites:

crates/core/chio-core-types/src/crypto.rs902-919rust
pub trait SigningBackend: Send + Sync {
    /// Algorithm this backend produces.
    fn algorithm(&self) -> SigningAlgorithm;

    /// Public half of this backend's signing identity.
    fn public_key(&self) -> PublicKey;

    /// Produce a detached signature over `message`.
    fn sign_bytes(&self, message: &[u8]) -> Result<Signature>;

    /// Produce a detached signature over canonical JSON bytes.
    fn sign_canonical_bytes(
        &self,
        canonical: &CanonicalBytes<CanonicalJsonWitness>,
    ) -> Result<Signature> {
        self.sign_bytes(canonical.as_bytes())
    }
}

Three implementations:

  • Ed25519Backend · default, no feature flag required.
  • P256Backend · gated on the fips crate feature.
  • P384Backend · gated on the fips crate feature.

Verification reads the algorithm off the self-describing prefix on the PublicKey and Signature; the envelope algorithm field is informational. Building without the fips feature causes P-256 / P-384 receipts to fail verification with Ok(false) rather than attempting an unsupported curve.


The ReceiptStore trait

Any backend that implements ReceiptStore can persist receipts. The core append methods are:

crates/kernel/chio-kernel/src/receipt_store.rsrust
pub trait ReceiptStore: Send + Sync {
    fn append_chio_receipt(&self, receipt: &ChioReceipt) -> Result<(), ReceiptStoreError>;
    // ...
    fn append_chio_receipt_returning_seq(
        &self,
        receipt: &ChioReceipt,
    ) -> Result<Option<u64>, ReceiptStoreError> {
        self.append_chio_receipt(receipt)?;
        Ok(None)
    }
    // ...
    fn append_child_receipt(&self, receipt: &ChildRequestReceipt) -> Result<(), ReceiptStoreError>;
    // ...
    fn store_checkpoint(&self, _checkpoint: &KernelCheckpoint) -> Result<(), ReceiptStoreError> {
        Err(ReceiptStoreError::Conflict(
            "receipt checkpoint storage is not supported by this receipt store".to_string(),
        ))
    }
    // ...
    fn load_checkpoint_by_seq(
        &self,
        _checkpoint_seq: u64,
    ) -> Result<Option<KernelCheckpoint>, ReceiptStoreError> {
        Ok(None)
    }
    // ...
}

The elisions are large. The block shows five methods; the rest of the trait (receipt_store.rs:418-909) is mostly defaulted, covering timeout-aware appends, canonical-bytes reads, pending settlement observations, capability lineage, federated evidence, and the retention hooks below.

Two methods are required and the rest carry defaults, so a backend overrides only what it persists. Read the checkpoint defaults as behavior rather than as filler: a store that does not implement checkpointing refuses to store one with receipt checkpoint storage is not supported by this receipt store and reports none on read, which is why checkpoint_batch_size and store capability have to agree before a deployment claims a Merkle chain. The kernel queries receipts through ReceiptQuery:

crates/kernel/chio-kernel/src/receipt_query.rsrust
pub struct ReceiptQuery {
    /// Filter by capability ID (exact match).
    pub capability_id: Option<String>,
    /// Filter by tool server name (exact match).
    pub tool_server: Option<String>,
    /// Filter by tool name (exact match).
    pub tool_name: Option<String>,
    /// Filter by decision outcome (maps to decision_kind column:
    /// "allow", "deny", "cancelled", "incomplete").
    pub outcome: Option<String>,
    /// Include only receipts with timestamp >= since (Unix seconds, inclusive).
    pub since: Option<u64>,
    /// Include only receipts with timestamp <= until (Unix seconds, inclusive).
    pub until: Option<u64>,
    /// Include only receipts with financial cost_charged >= min_cost (minor units).
    /// Receipts without financial metadata are excluded when this filter is set.
    pub min_cost: Option<u64>,
    /// Include only receipts with financial cost_charged <= max_cost (minor units).
    /// Receipts without financial metadata are excluded when this filter is set.
    pub max_cost: Option<u64>,
    /// Currency for cost filters. Required when either cost bound is present.
    pub cost_currency: Option<String>,
    /// Cursor for forward pagination: return only receipts with seq > cursor (exclusive).
    pub cursor: Option<u64>,
    /// Maximum number of receipts to return per page (capped at MAX_QUERY_LIMIT).
    pub limit: usize,
    /// Filter by agent subject public key (hex-encoded Ed25519). Resolved through
    /// capability_lineage JOIN -- does not replay issuance logs.
    pub agent_subject: Option<String>,
    /// Optional tenant narrowing filter. This is never authority by itself:
    /// callers must also provide a matching explicit `read_context`.
    pub tenant_filter: Option<String>,
    /// Explicit read context resolved from authenticated authority.
    pub read_context: Option<ReceiptReadContext>,
}

limit is capped at MAX_QUERY_LIMIT, which is 200, and the cost filters are the one pair with a companion requirement: cost_currency is required whenever either bound is set, and receipts with no financial metadata drop out of the result rather than being treated as zero.

tenant_filter does not grant authority by itself. What authorizes and shapes the query is the explicit read_context: a boundary (AdminAll or TenantScoped), a source (LocalOperator, AdminService, or AuthenticatedTenant), and an include_null_tenant flag. Remote and control-plane callers construct it from authenticated context before the query reaches the store. See Multi-tenant isolation below.


SqliteReceiptStore

The production reference implementation lives in chio-store-sqlite. It owns an r2d2 connection pool and a tenant-isolation flag. The schema for tool receipts:

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rssql
CREATE TABLE IF NOT EXISTS chio_tool_receipts (
    seq INTEGER PRIMARY KEY AUTOINCREMENT,
    receipt_id TEXT NOT NULL UNIQUE,
    timestamp INTEGER NOT NULL,
    capability_id TEXT NOT NULL,
    subject_key TEXT,
    issuer_key TEXT,
    grant_index INTEGER,
    tool_server TEXT NOT NULL,
    tool_name TEXT NOT NULL,
    decision_kind TEXT NOT NULL,
    policy_hash TEXT NOT NULL,
    content_hash TEXT NOT NULL,
    raw_json TEXT NOT NULL,
    cost_currency TEXT CHECK (
        cost_currency IS NULL OR (
            typeof(cost_currency) = 'text' AND
            length(cost_currency) = 3 AND
            cost_currency NOT GLOB '*[^A-Z]*'
        )
    ),
    cost_charged_be BLOB CHECK (
        (cost_currency IS NULL AND cost_charged_be IS NULL) OR
        (
            cost_currency IS NOT NULL AND
            typeof(cost_charged_be) = 'blob' AND
            length(cost_charged_be) = 8
        )
    )
);

CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_timestamp
    ON chio_tool_receipts(timestamp);
CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_capability
    ON chio_tool_receipts(capability_id);
CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_subject
    ON chio_tool_receipts(subject_key);
CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_grant
    ON chio_tool_receipts(capability_id, grant_index);
CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_tool
    ON chio_tool_receipts(tool_server, tool_name);
CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_decision
    ON chio_tool_receipts(decision_kind);

The bootstrap statement has no tenant_id column. A later migration adds it with ALTER TABLE chio_tool_receipts ADD COLUMN tenant_id TEXT and an index beside it, which is why the column is nullable and why pre-multitenant rows read back with a NULL tenant.

The two cost columns are checked, not just typed: cost_currency must be three uppercase letters or NULL, and cost_charged_be must be an 8-byte blob present exactly when a currency is. A row cannot record an amount without naming its unit.

The canonical receipt JSON lives in raw_json; the columnar fields exist for query performance only. The store rebuilds receipts from raw_json on read and re-verifies signatures.


Retention

Retention is a runtime concern, not a database trigger. The kernel uses RetentionConfig to rotate receipts older than retention_days or larger than max_size_bytes:

crates/kernel/chio-kernel/src/receipt_store.rs20-37rust
pub struct RetentionConfig {
    /// Number of days to retain receipts in the live database. Default: 90.
    pub retention_days: u64,
    /// Maximum size in bytes before the live database is rotated. Default: 10 GB.
    pub max_size_bytes: u64,
    /// Path for the archive SQLite file. Must be writable on first rotation.
    pub archive_path: String,
    /// Optional tenant scope for retention. When set, rotation only archives
    /// receipts for this tenant and leaves other tenant evidence untouched.
    pub tenant_id: Option<String>,
    /// How often the kernel maintenance task evaluates rotation, in seconds.
    /// Default: 3600 (one hour).
    pub check_interval_secs: u64,
    /// Internal: set by `archive_receipts_before` to bypass the day/size
    /// threshold and rotate at an explicit cutoff. Not part of any wire form
    /// (no serialized representation of `RetentionConfig` exists).
    pub explicit_cutoff_unix_secs: Option<u64>,
}
crates/kernel/chio-kernel/src/receipt_store.rs39-50rust
impl Default for RetentionConfig {
    fn default() -> Self {
        Self {
            retention_days: 90,
            max_size_bytes: 10_737_418_240,
            archive_path: "receipts-archive.sqlite3".to_string(),
            tenant_id: None,
            check_interval_secs: 3_600,
            explicit_cutoff_unix_secs: None,
        }
    }
}

A kernel maintenance worker evaluates rotation every check_interval_secs (default hourly). Rotation moves aged-out receipts into a separate SQLite archive file, written only by rotation, while keeping their checkpoint relationships intact: read that file directly and an archived receipt still re-derives the signed Merkle root that committed it. The live store, having deleted those rows, does not. The full mechanism, including the checkpoint-aligned watermark, the co-archive-then-delete transaction, and why a tenant_id scope is refused rather than honored, is Retention & Archive.


Merkle checkpoints

Periodically the kernel batches receipts into a Merkle tree and signs the root. The leaves are the canonical bytes of the receipts in the batch, so a checkpoint commits to exactly the bytes a verifier can reproduce from the stored receipts. CHECKPOINT_SCHEMA, the identifier new issuance writes, is chio.checkpoint_statement.v2; chio.checkpoint_statement.v1 is the legacy statement without a chain commitment, and is_supported_checkpoint_schema accepts both.

crates/kernel/chio-kernel/src/checkpoint.rsrust
pub struct KernelCheckpointBody {
    /// Schema identifier for new checkpoint issuance.
    pub schema: String,
    /// Monotonic checkpoint counter.
    pub checkpoint_seq: u64,
    /// First receipt seq in this batch.
    pub batch_start_seq: u64,
    /// Last receipt seq in this batch.
    pub batch_end_seq: u64,
    /// Number of leaves in the Merkle tree.
    pub tree_size: usize,
    /// Root from MerkleTree::from_leaves.
    pub merkle_root: Hash,
    /// Unix timestamp (seconds) when the checkpoint was issued.
    pub issued_at: u64,
    /// The kernel's signing key (public).
    pub kernel_key: PublicKey,
    /// Hash of the immediately preceding checkpoint body when this checkpoint extends a prior batch.
    pub previous_checkpoint_sha256: Option<String>,
    /// RFC 6962 root over the checkpoint-chain leaves for checkpoint_seq 1
    /// through this checkpoint, one leaf per checkpoint binding its sequence,
    /// entry range, and batch root (see [`checkpoint_chain_leaf_hash`]). This
    /// is the commitment that consistency proofs verify against. Absent on
    /// v1 checkpoints and on detached v2 checkpoints built without chain
    /// context.
    pub chain_root: Option<Hash>,
}

Default batch size is DEFAULT_CHECKPOINT_BATCH_SIZE = 100, so the kernel emits a checkpoint every hundred receipts unless the operator overrides checkpoint_batch_size on kernel config. A value of 0 installs no background signer at all, which also means prefix retention can only archive what a checkpoint already covers; web3-enabled deployments refuse the value outright with web3-enabled deployments require checkpoint_batch_size > 0.

The chain links two ways. previous_checkpoint_sha256 hashes the predecessor body, so a deletion or a rewrite upstream invalidates every checkpoint downstream of the gap. chain_root is the stronger form: an RFC 6962 root over one leaf per checkpoint, each binding that checkpoint's sequence, entry range, and batch root, and it is what consistency proofs verify against. Issuing one costs O(log n) hashes because a long-lived writer keeps the frontier rather than rehashing the chain.

rendering
The path from a mediated tool call to a signed checkpoint. Anchoring a signed root outside the kernel is not drawn, because no step in the kernel performs it.
sourcecrates/core/chio-core-types/src/receipt/body.rs:246-311crates/kernel/chio-kernel/src/checkpoint.rs:1649-1683at fe56570

Inclusion proofs are ReceiptInclusionProof records that name a checkpoint, a leaf index, and a sibling-hash path. Anchoring the signed root to a transparency log, a blockchain, or a notary service happens outside the kernel: the checkpoint statement is portable evidence that the kernel committed to a specific receipt set at a specific time, and what an operator does with that evidence is a separate decision.

Receipts replicate across a cluster and checkpoints do not, so what a fleet can prove from many nodes' logs is narrower than what one node can prove from its own: Receipt Aggregation.


Multi-tenant isolation

tenant_id on ChioReceipt is the isolation key. Two rules govern its lifecycle:

  • Derived, not declared. The kernel sets tenant_id from the authenticated session's enterprise identity context. It is never taken from a caller-provided field on the request, because caller choice would defeat the isolation intent.
  • Scoped by an explicit read context. Whether a query sees untenanted (NULL tenant_id) rows is governed by ReceiptReadContext, which ReceiptQuery carries as read_context. Its include_null_tenant flag tracks the context's source, not its boundary. Both local-operator constructors set it true: local_operator_admin_all() (an AdminAll boundary) and local_operator_tenant(tenant) (a TenantScoped one). The remote-sourced constructors keep it false: admin_service() and authenticated_tenant(tenant). So an authenticated-tenant query does not see legacy NULL-tenant rows: NULL-row visibility follows the read context's source, an explicit decision, not a blanket tenant_filter behavior.

HTTP edge resolves the read context

At the HTTP edge, resolve ReceiptReadContext from the authenticated session, not from a query parameter, and treat tenant_filter as a narrowing filter that is never authority on its own. A caller-supplied boundary is a confused-deputy bug.

Worked example: verify a receipt

rust
use chio_core_types::receipt::ChioReceipt;

let receipt: ChioReceipt = serde_json::from_str(json_blob)?;

// Step 1: signature.
assert!(receipt.verify_signature()?, "signature did not verify");

// Step 2: action hash matches parameters.
assert!(receipt.action.verify_hash()?, "parameter_hash drift");

// Step 3: expected kernel key.
assert_eq!(receipt.kernel_key, expected_kernel_pubkey);

// Step 4: optional - inclusion proof against a checkpoint root.
// Build a MerkleTree from the canonical bytes of the batch range,
// then verify ReceiptInclusionProof.path against batch_root.

println!("decision: {:?}", receipt.decision);
for ev in &receipt.evidence {
    println!("  {} -> verdict={}", ev.guard_name, ev.verdict);
}

Next steps