# @coproduct_inc/verify

> **New here?** Read [WHY-VERIFY.md](./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:

1. **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.
2. **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).

---

## Cage your agent — the 5-minute drop-in

You ship an agent that takes actions for a customer. They won't take your word
that it stayed in bounds. **Cage its tool calls; hand them a receipt they verify
themselves.** Useful to you alone today (a tighter agent + an artifact your
customer can check); more valuable as a network of recomputable receipts forms.

```ts
import { Cage, generateKeypair, verifyReceipt } from "@coproduct_inc/verify";

const cage = new Cage({
  principal: "spiffe://acme/support-agent",          // a stable id, not a name
  allowedTools: ["search", "read_ticket", "refund"], // its capability boundary
});

// Wrap each tool. Out-of-boundary calls are blocked; a guard adds a runtime
// check (never refund over $50). Works for sync and async tools.
const search = cage.tool("search", realSearch);
const refund = cage.tool("refund", realRefund, { guard: (cents) => cents <= 5000 });
const wire   = cage.tool("wire_transfer", realWire);  // NOT granted

await search("refund policy");        // recorded: allow
try { await wire("0xbad"); } catch {} // BLOCKED + recorded — your customer sees the attempt

// Emit a signed receipt and hand it over:
const { privateKey } = generateKeypair();
const receipt = cage.attest(privateKey);

// THEY verify it — offline, no trust in you (or: npx @coproduct_inc/verify receipt.json):
const report = verifyReceipt(receipt);  // report.ok === clean run, in bounds, signature valid
```

Run it end to end: `node examples/cage-quickstart.mjs`. The receipt attests the
**tool boundary** (every call was to a permitted tool); for argument-level
*proofs* ("amount < cap over all inputs") see the bytecode prover.

**No code? The `nucleus-cage` CLI.** Replay an agent's tool-call log through the
cage and get a signed, verifiable receipt — a 60-second CI step:

```sh
# calls.json: [ { "tool": "search", "subject": "refund policy" }, { "tool": "pay" } ]
npx -p @coproduct_inc/verify nucleus-cage calls.json \
  --principal spiffe://acme/support-agent --allow search,read_ticket > receipt.json
# → exit 0 in-bounds · exit 1 if any call left the boundary (and is recorded)
npx @coproduct_inc/verify receipt.json          # the customer rechecks, offline
npx -p @coproduct_inc/verify nucleus-cage calls.json \
  --principal … --allow … --badge https://you/recheck   # paste-able README badge
```

### Multi-agent? Compose the DAG — and catch what node-checks can't

A single agent you can eyeball; a DAG of agents you can't. The dangerous failure
only exists at DAG scale: **every node is locally in-bounds, yet a secret
launders from a source, through an innocent middle, to a public sink.** Per-node
review all passes; only the whole-workflow check catches it.

```ts
import { Workflow, verifyWorkflowReceipt, generateKeypair } from "@coproduct_inc/verify";

const wf = new Workflow("spiffe://acme/intake-pipeline");
const reader = wf.node({ id: "reader",    boundary: ["read_secret", "emit"] });
const enrich = wf.node({ id: "enricher",  boundary: ["transform", "emit"], after: ["reader"] });
const pub    = wf.node({ id: "publisher", boundary: ["publish"],          after: ["enricher"] });
// …run your agents through reader/enrich/pub's caged tools…

const receipt = wf.attest(generateKeypair().privateKey);

verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"] });
// → nodesOk: true (every node in-bounds) … but ok: false, flowOk: false:
//   "information-flow leak: publisher→publish" — the secret reached the sink across the DAG.
```

It rides on whatever orchestrator you already use (LangGraph, Temporal, CrewAI…)
— you cage the tools, not the framework. Run it: `node examples/workflow-leak.mjs`.

The flow check is grounded in the **Denning IFC lattice** (`./ifc`, a faithful
port of `nucleus-ifc` whose lattice laws are Lean-proven). Beyond confidentiality
(`sources`/`sinks`), the `adversarialSources` + `actionSinks` axes catch
**indirect prompt injection** — web content (Adversarial integrity, no authority
to instruct) reaching an action sink is refused across the DAG, and a
declassifier breaks the path.

---

## In-bounds attestation — the drop-in

```ts
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:

```sh
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 --json
```

In CI, via the bundled GitHub Action:

```yaml
- uses: coproduct/nucleus-platform/packages/nucleus-verify@main
  with:
    receipt: ./agent-run-receipt.json
    expect-principal: spiffe://acme/agent/billing-assistant
```

> The Action resolves the verifier with `npx @coproduct_inc/verify`, so it requires the package to be published to npm (or `package-spec` pointed at a local tarball). Publishing is an operator step.

### The OpenClaw guard, in one example

```ts
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):

```sh
pnpm build && pnpm demo   # examples/openclaw-replay.mjs
```

### Why it holds

- **`recomputeVerdict` is the soundness floor.** It is pure and deterministic: an event is in-bounds iff its **`principal`** equals the boundary's principal **and** its `tool` is in `allowedTools`. `displayName` is never consulted. A forged `inBounds: true` cannot 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 `inBounds` breaks both.
- **The permission fingerprint binds identity to capability.** `permissionFingerprint` is SHA-256 over the sorted, de-duplicated `allowedTools` — mirroring the X.509 permission-fingerprint extension in `nucleus-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.** `rootHash` is `hᵢ = 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`):

```jsonc
{
  "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:

```sh
# 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: refused
```

`nucleus-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.

```ts
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_ok
```

A 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`](../../docs/BET-C-VERIFY-RECOMPUTE.md).

---

## Build

```sh
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.
