@zanii/blackbox 0.3.0 → 0.4.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/README.md +26 -1
- package/dist/a2a/index.d.ts +27 -0
- package/dist/a2a/index.js +104 -1
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.js +1 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.js +20 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.js +36 -9
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +28 -6
- package/dist/index.js +28 -6
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/otlp/index.js +28 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +23 -6
- package/dist/policy/index.js +151 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +89 -1
- package/dist/session/index.js +143 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Zanii's policy format, in and out (spec/policy.md §8). A rule that can't be said the same way on
|
|
2
|
+
// the other side is skipped and named, never loosened: dropping a condition would widen what a rule
|
|
3
|
+
// denies or allows. Pure; mirrors sdks/python/src/zanii_blackbox/policy_zanii.py; pinned by
|
|
4
|
+
// spec/vectors/policy-zanii.json.
|
|
5
|
+
const EFFECTS = new Set(["deny", "allow", "require_approval"]);
|
|
6
|
+
const idOf = (raw, n) => {
|
|
7
|
+
const s = String(raw ?? "")
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "")
|
|
11
|
+
.slice(0, 60);
|
|
12
|
+
return s || `rule-${n + 1}`;
|
|
13
|
+
};
|
|
14
|
+
/** A Zanii target as a Blackbox tool pattern, or null (`mcp.<server>.<tool>`, `mcp.<server>.*`, `*`). */
|
|
15
|
+
function toolOf(target) {
|
|
16
|
+
if (target === "*" || target === "mcp.*")
|
|
17
|
+
return target === "*" ? "*" : "mcp__*";
|
|
18
|
+
const m = /^mcp\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_.*-]+)$/.exec(target);
|
|
19
|
+
return m ? `mcp__${m[1]}__${m[2]}` : null;
|
|
20
|
+
}
|
|
21
|
+
/** A Blackbox tool pattern as a Zanii target, or null (a bare tool name matches any server). */
|
|
22
|
+
function targetOf(tool) {
|
|
23
|
+
if (tool === "*")
|
|
24
|
+
return "*";
|
|
25
|
+
if (tool === "mcp__*")
|
|
26
|
+
return "mcp.*";
|
|
27
|
+
const m = /^mcp__([A-Za-z0-9_-]+?)__(.+)$/.exec(tool);
|
|
28
|
+
return m ? `mcp.${m[1]}.${m[2]}` : null;
|
|
29
|
+
}
|
|
30
|
+
/** Zanii → Blackbox: `{policy: {version: 1, rules}, skipped}`. */
|
|
31
|
+
export function fromZaniiPolicy(z) {
|
|
32
|
+
const p = (z ?? {});
|
|
33
|
+
const rules = [];
|
|
34
|
+
const skipped = [];
|
|
35
|
+
const list = Array.isArray(p.rules) ? p.rules : [];
|
|
36
|
+
for (const [n, r] of list.entries()) {
|
|
37
|
+
const id = idOf(r.id, n);
|
|
38
|
+
const skip = (reason) => skipped.push({ id, reason });
|
|
39
|
+
if (!EFFECTS.has(String(r.effect)))
|
|
40
|
+
skip(`effect ${String(r.effect)}`);
|
|
41
|
+
else if (r.where !== undefined)
|
|
42
|
+
skip("conditions on the payload (where) have no exact Blackbox form");
|
|
43
|
+
else if (r.rateLimit !== undefined)
|
|
44
|
+
skip("rate limits are not tool policy (use the session limits)");
|
|
45
|
+
else {
|
|
46
|
+
const targets = Array.isArray(r.targets) ? r.targets.map(String) : ["*"];
|
|
47
|
+
const tools = targets.map(toolOf);
|
|
48
|
+
const bad = targets.filter((_, i) => tools[i] === null);
|
|
49
|
+
if (bad.length)
|
|
50
|
+
skip(`not an MCP tool target: ${bad.join(", ")}`);
|
|
51
|
+
else
|
|
52
|
+
for (const [i, tool] of tools.entries())
|
|
53
|
+
rules.push({
|
|
54
|
+
id: tools.length > 1 ? `${id}-${i + 1}`.slice(0, 64) : id,
|
|
55
|
+
tool: tool,
|
|
56
|
+
action: String(r.effect),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (p.default === "deny")
|
|
61
|
+
rules.push({ id: "zanii-default", tool: "*", action: "deny" });
|
|
62
|
+
return { policy: { version: 1, rules }, skipped };
|
|
63
|
+
}
|
|
64
|
+
/** Blackbox → Zanii: `{policy: {default: "allow", rules}, skipped}`. */
|
|
65
|
+
export function toZaniiPolicy(ours) {
|
|
66
|
+
const p = (ours ?? {});
|
|
67
|
+
const rules = [];
|
|
68
|
+
const skipped = [];
|
|
69
|
+
for (const r of (Array.isArray(p.rules) ? p.rules : [])) {
|
|
70
|
+
const id = String(r.id);
|
|
71
|
+
const extra = ["args_match", "hints", "environment"].filter((k) => r[k] !== undefined);
|
|
72
|
+
if (r.audit === true)
|
|
73
|
+
skipped.push({ id, reason: "an audit-only rule decides nothing" });
|
|
74
|
+
else if (r.action === "require_proof")
|
|
75
|
+
skipped.push({ id, reason: "Zanii has no require_proof effect" });
|
|
76
|
+
else if (extra.length)
|
|
77
|
+
skipped.push({ id, reason: `${extra.join(", ")} have no Zanii form` });
|
|
78
|
+
else {
|
|
79
|
+
const target = targetOf(String(r.tool));
|
|
80
|
+
if (target === null)
|
|
81
|
+
skipped.push({ id, reason: `a bare tool name matches any server: ${String(r.tool)}` });
|
|
82
|
+
else
|
|
83
|
+
rules.push({ id, effect: String(r.action), targets: [target] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { policy: { default: "allow", rules }, skipped };
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Obj = Record<string, unknown>;
|
|
2
|
+
export declare const PQ_ALG = "ML-DSA-65";
|
|
3
|
+
/** The ML-DSA-65 seed for a gateway identity: HMAC-SHA256(its Ed25519 key, a fixed label). */
|
|
4
|
+
export declare const pqSeedOf: (identityPrivateKey: Uint8Array) => Uint8Array;
|
|
5
|
+
/** The raw ML-DSA-65 public key (1952 bytes) for a 32-byte seed. */
|
|
6
|
+
export declare const pqPublicKey: (seed: Uint8Array) => Uint8Array;
|
|
7
|
+
/** Whether this runtime has ML-DSA-65 (Node ≥ 24.6 with OpenSSL ≥ 3.5). */
|
|
8
|
+
export declare function pqAvailable(): boolean;
|
|
9
|
+
/** Zanii's pq.binding: both keys sign the same body, so holding one key can't forge it. */
|
|
10
|
+
export declare function bindPqKey(o: {
|
|
11
|
+
did: string;
|
|
12
|
+
ts: string;
|
|
13
|
+
edPrivateKey: Uint8Array;
|
|
14
|
+
pqSeed: Uint8Array;
|
|
15
|
+
}): Obj;
|
|
16
|
+
/** Zanii's check: the structure, and BOTH signatures over the same body. */
|
|
17
|
+
export declare function verifyPqBinding(binding: unknown): {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
reasons: string[];
|
|
20
|
+
};
|
|
21
|
+
/** Zanii's transition payload, recorded UNSALTED: the binding as compact JSON. */
|
|
22
|
+
export declare function pqTransitionPayload(binding: Obj): string;
|
|
23
|
+
export {};
|
package/dist/pq/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Post-quantum binding of the gateway identity (spec/anchoring.md §13): Zanii's pq.binding, an
|
|
2
|
+
// ML-DSA-65 key (FIPS 204) bound to a did:key and signed by both keys, and the unsalted transition
|
|
3
|
+
// payload anchored to prove the binding existed before any break of Ed25519 (@zanii/pq). Needs
|
|
4
|
+
// Node ≥ 24.6 for ML-DSA. Pure; mirrors sdks/python/src/zanii_blackbox/pq.py; pinned by
|
|
5
|
+
// spec/vectors/pq-binding.json.
|
|
6
|
+
import { createHmac, createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
|
|
7
|
+
import { canonicalBytes, publicKeyFromDid } from "@zanii/core";
|
|
8
|
+
import { ed25519Sign, ed25519Verify } from "../transparency/index.js";
|
|
9
|
+
export const PQ_ALG = "ML-DSA-65";
|
|
10
|
+
const SEED_INFO = "zanii-blackbox identity ml-dsa-65 v1";
|
|
11
|
+
const PKCS8 = Buffer.from("3034020100300b060960864801650304031204228020", "hex");
|
|
12
|
+
const SPKI = Buffer.from("308207b2300b0609608648016503040312038207a100", "hex");
|
|
13
|
+
/** The ML-DSA-65 seed for a gateway identity: HMAC-SHA256(its Ed25519 key, a fixed label). */
|
|
14
|
+
export const pqSeedOf = (identityPrivateKey) => new Uint8Array(createHmac("sha256", identityPrivateKey).update(SEED_INFO).digest());
|
|
15
|
+
const privateKey = (seed) => {
|
|
16
|
+
if (seed.length !== 32)
|
|
17
|
+
throw new Error("an ML-DSA-65 seed is 32 bytes");
|
|
18
|
+
return createPrivateKey({ key: Buffer.concat([PKCS8, seed]), format: "der", type: "pkcs8" });
|
|
19
|
+
};
|
|
20
|
+
/** The raw ML-DSA-65 public key (1952 bytes) for a 32-byte seed. */
|
|
21
|
+
export const pqPublicKey = (seed) => new Uint8Array(createPublicKey(privateKey(seed)).export({ format: "der", type: "spki" }).subarray(SPKI.length));
|
|
22
|
+
/** Whether this runtime has ML-DSA-65 (Node ≥ 24.6 with OpenSSL ≥ 3.5). */
|
|
23
|
+
export function pqAvailable() {
|
|
24
|
+
try {
|
|
25
|
+
pqPublicKey(new Uint8Array(32));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const validTs = (ts) => typeof ts === "string" && !Number.isNaN(Date.parse(ts));
|
|
33
|
+
const hex = (b) => Buffer.from(b).toString("hex");
|
|
34
|
+
/** Zanii's pq.binding: both keys sign the same body, so holding one key can't forge it. */
|
|
35
|
+
export function bindPqKey(o) {
|
|
36
|
+
if (!o.did)
|
|
37
|
+
throw new Error("did is required");
|
|
38
|
+
if (!validTs(o.ts))
|
|
39
|
+
throw new Error("ts must be an ISO timestamp");
|
|
40
|
+
const unsigned = {
|
|
41
|
+
v: 1,
|
|
42
|
+
type: "pq.binding",
|
|
43
|
+
did: o.did,
|
|
44
|
+
alg: PQ_ALG,
|
|
45
|
+
pq_pub: hex(pqPublicKey(o.pqSeed)),
|
|
46
|
+
ts: o.ts,
|
|
47
|
+
};
|
|
48
|
+
const raw = canonicalBytes(unsigned);
|
|
49
|
+
return {
|
|
50
|
+
...unsigned,
|
|
51
|
+
sig_ed: `ed25519:${hex(ed25519Sign(o.edPrivateKey, raw))}`,
|
|
52
|
+
sig_pq: `mldsa65:${hex(sign(null, raw, privateKey(o.pqSeed)))}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const pqVerify = (publicKey, msg, sig) => {
|
|
56
|
+
try {
|
|
57
|
+
const key = createPublicKey({
|
|
58
|
+
key: Buffer.concat([SPKI, publicKey]),
|
|
59
|
+
format: "der",
|
|
60
|
+
type: "spki",
|
|
61
|
+
});
|
|
62
|
+
return verify(null, msg, key, sig);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** Zanii's check: the structure, and BOTH signatures over the same body. */
|
|
69
|
+
export function verifyPqBinding(binding) {
|
|
70
|
+
const b = (binding ?? {});
|
|
71
|
+
const reasons = [];
|
|
72
|
+
if (b.v !== 1 || b.type !== "pq.binding")
|
|
73
|
+
reasons.push("not a pq.binding (v1)");
|
|
74
|
+
if (b.alg !== PQ_ALG)
|
|
75
|
+
reasons.push("unsupported alg (want ML-DSA-65)");
|
|
76
|
+
if (!b.did)
|
|
77
|
+
reasons.push("missing did");
|
|
78
|
+
const pqPub = typeof b.pq_pub === "string" && /^([0-9a-f]{2})+$/.test(b.pq_pub)
|
|
79
|
+
? new Uint8Array(Buffer.from(b.pq_pub, "hex"))
|
|
80
|
+
: null;
|
|
81
|
+
if (!pqPub)
|
|
82
|
+
reasons.push("missing/invalid pq_pub");
|
|
83
|
+
if (!validTs(b.ts))
|
|
84
|
+
reasons.push("missing/invalid ts");
|
|
85
|
+
if (reasons.length > 0 || !pqPub)
|
|
86
|
+
return { ok: false, reasons };
|
|
87
|
+
const { sig_ed: sigEd, sig_pq: sigPq, ...unsigned } = b;
|
|
88
|
+
const raw = canonicalBytes(unsigned);
|
|
89
|
+
const pub = publicKeyFromDid(String(b.did));
|
|
90
|
+
const ed = typeof sigEd === "string" ? /^ed25519:([0-9a-f]{128})$/.exec(sigEd) : null;
|
|
91
|
+
if (!pub || !ed || !ed25519Verify(pub, raw, Buffer.from(ed[1], "hex")))
|
|
92
|
+
reasons.push("Ed25519 signature does not verify against the did");
|
|
93
|
+
const pq = typeof sigPq === "string" ? /^mldsa65:((?:[0-9a-f]{2})+)$/.exec(sigPq) : null;
|
|
94
|
+
if (!pq || !pqVerify(pqPub, raw, Buffer.from(pq[1], "hex")))
|
|
95
|
+
reasons.push("ML-DSA signature does not verify against pq_pub (possession not proven)");
|
|
96
|
+
return { ok: reasons.length === 0, reasons };
|
|
97
|
+
}
|
|
98
|
+
/** Zanii's transition payload, recorded UNSALTED: the binding as compact JSON. */
|
|
99
|
+
export function pqTransitionPayload(binding) {
|
|
100
|
+
const check = verifyPqBinding(binding);
|
|
101
|
+
if (!check.ok)
|
|
102
|
+
throw new Error(`refusing to anchor an invalid binding: ${check.reasons.join("; ")}`);
|
|
103
|
+
return JSON.stringify(binding);
|
|
104
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SearchQuery {
|
|
2
|
+
/** An event kind, e.g. `tool.call`, `finding`, `control`. */
|
|
3
|
+
kind?: string;
|
|
4
|
+
/** A tool's name, as called: `delete_record`, `mcp__db__delete_record`, an SDK tool. */
|
|
5
|
+
tool?: string;
|
|
6
|
+
/** A finding code, e.g. `TOOL_CHANGED`. */
|
|
7
|
+
code?: string;
|
|
8
|
+
/** A person who decided an approval or a grant: their `sub`, `email` (any case) or `name`. */
|
|
9
|
+
person?: string;
|
|
10
|
+
/** The session's environment (`session.open` meta.environment). */
|
|
11
|
+
environment?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SearchMatch {
|
|
14
|
+
seq: number;
|
|
15
|
+
kind: string;
|
|
16
|
+
ts: string;
|
|
17
|
+
/** What matched: the tool, the code, the person, or the kind. */
|
|
18
|
+
match: string;
|
|
19
|
+
}
|
|
20
|
+
/** The query's fields, checked: each a 1-256 character string; `environment` alone is allowed. */
|
|
21
|
+
export declare function checkSearch(q: Record<string, unknown>): string | undefined;
|
|
22
|
+
/** Every event of the record that matches all of the query's fields, in seq order. */
|
|
23
|
+
export declare function searchEvents(lines: readonly string[], q: SearchQuery): SearchMatch[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Structured search over a session's record (spec/search.md): exact matches on what the events say
|
|
2
|
+
// (a tool, a fault code, a person, an event kind, the session's environment), never text inside
|
|
3
|
+
// bodies. Precise by construction, and it reads nothing that was redacted. Pure; mirrors
|
|
4
|
+
// sdks/python/src/zanii_blackbox/search.py; pinned by spec/vectors/search.json.
|
|
5
|
+
const DECISIONS = new Set(["approval", "approval_vote", "permission"]);
|
|
6
|
+
/** The query's fields, checked: each a 1-256 character string; `environment` alone is allowed. */
|
|
7
|
+
export function checkSearch(q) {
|
|
8
|
+
const fields = ["kind", "tool", "code", "person", "environment"];
|
|
9
|
+
for (const k of Object.keys(q))
|
|
10
|
+
if (!fields.includes(k))
|
|
11
|
+
return `unknown search field ${JSON.stringify(k)}`;
|
|
12
|
+
for (const k of fields) {
|
|
13
|
+
const v = q[k];
|
|
14
|
+
if (v !== undefined && (typeof v !== "string" || v.length < 1 || v.length > 256))
|
|
15
|
+
return `${k} must be 1-256 characters`;
|
|
16
|
+
}
|
|
17
|
+
return fields.some((k) => q[k] !== undefined)
|
|
18
|
+
? undefined
|
|
19
|
+
: "give kind, tool, code, person or environment";
|
|
20
|
+
}
|
|
21
|
+
function toolOf(e) {
|
|
22
|
+
if (e.kind === "tool.call" && typeof e.meta.tool === "string")
|
|
23
|
+
return typeof e.meta.server === "string"
|
|
24
|
+
? [e.meta.tool, `mcp__${e.meta.server}__${e.meta.tool}`]
|
|
25
|
+
: [e.meta.tool];
|
|
26
|
+
if (e.kind === "sdk.event" && e.meta.type === "tool.call" && typeof e.meta.name === "string")
|
|
27
|
+
return [e.meta.name];
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
function personMatch(e, who) {
|
|
31
|
+
if (e.kind !== "control" || !DECISIONS.has(String(e.meta.action)))
|
|
32
|
+
return false;
|
|
33
|
+
const p = e.meta.person;
|
|
34
|
+
if (!p)
|
|
35
|
+
return false;
|
|
36
|
+
return (p.sub === who ||
|
|
37
|
+
p.name === who ||
|
|
38
|
+
(typeof p.email === "string" && p.email.toLowerCase() === who.toLowerCase()));
|
|
39
|
+
}
|
|
40
|
+
/** Every event of the record that matches all of the query's fields, in seq order. */
|
|
41
|
+
export function searchEvents(lines, q) {
|
|
42
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
43
|
+
const open = events.find((e) => e.kind === "session.open");
|
|
44
|
+
if (q.environment !== undefined && open?.meta.environment !== q.environment)
|
|
45
|
+
return [];
|
|
46
|
+
const eventWise = q.kind !== undefined || q.tool !== undefined || q.code !== undefined || q.person !== undefined;
|
|
47
|
+
if (!eventWise)
|
|
48
|
+
return open
|
|
49
|
+
? [{ seq: open.seq, kind: open.kind, ts: open.ts, match: q.environment }]
|
|
50
|
+
: [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const e of events) {
|
|
53
|
+
if (q.kind !== undefined && e.kind !== q.kind)
|
|
54
|
+
continue;
|
|
55
|
+
if (q.tool !== undefined && !toolOf(e).includes(q.tool))
|
|
56
|
+
continue;
|
|
57
|
+
if (q.code !== undefined && !(e.kind === "finding" && e.meta.code === q.code))
|
|
58
|
+
continue;
|
|
59
|
+
if (q.person !== undefined && !personMatch(e, q.person))
|
|
60
|
+
continue;
|
|
61
|
+
out.push({
|
|
62
|
+
seq: e.seq,
|
|
63
|
+
kind: e.kind,
|
|
64
|
+
ts: e.ts,
|
|
65
|
+
match: q.tool ?? q.code ?? q.person ?? q.kind,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Attestation } from "../attest/index.ts";
|
|
2
|
+
import { buildPayment, type screenCounterparty } from "../finance/index.ts";
|
|
2
3
|
export interface SessionOptions {
|
|
3
4
|
/** The gateway, e.g. http://127.0.0.1:8787 */
|
|
4
5
|
url: string;
|
|
@@ -33,6 +34,8 @@ export interface SessionOptions {
|
|
|
33
34
|
tenant?: string;
|
|
34
35
|
/** spec/authority.md: `supervised` from the start (risky actions wait for a second person). */
|
|
35
36
|
authority?: "agent" | "supervised";
|
|
37
|
+
/** spec/api.md: where it runs (`production`, `staging`, …); policy rules can match it. */
|
|
38
|
+
environment?: string;
|
|
36
39
|
/** spec/preflight.md: the equipment the run needs. A no-go returns a disabled session (and
|
|
37
40
|
* `stats.lastError` says why); optional items that are down show in `state().degraded`. */
|
|
38
41
|
preflight?: {
|
|
@@ -144,6 +147,10 @@ export declare class BlackboxSession {
|
|
|
144
147
|
private timers;
|
|
145
148
|
private closed;
|
|
146
149
|
private landings;
|
|
150
|
+
/** spec/approvals.md §2: approved tools with arguments, until their call is reported. */
|
|
151
|
+
private approved;
|
|
152
|
+
/** spec/agents.md §4: the memory chain's tip. */
|
|
153
|
+
private memoryLast;
|
|
147
154
|
/** A session that records nothing. `reason`, when opening failed, goes to `stats.lastError` (audit K1). */
|
|
148
155
|
static disabled(options: SessionOptions, reason?: string): BlackboxSession;
|
|
149
156
|
private readonly options;
|
|
@@ -161,6 +168,67 @@ export declare class BlackboxSession {
|
|
|
161
168
|
};
|
|
162
169
|
}): void;
|
|
163
170
|
step(name: string, data?: Record<string, unknown>): void;
|
|
171
|
+
/**
|
|
172
|
+
* spec/gov.md §2: a decision about a person, under the rulebook `manifestHash`, the person named
|
|
173
|
+
* only by their subject tag. Returns the factors' nonce (keep it to disclose them in a dispute).
|
|
174
|
+
* Throws on a decision that can't be lawful evidence: no rulebook, a bad kind, no tag.
|
|
175
|
+
*/
|
|
176
|
+
decision(kind: string, o: {
|
|
177
|
+
manifestHash: string;
|
|
178
|
+
outcome: Record<string, unknown>;
|
|
179
|
+
subjectTag: string;
|
|
180
|
+
factors?: Record<string, unknown>;
|
|
181
|
+
appealBy?: string;
|
|
182
|
+
}): {
|
|
183
|
+
nonce: string;
|
|
184
|
+
};
|
|
185
|
+
/** spec/health.md §2: who accessed a patient's record (an episode tag, never the seed), and why. */
|
|
186
|
+
healthAccess(o: {
|
|
187
|
+
episodeTag: string;
|
|
188
|
+
actor: string;
|
|
189
|
+
purpose: string;
|
|
190
|
+
consentRef?: string;
|
|
191
|
+
details?: Record<string, unknown>;
|
|
192
|
+
}): {
|
|
193
|
+
nonce: string;
|
|
194
|
+
};
|
|
195
|
+
/** spec/health.md §2: a model's recommendation under a protocol. `hash` is what the clinician signs. */
|
|
196
|
+
recommendation(o: {
|
|
197
|
+
episodeTag: string;
|
|
198
|
+
modelId: string;
|
|
199
|
+
manifestHash: string;
|
|
200
|
+
outcome: Record<string, unknown>;
|
|
201
|
+
runtimeHash?: string;
|
|
202
|
+
deviation?: Record<string, unknown>;
|
|
203
|
+
}): {
|
|
204
|
+
nonce: string;
|
|
205
|
+
hash: string;
|
|
206
|
+
};
|
|
207
|
+
/** spec/health.md §2: a clinician's signed confirmation (clinicianConfirmation). Throws on a bad signature. */
|
|
208
|
+
clinicianConfirmation(episodeTag: string, payload: Record<string, unknown>): void;
|
|
209
|
+
/** spec/health.md §2: an emergency access, signed by the clinician (breakGlass). Loud on purpose. */
|
|
210
|
+
breakGlass(episodeTag: string, payload: Record<string, unknown>): void;
|
|
211
|
+
private signedHealth;
|
|
212
|
+
/** spec/finance.md §2: a payment, exact (minor units), with its settlement reference. Throws on a bad amount. */
|
|
213
|
+
payment(input: Parameters<typeof buildPayment>[0]): void;
|
|
214
|
+
/** spec/finance.md §3: a counterparty screening (screenCounterparty), before paying them. */
|
|
215
|
+
screening(result: ReturnType<typeof screenCounterparty>): void;
|
|
216
|
+
/** spec/finance.md §4: a tax filing the agent prepared. It never files; a Tax Agent does. */
|
|
217
|
+
ftaPrepared(o: {
|
|
218
|
+
filingType: string;
|
|
219
|
+
period: string;
|
|
220
|
+
filingRef?: string;
|
|
221
|
+
manifestHash?: string;
|
|
222
|
+
by?: string;
|
|
223
|
+
}): void;
|
|
224
|
+
/** spec/finance.md §4: the prepared filing handed to a licensed Tax Agent. */
|
|
225
|
+
ftaHandoff(o: {
|
|
226
|
+
filingType: string;
|
|
227
|
+
period: string;
|
|
228
|
+
toTaxAgent: string;
|
|
229
|
+
filingRef?: string;
|
|
230
|
+
by?: string;
|
|
231
|
+
}): void;
|
|
164
232
|
toolCall(name: string, args?: unknown): void;
|
|
165
233
|
toolResult(name: string, result?: {
|
|
166
234
|
ok?: boolean;
|
|
@@ -200,6 +268,15 @@ export declare class BlackboxSession {
|
|
|
200
268
|
summary?: string;
|
|
201
269
|
file_path?: string;
|
|
202
270
|
}): void;
|
|
271
|
+
/** spec/findings.md §11 (H3): pin the documents the agent read, by hash; the content never leaves.
|
|
272
|
+
* Each doc is `{id, uri?, version?}` with its `content` (hashed here) or its `sha256`. */
|
|
273
|
+
retrieved(docs: ReadonlyArray<{
|
|
274
|
+
id: string;
|
|
275
|
+
uri?: string;
|
|
276
|
+
version?: string;
|
|
277
|
+
content?: string | Uint8Array;
|
|
278
|
+
sha256?: string;
|
|
279
|
+
}>): void;
|
|
203
280
|
/** Stage 2 R3: the result of one of the customer's own checks (tests, a schema, a policy, a
|
|
204
281
|
* partial goal), mid-run or at the end. A pass then a fail is VERIFY_REGRESSION; a success whose last
|
|
205
282
|
* check failed is FALSE_SUCCESS (spec/findings.md §6). */
|
|
@@ -234,7 +311,18 @@ export declare class BlackboxSession {
|
|
|
234
311
|
height?: number;
|
|
235
312
|
}): void;
|
|
236
313
|
/** spec/agents.md §1: the agent's memory store wrote, revoked or returned memories. */
|
|
237
|
-
|
|
314
|
+
/** spec/agents.md §4: with `content`, the write also carries Zanii's memory entry (a salted
|
|
315
|
+
* commitment, chained after `prev`, else this session's last entry). Keep the salt it returns. */
|
|
316
|
+
memoryWrite(memoryId: string, summary?: string, opts?: {
|
|
317
|
+
content: string;
|
|
318
|
+
kind?: string;
|
|
319
|
+
prev?: Record<string, unknown>;
|
|
320
|
+
agent?: string;
|
|
321
|
+
tags?: string[];
|
|
322
|
+
}): {
|
|
323
|
+
entry: Record<string, unknown>;
|
|
324
|
+
salt: string;
|
|
325
|
+
} | undefined;
|
|
238
326
|
memoryRevoke(memoryId: string, reason?: string): void;
|
|
239
327
|
memoryRead(memoryIds: string[], query?: string): void;
|
|
240
328
|
/** Ships what's pending now. Resolves true when everything recorded is acknowledged. With
|
package/dist/session/index.js
CHANGED
|
@@ -6,7 +6,19 @@ import { request as httpRequest } from "node:http";
|
|
|
6
6
|
import { request as httpsRequest } from "node:https";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { argsHash } from "../approvals/index.js";
|
|
9
10
|
import { attest, isReadOnly } from "../attest/index.js";
|
|
11
|
+
import { buildPayment, ftaPayload, screeningPayload, } from "../finance/index.js";
|
|
12
|
+
import { decisionPayload } from "../gov/index.js";
|
|
13
|
+
import { accessPayload, checkEpisodeTag, recommendationPayload, verifyHealthSignature, } from "../health/index.js";
|
|
14
|
+
import { memoryEntry } from "../memory/index.js";
|
|
15
|
+
/** spec/preflight.md §1 (L3): with an `egress` item, the SDK checks it here, on the agent's host. */
|
|
16
|
+
async function withEgress(p) {
|
|
17
|
+
if (![...(p.require ?? []), ...(p.optional ?? [])].includes("egress"))
|
|
18
|
+
return p;
|
|
19
|
+
const { url, open } = await checkEgress({ timeoutMs: 3000 });
|
|
20
|
+
return { ...p, egress: { url, open } };
|
|
21
|
+
}
|
|
10
22
|
/** spec/data.md §6: a direct HTTPS request to `url` (default https://example.com). Never throws. */
|
|
11
23
|
export async function checkEgress(options = {}) {
|
|
12
24
|
const url = options.url ?? "https://example.com";
|
|
@@ -71,7 +83,8 @@ export async function session(options) {
|
|
|
71
83
|
...(o.replay ? { replay: o.replay } : {}),
|
|
72
84
|
...(o.tenant ? { tenant: o.tenant } : {}),
|
|
73
85
|
...(o.authority ? { authority: o.authority } : {}),
|
|
74
|
-
...(o.
|
|
86
|
+
...(o.environment ? { environment: o.environment } : {}),
|
|
87
|
+
...(o.preflight ? { preflight: await withEgress(o.preflight) } : {}),
|
|
75
88
|
...(o.drill ? { drill: o.drill } : {}),
|
|
76
89
|
sdk: true, // this SDK will report the session's model calls (L2.3.2)
|
|
77
90
|
});
|
|
@@ -103,6 +116,10 @@ export class BlackboxSession {
|
|
|
103
116
|
timers = [];
|
|
104
117
|
closed = false;
|
|
105
118
|
landings = 0;
|
|
119
|
+
/** spec/approvals.md §2: approved tools with arguments, until their call is reported. */
|
|
120
|
+
approved = new Map();
|
|
121
|
+
/** spec/agents.md §4: the memory chain's tip. */
|
|
122
|
+
memoryLast = null;
|
|
106
123
|
/** A session that records nothing. `reason`, when opening failed, goes to `stats.lastError` (audit K1). */
|
|
107
124
|
static disabled(options, reason) {
|
|
108
125
|
const s = new BlackboxSession(options, "", "", false);
|
|
@@ -208,8 +225,82 @@ export class BlackboxSession {
|
|
|
208
225
|
step(name, data) {
|
|
209
226
|
this.event("step", name, data);
|
|
210
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* spec/gov.md §2: a decision about a person, under the rulebook `manifestHash`, the person named
|
|
230
|
+
* only by their subject tag. Returns the factors' nonce (keep it to disclose them in a dispute).
|
|
231
|
+
* Throws on a decision that can't be lawful evidence: no rulebook, a bad kind, no tag.
|
|
232
|
+
*/
|
|
233
|
+
decision(kind, o) {
|
|
234
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(o.subjectTag))
|
|
235
|
+
throw new Error("subjectTag must be a subject tag (subjectTag(did, authority))");
|
|
236
|
+
const { payload, nonce } = decisionPayload({
|
|
237
|
+
kind,
|
|
238
|
+
manifestHash: o.manifestHash,
|
|
239
|
+
outcome: o.outcome,
|
|
240
|
+
ts: new Date().toISOString(),
|
|
241
|
+
...(o.factors !== undefined ? { factors: o.factors } : {}),
|
|
242
|
+
...(o.appealBy !== undefined ? { appealBy: o.appealBy } : {}),
|
|
243
|
+
});
|
|
244
|
+
this.event("decision", kind, { decision: payload, subject_tag: o.subjectTag });
|
|
245
|
+
return { nonce };
|
|
246
|
+
}
|
|
247
|
+
/** spec/health.md §2: who accessed a patient's record (an episode tag, never the seed), and why. */
|
|
248
|
+
healthAccess(o) {
|
|
249
|
+
const tag = checkEpisodeTag(o.episodeTag);
|
|
250
|
+
const { payload, nonce } = accessPayload({ ...o, ts: new Date().toISOString() });
|
|
251
|
+
this.event("health.access", "access", { payload, episode_tag: tag });
|
|
252
|
+
return { nonce };
|
|
253
|
+
}
|
|
254
|
+
/** spec/health.md §2: a model's recommendation under a protocol. `hash` is what the clinician signs. */
|
|
255
|
+
recommendation(o) {
|
|
256
|
+
const tag = checkEpisodeTag(o.episodeTag);
|
|
257
|
+
const { payload, nonce, hash } = recommendationPayload({ ...o, ts: new Date().toISOString() });
|
|
258
|
+
this.event("health.recommendation", "recommendation", { payload, episode_tag: tag });
|
|
259
|
+
return { nonce, hash };
|
|
260
|
+
}
|
|
261
|
+
/** spec/health.md §2: a clinician's signed confirmation (clinicianConfirmation). Throws on a bad signature. */
|
|
262
|
+
clinicianConfirmation(episodeTag, payload) {
|
|
263
|
+
this.signedHealth(episodeTag, payload, "confirmation");
|
|
264
|
+
}
|
|
265
|
+
/** spec/health.md §2: an emergency access, signed by the clinician (breakGlass). Loud on purpose. */
|
|
266
|
+
breakGlass(episodeTag, payload) {
|
|
267
|
+
this.signedHealth(episodeTag, payload, "break_glass");
|
|
268
|
+
}
|
|
269
|
+
signedHealth(episodeTag, payload, kind) {
|
|
270
|
+
const tag = checkEpisodeTag(episodeTag);
|
|
271
|
+
if (payload.kind !== kind || !verifyHealthSignature(payload, tag))
|
|
272
|
+
throw new Error(`not a ${kind} signed by the clinician it names, for this episode`);
|
|
273
|
+
this.event(`health.${kind}`, kind, { payload, episode_tag: tag });
|
|
274
|
+
}
|
|
275
|
+
/** spec/finance.md §2: a payment, exact (minor units), with its settlement reference. Throws on a bad amount. */
|
|
276
|
+
payment(input) {
|
|
277
|
+
this.event("payment", input.rail, buildPayment(input));
|
|
278
|
+
}
|
|
279
|
+
/** spec/finance.md §3: a counterparty screening (screenCounterparty), before paying them. */
|
|
280
|
+
screening(result) {
|
|
281
|
+
this.event("kya.screening", result.did, screeningPayload(result, new Date().toISOString(), this.id));
|
|
282
|
+
}
|
|
283
|
+
/** spec/finance.md §4: a tax filing the agent prepared. It never files; a Tax Agent does. */
|
|
284
|
+
ftaPrepared(o) {
|
|
285
|
+
this.event("fta.prepared", o.filingType, ftaPayload({ ...o, action: "prepared", by: o.by ?? this.id }));
|
|
286
|
+
}
|
|
287
|
+
/** spec/finance.md §4: the prepared filing handed to a licensed Tax Agent. */
|
|
288
|
+
ftaHandoff(o) {
|
|
289
|
+
this.event("fta.handoff", o.filingType, ftaPayload({ ...o, action: "handoff", by: o.by ?? this.id }));
|
|
290
|
+
}
|
|
211
291
|
toolCall(name, args) {
|
|
212
|
-
|
|
292
|
+
// spec/approvals.md §2: the first call after an approval names it, with what it ran
|
|
293
|
+
const approvalId = this.approved.get(name);
|
|
294
|
+
if (approvalId !== undefined)
|
|
295
|
+
this.approved.delete(name);
|
|
296
|
+
this.event("tool.call", name, args === undefined && approvalId === undefined
|
|
297
|
+
? undefined
|
|
298
|
+
: {
|
|
299
|
+
...(args !== undefined ? { args } : {}),
|
|
300
|
+
...(approvalId !== undefined
|
|
301
|
+
? { approval_id: approvalId, args_sha256: argsHash(args ?? null) }
|
|
302
|
+
: {}),
|
|
303
|
+
});
|
|
213
304
|
}
|
|
214
305
|
toolResult(name, result) {
|
|
215
306
|
this.event("tool.result", name, result);
|
|
@@ -277,6 +368,30 @@ export class BlackboxSession {
|
|
|
277
368
|
contextChange(change) {
|
|
278
369
|
this.event("context.change", change.kind, { ...change });
|
|
279
370
|
}
|
|
371
|
+
/** spec/findings.md §11 (H3): pin the documents the agent read, by hash; the content never leaves.
|
|
372
|
+
* Each doc is `{id, uri?, version?}` with its `content` (hashed here) or its `sha256`. */
|
|
373
|
+
retrieved(docs) {
|
|
374
|
+
const out = [];
|
|
375
|
+
for (const d of docs.slice(0, 100)) {
|
|
376
|
+
if (typeof d?.id !== "string" || d.id.length < 1)
|
|
377
|
+
continue;
|
|
378
|
+
const bytes = typeof d.content === "string" ? Buffer.from(d.content) : d.content;
|
|
379
|
+
const sha256 = bytes
|
|
380
|
+
? `sha256:${createHash("sha256").update(bytes).digest("hex")}`
|
|
381
|
+
: d.sha256;
|
|
382
|
+
if (typeof sha256 !== "string" || !/^sha256:[0-9a-f]{64}$/.test(sha256))
|
|
383
|
+
continue;
|
|
384
|
+
out.push({
|
|
385
|
+
id: d.id.slice(0, 256),
|
|
386
|
+
...(typeof d.uri === "string" ? { uri: d.uri.slice(0, 2048) } : {}),
|
|
387
|
+
...(typeof d.version === "string" ? { version: d.version.slice(0, 128) } : {}),
|
|
388
|
+
sha256,
|
|
389
|
+
...(bytes ? { bytes: bytes.length } : {}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (out.length)
|
|
393
|
+
this.event("retrieved", undefined, { docs: out });
|
|
394
|
+
}
|
|
280
395
|
/** Stage 2 R3: the result of one of the customer's own checks (tests, a schema, a policy, a
|
|
281
396
|
* partial goal), mid-run or at the end. A pass then a fail is VERIFY_REGRESSION; a success whose last
|
|
282
397
|
* check failed is FALSE_SUCCESS (spec/findings.md §6). */
|
|
@@ -338,8 +453,22 @@ export class BlackboxSession {
|
|
|
338
453
|
this.event("screen", undefined, data, { attachment: { contentType: type, bytes: image } });
|
|
339
454
|
}
|
|
340
455
|
/** spec/agents.md §1: the agent's memory store wrote, revoked or returned memories. */
|
|
341
|
-
|
|
342
|
-
|
|
456
|
+
/** spec/agents.md §4: with `content`, the write also carries Zanii's memory entry (a salted
|
|
457
|
+
* commitment, chained after `prev`, else this session's last entry). Keep the salt it returns. */
|
|
458
|
+
memoryWrite(memoryId, summary, opts) {
|
|
459
|
+
const data = { memory_id: memoryId, ...(summary ? { summary } : {}) };
|
|
460
|
+
let made;
|
|
461
|
+
if (opts) {
|
|
462
|
+
made = memoryEntry({
|
|
463
|
+
...opts,
|
|
464
|
+
ts: new Date().toISOString(),
|
|
465
|
+
prev: opts.prev ?? this.memoryLast,
|
|
466
|
+
});
|
|
467
|
+
this.memoryLast = made.payload;
|
|
468
|
+
data.entry = made.payload;
|
|
469
|
+
}
|
|
470
|
+
this.event("memory.write", undefined, data);
|
|
471
|
+
return made && { entry: made.payload, salt: made.salt };
|
|
343
472
|
}
|
|
344
473
|
memoryRevoke(memoryId, reason) {
|
|
345
474
|
this.event("memory.revoke", undefined, { memory_id: memoryId, ...(reason ? { reason } : {}) });
|
|
@@ -396,17 +525,20 @@ export class BlackboxSession {
|
|
|
396
525
|
* and waits (polling) for the answer. Never throws. Only `"approved"` means go ahead; anything
|
|
397
526
|
* else (`rejected`, `timeout`, or `error` when the gateway can't be asked) means don't. */
|
|
398
527
|
async requestApproval(tool, options = {}) {
|
|
399
|
-
|
|
528
|
+
const { status, id } = await this.ask("approvals", { tool, ...(options.args ? { args: options.args } : {}) }, options);
|
|
529
|
+
if (status === "approved" && options.args && id)
|
|
530
|
+
this.approved.set(tool, id);
|
|
531
|
+
return status;
|
|
400
532
|
}
|
|
401
533
|
/** N2 (idea C2): asks a person to let this session use a gateway tool its policy denies (the
|
|
402
534
|
* denial's `requires.tool`, e.g. `mcp__files__delete`). A yes is a standing grant for the
|
|
403
535
|
* session: retry the call. Answers like `requestApproval`. Never throws. */
|
|
404
536
|
async requestPermission(tool, options = {}) {
|
|
405
|
-
return this.ask("permissions", { tool }, options);
|
|
537
|
+
return (await this.ask("permissions", { tool }, options)).status;
|
|
406
538
|
}
|
|
407
539
|
async ask(route, body, options) {
|
|
408
540
|
if (!this.enabled)
|
|
409
|
-
return "error";
|
|
541
|
+
return { status: "error" };
|
|
410
542
|
try {
|
|
411
543
|
const r = await post(this.options.url, `/v1/sessions/${this.id}/${route}`, this.token, {
|
|
412
544
|
...body,
|
|
@@ -415,21 +547,21 @@ export class BlackboxSession {
|
|
|
415
547
|
const id = r.json.approval_id;
|
|
416
548
|
if (r.status !== 201 || typeof id !== "string") {
|
|
417
549
|
this.fail("rejected", `asking for an approval: the gateway answered ${r.status}`);
|
|
418
|
-
return "error";
|
|
550
|
+
return { status: "error" };
|
|
419
551
|
}
|
|
420
552
|
const deadline = Date.now() + (options.timeoutMs ?? 310_000);
|
|
421
553
|
while (Date.now() < deadline) {
|
|
422
554
|
const s = await send("GET", this.options.url, `/v1/sessions/${this.id}/approvals/${id}`, this.token);
|
|
423
555
|
const status = s.json.status;
|
|
424
556
|
if (status === "approved" || status === "rejected" || status === "timeout")
|
|
425
|
-
return status;
|
|
557
|
+
return { status, id };
|
|
426
558
|
await sleep(options.pollMs ?? 1_000);
|
|
427
559
|
}
|
|
428
|
-
return "timeout";
|
|
560
|
+
return { status: "timeout", id };
|
|
429
561
|
}
|
|
430
562
|
catch (error) {
|
|
431
563
|
this.fail("network", `asking for an approval: ${message(error)}`);
|
|
432
|
-
return "error";
|
|
564
|
+
return { status: "error" };
|
|
433
565
|
}
|
|
434
566
|
}
|
|
435
567
|
/** Audit S8: `await using s = await session(…)` closes the session when the scope ends. */
|