@zanii/blackbox 0.16.0 → 0.17.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.
@@ -232,6 +232,17 @@ export const FAULTS = {
232
232
  en: "A tool's definition changed mid-session",
233
233
  ar: "تغيّر تعريف أداة أثناء الجلسة",
234
234
  },
235
+ PROMPT_INJECTION: {
236
+ fault: "BBX-7403",
237
+ squawk: "HIJACK",
238
+ en: "Prompt injection reached the model",
239
+ ar: "وصلت محاولة حقن تعليمات إلى النموذج",
240
+ },
241
+ TOOL_DRIFTED: {
242
+ fault: "BBX-7404",
243
+ en: "A tool's definition differs from earlier sessions",
244
+ ar: "يختلف تعريف أداة عمّا رأته الجلسات السابقة",
245
+ },
235
246
  REVOKED_MEMORY_READ: {
236
247
  fault: "BBX-7101",
237
248
  en: "A revoked memory was used",
@@ -45,6 +45,12 @@ export declare class Evidence {
45
45
  private pairSums;
46
46
  }
47
47
  /** The final answers on a record, each with the evidence it should rest on. */
48
+ /**
49
+ * spec/findings.md §9: what the answer's own web search returned (Anthropic's server tool): each
50
+ * result's title, and each citation's quoted text and title. The provider supplies these from the
51
+ * pages, not the model, so a fact they hold is backed. Never the answer's own words.
52
+ */
53
+ export declare function webEvidence(c: Call): string;
48
54
  export declare function answersOf(lines: readonly string[], bodies: Bodies): Array<{
49
55
  seq: number;
50
56
  text: string;
@@ -460,6 +460,30 @@ export class Evidence {
460
460
  }
461
461
  }
462
462
  /** The final answers on a record, each with the evidence it should rest on. */
463
+ /**
464
+ * spec/findings.md §9: what the answer's own web search returned (Anthropic's server tool): each
465
+ * result's title, and each citation's quoted text and title. The provider supplies these from the
466
+ * pages, not the model, so a fact they hold is backed. Never the answer's own words.
467
+ */
468
+ export function webEvidence(c) {
469
+ const parts = [];
470
+ const add = (v) => {
471
+ if (typeof v === "string" && v)
472
+ parts.push(v);
473
+ };
474
+ for (const [, b] of [...c.blocks].sort(([x], [y]) => x - y)) {
475
+ const block = b;
476
+ if (block.type === "web_search_tool_result" && Array.isArray(block.content))
477
+ for (const r of block.content)
478
+ add(r?.title);
479
+ if (Array.isArray(block.citations))
480
+ for (const cit of block.citations) {
481
+ add(cit?.cited_text);
482
+ add(cit?.title);
483
+ }
484
+ }
485
+ return parts.join("\n");
486
+ }
463
487
  export function answersOf(lines, bodies) {
464
488
  const calls = callsOf(lines, bodies);
465
489
  const ends = new Set(toolCallsOf(calls).map((t) => t.seq));
@@ -480,9 +504,11 @@ export function answersOf(lines, bodies) {
480
504
  const text = answerText(c);
481
505
  if (!text)
482
506
  continue;
507
+ const web = webEvidence(c);
483
508
  const pieces = [
484
509
  { seq: c.requestSeq, text: canonical(c.request) },
485
510
  ...results.filter((r) => r.seq < c.endSeq),
511
+ ...(web ? [{ seq: c.endSeq, text: web }] : []),
486
512
  ];
487
513
  out.push({
488
514
  seq: c.endSeq,
@@ -82,6 +82,12 @@ export declare function exitDurability(events: Ev[]): Finding[];
82
82
  * is the reference; a different one later is a finding, and becomes the new reference.
83
83
  */
84
84
  export declare function toolChanged(events: Ev[]): Finding[];
85
+ /**
86
+ * spec/findings.md §1a: a tool's definition differs from the one this tenant's earlier sessions saw
87
+ * (a rug pull between sessions). The gateway puts the previous hashes next to a `tools/list` answer
88
+ * (`meta.tool_defs_prev`); a tool listed with another hash is a finding, once per server and tool.
89
+ */
90
+ export declare function toolDrifted(events: Ev[]): Finding[];
85
91
  /** N6 (idea S3): the gateway refused a request that carried the session's own token. */
86
92
  export declare function tokenMisuse(events: Ev[]): Finding[];
87
93
  /** Stage 2 R3: a `verify` check that passed, then failed later in the run. */
@@ -14,6 +14,7 @@ import { wallFindings } from "../walls/index.js";
14
14
  import { detectors } from "./detectors.js";
15
15
  import { groundingFindings, judgeFindings } from "./grounding.js";
16
16
  import { toolHallucinations } from "./hallucination.js";
17
+ import { injectionFindings } from "./injection.js";
17
18
  import { flightPlan } from "./landing.js";
18
19
  import { memoryFindings } from "./memory.js";
19
20
  export { detectorAccuracy, wilsonLow } from "./accuracy.js";
@@ -56,6 +57,8 @@ export function analyze(lines, options) {
56
57
  ...orphanedToolCalls(events, now),
57
58
  ...tokenMisuse(events),
58
59
  ...toolChanged(events),
60
+ ...toolDrifted(events),
61
+ ...injectionFindings(lines, options.bodies),
59
62
  ...verifyRegression(events),
60
63
  ...falseSuccess(events, now),
61
64
  ...toolHallucinations(lines, options.bodies),
@@ -401,6 +404,42 @@ export function toolChanged(events) {
401
404
  }
402
405
  return out;
403
406
  }
407
+ /**
408
+ * spec/findings.md §1a: a tool's definition differs from the one this tenant's earlier sessions saw
409
+ * (a rug pull between sessions). The gateway puts the previous hashes next to a `tools/list` answer
410
+ * (`meta.tool_defs_prev`); a tool listed with another hash is a finding, once per server and tool.
411
+ */
412
+ export function toolDrifted(events) {
413
+ const out = [];
414
+ const done = new Set();
415
+ for (const e of events) {
416
+ const { server, tool_defs: defs, tool_defs_prev: prev } = e.meta;
417
+ if (e.kind !== "tool.result" || typeof server !== "string")
418
+ continue;
419
+ if (!defs || typeof defs !== "object" || Array.isArray(defs))
420
+ continue;
421
+ if (!prev || typeof prev !== "object" || Array.isArray(prev))
422
+ continue;
423
+ for (const tool of Object.keys(defs).sort()) {
424
+ const after = defs[tool];
425
+ const before = prev[tool];
426
+ const key = `${server}\u0000${tool}`;
427
+ if (typeof after !== "string" || typeof before !== "string" || before === after)
428
+ continue;
429
+ if (done.has(key))
430
+ continue;
431
+ done.add(key);
432
+ out.push({
433
+ code: "TOOL_DRIFTED",
434
+ source: "detectors",
435
+ severity: "warning",
436
+ ref: { seq: e.seq, server, tool, before, after },
437
+ detail: "A tool's description, schema or annotations differ from what this tenant's earlier sessions saw: the tool was changed between runs.",
438
+ });
439
+ }
440
+ }
441
+ return out;
442
+ }
404
443
  /** N6 (idea S3): the gateway refused a request that carried the session's own token. */
405
444
  export function tokenMisuse(events) {
406
445
  return events
@@ -0,0 +1,22 @@
1
+ /** The rules, in the order they're tried. Patterns run on ASCII-lowercased, space-folded text. */
2
+ export declare const INJECTION_RULES: ReadonlyArray<{
3
+ id: string;
4
+ re: RegExp;
5
+ }>;
6
+ /** Text no reader sees: Unicode tag characters, bidi overrides, a run of 4+ zero-width characters. */
7
+ export declare function hiddenText(text: string): boolean;
8
+ /** The first rule a text breaks, or null. */
9
+ export declare function injectionRule(text: string): string | null;
10
+ /** spec/findings.md §1b: PROMPT_INJECTION findings over a record. */
11
+ export declare function injectionFindings(lines: readonly string[], bodies: (hash: string) => Uint8Array | undefined): {
12
+ code: string;
13
+ source: string;
14
+ severity: "warning";
15
+ ref: {
16
+ seq: number;
17
+ source: string;
18
+ rule: string;
19
+ tool?: string;
20
+ };
21
+ detail: string;
22
+ }[];
@@ -0,0 +1,173 @@
1
+ // spec/findings.md §1b: prompt injection (PROMPT_INJECTION, squawk HIJACK). What reaches the model
2
+ // from outside the agent's instructions is scanned: each MCP tool result, and the newest turn of
3
+ // each model request (the tool results and the person's words it carries). Instruction overrides,
4
+ // role hijacks, exfiltration asks and hidden text, in English and Arabic. One finding per event and
5
+ // source; the finding names the rule, never the text. Mirrors analysis/injection.py; pinned by
6
+ // spec/vectors/injection.json.
7
+ const decoder = new TextDecoder();
8
+ const MAX_TEXT = 200_000; // characters scanned per event: an injection is near the start or not worth the cost
9
+ /** The rules, in the order they're tried. Patterns run on ASCII-lowercased, space-folded text. */
10
+ export const INJECTION_RULES = [
11
+ {
12
+ id: "override",
13
+ re: /\b(ignore|disregard|forget|override|bypass)( all| any| the| your| these| those| my)* (previous|prior|above|earlier|preceding|system|original|safety)( \w+)? (instructions|prompts?|rules|directions|guidelines|messages|policies)\b/,
14
+ },
15
+ { id: "override", re: /(تجاهل|انس|انسى|تخط) (جميع |كل )?(التعليمات|الأوامر|القواعد|ما سبق)/ },
16
+ {
17
+ id: "role",
18
+ re: /(\byou are now\b|\bfrom now on,? you are\b|\bact as\b) (dan|an? unrestricted|a jailbroken|in developer mode)\b|<\|im_start\|>|<\|system\|>|\[system\]|### (system|new instructions)|\bnew instructions:/,
19
+ },
20
+ {
21
+ id: "exfiltrate",
22
+ re: /\b(send|post|upload|forward|email|exfiltrate|leak|paste)\b.{0,40}\b(system prompt|api keys?|passwords?|credentials|secrets?|conversation history|chat history|access tokens?|private keys?)\b/,
23
+ },
24
+ {
25
+ id: "exfiltrate",
26
+ re: /\b(reveal|print|repeat|show|output) (me )?(your|the) (system prompt|hidden instructions|initial instructions|original instructions)\b/,
27
+ },
28
+ {
29
+ id: "exfiltrate",
30
+ re: /(أرسل|ارسل|أفصح عن|اكشف) .{0,40}(كلمة المرور|كلمات المرور|مفتاح|بيانات الدخول|التعليمات المخفية)/,
31
+ },
32
+ ];
33
+ /** Text no reader sees: Unicode tag characters, bidi overrides, a run of 4+ zero-width characters. */
34
+ export function hiddenText(text) {
35
+ let zw = 0;
36
+ for (const ch of text) {
37
+ const c = ch.codePointAt(0);
38
+ if (c >= 0xe0000 && c <= 0xe007f)
39
+ return true;
40
+ if ((c >= 0x202a && c <= 0x202e) || (c >= 0x2066 && c <= 0x2069))
41
+ return true;
42
+ if (c === 0x200b || c === 0x200c || c === 0x200d || c === 0x2060 || c === 0xfeff) {
43
+ zw += 1;
44
+ if (zw >= 4)
45
+ return true;
46
+ }
47
+ else
48
+ zw = 0;
49
+ }
50
+ return false;
51
+ }
52
+ /** ASCII lowercase; spaces, tabs, newlines and line separators to one space: the same in every language. */
53
+ const fold = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase()).replace(/[ \t\r\n\u2028\u2029]+/g, " ");
54
+ /** The first rule a text breaks, or null. */
55
+ export function injectionRule(text) {
56
+ const t = Array.from(text).slice(0, MAX_TEXT).join("");
57
+ if (hiddenText(t))
58
+ return "hidden";
59
+ const f = fold(t);
60
+ for (const r of INJECTION_RULES)
61
+ if (r.re.test(f))
62
+ return r.id;
63
+ return null;
64
+ }
65
+ /** Every string in a JSON value, depth first, in key order. */
66
+ function strings(v, out) {
67
+ if (typeof v === "string")
68
+ out.push(v);
69
+ else if (Array.isArray(v))
70
+ for (const x of v)
71
+ strings(x, out);
72
+ else if (v && typeof v === "object")
73
+ for (const x of Object.values(v))
74
+ strings(x, out);
75
+ }
76
+ const textOf = (v) => {
77
+ const out = [];
78
+ strings(v, out);
79
+ return out.join("\n");
80
+ };
81
+ /** The newest turn of a model request: what it adds to the conversation. */
82
+ function newestTurn(body) {
83
+ const out = [];
84
+ const messages = body.messages;
85
+ if (Array.isArray(messages) && messages.length) {
86
+ // Chat Completions: the trailing tool messages, and the last message itself
87
+ let i = messages.length - 1;
88
+ while (i > 0 && messages[i].role === "tool")
89
+ i -= 1;
90
+ for (const m of messages.slice(i)) {
91
+ if (m.role === "tool")
92
+ out.push({ source: "tool_result", text: textOf(m.content) });
93
+ else if (m.role === "user") {
94
+ // Anthropic: a user turn holds the tool results and the person's words
95
+ const blocks = Array.isArray(m.content) ? m.content : [m.content];
96
+ const results = blocks.filter((b) => b?.type === "tool_result");
97
+ const words = blocks.filter((b) => b?.type !== "tool_result");
98
+ if (results.length)
99
+ out.push({ source: "tool_result", text: textOf(results) });
100
+ if (words.length)
101
+ out.push({ source: "user", text: textOf(words) });
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ const input = body.input;
107
+ if (typeof input === "string")
108
+ return [{ source: "user", text: input }];
109
+ if (Array.isArray(input) && input.length) {
110
+ // Responses: the trailing function_call_output items, then the last item
111
+ let i = input.length - 1;
112
+ while (i > 0 && input[i].type === "function_call_output")
113
+ i -= 1;
114
+ for (const item of input.slice(i)) {
115
+ if (item.type === "function_call_output")
116
+ out.push({ source: "tool_result", text: textOf(item.output) });
117
+ else if (item.role === "user")
118
+ out.push({ source: "user", text: textOf(item.content) });
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ /** spec/findings.md §1b: PROMPT_INJECTION findings over a record. */
124
+ export function injectionFindings(lines, bodies) {
125
+ const out = [];
126
+ const add = (seq, source, rule, tool) => out.push({
127
+ code: "PROMPT_INJECTION",
128
+ source: "detectors",
129
+ severity: "warning",
130
+ ref: { seq, source, rule, ...(tool !== undefined ? { tool } : {}) },
131
+ detail: rule === "hidden"
132
+ ? "Text the reader can't see (hidden characters) reached the model: a classic way to smuggle instructions."
133
+ : "Text that reached the model tries to give it new instructions: an override, a role change, or a request to send out secrets.",
134
+ });
135
+ for (const l of lines) {
136
+ const e = JSON.parse(l);
137
+ if (e.kind !== "tool.result" && e.kind !== "llm.request")
138
+ continue;
139
+ const raw = bodies(e.body_hash);
140
+ if (!raw || raw.length === 0)
141
+ continue;
142
+ let doc;
143
+ try {
144
+ doc = JSON.parse(decoder.decode(raw));
145
+ }
146
+ catch {
147
+ continue;
148
+ }
149
+ if (e.kind === "tool.result") {
150
+ const result = doc?.result;
151
+ if (result === undefined)
152
+ continue;
153
+ const rule = injectionRule(textOf(result));
154
+ const tool = typeof e.meta.tool === "string" ? e.meta.tool : undefined;
155
+ if (rule)
156
+ add(e.seq, "tool_result", rule, tool);
157
+ continue;
158
+ }
159
+ if (!doc || typeof doc !== "object" || Array.isArray(doc))
160
+ continue;
161
+ const seen = new Set();
162
+ for (const part of newestTurn(doc)) {
163
+ if (seen.has(part.source))
164
+ continue;
165
+ const rule = injectionRule(part.text);
166
+ if (rule) {
167
+ seen.add(part.source);
168
+ add(e.seq, part.source, rule);
169
+ }
170
+ }
171
+ }
172
+ return out;
173
+ }
@@ -18,6 +18,7 @@ export const MITRE_ATLAS = {
18
18
  "AML.T0034.000": "Cost Harvesting: Excessive Queries",
19
19
  "AML.T0034.001": "Cost Harvesting: Resource-Intensive Queries",
20
20
  "AML.T0034.002": "Cost Harvesting: Agentic Resource Consumption",
21
+ "AML.T0051": "LLM Prompt Injection",
21
22
  "AML.T0053": "AI Agent Tool Invocation",
22
23
  "AML.T0057": "LLM Data Leakage",
23
24
  "AML.T0080.000": "AI Agent Context Poisoning: Memory",
@@ -53,6 +54,8 @@ export const TAXONOMY = {
53
54
  STERILE_VIOLATION: { owasp: ["ASI02", "ASI10"], atlas: ["AML.T0053"] },
54
55
  TOKEN_MISUSE: { owasp: ["ASI03"], atlas: ["AML.T0091.000", "AML.T0086"] },
55
56
  TOOL_CHANGED: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
57
+ TOOL_DRIFTED: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
58
+ PROMPT_INJECTION: { owasp: ["ASI01"], atlas: ["AML.T0051"] },
56
59
  TOOL_OUTPUT_MISMATCH: { owasp: ["ASI04", "ASI06"], atlas: ["AML.T0110.002", "AML.T0099"] },
57
60
  TOOL_WITNESS_INVALID: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
58
61
  };
@@ -64,8 +64,11 @@ export declare function insights(summaries: readonly SummaryIn[], window: {
64
64
  from: string;
65
65
  to: string;
66
66
  }): Insights;
67
- /** spec/insights.md §3: the record as one line per event, metadata only, at most 600 events. */
68
- export declare function copilotContext(lines: readonly string[]): string;
67
+ /**
68
+ * spec/insights.md §3: the record as one line per event, metadata only, at most 600 events. With
69
+ * `body` (a body by its hash), a verify event also says `ok=true` or `ok=false`.
70
+ */
71
+ export declare function copilotContext(lines: readonly string[], body?: (hash: string) => Uint8Array | null | undefined): string;
69
72
  /** spec/insights.md §3: the system prompt and the user turn for the investigator's model. */
70
73
  export declare function copilotPrompt(question: string, context: string, lang?: string): {
71
74
  system: string;
@@ -195,8 +195,28 @@ function evenly(items, n) {
195
195
  return [...items];
196
196
  return Array.from({ length: n }, (_, i) => items[Math.floor((i * items.length) / n)]);
197
197
  }
198
- /** spec/insights.md §3: the record as one line per event, metadata only, at most 600 events. */
199
- export function copilotContext(lines) {
198
+ /** A verify event's pass or fail, from its body: the one value read from a body. Never `detail`. */
199
+ function verifyOk(e, body) {
200
+ const meta = (e.meta ?? {});
201
+ if (!body || e.kind !== "sdk.event" || meta.type !== "verify")
202
+ return null;
203
+ const raw = body(String(e.body_hash));
204
+ if (!raw || raw.length === 0)
205
+ return null; // erased, or never sent
206
+ try {
207
+ const doc = JSON.parse(new TextDecoder().decode(raw));
208
+ const ok = doc && typeof doc === "object" && !Array.isArray(doc) ? doc.ok : null;
209
+ return typeof ok === "boolean" ? ok : null;
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ /**
216
+ * spec/insights.md §3: the record as one line per event, metadata only, at most 600 events. With
217
+ * `body` (a body by its hash), a verify event also says `ok=true` or `ok=false`.
218
+ */
219
+ export function copilotContext(lines, body) {
200
220
  const events = lines.map((l) => JSON.parse(l));
201
221
  const n = events.length;
202
222
  let shown;
@@ -241,12 +261,16 @@ export function copilotContext(lines) {
241
261
  if (v !== null)
242
262
  parts.push(`${k}=${v}`);
243
263
  }
264
+ const ok = verifyOk(e, body);
265
+ if (ok !== null)
266
+ parts.push(`ok=${ok}`);
244
267
  out.push(parts.join(" "));
245
268
  }
246
269
  return out.join("\n");
247
270
  }
248
271
  const SYSTEM = "You are an investigator reading one AI agent's flight record from Zanii Blackbox. The record " +
249
- "below is one line per event: #seq, time, kind, then its metadata. Answer the question only " +
272
+ "below is one line per event: #seq, time, kind, then its metadata; ok=true or ok=false on a " +
273
+ "verify event is the result of the agent's own check. Answer the question only " +
250
274
  "from the record. Cite the event numbers your answer rests on. If the record doesn't say, say " +
251
275
  "so. Be brief and concrete. Answer with JSON only: " +
252
276
  '{"answer": "<your answer>", "cites": [<seq>, ...]}';
package/dist/index.d.ts CHANGED
@@ -2,7 +2,8 @@ export { A2A_STREAMING, type AgentCardReport, a2aMethod, a2aRequestMeta, a2aResp
2
2
  export { causality, memoryXray } from "./agents/index.ts";
3
3
  export { type DetectorOptions, detectors, fingerprint } from "./analysis/detectors.ts";
4
4
  export { FAULTS, type Fault, faultOf } from "./analysis/faults.ts";
5
- export { type Accuracy, type AnalyzeOptions, type AnswerClaims, analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, type ClaimEntry, type ClaimVerdict, type CriterionResult, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, type Fact, type Finding as AnalysisFinding, factsIn, findingKey, type GroundedClaim, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, type Label as AccuracyLabel, type Landing, landing, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, type Outcome, type OutcomeRollup, packsFor, parseGroundingAnswer, parseJudgeAnswer, type ReferenceFact, type ReferencePack, type ReferenceVerdict, referenceContext, rollup, type Severity, schemaProblem, toolHallucinations, verifyAnswerCredential, type WasteReason, type WasteReport, waste, wilsonLow, } from "./analysis/index.ts";
5
+ export { type Accuracy, type AnalyzeOptions, type AnswerClaims, analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, type ClaimEntry, type ClaimVerdict, type CriterionResult, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, type Fact, type Finding as AnalysisFinding, factsIn, findingKey, type GroundedClaim, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, type Label as AccuracyLabel, type Landing, landing, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, type Outcome, type OutcomeRollup, packsFor, parseGroundingAnswer, parseJudgeAnswer, type ReferenceFact, type ReferencePack, type ReferenceVerdict, referenceContext, rollup, type Severity, schemaProblem, toolDrifted, toolHallucinations, verifyAnswerCredential, type WasteReason, type WasteReport, waste, wilsonLow, } from "./analysis/index.ts";
6
+ export { injectionFindings, injectionRule } from "./analysis/injection.ts";
6
7
  export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, type Taxonomy, taxonomyOf, } from "./analysis/taxonomy.ts";
7
8
  export { type ApprovalFinding, approvalConfirmation, approvalFindings, argsHash, verifyConfirmation, } from "./approvals/index.ts";
8
9
  export { textWarnings } from "./approvals/warnings.ts";
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@ export { A2A_STREAMING, a2aMethod, a2aRequestMeta, a2aResponseMeta, a2aRestMetho
4
4
  export { causality, memoryXray } from "./agents/index.js";
5
5
  export { detectors, fingerprint } from "./analysis/detectors.js";
6
6
  export { FAULTS, faultOf } from "./analysis/faults.js";
7
- export { analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, factsIn, findingKey, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, landing, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, packsFor, parseGroundingAnswer, parseJudgeAnswer, referenceContext, rollup, schemaProblem, toolHallucinations, verifyAnswerCredential, waste, wilsonLow, } from "./analysis/index.js";
7
+ export { analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, factsIn, findingKey, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, landing, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, packsFor, parseGroundingAnswer, parseJudgeAnswer, referenceContext, rollup, schemaProblem, toolDrifted, toolHallucinations, verifyAnswerCredential, waste, wilsonLow, } from "./analysis/index.js";
8
+ export { injectionFindings, injectionRule } from "./analysis/injection.js";
8
9
  export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, taxonomyOf, } from "./analysis/taxonomy.js";
9
10
  export { approvalConfirmation, approvalFindings, argsHash, verifyConfirmation, } from "./approvals/index.js";
10
11
  export { textWarnings } from "./approvals/warnings.js";
@@ -6,6 +6,8 @@ export interface Block {
6
6
  id?: string;
7
7
  name?: string;
8
8
  input?: unknown;
9
+ /** Anthropic: where a text block's words come from (web search: the page, its title, the quote). */
10
+ citations?: unknown[];
9
11
  }
10
12
  export interface Call {
11
13
  requestSeq: number;
@@ -182,6 +182,10 @@ function rebuild(call, body) {
182
182
  block.thinking = (block.thinking ?? "") + d.thinking;
183
183
  else if (d.type === "input_json_delta" && typeof d.partial_json === "string")
184
184
  partial.set(event.index, (partial.get(event.index) ?? "") + d.partial_json);
185
+ else if (d.type === "citations_delta" && isObj(d.citation)) {
186
+ block.citations ??= [];
187
+ block.citations.push(d.citation);
188
+ }
185
189
  }
186
190
  else if (type === "content_block_stop" && typeof event.index === "number") {
187
191
  const block = call.blocks.get(event.index);
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.16.0";
1
+ export declare const VERSION = "0.17.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The SDK version, on its own so the CLI can print it without loading the whole SDK.
2
- export const VERSION = "0.16.0";
2
+ export const VERSION = "0.17.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zanii/blackbox",
3
3
  "license": "Apache-2.0",
4
- "version": "0.16.0",
4
+ "version": "0.17.0",
5
5
  "description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
6
6
  "keywords": [
7
7
  "ai-agents",