Chio/Docs
LOGIN · JOIN

Inspect Your Receipts

Find a refusal in your project's history and verify the evidence on a machine that never saw your project.

Guide chio@5f63e6e

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.

Start

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.

receipts.tsreceipts.pysrc/main.rs · in mainTerminal
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);
Output
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

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.

receipts.tsreceipts.pysrc/main.rs · in mainTerminal
const [secrets, tests] = refusals;
console.log(secrets.describe());
console.log(tests.describe());
Output
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

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.

receipts.tsreceipts.pysrc/main.rs · in mainTerminal
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());
Output
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

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.

Terminalbash
chio check --agent fixer read /workspace/tests/test_pricing.py
Output
mode       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.

receipts.tsreceipts.pysrc/main.rs · in mainTerminal
const evidence = await project.history.export("evidence/", { agent: "fixer" });
console.log(await evidence.verify());
Output
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

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:

Corbel Assurancebash
chio evidence verify --input evidence --trusted-kernel-pubkey 4f1c…a2e9
Output
verified   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.

Terminalbash
chio receipt checkpoint create
chio receipt checkpoint verify
Output
checkpoint  #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.

Corbel Assurancebash
cp -R evidence evidence-tampered
printf '\n' >> evidence-tampered/receipts.ndjson
chio evidence verify --input evidence-tampered --trusted-kernel-pubkey 4f1c…a2e9
Output
error[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.

Next: connect an application