@coproduct_inc/verify 0.2.0 → 0.5.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/dist/claim.js ADDED
@@ -0,0 +1,186 @@
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
+ import { verify, recomputeClearing } from "./index.js";
39
+ import { verifyReceipt } from "./attestation.js";
40
+ function receiptJson(ev) {
41
+ return typeof ev.receipt === "string" ? ev.receipt : JSON.stringify(ev.receipt);
42
+ }
43
+ /**
44
+ * Adjudicate a claim by recomputation. Never throws — a malformed receipt,
45
+ * an unknown kind, or an engine error surface as `verified: false` with a
46
+ * `reason`, so the widget can always render a definite ✓/✗.
47
+ */
48
+ export async function verifyClaim(claim) {
49
+ const base = { kind: claim.kind, statement: claim.statement };
50
+ try {
51
+ switch (claim.kind) {
52
+ case "clearing":
53
+ return await verifyClearingClaim(claim, base);
54
+ case "in_bounds":
55
+ return verifyInBoundsClaim(claim, base);
56
+ case "signature":
57
+ return await verifySignatureClaim(claim, base);
58
+ default:
59
+ return {
60
+ ...base,
61
+ verified: false,
62
+ recomputed: null,
63
+ asserted: null,
64
+ trace: [{ check: "kind-supported", ok: false, detail: `unsupported kind: ${claim.kind}` }],
65
+ reason: `unsupported claim kind: ${claim.kind} (not recomputable)`,
66
+ };
67
+ }
68
+ }
69
+ catch (e) {
70
+ return {
71
+ ...base,
72
+ verified: false,
73
+ recomputed: null,
74
+ asserted: null,
75
+ trace: [{ check: "engine", ok: false, detail: String(e) }],
76
+ reason: `recompute engine error: ${e instanceof Error ? e.message : String(e)}`,
77
+ };
78
+ }
79
+ }
80
+ async function verifyClearingClaim(claim, base) {
81
+ const json = receiptJson(claim.evidence);
82
+ const trace = [];
83
+ let recomputedPrice;
84
+ let coreOk;
85
+ let reason;
86
+ if (claim.evidence.jwks) {
87
+ // Full path: signature + root hash + price recompute.
88
+ const r = await verify(json, claim.evidence.jwks);
89
+ trace.push({ check: "signature", ok: r.signature_ok });
90
+ trace.push({ check: "root-hash", ok: r.root_hash_ok });
91
+ trace.push({ check: "price-recomputed", ok: r.price_recomputed_ok });
92
+ recomputedPrice = r.recomputed_price_micro_usd;
93
+ coreOk = r.ok;
94
+ reason = r.reason;
95
+ }
96
+ else {
97
+ // Recompute-only path (no pinned JWKS supplied).
98
+ const r = await recomputeClearing(json);
99
+ trace.push({ check: "price-recomputed", ok: r.matches_receipt });
100
+ trace.push({ check: "winner-recomputed", ok: r.winner_matches });
101
+ recomputedPrice = r.recomputed_price_micro_usd;
102
+ coreOk = r.matches_receipt;
103
+ reason = r.reason;
104
+ }
105
+ const asserted = crossCheck(claim.asserted, recomputedPrice, trace, "asserted-price");
106
+ return {
107
+ ...base,
108
+ verified: coreOk && (asserted?.matched ?? true),
109
+ recomputed: { label: "clearing_price_micro_usd", value: recomputedPrice },
110
+ asserted,
111
+ trace,
112
+ reason: !coreOk ? (reason ?? "clearing did not recompute") : assertedReason(asserted),
113
+ };
114
+ }
115
+ function verifyInBoundsClaim(claim, base) {
116
+ const receipt = typeof claim.evidence.receipt === "string"
117
+ ? JSON.parse(claim.evidence.receipt)
118
+ : claim.evidence.receipt;
119
+ const r = verifyReceipt(receipt, {
120
+ expectPrincipal: claim.evidence.expectPrincipal,
121
+ expectPublicKey: claim.evidence.expectPublicKey,
122
+ });
123
+ const trace = [
124
+ { check: "signature", ok: r.signatureOk },
125
+ { check: "root-hash", ok: r.rootHashOk },
126
+ { check: "verdict-consistent", ok: r.verdictConsistentOk },
127
+ { check: "in-bounds", ok: r.inBounds },
128
+ ];
129
+ if (claim.evidence.expectPrincipal !== undefined) {
130
+ trace.push({ check: "principal-pinned", ok: r.principalOk ?? false });
131
+ }
132
+ const asserted = crossCheck(claim.asserted, r.inBounds, trace, "asserted-in-bounds");
133
+ return {
134
+ ...base,
135
+ verified: r.ok && (asserted?.matched ?? true),
136
+ recomputed: { label: "in_bounds", value: r.inBounds },
137
+ asserted,
138
+ trace,
139
+ reason: !r.ok ? (r.reason ?? "attestation did not verify") : assertedReason(asserted),
140
+ };
141
+ }
142
+ async function verifySignatureClaim(claim, base) {
143
+ if (!claim.evidence.jwks) {
144
+ return {
145
+ ...base,
146
+ verified: false,
147
+ recomputed: null,
148
+ asserted: null,
149
+ trace: [{ check: "jwks-present", ok: false }],
150
+ reason: "signature claim requires evidence.jwks",
151
+ };
152
+ }
153
+ // Reuse the full verifier but gate on authenticity only (signature + root
154
+ // hash), not price correctness — the `signature` kind asserts provenance.
155
+ const r = await verify(receiptJson(claim.evidence), claim.evidence.jwks);
156
+ const ok = r.signature_ok && r.root_hash_ok;
157
+ return {
158
+ ...base,
159
+ verified: ok,
160
+ recomputed: { label: "authentic", value: ok },
161
+ asserted: null,
162
+ trace: [
163
+ { check: "signature", ok: r.signature_ok },
164
+ { check: "root-hash", ok: r.root_hash_ok },
165
+ ],
166
+ reason: ok ? null : (r.reason ?? "signature or root hash failed"),
167
+ };
168
+ }
169
+ /** Cross-check a prose-asserted value against the recomputed value. */
170
+ function crossCheck(asserted, recomputed, trace, check) {
171
+ if (asserted === undefined)
172
+ return null;
173
+ const matched = asserted.value === recomputed;
174
+ trace.push({
175
+ check,
176
+ ok: matched,
177
+ detail: matched ? undefined : `asserted ${String(asserted.value)} ≠ recomputed ${String(recomputed)}`,
178
+ });
179
+ return { value: asserted.value, matched };
180
+ }
181
+ function assertedReason(asserted) {
182
+ if (asserted && !asserted.matched) {
183
+ return `asserted value (${String(asserted.value)}) does not match the recompute`;
184
+ }
185
+ return null;
186
+ }
package/dist/index.d.ts CHANGED
@@ -95,3 +95,7 @@ export declare function recomputeClearing(receiptJson: string): Promise<Recomput
95
95
  export declare function verifySignature(receiptJson: string, jwksJson: string): Promise<unknown>;
96
96
  export * from "./attestation.js";
97
97
  export * from "./toolproxy.js";
98
+ export * from "./cage.js";
99
+ export * from "./workflow.js";
100
+ export * from "./license.js";
101
+ export * from "./claim.js";
package/dist/index.js CHANGED
@@ -70,3 +70,16 @@ export * from "./attestation.js";
70
70
  // Producer-side instrumentation: turn a nucleus-tool-proxy trace into a signed
71
71
  // in-bounds receipt. See `./toolproxy` and the `nucleus-attest` CLI.
72
72
  export * from "./toolproxy.js";
73
+ // The 5-minute drop-in: a runtime `Cage` wraps an agent's tool calls (enforce +
74
+ // record) and emits a signed receipt the lines above verify. See `./cage`.
75
+ export * from "./cage.js";
76
+ // The DAG cage: compose a multi-agent run into one verifiable `WorkflowReceipt`,
77
+ // including the cross-node information flow ("locally fine, globally leaks").
78
+ export * from "./workflow.js";
79
+ // Offline Ed25519 license keys: entitlement with no user DB / no callback.
80
+ // See `./license` and the `nucleus-license` CLI.
81
+ export * from "./license.js";
82
+ // Recompute-verified CLAIM façade: one `verifyClaim(claim) → ClaimVerdict`
83
+ // over all of the above engines, for embeddable ✓/✗ + recompute-trace
84
+ // widgets. See `./claim` and docs/RECOMPUTE-VERIFIED-CLAIMS.md.
85
+ export * from "./claim.js";
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `nucleus-license` — issue + verify offline Ed25519 license keys.
4
+ *
5
+ * nucleus-license keygen [--out-key vendor.pem] [--out-pub vendor.pub]
6
+ * → generate a vendor keypair; prints the base64url public key to PIN.
7
+ *
8
+ * nucleus-license issue --key vendor.pem --sub a@b.com --tier pro \
9
+ * [--features dashboard,export] [--exp 2027-06-01] [--iat 2026-06-04]
10
+ * → prints a license token (deliver this string as the MoR "license key").
11
+ *
12
+ * nucleus-license verify <token> --pubkey <base64url> [--now 2026-06-04]
13
+ * → exit 0 if valid, 1 if not.
14
+ *
15
+ * No server, no DB: the Merchant of Record holds the customer; this issues and
16
+ * checks a signature. Keep the vendor private key secret.
17
+ */
18
+ export {};
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `nucleus-license` — issue + verify offline Ed25519 license keys.
4
+ *
5
+ * nucleus-license keygen [--out-key vendor.pem] [--out-pub vendor.pub]
6
+ * → generate a vendor keypair; prints the base64url public key to PIN.
7
+ *
8
+ * nucleus-license issue --key vendor.pem --sub a@b.com --tier pro \
9
+ * [--features dashboard,export] [--exp 2027-06-01] [--iat 2026-06-04]
10
+ * → prints a license token (deliver this string as the MoR "license key").
11
+ *
12
+ * nucleus-license verify <token> --pubkey <base64url> [--now 2026-06-04]
13
+ * → exit 0 if valid, 1 if not.
14
+ *
15
+ * No server, no DB: the Merchant of Record holds the customer; this issues and
16
+ * checks a signature. Keep the vendor private key secret.
17
+ */
18
+ import { readFileSync, writeFileSync } from "node:fs";
19
+ import { generateLicenseKeypair, exportLicensePublicKey, signLicense, verifyLicense, licensePrivateKeyFromPem, } from "./license.js";
20
+ function opt(flag) {
21
+ const i = process.argv.indexOf(flag);
22
+ return i >= 0 ? process.argv[i + 1] : undefined;
23
+ }
24
+ function keygen() {
25
+ const { publicKey, privateKey } = generateLicenseKeypair();
26
+ const pub = exportLicensePublicKey(publicKey);
27
+ const pem = privateKey.export({ type: "pkcs8", format: "pem" });
28
+ const outKey = opt("--out-key");
29
+ const outPub = opt("--out-pub");
30
+ if (outKey)
31
+ writeFileSync(outKey, pem, { mode: 0o600 });
32
+ if (outPub)
33
+ writeFileSync(outPub, pub + "\n");
34
+ process.stdout.write(`vendor public key (PIN this in the verifier):\n${pub}\n`);
35
+ if (!outKey) {
36
+ process.stdout.write("\nvendor PRIVATE key (keep SECRET — store in your MoR webhook secret):\n");
37
+ process.stdout.write(pem);
38
+ }
39
+ else {
40
+ process.stderr.write(`wrote private key → ${outKey} (chmod 600)\n`);
41
+ }
42
+ return 0;
43
+ }
44
+ function issue() {
45
+ const keyPath = opt("--key");
46
+ const sub = opt("--sub");
47
+ const tier = (opt("--tier") ?? "pro");
48
+ if (!keyPath || !sub) {
49
+ process.stderr.write("error: issue needs --key <vendor.pem> and --sub <id>\n");
50
+ return 2;
51
+ }
52
+ const features = (opt("--features") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
53
+ const claims = {
54
+ sub,
55
+ tier,
56
+ features,
57
+ iat: opt("--iat") ?? new Date().toISOString().slice(0, 10),
58
+ ...(opt("--exp") ? { exp: opt("--exp") } : {}),
59
+ };
60
+ const key = licensePrivateKeyFromPem(readFileSync(keyPath, "utf8"));
61
+ process.stdout.write(signLicense(claims, key) + "\n");
62
+ return 0;
63
+ }
64
+ function verify() {
65
+ const token = process.argv[3];
66
+ const pubkey = opt("--pubkey");
67
+ if (!token || token.startsWith("--") || !pubkey) {
68
+ process.stderr.write("error: verify needs <token> and --pubkey <base64url>\n");
69
+ return 2;
70
+ }
71
+ const r = verifyLicense(token, pubkey, opt("--now"));
72
+ if (r.valid) {
73
+ process.stdout.write(`✓ valid — ${r.license.tier} for ${r.license.sub}` + (r.license.exp ? ` (exp ${r.license.exp})` : "") + `\n`);
74
+ return 0;
75
+ }
76
+ process.stderr.write(`✗ invalid — ${r.reason}\n`);
77
+ return 1;
78
+ }
79
+ function main() {
80
+ const cmd = process.argv[2];
81
+ switch (cmd) {
82
+ case "keygen":
83
+ return keygen();
84
+ case "issue":
85
+ return issue();
86
+ case "verify":
87
+ return verify();
88
+ default:
89
+ process.stdout.write("nucleus-license — offline Ed25519 license keys\n\n" +
90
+ " nucleus-license keygen [--out-key vendor.pem] [--out-pub vendor.pub]\n" +
91
+ " nucleus-license issue --key vendor.pem --sub a@b.com --tier pro [--features x,y] [--exp ISO]\n" +
92
+ " nucleus-license verify <token> --pubkey <base64url> [--now ISO]\n");
93
+ return cmd ? 2 : 0;
94
+ }
95
+ }
96
+ process.exit(main());
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Offline Ed25519 license keys — entitlement without a user database or a
3
+ * callback. The vendor signs a tiny claims payload with a private key; the CLI
4
+ * (or a Pro feature path) verifies it against a PINNED public key. No server,
5
+ * no license API, no phone-home: the Merchant of Record (Polar / Lemon Squeezy)
6
+ * holds the customer record and delivers the signed string as the "license
7
+ * key"; this module is the verifier.
8
+ *
9
+ * Token format (compact, copy-pasteable):
10
+ * nvlic1.<base64url(canonical-json payload)>.<base64url(ed25519 sig)>
11
+ * The signature is over the payload SEGMENT bytes (the base64url string), so
12
+ * verification never re-serializes — it checks the exact bytes that were signed.
13
+ *
14
+ * On-brand: a verification company gating its own Pro tier by a verifiable,
15
+ * offline-checkable signature rather than a trust-the-server license call.
16
+ */
17
+ import { type KeyObject } from "node:crypto";
18
+ export declare const LICENSE_PREFIX: "nvlic1";
19
+ /** The claims a license asserts. Keep it small — it's pasted by hand. */
20
+ export interface LicenseClaims {
21
+ /** Who the license is for (email / customer id from the MoR). */
22
+ sub: string;
23
+ /** Entitlement tier. */
24
+ tier: "pro" | "team" | "enterprise";
25
+ /** Unlocked features (free-form; checked by the Pro path). */
26
+ features: string[];
27
+ /** Issued-at (ISO date). Informational. */
28
+ iat: string;
29
+ /** Optional expiry (ISO date). Absent = perpetual. */
30
+ exp?: string;
31
+ }
32
+ export interface LicenseReport {
33
+ valid: boolean;
34
+ /** The verified claims, when valid. */
35
+ license: LicenseClaims | null;
36
+ /** First failing reason, or null when valid. */
37
+ reason: string | null;
38
+ }
39
+ /** Generate an Ed25519 vendor keypair. Keep the private key secret; ship the public. */
40
+ export declare function generateLicenseKeypair(): {
41
+ publicKey: KeyObject;
42
+ privateKey: KeyObject;
43
+ };
44
+ /** Export a public key to the base64url raw form the verifier pins. */
45
+ export declare function exportLicensePublicKey(publicKey: KeyObject): string;
46
+ /** Sign a license token with the vendor private key. */
47
+ export declare function signLicense(claims: LicenseClaims, privateKey: KeyObject): string;
48
+ /** Load an Ed25519 private key from PEM (PKCS#8). */
49
+ export declare function licensePrivateKeyFromPem(pem: string): KeyObject;
50
+ /**
51
+ * Verify a license token offline against the pinned vendor public key
52
+ * (base64url raw Ed25519). Checks format + signature + expiry. Never throws.
53
+ *
54
+ * @param now optional ISO timestamp for expiry checks (defaults to absent =
55
+ * skip expiry; pass an explicit time to enforce it deterministically).
56
+ */
57
+ export declare function verifyLicense(token: string, vendorPublicKeyB64url: string, now?: string): LicenseReport;
58
+ /** Convenience: does a verified license grant a given feature (or tier-implied)? */
59
+ export declare function licenseGrants(report: LicenseReport, feature: string): boolean;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Offline Ed25519 license keys — entitlement without a user database or a
3
+ * callback. The vendor signs a tiny claims payload with a private key; the CLI
4
+ * (or a Pro feature path) verifies it against a PINNED public key. No server,
5
+ * no license API, no phone-home: the Merchant of Record (Polar / Lemon Squeezy)
6
+ * holds the customer record and delivers the signed string as the "license
7
+ * key"; this module is the verifier.
8
+ *
9
+ * Token format (compact, copy-pasteable):
10
+ * nvlic1.<base64url(canonical-json payload)>.<base64url(ed25519 sig)>
11
+ * The signature is over the payload SEGMENT bytes (the base64url string), so
12
+ * verification never re-serializes — it checks the exact bytes that were signed.
13
+ *
14
+ * On-brand: a verification company gating its own Pro tier by a verifiable,
15
+ * offline-checkable signature rather than a trust-the-server license call.
16
+ */
17
+ import { createPublicKey, createPrivateKey, sign as edSign, verify as edVerify, generateKeyPairSync } from "node:crypto";
18
+ import { canonicalize } from "./attestation.js";
19
+ export const LICENSE_PREFIX = "nvlic1";
20
+ function b64url(buf) {
21
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
22
+ }
23
+ function fromB64url(s) {
24
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
25
+ }
26
+ function importPublicKey(b64urlX) {
27
+ return createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: b64urlX }, format: "jwk" });
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // Issuer side (run once per sale, or from a MoR webhook).
31
+ // ---------------------------------------------------------------------------
32
+ /** Generate an Ed25519 vendor keypair. Keep the private key secret; ship the public. */
33
+ export function generateLicenseKeypair() {
34
+ return generateKeyPairSync("ed25519");
35
+ }
36
+ /** Export a public key to the base64url raw form the verifier pins. */
37
+ export function exportLicensePublicKey(publicKey) {
38
+ const jwk = publicKey.export({ format: "jwk" });
39
+ if (!jwk.x)
40
+ throw new Error("not an Ed25519 public key");
41
+ return jwk.x;
42
+ }
43
+ /** Sign a license token with the vendor private key. */
44
+ export function signLicense(claims, privateKey) {
45
+ const payloadB64 = b64url(Buffer.from(canonicalize(claims), "utf8"));
46
+ const sig = edSign(null, Buffer.from(payloadB64, "utf8"), privateKey);
47
+ return `${LICENSE_PREFIX}.${payloadB64}.${b64url(sig)}`;
48
+ }
49
+ /** Load an Ed25519 private key from PEM (PKCS#8). */
50
+ export function licensePrivateKeyFromPem(pem) {
51
+ return createPrivateKey(pem);
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Verifier side (ships in the CLI / Pro path; pins the vendor public key).
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Verify a license token offline against the pinned vendor public key
58
+ * (base64url raw Ed25519). Checks format + signature + expiry. Never throws.
59
+ *
60
+ * @param now optional ISO timestamp for expiry checks (defaults to absent =
61
+ * skip expiry; pass an explicit time to enforce it deterministically).
62
+ */
63
+ export function verifyLicense(token, vendorPublicKeyB64url, now) {
64
+ const parts = token.trim().split(".");
65
+ if (parts.length !== 3 || parts[0] !== LICENSE_PREFIX) {
66
+ return { valid: false, license: null, reason: "malformed license token" };
67
+ }
68
+ const [, payloadB64, sigB64] = parts;
69
+ let sigOk = false;
70
+ try {
71
+ const pub = importPublicKey(vendorPublicKeyB64url);
72
+ sigOk = edVerify(null, Buffer.from(payloadB64, "utf8"), pub, fromB64url(sigB64));
73
+ }
74
+ catch {
75
+ sigOk = false;
76
+ }
77
+ if (!sigOk)
78
+ return { valid: false, license: null, reason: "signature did not verify under the pinned key" };
79
+ let claims;
80
+ try {
81
+ claims = JSON.parse(fromB64url(payloadB64).toString("utf8"));
82
+ }
83
+ catch (e) {
84
+ return { valid: false, license: null, reason: `license payload not valid JSON: ${e.message}` };
85
+ }
86
+ if (claims.exp && now && now > claims.exp) {
87
+ return { valid: false, license: claims, reason: `license expired on ${claims.exp}` };
88
+ }
89
+ return { valid: true, license: claims, reason: null };
90
+ }
91
+ /** Convenience: does a verified license grant a given feature (or tier-implied)? */
92
+ export function licenseGrants(report, feature) {
93
+ return report.valid && !!report.license && report.license.features.includes(feature);
94
+ }
@@ -0,0 +1,85 @@
1
+ import { type KeyObject } from "node:crypto";
2
+ import { Cage } from "./cage.js";
3
+ import { type TraceEvent } from "./attestation.js";
4
+ declare const WF_KIND = "nucleus.workflow-receipt.v1";
5
+ export interface WorkflowNodeSpec {
6
+ /** Unique node id within the workflow. */
7
+ id: string;
8
+ /** This node's capability boundary (the tools it may call). */
9
+ boundary: string[];
10
+ /** Parent node ids — the DAG edges (data flows parent → this node). */
11
+ after?: string[];
12
+ }
13
+ export interface WorkflowReceiptNode {
14
+ id: string;
15
+ after: string[];
16
+ boundary: string[];
17
+ events: TraceEvent[];
18
+ /** Independently recomputed by the verifier; the emitter's claim is not trusted. */
19
+ inBounds: boolean;
20
+ }
21
+ export interface WorkflowReceipt {
22
+ kind: typeof WF_KIND;
23
+ principal: string;
24
+ nodes: WorkflowReceiptNode[];
25
+ rootHash: string;
26
+ publicKey: string;
27
+ signature: string;
28
+ signedAt?: string;
29
+ }
30
+ /** An information-flow policy checked across the whole DAG. */
31
+ export interface FlowPolicy {
32
+ /** Tools that introduce taint (e.g. reading a secret). */
33
+ sources: string[];
34
+ /** Tools that must never be reached while tainted (e.g. an external write). */
35
+ sinks: string[];
36
+ /** Nodes that sanitize — they clear incoming taint (a sanitizer in the chain). */
37
+ declassifiers?: string[];
38
+ }
39
+ export interface FlowLeak {
40
+ node: string;
41
+ sink: string;
42
+ taintedVia: "reads-source" | "upstream";
43
+ }
44
+ export interface WorkflowVerifyReport {
45
+ /** `signatureOk && dagOk && nodesOk && flowOk`. */
46
+ ok: boolean;
47
+ /** Ed25519 signature verifies AND the recomputed rootHash matches. */
48
+ signatureOk: boolean;
49
+ /** Edges reference real nodes and the graph is acyclic. */
50
+ dagOk: boolean;
51
+ /** Every node's recorded calls were within its declared boundary. */
52
+ nodesOk: boolean;
53
+ /** No source→sink information flow across the DAG (per the flow policy). */
54
+ flowOk: boolean;
55
+ /** The source→sink leaks found (empty when `flowOk`). */
56
+ leaks: FlowLeak[];
57
+ /** Nodes whose recompute found an out-of-boundary call. */
58
+ outOfBoundsNodes: string[];
59
+ reason: string | null;
60
+ }
61
+ /**
62
+ * A DAG cage. Register nodes (each gets a {@link Cage}), run your agents through
63
+ * the caged tools, then {@link Workflow.attest} a single signed receipt over the
64
+ * whole graph — verifiable offline, including the cross-node information flow.
65
+ */
66
+ export declare class Workflow {
67
+ private readonly principal;
68
+ private readonly nodes;
69
+ constructor(principal: string);
70
+ /** Register a node and return its cage. The cage's tool calls are recorded
71
+ * under this node; `after` declares the edges into it. */
72
+ node(spec: WorkflowNodeSpec): Cage;
73
+ private receiptNodes;
74
+ /** Sign one workflow receipt over the whole DAG. */
75
+ attest(privateKey: KeyObject, opts?: {
76
+ signedAt?: string;
77
+ }): WorkflowReceipt;
78
+ }
79
+ /**
80
+ * Verify a workflow receipt fully and offline: the signature + the per-node
81
+ * in-bounds recompute + the DAG (acyclic, real edges) + the cross-node
82
+ * information flow. Never throws — failures surface as `ok:false` + `reason`.
83
+ */
84
+ export declare function verifyWorkflowReceipt(wr: WorkflowReceipt, flow?: FlowPolicy): WorkflowVerifyReport;
85
+ export {};