@coproduct_inc/verify
v0.3.0
Published
Offline, zero-trust verification of agent capability-boundary in-bounds attestation receipts: emit and verify an Ed25519-signed, hash-chained receipt that an agent run stayed within a declared boundary, keyed on a cryptographic principal (not a mutable di
Maintainers
Readme
@coproduct_inc/verify
New here? Read WHY-VERIFY.md ("verify, don't trust" — the one-pager + Microsoft-AGT contrast), or run the 90-second demo:
bash examples/converting-demo.sh(clean attests · OpenClaw bypass refused · forged verdict caught · CI gate).
Offline, zero-trust verification for Nucleus. Two receipt families share one package:
- Capability-boundary in-bounds attestation (headline) — given an agent run's tool-call trace and a declared capability boundary, emit and offline-verify an Ed25519-signed, hash-chained receipt that the agent stayed within bounds. The boundary is keyed on a cryptographic principal, never a mutable display name — which is exactly what rules out OpenClaw-class trust-boundary bypass.
- Auction receipts — re-run the VCG/Vickrey clearing locally and assert the receipt's price (the package's original use case; documented at the bottom).
In-bounds attestation — the drop-in
import { verifyReceipt } from "@coproduct_inc/verify";
const report = verifyReceipt(receipt); // a parsed Receipt object
if (!report.ok) throw new Error(report.reason); // refuses to attest
// report.ok === signatureOk && rootHashOk && verdictConsistentOk && inBounds…or from JSON / the CLI:
nucleus-verify run-receipt.json --expect-principal spiffe://acme/agent/billing
# exit 0 → verified and in bounds | exit 1 → refused / out of bounds
cat run-receipt.json | nucleus-verify --jsonIn CI, via the bundled GitHub Action:
- uses: coproduct/nucleus-platform/packages/nucleus-verify@main
with:
receipt: ./agent-run-receipt.json
expect-principal: spiffe://acme/agent/billing-assistantThe Action resolves the verifier with
npx @coproduct_inc/verify, so it requires the package to be published to npm (orpackage-specpointed at a local tarball). Publishing is an operator step.
The OpenClaw guard, in one example
import { makeBoundary, signReceipt, verifyReceipt, generateKeypair } from "@coproduct_inc/verify";
const principal = "spiffe://acme/agent/billing-assistant";
const boundary = makeBoundary(principal, ["read_invoice", "list_invoices", "summarize"]);
const { privateKey } = generateKeypair();
// An injected call wears the allowlisted DISPLAY NAME but a different principal:
const trace = [
{ seq: 0, tool: "list_invoices", principal, displayName: "Billing Assistant" },
{ seq: 1, tool: "read_invoice", principal: "spiffe://acme/agent/unknown-7f3a", displayName: "Billing Assistant" },
];
const receipt = signReceipt(boundary, trace, privateKey);
verifyReceipt(receipt).ok; // → false: "out of bounds at event #1: … principal … but the boundary is bound to …"Run the full replay (clean attests · bypass refused · forged claim rejected):
pnpm build && pnpm demo # examples/openclaw-replay.mjsWhy it holds
recomputeVerdictis the soundness floor. It is pure and deterministic: an event is in-bounds iff itsprincipalequals the boundary's principal and itstoolis inallowedTools.displayNameis never consulted. A forgedinBounds: truecannot survive an independent re-run.- Two independent defenses against a tampered claim. The verifier recomputes the verdict and checks the Ed25519 signature over the canonical body (which commits to the verdict). Flipping
inBoundsbreaks both. - The permission fingerprint binds identity to capability.
permissionFingerprintis SHA-256 over the sorted, de-duplicatedallowedTools— mirroring the X.509 permission-fingerprint extension innucleus-tool-proxy/nucleus-identity(identity_fusion: "who you are" bound to "what you can do"). Widening the tool set changes the fingerprint and invalidates the boundary. - The hash-chain pins trace order.
rootHashishᵢ = SHA256(hᵢ₋₁ ‖ canonical(eventᵢ)); reorder or insert and the root changes.
Producer-side: from a real nucleus-tool-proxy trace
The receipt is emitted from the tool-proxy's actual authorization output, not a hand-authored fixture. A trace export pairs the declared boundary (a SPIFFE principal + the task-shield allowlist — portcullis_core::task_shield::TaskWitness's allow-set) with one record per observed call mirroring the verdict_sink tool_call span fields (actor, operation, verdict, subject, deny_reason):
{
"boundary": { "principal": "spiffe://acme/agent/billing", "allowedTools": ["read_files","glob_search","grep_search"] },
"records": [
{ "actor": "spiffe://acme/agent/billing", "operation": "read_files", "verdict": "allow", "subject": "invoices/2026-06.pdf" }
]
}Pipe it through the nucleus-attest producer to get a signed receipt, then verify:
# emit a REAL trace from portcullis (TaskWitness::permits decides each verdict):
cargo run -q -p nucleus-policy --example emit_toolproxy_trace -- clean \
| nucleus-attest \
| nucleus-verify - # exit 0: in bounds · exit 1: refusednucleus-attest also runs a cross-check: the proxy's own allow/deny per call must agree with the receipt's independently recomputed in-bounds verdict — a mismatch surfaces boundary drift. The producer never asserts in-bounds itself; it only records what happened (attestToolProxyRun/./toolproxy). The Rust emitter lives at crates/nucleus-policy/examples/emit_toolproxy_trace.rs.
Honest scope
This attests that the observed trace stayed within the declared boundary — integrity-axis, model-level evidence (the bar-2 IFC noninterference guarantee). It is not:
- an end-to-end proof of arbitrary agent behaviour, nor
- a guarantee that the trace is a complete record of what the agent did — completeness of capture is the instrumented runtime's responsibility.
It is the verifiable evidence layer that sits above probabilistic guardrails, not a replacement for them. The proof is the differentiator behind the policy; what you verify is the declared boundary.
Report fields
| field | meaning |
| --- | --- |
| ok | signatureOk && rootHashOk && verdictConsistentOk && inBounds (plus any pinned-key / pinned-principal checks) |
| signatureOk | Ed25519 signature verified under the receipt's public key |
| rootHashOk | SHA-256 hash-chain over events reproduces receipt.rootHash |
| verdictConsistentOk | independently recomputed verdict deep-equals the claimed verdict |
| inBounds | the recomputed verdict says every event is in bounds |
| recomputed | the verifier's own verdict (authoritative over the claim) |
| reason | first failing reason, or null when ok |
Auction receipts
The agent never trusts the hub's clearing price: the bundled WASM core (the same nucleus-wasm binary the browser demo runs) re-runs the auction clearing locally from the signed bid set and asserts the recomputed price equals the price the receipt claims — on top of the Ed25519 signature + BLAKE3 root-hash check.
import { verify } from "@coproduct_inc/verify";
const r = await verify(receiptJson, jwksJson);
if (!r.ok) throw new Error(r.reason ?? "receipt failed verification");
// r.ok === signature_ok && root_hash_ok && price_recomputed_okA signature proves the issuer signed those bytes; it does not prove the price in those bytes is the price the auction rules produce. verify() closes that gap by recomputing the clearing from the signed bids in the caller's own process. The committed price is single-good Vickrey (what the hub's receipt route runs), pinned to the real hub by a native parity test; the Pigou-VCG figure is parity-pinned to the 0-sorry Lean theorems and reported as a cross-check. See docs/BET-C-VERIFY-RECOMPUTE.md.
Build
pnpm install
pnpm build # tsc → dist/ (attestation core + CLI, zero runtime deps)
pnpm test # node --test against dist/ (attestation + auction)
pnpm demo # OpenClaw replay
pnpm build:wasm # regenerate wasm/nodejs from crates/nucleus-wasm (auction path only)The attestation core and CLI are pure Node (node:crypto, no WASM, no npm deps). The WASM artifact under wasm/nodejs/ (auction path) is committed so the package stays self-contained; pnpm build:wasm regenerates it byte-for-byte from the Rust crate.
