@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.
Files changed (82) hide show
  1. package/README.md +26 -1
  2. package/dist/a2a/index.d.ts +27 -0
  3. package/dist/a2a/index.js +104 -1
  4. package/dist/agents/index.d.ts +8 -0
  5. package/dist/analysis/accuracy.d.ts +24 -0
  6. package/dist/analysis/accuracy.js +45 -0
  7. package/dist/analysis/credential.d.ts +101 -0
  8. package/dist/analysis/credential.js +142 -0
  9. package/dist/analysis/faults.js +115 -0
  10. package/dist/analysis/grounding.d.ts +122 -0
  11. package/dist/analysis/grounding.js +445 -0
  12. package/dist/analysis/hallucination.d.ts +32 -0
  13. package/dist/analysis/hallucination.js +357 -0
  14. package/dist/analysis/index.d.ts +23 -0
  15. package/dist/analysis/index.js +93 -0
  16. package/dist/analysis/memory.d.ts +8 -0
  17. package/dist/analysis/memory.js +35 -8
  18. package/dist/analysis/reference.d.ts +49 -0
  19. package/dist/analysis/reference.js +164 -0
  20. package/dist/analysis/taxonomy.js +1 -0
  21. package/dist/approvals/index.d.ts +23 -0
  22. package/dist/approvals/index.js +48 -0
  23. package/dist/archive/parquet.d.ts +2 -0
  24. package/dist/archive/parquet.js +185 -0
  25. package/dist/badge/index.d.ts +16 -0
  26. package/dist/badge/index.js +48 -0
  27. package/dist/bom/index.js +20 -0
  28. package/dist/cli.js +114 -10
  29. package/dist/compliance/art12.js +36 -9
  30. package/dist/compliance/index.d.ts +36 -2
  31. package/dist/compliance/index.js +78 -11
  32. package/dist/compliance/zanii.d.ts +29 -0
  33. package/dist/compliance/zanii.js +84 -0
  34. package/dist/constitution/index.d.ts +57 -0
  35. package/dist/constitution/index.js +131 -0
  36. package/dist/cv/index.d.ts +39 -0
  37. package/dist/cv/index.js +108 -0
  38. package/dist/disclosure/index.d.ts +31 -0
  39. package/dist/disclosure/index.js +113 -0
  40. package/dist/encryption/index.d.ts +9 -0
  41. package/dist/encryption/index.js +31 -0
  42. package/dist/evidence/index.d.ts +60 -0
  43. package/dist/evidence/index.js +151 -0
  44. package/dist/federation/index.d.ts +35 -0
  45. package/dist/federation/index.js +102 -0
  46. package/dist/finance/index.d.ts +126 -0
  47. package/dist/finance/index.js +320 -0
  48. package/dist/fleet/index.js +9 -0
  49. package/dist/gov/index.d.ts +108 -0
  50. package/dist/gov/index.js +225 -0
  51. package/dist/health/index.d.ts +120 -0
  52. package/dist/health/index.js +233 -0
  53. package/dist/index.d.ts +28 -6
  54. package/dist/index.js +28 -6
  55. package/dist/memory/index.d.ts +36 -0
  56. package/dist/memory/index.js +85 -0
  57. package/dist/occurrence/index.d.ts +11 -0
  58. package/dist/occurrence/index.js +18 -0
  59. package/dist/otlp/index.js +28 -1
  60. package/dist/packs/index.js +44 -4
  61. package/dist/policy/delta.js +7 -1
  62. package/dist/policy/index.d.ts +23 -6
  63. package/dist/policy/index.js +151 -8
  64. package/dist/policy/zanii.d.ts +31 -0
  65. package/dist/policy/zanii.js +87 -0
  66. package/dist/pq/index.d.ts +23 -0
  67. package/dist/pq/index.js +104 -0
  68. package/dist/search/index.d.ts +23 -0
  69. package/dist/search/index.js +69 -0
  70. package/dist/session/index.d.ts +89 -1
  71. package/dist/session/index.js +143 -11
  72. package/dist/sla/index.d.ts +61 -0
  73. package/dist/sla/index.js +197 -0
  74. package/dist/succession/index.d.ts +50 -0
  75. package/dist/succession/index.js +123 -0
  76. package/dist/tokens/index.d.ts +6 -0
  77. package/dist/tokens/index.js +46 -0
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/dist/walls/index.d.ts +31 -0
  81. package/dist/walls/index.js +119 -0
  82. 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 {};
@@ -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
+ }
@@ -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
- memoryWrite(memoryId: string, summary?: string): void;
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
@@ -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.preflight ? { preflight: o.preflight } : {}),
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
- this.event("tool.call", name, args === undefined ? undefined : { args });
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
- memoryWrite(memoryId, summary) {
342
- this.event("memory.write", undefined, { memory_id: memoryId, ...(summary ? { summary } : {}) });
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
- return this.ask("approvals", { tool, ...(options.args ? { args: options.args } : {}) }, options);
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. */