@coproduct_inc/verify 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +71 -0
- package/dist/attestation.browser.d.ts +1 -0
- package/dist/attestation.browser.js +3 -0
- package/dist/cage.d.ts +54 -0
- package/dist/cage.js +66 -0
- package/dist/claim-cli.d.ts +23 -0
- package/dist/claim-cli.js +185 -0
- package/dist/claim.d.ts +98 -0
- package/dist/claim.js +186 -0
- package/dist/ifc-decide.d.ts +20 -0
- package/dist/ifc-decide.js +18 -0
- package/dist/ifc.d.ts +52 -0
- package/dist/ifc.js +106 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +15 -0
- package/dist/workflow.d.ts +96 -0
- package/dist/workflow.js +201 -0
- package/examples/cage-quickstart.mjs +37 -0
- package/examples/workflow-leak.mjs +40 -0
- package/package.json +25 -5
- package/wasm/nodejs/nucleus_wasm.d.ts +15 -0
- package/wasm/nodejs/nucleus_wasm.js +28 -0
- package/wasm/nodejs/nucleus_wasm_bg.wasm +0 -0
- package/wasm/nodejs/nucleus_wasm_bg.wasm.d.ts +1 -0
- package/wasm/nodejs/package.json +1 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@coproduct_inc/verify`.
|
|
4
4
|
|
|
5
|
+
## 0.6.0
|
|
6
|
+
|
|
7
|
+
- **The `Workflow` flow check now CONSUMES the one proven IFC model** —
|
|
8
|
+
`nucleus-ifc`'s `FlowDeclaration → IfcVerdict` decision, compiled into the
|
|
9
|
+
package's wasm and exposed as `ifcDecide` (`./ifc-decide`). The DAG walk
|
|
10
|
+
accumulates each node's declared inputs (`secret`, `web_content`, …) and asks
|
|
11
|
+
the proven `decide()` whether they may reach a sink — the same decision
|
|
12
|
+
nucleus's production gate runs, recomputed locally. No TS mirror in the
|
|
13
|
+
decision path.
|
|
14
|
+
- New `FlowPolicy` axes `adversarialSources` + `actionSinks` enable **indirect
|
|
15
|
+
prompt-injection** detection: web content (Adversarial integrity, NoAuthority)
|
|
16
|
+
reaching an action sink is refused across the DAG; a declassifier breaks the
|
|
17
|
+
path. Existing confidentiality policies are unchanged.
|
|
18
|
+
- **IFC lattice helpers** (`./ifc`) — a TS reflection of the same Denning lattice
|
|
19
|
+
for direct in-process use, kept **parity-checked against `ifcDecide`** (and the
|
|
20
|
+
Lean-proven laws via `ifcLatticeLaws()`).
|
|
21
|
+
- Build: `nucleus-wasm` now depends on `nucleus-ifc` (lattice-only, no `ring`) and
|
|
22
|
+
exports `ifc_decide`; the `build:wasm` script preserves the wasm dir's
|
|
23
|
+
`commonjs` marker so the CJS bindings load inside this ESM package.
|
|
24
|
+
|
|
25
|
+
## 0.5.0
|
|
26
|
+
|
|
27
|
+
- **`Workflow` — the DAG cage** (`./workflow`, re-exported from root). Compose a
|
|
28
|
+
multi-agent run into ONE signed `WorkflowReceipt`: register nodes (each a
|
|
29
|
+
`Cage`) with `after` edges, then `wf.attest(privateKey)`. `verifyWorkflowReceipt`
|
|
30
|
+
checks it offline — signature + per-node in-bounds recompute + the DAG (acyclic,
|
|
31
|
+
real edges) + a **cross-node information-flow policy** (`{ sources, sinks,
|
|
32
|
+
declassifiers }`). The keystone: every node can be locally in-bounds yet the
|
|
33
|
+
workflow refuses a run where a secret laundered source→…→sink across the DAG —
|
|
34
|
+
the failure that only exists at multi-agent scale, caught at the orchestration
|
|
35
|
+
layer. See `examples/workflow-leak.mjs`.
|
|
36
|
+
|
|
37
|
+
## 0.4.0
|
|
38
|
+
|
|
39
|
+
- **`Cage` — the 5-minute producer drop-in** (`./cage`, re-exported from root).
|
|
40
|
+
A runtime cage wraps an agent's tool calls (`cage.tool(name, fn, { guard,
|
|
41
|
+
subject })`): out-of-boundary calls are blocked, every call is recorded, and
|
|
42
|
+
`cage.attest(privateKey)` emits a signed in-bounds receipt the existing
|
|
43
|
+
`verifyReceipt` / CLI checks offline. Closes the loop — producers cage,
|
|
44
|
+
consumers verify, nobody trusts the seller. See `examples/cage-quickstart.mjs`.
|
|
45
|
+
|
|
5
46
|
## 0.3.0
|
|
6
47
|
|
|
7
48
|
### Added
|
package/README.md
CHANGED
|
@@ -12,6 +12,77 @@ Offline, **zero-trust** verification for Nucleus. Two receipt families share one
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## Cage your agent — the 5-minute drop-in
|
|
16
|
+
|
|
17
|
+
You ship an agent that takes actions for a customer. They won't take your word
|
|
18
|
+
that it stayed in bounds. **Cage its tool calls; hand them a receipt they verify
|
|
19
|
+
themselves.** Useful to you alone today (a tighter agent + an artifact your
|
|
20
|
+
customer can check); more valuable as a network of recomputable receipts forms.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Cage, generateKeypair, verifyReceipt } from "@coproduct_inc/verify";
|
|
24
|
+
|
|
25
|
+
const cage = new Cage({
|
|
26
|
+
principal: "spiffe://acme/support-agent", // a stable id, not a name
|
|
27
|
+
allowedTools: ["search", "read_ticket", "refund"], // its capability boundary
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Wrap each tool. Out-of-boundary calls are blocked; a guard adds a runtime
|
|
31
|
+
// check (never refund over $50). Works for sync and async tools.
|
|
32
|
+
const search = cage.tool("search", realSearch);
|
|
33
|
+
const refund = cage.tool("refund", realRefund, { guard: (cents) => cents <= 5000 });
|
|
34
|
+
const wire = cage.tool("wire_transfer", realWire); // NOT granted
|
|
35
|
+
|
|
36
|
+
await search("refund policy"); // recorded: allow
|
|
37
|
+
try { await wire("0xbad"); } catch {} // BLOCKED + recorded — your customer sees the attempt
|
|
38
|
+
|
|
39
|
+
// Emit a signed receipt and hand it over:
|
|
40
|
+
const { privateKey } = generateKeypair();
|
|
41
|
+
const receipt = cage.attest(privateKey);
|
|
42
|
+
|
|
43
|
+
// THEY verify it — offline, no trust in you (or: npx @coproduct_inc/verify receipt.json):
|
|
44
|
+
const report = verifyReceipt(receipt); // report.ok === clean run, in bounds, signature valid
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run it end to end: `node examples/cage-quickstart.mjs`. The receipt attests the
|
|
48
|
+
**tool boundary** (every call was to a permitted tool); for argument-level
|
|
49
|
+
*proofs* ("amount < cap over all inputs") see the bytecode prover.
|
|
50
|
+
|
|
51
|
+
### Multi-agent? Compose the DAG — and catch what node-checks can't
|
|
52
|
+
|
|
53
|
+
A single agent you can eyeball; a DAG of agents you can't. The dangerous failure
|
|
54
|
+
only exists at DAG scale: **every node is locally in-bounds, yet a secret
|
|
55
|
+
launders from a source, through an innocent middle, to a public sink.** Per-node
|
|
56
|
+
review all passes; only the whole-workflow check catches it.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Workflow, verifyWorkflowReceipt, generateKeypair } from "@coproduct_inc/verify";
|
|
60
|
+
|
|
61
|
+
const wf = new Workflow("spiffe://acme/intake-pipeline");
|
|
62
|
+
const reader = wf.node({ id: "reader", boundary: ["read_secret", "emit"] });
|
|
63
|
+
const enrich = wf.node({ id: "enricher", boundary: ["transform", "emit"], after: ["reader"] });
|
|
64
|
+
const pub = wf.node({ id: "publisher", boundary: ["publish"], after: ["enricher"] });
|
|
65
|
+
// …run your agents through reader/enrich/pub's caged tools…
|
|
66
|
+
|
|
67
|
+
const receipt = wf.attest(generateKeypair().privateKey);
|
|
68
|
+
|
|
69
|
+
verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"] });
|
|
70
|
+
// → nodesOk: true (every node in-bounds) … but ok: false, flowOk: false:
|
|
71
|
+
// "information-flow leak: publisher→publish" — the secret reached the sink across the DAG.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It rides on whatever orchestrator you already use (LangGraph, Temporal, CrewAI…)
|
|
75
|
+
— you cage the tools, not the framework. Run it: `node examples/workflow-leak.mjs`.
|
|
76
|
+
|
|
77
|
+
The flow check is grounded in the **Denning IFC lattice** (`./ifc`, a faithful
|
|
78
|
+
port of `nucleus-ifc` whose lattice laws are Lean-proven). Beyond confidentiality
|
|
79
|
+
(`sources`/`sinks`), the `adversarialSources` + `actionSinks` axes catch
|
|
80
|
+
**indirect prompt injection** — web content (Adversarial integrity, no authority
|
|
81
|
+
to instruct) reaching an action sink is refused across the DAG, and a
|
|
82
|
+
declassifier breaks the path.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
15
86
|
## In-bounds attestation — the drop-in
|
|
16
87
|
|
|
17
88
|
```ts
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { verifyReceipt, verifyReceiptJson, recomputeVerdict, canonicalize, rootHash, makeBoundary, permissionFingerprint, } from "./attestation.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Browser-only entry: the SAME in-bounds attestation verifier, with node:crypto
|
|
2
|
+
// aliased to a @noble shim at bundle time. Verify-only (no signReceipt).
|
|
3
|
+
export { verifyReceipt, verifyReceiptJson, recomputeVerdict, canonicalize, rootHash, makeBoundary, permissionFingerprint, } from "./attestation.js";
|
package/dist/cage.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { KeyObject } from "node:crypto";
|
|
2
|
+
import type { Receipt } from "./attestation.js";
|
|
3
|
+
import { type ToolProxyExport } from "./toolproxy.js";
|
|
4
|
+
/** Thrown when a caged tool call violates policy (and is blocked). */
|
|
5
|
+
export declare class CageDenied extends Error {
|
|
6
|
+
readonly tool: string;
|
|
7
|
+
constructor(tool: string, message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface CageOptions {
|
|
10
|
+
/** Cryptographic principal the receipt is keyed on — a stable id (SPIFFE id,
|
|
11
|
+
* key fingerprint, anything), NOT a mutable display name. */
|
|
12
|
+
principal: string;
|
|
13
|
+
/** The tools this agent is permitted to call — its capability boundary. A call
|
|
14
|
+
* to anything else is denied and recorded. */
|
|
15
|
+
allowedTools: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface ToolOptions<A extends unknown[]> {
|
|
18
|
+
/** A per-call predicate to deny an in-boundary tool on specific arguments
|
|
19
|
+
* (e.g. only allow `pay` below a cap). Returning false → blocked at runtime +
|
|
20
|
+
* recorded as a deny. NOTE: the receipt attests the TOOL boundary (tool ∈
|
|
21
|
+
* allowedTools); a guard is finer RUNTIME hardening — a guard-denied call to
|
|
22
|
+
* an allowed tool is still "in bounds" in the receipt (the tool was permitted).
|
|
23
|
+
* Argument-level *proof* (e.g. "amount < cap over all inputs") is the
|
|
24
|
+
* bytecode prover's job, not this receipt's. */
|
|
25
|
+
guard?: (...args: A) => boolean;
|
|
26
|
+
/** Bind the call to its target (a URL, path, counterparty…) — recorded as a
|
|
27
|
+
* digest in the receipt, so the receipt is specific to what was acted on. */
|
|
28
|
+
subject?: (...args: A) => string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A runtime cage for an agent's tool calls. Wrap each tool with {@link Cage.tool};
|
|
32
|
+
* every call is policy-checked and recorded, and a denied call is BLOCKED. At the
|
|
33
|
+
* end {@link Cage.attest} signs an in-bounds receipt verifiable offline by this
|
|
34
|
+
* same package.
|
|
35
|
+
*/
|
|
36
|
+
export declare class Cage {
|
|
37
|
+
private readonly opts;
|
|
38
|
+
private readonly records;
|
|
39
|
+
constructor(opts: CageOptions);
|
|
40
|
+
/** Wrap a tool function so each call is caged + recorded. Works for sync and
|
|
41
|
+
* async tools (a denied call throws `CageDenied` before the tool runs). */
|
|
42
|
+
tool<A extends unknown[], R>(name: string, fn: (...args: A) => R, options?: ToolOptions<A>): (...args: A) => R;
|
|
43
|
+
/** Record a call you invoke yourself (when wrapping the fn isn't convenient). */
|
|
44
|
+
record(name: string, allowed: boolean, subject?: string): void;
|
|
45
|
+
/** How many calls have been caged so far. */
|
|
46
|
+
get callCount(): number;
|
|
47
|
+
/** The raw verdict log — the tool-proxy export the verifier consumes. */
|
|
48
|
+
toExport(): ToolProxyExport;
|
|
49
|
+
/** Sign an in-bounds attestation receipt. Verify it anywhere with
|
|
50
|
+
* `verifyReceipt` / the `@coproduct_inc/verify` CLI — no trust in you required. */
|
|
51
|
+
attest(privateKey: KeyObject, opts?: {
|
|
52
|
+
signedAt?: string;
|
|
53
|
+
}): Receipt;
|
|
54
|
+
}
|
package/dist/cage.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { attestToolProxyRun } from "./toolproxy.js";
|
|
2
|
+
/** Thrown when a caged tool call violates policy (and is blocked). */
|
|
3
|
+
export class CageDenied extends Error {
|
|
4
|
+
tool;
|
|
5
|
+
constructor(tool, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.tool = tool;
|
|
8
|
+
this.name = "CageDenied";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A runtime cage for an agent's tool calls. Wrap each tool with {@link Cage.tool};
|
|
13
|
+
* every call is policy-checked and recorded, and a denied call is BLOCKED. At the
|
|
14
|
+
* end {@link Cage.attest} signs an in-bounds receipt verifiable offline by this
|
|
15
|
+
* same package.
|
|
16
|
+
*/
|
|
17
|
+
export class Cage {
|
|
18
|
+
opts;
|
|
19
|
+
records = [];
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
}
|
|
23
|
+
/** Wrap a tool function so each call is caged + recorded. Works for sync and
|
|
24
|
+
* async tools (a denied call throws `CageDenied` before the tool runs). */
|
|
25
|
+
tool(name, fn, options) {
|
|
26
|
+
return (...args) => {
|
|
27
|
+
const inBoundary = this.opts.allowedTools.includes(name);
|
|
28
|
+
const guardOk = !options?.guard || options.guard(...args);
|
|
29
|
+
const ok = inBoundary && guardOk;
|
|
30
|
+
const rec = { operation: name, verdict: ok ? "allow" : "deny", actor: this.opts.principal };
|
|
31
|
+
const subject = options?.subject?.(...args);
|
|
32
|
+
if (subject)
|
|
33
|
+
rec.subject = subject;
|
|
34
|
+
this.records.push(rec);
|
|
35
|
+
if (!ok) {
|
|
36
|
+
throw new CageDenied(name, inBoundary
|
|
37
|
+
? `tool "${name}" denied by its guard on these arguments`
|
|
38
|
+
: `tool "${name}" is outside the agent's capability boundary {${this.opts.allowedTools.join(", ")}}`);
|
|
39
|
+
}
|
|
40
|
+
return fn(...args);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Record a call you invoke yourself (when wrapping the fn isn't convenient). */
|
|
44
|
+
record(name, allowed, subject) {
|
|
45
|
+
const rec = { operation: name, verdict: allowed ? "allow" : "deny", actor: this.opts.principal };
|
|
46
|
+
if (subject)
|
|
47
|
+
rec.subject = subject;
|
|
48
|
+
this.records.push(rec);
|
|
49
|
+
}
|
|
50
|
+
/** How many calls have been caged so far. */
|
|
51
|
+
get callCount() {
|
|
52
|
+
return this.records.length;
|
|
53
|
+
}
|
|
54
|
+
/** The raw verdict log — the tool-proxy export the verifier consumes. */
|
|
55
|
+
toExport() {
|
|
56
|
+
return {
|
|
57
|
+
boundary: { principal: this.opts.principal, allowedTools: this.opts.allowedTools },
|
|
58
|
+
records: [...this.records],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Sign an in-bounds attestation receipt. Verify it anywhere with
|
|
62
|
+
* `verifyReceipt` / the `@coproduct_inc/verify` CLI — no trust in you required. */
|
|
63
|
+
attest(privateKey, opts) {
|
|
64
|
+
return attestToolProxyRun(this.toExport(), privateKey, opts).receipt;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `nucleus-verify-claim` — the **proof gate**. Adjudicate one or more
|
|
4
|
+
* recompute-verified claims and use the exit code as a CI/merge gate.
|
|
5
|
+
*
|
|
6
|
+
* 0 → every claim verified (or no claim files found, unless --require)
|
|
7
|
+
* 1 → at least one claim failed verification
|
|
8
|
+
* 2 → usage / I/O error
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* nucleus-verify-claim <claim.json...> [--summary <file>] [--json]
|
|
12
|
+
* [--require] [--quiet]
|
|
13
|
+
*
|
|
14
|
+
* Each `<claim.json>` holds a `Claim` (or an array of them). Evidence may be
|
|
15
|
+
* inlined (`evidence.receipt`) or referenced by path (`evidence.receiptPath`,
|
|
16
|
+
* resolved relative to the claim file) so a repo commits a small claim that
|
|
17
|
+
* points at a checked-in receipt.
|
|
18
|
+
*
|
|
19
|
+
* Designed as a drop-in GitHub Action step: writes a Markdown table to
|
|
20
|
+
* `--summary` (point it at `$GITHUB_STEP_SUMMARY`) and a one-line-per-claim
|
|
21
|
+
* log, then fails the job if any claim did not recompute.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `nucleus-verify-claim` — the **proof gate**. Adjudicate one or more
|
|
4
|
+
* recompute-verified claims and use the exit code as a CI/merge gate.
|
|
5
|
+
*
|
|
6
|
+
* 0 → every claim verified (or no claim files found, unless --require)
|
|
7
|
+
* 1 → at least one claim failed verification
|
|
8
|
+
* 2 → usage / I/O error
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* nucleus-verify-claim <claim.json...> [--summary <file>] [--json]
|
|
12
|
+
* [--require] [--quiet]
|
|
13
|
+
*
|
|
14
|
+
* Each `<claim.json>` holds a `Claim` (or an array of them). Evidence may be
|
|
15
|
+
* inlined (`evidence.receipt`) or referenced by path (`evidence.receiptPath`,
|
|
16
|
+
* resolved relative to the claim file) so a repo commits a small claim that
|
|
17
|
+
* points at a checked-in receipt.
|
|
18
|
+
*
|
|
19
|
+
* Designed as a drop-in GitHub Action step: writes a Markdown table to
|
|
20
|
+
* `--summary` (point it at `$GITHUB_STEP_SUMMARY`) and a one-line-per-claim
|
|
21
|
+
* log, then fails the job if any claim did not recompute.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
24
|
+
import { dirname, resolve } from "node:path";
|
|
25
|
+
import { verifyClaim } from "./claim.js";
|
|
26
|
+
const HELP = `nucleus-verify-claim — recompute-verified proof gate
|
|
27
|
+
|
|
28
|
+
USAGE:
|
|
29
|
+
nucleus-verify-claim <claim.json...> [options]
|
|
30
|
+
|
|
31
|
+
OPTIONS:
|
|
32
|
+
--summary <file> append a Markdown result table to <file>
|
|
33
|
+
(use "$GITHUB_STEP_SUMMARY" inside a GitHub Action)
|
|
34
|
+
--json print the full verdicts as JSON to stdout
|
|
35
|
+
--require fail (exit 1) if NO claim files were supplied/matched
|
|
36
|
+
--quiet suppress per-claim log lines (exit code + summary only)
|
|
37
|
+
-h, --help show this help
|
|
38
|
+
|
|
39
|
+
EXIT CODES:
|
|
40
|
+
0 all claims verified 1 a claim failed / none found with --require 2 usage / I/O error`;
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const args = { paths: [], json: false, require: false, quiet: false, help: false };
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
switch (a) {
|
|
46
|
+
case "-h":
|
|
47
|
+
case "--help":
|
|
48
|
+
args.help = true;
|
|
49
|
+
break;
|
|
50
|
+
case "--json":
|
|
51
|
+
args.json = true;
|
|
52
|
+
break;
|
|
53
|
+
case "--require":
|
|
54
|
+
args.require = true;
|
|
55
|
+
break;
|
|
56
|
+
case "--quiet":
|
|
57
|
+
args.quiet = true;
|
|
58
|
+
break;
|
|
59
|
+
case "--summary":
|
|
60
|
+
args.summary = argv[++i];
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
if (a.startsWith("--"))
|
|
64
|
+
throw new Error(`unknown option ${a}`);
|
|
65
|
+
args.paths.push(a);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return args;
|
|
69
|
+
}
|
|
70
|
+
/** Load the claims from one file, resolving any `receiptPath` against it. */
|
|
71
|
+
function loadClaims(path) {
|
|
72
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
73
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
74
|
+
return entries.map((entry) => {
|
|
75
|
+
const ev = entry.evidence ?? {};
|
|
76
|
+
if (ev.receiptPath && ev.receipt === undefined) {
|
|
77
|
+
const receiptText = readFileSync(resolve(dirname(path), ev.receiptPath), "utf8");
|
|
78
|
+
// Keep object receipts as objects (in_bounds) and string receipts as
|
|
79
|
+
// strings (clearing wire form) — try to parse, fall back to raw text.
|
|
80
|
+
let receipt;
|
|
81
|
+
try {
|
|
82
|
+
receipt = JSON.parse(receiptText);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
receipt = receiptText;
|
|
86
|
+
}
|
|
87
|
+
return { ...entry, evidence: { ...ev, receipt } };
|
|
88
|
+
}
|
|
89
|
+
return entry;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function statusCell(v) {
|
|
93
|
+
return v.verified ? "✓ verified" : `✗ ${v.reason ?? "failed"}`;
|
|
94
|
+
}
|
|
95
|
+
function writeSummary(file, verdicts) {
|
|
96
|
+
const failed = verdicts.filter((v) => !v.verified).length;
|
|
97
|
+
const lines = [
|
|
98
|
+
"## Proof gate — recompute-verified claims",
|
|
99
|
+
"",
|
|
100
|
+
"| claim | kind | verdict |",
|
|
101
|
+
"| --- | --- | --- |",
|
|
102
|
+
...verdicts.map((v) => `| ${escapePipes(v.statement)} | \`${v.kind}\` | ${escapePipes(statusCell(v))} |`),
|
|
103
|
+
"",
|
|
104
|
+
failed === 0
|
|
105
|
+
? `**✓ all ${verdicts.length} claim${verdicts.length === 1 ? "" : "s"} recompute-verified.**`
|
|
106
|
+
: `**✗ ${failed} of ${verdicts.length} claim${verdicts.length === 1 ? "" : "s"} failed.**`,
|
|
107
|
+
"",
|
|
108
|
+
];
|
|
109
|
+
// `$GITHUB_STEP_SUMMARY` is appended to across steps; honor that.
|
|
110
|
+
const body = lines.join("\n");
|
|
111
|
+
try {
|
|
112
|
+
appendFileSync(file, body + "\n");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
writeFileSync(file, body + "\n");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function escapePipes(s) {
|
|
119
|
+
return s.replace(/\|/g, "\\|");
|
|
120
|
+
}
|
|
121
|
+
async function main() {
|
|
122
|
+
let args;
|
|
123
|
+
try {
|
|
124
|
+
args = parseArgs(process.argv.slice(2));
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
process.stderr.write(`error: ${e.message}\n\n${HELP}\n`);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
if (args.help) {
|
|
131
|
+
process.stdout.write(HELP + "\n");
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
if (args.paths.length === 0) {
|
|
135
|
+
if (args.require) {
|
|
136
|
+
process.stderr.write("error: no claim files supplied (--require)\n");
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write("proof gate: no claims to verify.\n");
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
let claims;
|
|
143
|
+
try {
|
|
144
|
+
claims = args.paths.flatMap(loadClaims);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
process.stderr.write(`error: cannot read claims: ${e.message}\n`);
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
const verdicts = [];
|
|
151
|
+
for (const claim of claims) {
|
|
152
|
+
verdicts.push(await verifyClaim(claim));
|
|
153
|
+
}
|
|
154
|
+
if (args.json) {
|
|
155
|
+
process.stdout.write(JSON.stringify(verdicts, null, 2) + "\n");
|
|
156
|
+
}
|
|
157
|
+
else if (!args.quiet) {
|
|
158
|
+
for (const v of verdicts) {
|
|
159
|
+
const mark = v.verified ? "✓" : "✗";
|
|
160
|
+
const stream = v.verified ? process.stdout : process.stderr;
|
|
161
|
+
stream.write(`${mark} [${v.kind}] ${v.statement} — ${statusCell(v)}\n`);
|
|
162
|
+
if (!v.verified) {
|
|
163
|
+
for (const step of v.trace.filter((s) => !s.ok)) {
|
|
164
|
+
stream.write(` ↳ ${step.check}${step.detail ? `: ${step.detail}` : ""}\n`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (args.summary) {
|
|
170
|
+
try {
|
|
171
|
+
writeSummary(args.summary, verdicts);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
process.stderr.write(`warning: could not write summary: ${e.message}\n`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const failed = verdicts.filter((v) => !v.verified).length;
|
|
178
|
+
if (!args.quiet) {
|
|
179
|
+
process.stdout.write(failed === 0
|
|
180
|
+
? `proof gate: ✓ ${verdicts.length} verified.\n`
|
|
181
|
+
: `proof gate: ✗ ${failed} of ${verdicts.length} failed.\n`);
|
|
182
|
+
}
|
|
183
|
+
return failed === 0 ? 0 : 1;
|
|
184
|
+
}
|
|
185
|
+
main().then((code) => process.exit(code));
|
package/dist/claim.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `claim.ts` — a **recompute-verified claim** façade.
|
|
3
|
+
*
|
|
4
|
+
* One uniform call — `verifyClaim(claim) → ClaimVerdict` — over the package's
|
|
5
|
+
* existing recompute engines, so any surface (a comment-box widget, a CI
|
|
6
|
+
* check, an MCP tool) can render a single ✓/✗ plus a human-readable recompute
|
|
7
|
+
* trace next to a claim, without knowing which engine adjudicated it.
|
|
8
|
+
*
|
|
9
|
+
* ## The honest boundary
|
|
10
|
+
*
|
|
11
|
+
* This verifies **only** claims that reduce to a *deterministic recomputation
|
|
12
|
+
* over committed evidence* — never arbitrary prose. A claim is adjudicable iff
|
|
13
|
+
* its value is the output of a pure function the caller can re-run from the
|
|
14
|
+
* cited inputs:
|
|
15
|
+
*
|
|
16
|
+
* - `clearing` — re-run an auction's clearing from its SIGNED bid set and
|
|
17
|
+
* check the receipt's price/winner (WASM kernel; the exact
|
|
18
|
+
* `verify` / `recomputeClearing` path).
|
|
19
|
+
* - `in_bounds` — re-run an agent run's capability verdict from its tool
|
|
20
|
+
* trace + declared boundary (pure-node attestation engine).
|
|
21
|
+
* - `signature` — authenticity only: Ed25519 + BLAKE3 root hash, no
|
|
22
|
+
* value recompute.
|
|
23
|
+
*
|
|
24
|
+
* Anything NOT reducible to a recompute (e.g. "10k users downloaded this")
|
|
25
|
+
* returns `verified: false` with an explicit `unsupported`/`reason` — it is
|
|
26
|
+
* never silently passed. `eval_score` and `dataset_aggregate` are the planned
|
|
27
|
+
* next kinds (see docs/RECOMPUTE-VERIFIED-CLAIMS.md); until wired they report
|
|
28
|
+
* unsupported rather than faking a check.
|
|
29
|
+
*
|
|
30
|
+
* ## Proof-carrying numbers
|
|
31
|
+
*
|
|
32
|
+
* When a claim carries an `asserted` value (the number a human wrote in prose,
|
|
33
|
+
* e.g. "cleared at 400000 µ$"), it is cross-checked against the recomputed
|
|
34
|
+
* value. A receipt that is authentic but whose prose number was altered fails
|
|
35
|
+
* (`verified: false`, `asserted.matched: false`) — the receipt can't lie, and
|
|
36
|
+
* neither can the sentence quoting it.
|
|
37
|
+
*/
|
|
38
|
+
/** The closed set of recomputable claim kinds. */
|
|
39
|
+
export type ClaimKind = "clearing" | "in_bounds" | "signature";
|
|
40
|
+
/** Evidence a claim is recomputed against. Shape depends on `kind`. */
|
|
41
|
+
export interface ClaimEvidence {
|
|
42
|
+
/**
|
|
43
|
+
* For `clearing` / `signature`: the auction receipt as a JSON **string**
|
|
44
|
+
* (the hub's wire form). For `in_bounds`: the attestation receipt **object**
|
|
45
|
+
* (as produced by `signReceipt` / returned from a tool-proxy run).
|
|
46
|
+
*/
|
|
47
|
+
receipt: string | Record<string, unknown>;
|
|
48
|
+
/** Pinned issuer JWKS JSON string. Required for `signature`; for `clearing`
|
|
49
|
+
* it upgrades the check from recompute-only to recompute + signature. */
|
|
50
|
+
jwks?: string;
|
|
51
|
+
/** `in_bounds`: pin the expected cryptographic principal / public key. */
|
|
52
|
+
expectPrincipal?: string;
|
|
53
|
+
expectPublicKey?: string;
|
|
54
|
+
}
|
|
55
|
+
/** An author-asserted value to cross-check against the recompute. */
|
|
56
|
+
export interface ClaimAssertion {
|
|
57
|
+
/** `clearing`: integer micro-USD price. `in_bounds`: boolean. */
|
|
58
|
+
value: number | boolean | string;
|
|
59
|
+
}
|
|
60
|
+
export interface Claim {
|
|
61
|
+
/** Human-readable statement rendered next to the ✓/✗. */
|
|
62
|
+
statement: string;
|
|
63
|
+
kind: ClaimKind;
|
|
64
|
+
evidence: ClaimEvidence;
|
|
65
|
+
/** Optional prose value to cross-check (proof-carrying numbers). */
|
|
66
|
+
asserted?: ClaimAssertion;
|
|
67
|
+
}
|
|
68
|
+
export interface ClaimTraceStep {
|
|
69
|
+
check: string;
|
|
70
|
+
ok: boolean;
|
|
71
|
+
detail?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ClaimVerdict {
|
|
74
|
+
/** The single boolean to gate the ✓/✗ on. */
|
|
75
|
+
verified: boolean;
|
|
76
|
+
kind: ClaimKind;
|
|
77
|
+
statement: string;
|
|
78
|
+
/** What the recompute produced (kind-specific), or null on a hard error. */
|
|
79
|
+
recomputed: {
|
|
80
|
+
label: string;
|
|
81
|
+
value: unknown;
|
|
82
|
+
} | null;
|
|
83
|
+
/** The author's asserted value + whether it matched the recompute. */
|
|
84
|
+
asserted: {
|
|
85
|
+
value: unknown;
|
|
86
|
+
matched: boolean;
|
|
87
|
+
} | null;
|
|
88
|
+
/** Ordered, human-readable trace of the checks that ran. */
|
|
89
|
+
trace: ClaimTraceStep[];
|
|
90
|
+
/** First failing reason, or null when verified. */
|
|
91
|
+
reason: string | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Adjudicate a claim by recomputation. Never throws — a malformed receipt,
|
|
95
|
+
* an unknown kind, or an engine error surface as `verified: false` with a
|
|
96
|
+
* `reason`, so the widget can always render a definite ✓/✗.
|
|
97
|
+
*/
|
|
98
|
+
export declare function verifyClaim(claim: Claim): Promise<ClaimVerdict>;
|