@zanii/blackbox 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,15 +1,21 @@
1
+ export { A2A_STREAMING, type AgentCardReport, a2aMethod, a2aRequestMeta, a2aResponseMeta, agentCardDigest, agentCardPayload, verifyAgentCard, } from "./a2a/index.ts";
1
2
  export { causality, memoryXray } from "./agents/index.ts";
2
3
  export { type DetectorOptions, detectors, fingerprint } from "./analysis/detectors.ts";
3
4
  export { FAULTS, type Fault, faultOf } from "./analysis/faults.ts";
4
5
  export { type AnalyzeOptions, analyze, type ClaimVerdict, type CriterionResult, checkFlightPlan, checkpointReplay, dualWitness, eventsOf, type Finding as AnalysisFinding, findingKey, type Landing, landing, OUTCOMES, type Outcome, type OutcomeRollup, rollup, type Severity, type WasteReason, type WasteReport, waste, } from "./analysis/index.ts";
6
+ export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, type Taxonomy, taxonomyOf, } from "./analysis/taxonomy.ts";
5
7
  export { type ApprovalFinding, approvalFindings } from "./approvals/index.ts";
6
8
  export { textWarnings } from "./approvals/warnings.ts";
9
+ export { type ArchiveFile, type ArchiveManifest, type ArchiveReport, archiveDigest, archiveEntry, archiveManifest, verifyArchive, } from "./archive/index.ts";
7
10
  export { type Attestation, attest, isReadOnly, normalise, parseCommand } from "./attest/index.ts";
8
11
  export { type AuthorityFinding, authorityAt, authorityTimeline, automationSurprise, type Mode as AuthorityMode, type Span as AuthoritySpan, } from "./authority/index.ts";
9
12
  export { type Invoice, type InvoiceLine, invoice, loadPlans, type Plan, type Plans, renderTaxInvoice, stripeSignatureOk, type TaxInvoice, type TaxInvoiceInput, taxInvoice, } from "./billing/index.ts";
13
+ export { type AgentIdentity, checkAgent, sessionBom } from "./bom/index.ts";
10
14
  export { BlackboxApiError, type Client, type ClientOptions, client, type SessionQuery, } from "./client/index.ts";
15
+ export { ART12_MIN_RETENTION_DAYS, type Art12Check, type Art12Report, art12Check, } from "./compliance/art12.ts";
11
16
  export { complianceFacts, complianceReport, type Facts, type Framework, type Report as ComplianceReport, renderReport, } from "./compliance/index.ts";
12
17
  export { type CallCost, type CostReport, callMicroUsd, effectivePrice, formatAed, type LoadedPrices, loadPrices, type ModelPrice, type PriceTable, priceCall, priceFor, promptTokens, requestsOf, sessionCost, type Tokens, toAedFils, tokensOf, } from "./cost/index.ts";
18
+ export { type AuditRow, CORE_EVIDENCE, CORE_IDENTIFIERS, type Connector as AuditConnector, canaryIdentifier, checkAuditConnector, type DataFinding, type DataRules, dataClasses, dataFindings, dataFingerprint, dataMeta, dataScan, dataTopology, hasEvidence, type Identifier, type Lineage, lineage, luhn, normalizeValue, parseAuditLog, parseCsv, reconcileSystem, type SystemReport, type Topology, toolWitness, unevidenced, verifyToolWitness, witnessOf, } from "./data/index.ts";
13
19
  export { type Directive, type DirectiveItem, type DirectiveOptions, directives, fleetDirectives, } from "./directives/index.ts";
14
20
  export { checkDrill, drillReport, type Fault as DrillFault } from "./drills/index.ts";
15
21
  export { checkDuty, type DutyFinding, type DutyLimits, dutyFindings } from "./duty/index.ts";
@@ -24,10 +30,10 @@ export { type AmountUnit, anthropicStatement, type Baseline, baselines, type Con
24
30
  export { filingPack, type OccurrenceFacts, type OccurrenceFramework, type OccurrenceReport, occurrenceFacts, occurrenceReport, renderOccurrence, } from "./occurrence/index.ts";
25
31
  export { ingestOcsf, type OcsfEvent, type OcsfVersion, ocsfLine, toOcsf } from "./ocsf/index.ts";
26
32
  export { toOtlp } from "./otlp/index.ts";
27
- export { checkPack, corePack, type PackEntry, packFrameworks } from "./packs/index.ts";
33
+ export { checkIdentifier, checkPack, corePack, type PackEntry, packData, packFrameworks, } from "./packs/index.ts";
28
34
  export { type PolicyChange, type PolicyDelta, policyDelta } from "./policy/delta.ts";
29
35
  export { type Draft, type DraftGroup, policyDrafts } from "./policy/drafts.ts";
30
- export { audited, type CompiledPolicy, compilePolicy, type Decision, decide, loadPolicy, type Policy, policyFindings, type Rule, } from "./policy/index.ts";
36
+ export { audited, type CompiledPolicy, compilePolicy, type Decision, decide, loadPolicy, mcpToolHints, type Policy, policyFindings, type Rule, type ToolHints, } from "./policy/index.ts";
31
37
  export { calibration, FEATURES, features, MIN_EACH, type PrecogModel, precogFindings, precogReport, predict, prefixOf, splitOf, TARGET, train, } from "./precog/index.ts";
32
38
  export { type InterventionEvidence, interventionEvidence, interventionGain, } from "./precog/intervention.ts";
33
39
  export { activities, buildNormal, type NormalModel, type NormalScore, normalScore, normalThreshold, } from "./precog/normal.ts";
@@ -37,7 +43,9 @@ export { type Finding, type FindingCode, type Harness, loadLocal, type Reconcile
37
43
  export { cassette, requestKey, type Take } from "./replay/index.ts";
38
44
  export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.ts";
39
45
  export { type DrainResult, drainSpools } from "./session/drain.ts";
40
- export { BlackboxSession, type FlightPlan, type LlmCallIds, type SessionError, type SessionOptions, type SessionState, type SessionStats, session, stableEventId, } from "./session/index.ts";
46
+ export { BlackboxSession, checkEgress, type EgressCheck, type FlightPlan, type LlmCallIds, type SessionError, type SessionOptions, type SessionState, type SessionStats, session, stableEventId, } from "./session/index.ts";
47
+ export { type TimestampReport, timestampRequest, verifyTimestamp, } from "./timestamp/index.ts";
48
+ export { addCheckpointBody, type Cbor, cborDecode, cborEncode, checkpointText, consistencyProof, cosign, cosignMldsa, ed25519PublicKey, inclusionProof, keyRotationNote, leafHash, logKid, makeReceipt, merkleRoot, mldsaAvailable, mldsaPublicKey, noteVkey, parseAddCheckpoint, parseCheckpoint, parseVkey, RECEIPT_SUB, SESSION_CONTENT_TYPE, sessionStatement, signNote, signStatement, splitNote, statementEntry, subtreeMessage, transparentStatement, verifyConsistency, verifyCoseSign1, verifyCosignature, verifyInclusion, verifyKeyRotation, verifyNote, verifyReceipt, verifyTransparency, } from "./transparency/index.ts";
41
49
  export { type Compensation, checkCompensations, runUndo, type UndoFinding, type UndoStatus, type UndoStep, undoFindings, undoPlan, } from "./undo/index.ts";
42
50
  export * from "./verify/index.ts";
43
51
  export { VERSION } from "./version.ts";
package/dist/index.js CHANGED
@@ -1,17 +1,23 @@
1
1
  // Public API of @zanii/blackbox. Everything customers can import is re-exported here.
2
2
  // Keep this file in step with sdks/python/src/zanii_blackbox/__init__.py (the SDKs mirror each other).
3
+ export { A2A_STREAMING, a2aMethod, a2aRequestMeta, a2aResponseMeta, agentCardDigest, agentCardPayload, verifyAgentCard, } from "./a2a/index.js";
3
4
  export { causality, memoryXray } from "./agents/index.js";
4
5
  export { detectors, fingerprint } from "./analysis/detectors.js";
5
6
  export { FAULTS, faultOf } from "./analysis/faults.js";
6
7
  export { analyze, checkFlightPlan, checkpointReplay, dualWitness, eventsOf, findingKey, landing, OUTCOMES, rollup, waste, } from "./analysis/index.js";
8
+ export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, taxonomyOf, } from "./analysis/taxonomy.js";
7
9
  export { approvalFindings } from "./approvals/index.js";
8
10
  export { textWarnings } from "./approvals/warnings.js";
11
+ export { archiveDigest, archiveEntry, archiveManifest, verifyArchive, } from "./archive/index.js";
9
12
  export { attest, isReadOnly, normalise, parseCommand } from "./attest/index.js";
10
13
  export { authorityAt, authorityTimeline, automationSurprise, } from "./authority/index.js";
11
14
  export { invoice, loadPlans, renderTaxInvoice, stripeSignatureOk, taxInvoice, } from "./billing/index.js";
15
+ export { checkAgent, sessionBom } from "./bom/index.js";
12
16
  export { BlackboxApiError, client, } from "./client/index.js";
17
+ export { ART12_MIN_RETENTION_DAYS, art12Check, } from "./compliance/art12.js";
13
18
  export { complianceFacts, complianceReport, renderReport, } from "./compliance/index.js";
14
19
  export { callMicroUsd, effectivePrice, formatAed, loadPrices, priceCall, priceFor, promptTokens, requestsOf, sessionCost, toAedFils, tokensOf, } from "./cost/index.js";
20
+ export { CORE_EVIDENCE, CORE_IDENTIFIERS, canaryIdentifier, checkAuditConnector, dataClasses, dataFindings, dataFingerprint, dataMeta, dataScan, dataTopology, hasEvidence, lineage, luhn, normalizeValue, parseAuditLog, parseCsv, reconcileSystem, toolWitness, unevidenced, verifyToolWitness, witnessOf, } from "./data/index.js";
15
21
  export { directives, fleetDirectives, } from "./directives/index.js";
16
22
  export { checkDrill, drillReport } from "./drills/index.js";
17
23
  export { checkDuty, dutyFindings } from "./duty/index.js";
@@ -26,10 +32,10 @@ export { anthropicStatement, baselines, checkConnector, checkStatement, csvState
26
32
  export { filingPack, occurrenceFacts, occurrenceReport, renderOccurrence, } from "./occurrence/index.js";
27
33
  export { ingestOcsf, ocsfLine, toOcsf } from "./ocsf/index.js";
28
34
  export { toOtlp } from "./otlp/index.js";
29
- export { checkPack, corePack, packFrameworks } from "./packs/index.js";
35
+ export { checkIdentifier, checkPack, corePack, packData, packFrameworks, } from "./packs/index.js";
30
36
  export { policyDelta } from "./policy/delta.js";
31
37
  export { policyDrafts } from "./policy/drafts.js";
32
- export { audited, compilePolicy, decide, loadPolicy, policyFindings, } from "./policy/index.js";
38
+ export { audited, compilePolicy, decide, loadPolicy, mcpToolHints, policyFindings, } from "./policy/index.js";
33
39
  export { calibration, FEATURES, features, MIN_EACH, precogFindings, precogReport, predict, prefixOf, splitOf, TARGET, train, } from "./precog/index.js";
34
40
  export { interventionEvidence, interventionGain, } from "./precog/intervention.js";
35
41
  export { activities, buildNormal, normalScore, normalThreshold, } from "./precog/normal.js";
@@ -39,7 +45,9 @@ export { loadLocal, RecordNotVerified, reconcile, } from "./reconcile/index.js";
39
45
  export { cassette, requestKey } from "./replay/index.js";
40
46
  export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.js";
41
47
  export { drainSpools } from "./session/drain.js";
42
- export { BlackboxSession, session, stableEventId, } from "./session/index.js";
48
+ export { BlackboxSession, checkEgress, session, stableEventId, } from "./session/index.js";
49
+ export { timestampRequest, verifyTimestamp, } from "./timestamp/index.js";
50
+ export { addCheckpointBody, cborDecode, cborEncode, checkpointText, consistencyProof, cosign, cosignMldsa, ed25519PublicKey, inclusionProof, keyRotationNote, leafHash, logKid, makeReceipt, merkleRoot, mldsaAvailable, mldsaPublicKey, noteVkey, parseAddCheckpoint, parseCheckpoint, parseVkey, RECEIPT_SUB, SESSION_CONTENT_TYPE, sessionStatement, signNote, signStatement, splitNote, statementEntry, subtreeMessage, transparentStatement, verifyConsistency, verifyCoseSign1, verifyCosignature, verifyInclusion, verifyKeyRotation, verifyNote, verifyReceipt, verifyTransparency, } from "./transparency/index.js";
43
51
  export { checkCompensations, runUndo, undoFindings, undoPlan, } from "./undo/index.js";
44
52
  export * from "./verify/index.js";
45
53
  export { VERSION } from "./version.js";
@@ -1,5 +1,5 @@
1
1
  import type { Json } from "../verify/index.ts";
2
- export type OcsfVersion = "1.8" | "1.3" | "1.1";
2
+ export type OcsfVersion = "1.9" | "1.8" | "1.3" | "1.1";
3
3
  export type OcsfEvent = {
4
4
  [key: string]: Json;
5
5
  };
@@ -1,6 +1,7 @@
1
1
  // OCSF export (spec/ocsf.md, idea S1): a session's record as OCSF 1.8.0 events for a SIEM (Splunk,
2
2
  // AWS Security Lake, CrowdStrike, Elastic), with a lossy downgrade to 1.1 / 1.3 and a one-line text
3
3
  // form. Mirrors ocsf.py.
4
+ import { taxonomyOf } from "../analysis/taxonomy.js";
4
5
  const SEVERITY = {
5
6
  advisory: [2, "Low"],
6
7
  caution: [3, "Medium"],
@@ -45,11 +46,16 @@ const FINDING = [2004, "Detection Finding", 2, "Findings"];
45
46
  * gets what the gateway saw. */
46
47
  export function toOcsf(lines, options = {}) {
47
48
  const pv = options.productVersion ?? "unknown";
49
+ let label;
50
+ let session = "";
48
51
  const requests = new Map();
49
52
  const out = [];
50
53
  for (const line of lines) {
51
54
  const e = JSON.parse(line);
52
55
  const m = e.meta;
56
+ session = e.session_id;
57
+ if (e.kind === "session.open" && typeof m.label === "string")
58
+ label = m.label;
53
59
  if (e.kind === "llm.request") {
54
60
  requests.set(e.seq, {
55
61
  ...(typeof m.provider === "string" ? { provider: m.provider } : {}),
@@ -114,9 +120,36 @@ export function toOcsf(lines, options = {}) {
114
120
  out.push(ev);
115
121
  }
116
122
  }
117
- return options.version && options.version !== "1.8"
118
- ? out.map((ev) => downgrade(ev, options.version))
119
- : out;
123
+ const version = options.version ?? "1.9";
124
+ if (version === "1.9")
125
+ return out.map((ev) => upgrade(ev, label, session));
126
+ return version === "1.8" ? out : out.map((ev) => downgrade(ev, version));
127
+ }
128
+ /** OCSF 1.9 (spec/ocsf.md §2): the agent (`ai_agent`) on model and tool events, and each finding's
129
+ * MITRE ATLAS techniques (`attacks`) and OWASP ASI ids (`metadata.tags`). */
130
+ function upgrade(ev, label, session) {
131
+ const metadata = {
132
+ ...ev.metadata,
133
+ version: "1.9.0",
134
+ };
135
+ const out = { ...ev, metadata };
136
+ const ai = ev.ai_model !== undefined ||
137
+ ev.api?.operation === "tools/call";
138
+ if (ai) {
139
+ out.ai_agent = { name: label ?? "agent", instance_uid: session };
140
+ metadata.profiles = ["ai_operation"];
141
+ }
142
+ if (ev.class_uid === 2004) {
143
+ const t = taxonomyOf(String(ev.finding_info.title));
144
+ if (t.atlas.length)
145
+ out.attacks = t.atlas.map((a) => ({
146
+ technique: { uid: a.id, name: a.name },
147
+ version: "ATLAS v2026.09",
148
+ }));
149
+ if (t.owasp.length)
150
+ metadata.tags = t.owasp.map((o) => ({ name: "owasp_asi", value: o.id }));
151
+ }
152
+ return out;
120
153
  }
121
154
  /** Idea S1: for a SIEM on an older OCSF: drops what came later (`ai_model`), the `ai_operation`
122
155
  * profile, and says so in `unmapped.downgraded_from`. */
@@ -7,7 +7,14 @@ type Attr = {
7
7
  key: string;
8
8
  value: Value;
9
9
  };
10
- export declare function toOtlp(lines: readonly string[]): {
10
+ /**
11
+ * spec/otlp.md: a session as OTLP/JSON. `semconv: 2` (the default) follows the GenAI semantic
12
+ * conventions as of v1.42 (`gen_ai.provider.name`, `invoke_agent`, MCP `tools/call`); `semconv: 1`
13
+ * is the first version's output, pinned by spec/vectors/otlp.json.
14
+ */
15
+ export declare function toOtlp(lines: readonly string[], options?: {
16
+ semconv?: 1 | 2;
17
+ }): {
11
18
  resourceSpans: {
12
19
  resource: {
13
20
  attributes: Attr[];
@@ -1,12 +1,21 @@
1
1
  // A session as OpenTelemetry traces (spec/otlp.md): OTLP/JSON with the GenAI semantic conventions.
2
2
  // Deterministic (ids derived from the session), so both SDKs produce the same bytes. Mirrors otlp.py.
3
3
  import { createHash } from "node:crypto";
4
+ import { taxonomyOf } from "../analysis/taxonomy.js";
4
5
  import { tokensOf } from "../cost/index.js";
5
6
  const hex = (s, n) => createHash("sha256").update(s).digest("hex").slice(0, n);
6
7
  const nanos = (ts) => `${Date.parse(ts)}000000`;
7
8
  const str = (key, v) => typeof v === "string" ? [{ key, value: { stringValue: v } }] : [];
8
9
  const int = (key, v) => typeof v === "number" && Number.isSafeInteger(v) ? [{ key, value: { intValue: String(v) } }] : [];
9
- export function toOtlp(lines) {
10
+ /**
11
+ * spec/otlp.md: a session as OTLP/JSON. `semconv: 2` (the default) follows the GenAI semantic
12
+ * conventions as of v1.42 (`gen_ai.provider.name`, `invoke_agent`, MCP `tools/call`); `semconv: 1`
13
+ * is the first version's output, pinned by spec/vectors/otlp.json.
14
+ */
15
+ export function toOtlp(lines, options = {}) {
16
+ return options.semconv === 1 ? toOtlpV1(lines) : toOtlpV2(lines);
17
+ }
18
+ function toOtlpV1(lines) {
10
19
  const events = lines.map((l) => JSON.parse(l));
11
20
  const first = events[0];
12
21
  if (!first)
@@ -141,3 +150,224 @@ export function toOtlp(lines) {
141
150
  ],
142
151
  };
143
152
  }
153
+ // ---------------------------------------------------------------- semconv 2 (GenAI v1.42)
154
+ /** Provider kinds as `gen_ai.provider.name` well-known values; any other provider id as it is. */
155
+ const PROVIDER_NAMES = {
156
+ anthropic: "anthropic",
157
+ openai: "openai",
158
+ "azure-openai": "azure.ai.openai",
159
+ gemini: "gcp.gemini",
160
+ vertex: "gcp.vertex_ai",
161
+ bedrock: "aws.bedrock",
162
+ };
163
+ const list = (key, vs) => vs.length
164
+ ? [{ key, value: { arrayValue: { values: vs.map((v) => ({ stringValue: v })) } } }]
165
+ : [];
166
+ const strs = (key, v) => typeof v === "string" ? [{ key, value: { arrayValue: { values: [{ stringValue: v }] } } }] : [];
167
+ const failure = (type, message) => ({
168
+ attrs: str("error.type", type),
169
+ status: { status: { code: 2, message } },
170
+ });
171
+ function toOtlpV2(lines) {
172
+ const events = lines.map((l) => JSON.parse(l));
173
+ const first = events[0];
174
+ if (!first)
175
+ return { resourceSpans: [] };
176
+ const session = first.session_id;
177
+ const traceId = hex(session, 32);
178
+ const spanId = (seq) => hex(`${session}:${seq}`, 16);
179
+ const root = spanId(0);
180
+ const last = events[events.length - 1];
181
+ const label = typeof first.meta.label === "string" ? first.meta.label : undefined;
182
+ const conversation = str("gen_ai.conversation.id", session);
183
+ const spans = [];
184
+ // The session is the agent's invocation, with the findings as its events.
185
+ spans.push({
186
+ traceId,
187
+ spanId: root,
188
+ name: label ? `invoke_agent ${label}` : "invoke_agent",
189
+ kind: 1,
190
+ startTimeUnixNano: nanos(first.ts),
191
+ endTimeUnixNano: nanos(last.ts),
192
+ attributes: [
193
+ ...str("gen_ai.operation.name", "invoke_agent"),
194
+ ...str("gen_ai.agent.name", label),
195
+ ...conversation,
196
+ ...str("blackbox.session_id", session),
197
+ ...str("blackbox.mode", first.meta.mode),
198
+ ...str("blackbox.tenant", first.meta.tenant),
199
+ ...int("blackbox.events", events.length),
200
+ ],
201
+ events: events
202
+ .filter((e) => e.kind === "finding")
203
+ .map((e) => ({
204
+ timeUnixNano: nanos(e.ts),
205
+ name: String(e.meta.code),
206
+ attributes: [
207
+ ...str("blackbox.severity", e.meta.severity),
208
+ ...str("blackbox.fault", e.meta.fault),
209
+ ...list("blackbox.owasp_asi", taxonomyOf(String(e.meta.code)).owasp.map((o) => o.id)),
210
+ ...list("blackbox.mitre_atlas", taxonomyOf(String(e.meta.code)).atlas.map((a) => a.id)),
211
+ ...int("blackbox.seq", e.seq),
212
+ ],
213
+ })),
214
+ });
215
+ // Model calls: CLIENT spans named `chat {model}`.
216
+ const ends = new Map();
217
+ for (const e of events)
218
+ if ((e.kind === "llm.response" || e.kind === "llm.incomplete") &&
219
+ typeof e.meta.request_seq === "number")
220
+ ends.set(e.meta.request_seq, e);
221
+ for (const e of events.filter((x) => x.kind === "llm.request")) {
222
+ const end = ends.get(e.seq);
223
+ const usage = end?.meta.usage && typeof end.meta.usage === "object"
224
+ ? tokensOf(end.meta.usage)
225
+ : null;
226
+ const status = end?.meta.status;
227
+ const model = typeof end?.meta.model === "string" ? end.meta.model : undefined;
228
+ const provider = typeof e.meta.provider === "string" ? e.meta.provider : undefined;
229
+ const reason = String(end?.meta.reason ?? "incomplete");
230
+ const fail = !end
231
+ ? failure("incomplete", "no response recorded")
232
+ : end.kind === "llm.incomplete"
233
+ ? failure(reason, reason)
234
+ : typeof status === "number" && status >= 400
235
+ ? failure(String(status), `status ${status}`)
236
+ : null;
237
+ spans.push({
238
+ traceId,
239
+ spanId: spanId(e.seq),
240
+ parentSpanId: root,
241
+ name: model ? `chat ${model}` : "chat",
242
+ kind: 3,
243
+ startTimeUnixNano: nanos(e.ts),
244
+ endTimeUnixNano: nanos((end ?? e).ts),
245
+ attributes: [
246
+ ...str("gen_ai.operation.name", "chat"),
247
+ ...str("gen_ai.provider.name", provider ? (PROVIDER_NAMES[provider] ?? provider) : undefined),
248
+ ...str("gen_ai.response.model", model),
249
+ ...str("gen_ai.response.id", end?.meta.message_id ?? end?.meta.response_id),
250
+ ...strs("gen_ai.response.finish_reasons", end?.meta.stop_reason ?? end?.meta.finish_reason),
251
+ ...(usage
252
+ ? [
253
+ ...int("gen_ai.usage.input_tokens", usage.input + usage.cache_read + usage.cache_write),
254
+ ...int("gen_ai.usage.output_tokens", usage.output),
255
+ ...(usage.cache_read
256
+ ? int("gen_ai.usage.cache_read.input_tokens", usage.cache_read)
257
+ : []),
258
+ ...(usage.cache_write
259
+ ? int("gen_ai.usage.cache_write.input_tokens", usage.cache_write)
260
+ : []),
261
+ ]
262
+ : []),
263
+ ...conversation,
264
+ ...str("blackbox.provider", provider),
265
+ ...str("blackbox.region", e.meta.region),
266
+ ...int("http.response.status_code", status),
267
+ ...(fail ? fail.attrs : []),
268
+ ...int("blackbox.seq", e.seq),
269
+ ],
270
+ ...(fail ? fail.status : {}),
271
+ });
272
+ }
273
+ // MCP and broker calls, closed by their result (call_seq). MCP spans follow the MCP conventions.
274
+ const results = new Map();
275
+ for (const e of events)
276
+ if (e.kind === "tool.result" && typeof e.meta.call_seq === "number")
277
+ results.set(e.meta.call_seq, e);
278
+ for (const e of events.filter((x) => x.kind === "tool.call")) {
279
+ const end = results.get(e.seq);
280
+ const broker = e.meta.via === "broker";
281
+ const method = typeof e.meta.method === "string" ? e.meta.method : undefined;
282
+ const tool = typeof e.meta.tool === "string" ? e.meta.tool : undefined;
283
+ const isCall = broker || method === "tools/call";
284
+ const fail = e.meta.policy !== undefined
285
+ ? failure("policy_denied", "denied by policy")
286
+ : end?.meta.rpc_error !== undefined
287
+ ? failure(String(end.meta.rpc_error), "rpc error")
288
+ : end?.meta.is_error === true
289
+ ? failure("tool_error", "tool error")
290
+ : broker && typeof end?.meta.status === "number" && end.meta.status >= 400
291
+ ? failure(String(end.meta.status), `status ${end.meta.status}`)
292
+ : null;
293
+ const rpcId = e.meta.rpc_id;
294
+ spans.push({
295
+ traceId,
296
+ spanId: spanId(e.seq),
297
+ parentSpanId: root,
298
+ name: broker
299
+ ? `execute_tool ${tool ?? "api"}`
300
+ : `${method ?? "mcp"}${tool && isCall ? ` ${tool}` : ""}`,
301
+ kind: 3,
302
+ startTimeUnixNano: nanos(e.ts),
303
+ endTimeUnixNano: nanos((end ?? e).ts),
304
+ attributes: [
305
+ ...(isCall ? str("gen_ai.operation.name", "execute_tool") : []),
306
+ ...str("gen_ai.tool.name", tool),
307
+ ...(broker ? str("gen_ai.tool.type", "extension") : []),
308
+ ...(broker ? [] : str("mcp.method.name", method)),
309
+ ...(broker ? [] : str("mcp.protocol.version", e.meta.protocol_version)),
310
+ ...(typeof rpcId === "string" || typeof rpcId === "number"
311
+ ? str("jsonrpc.request.id", String(rpcId))
312
+ : []),
313
+ ...(broker ? str("http.request.method", e.meta.method) : []),
314
+ ...(broker ? str("url.path", e.meta.path) : []),
315
+ ...str("blackbox.mcp_server", e.meta.server),
316
+ ...int("rpc.response.status_code", end?.meta.rpc_error),
317
+ ...conversation,
318
+ ...(fail ? fail.attrs : []),
319
+ ...int("blackbox.seq", e.seq),
320
+ ],
321
+ ...(fail ? fail.status : {}),
322
+ });
323
+ }
324
+ // The agent's own tools (SDK events): INTERNAL spans, from a call to the next result of its name.
325
+ const open = new Map();
326
+ const sdk = (e, type) => e.kind === "sdk.event" && e.meta.type === type;
327
+ for (const e of events) {
328
+ const name = typeof e.meta.name === "string" ? e.meta.name : undefined;
329
+ if (!name)
330
+ continue;
331
+ if (sdk(e, "tool.call"))
332
+ open.set(name, [...(open.get(name) ?? []), e]);
333
+ if (!sdk(e, "tool.result"))
334
+ continue;
335
+ const call = open.get(name)?.shift();
336
+ spans.push({
337
+ traceId,
338
+ spanId: spanId(e.seq),
339
+ parentSpanId: root,
340
+ name: `execute_tool ${name}`,
341
+ kind: 1,
342
+ startTimeUnixNano: nanos((call ?? e).ts),
343
+ endTimeUnixNano: nanos(e.ts),
344
+ attributes: [
345
+ ...str("gen_ai.operation.name", "execute_tool"),
346
+ ...str("gen_ai.tool.name", name),
347
+ ...str("gen_ai.tool.type", "function"),
348
+ ...conversation,
349
+ ...str("blackbox.reported_by", "agent"),
350
+ ...int("blackbox.seq", e.seq),
351
+ ],
352
+ });
353
+ }
354
+ return {
355
+ resourceSpans: [
356
+ {
357
+ resource: {
358
+ attributes: [
359
+ ...str("service.name", "zanii-blackbox"),
360
+ ...str("blackbox.session_id", session),
361
+ ],
362
+ },
363
+ scopeSpans: [
364
+ {
365
+ scope: { name: "zanii-blackbox", version: "2" },
366
+ schemaUrl: "https://opentelemetry.io/schemas/1.44.0",
367
+ spans,
368
+ },
369
+ ],
370
+ },
371
+ ],
372
+ };
373
+ }
@@ -1,3 +1,4 @@
1
+ import { type DataRules } from "../data/index.ts";
1
2
  type Obj = Record<string, unknown>;
2
3
  export interface PackEntry {
3
4
  pack: string;
@@ -9,6 +10,13 @@ export interface PackEntry {
9
10
  }
10
11
  /** spec/packs.md §1: why a pack is invalid, or undefined. */
11
12
  export declare function checkPack(value: unknown): string | undefined;
13
+ /** spec/data.md §1: why an identifier is invalid, or undefined. */
14
+ export declare function checkIdentifier(v: unknown, at: string): string | undefined;
15
+ /**
16
+ * spec/data.md §4: the data rules of packs merged in order, then the core. Pack identifiers come
17
+ * before the core ones; throws on an invalid pack or an identifier id in two packs.
18
+ */
19
+ export declare function packData(packs: readonly unknown[]): DataRules;
12
20
  /** spec/packs.md §2: the core pack, from the built-in frameworks files. */
13
21
  export declare function corePack(compliance: {
14
22
  notes: string;
@@ -1,6 +1,7 @@
1
1
  // Framework packs (spec/packs.md): compliance and occurrence frameworks as data a deployment
2
2
  // switches on. Pure; mirrors sdks/python/src/zanii_blackbox/packs.py; pinned by
3
- // spec/vectors/packs.json.
3
+ // spec/vectors/packs.json. The `data` part is spec/data.md §4.
4
+ import { CORE_EVIDENCE, CORE_IDENTIFIERS } from "../data/index.js";
4
5
  const FACTS = new Set([
5
6
  "sessions",
6
7
  "events",
@@ -130,7 +131,9 @@ function checkArabic(ar, fw, at) {
130
131
  export function checkPack(value) {
131
132
  if (!isObj(value))
132
133
  return "a pack must be an object";
134
+ // `data` came later (spec/data.md §4); the pinned messages below name the v1 keys only.
133
135
  if (!only(value, [
136
+ "data",
134
137
  "v",
135
138
  "id",
136
139
  "title",
@@ -154,7 +157,7 @@ export function checkPack(value) {
154
157
  typeof value.reviewed_at !== "string" ||
155
158
  !DATE.test(value.reviewed_at)))
156
159
  return "a reviewed pack needs reviewed_by and reviewed_at (YYYY-MM-DD)";
157
- if (value.compliance === undefined && value.occurrence === undefined)
160
+ if (value.compliance === undefined && value.occurrence === undefined && value.data === undefined)
158
161
  return "a pack needs compliance, occurrence or both";
159
162
  for (const kind of ["compliance", "occurrence"])
160
163
  if (value[kind] !== undefined) {
@@ -162,8 +165,131 @@ export function checkPack(value) {
162
165
  if (bad)
163
166
  return bad;
164
167
  }
168
+ return value.data === undefined ? undefined : checkData(value.data);
169
+ }
170
+ const IDENTIFIER_ID = /^[a-z0-9_]{1,64}$/;
171
+ const NORMALIZE = new Set(["digits", "lower", "upper", "exact"]);
172
+ // Escapes Python reads over Unicode and JavaScript over ASCII (spec/data.md §1).
173
+ const UNPORTABLE = /\\[dDwWbBsS]/;
174
+ const ALLOW = /^(?:region|model|tool|api):(?:[A-Za-z0-9._-]{1,128}\*?|\*)$/;
175
+ const TOOL = /^[A-Za-z0-9._-]{1,128}$/;
176
+ const FIELD = /^[A-Za-z0-9_.-]{1,64}$/;
177
+ const compiles = (pattern) => {
178
+ try {
179
+ new RegExp(pattern);
180
+ return true;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ };
186
+ /** spec/data.md §1: why an identifier is invalid, or undefined. */
187
+ export function checkIdentifier(v, at) {
188
+ if (!isObj(v) ||
189
+ !only(v, ["id", "title", "pattern", "normalize", "keep_last", "check", "personal"]))
190
+ return `${at} may only have id, title, pattern, normalize, keep_last, check, personal`;
191
+ if (typeof v.id !== "string" || !IDENTIFIER_ID.test(v.id))
192
+ return `${at}.id must be 1-64 of a-z 0-9 _`;
193
+ if (!text(v.title, 200))
194
+ return `${at}.title must be 1-200 characters`;
195
+ if (typeof v.pattern !== "string" || !text(v.pattern, 500) || !compiles(v.pattern))
196
+ return `${at}.pattern must be a regular expression of 1-500 characters`;
197
+ if (UNPORTABLE.test(v.pattern))
198
+ return `${at}.pattern must not use \\d \\w \\b or \\s`;
199
+ if (!NORMALIZE.has(v.normalize))
200
+ return `${at}.normalize must be digits, lower, upper or exact`;
201
+ if (v.keep_last !== undefined &&
202
+ (!Number.isInteger(v.keep_last) || v.keep_last < 1 || v.keep_last > 30))
203
+ return `${at}.keep_last must be 1-30`;
204
+ if (v.check !== undefined && v.check !== "luhn")
205
+ return `${at}.check must be luhn`;
206
+ if (typeof v.personal !== "boolean")
207
+ return `${at}.personal must be true or false`;
208
+ return undefined;
209
+ }
210
+ function checkData(part) {
211
+ if (!isObj(part) || !only(part, ["notes", "identifiers", "residency", "restricted", "evidence"]))
212
+ return "data may only have notes, identifiers, residency, restricted, evidence";
213
+ if (!text(part.notes, 2000))
214
+ return "data.notes must be 1-2000 characters";
215
+ const ids = part.identifiers ?? [];
216
+ if (!Array.isArray(ids) || ids.length > 50)
217
+ return "data.identifiers must hold 0-50 identifiers";
218
+ const seen = new Set(CORE_IDENTIFIERS.map((i) => i.id));
219
+ for (const [i, v] of ids.entries()) {
220
+ const bad = checkIdentifier(v, `data.identifiers[${i}]`);
221
+ if (bad)
222
+ return bad;
223
+ const id = v.id;
224
+ if (seen.has(id))
225
+ return `data.identifiers[${i}].id ${id} is already defined`;
226
+ seen.add(id);
227
+ }
228
+ if (part.residency !== undefined && typeof part.residency !== "boolean")
229
+ return "data.residency must be true or false";
230
+ const rules = part.restricted ?? [];
231
+ if (!Array.isArray(rules) || rules.length > 50)
232
+ return "data.restricted must hold 0-50 rules";
233
+ for (const [i, r] of rules.entries()) {
234
+ const at = `data.restricted[${i}]`;
235
+ if (!isObj(r) || !only(r, ["identifiers", "allow"]))
236
+ return `${at} must be {identifiers, allow}`;
237
+ if (!Array.isArray(r.identifiers) ||
238
+ r.identifiers.length < 1 ||
239
+ r.identifiers.length > 50 ||
240
+ !r.identifiers.every((x) => typeof x === "string" && seen.has(x)))
241
+ return `${at}.identifiers must hold 1-50 known identifier ids`;
242
+ if (!Array.isArray(r.allow) ||
243
+ r.allow.length < 1 ||
244
+ r.allow.length > 20 ||
245
+ !r.allow.every((x) => typeof x === "string" && ALLOW.test(x)))
246
+ return `${at}.allow must hold 1-20 of region:, model:, tool:, api:`;
247
+ }
248
+ const evidence = part.evidence ?? [];
249
+ if (!Array.isArray(evidence) || evidence.length > 200)
250
+ return "data.evidence must hold 0-200 rules";
251
+ for (const [i, e] of evidence.entries())
252
+ if (!isObj(e) ||
253
+ !only(e, ["tool", "field"]) ||
254
+ typeof e.tool !== "string" ||
255
+ !TOOL.test(e.tool) ||
256
+ typeof e.field !== "string" ||
257
+ !FIELD.test(e.field))
258
+ return `data.evidence[${i}] must be {tool, field}`;
165
259
  return undefined;
166
260
  }
261
+ /**
262
+ * spec/data.md §4: the data rules of packs merged in order, then the core. Pack identifiers come
263
+ * before the core ones; throws on an invalid pack or an identifier id in two packs.
264
+ */
265
+ export function packData(packs) {
266
+ const identifiers = [];
267
+ const restricted = [];
268
+ const evidence = [];
269
+ const owner = new Map();
270
+ let residency = false;
271
+ for (const p of packs) {
272
+ const bad = checkPack(p);
273
+ const id = isObj(p) && typeof p.id === "string" ? p.id : "?";
274
+ if (bad)
275
+ throw new Error(`pack ${id}: ${bad}`);
276
+ const data = p.data;
277
+ if (!data)
278
+ continue;
279
+ for (const ident of data.identifiers ?? []) {
280
+ const had = owner.get(ident.id);
281
+ if (had)
282
+ throw new Error(`identifier ${ident.id} is in packs ${had} and ${id}`);
283
+ owner.set(ident.id, id);
284
+ identifiers.push(ident);
285
+ }
286
+ residency ||= data.residency === true;
287
+ restricted.push(...(data.restricted ?? []));
288
+ evidence.push(...(data.evidence ?? []));
289
+ }
290
+ identifiers.push(...CORE_IDENTIFIERS);
291
+ return { identifiers, residency, restricted, evidence: [...evidence, ...CORE_EVIDENCE] };
292
+ }
167
293
  /** spec/packs.md §2: the core pack, from the built-in frameworks files. */
168
294
  export function corePack(compliance, occurrence) {
169
295
  return {
@@ -11,7 +11,23 @@ export interface Rule {
11
11
  reason?: string;
12
12
  /** N3 (idea C5): record-only. Never decides a call; what it would do is recorded (POLICY_AUDIT). */
13
13
  audit?: boolean;
14
+ /** spec/policy.md §1: MCP tool annotations the call must have (from the server's `tools/list`). */
15
+ hints?: Partial<ToolHints>;
14
16
  }
17
+ /** MCP tool annotations as booleans, with the spec's defaults for what a server leaves out. */
18
+ export interface ToolHints {
19
+ read_only: boolean;
20
+ destructive: boolean;
21
+ idempotent: boolean;
22
+ open_world: boolean;
23
+ }
24
+ /**
25
+ * spec/policy.md §1: a tool's annotations (MCP `readOnlyHint`, `destructiveHint`, `idempotentHint`,
26
+ * `openWorldHint`) as hints. Missing ones take MCP's defaults: not read-only, destructive, not
27
+ * idempotent, open-world; destructive and idempotent only mean something for a tool that writes.
28
+ * An unknown tool gets the defaults too. Hints are the server's word, not proof.
29
+ */
30
+ export declare function mcpToolHints(annotations?: unknown): ToolHints;
15
31
  export interface Policy {
16
32
  version: 1;
17
33
  rules: Rule[];
@@ -31,11 +47,12 @@ interface Compiled {
31
47
  /** Parses a policy file; throws on anything invalid (the server refuses to start). */
32
48
  export declare function loadPolicy(bytes: Uint8Array): Policy;
33
49
  export declare function compilePolicy(policy: Policy): Compiled[];
34
- /** The first rule that matches this call, or null (allowed). `names`: the tool's names (an MCP tool has two). */
35
- export declare function decide(compiled: readonly Compiled[], names: readonly string[], args: unknown): Decision | null;
50
+ /** The first rule that matches this call, or null (allowed). `names`: the tool's names (an MCP tool
51
+ * has two). `hints`: an MCP tool's annotations; a rule with `hints` never matches a call without. */
52
+ export declare function decide(compiled: readonly Compiled[], names: readonly string[], args: unknown, hints?: ToolHints): Decision | null;
36
53
  export type CompiledPolicy = ReturnType<typeof compilePolicy>;
37
54
  /** N3 (idea C5): every audit rule this call matches, and what it would do if it were enforced. */
38
- export declare function audited(compiled: readonly Compiled[], names: readonly string[], args: unknown): Array<{
55
+ export declare function audited(compiled: readonly Compiled[], names: readonly string[], args: unknown, hints?: ToolHints): Array<{
39
56
  rule: string;
40
57
  would: Rule["action"];
41
58
  }>;