@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/dist/workflow.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// The DAG cage: compose a multi-agent run into ONE verifiable workflow receipt.
|
|
2
|
+
//
|
|
3
|
+
// A single agent you can eyeball; a DAG of agents you can't. The dangerous
|
|
4
|
+
// failure mode only exists at DAG scale: every node is locally in-bounds, yet a
|
|
5
|
+
// secret laundered from a source node, through an innocent middle, to a sink.
|
|
6
|
+
// Per-node checks ALL pass; only the whole-workflow check catches it. This is
|
|
7
|
+
// that check, at the orchestration layer — a receipt-level analog of the
|
|
8
|
+
// bytecode noninterference proof, that rides on whatever orchestrator you use.
|
|
9
|
+
//
|
|
10
|
+
// const wf = new Workflow("spiffe://acme/intake-pipeline");
|
|
11
|
+
// const reader = wf.node({ id: "reader", boundary: ["read_secret", "emit"] });
|
|
12
|
+
// const middle = wf.node({ id: "middle", boundary: ["transform", "emit"], after: ["reader"] });
|
|
13
|
+
// const writer = wf.node({ id: "writer", boundary: ["publish"], after: ["middle"] });
|
|
14
|
+
// …run your agents, calling these caged tools…
|
|
15
|
+
// const receipt = wf.attest(privateKey);
|
|
16
|
+
//
|
|
17
|
+
// // your customer verifies the whole DAG offline, including the flow policy:
|
|
18
|
+
// verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"] });
|
|
19
|
+
// // → flowOk:false if a secret could reach `publish`, even though every node was in-bounds.
|
|
20
|
+
import { createHash, createPublicKey, sign as edSign, verify as edVerify } from "node:crypto";
|
|
21
|
+
import { Cage } from "./cage.js";
|
|
22
|
+
import { canonicalize, exportPublicKey } from "./attestation.js";
|
|
23
|
+
import { traceEventsFromRecords } from "./toolproxy.js";
|
|
24
|
+
import { ifcDecide } from "./ifc-decide.js";
|
|
25
|
+
const WF_KIND = "nucleus.workflow-receipt.v1";
|
|
26
|
+
function sha256Hex(s) {
|
|
27
|
+
return createHash("sha256").update(s).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
function importPublicKey(x) {
|
|
30
|
+
return createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x }, format: "jwk" });
|
|
31
|
+
}
|
|
32
|
+
/** Topological order of the node ids; throws on a cycle. */
|
|
33
|
+
function topoOrder(nodes) {
|
|
34
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
35
|
+
const indeg = new Map();
|
|
36
|
+
const children = new Map();
|
|
37
|
+
for (const n of nodes) {
|
|
38
|
+
indeg.set(n.id, 0);
|
|
39
|
+
children.set(n.id, []);
|
|
40
|
+
}
|
|
41
|
+
for (const n of nodes) {
|
|
42
|
+
for (const p of n.after) {
|
|
43
|
+
if (!ids.has(p))
|
|
44
|
+
throw new Error(`edge from unknown node "${p}"`);
|
|
45
|
+
indeg.set(n.id, (indeg.get(n.id) ?? 0) + 1);
|
|
46
|
+
children.get(p).push(n.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const queue = [...indeg].filter(([, d]) => d === 0).map(([id]) => id);
|
|
50
|
+
const order = [];
|
|
51
|
+
while (queue.length) {
|
|
52
|
+
const id = queue.shift();
|
|
53
|
+
order.push(id);
|
|
54
|
+
for (const c of children.get(id)) {
|
|
55
|
+
indeg.set(c, indeg.get(c) - 1);
|
|
56
|
+
if (indeg.get(c) === 0)
|
|
57
|
+
queue.push(c);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (order.length !== nodes.length)
|
|
61
|
+
throw new Error("workflow graph has a cycle");
|
|
62
|
+
return order;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Recompute the information flow across the DAG, delegating each sink's VERDICT
|
|
66
|
+
* to the proven model (`ifcDecide`, compiled from nucleus-ifc). The DAG walk
|
|
67
|
+
* accumulates the declared-input KINDS reaching each node (a `source` tool
|
|
68
|
+
* declares a `secret` input, an `adversarialSource` declares `web_content`); a
|
|
69
|
+
* declassifier clears them; at a sink we ask the proven `decide()` whether that
|
|
70
|
+
* input set may reach that sink. So the topology is ours, the IFC decision is
|
|
71
|
+
* the one nucleus runs — not a TS mirror.
|
|
72
|
+
*/
|
|
73
|
+
function recomputeFlow(nodes, flow) {
|
|
74
|
+
const sources = new Set(flow.sources);
|
|
75
|
+
const adversarial = new Set(flow.adversarialSources ?? []);
|
|
76
|
+
const sinks = new Set(flow.sinks);
|
|
77
|
+
const actionSinks = new Set(flow.actionSinks ?? []);
|
|
78
|
+
const declass = new Set(flow.declassifiers ?? []);
|
|
79
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
80
|
+
const inputsAt = new Map();
|
|
81
|
+
const leaks = [];
|
|
82
|
+
for (const id of topoOrder(nodes)) {
|
|
83
|
+
const n = byId.get(id);
|
|
84
|
+
const tools = new Set(n.events.map((e) => e.tool));
|
|
85
|
+
const acc = new Set();
|
|
86
|
+
for (const p of n.after)
|
|
87
|
+
for (const t of inputsAt.get(p))
|
|
88
|
+
acc.add(t); // inherit upstream inputs
|
|
89
|
+
const readsSecret = [...tools].some((t) => sources.has(t));
|
|
90
|
+
const readsWeb = [...tools].some((t) => adversarial.has(t));
|
|
91
|
+
if (readsSecret)
|
|
92
|
+
acc.add("secret");
|
|
93
|
+
if (readsWeb)
|
|
94
|
+
acc.add("web_content");
|
|
95
|
+
if (declass.has(id))
|
|
96
|
+
acc.clear(); // a declassifier sanitizes incoming + own inputs
|
|
97
|
+
inputsAt.set(id, acc);
|
|
98
|
+
const declared = [...acc];
|
|
99
|
+
const via = (readsSecret || readsWeb) && !declass.has(id) ? "reads-source" : "upstream";
|
|
100
|
+
for (const t of tools) {
|
|
101
|
+
if (sinks.has(t) && !ifcDecide(declared, { sinkPublic: true }).allow)
|
|
102
|
+
leaks.push({ node: id, sink: t, taintedVia: via });
|
|
103
|
+
if (actionSinks.has(t) && !ifcDecide(declared, { requiresAuthority: true }).allow)
|
|
104
|
+
leaks.push({ node: id, sink: t, taintedVia: via });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return leaks;
|
|
108
|
+
}
|
|
109
|
+
function nodesForHash(nodes) {
|
|
110
|
+
return nodes.map((n) => ({ id: n.id, after: n.after, boundary: n.boundary, events: n.events }));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A DAG cage. Register nodes (each gets a {@link Cage}), run your agents through
|
|
114
|
+
* the caged tools, then {@link Workflow.attest} a single signed receipt over the
|
|
115
|
+
* whole graph — verifiable offline, including the cross-node information flow.
|
|
116
|
+
*/
|
|
117
|
+
export class Workflow {
|
|
118
|
+
principal;
|
|
119
|
+
nodes = new Map();
|
|
120
|
+
constructor(principal) {
|
|
121
|
+
this.principal = principal;
|
|
122
|
+
}
|
|
123
|
+
/** Register a node and return its cage. The cage's tool calls are recorded
|
|
124
|
+
* under this node; `after` declares the edges into it. */
|
|
125
|
+
node(spec) {
|
|
126
|
+
if (this.nodes.has(spec.id))
|
|
127
|
+
throw new Error(`duplicate node id "${spec.id}"`);
|
|
128
|
+
const cage = new Cage({ principal: `${this.principal}#${spec.id}`, allowedTools: spec.boundary });
|
|
129
|
+
this.nodes.set(spec.id, { id: spec.id, after: spec.after ?? [], boundary: spec.boundary, cage });
|
|
130
|
+
return cage;
|
|
131
|
+
}
|
|
132
|
+
receiptNodes() {
|
|
133
|
+
return [...this.nodes.values()].map((n) => {
|
|
134
|
+
const events = traceEventsFromRecords(n.cage.toExport().records);
|
|
135
|
+
const inBounds = events.every((e) => n.boundary.includes(e.tool));
|
|
136
|
+
return { id: n.id, after: n.after, boundary: n.boundary, events, inBounds };
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Sign one workflow receipt over the whole DAG. */
|
|
140
|
+
attest(privateKey, opts = {}) {
|
|
141
|
+
const nodes = this.receiptNodes();
|
|
142
|
+
const rootHash = sha256Hex(canonicalize(nodesForHash(nodes)));
|
|
143
|
+
const pub = createPublicKey(privateKey);
|
|
144
|
+
const body = canonicalize({ kind: WF_KIND, principal: this.principal, rootHash });
|
|
145
|
+
const signature = edSign(null, Buffer.from(body, "utf8"), privateKey).toString("base64");
|
|
146
|
+
return {
|
|
147
|
+
kind: WF_KIND,
|
|
148
|
+
principal: this.principal,
|
|
149
|
+
nodes,
|
|
150
|
+
rootHash,
|
|
151
|
+
publicKey: exportPublicKey(pub),
|
|
152
|
+
signature,
|
|
153
|
+
...(opts.signedAt ? { signedAt: opts.signedAt } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Verify a workflow receipt fully and offline: the signature + the per-node
|
|
159
|
+
* in-bounds recompute + the DAG (acyclic, real edges) + the cross-node
|
|
160
|
+
* information flow. Never throws — failures surface as `ok:false` + `reason`.
|
|
161
|
+
*/
|
|
162
|
+
export function verifyWorkflowReceipt(wr, flow) {
|
|
163
|
+
// 1. signature + rootHash (the receipt is bound to its node set).
|
|
164
|
+
const recomputedRoot = sha256Hex(canonicalize(nodesForHash(wr.nodes)));
|
|
165
|
+
const rootOk = recomputedRoot === wr.rootHash;
|
|
166
|
+
let sigOnly = false;
|
|
167
|
+
try {
|
|
168
|
+
const body = canonicalize({ kind: wr.kind, principal: wr.principal, rootHash: wr.rootHash });
|
|
169
|
+
sigOnly = edVerify(null, Buffer.from(body, "utf8"), importPublicKey(wr.publicKey), Buffer.from(wr.signature, "base64"));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
sigOnly = false;
|
|
173
|
+
}
|
|
174
|
+
const signatureOk = sigOnly && rootOk;
|
|
175
|
+
// 2. per-node in-bounds, recomputed from the signed events (not trusted).
|
|
176
|
+
const outOfBoundsNodes = wr.nodes.filter((n) => !n.events.every((e) => n.boundary.includes(e.tool))).map((n) => n.id);
|
|
177
|
+
const nodesOk = outOfBoundsNodes.length === 0;
|
|
178
|
+
// 3. DAG well-formed + 4. information flow.
|
|
179
|
+
let dagOk = true;
|
|
180
|
+
let leaks = [];
|
|
181
|
+
try {
|
|
182
|
+
topoOrder(wr.nodes); // throws on cycle / unknown edge
|
|
183
|
+
if (flow)
|
|
184
|
+
leaks = recomputeFlow(wr.nodes, flow);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
dagOk = false;
|
|
188
|
+
}
|
|
189
|
+
const flowOk = leaks.length === 0;
|
|
190
|
+
const ok = signatureOk && nodesOk && dagOk && flowOk;
|
|
191
|
+
let reason = null;
|
|
192
|
+
if (!signatureOk)
|
|
193
|
+
reason = rootOk ? "signature did not verify" : "rootHash does not match the node set (tampered)";
|
|
194
|
+
else if (!dagOk)
|
|
195
|
+
reason = "workflow graph is cyclic or references an unknown node";
|
|
196
|
+
else if (!nodesOk)
|
|
197
|
+
reason = `nodes out of bounds: ${outOfBoundsNodes.join(", ")}`;
|
|
198
|
+
else if (!flowOk)
|
|
199
|
+
reason = `information-flow leak: ${leaks.map((l) => `${l.node}→${l.sink}`).join(", ")}`;
|
|
200
|
+
return { ok, signatureOk, dagOk, nodesOk, flowOk, leaks, outOfBoundsNodes, reason };
|
|
201
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The 5-minute drop-in, end to end. Run: node examples/cage-quickstart.mjs
|
|
2
|
+
//
|
|
3
|
+
// You ship an AI agent that takes actions for a customer. They won't take your
|
|
4
|
+
// word that it stayed in bounds. So: cage its tool calls, and hand them a
|
|
5
|
+
// receipt they verify themselves — trust the math, not you.
|
|
6
|
+
import { Cage, generateKeypair, verifyReceipt } from "../dist/index.js";
|
|
7
|
+
|
|
8
|
+
// 1. Declare the agent's capability boundary (what it's allowed to touch).
|
|
9
|
+
const cage = new Cage({
|
|
10
|
+
principal: "spiffe://acme/support-agent", // any stable id keyed to a key, not a name
|
|
11
|
+
allowedTools: ["search", "read_ticket", "send_reply", "refund"],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// 2. Wrap each tool. A guard adds a finer runtime check (never refund over $50).
|
|
15
|
+
const search = cage.tool("search", async (q) => `(results for "${q}")`);
|
|
16
|
+
const reply = cage.tool("send_reply", async (msg) => `sent: ${msg}`);
|
|
17
|
+
const refund = cage.tool("refund", async (cents) => `refunded ${cents}c`, { guard: (c) => c <= 5000 });
|
|
18
|
+
const wire = cage.tool("wire_transfer", async (acct) => `wired to ${acct}`); // NOT granted
|
|
19
|
+
|
|
20
|
+
// 3. Run your agent — calling ONLY the caged tools.
|
|
21
|
+
await search("refund policy");
|
|
22
|
+
await reply("Here's our policy …");
|
|
23
|
+
try { await refund(999999); } catch (e) { console.log("⛔ guard blocked over-cap refund:", e.message); }
|
|
24
|
+
try { await wire("0xbad"); } catch (e) { console.log("⛔ blocked out-of-boundary tool:", e.message); }
|
|
25
|
+
|
|
26
|
+
// 4. Emit a signed receipt and hand it to your customer.
|
|
27
|
+
const { privateKey } = generateKeypair();
|
|
28
|
+
const receipt = cage.attest(privateKey);
|
|
29
|
+
|
|
30
|
+
// 5. THEY verify it — offline, no trust in you. (Or: npx @coproduct_inc/verify receipt.json)
|
|
31
|
+
const report = verifyReceipt(receipt);
|
|
32
|
+
console.log("\nreceipt verified:", report.ok ? "✓ clean run" : "✗ " + report.reason);
|
|
33
|
+
console.log(" signature ok :", report.signatureOk);
|
|
34
|
+
console.log(" in bounds :", report.inBounds, "(every call was to an allowed tool)");
|
|
35
|
+
console.log(" recorded calls:", receipt.events.map((e) => e.tool).join(", "));
|
|
36
|
+
console.log("\nThe receipt is the wedge: useful to you alone today, more valuable as a");
|
|
37
|
+
console.log("network of recomputable receipts forms. Don't trust the seller — recompute.");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The DAG cage, end to end — and the failure that only exists at multi-agent
|
|
2
|
+
// scale. Run: node examples/workflow-leak.mjs
|
|
3
|
+
//
|
|
4
|
+
// A single agent you can eyeball. A DAG of agents you can't: every node passes
|
|
5
|
+
// its OWN check, yet a secret launders through the pipeline to a public write.
|
|
6
|
+
// Per-node review misses it. The workflow receipt catches it.
|
|
7
|
+
import { Workflow, verifyWorkflowReceipt, generateKeypair } from "../dist/index.js";
|
|
8
|
+
|
|
9
|
+
// A 3-agent intake pipeline: reader → enricher → publisher.
|
|
10
|
+
const wf = new Workflow("spiffe://acme/intake-pipeline");
|
|
11
|
+
const reader = wf.node({ id: "reader", boundary: ["read_secret", "emit"] });
|
|
12
|
+
const enrich = wf.node({ id: "enricher", boundary: ["transform", "emit"], after: ["reader"] });
|
|
13
|
+
const publish = wf.node({ id: "publisher", boundary: ["publish"], after: ["enricher"] });
|
|
14
|
+
|
|
15
|
+
// Run your agents through the caged tools. Each node only touches tools it's
|
|
16
|
+
// allowed to — locally, everything is in policy.
|
|
17
|
+
reader.tool("read_secret", () => "customer SSN")(); // allowed for the reader
|
|
18
|
+
reader.tool("emit", (x) => x)();
|
|
19
|
+
enrich.tool("transform", (x) => x)(); // an "innocent" pass-through middle
|
|
20
|
+
enrich.tool("emit", (x) => x)();
|
|
21
|
+
publish.tool("publish", (x) => x)(); // allowed for the publisher
|
|
22
|
+
|
|
23
|
+
const { privateKey } = generateKeypair();
|
|
24
|
+
const receipt = wf.attest(privateKey);
|
|
25
|
+
|
|
26
|
+
// Your customer verifies the WHOLE DAG offline, with a flow policy:
|
|
27
|
+
// the secret source must never reach the public sink.
|
|
28
|
+
const report = verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"] });
|
|
29
|
+
|
|
30
|
+
console.log("per-node review (what a code reviewer sees):");
|
|
31
|
+
for (const n of receipt.nodes) console.log(` ${n.id.padEnd(10)} in-bounds: ${n.inBounds} (${n.events.map((e) => e.tool).join(", ")})`);
|
|
32
|
+
console.log("\nevery node is in-bounds:", report.nodesOk);
|
|
33
|
+
console.log("signature valid :", report.signatureOk);
|
|
34
|
+
console.log("\nwhole-workflow verdict:", report.ok ? "✓ ok" : "✗ " + report.reason);
|
|
35
|
+
for (const l of report.leaks) console.log(` ⛔ leak: a secret reaches "${l.sink}" at node "${l.node}" (${l.taintedVia})`);
|
|
36
|
+
|
|
37
|
+
console.log("\nThe per-node checks ALL passed. Only the composition catches it.");
|
|
38
|
+
console.log("Add a sanitizer? declare the middle a declassifier:");
|
|
39
|
+
const fixed = verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"], declassifiers: ["enricher"] });
|
|
40
|
+
console.log(" with enricher sanitizing →", fixed.ok ? "✓ ok (flow broken)" : "✗ " + fixed.reason);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coproduct_inc/verify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "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 display name)
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "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 display name) \u2014 the evidence layer above probabilistic guardrails. Also verifies Nucleus auction receipts by re-running the clearing locally. Zero runtime deps for the attestation core (node:crypto).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -9,13 +9,22 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"nucleus-verify": "dist/cli.js",
|
|
11
11
|
"nucleus-attest": "dist/producer-cli.js",
|
|
12
|
-
"nucleus-license": "dist/license-cli.js"
|
|
12
|
+
"nucleus-license": "dist/license-cli.js",
|
|
13
|
+
"nucleus-verify-claim": "dist/claim-cli.js"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
16
17
|
"types": "./dist/index.d.ts",
|
|
17
18
|
"import": "./dist/index.js"
|
|
18
19
|
},
|
|
20
|
+
"./workflow": {
|
|
21
|
+
"types": "./dist/workflow.d.ts",
|
|
22
|
+
"import": "./dist/workflow.js"
|
|
23
|
+
},
|
|
24
|
+
"./ifc": {
|
|
25
|
+
"types": "./dist/ifc.d.ts",
|
|
26
|
+
"import": "./dist/ifc.js"
|
|
27
|
+
},
|
|
19
28
|
"./attestation": {
|
|
20
29
|
"types": "./dist/attestation.d.ts",
|
|
21
30
|
"import": "./dist/attestation.js"
|
|
@@ -24,6 +33,10 @@
|
|
|
24
33
|
"types": "./dist/toolproxy.d.ts",
|
|
25
34
|
"import": "./dist/toolproxy.js"
|
|
26
35
|
},
|
|
36
|
+
"./cage": {
|
|
37
|
+
"types": "./dist/cage.d.ts",
|
|
38
|
+
"import": "./dist/cage.js"
|
|
39
|
+
},
|
|
27
40
|
"./license": {
|
|
28
41
|
"types": "./dist/license.d.ts",
|
|
29
42
|
"import": "./dist/license.js"
|
|
@@ -37,6 +50,8 @@
|
|
|
37
50
|
"wasm/nodejs/nucleus_wasm_bg.wasm",
|
|
38
51
|
"wasm/nodejs/nucleus_wasm_bg.wasm.d.ts",
|
|
39
52
|
"examples/openclaw-replay.mjs",
|
|
53
|
+
"examples/cage-quickstart.mjs",
|
|
54
|
+
"examples/workflow-leak.mjs",
|
|
40
55
|
"examples/converting-demo.sh",
|
|
41
56
|
"examples/sample-toolproxy-trace.clean.json",
|
|
42
57
|
"examples/sample-toolproxy-trace.bypass.json",
|
|
@@ -46,11 +61,12 @@
|
|
|
46
61
|
"CHANGELOG.md"
|
|
47
62
|
],
|
|
48
63
|
"scripts": {
|
|
49
|
-
"build:wasm": "wasm-pack build ../../crates/nucleus-wasm --target nodejs --out-dir ../../packages/nucleus-verify/wasm/nodejs && rm -f wasm/nodejs/.gitignore wasm/nodejs/package.json",
|
|
64
|
+
"build:wasm": "wasm-pack build ../../crates/nucleus-wasm --target nodejs --out-dir ../../packages/nucleus-verify/wasm/nodejs && rm -f wasm/nodejs/.gitignore && printf '%s' '{\"type\":\"commonjs\"}' > wasm/nodejs/package.json",
|
|
50
65
|
"build": "tsc -p tsconfig.json",
|
|
51
66
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
67
|
"test": "node --test",
|
|
53
|
-
"demo": "node examples/openclaw-replay.mjs"
|
|
68
|
+
"demo": "node examples/openclaw-replay.mjs",
|
|
69
|
+
"build:browser": "esbuild src/attestation.browser.ts --bundle --format=esm --alias:node:crypto=./src/shim/node-crypto.mjs --banner:js=\"globalThis.Buffer||={from:(i,e)=>e==='base64'?Uint8Array.from(atob(i),c=>c.charCodeAt(0)):new TextEncoder().encode(i)};\" --outfile=dist/attestation.browser.js"
|
|
54
70
|
},
|
|
55
71
|
"keywords": [
|
|
56
72
|
"nucleus",
|
|
@@ -73,5 +89,9 @@
|
|
|
73
89
|
"devDependencies": {
|
|
74
90
|
"@types/node": "^25.9.1",
|
|
75
91
|
"typescript": "^5.7.2"
|
|
92
|
+
},
|
|
93
|
+
"dependencies": {
|
|
94
|
+
"@noble/ed25519": "3.1.0",
|
|
95
|
+
"@noble/hashes": "2.2.0"
|
|
76
96
|
}
|
|
77
97
|
}
|
|
@@ -51,6 +51,21 @@ export function compute_dependency_graph(chain_jsonl: string): any;
|
|
|
51
51
|
*/
|
|
52
52
|
export function compute_savings_summary(plan_json: string, cost_map_json: string): any;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* **The ONE proven IFC decision, surfaced to JS.** Recomputes the
|
|
56
|
+
* `FlowDeclaration → IfcVerdict` decision over the Denning lattice from
|
|
57
|
+
* `nucleus-ifc` (whose lattice laws are Lean-proven in `portcullis-core`) — the
|
|
58
|
+
* SAME `decide()` the production gate runs. `@coproduct_inc/verify`'s DAG-cage
|
|
59
|
+
* flow check delegates here instead of a TS mirror, so there is one model.
|
|
60
|
+
*
|
|
61
|
+
* `input_tokens_json` is a JSON array of declared-input tokens — one of:
|
|
62
|
+
* `user_prompt`, `web_content`, `tool_response`, `file_read`, `env_var`,
|
|
63
|
+
* `secret`, `database_row`, `memory_read`, `http_response`. `requires_authority`
|
|
64
|
+
* = the action needs directive authority; `sink_public` = the sink is publicly
|
|
65
|
+
* visible. Returns the recomputable `IfcVerdict` (allow/deny + reason).
|
|
66
|
+
*/
|
|
67
|
+
export function ifc_decide(input_tokens_json: string, requires_authority: boolean, sink_public: boolean): any;
|
|
68
|
+
|
|
54
69
|
/**
|
|
55
70
|
* Install a browser-friendly panic hook. The hook routes any wasm-side
|
|
56
71
|
* `panic!` into `console.error`, which means failed assertions in the
|
|
@@ -89,6 +89,34 @@ function compute_savings_summary(plan_json, cost_map_json) {
|
|
|
89
89
|
}
|
|
90
90
|
exports.compute_savings_summary = compute_savings_summary;
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* **The ONE proven IFC decision, surfaced to JS.** Recomputes the
|
|
94
|
+
* `FlowDeclaration → IfcVerdict` decision over the Denning lattice from
|
|
95
|
+
* `nucleus-ifc` (whose lattice laws are Lean-proven in `portcullis-core`) — the
|
|
96
|
+
* SAME `decide()` the production gate runs. `@coproduct_inc/verify`'s DAG-cage
|
|
97
|
+
* flow check delegates here instead of a TS mirror, so there is one model.
|
|
98
|
+
*
|
|
99
|
+
* `input_tokens_json` is a JSON array of declared-input tokens — one of:
|
|
100
|
+
* `user_prompt`, `web_content`, `tool_response`, `file_read`, `env_var`,
|
|
101
|
+
* `secret`, `database_row`, `memory_read`, `http_response`. `requires_authority`
|
|
102
|
+
* = the action needs directive authority; `sink_public` = the sink is publicly
|
|
103
|
+
* visible. Returns the recomputable `IfcVerdict` (allow/deny + reason).
|
|
104
|
+
* @param {string} input_tokens_json
|
|
105
|
+
* @param {boolean} requires_authority
|
|
106
|
+
* @param {boolean} sink_public
|
|
107
|
+
* @returns {any}
|
|
108
|
+
*/
|
|
109
|
+
function ifc_decide(input_tokens_json, requires_authority, sink_public) {
|
|
110
|
+
const ptr0 = passStringToWasm0(input_tokens_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
111
|
+
const len0 = WASM_VECTOR_LEN;
|
|
112
|
+
const ret = wasm.ifc_decide(ptr0, len0, requires_authority, sink_public);
|
|
113
|
+
if (ret[2]) {
|
|
114
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
115
|
+
}
|
|
116
|
+
return takeFromExternrefTable0(ret[0]);
|
|
117
|
+
}
|
|
118
|
+
exports.ifc_decide = ifc_decide;
|
|
119
|
+
|
|
92
120
|
/**
|
|
93
121
|
* Install a browser-friendly panic hook. The hook routes any wasm-side
|
|
94
122
|
* `panic!` into `console.error`, which means failed assertions in the
|
|
Binary file
|
|
@@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory;
|
|
|
4
4
|
export const analyze_correction_event: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
5
5
|
export const compute_dependency_graph: (a: number, b: number) => [number, number, number];
|
|
6
6
|
export const compute_savings_summary: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
7
|
+
export const ifc_decide: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
7
8
|
export const parse_claude_code_session: (a: number, b: number) => [number, number, number];
|
|
8
9
|
export const parse_lineage_chain: (a: number, b: number) => [number, number, number];
|
|
9
10
|
export const verify_auction_receipt: (a: number, b: number, c: number, d: number) => [number, number, number];
|
package/wasm/nodejs/package.json
CHANGED
|
@@ -1,9 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nucleus-wasm-nodejs-core",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "commonjs",
|
|
6
|
-
"main": "nucleus_wasm.js",
|
|
7
|
-
"types": "nucleus_wasm.d.ts",
|
|
8
|
-
"comment": "wasm-pack `nodejs` target (CommonJS). This nested package.json pins `type: commonjs` so the CJS `require`/`module.exports`/`__dirname` glue keeps working when imported from the ESM `@nucleus/verify` parent. Regenerated by `pnpm build:wasm`; do not hand-edit the sibling .js/.wasm."
|
|
9
|
-
}
|
|
1
|
+
{"type":"commonjs"}
|