Inspect Your Receipts
Find a refusal in your project's history and verify the evidence on a machine that never saw your project.
On this page
Every decision your kernel made for the fixer left a signed receipt. Query them from your code, read why each call was allowed or refused, and hand the export to an auditor who has never seen your project.
Continues from Write a Policy, in the quickstart project. The fixer's history holds three issues and three refusals: the read of /secrets/.env in #412, the test write that tests-are-evidence refused in #413, and the fetch that deps-from-pypi refused in #415.
1. Find the fixer’s refusals
Ask the project’s history for every decision it denied the fixer, from your code or from the command line. Receipts arrive in the order the kernel signed them; the CLI prints each one as a line of JSON, ready for jq.
import { Computer } from "@chio-protocol/sdk";
const project = await Computer.open("quickstart");
const refusals = await project.history.receipts({ outcome: "deny", agent: "fixer" });
for (const receipt of refusals) console.log(receipt.summary);rcpt-01a8…9c04 deny fixer read /secrets/.env outside the grant rcpt-01b3…77e1 deny fixer write /workspace/tests/test_pricing.py rule tests-are-evidence rcpt-01c4…2e19 deny fixer run curl -sSL https://pkgs.example.net/bootstrap.sh rule deps-from-pypi
from chio import Computer
project = Computer.open("quickstart")
refusals = project.history.receipts(outcome="deny", agent="fixer")
for receipt in refusals:
print(receipt.summary)rcpt-01a8…9c04 deny fixer read /secrets/.env outside the grant rcpt-01b3…77e1 deny fixer write /workspace/tests/test_pricing.py rule tests-are-evidence rcpt-01c4…2e19 deny fixer run curl -sSL https://pkgs.example.net/bootstrap.sh rule deps-from-pypi
use chio::Computer;
let project = Computer::open("quickstart").await?;
let refusals = project.history().receipts().outcome("deny").agent("fixer").all().await?;
for receipt in &refusals {
println!("{}", receipt.summary());
}rcpt-01a8…9c04 deny fixer read /secrets/.env outside the grant rcpt-01b3…77e1 deny fixer write /workspace/tests/test_pricing.py rule tests-are-evidence rcpt-01c4…2e19 deny fixer run curl -sSL https://pkgs.example.net/bootstrap.sh rule deps-from-pypi
chio receipt list --agent fixer --outcome deny \
| jq -c '{id, tool: .tool_name, reason: .decision.reason}'{"id":"rcpt-01a8…9c04","tool":"read","reason":"not in capability scope"}
{"id":"rcpt-01b3…77e1","tool":"write","reason":"guard denied the request: rule tests-are-evidence"}
{"id":"rcpt-01c4…2e19","tool":"run","reason":"guard denied the request: rule deps-from-pypi"}You should see three refusals, one from each issue. Each line starts with the receipt ID, which selects one signed record however many times the fixer called the same operation.
2. Read why each one was refused
Describe the first two. Each names the request, the verdict and its reason, the grant it was checked against, and the policy in force. On the command line, chio receipt explain adds the scope difference for a scope refusal and a repair hint for both.
const [secrets, tests] = refusals;
console.log(secrets.describe());
console.log(tests.describe());receipt rcpt-01a8…9c04 request fixer read /secrets/.env verdict deny: outside the grant grant cap-01a8…31f2 policy sha256 3c7b…8010 signed your kernel 4f1c…a2e9, signature verifies receipt rcpt-01b3…77e1 request fixer write /workspace/tests/test_pricing.py verdict deny: rule tests-are-evidence grant cap-01b3…07e4, reissued from cap-01a8…31f2 policy sha256 9e42…c5d1 evidence tests-are-evidence: deny write /workspace/tests/** signed your kernel 4f1c…a2e9, signature verifies
secrets, tests = refusals[:2]
print(secrets.describe())
print(tests.describe())receipt rcpt-01a8…9c04 request fixer read /secrets/.env verdict deny: outside the grant grant cap-01a8…31f2 policy sha256 3c7b…8010 signed your kernel 4f1c…a2e9, signature verifies receipt rcpt-01b3…77e1 request fixer write /workspace/tests/test_pricing.py verdict deny: rule tests-are-evidence grant cap-01b3…07e4, reissued from cap-01a8…31f2 policy sha256 9e42…c5d1 evidence tests-are-evidence: deny write /workspace/tests/** signed your kernel 4f1c…a2e9, signature verifies
println!("{}", refusals[0].describe());
println!("{}", refusals[1].describe());receipt rcpt-01a8…9c04 request fixer read /secrets/.env verdict deny: outside the grant grant cap-01a8…31f2 policy sha256 3c7b…8010 signed your kernel 4f1c…a2e9, signature verifies receipt rcpt-01b3…77e1 request fixer write /workspace/tests/test_pricing.py verdict deny: rule tests-are-evidence grant cap-01b3…07e4, reissued from cap-01a8…31f2 policy sha256 9e42…c5d1 evidence tests-are-evidence: deny write /workspace/tests/** signed your kernel 4f1c…a2e9, signature verifies
chio receipt explain rcpt-01a8…9c04
chio receipt explain rcpt-01b3…77e1receipt: rcpt-01a8…9c04 decision: deny reason: not in capability scope policy_hash: 3c7b…8010 scope_diff: requested read /secrets/.env; granted read /workspace/**, invoke /models/default parents: 0 repair_hint: request a grant that covers the path; policy cannot widen a grant receipt: rcpt-01b3…77e1 decision: deny reason: guard denied the request: rule tests-are-evidence guard: tests-are-evidence policy_hash: 9e42…c5d1 parents: 0 repair_hint: inspect the rule named by guard, then change the request or the policy
You should see two different causes. The grant does not cover /secrets, so the kernel refused that read at the scope check, before any rule ran, and recorded no rule evidence. The grant does allow writes to /workspace, so the test write passed the scope check; the tests-are-evidence rule refused it, and the receipt names the rule as its evidence.
The cause tells you where to look. A scope refusal points to the grant and the requested resource. A rule refusal points to policy and the request’s inputs. No rule can widen a grant, so a scope refusal is never fixed in policy.
Read more in Receipts.
3. Compare an allowed record with a refused one
Find the read the fixer made just before it reached for your secrets.
const allowed = await project.history.receipts({ outcome: "allow" });
const read = allowed.find(
(receipt) => receipt.request === "fixer read /workspace/tests/test_pricing.py",
);
console.log(read?.describe());receipt rcpt-01a8…5b17 request fixer read /workspace/tests/test_pricing.py verdict allow grant cap-01a8…31f2 policy sha256 3c7b…8010 signed your kernel 4f1c…a2e9, signature verifies
allowed = project.history.receipts(outcome="allow")
read = next(
r for r in allowed if r.request == "fixer read /workspace/tests/test_pricing.py"
)
print(read.describe())receipt rcpt-01a8…5b17 request fixer read /workspace/tests/test_pricing.py verdict allow grant cap-01a8…31f2 policy sha256 3c7b…8010 signed your kernel 4f1c…a2e9, signature verifies
let allowed = project.history().receipts().outcome("allow").all().await?;
let read = allowed
.iter()
.find(|r| r.request() == "fixer read /workspace/tests/test_pricing.py")
.expect("the fixer read the failing test");
println!("{}", read.describe());receipt rcpt-01a8…5b17 request fixer read /workspace/tests/test_pricing.py verdict allow grant cap-01a8…31f2 policy sha256 3c7b…8010 signed your kernel 4f1c…a2e9, signature verifies
chio receipt list --agent fixer --outcome allow --tool-name read \
| jq -c 'select(.action.parameters.path == "/workspace/tests/test_pricing.py")
| {id, capability_id, policy_hash}'{"id":"rcpt-01a8…5b17","capability_id":"cap-01a8…31f2","policy_hash":"3c7b…8010"}You should see the grant and policy of the refused read under a different receipt ID. Verdicts repeat across records, but receipt IDs do not: each one is derived from its own record’s canonical body.
The test-write refusal carries a different policy hash, 9e42…c5d1, because you added tests-are-evidence between the two runs, and a different grant, because the second run used a reissued one. The policy hash identifies policy content without storing the rules, and a capability ID names a token without its scope. Keep your policy versions and grants beside the receipts when an investigation needs their contents.
4. Tell an execution from a preflight check
The read in step 3 ran: it appears in the fixer’s run events. chio check evaluates the same request against the grant and policy without dispatching it.
chio check --agent fixer read /workspace/tests/test_pricing.pymode preflight, nothing dispatched verdict allow receipt rcpt-01c9…0a3e, mediated_decision
You should see an allow and a new receipt. Both records carry receipt_kind: mediated_decision, so the kind alone cannot tell the run’s read from the check. Keep the command or request context with each record. An allow means the kernel authorized the operation; the run’s events and its test result are the separate observation that it happened.
5. Export the evidence and verify it elsewhere
Export the history to a fresh directory and verify the package.
const evidence = await project.history.export("evidence/", { agent: "fixer" });
console.log(await evidence.verify());files 3 of 3 match the manifest's SHA-256 ids 23 of 23 recomputed from their canonical bodies signatures 23 of 23 verify under kernel 4f1c…a2e9 refusals 3: read /secrets/.env, write tests/test_pricing.py, egress pkgs.example.net verified offline
evidence = project.history.export("evidence/", agent="fixer")
print(evidence.verify())files 3 of 3 match the manifest's SHA-256 ids 23 of 23 recomputed from their canonical bodies signatures 23 of 23 verify under kernel 4f1c…a2e9 refusals 3: read /secrets/.env, write tests/test_pricing.py, egress pkgs.example.net verified offline
let evidence = project.history().receipts().agent("fixer").export("evidence/")?;
println!("{}", evidence.verify()?);files 3 of 3 match the manifest's SHA-256 ids 23 of 23 recomputed from their canonical bodies signatures 23 of 23 verify under kernel 4f1c…a2e9 refusals 3: read /secrets/.env, write tests/test_pricing.py, egress pkgs.example.net verified offline
chio evidence export --agent fixer --output evidence
chio evidence verify --input evidencefiles 3 of 3 match the manifest's SHA-256 ids 23 of 23 recomputed from their canonical bodies signatures 23 of 23 verify under kernel 4f1c…a2e9 refusals 3: read /secrets/.env, write tests/test_pricing.py, egress pkgs.example.net verified offline
Send evidence/ to Corbel Assurance, your auditor. Their verifier needs the CLI, and neither your project nor a running kernel. They pin the kernel key you gave them:
chio evidence verify --input evidence --trusted-kernel-pubkey 4f1c…a2e9verified 23 receipts from kernel 4f1c…a2e9, offline
Each check is narrower than the last. File hashes show the package holds the bytes you exported. Recomputed IDs show each record matches its identifier. Signatures show each record verifies under the key it names, and the pin decides whether that key is yours: a package re-signed under another key passes every check inside it. No check confirms an external effect or shows that the package holds all the history a recipient needs.
6. Check that the log itself is whole
An export proves the records it holds. A checkpoint proves the log they came from: your kernel signs a Merkle root over a batch of receipts, and each checkpoint links to the one before it, so a record removed or rewritten afterward no longer matches a root that was already signed. The Quickstart project has not published one yet. Create the first, then verify the chain.
chio receipt checkpoint create
chio receipt checkpoint verifycheckpoint #1 covers receipts 1 to 23, root 7d0e…b4a1 chain 1 of 1 checkpoints link to their predecessor roots 1 of 1 Merkle roots recompute from their receipts signed your kernel 4f1c…a2e9, every checkpoint verifies
You should see one checkpoint covering all 23 receipts, with its root recomputed and its signature verified. Export again with --require-proofs and the package carries an inclusion proof for every receipt, so the auditor checks membership in a signed log as well as each signature.
Read more in Receipts.
7. Change one byte and watch verification fail
On the auditor's machine, copy the package, append one newline to the copy’s records, and verify it.
cp -R evidence evidence-tampered
printf '\n' >> evidence-tampered/receipts.ndjson
chio evidence verify --input evidence-tampered --trusted-kernel-pubkey 4f1c…a2e9error[CHIO-ATTEST-PROVENANCE-MISSING]: evidence package file hash mismatch for receipts.ndjson help: receipts.ndjson changed after the package was exported; verify the package as exported, or export it again code: urn:chio:error:attest:provenance-missing exit 1
You should see exit code 1 and a file-hash mismatch. The newline fails the first check, the manifest, before any signature is read. Edit a field instead and the recomputed ID fails; recompute the ID too and the signature fails. To recover, export again from your project rather than repairing a package by hand.
Read more in Receipts.