@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/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
+ }
@@ -0,0 +1,20 @@
1
+ /** The declared-input kinds the IFC model recognises (nucleus-ifc DeclaredInput). */
2
+ export type DeclaredInputToken = "user_prompt" | "web_content" | "tool_response" | "file_read" | "env_var" | "secret" | "database_row" | "memory_read" | "http_response";
3
+ /** A recomputable IFC verdict (the serialized `nucleus_ifc::decision::IfcVerdict`). */
4
+ export interface IfcVerdict {
5
+ allow: boolean;
6
+ reason: string;
7
+ declared_inputs: string[];
8
+ }
9
+ /**
10
+ * Recompute the proven IFC decision for an action: given the declared inputs
11
+ * reaching a sink and the sink's nature, is the flow safe? `requiresAuthority` =
12
+ * the action needs directive authority (so adversarial / no-authority inputs are
13
+ * refused — the prompt-injection guard); `sinkPublic` = the sink is publicly
14
+ * visible (so secret inputs are refused). Returns the same `IfcVerdict` nucleus's
15
+ * production gate returns — you trust the proven model, not this package.
16
+ */
17
+ export declare function ifcDecide(inputs: DeclaredInputToken[], opts?: {
18
+ requiresAuthority?: boolean;
19
+ sinkPublic?: boolean;
20
+ }): IfcVerdict;
@@ -0,0 +1,18 @@
1
+ // The ONE proven IFC decision, consumed from the real model — not a TS mirror.
2
+ // `ifc_decide` is compiled from `nucleus-ifc` (the Denning lattice + the
3
+ // FlowDeclaration→IfcVerdict decision the production gate runs; its lattice laws
4
+ // are Lean-proven in portcullis-core) into the package's wasm. The DAG-cage flow
5
+ // check (`./workflow`) delegates its verdict here, so the recomputable decision
6
+ // is the same one nucleus runs — closing the port→source gap.
7
+ import * as wasm from "../wasm/nodejs/nucleus_wasm.js";
8
+ /**
9
+ * Recompute the proven IFC decision for an action: given the declared inputs
10
+ * reaching a sink and the sink's nature, is the flow safe? `requiresAuthority` =
11
+ * the action needs directive authority (so adversarial / no-authority inputs are
12
+ * refused — the prompt-injection guard); `sinkPublic` = the sink is publicly
13
+ * visible (so secret inputs are refused). Returns the same `IfcVerdict` nucleus's
14
+ * production gate returns — you trust the proven model, not this package.
15
+ */
16
+ export function ifcDecide(inputs, opts = {}) {
17
+ return wasm.ifc_decide(JSON.stringify(inputs), !!opts.requiresAuthority, !!opts.sinkPublic);
18
+ }
package/dist/ifc.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ export declare enum ConfLevel {
2
+ Public = 0,
3
+ Internal = 1,
4
+ Secret = 2
5
+ }
6
+ export declare enum IntegLevel {
7
+ Adversarial = 0,
8
+ Untrusted = 1,
9
+ Trusted = 2
10
+ }
11
+ export declare enum AuthorityLevel {
12
+ NoAuthority = 0,
13
+ Informational = 1,
14
+ Suggestive = 2,
15
+ Directive = 3
16
+ }
17
+ export interface IFCLabel {
18
+ conf: ConfLevel;
19
+ integ: IntegLevel;
20
+ authority: AuthorityLevel;
21
+ }
22
+ /** Least-restrictive label (public, trusted, directive) — the join identity. */
23
+ export declare const BOTTOM: IFCLabel;
24
+ /** Lattice join (⊔): how labels combine as data flows together. Confidentiality
25
+ * rises (max), integrity and authority fall to the least-trusted input (min) —
26
+ * taint cannot be laundered by mixing in trusted data. */
27
+ export declare function join(a: IFCLabel, b: IFCLabel): IFCLabel;
28
+ /**
29
+ * Can data labelled `a` flow to a sink that admits up to `sink` (a ⊑ sink)?
30
+ * The sink encodes: the MAX confidentiality it may receive, and the MIN
31
+ * integrity/authority it requires. A flow is allowed iff confidentiality is not
32
+ * too high, integrity is at least required, and authority is at least required.
33
+ */
34
+ export declare function flowsTo(a: IFCLabel, sink: IFCLabel): boolean;
35
+ /** Web/scraped content: public, ADVERSARIAL integrity, NO authority to instruct. */
36
+ export declare const WEB_CONTENT: IFCLabel;
37
+ /** A read secret (API key, PII): SECRET confidentiality. */
38
+ export declare const SECRET_READ: IFCLabel;
39
+ /** A user prompt: trusted + directive (it MAY steer the agent). */
40
+ export declare const USER_PROMPT: IFCLabel;
41
+ /** A PUBLIC external sink (egress): admits only Public conf; demands no trust. */
42
+ export declare const PUBLIC_EGRESS: IFCLabel;
43
+ /** An ACTION sink that fires only on trusted, directive-authority data — the
44
+ * prompt-injection guard: adversarial / no-authority data can't drive it. */
45
+ export declare const TRUSTED_ACTION: IFCLabel;
46
+ /**
47
+ * Check the lattice laws this port relies on (the ones Lean-proven in nucleus):
48
+ * idempotent / commutative / associative join, taint-absorption on each axis,
49
+ * and the bounds. Returns the first violation, or null when all hold — so a test
50
+ * can assert parity with the proven model instead of trusting the port.
51
+ */
52
+ export declare function ifcLatticeLaws(): string | null;
package/dist/ifc.js ADDED
@@ -0,0 +1,106 @@
1
+ // A TypeScript REFLECTION of the Denning information-flow lattice from
2
+ // `nucleus-ifc` / `portcullis-core` (the production IFC model). The workflow flow
3
+ // check itself consumes the REAL compiled decision (`./ifc-decide` → wasm); these
4
+ // helpers are for direct in-process lattice work, kept parity-checked against
5
+ // that real model (see test/ifc.test.mjs) and against the Lean-proven laws via
6
+ // `ifcLatticeLaws()` (portcullis-core/lean/IFCSemilatticeProofs.lean). So this is
7
+ // a checked reflection, not a second source of truth.
8
+ //
9
+ // Three axes, each with its lattice direction:
10
+ // confidentiality — COVARIANT (join = max; Secret dominates: don't leak it out)
11
+ // integrity — CONTRAVARIANT (join = min; Adversarial dominates: Biba)
12
+ // authority — CONTRAVARIANT (join = min; NoAuthority dominates: untrusted
13
+ // data can be READ but never acquires authority to INSTRUCT —
14
+ // the formal block on indirect prompt injection)
15
+ export var ConfLevel;
16
+ (function (ConfLevel) {
17
+ ConfLevel[ConfLevel["Public"] = 0] = "Public";
18
+ ConfLevel[ConfLevel["Internal"] = 1] = "Internal";
19
+ ConfLevel[ConfLevel["Secret"] = 2] = "Secret";
20
+ })(ConfLevel || (ConfLevel = {}));
21
+ export var IntegLevel;
22
+ (function (IntegLevel) {
23
+ IntegLevel[IntegLevel["Adversarial"] = 0] = "Adversarial";
24
+ IntegLevel[IntegLevel["Untrusted"] = 1] = "Untrusted";
25
+ IntegLevel[IntegLevel["Trusted"] = 2] = "Trusted";
26
+ })(IntegLevel || (IntegLevel = {}));
27
+ export var AuthorityLevel;
28
+ (function (AuthorityLevel) {
29
+ AuthorityLevel[AuthorityLevel["NoAuthority"] = 0] = "NoAuthority";
30
+ AuthorityLevel[AuthorityLevel["Informational"] = 1] = "Informational";
31
+ AuthorityLevel[AuthorityLevel["Suggestive"] = 2] = "Suggestive";
32
+ AuthorityLevel[AuthorityLevel["Directive"] = 3] = "Directive";
33
+ })(AuthorityLevel || (AuthorityLevel = {}));
34
+ /** Least-restrictive label (public, trusted, directive) — the join identity. */
35
+ export const BOTTOM = {
36
+ conf: ConfLevel.Public,
37
+ integ: IntegLevel.Trusted,
38
+ authority: AuthorityLevel.Directive,
39
+ };
40
+ /** Lattice join (⊔): how labels combine as data flows together. Confidentiality
41
+ * rises (max), integrity and authority fall to the least-trusted input (min) —
42
+ * taint cannot be laundered by mixing in trusted data. */
43
+ export function join(a, b) {
44
+ return {
45
+ conf: Math.max(a.conf, b.conf),
46
+ integ: Math.min(a.integ, b.integ),
47
+ authority: Math.min(a.authority, b.authority),
48
+ };
49
+ }
50
+ /**
51
+ * Can data labelled `a` flow to a sink that admits up to `sink` (a ⊑ sink)?
52
+ * The sink encodes: the MAX confidentiality it may receive, and the MIN
53
+ * integrity/authority it requires. A flow is allowed iff confidentiality is not
54
+ * too high, integrity is at least required, and authority is at least required.
55
+ */
56
+ export function flowsTo(a, sink) {
57
+ return a.conf <= sink.conf && a.integ >= sink.integ && a.authority >= sink.authority;
58
+ }
59
+ // ── node-kind presets (mirror nucleus-ifc's NodeKind labelling) ──────────────
60
+ /** Web/scraped content: public, ADVERSARIAL integrity, NO authority to instruct. */
61
+ export const WEB_CONTENT = { conf: ConfLevel.Public, integ: IntegLevel.Adversarial, authority: AuthorityLevel.NoAuthority };
62
+ /** A read secret (API key, PII): SECRET confidentiality. */
63
+ export const SECRET_READ = { conf: ConfLevel.Secret, integ: IntegLevel.Trusted, authority: AuthorityLevel.NoAuthority };
64
+ /** A user prompt: trusted + directive (it MAY steer the agent). */
65
+ export const USER_PROMPT = { conf: ConfLevel.Internal, integ: IntegLevel.Trusted, authority: AuthorityLevel.Directive };
66
+ /** A PUBLIC external sink (egress): admits only Public conf; demands no trust. */
67
+ export const PUBLIC_EGRESS = { conf: ConfLevel.Public, integ: IntegLevel.Adversarial, authority: AuthorityLevel.NoAuthority };
68
+ /** An ACTION sink that fires only on trusted, directive-authority data — the
69
+ * prompt-injection guard: adversarial / no-authority data can't drive it. */
70
+ export const TRUSTED_ACTION = { conf: ConfLevel.Secret, integ: IntegLevel.Untrusted, authority: AuthorityLevel.Directive };
71
+ /**
72
+ * Check the lattice laws this port relies on (the ones Lean-proven in nucleus):
73
+ * idempotent / commutative / associative join, taint-absorption on each axis,
74
+ * and the bounds. Returns the first violation, or null when all hold — so a test
75
+ * can assert parity with the proven model instead of trusting the port.
76
+ */
77
+ export function ifcLatticeLaws() {
78
+ const labels = [];
79
+ for (const c of [ConfLevel.Public, ConfLevel.Internal, ConfLevel.Secret])
80
+ for (const i of [IntegLevel.Adversarial, IntegLevel.Untrusted, IntegLevel.Trusted])
81
+ for (const au of [AuthorityLevel.NoAuthority, AuthorityLevel.Informational, AuthorityLevel.Suggestive, AuthorityLevel.Directive])
82
+ labels.push({ conf: c, integ: i, authority: au });
83
+ const eq = (x, y) => x.conf === y.conf && x.integ === y.integ && x.authority === y.authority;
84
+ for (const a of labels) {
85
+ if (!eq(join(a, a), a))
86
+ return "join not idempotent";
87
+ if (!eq(join(a, BOTTOM), a))
88
+ return "BOTTOM is not the identity";
89
+ // taint-absorption: mixing in the most-tainted value on each axis dominates
90
+ if (join(a, WEB_CONTENT).integ !== IntegLevel.Adversarial)
91
+ return "adversarial integrity does not absorb";
92
+ if (join(a, SECRET_READ).conf !== ConfLevel.Secret)
93
+ return "secret confidentiality does not absorb";
94
+ if (join(a, WEB_CONTENT).authority !== AuthorityLevel.NoAuthority)
95
+ return "no-authority does not absorb";
96
+ for (const b of labels) {
97
+ if (!eq(join(a, b), join(b, a)))
98
+ return "join not commutative";
99
+ for (const c of labels) {
100
+ if (!eq(join(join(a, b), c), join(a, join(b, c))))
101
+ return "join not associative";
102
+ }
103
+ }
104
+ }
105
+ return null;
106
+ }
package/dist/index.d.ts CHANGED
@@ -95,4 +95,9 @@ 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 "./ifc-decide.js";
100
+ export * from "./ifc.js";
101
+ export * from "./workflow.js";
98
102
  export * from "./license.js";
103
+ export * from "./claim.js";
package/dist/index.js CHANGED
@@ -70,6 +70,21 @@ 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 ONE proven IFC decision, consumed from nucleus-ifc via wasm (`ifcDecide`).
77
+ export * from "./ifc-decide.js";
78
+ // The Denning lattice helpers (a faithful TS reflection of the same model, kept
79
+ // parity-checked against `ifcDecide`) for direct in-process use.
80
+ export * from "./ifc.js";
81
+ // The DAG cage: compose a multi-agent run into one verifiable `WorkflowReceipt`,
82
+ // including the cross-node information flow ("locally fine, globally leaks").
83
+ export * from "./workflow.js";
73
84
  // Offline Ed25519 license keys: entitlement with no user DB / no callback.
74
85
  // See `./license` and the `nucleus-license` CLI.
75
86
  export * from "./license.js";
87
+ // Recompute-verified CLAIM façade: one `verifyClaim(claim) → ClaimVerdict`
88
+ // over all of the above engines, for embeddable ✓/✗ + recompute-trace
89
+ // widgets. See `./claim` and docs/RECOMPUTE-VERIFIED-CLAIMS.md.
90
+ export * from "./claim.js";
@@ -0,0 +1,96 @@
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
+ /**
31
+ * An information-flow policy checked across the whole DAG, grounded in the
32
+ * Denning lattice (`./ifc`, a faithful port of nucleus-ifc — Lean-proven laws).
33
+ * `sources`/`sinks` are the confidentiality axis (don't leak a secret to a
34
+ * public sink); the optional axes cover integrity / indirect prompt injection.
35
+ */
36
+ export interface FlowPolicy {
37
+ /** Tools that read SECRET data (raise confidentiality). */
38
+ sources: string[];
39
+ /** Public-egress tools a secret must never reach. */
40
+ sinks: string[];
41
+ /** Nodes that sanitize — they clear incoming taint (a declassifier). */
42
+ declassifiers?: string[];
43
+ /** Tools that introduce ADVERSARIAL integrity (e.g. web fetch) — the indirect
44
+ * prompt-injection taint. */
45
+ adversarialSources?: string[];
46
+ /** Action tools that must only fire on trusted, directive-authority data —
47
+ * adversarial / no-authority data reaching them is a leak. */
48
+ actionSinks?: string[];
49
+ }
50
+ export interface FlowLeak {
51
+ node: string;
52
+ sink: string;
53
+ taintedVia: "reads-source" | "upstream";
54
+ }
55
+ export interface WorkflowVerifyReport {
56
+ /** `signatureOk && dagOk && nodesOk && flowOk`. */
57
+ ok: boolean;
58
+ /** Ed25519 signature verifies AND the recomputed rootHash matches. */
59
+ signatureOk: boolean;
60
+ /** Edges reference real nodes and the graph is acyclic. */
61
+ dagOk: boolean;
62
+ /** Every node's recorded calls were within its declared boundary. */
63
+ nodesOk: boolean;
64
+ /** No source→sink information flow across the DAG (per the flow policy). */
65
+ flowOk: boolean;
66
+ /** The source→sink leaks found (empty when `flowOk`). */
67
+ leaks: FlowLeak[];
68
+ /** Nodes whose recompute found an out-of-boundary call. */
69
+ outOfBoundsNodes: string[];
70
+ reason: string | null;
71
+ }
72
+ /**
73
+ * A DAG cage. Register nodes (each gets a {@link Cage}), run your agents through
74
+ * the caged tools, then {@link Workflow.attest} a single signed receipt over the
75
+ * whole graph — verifiable offline, including the cross-node information flow.
76
+ */
77
+ export declare class Workflow {
78
+ private readonly principal;
79
+ private readonly nodes;
80
+ constructor(principal: string);
81
+ /** Register a node and return its cage. The cage's tool calls are recorded
82
+ * under this node; `after` declares the edges into it. */
83
+ node(spec: WorkflowNodeSpec): Cage;
84
+ private receiptNodes;
85
+ /** Sign one workflow receipt over the whole DAG. */
86
+ attest(privateKey: KeyObject, opts?: {
87
+ signedAt?: string;
88
+ }): WorkflowReceipt;
89
+ }
90
+ /**
91
+ * Verify a workflow receipt fully and offline: the signature + the per-node
92
+ * in-bounds recompute + the DAG (acyclic, real edges) + the cross-node
93
+ * information flow. Never throws — failures surface as `ok:false` + `reason`.
94
+ */
95
+ export declare function verifyWorkflowReceipt(wr: WorkflowReceipt, flow?: FlowPolicy): WorkflowVerifyReport;
96
+ export {};