@zanii/blackbox 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/a2a/index.d.ts +77 -0
- package/dist/a2a/index.js +305 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +34 -5
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +107 -1
- package/dist/session/index.js +189 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
package/dist/otlp/index.js
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
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
|
-
|
|
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 linkCallers(options.semconv === 1 ? toOtlpV1(lines) : toOtlpV2(lines), lines);
|
|
17
|
+
}
|
|
18
|
+
const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
|
|
19
|
+
/** spec/otlp.md §3: a call that carried the caller's W3C `traceparent` links to the caller's span. */
|
|
20
|
+
function linkCallers(out, lines) {
|
|
21
|
+
const links = new Map();
|
|
22
|
+
let session = "";
|
|
23
|
+
for (const l of lines) {
|
|
24
|
+
const e = JSON.parse(l);
|
|
25
|
+
session ||= e.session_id;
|
|
26
|
+
const tp = e.meta.headers?.traceparent;
|
|
27
|
+
const m = typeof tp === "string" ? TRACEPARENT.exec(tp) : null;
|
|
28
|
+
if (m && !/^0+$/.test(m[1]) && !/^0+$/.test(m[2]))
|
|
29
|
+
links.set(hex(`${session}:${e.seq}`, 16), {
|
|
30
|
+
traceId: m[1],
|
|
31
|
+
spanId: m[2],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (links.size === 0)
|
|
35
|
+
return out;
|
|
36
|
+
for (const rs of out.resourceSpans)
|
|
37
|
+
for (const ss of rs.scopeSpans)
|
|
38
|
+
for (const sp of ss.spans) {
|
|
39
|
+
const link = links.get(sp.spanId);
|
|
40
|
+
if (link)
|
|
41
|
+
sp.links = [link];
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function toOtlpV1(lines) {
|
|
10
46
|
const events = lines.map((l) => JSON.parse(l));
|
|
11
47
|
const first = events[0];
|
|
12
48
|
if (!first)
|
|
@@ -141,3 +177,224 @@ export function toOtlp(lines) {
|
|
|
141
177
|
],
|
|
142
178
|
};
|
|
143
179
|
}
|
|
180
|
+
// ---------------------------------------------------------------- semconv 2 (GenAI v1.42)
|
|
181
|
+
/** Provider kinds as `gen_ai.provider.name` well-known values; any other provider id as it is. */
|
|
182
|
+
const PROVIDER_NAMES = {
|
|
183
|
+
anthropic: "anthropic",
|
|
184
|
+
openai: "openai",
|
|
185
|
+
"azure-openai": "azure.ai.openai",
|
|
186
|
+
gemini: "gcp.gemini",
|
|
187
|
+
vertex: "gcp.vertex_ai",
|
|
188
|
+
bedrock: "aws.bedrock",
|
|
189
|
+
};
|
|
190
|
+
const list = (key, vs) => vs.length
|
|
191
|
+
? [{ key, value: { arrayValue: { values: vs.map((v) => ({ stringValue: v })) } } }]
|
|
192
|
+
: [];
|
|
193
|
+
const strs = (key, v) => typeof v === "string" ? [{ key, value: { arrayValue: { values: [{ stringValue: v }] } } }] : [];
|
|
194
|
+
const failure = (type, message) => ({
|
|
195
|
+
attrs: str("error.type", type),
|
|
196
|
+
status: { status: { code: 2, message } },
|
|
197
|
+
});
|
|
198
|
+
function toOtlpV2(lines) {
|
|
199
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
200
|
+
const first = events[0];
|
|
201
|
+
if (!first)
|
|
202
|
+
return { resourceSpans: [] };
|
|
203
|
+
const session = first.session_id;
|
|
204
|
+
const traceId = hex(session, 32);
|
|
205
|
+
const spanId = (seq) => hex(`${session}:${seq}`, 16);
|
|
206
|
+
const root = spanId(0);
|
|
207
|
+
const last = events[events.length - 1];
|
|
208
|
+
const label = typeof first.meta.label === "string" ? first.meta.label : undefined;
|
|
209
|
+
const conversation = str("gen_ai.conversation.id", session);
|
|
210
|
+
const spans = [];
|
|
211
|
+
// The session is the agent's invocation, with the findings as its events.
|
|
212
|
+
spans.push({
|
|
213
|
+
traceId,
|
|
214
|
+
spanId: root,
|
|
215
|
+
name: label ? `invoke_agent ${label}` : "invoke_agent",
|
|
216
|
+
kind: 1,
|
|
217
|
+
startTimeUnixNano: nanos(first.ts),
|
|
218
|
+
endTimeUnixNano: nanos(last.ts),
|
|
219
|
+
attributes: [
|
|
220
|
+
...str("gen_ai.operation.name", "invoke_agent"),
|
|
221
|
+
...str("gen_ai.agent.name", label),
|
|
222
|
+
...conversation,
|
|
223
|
+
...str("blackbox.session_id", session),
|
|
224
|
+
...str("blackbox.mode", first.meta.mode),
|
|
225
|
+
...str("blackbox.tenant", first.meta.tenant),
|
|
226
|
+
...int("blackbox.events", events.length),
|
|
227
|
+
],
|
|
228
|
+
events: events
|
|
229
|
+
.filter((e) => e.kind === "finding")
|
|
230
|
+
.map((e) => ({
|
|
231
|
+
timeUnixNano: nanos(e.ts),
|
|
232
|
+
name: String(e.meta.code),
|
|
233
|
+
attributes: [
|
|
234
|
+
...str("blackbox.severity", e.meta.severity),
|
|
235
|
+
...str("blackbox.fault", e.meta.fault),
|
|
236
|
+
...list("blackbox.owasp_asi", taxonomyOf(String(e.meta.code)).owasp.map((o) => o.id)),
|
|
237
|
+
...list("blackbox.mitre_atlas", taxonomyOf(String(e.meta.code)).atlas.map((a) => a.id)),
|
|
238
|
+
...int("blackbox.seq", e.seq),
|
|
239
|
+
],
|
|
240
|
+
})),
|
|
241
|
+
});
|
|
242
|
+
// Model calls: CLIENT spans named `chat {model}`.
|
|
243
|
+
const ends = new Map();
|
|
244
|
+
for (const e of events)
|
|
245
|
+
if ((e.kind === "llm.response" || e.kind === "llm.incomplete") &&
|
|
246
|
+
typeof e.meta.request_seq === "number")
|
|
247
|
+
ends.set(e.meta.request_seq, e);
|
|
248
|
+
for (const e of events.filter((x) => x.kind === "llm.request")) {
|
|
249
|
+
const end = ends.get(e.seq);
|
|
250
|
+
const usage = end?.meta.usage && typeof end.meta.usage === "object"
|
|
251
|
+
? tokensOf(end.meta.usage)
|
|
252
|
+
: null;
|
|
253
|
+
const status = end?.meta.status;
|
|
254
|
+
const model = typeof end?.meta.model === "string" ? end.meta.model : undefined;
|
|
255
|
+
const provider = typeof e.meta.provider === "string" ? e.meta.provider : undefined;
|
|
256
|
+
const reason = String(end?.meta.reason ?? "incomplete");
|
|
257
|
+
const fail = !end
|
|
258
|
+
? failure("incomplete", "no response recorded")
|
|
259
|
+
: end.kind === "llm.incomplete"
|
|
260
|
+
? failure(reason, reason)
|
|
261
|
+
: typeof status === "number" && status >= 400
|
|
262
|
+
? failure(String(status), `status ${status}`)
|
|
263
|
+
: null;
|
|
264
|
+
spans.push({
|
|
265
|
+
traceId,
|
|
266
|
+
spanId: spanId(e.seq),
|
|
267
|
+
parentSpanId: root,
|
|
268
|
+
name: model ? `chat ${model}` : "chat",
|
|
269
|
+
kind: 3,
|
|
270
|
+
startTimeUnixNano: nanos(e.ts),
|
|
271
|
+
endTimeUnixNano: nanos((end ?? e).ts),
|
|
272
|
+
attributes: [
|
|
273
|
+
...str("gen_ai.operation.name", "chat"),
|
|
274
|
+
...str("gen_ai.provider.name", provider ? (PROVIDER_NAMES[provider] ?? provider) : undefined),
|
|
275
|
+
...str("gen_ai.response.model", model),
|
|
276
|
+
...str("gen_ai.response.id", end?.meta.message_id ?? end?.meta.response_id),
|
|
277
|
+
...strs("gen_ai.response.finish_reasons", end?.meta.stop_reason ?? end?.meta.finish_reason),
|
|
278
|
+
...(usage
|
|
279
|
+
? [
|
|
280
|
+
...int("gen_ai.usage.input_tokens", usage.input + usage.cache_read + usage.cache_write),
|
|
281
|
+
...int("gen_ai.usage.output_tokens", usage.output),
|
|
282
|
+
...(usage.cache_read
|
|
283
|
+
? int("gen_ai.usage.cache_read.input_tokens", usage.cache_read)
|
|
284
|
+
: []),
|
|
285
|
+
...(usage.cache_write
|
|
286
|
+
? int("gen_ai.usage.cache_write.input_tokens", usage.cache_write)
|
|
287
|
+
: []),
|
|
288
|
+
]
|
|
289
|
+
: []),
|
|
290
|
+
...conversation,
|
|
291
|
+
...str("blackbox.provider", provider),
|
|
292
|
+
...str("blackbox.region", e.meta.region),
|
|
293
|
+
...int("http.response.status_code", status),
|
|
294
|
+
...(fail ? fail.attrs : []),
|
|
295
|
+
...int("blackbox.seq", e.seq),
|
|
296
|
+
],
|
|
297
|
+
...(fail ? fail.status : {}),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// MCP and broker calls, closed by their result (call_seq). MCP spans follow the MCP conventions.
|
|
301
|
+
const results = new Map();
|
|
302
|
+
for (const e of events)
|
|
303
|
+
if (e.kind === "tool.result" && typeof e.meta.call_seq === "number")
|
|
304
|
+
results.set(e.meta.call_seq, e);
|
|
305
|
+
for (const e of events.filter((x) => x.kind === "tool.call")) {
|
|
306
|
+
const end = results.get(e.seq);
|
|
307
|
+
const broker = e.meta.via === "broker";
|
|
308
|
+
const method = typeof e.meta.method === "string" ? e.meta.method : undefined;
|
|
309
|
+
const tool = typeof e.meta.tool === "string" ? e.meta.tool : undefined;
|
|
310
|
+
const isCall = broker || method === "tools/call";
|
|
311
|
+
const fail = e.meta.policy !== undefined
|
|
312
|
+
? failure("policy_denied", "denied by policy")
|
|
313
|
+
: end?.meta.rpc_error !== undefined
|
|
314
|
+
? failure(String(end.meta.rpc_error), "rpc error")
|
|
315
|
+
: end?.meta.is_error === true
|
|
316
|
+
? failure("tool_error", "tool error")
|
|
317
|
+
: broker && typeof end?.meta.status === "number" && end.meta.status >= 400
|
|
318
|
+
? failure(String(end.meta.status), `status ${end.meta.status}`)
|
|
319
|
+
: null;
|
|
320
|
+
const rpcId = e.meta.rpc_id;
|
|
321
|
+
spans.push({
|
|
322
|
+
traceId,
|
|
323
|
+
spanId: spanId(e.seq),
|
|
324
|
+
parentSpanId: root,
|
|
325
|
+
name: broker
|
|
326
|
+
? `execute_tool ${tool ?? "api"}`
|
|
327
|
+
: `${method ?? "mcp"}${tool && isCall ? ` ${tool}` : ""}`,
|
|
328
|
+
kind: 3,
|
|
329
|
+
startTimeUnixNano: nanos(e.ts),
|
|
330
|
+
endTimeUnixNano: nanos((end ?? e).ts),
|
|
331
|
+
attributes: [
|
|
332
|
+
...(isCall ? str("gen_ai.operation.name", "execute_tool") : []),
|
|
333
|
+
...str("gen_ai.tool.name", tool),
|
|
334
|
+
...(broker ? str("gen_ai.tool.type", "extension") : []),
|
|
335
|
+
...(broker ? [] : str("mcp.method.name", method)),
|
|
336
|
+
...(broker ? [] : str("mcp.protocol.version", e.meta.protocol_version)),
|
|
337
|
+
...(typeof rpcId === "string" || typeof rpcId === "number"
|
|
338
|
+
? str("jsonrpc.request.id", String(rpcId))
|
|
339
|
+
: []),
|
|
340
|
+
...(broker ? str("http.request.method", e.meta.method) : []),
|
|
341
|
+
...(broker ? str("url.path", e.meta.path) : []),
|
|
342
|
+
...str("blackbox.mcp_server", e.meta.server),
|
|
343
|
+
...int("rpc.response.status_code", end?.meta.rpc_error),
|
|
344
|
+
...conversation,
|
|
345
|
+
...(fail ? fail.attrs : []),
|
|
346
|
+
...int("blackbox.seq", e.seq),
|
|
347
|
+
],
|
|
348
|
+
...(fail ? fail.status : {}),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// The agent's own tools (SDK events): INTERNAL spans, from a call to the next result of its name.
|
|
352
|
+
const open = new Map();
|
|
353
|
+
const sdk = (e, type) => e.kind === "sdk.event" && e.meta.type === type;
|
|
354
|
+
for (const e of events) {
|
|
355
|
+
const name = typeof e.meta.name === "string" ? e.meta.name : undefined;
|
|
356
|
+
if (!name)
|
|
357
|
+
continue;
|
|
358
|
+
if (sdk(e, "tool.call"))
|
|
359
|
+
open.set(name, [...(open.get(name) ?? []), e]);
|
|
360
|
+
if (!sdk(e, "tool.result"))
|
|
361
|
+
continue;
|
|
362
|
+
const call = open.get(name)?.shift();
|
|
363
|
+
spans.push({
|
|
364
|
+
traceId,
|
|
365
|
+
spanId: spanId(e.seq),
|
|
366
|
+
parentSpanId: root,
|
|
367
|
+
name: `execute_tool ${name}`,
|
|
368
|
+
kind: 1,
|
|
369
|
+
startTimeUnixNano: nanos((call ?? e).ts),
|
|
370
|
+
endTimeUnixNano: nanos(e.ts),
|
|
371
|
+
attributes: [
|
|
372
|
+
...str("gen_ai.operation.name", "execute_tool"),
|
|
373
|
+
...str("gen_ai.tool.name", name),
|
|
374
|
+
...str("gen_ai.tool.type", "function"),
|
|
375
|
+
...conversation,
|
|
376
|
+
...str("blackbox.reported_by", "agent"),
|
|
377
|
+
...int("blackbox.seq", e.seq),
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
resourceSpans: [
|
|
383
|
+
{
|
|
384
|
+
resource: {
|
|
385
|
+
attributes: [
|
|
386
|
+
...str("service.name", "zanii-blackbox"),
|
|
387
|
+
...str("blackbox.session_id", session),
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
scopeSpans: [
|
|
391
|
+
{
|
|
392
|
+
scope: { name: "zanii-blackbox", version: "2" },
|
|
393
|
+
schemaUrl: "https://opentelemetry.io/schemas/1.44.0",
|
|
394
|
+
spans,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
}
|
package/dist/packs/index.js
CHANGED
|
@@ -16,6 +16,13 @@ const FACTS = new Set([
|
|
|
16
16
|
"resumes_with_note",
|
|
17
17
|
"outcomes",
|
|
18
18
|
"data_region",
|
|
19
|
+
// spec/compliance.md §1a (H6)
|
|
20
|
+
"model_responses",
|
|
21
|
+
"hallucination_findings",
|
|
22
|
+
"grounding_checks",
|
|
23
|
+
"judge_checks",
|
|
24
|
+
"hallucination_controls",
|
|
25
|
+
"detector_accuracy",
|
|
19
26
|
]);
|
|
20
27
|
const FILLS = new Set(["record", "findings", "redactions", "operator"]);
|
|
21
28
|
const PACK_ID = /^[a-z0-9-]{1,64}$/;
|
|
@@ -63,8 +70,8 @@ function checkPart(part, kind) {
|
|
|
63
70
|
if (!isObj(fw) || !text(fw.title, 500))
|
|
64
71
|
return `${at}.title must be 1-500 characters`;
|
|
65
72
|
if (kind === "compliance") {
|
|
66
|
-
if (!only(fw, ["title", "sections"]))
|
|
67
|
-
return `${at} may only have title and
|
|
73
|
+
if (!only(fw, ["title", "sections", "ar"]))
|
|
74
|
+
return `${at} may only have title, sections and ar`;
|
|
68
75
|
const bad = checkSections(fw.sections, 50, at, (s, sat) => {
|
|
69
76
|
if (!only(s, ["id", "title", "requirement", "facts"]))
|
|
70
77
|
return `${sat} may only have id, title, requirement, facts`;
|
|
@@ -79,10 +86,15 @@ function checkPart(part, kind) {
|
|
|
79
86
|
});
|
|
80
87
|
if (bad)
|
|
81
88
|
return bad;
|
|
89
|
+
if (fw.ar !== undefined) {
|
|
90
|
+
const badAr = checkComplianceArabic(fw.ar, fw, `${at}.ar`);
|
|
91
|
+
if (badAr)
|
|
92
|
+
return badAr;
|
|
93
|
+
}
|
|
82
94
|
}
|
|
83
95
|
else {
|
|
84
|
-
if (!only(fw, ["title", "authority", "deadlines", "sections", "ar"]))
|
|
85
|
-
return `${at} may only have title, authority, deadlines, sections`;
|
|
96
|
+
if (!only(fw, ["title", "authority", "deadlines", "clock_hours", "sections", "ar"]))
|
|
97
|
+
return `${at} may only have title, authority, deadlines, clock_hours, sections`;
|
|
86
98
|
if (!text(fw.authority, 500))
|
|
87
99
|
return `${at}.authority must be 1-500 characters`;
|
|
88
100
|
if (!Array.isArray(fw.deadlines) ||
|
|
@@ -90,6 +102,13 @@ function checkPart(part, kind) {
|
|
|
90
102
|
fw.deadlines.length > 10 ||
|
|
91
103
|
!fw.deadlines.every((d) => text(d, 500)))
|
|
92
104
|
return `${at}.deadlines must hold 1-10 texts of 1-500 characters`;
|
|
105
|
+
// spec/packs.md §1: each deadline's hours after awareness, or null when it runs from another event
|
|
106
|
+
if (fw.clock_hours !== undefined &&
|
|
107
|
+
!(Array.isArray(fw.clock_hours) &&
|
|
108
|
+
fw.clock_hours.length === fw.deadlines.length &&
|
|
109
|
+
fw.clock_hours.every((h) => h === null ||
|
|
110
|
+
(Number.isSafeInteger(h) && h >= 1 && h <= 8760))))
|
|
111
|
+
return `${at}.clock_hours must give each deadline 1-8760 hours, or null`;
|
|
93
112
|
const bad = checkSections(fw.sections, 30, at, (s, sat) => {
|
|
94
113
|
if (!only(s, ["id", "title", "fill"]))
|
|
95
114
|
return `${sat} may only have id, title, fill`;
|
|
@@ -108,6 +127,27 @@ function checkPart(part, kind) {
|
|
|
108
127
|
}
|
|
109
128
|
return undefined;
|
|
110
129
|
}
|
|
130
|
+
/** spec/packs.md §1: a compliance framework's Arabic: its title, and each section's title and
|
|
131
|
+
* requirement, by id. */
|
|
132
|
+
function checkComplianceArabic(ar, fw, at) {
|
|
133
|
+
if (!isObj(ar) || !only(ar, ["title", "sections"]))
|
|
134
|
+
return `${at} must be {title, sections}`;
|
|
135
|
+
if (!text(ar.title, 500))
|
|
136
|
+
return `${at}.title must be 1-500 characters`;
|
|
137
|
+
const ids = fw.sections.map((s) => s.id);
|
|
138
|
+
const secs = ar.sections;
|
|
139
|
+
if (!isObj(secs) ||
|
|
140
|
+
Object.keys(secs).length !== ids.length ||
|
|
141
|
+
!ids.every((id) => {
|
|
142
|
+
const s = secs[id];
|
|
143
|
+
return (isObj(s) &&
|
|
144
|
+
only(s, ["title", "requirement"]) &&
|
|
145
|
+
text(s.title, 500) &&
|
|
146
|
+
text(s.requirement, 2000));
|
|
147
|
+
}))
|
|
148
|
+
return `${at}.sections must translate each section's title and requirement, by id`;
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
111
151
|
/** spec/packs.md §1: an occurrence framework's Arabic: the same deadlines and sections, translated. */
|
|
112
152
|
function checkArabic(ar, fw, at) {
|
|
113
153
|
if (!isObj(ar) || !only(ar, ["title", "authority", "deadlines", "sections"]))
|
package/dist/policy/delta.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
// in doubt, it adds power. Mirrors policy/delta.py.
|
|
4
4
|
/** How much a rule holds back: an enforced allow opens (0), an audit rule does nothing (1), a hold
|
|
5
5
|
* (2) and a deny (3) close. */
|
|
6
|
-
const rank = (r) => r.audit === true
|
|
6
|
+
const rank = (r) => r.audit === true || r.action === "require_proof"
|
|
7
|
+
? 1
|
|
8
|
+
: r.action === "allow"
|
|
9
|
+
? 0
|
|
10
|
+
: r.action === "require_approval"
|
|
11
|
+
? 2
|
|
12
|
+
: 3;
|
|
7
13
|
const matcher = (r) => JSON.stringify([r.tool, r.args_match ?? null, r.ignore_case === true]);
|
|
8
14
|
export function policyDelta(current, proposed) {
|
|
9
15
|
const before = new Map(current.rules.map((r) => [r.id, r]));
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -6,12 +6,42 @@ export interface Rule {
|
|
|
6
6
|
tool: string;
|
|
7
7
|
args_match?: string;
|
|
8
8
|
ignore_case?: boolean;
|
|
9
|
-
/** spec/approvals.md: `require_approval` holds the call for a second person.
|
|
10
|
-
|
|
9
|
+
/** spec/approvals.md: `require_approval` holds the call for a second person; spec/policy.md
|
|
10
|
+
* §9: `require_proof` lets it run, and its result must prove itself. */
|
|
11
|
+
action: "deny" | "allow" | "require_approval" | "require_proof";
|
|
12
|
+
/** spec/policy.md §9: with `require_proof`, the system's own id in the result (`refund_id`). */
|
|
13
|
+
field?: string;
|
|
11
14
|
reason?: string;
|
|
12
15
|
/** N3 (idea C5): record-only. Never decides a call; what it would do is recorded (POLICY_AUDIT). */
|
|
13
16
|
audit?: boolean;
|
|
17
|
+
/** spec/policy.md §1: MCP tool annotations the call must have (from the server's `tools/list`). */
|
|
18
|
+
hints?: Partial<ToolHints>;
|
|
19
|
+
/** spec/policy.md §1: the session environments the rule holds in (a session's `environment`). */
|
|
20
|
+
environment?: string[];
|
|
14
21
|
}
|
|
22
|
+
/** spec/api.md: a session's environment, e.g. `production`, `staging`, `dev`. */
|
|
23
|
+
export declare const ENVIRONMENT: RegExp;
|
|
24
|
+
/** MCP tool annotations as booleans, with the spec's defaults for what a server leaves out. */
|
|
25
|
+
export interface ToolHints {
|
|
26
|
+
read_only: boolean;
|
|
27
|
+
destructive: boolean;
|
|
28
|
+
idempotent: boolean;
|
|
29
|
+
open_world: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* spec/policy.md §1: a tool's annotations (MCP `readOnlyHint`, `destructiveHint`, `idempotentHint`,
|
|
33
|
+
* `openWorldHint`) as hints. Missing ones take MCP's defaults: not read-only, destructive, not
|
|
34
|
+
* idempotent, open-world; destructive and idempotent only mean something for a tool that writes.
|
|
35
|
+
* An unknown tool gets the defaults too. Hints are the server's word, not proof.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mcpToolHints(annotations?: unknown): ToolHints;
|
|
38
|
+
/**
|
|
39
|
+
* spec/policy.md §1a: a tool definition's identity, `sha256:<hex>` of the canonical JSON of its
|
|
40
|
+
* name, title, description, schemas and annotations (the fields present). A changed description
|
|
41
|
+
* or schema changes it: what the agent was shown is no longer what was approved. `null` for
|
|
42
|
+
* something that isn't a named tool.
|
|
43
|
+
*/
|
|
44
|
+
export declare function toolDefinitionHash(tool: unknown): string | null;
|
|
15
45
|
export interface Policy {
|
|
16
46
|
version: 1;
|
|
17
47
|
rules: Rule[];
|
|
@@ -20,7 +50,7 @@ export interface Policy {
|
|
|
20
50
|
}
|
|
21
51
|
export interface Decision {
|
|
22
52
|
rule: string;
|
|
23
|
-
action: "deny" | "allow" | "require_approval";
|
|
53
|
+
action: "deny" | "allow" | "require_approval" | "require_proof";
|
|
24
54
|
reason?: string;
|
|
25
55
|
}
|
|
26
56
|
interface Compiled {
|
|
@@ -31,11 +61,15 @@ interface Compiled {
|
|
|
31
61
|
/** Parses a policy file; throws on anything invalid (the server refuses to start). */
|
|
32
62
|
export declare function loadPolicy(bytes: Uint8Array): Policy;
|
|
33
63
|
export declare function compilePolicy(policy: Policy): Compiled[];
|
|
34
|
-
/**
|
|
35
|
-
export declare function
|
|
64
|
+
/** spec/api.md: the session's environment, from its `session.open` (the first line). */
|
|
65
|
+
export declare function environmentOf(lines: readonly string[]): string | undefined;
|
|
66
|
+
/** The first rule that matches this call, or null (allowed). `names`: the tool's names (an MCP tool
|
|
67
|
+
* has two). `hints`: an MCP tool's annotations; a rule with `hints` never matches a call without.
|
|
68
|
+
* `environment`: the session's; a rule with `environment` never matches a session without one. */
|
|
69
|
+
export declare function decide(compiled: readonly Compiled[], names: readonly string[], args: unknown, hints?: ToolHints, environment?: string): Decision | null;
|
|
36
70
|
export type CompiledPolicy = ReturnType<typeof compilePolicy>;
|
|
37
71
|
/** 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<{
|
|
72
|
+
export declare function audited(compiled: readonly Compiled[], names: readonly string[], args: unknown, hints?: ToolHints, environment?: string): Array<{
|
|
39
73
|
rule: string;
|
|
40
74
|
would: Rule["action"];
|
|
41
75
|
}>;
|