Guard Evidence
Find what a guard evidence row records, which producer wrote it, and what the signed receipt around it establishes.
On this page
Every signed receipt can carry, in ChioReceipt.evidence, the runtime controls’ own account of its decision: one GuardEvidence row per report of an admission decision or an output intervention. Guards attach rows to their decisions and output hooks attach rows to returned results; the kernel assembles the rows into the receipt, checks its decision fields, and signs it.
Another operator can then read which controls reported a refusal, an advisory signal, or a redaction beside the mediated request. Receipts covers the surrounding signed record, and Node Observability covers exported metrics, spans, and logs.
The pipeline names the child that denied
A forbidden-path child denies with no rows of its own. The composed pipeline appends action=deny; reason=guard denied request under the child’s name with verdict: false, and evaluation stops before dispatch, so no output rows follow. Pipelines and composition.
ChioReceipt.evidence, by guard name and verdict; details strings are abbreviated. Rows that custom guards and hooks attach follow the same two vectors and are left out.Try verdict, output-sanitizer, or receipt_body_fields_coupled. Each entry has its own link.
Evidence row fields
A GuardDecision carries a verdict and a vector of evidence rows. Admission uses the verdict; receipt assembly retains the rows, including rows collected before a denial. An allowed decision can carry evidence, and a denied decision can carry none.
guard_name
Names the producer or the control it reports. The wire schema requires at least one character and assigns no ownership of the name.
Example"forbidden-path"
verdict
Read it in the producer’s context. An advisory signal that was not promoted and a sanitizer redaction both record true.
Examplefalse
details
Free text supplied by the producer. The row defines no shared structured schema, timestamp, severity field, or version discriminator for it.
Example"action=deny; reason=guard denied request"
The wire schema rejects additional row properties. Serialization omits an empty evidence array, so consumers accept a missing evidence key.
Rust decision constructors and wire schema
pub struct GuardDecision {
pub verdict: Verdict,
pub evidence: Vec<GuardEvidence>,
}impl GuardDecision {
#[must_use]
pub fn allow() -> Self {
Self {
verdict: Verdict::Allow,
evidence: Vec::new(),
}
}
#[must_use]
pub fn allow_with_evidence(evidence: Vec<GuardEvidence>) -> Self {
Self {
verdict: Verdict::Allow,
evidence,
}
}
#[must_use]
pub fn deny(evidence: Vec<GuardEvidence>) -> Self {
Self {
verdict: Verdict::Deny,
evidence,
}
}
#[must_use]
pub fn pending_approval(evidence: Vec<GuardEvidence>) -> Self {
Self {
verdict: Verdict::PendingApproval,
evidence,
}
}
#[must_use]
pub fn from_verdict(verdict: Verdict) -> Self {
match verdict {
Verdict::Allow => Self::allow(),
Verdict::Deny => Self::deny(Vec::new()),
Verdict::PendingApproval => Self::pending_approval(Vec::new()),
}
}
}The serialized row is defined in crates/core/chio-core-types/src/receipt/metadata.rs. Its public schema is receipt/record.schema.json, under $defs.guardEvidence. The in-process GuardDecision is a separate type from the wire record.
Producer names
Native pipeline rows use the child’s Guard::name(), such as forbidden-path. The default sanitizer name is output-sanitizer, and each instance can change it. Treat these names as producer-controlled identifiers when you configure grouping, alerting, and webhook filters.
A name identifies a claim, and the guard implementation behind it is authenticated elsewhere. The row format has no registry or uniqueness constraint, and custom producers supply their own names. Schema examples that use Rust type names leave the runtime naming convention to Guard::name().
Refusal scope and guard name
Decision::Deny.guard often identifies the kernel scope or delivery gate that refused the request. An ordinary guard denial uses kernel; delivery failures can name delivery_contract or finding_delivery; a persistence-readiness refusal uses kernel.receipt_persistence.
When a GuardPipeline is registered on the kernel, the human-readable denial reason names that pipeline, and its synthesized evidence row names the denying child. Locate the child from the row, never by parsing the reason string.
Evidence producers
A guard may return an empty evidence vector. The composed pipeline still supplies a denial record, and advisory and post-invocation producers add different kinds of information.
GuardPipeline. Accumulates child rows and appends a row naming the child that denies or errors, before short-circuiting.AdvisoryPipeline. Converts each emitted signal to a row whose verdict is the inverse of itspromotedflag.SanitizerHook. Reports a redaction as an allowed result, with finding counts and detector IDs in the details string.- Custom guard or hook. Attaches whatever information its implementation exposes through the decision or the hook evidence channel.
Pipeline rows
For a child denial the composed pipeline adds action=deny; reason=guard denied request; for a child error it adds a fail-closed error description, action=error; reason=fail-closed; error=…. It keeps the evidence collected before the denial and the child’s own rows ahead of this record.
The kernel’s direct guard loop carries the evidence each guard returns and synthesizes no child row. Register a GuardPipeline, or attach the row in your guard, when downstream consumers need the denying control’s name.
A pending-approval verdict is sticky inside the composed pipeline, and later children can still deny. The ordinary kernel guard loop refuses that verdict as unsupported, because human approval has its own integration path; see Verdicts & Approval.
Output hook rows
A post-invocation hook exposes one row through take_evidence. The pipeline drains it after inspection and serializes evaluations, which protects that side channel from concurrent requests. A sanitizer redaction records verdict: true because the sanitized result remains deliverable.
The sanitizer reports detector IDs and counts and leaves the matched content out. A consumer that counts only false verdicts misses these redactions. Response Sanitization defines the output contract.
Diagnostics outside the evidence channel
Diagnostics become receipt rows only when a producer returns them. WASM runtime metrics and guard_evidence_metadata() stay separate from the returned guard decision. External provider helpers can construct evidence, but their asynchronous adapter returns a verdict, so an integrator attaches the helper’s result to carry it onto this receipt path.
Dispatch revalidation returns Result<(), KernelError> and adds no rows through that return value. The retained evidence is the set of records the admission and output paths supplied, which is narrower than a trace of every check.
Receipt assembly
The hosted kernel installs pre-invocation and post-invocation evidence around receipt construction. The builder concatenates the pre-invocation rows first, then the post-invocation rows, keeping collection order within each. Denial paths keep the rows gathered before the refusal.
let mut evidence = current_pre_invocation_guard_evidence();
evidence.extend(current_post_invocation_guard_evidence());The array participates in the content-addressed receipt ID and the signature. Editing, inserting, removing, or reordering a row changes the signed record, and verification against the original trusted signature and ID detects the change.
Signing-path checks
Before signing, receipt_body_fields_coupled compares the assembled body with ReceiptCouplingExpectation. The capability, server, tool, action parameters and hash, content hash, decision, policy hash, trust level, and mediated-decision semantic fields must agree, and a mismatch returns a receipt-signing error.
The signing handle also recomputes the content hash from the supplied canonical preimage and refuses a mismatch. These are runtime checks; the shared bounded predicate and its formal harnesses add a separate assurance layer, described in Assurance Model.
The coupling check’s evidence-class condition covers receipt semantics, such as its kind and boundary class. It leaves the ownership of each guard_name and the truth of its details to the configured producer and execution boundary: a trusted receipt signer attests to the assembled record, and assessing its claims also requires trust in those producers.
Hosted scopes, runtime source, and verification fixtures
The scoped evidence slots restore their prior contents when dropped, which supports nested receipt construction and cleanup. The evaluate and finalization paths install the applicable rows around terminal receipt construction; durable recovery and cancellation paths restore the evidence they carry before rebuilding records.
- Evidence scopes: installation and restoration of the two vectors.
- Runtime coupling: exact expected fields and semantic checks.
- Receipt assembly and signing: evidence concatenation, coupling, and the content-preimage handle.
kernel/tests/guard_pipeline.rs: allow, async allow, and deny fixtures that assert evidence on signed receipts.
The asynchronous fixture exercises an invocation that yields before completion and establishes that this path preserves evidence. The slot is a thread-local variable, which is a different mechanism from a task-local store.
Advisory rows
An advisory signal carries severity, description, promotion state, and optional metadata. signal_evidence flattens these into the details string of a row named for the advisory guard, such as anomaly-advisory, and sets verdict: !signal.promoted. A severe signal that remains advisory records true; promotion to a denial records false.
GuardOutput is a richer Rust representation with deterministic and advisory variants, and it is a different shape from ChioReceipt.evidence: the wire row has no variant discriminator, so consumers parse the three fields above. Advisory Guards covers signal promotion.
The details string
The pipeline writes semicolon-separated descriptions, the advisory producer embeds stringified JSON metadata, and the sanitizer emits a finding census. Treat details as opaque unless you control the producer’s format and version; a universal parser that splits every row on semicolons or equals signs misreads arbitrary descriptions.
Keep custom details concise, and leave request secrets and matched sensitive content out of them. The schema sets no row-count or details-length maximum, and these bytes participate in hashing and retention, so an embedding sets its own evidence-size limits.
Export and alerting considerations
OCSF exports preserve each row as an enrichment whose name contains its array index. Datadog tags and webhook filters use the guard name. A name change can affect routing, and an ordering change can affect index-based enrichment keys, so version your consumer assumptions with the configured pipeline.
Guard metrics use their own emitter and label contracts. Check that integration’s coverage before joining a metric series one-to-one with this array.
HTTP receipt evidence
The HTTP mediation edge has an HttpReceipt with its own signature and projected evidence. Names such as CapabilityGuard and DefaultPolicyGuard describe that edge’s admission logic and identify no registered native guard.
When present, the metadata field chio_kernel_receipt_id links to the kernel record; use that ID to inspect the kernel’s evidence. Converting an HTTP receipt with a kernel keypair creates and signs a new kernel record, a separate attestation from the original signature. Transport refusals before kernel evaluation can have no projected rows.
Pipeline denial example
When fixer reads /workspace/.env, a path its grant covers, the forbidden-path child denies with no rows of its own, and the composed pipeline adds this record to the receipt:
[
{
"guard_name": "forbidden-path",
"verdict": false,
"details": "action=deny; reason=guard denied request"
}
]Read the same rows from the receipt store. chio receipt list prints each receipt as one line of JSON, and the projection keeps the path and the evidence array:
chio receipt list --agent fixer --outcome deny --tool-name read_file \
| jq -c '{path: .action.parameters.path, evidence}'{"path":"/workspace/.env","evidence":[{"guard_name":"forbidden-path","verdict":false,"details":"action=deny; reason=guard denied request"}]}The row identifies the refusing child. The forbidden path, the other guards that ran, and the checks before it live elsewhere, so read the receipt’s request and policy context beside it. Testing Guards & Policies reproduces a denial.
An allow with no rows is a valid decision record, and so is a refusal that stopped before the pipeline: the Quickstart fixer’s read of /secrets/.env failed the scope check, so its receipt carries no guard rows. Receipts reads that refusal field by field. A guard denial through a registered pipeline adds the diagnostic row.
What a verified record establishes
- The receipt verifies against an expected trusted signer. The signed body, including its evidence array, matches that signer’s attestation.
- Coupling and signing succeed. The checked decision fields agree with the supplied expectations, and the content hash matches the canonical preimage supplied to signing.
- A row names a guard. The receipt carries that name and its claim; authenticating the guard implementation and the name’s owner takes evidence beyond the row.
- The evidence array is absent. No rows were serialized, which leaves open whether checks ran.
- A row’s verdict is true. Read it with the producer contract: an advisory signal or a redaction may still have occurred.