@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/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 {};
|