@coproduct_inc/verify 0.5.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 +20 -0
- package/README.md +7 -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 +2 -0
- package/dist/index.js +5 -0
- package/dist/workflow.d.ts +15 -4
- package/dist/workflow.js +33 -11
- package/package.json +6 -2
- 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,26 @@
|
|
|
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
|
+
|
|
5
25
|
## 0.5.0
|
|
6
26
|
|
|
7
27
|
- **`Workflow` — the DAG cage** (`./workflow`, re-exported from root). Compose a
|
package/README.md
CHANGED
|
@@ -74,6 +74,13 @@ verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"] })
|
|
|
74
74
|
It rides on whatever orchestrator you already use (LangGraph, Temporal, CrewAI…)
|
|
75
75
|
— you cage the tools, not the framework. Run it: `node examples/workflow-leak.mjs`.
|
|
76
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
|
+
|
|
77
84
|
---
|
|
78
85
|
|
|
79
86
|
## In-bounds attestation — the drop-in
|
|
@@ -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
|
@@ -96,6 +96,8 @@ export declare function verifySignature(receiptJson: string, jwksJson: string):
|
|
|
96
96
|
export * from "./attestation.js";
|
|
97
97
|
export * from "./toolproxy.js";
|
|
98
98
|
export * from "./cage.js";
|
|
99
|
+
export * from "./ifc-decide.js";
|
|
100
|
+
export * from "./ifc.js";
|
|
99
101
|
export * from "./workflow.js";
|
|
100
102
|
export * from "./license.js";
|
|
101
103
|
export * from "./claim.js";
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,11 @@ export * from "./toolproxy.js";
|
|
|
73
73
|
// The 5-minute drop-in: a runtime `Cage` wraps an agent's tool calls (enforce +
|
|
74
74
|
// record) and emits a signed receipt the lines above verify. See `./cage`.
|
|
75
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";
|
|
76
81
|
// The DAG cage: compose a multi-agent run into one verifiable `WorkflowReceipt`,
|
|
77
82
|
// including the cross-node information flow ("locally fine, globally leaks").
|
|
78
83
|
export * from "./workflow.js";
|
package/dist/workflow.d.ts
CHANGED
|
@@ -27,14 +27,25 @@ export interface WorkflowReceipt {
|
|
|
27
27
|
signature: string;
|
|
28
28
|
signedAt?: string;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
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
|
+
*/
|
|
31
36
|
export interface FlowPolicy {
|
|
32
|
-
/** Tools that
|
|
37
|
+
/** Tools that read SECRET data (raise confidentiality). */
|
|
33
38
|
sources: string[];
|
|
34
|
-
/**
|
|
39
|
+
/** Public-egress tools a secret must never reach. */
|
|
35
40
|
sinks: string[];
|
|
36
|
-
/** Nodes that sanitize — they clear incoming taint (a
|
|
41
|
+
/** Nodes that sanitize — they clear incoming taint (a declassifier). */
|
|
37
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[];
|
|
38
49
|
}
|
|
39
50
|
export interface FlowLeak {
|
|
40
51
|
node: string;
|
package/dist/workflow.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createHash, createPublicKey, sign as edSign, verify as edVerify } from
|
|
|
21
21
|
import { Cage } from "./cage.js";
|
|
22
22
|
import { canonicalize, exportPublicKey } from "./attestation.js";
|
|
23
23
|
import { traceEventsFromRecords } from "./toolproxy.js";
|
|
24
|
+
import { ifcDecide } from "./ifc-decide.js";
|
|
24
25
|
const WF_KIND = "nucleus.workflow-receipt.v1";
|
|
25
26
|
function sha256Hex(s) {
|
|
26
27
|
return createHash("sha256").update(s).digest("hex");
|
|
@@ -60,26 +61,47 @@ function topoOrder(nodes) {
|
|
|
60
61
|
throw new Error("workflow graph has a cycle");
|
|
61
62
|
return order;
|
|
62
63
|
}
|
|
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
|
+
*/
|
|
64
73
|
function recomputeFlow(nodes, flow) {
|
|
65
74
|
const sources = new Set(flow.sources);
|
|
75
|
+
const adversarial = new Set(flow.adversarialSources ?? []);
|
|
66
76
|
const sinks = new Set(flow.sinks);
|
|
77
|
+
const actionSinks = new Set(flow.actionSinks ?? []);
|
|
67
78
|
const declass = new Set(flow.declassifiers ?? []);
|
|
68
79
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
69
|
-
const
|
|
80
|
+
const inputsAt = new Map();
|
|
70
81
|
const leaks = [];
|
|
71
82
|
for (const id of topoOrder(nodes)) {
|
|
72
83
|
const n = byId.get(id);
|
|
73
84
|
const tools = new Set(n.events.map((e) => e.tool));
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 });
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
return leaks;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coproduct_inc/verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
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",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"types": "./dist/workflow.d.ts",
|
|
22
22
|
"import": "./dist/workflow.js"
|
|
23
23
|
},
|
|
24
|
+
"./ifc": {
|
|
25
|
+
"types": "./dist/ifc.d.ts",
|
|
26
|
+
"import": "./dist/ifc.js"
|
|
27
|
+
},
|
|
24
28
|
"./attestation": {
|
|
25
29
|
"types": "./dist/attestation.d.ts",
|
|
26
30
|
"import": "./dist/attestation.js"
|
|
@@ -57,7 +61,7 @@
|
|
|
57
61
|
"CHANGELOG.md"
|
|
58
62
|
],
|
|
59
63
|
"scripts": {
|
|
60
|
-
"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",
|
|
61
65
|
"build": "tsc -p tsconfig.json",
|
|
62
66
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
63
67
|
"test": "node --test",
|
|
@@ -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"}
|