@zanii/blackbox 0.0.0-stage → 0.2.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 (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +109 -2
  3. package/dist/agents/index.d.ts +34 -0
  4. package/dist/agents/index.js +73 -0
  5. package/dist/analysis/detectors.d.ts +36 -0
  6. package/dist/analysis/detectors.js +339 -0
  7. package/dist/analysis/faults.d.ts +9 -0
  8. package/dist/analysis/faults.js +250 -0
  9. package/dist/analysis/index.d.ts +68 -0
  10. package/dist/analysis/index.js +388 -0
  11. package/dist/analysis/landing.d.ts +25 -0
  12. package/dist/analysis/landing.js +225 -0
  13. package/dist/analysis/memory.d.ts +13 -0
  14. package/dist/analysis/memory.js +33 -0
  15. package/dist/analysis/waste.d.ts +29 -0
  16. package/dist/analysis/waste.js +79 -0
  17. package/dist/approvals/index.d.ts +11 -0
  18. package/dist/approvals/index.js +27 -0
  19. package/dist/approvals/warnings.d.ts +2 -0
  20. package/dist/approvals/warnings.js +28 -0
  21. package/dist/attest/index.d.ts +17 -0
  22. package/dist/attest/index.js +106 -0
  23. package/dist/authority/index.d.ts +24 -0
  24. package/dist/authority/index.js +77 -0
  25. package/dist/billing/index.d.ts +99 -0
  26. package/dist/billing/index.js +174 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +1057 -0
  29. package/dist/client/index.d.ts +146 -0
  30. package/dist/client/index.js +210 -0
  31. package/dist/compliance/index.d.ts +41 -0
  32. package/dist/compliance/index.js +96 -0
  33. package/dist/cost/index.d.ts +133 -0
  34. package/dist/cost/index.js +293 -0
  35. package/dist/data/index.d.ts +191 -0
  36. package/dist/data/index.js +762 -0
  37. package/dist/directives/index.d.ts +35 -0
  38. package/dist/directives/index.js +80 -0
  39. package/dist/drills/index.d.ts +43 -0
  40. package/dist/drills/index.js +101 -0
  41. package/dist/duty/index.d.ts +21 -0
  42. package/dist/duty/index.js +68 -0
  43. package/dist/fleet/index.d.ts +141 -0
  44. package/dist/fleet/index.js +454 -0
  45. package/dist/hooks/ai-sdk.d.ts +42 -0
  46. package/dist/hooks/ai-sdk.js +62 -0
  47. package/dist/hooks/claude-agent-sdk.d.ts +14 -0
  48. package/dist/hooks/claude-agent-sdk.js +70 -0
  49. package/dist/hooks/index.d.ts +7 -0
  50. package/dist/hooks/index.js +10 -0
  51. package/dist/hooks/langchain-agent.d.ts +69 -0
  52. package/dist/hooks/langchain-agent.js +163 -0
  53. package/dist/hooks/langchain.d.ts +41 -0
  54. package/dist/hooks/langchain.js +216 -0
  55. package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
  56. package/dist/hooks/langgraph-checkpoint.js +73 -0
  57. package/dist/hooks/memory.d.ts +17 -0
  58. package/dist/hooks/memory.js +64 -0
  59. package/dist/hooks/openai-agents.d.ts +6 -0
  60. package/dist/hooks/openai-agents.js +40 -0
  61. package/dist/hooks/protect.d.ts +7 -0
  62. package/dist/hooks/protect.js +39 -0
  63. package/dist/hooks/providers.d.ts +16 -0
  64. package/dist/hooks/providers.js +149 -0
  65. package/dist/hooks/shared.d.ts +11 -0
  66. package/dist/hooks/shared.js +39 -0
  67. package/dist/index.d.ts +46 -0
  68. package/dist/index.js +48 -0
  69. package/dist/investigate/index.d.ts +66 -0
  70. package/dist/investigate/index.js +119 -0
  71. package/dist/mcp-server/index.d.ts +85 -0
  72. package/dist/mcp-server/index.js +216 -0
  73. package/dist/mcp-wrap/index.d.ts +17 -0
  74. package/dist/mcp-wrap/index.js +170 -0
  75. package/dist/money/index.d.ts +114 -0
  76. package/dist/money/index.js +622 -0
  77. package/dist/occurrence/index.d.ts +108 -0
  78. package/dist/occurrence/index.js +168 -0
  79. package/dist/ocsf/index.d.ts +22 -0
  80. package/dist/ocsf/index.js +168 -0
  81. package/dist/otlp/index.d.ts +24 -0
  82. package/dist/otlp/index.js +143 -0
  83. package/dist/packs/index.d.ts +48 -0
  84. package/dist/packs/index.js +343 -0
  85. package/dist/policy/delta.d.ts +11 -0
  86. package/dist/policy/delta.js +39 -0
  87. package/dist/policy/drafts.d.ts +34 -0
  88. package/dist/policy/drafts.js +129 -0
  89. package/dist/policy/index.d.ts +47 -0
  90. package/dist/policy/index.js +154 -0
  91. package/dist/precog/index.d.ts +96 -0
  92. package/dist/precog/index.js +167 -0
  93. package/dist/precog/intervention.d.ts +22 -0
  94. package/dist/precog/intervention.js +44 -0
  95. package/dist/precog/normal.d.ts +31 -0
  96. package/dist/precog/normal.js +89 -0
  97. package/dist/preflight/index.d.ts +11 -0
  98. package/dist/preflight/index.js +19 -0
  99. package/dist/ratings/index.d.ts +21 -0
  100. package/dist/ratings/index.js +48 -0
  101. package/dist/reconcile/claude-code.d.ts +19 -0
  102. package/dist/reconcile/claude-code.js +220 -0
  103. package/dist/reconcile/codex.d.ts +5 -0
  104. package/dist/reconcile/codex.js +191 -0
  105. package/dist/reconcile/index.d.ts +19 -0
  106. package/dist/reconcile/index.js +50 -0
  107. package/dist/reconcile/record.d.ts +49 -0
  108. package/dist/reconcile/record.js +225 -0
  109. package/dist/reconcile/shared.d.ts +65 -0
  110. package/dist/reconcile/shared.js +113 -0
  111. package/dist/replay/index.d.ts +11 -0
  112. package/dist/replay/index.js +64 -0
  113. package/dist/replay/repair.d.ts +10 -0
  114. package/dist/replay/repair.js +62 -0
  115. package/dist/session/drain.d.ts +13 -0
  116. package/dist/session/drain.js +35 -0
  117. package/dist/session/index.d.ts +275 -0
  118. package/dist/session/index.js +681 -0
  119. package/dist/undo/index.d.ts +45 -0
  120. package/dist/undo/index.js +212 -0
  121. package/dist/verify/anchor.d.ts +54 -0
  122. package/dist/verify/anchor.js +77 -0
  123. package/dist/verify/chain.d.ts +27 -0
  124. package/dist/verify/chain.js +105 -0
  125. package/dist/verify/envelope.d.ts +28 -0
  126. package/dist/verify/envelope.js +55 -0
  127. package/dist/verify/index.d.ts +3 -0
  128. package/dist/verify/index.js +3 -0
  129. package/dist/version.d.ts +1 -0
  130. package/dist/version.js +2 -0
  131. package/dist/weather/index.d.ts +24 -0
  132. package/dist/weather/index.js +45 -0
  133. package/dist/workspace-receipt/index.d.ts +15 -0
  134. package/dist/workspace-receipt/index.js +121 -0
  135. package/package.json +56 -3
@@ -0,0 +1,45 @@
1
+ export interface Compensation {
2
+ tool: string;
3
+ undo: {
4
+ tool: string;
5
+ args?: Record<string, unknown>;
6
+ } | null;
7
+ }
8
+ export type UndoStatus = "ready" | "irreversible" | "manual" | "undone";
9
+ export interface UndoStep {
10
+ seq: number;
11
+ tool: string;
12
+ via: string;
13
+ undo: {
14
+ tool: string;
15
+ args: Record<string, unknown>;
16
+ } | null;
17
+ status: UndoStatus;
18
+ reason?: string;
19
+ }
20
+ export interface UndoFinding {
21
+ code: "UNDO_FAILED";
22
+ source: "undo";
23
+ severity: "warning";
24
+ ref: {
25
+ seq: number;
26
+ undo_of: number;
27
+ };
28
+ }
29
+ type Bodies = (hash: string) => Uint8Array | undefined;
30
+ /** spec/undo.md §2: newest first, the order to reverse them in. */
31
+ export declare function undoPlan(lines: readonly string[], bodies: Bodies, compensations: readonly Compensation[]): UndoStep[];
32
+ /** spec/undo.md §3: an undo that the gateway ran and that failed. */
33
+ export declare function undoFindings(lines: readonly string[], bodies: Bodies): UndoFinding[];
34
+ /** Checks a policy file's `compensations` (spec/undo.md §1); throws on anything invalid. */
35
+ export declare function checkCompensations(value: unknown): Compensation[];
36
+ /** spec/undo.md §3: runs the `ready` steps that are the agent's own tools (`via: "sdk"`), newest
37
+ * first, each with the handler named after its undo tool; each is recorded with `undo_of`. Steps
38
+ * it can't run come back unchanged. Never throws: a failed handler is recorded and reported. */
39
+ export declare function runUndo(steps: readonly UndoStep[], handlers: Record<string, (args: Record<string, unknown>) => unknown>, session: {
40
+ event(type: string, name?: string, data?: Record<string, unknown>): void;
41
+ }): Promise<Array<UndoStep & {
42
+ outcome?: "done" | "failed";
43
+ error?: string;
44
+ }>>;
45
+ export {};
@@ -0,0 +1,212 @@
1
+ // Undo (spec/undo.md): the completed risky actions of a session, newest first, each with the call
2
+ // that reverses it. Pure; mirrors sdks/python/src/zanii_blackbox/undo.py; pinned by
3
+ // spec/vectors/undo.json.
4
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+ const glob = (pattern) => new RegExp(`^${pattern.split("*").map(escapeRe).join(".*")}$`);
6
+ const REDACTED = /\[REDACTED:[a-z_]+\]/;
7
+ function bodyJson(bodies, hash) {
8
+ const bytes = bodies(hash);
9
+ if (!bytes)
10
+ return undefined;
11
+ const text = new TextDecoder().decode(bytes);
12
+ try {
13
+ return JSON.parse(text);
14
+ }
15
+ catch {
16
+ // an SSE-framed message: the first data line that parses
17
+ for (const line of text.split("\n"))
18
+ if (line.startsWith("data:"))
19
+ try {
20
+ return JSON.parse(line.slice(5).trim());
21
+ }
22
+ catch { }
23
+ return undefined;
24
+ }
25
+ }
26
+ /** `$args.a.0.b` / `$result.x` against the original call; undefined when the path isn't there. */
27
+ function resolve(value, args, result) {
28
+ if (typeof value !== "string")
29
+ return value;
30
+ const m = /^\$(args|result)\.(.+)$/.exec(value);
31
+ if (!m)
32
+ return value;
33
+ let at = m[1] === "args" ? args : result;
34
+ for (const key of m[2].split(".")) {
35
+ if (at === null || typeof at !== "object")
36
+ return undefined;
37
+ at = at[Array.isArray(at) ? Number(key) : key];
38
+ }
39
+ return at;
40
+ }
41
+ /** Completed, successful tool calls (not policy-denied, not undos), and the seqs already undone. */
42
+ function callsOf(lines, bodies) {
43
+ const events = lines.map((l) => JSON.parse(l));
44
+ const calls = [];
45
+ const undone = new Set();
46
+ const results = new Map();
47
+ for (const e of events)
48
+ if (e.kind === "tool.result" && typeof e.meta.call_seq === "number")
49
+ results.set(e.meta.call_seq, e);
50
+ const sdkResults = events.filter((e) => e.kind === "sdk.event" && e.meta.type === "tool.result");
51
+ const usedResults = new Set();
52
+ for (const e of events) {
53
+ if (e.kind === "tool.call" &&
54
+ e.meta.method === "tools/call" &&
55
+ typeof e.meta.tool === "string") {
56
+ // A denied call never ran; a held one that was approved did (spec/approvals.md).
57
+ if (e.meta.policy?.action === "deny")
58
+ continue;
59
+ const r = results.get(e.seq);
60
+ const res = r
61
+ ? bodyJson(bodies, r.body_hash)
62
+ : undefined;
63
+ const succeeded = !!res && res.error === undefined && res.result?.isError !== true;
64
+ if (typeof e.meta.undo_of === "number") {
65
+ if (succeeded)
66
+ undone.add(e.meta.undo_of); // a failed undo leaves the step to retry
67
+ continue;
68
+ }
69
+ if (!res || !succeeded)
70
+ continue;
71
+ const req = bodyJson(bodies, e.body_hash);
72
+ const server = String(e.meta.server);
73
+ calls.push({
74
+ seq: e.seq,
75
+ tool: `mcp__${server}__${e.meta.tool}`,
76
+ names: [`mcp__${server}__${e.meta.tool}`, e.meta.tool],
77
+ via: `mcp:${server}`,
78
+ args: req?.params?.arguments ?? {},
79
+ result: res.result,
80
+ });
81
+ }
82
+ else if (e.kind === "sdk.event" &&
83
+ e.meta.type === "tool.call" &&
84
+ typeof e.meta.name === "string") {
85
+ const data = bodyJson(bodies, e.body_hash);
86
+ const r = sdkResults.find((x) => x.seq > e.seq && x.meta.name === e.meta.name && !usedResults.has(x.seq));
87
+ if (r)
88
+ usedResults.add(r.seq);
89
+ const res = r ? bodyJson(bodies, r.body_hash) : undefined;
90
+ if (typeof data?.undo_of === "number") {
91
+ if (r && res?.ok !== false)
92
+ undone.add(data.undo_of); // a failed undo leaves it to retry
93
+ continue;
94
+ }
95
+ if (!r || res?.ok === false)
96
+ continue;
97
+ calls.push({
98
+ seq: e.seq,
99
+ tool: e.meta.name,
100
+ names: [e.meta.name],
101
+ via: "sdk",
102
+ args: data?.args ?? {},
103
+ result: res ?? {},
104
+ });
105
+ }
106
+ }
107
+ return { calls, undone };
108
+ }
109
+ /** spec/undo.md §2: newest first, the order to reverse them in. */
110
+ export function undoPlan(lines, bodies, compensations) {
111
+ const matchers = compensations.map((c) => ({ c, re: glob(c.tool) }));
112
+ const { calls, undone } = callsOf(lines, bodies);
113
+ const steps = [];
114
+ for (const call of calls) {
115
+ const hit = matchers.find((m) => call.names.some((n) => m.re.test(n)));
116
+ if (!hit)
117
+ continue;
118
+ const base = { seq: call.seq, tool: call.tool, via: call.via };
119
+ if (hit.c.undo === null) {
120
+ steps.push({ ...base, undo: null, status: undone.has(call.seq) ? "undone" : "irreversible" });
121
+ continue;
122
+ }
123
+ const args = {};
124
+ let reason;
125
+ for (const [k, v] of Object.entries(hit.c.undo.args ?? {})) {
126
+ const value = resolve(v, call.args, call.result);
127
+ if (value === undefined)
128
+ reason ??= `${String(v)} isn't in the record`;
129
+ else if (REDACTED.test(JSON.stringify(value)))
130
+ reason ??= `${k} was redacted in the record`;
131
+ else
132
+ args[k] = value;
133
+ }
134
+ const undo = { tool: hit.c.undo.tool, args };
135
+ if (undone.has(call.seq))
136
+ steps.push({ ...base, undo, status: "undone" });
137
+ else if (reason)
138
+ steps.push({ ...base, undo, status: "manual", reason });
139
+ else
140
+ steps.push({ ...base, undo, status: "ready" });
141
+ }
142
+ return steps.sort((a, b) => b.seq - a.seq);
143
+ }
144
+ /** spec/undo.md §3: an undo that the gateway ran and that failed. */
145
+ export function undoFindings(lines, bodies) {
146
+ const out = [];
147
+ for (const line of lines) {
148
+ const e = JSON.parse(line);
149
+ if (e.kind !== "tool.result" || typeof e.meta.undo_of !== "number")
150
+ continue;
151
+ const res = bodyJson(bodies, e.body_hash);
152
+ if (!res || res.error !== undefined || res.result?.isError === true)
153
+ out.push({
154
+ code: "UNDO_FAILED",
155
+ source: "undo",
156
+ severity: "warning",
157
+ ref: { seq: e.seq, undo_of: e.meta.undo_of },
158
+ });
159
+ }
160
+ return out;
161
+ }
162
+ /** Checks a policy file's `compensations` (spec/undo.md §1); throws on anything invalid. */
163
+ export function checkCompensations(value) {
164
+ if (value === undefined)
165
+ return [];
166
+ if (!Array.isArray(value))
167
+ throw new Error("policy: compensations must be a list");
168
+ for (const [i, c] of value.entries()) {
169
+ const x = c;
170
+ if (typeof x.tool !== "string" || x.tool === "")
171
+ throw new Error(`policy: compensations[${i}].tool is required`);
172
+ if (x.undo === null)
173
+ continue;
174
+ const u = x.undo;
175
+ if (!u || typeof u !== "object" || typeof u.tool !== "string" || u.tool === "")
176
+ throw new Error(`policy: compensations[${i}].undo must be null or {tool, args?}`);
177
+ if (u.args !== undefined &&
178
+ (typeof u.args !== "object" || u.args === null || Array.isArray(u.args)))
179
+ throw new Error(`policy: compensations[${i}].undo.args must be an object`);
180
+ }
181
+ return value;
182
+ }
183
+ /** spec/undo.md §3: runs the `ready` steps that are the agent's own tools (`via: "sdk"`), newest
184
+ * first, each with the handler named after its undo tool; each is recorded with `undo_of`. Steps
185
+ * it can't run come back unchanged. Never throws: a failed handler is recorded and reported. */
186
+ export async function runUndo(steps, handlers, session) {
187
+ const out = [];
188
+ for (const step of steps) {
189
+ const handler = step.undo ? handlers[step.undo.tool] : undefined;
190
+ if (step.status !== "ready" || step.via !== "sdk" || !step.undo || !handler) {
191
+ out.push(step);
192
+ continue;
193
+ }
194
+ const { tool, args } = step.undo;
195
+ session.event("tool.call", tool, { args, undo_of: step.seq });
196
+ try {
197
+ await handler(args);
198
+ session.event("tool.result", tool, { ok: true, undo_of: step.seq });
199
+ out.push({ ...step, outcome: "done" });
200
+ }
201
+ catch (error) {
202
+ const text = error instanceof Error ? error.message : String(error);
203
+ session.event("tool.result", tool, {
204
+ ok: false,
205
+ error: text.slice(0, 500),
206
+ undo_of: step.seq,
207
+ });
208
+ out.push({ ...step, outcome: "failed", error: text });
209
+ }
210
+ }
211
+ return out;
212
+ }
@@ -0,0 +1,54 @@
1
+ import { fetchAndVerifyProof } from "@zanii/sdk";
2
+ import { type VerifyResult } from "./chain.ts";
3
+ export declare const ANCHOR_TARGET = "blackbox.anchor";
4
+ /** The anchor payload text A: canonical JSON of {count, head, session_id} (always ASCII). */
5
+ export declare function anchorPayload(sessionId: string, count: number, head: string): string;
6
+ /**
7
+ * The bytes the Zanii SDKs hash when A is recorded as a string payload: its JSON string literal.
8
+ * (Object payloads serialise differently in the TypeScript and Python SDKs; a string does not.)
9
+ */
10
+ export declare function anchorPayloadBytes(sessionId: string, count: number, head: string): string;
11
+ export interface BundleAnchor {
12
+ count: number;
13
+ head: string;
14
+ receipt_hash: string;
15
+ nonce: string;
16
+ anchored_at: string;
17
+ }
18
+ export interface Bundle {
19
+ v: 1;
20
+ session: {
21
+ session_id: string;
22
+ final_count: number | null;
23
+ [key: string]: unknown;
24
+ };
25
+ ledger_url: string | null;
26
+ anchored_by: string | null;
27
+ lines: string[];
28
+ anchors: BundleAnchor[];
29
+ }
30
+ export interface BundleReport {
31
+ ok: boolean;
32
+ chain: VerifyResult;
33
+ /** anchored_by matches the identity pinned in session.open. */
34
+ identity: {
35
+ ok: boolean;
36
+ message?: string;
37
+ };
38
+ /** One entry per anchor when proofs were checked; null when they weren't. */
39
+ proofs: Array<{
40
+ receipt_hash: string;
41
+ ok: boolean;
42
+ problems: string[];
43
+ }> | null;
44
+ }
45
+ /**
46
+ * Verifies an audit bundle: the chain against its anchors and final count, the pinned identity, and,
47
+ * with `proofs: true`, every anchor's receipt on the ledger (network).
48
+ */
49
+ export declare function verifyBundle(bundle: Bundle, options?: {
50
+ proofs?: boolean;
51
+ fetchImpl?: Parameters<typeof fetchAndVerifyProof>[2];
52
+ /** Audit K3: looks up a body by hash (a bodies folder); the bytes are checked against it. */
53
+ body?: (bodyHash: string) => Uint8Array | undefined;
54
+ }): Promise<BundleReport>;
@@ -0,0 +1,77 @@
1
+ // Anchors and audit bundles (spec/anchoring.md). Mirrors sdks/python/src/zanii_blackbox/verify/anchor.py.
2
+ import { canonicalBytes } from "@zanii/core";
3
+ import { fetchAndVerifyProof, verifyPayload } from "@zanii/sdk";
4
+ import { verifyChain } from "./chain.js";
5
+ export const ANCHOR_TARGET = "blackbox.anchor";
6
+ /** The anchor payload text A: canonical JSON of {count, head, session_id} (always ASCII). */
7
+ export function anchorPayload(sessionId, count, head) {
8
+ return new TextDecoder().decode(canonicalBytes({ count, head, session_id: sessionId }));
9
+ }
10
+ /**
11
+ * The bytes the Zanii SDKs hash when A is recorded as a string payload: its JSON string literal.
12
+ * (Object payloads serialise differently in the TypeScript and Python SDKs; a string does not.)
13
+ */
14
+ export function anchorPayloadBytes(sessionId, count, head) {
15
+ return JSON.stringify(anchorPayload(sessionId, count, head));
16
+ }
17
+ /**
18
+ * Verifies an audit bundle: the chain against its anchors and final count, the pinned identity, and,
19
+ * with `proofs: true`, every anchor's receipt on the ledger (network).
20
+ */
21
+ export async function verifyBundle(bundle, options = {}) {
22
+ const chain = verifyChain(bundle.lines, {
23
+ sessionId: bundle.session.session_id,
24
+ ...(options.body ? { body: options.body } : {}),
25
+ anchors: bundle.anchors,
26
+ ...(bundle.session.final_count === null ? {} : { count: bundle.session.final_count }),
27
+ });
28
+ let pinned;
29
+ try {
30
+ pinned = JSON.parse(bundle.lines[0] ?? "{}").meta
31
+ ?.anchored_by;
32
+ }
33
+ catch {
34
+ pinned = undefined;
35
+ }
36
+ const identity = bundle.anchors.length === 0 && bundle.anchored_by === null
37
+ ? { ok: true }
38
+ : pinned === bundle.anchored_by && typeof pinned === "string"
39
+ ? { ok: true }
40
+ : { ok: false, message: "anchored_by does not match the identity pinned in session.open" };
41
+ let proofs = null;
42
+ if (options.proofs) {
43
+ proofs = [];
44
+ for (const a of bundle.anchors) {
45
+ const problems = [];
46
+ if (!bundle.ledger_url)
47
+ problems.push("no ledger_url in the bundle");
48
+ else {
49
+ const proof = await fetchAndVerifyProof(bundle.ledger_url, a.receipt_hash, options.fetchImpl);
50
+ if (!proof.ok)
51
+ problems.push(`ledger proof failed: ${proof.checks
52
+ .filter((c) => !c.ok)
53
+ .map((c) => c.label)
54
+ .join(", ")}`);
55
+ const r = proof.receipt;
56
+ if (!r)
57
+ problems.push("no receipt in the proof");
58
+ else {
59
+ if (r.agent_id !== bundle.anchored_by)
60
+ problems.push("receipt was not signed by the pinned gateway identity");
61
+ if (r.target !== ANCHOR_TARGET)
62
+ problems.push(`receipt target is ${r.target}, not ${ANCHOR_TARGET}`);
63
+ if (!verifyPayload(anchorPayloadBytes(bundle.session.session_id, a.count, a.head), a.nonce, r.payload_hash)) {
64
+ problems.push("receipt payload does not commit to this (count, head)");
65
+ }
66
+ }
67
+ }
68
+ proofs.push({ receipt_hash: a.receipt_hash, ok: problems.length === 0, problems });
69
+ }
70
+ }
71
+ return {
72
+ ok: chain.ok && identity.ok && (proofs === null || proofs.every((p) => p.ok)),
73
+ chain,
74
+ identity,
75
+ proofs,
76
+ };
77
+ }
@@ -0,0 +1,27 @@
1
+ export type FailureCode = "NOT_JSON" | "NON_CANONICAL" | "BAD_NUMBER" | "SCHEMA" | "WRONG_SESSION" | "SEQ_GAP" | "BROKEN_LINK" | "BODY_MISMATCH" | "COUNT_MISMATCH" | "HEAD_MISMATCH" | "ANCHOR_TRUNCATED" | "ANCHOR_MISMATCH";
2
+ export type VerifyResult = {
3
+ ok: true;
4
+ count: number;
5
+ head: string;
6
+ } | {
7
+ ok: false;
8
+ code: FailureCode;
9
+ index: number;
10
+ message: string;
11
+ };
12
+ export interface VerifyOptions {
13
+ /** Expected session; defaults to the first line's session_id. */
14
+ sessionId?: string;
15
+ /** Looks up a body by hash. When it returns bytes, they are checked against body_hash. */
16
+ body?: (bodyHash: string) => Uint8Array | undefined;
17
+ /** Expected number of events (from session.close or an anchor). Detects truncation. */
18
+ count?: number;
19
+ /** Expected last event_hash (from an anchor). */
20
+ head?: string;
21
+ /** Published (count, head) anchors that the chain must still match (spec/anchoring.md §3). */
22
+ anchors?: ReadonlyArray<{
23
+ count: number;
24
+ head: string;
25
+ }>;
26
+ }
27
+ export declare function verifyChain(lines: readonly string[], options?: VerifyOptions): VerifyResult;
@@ -0,0 +1,105 @@
1
+ // Chain verification (spec/chain.md). Mirrors sdks/python/src/zanii_blackbox/verify/chain.py.
2
+ // Never throws on bad input: every problem is reported as a failure code.
3
+ import { canonicalBytes } from "@zanii/core";
4
+ import { EVENT_KINDS, GENESIS, hashBody, hashLine } from "./envelope.js";
5
+ const HASH = /^sha256:[0-9a-f]{64}$/;
6
+ const SESSION = /^ses_[A-Za-z0-9]{16,64}$/;
7
+ const TS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z$/;
8
+ const KINDS = new Set(EVENT_KINDS);
9
+ export function verifyChain(lines, options = {}) {
10
+ let prev = GENESIS;
11
+ const hashes = [];
12
+ let session = options.sessionId;
13
+ const fail = (code, index, message) => ({
14
+ ok: false,
15
+ code,
16
+ index,
17
+ message,
18
+ });
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ const parsed = parseStrict(line);
22
+ if (parsed.error === "NOT_JSON")
23
+ return fail("NOT_JSON", i, "line is not valid JSON");
24
+ if (parsed.error === "BAD_NUMBER")
25
+ return fail("BAD_NUMBER", i, `number ${parsed.token} is not a safe integer literal`);
26
+ const env = parsed.value;
27
+ if (new TextDecoder().decode(canonicalBytes(env)) !== line)
28
+ return fail("NON_CANONICAL", i, "line is not in canonical form");
29
+ if (env === null || typeof env !== "object" || Array.isArray(env))
30
+ return fail("SCHEMA", i, "line is not a JSON object");
31
+ const schemaError = checkSchema(env);
32
+ if (schemaError)
33
+ return fail("SCHEMA", i, schemaError);
34
+ session ??= env.session_id;
35
+ if (env.session_id !== session)
36
+ return fail("WRONG_SESSION", i, `session_id ${env.session_id} ≠ ${session}`);
37
+ if (env.seq !== i)
38
+ return fail("SEQ_GAP", i, `expected seq ${i}, found ${env.seq}`);
39
+ if (env.prev !== prev)
40
+ return fail("BROKEN_LINK", i, "prev does not match the previous event_hash");
41
+ const body = options.body?.(env.body_hash);
42
+ if (body !== undefined && hashBody(body) !== env.body_hash)
43
+ return fail("BODY_MISMATCH", i, "body bytes do not match body_hash");
44
+ prev = hashLine(line);
45
+ hashes.push(prev);
46
+ }
47
+ if (options.count !== undefined && options.count !== lines.length) {
48
+ return fail("COUNT_MISMATCH", lines.length, `expected ${options.count} events, found ${lines.length}`);
49
+ }
50
+ if (options.head !== undefined && options.head !== prev)
51
+ return fail("HEAD_MISMATCH", lines.length, "head does not match the expected head");
52
+ for (const a of options.anchors ?? []) {
53
+ if (a.count > lines.length) {
54
+ return fail("ANCHOR_TRUNCATED", lines.length, `anchored ${a.count} events, found ${lines.length}`);
55
+ }
56
+ if (a.count < 1 || hashes[a.count - 1] !== a.head) {
57
+ return fail("ANCHOR_MISMATCH", Math.max(0, a.count - 1), `event ${a.count - 1} does not match its anchor`);
58
+ }
59
+ }
60
+ return { ok: true, count: lines.length, head: prev };
61
+ }
62
+ const INTEGER_LITERAL = /^-?(0|[1-9][0-9]*)$/;
63
+ // JSON.parse only gives us the parsed value, where 1.0 and 1 look identical. The reviver's
64
+ // `context.source` (Node 22+) exposes each number token exactly as written.
65
+ function parseStrict(line) {
66
+ let badToken;
67
+ let value;
68
+ try {
69
+ value = JSON.parse(line, (_key, v, context) => {
70
+ if (typeof v === "number" && badToken === undefined) {
71
+ const source = context?.source ?? String(v);
72
+ if (!INTEGER_LITERAL.test(source) || !Number.isSafeInteger(v))
73
+ badToken = source;
74
+ }
75
+ return v;
76
+ });
77
+ }
78
+ catch {
79
+ return { error: "NOT_JSON" };
80
+ }
81
+ return badToken === undefined ? { value } : { error: "BAD_NUMBER", token: badToken };
82
+ }
83
+ function checkSchema(env) {
84
+ const allowed = new Set(["v", "session_id", "seq", "ts", "kind", "prev", "body_hash", "meta"]);
85
+ for (const key of Object.keys(env))
86
+ if (!allowed.has(key))
87
+ return `unexpected field ${key}`;
88
+ if (env.v !== 1)
89
+ return "v must be 1";
90
+ if (typeof env.session_id !== "string" || !SESSION.test(env.session_id))
91
+ return "bad session_id";
92
+ if (typeof env.seq !== "number" || env.seq < 0)
93
+ return "bad seq";
94
+ if (typeof env.ts !== "string" || !TS.test(env.ts))
95
+ return "bad ts";
96
+ if (typeof env.kind !== "string" || !KINDS.has(env.kind))
97
+ return "bad kind";
98
+ if (typeof env.prev !== "string" || (env.prev !== GENESIS && !HASH.test(env.prev)))
99
+ return "bad prev";
100
+ if (typeof env.body_hash !== "string" || !HASH.test(env.body_hash))
101
+ return "bad body_hash";
102
+ if (env.meta === null || typeof env.meta !== "object" || Array.isArray(env.meta))
103
+ return "meta must be an object";
104
+ return undefined;
105
+ }
@@ -0,0 +1,28 @@
1
+ export declare const GENESIS = "genesis";
2
+ export declare const EVENT_KINDS: readonly ["session.open", "session.close", "session.lost_contact", "llm.request", "llm.chunk", "llm.response", "llm.incomplete", "tool.call", "tool.result", "tool.notification", "workspace.receipt", "sdk.event", "control", "outcome", "finding", "capture.gap"];
3
+ export type EventKind = (typeof EVENT_KINDS)[number];
4
+ export type Json = null | boolean | number | string | Json[] | {
5
+ [key: string]: Json;
6
+ };
7
+ export interface Envelope {
8
+ v: 1;
9
+ session_id: string;
10
+ seq: number;
11
+ ts: string;
12
+ kind: EventKind;
13
+ prev: string;
14
+ body_hash: string;
15
+ meta: {
16
+ [key: string]: Json;
17
+ };
18
+ }
19
+ export declare class EncodingError extends Error {
20
+ }
21
+ /** Throws EncodingError if any number in `value` is a float or outside the safe-integer range. */
22
+ export declare function assertSafeIntegers(value: unknown, path?: string): void;
23
+ /** The stored form of an event: RFC 8785 canonical JSON. */
24
+ export declare function encodeEnvelope(envelope: Envelope): string;
25
+ /** `sha256:<hex>` of a line's UTF-8 bytes. For a canonical line this equals `jcsHash(envelope)`. */
26
+ export declare function hashLine(line: string): string;
27
+ /** `sha256:<hex>` of the exact body bytes. */
28
+ export declare function hashBody(body: Uint8Array): string;
@@ -0,0 +1,55 @@
1
+ // Event envelope encoding (spec/chain.md). Mirrors sdks/python/src/zanii_blackbox/verify/envelope.py.
2
+ import { createHash } from "node:crypto";
3
+ import { canonicalBytes } from "@zanii/core";
4
+ export const GENESIS = "genesis";
5
+ export const EVENT_KINDS = [
6
+ "session.open",
7
+ "session.close",
8
+ "session.lost_contact",
9
+ "llm.request",
10
+ "llm.chunk",
11
+ "llm.response",
12
+ "llm.incomplete",
13
+ "tool.call",
14
+ "tool.result",
15
+ "tool.notification",
16
+ "workspace.receipt",
17
+ "sdk.event",
18
+ "control",
19
+ "outcome",
20
+ "finding",
21
+ "capture.gap",
22
+ ];
23
+ export class EncodingError extends Error {
24
+ }
25
+ /** Throws EncodingError if any number in `value` is a float or outside the safe-integer range. */
26
+ export function assertSafeIntegers(value, path = "$") {
27
+ if (typeof value === "number") {
28
+ if (!Number.isSafeInteger(value))
29
+ throw new EncodingError(`${path}: ${value} is not a safe integer`);
30
+ }
31
+ else if (Array.isArray(value)) {
32
+ for (const [i, v] of value.entries())
33
+ assertSafeIntegers(v, `${path}[${i}]`);
34
+ }
35
+ else if (value !== null && typeof value === "object") {
36
+ for (const [k, v] of Object.entries(value))
37
+ assertSafeIntegers(v, `${path}.${k}`);
38
+ }
39
+ }
40
+ /** The stored form of an event: RFC 8785 canonical JSON. */
41
+ export function encodeEnvelope(envelope) {
42
+ assertSafeIntegers(envelope);
43
+ return new TextDecoder().decode(canonicalBytes(envelope));
44
+ }
45
+ /** `sha256:<hex>` of a line's UTF-8 bytes. For a canonical line this equals `jcsHash(envelope)`. */
46
+ export function hashLine(line) {
47
+ return sha256(Buffer.from(line, "utf8"));
48
+ }
49
+ /** `sha256:<hex>` of the exact body bytes. */
50
+ export function hashBody(body) {
51
+ return sha256(body);
52
+ }
53
+ function sha256(bytes) {
54
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
55
+ }
@@ -0,0 +1,3 @@
1
+ export { ANCHOR_TARGET, anchorPayload, anchorPayloadBytes, type Bundle, type BundleAnchor, type BundleReport, verifyBundle, } from "./anchor.ts";
2
+ export { type FailureCode, type VerifyOptions, type VerifyResult, verifyChain } from "./chain.ts";
3
+ export { assertSafeIntegers, EncodingError, type Envelope, EVENT_KINDS, type EventKind, encodeEnvelope, GENESIS, hashBody, hashLine, type Json, } from "./envelope.ts";
@@ -0,0 +1,3 @@
1
+ export { ANCHOR_TARGET, anchorPayload, anchorPayloadBytes, verifyBundle, } from "./anchor.js";
2
+ export { verifyChain } from "./chain.js";
3
+ export { assertSafeIntegers, EncodingError, EVENT_KINDS, encodeEnvelope, GENESIS, hashBody, hashLine, } from "./envelope.js";
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.2.0";
@@ -0,0 +1,2 @@
1
+ // The SDK version, on its own so the CLI can print it without loading the whole SDK.
2
+ export const VERSION = "0.2.0";
@@ -0,0 +1,24 @@
1
+ export interface Sample {
2
+ at_ms: number;
3
+ provider: string;
4
+ model: string | null;
5
+ ok: boolean;
6
+ ms: number;
7
+ }
8
+ export interface Window {
9
+ calls: number;
10
+ errors: number;
11
+ error_ppm: number;
12
+ p95_ms: number;
13
+ }
14
+ export interface Conditions {
15
+ provider: string;
16
+ model: string | null;
17
+ last_5m: Window;
18
+ last_15m: Window;
19
+ status: "clear" | "degraded" | "down";
20
+ }
21
+ /** spec/weather.md §2. */
22
+ export declare function weather(samples: readonly Sample[], nowMs: number, { slowMs }?: {
23
+ slowMs?: number;
24
+ }): Conditions[];