@zanii/blackbox 0.21.1 → 0.22.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/actions/index.d.ts +29 -0
- package/dist/actions/index.js +114 -0
- package/dist/admission/index.d.ts +30 -0
- package/dist/admission/index.js +106 -0
- package/dist/cli.js +27 -0
- package/dist/cost/index.d.ts +7 -0
- package/dist/cost/index.js +55 -12
- package/dist/evidence/index.d.ts +9 -0
- package/dist/evidence/index.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/reconcile/record.d.ts +2 -0
- package/dist/reconcile/record.js +47 -1
- package/dist/verify/chain.d.ts +4 -0
- package/dist/verify/chain.js +4 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Bodies, type Call } from "../reconcile/record.ts";
|
|
2
|
+
export interface Reasoning {
|
|
3
|
+
/** `text` (the model's own words), `summary` (the provider's), `encrypted` (there, unreadable) or `none`. */
|
|
4
|
+
kind: "text" | "summary" | "encrypted" | "none";
|
|
5
|
+
text: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface Action {
|
|
8
|
+
seq: number;
|
|
9
|
+
request_seq: number;
|
|
10
|
+
model: string | null;
|
|
11
|
+
tool: string;
|
|
12
|
+
id: string | null;
|
|
13
|
+
args: unknown;
|
|
14
|
+
reasoning: Reasoning;
|
|
15
|
+
/** What the model wrote before the action, in the same answer. */
|
|
16
|
+
said: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** The tool calls each model answer asked for, in order. Each takes the reasoning and text that came
|
|
19
|
+
* before it in the same answer, since the action before it; one with none of its own (a second call
|
|
20
|
+
* in a row) takes the answer's latest. */
|
|
21
|
+
export declare function actionsFromCalls(calls: readonly Call[]): Action[];
|
|
22
|
+
/** The actions in a verified record, each with the reasoning before it (spec/replay.md §6). */
|
|
23
|
+
export declare function actionsOf(lines: readonly string[], bodies: Bodies): Action[];
|
|
24
|
+
/** The actions for a reader: the first `limit`, each reasoning and `said` cut to `textMax` characters. */
|
|
25
|
+
export declare function actionsPage(actions: readonly Action[], limit?: number, textMax?: number): {
|
|
26
|
+
total: number;
|
|
27
|
+
capped: boolean;
|
|
28
|
+
actions: Action[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Each action an agent took, with the reasoning the model gave before it (spec/replay.md §6): the
|
|
2
|
+
// thinking (Anthropic `thinking`, Gemini thoughts, a Chat Completions `reasoning_content`), the
|
|
3
|
+
// provider's reasoning summary (OpenAI Responses), or a note that it was there but encrypted. Pure;
|
|
4
|
+
// mirrors sdks/python/src/zanii_blackbox/actions.py; pinned by spec/vectors/actions.json.
|
|
5
|
+
import { callsOf } from "../reconcile/record.js";
|
|
6
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7
|
+
/** Text parts win over a summary, either over encrypted. */
|
|
8
|
+
function reasoningOf(parts) {
|
|
9
|
+
for (const kind of ["text", "summary"]) {
|
|
10
|
+
const texts = parts.filter(([k, t]) => k === kind && t).map(([, t]) => t);
|
|
11
|
+
if (texts.length)
|
|
12
|
+
return { kind, text: texts.join("\n\n") };
|
|
13
|
+
}
|
|
14
|
+
if (parts.some(([k]) => k === "encrypted"))
|
|
15
|
+
return { kind: "encrypted", text: null };
|
|
16
|
+
return { kind: "none", text: null };
|
|
17
|
+
}
|
|
18
|
+
const action = (c, tool, id, args, why, said) => ({
|
|
19
|
+
seq: c.endSeq,
|
|
20
|
+
request_seq: c.requestSeq,
|
|
21
|
+
model: c.model,
|
|
22
|
+
tool,
|
|
23
|
+
id: typeof id === "string" ? id : null,
|
|
24
|
+
args: args ?? {},
|
|
25
|
+
reasoning: reasoningOf(why),
|
|
26
|
+
said: said.filter(Boolean).join("\n\n") || null,
|
|
27
|
+
});
|
|
28
|
+
/** The tool calls each model answer asked for, in order. Each takes the reasoning and text that came
|
|
29
|
+
* before it in the same answer, since the action before it; one with none of its own (a second call
|
|
30
|
+
* in a row) takes the answer's latest. */
|
|
31
|
+
export function actionsFromCalls(calls) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const c of [...calls].sort((a, b) => a.endSeq - b.endSeq)) {
|
|
34
|
+
let why = c.reasoning ? [["text", c.reasoning]] : [];
|
|
35
|
+
let said = [];
|
|
36
|
+
let last = [[], []];
|
|
37
|
+
for (const [, b] of [...c.blocks.entries()].sort(([x], [y]) => x - y)) {
|
|
38
|
+
if (b.type === "thinking")
|
|
39
|
+
why.push(typeof b.thinking === "string" && b.thinking ? ["text", b.thinking] : ["encrypted", ""]);
|
|
40
|
+
else if (b.type === "redacted_thinking")
|
|
41
|
+
why.push(["encrypted", ""]);
|
|
42
|
+
else if (b.type === "text" && typeof b.text === "string")
|
|
43
|
+
said.push(b.text);
|
|
44
|
+
else if (b.type === "tool_use" && typeof b.name === "string") {
|
|
45
|
+
if (why.length || said.length)
|
|
46
|
+
last = [why, said];
|
|
47
|
+
out.push(action(c, b.name, b.id, b.input, ...last));
|
|
48
|
+
why = [];
|
|
49
|
+
said = [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const item of c.items) {
|
|
53
|
+
const t = item.type;
|
|
54
|
+
if (t === "reasoning") {
|
|
55
|
+
const texts = (Array.isArray(item.summary) ? item.summary : [])
|
|
56
|
+
.filter(isObj)
|
|
57
|
+
.map((s) => String(s.text ?? ""));
|
|
58
|
+
const own = (Array.isArray(item.content) ? item.content : [])
|
|
59
|
+
.filter(isObj)
|
|
60
|
+
.map((s) => String(s.text ?? ""));
|
|
61
|
+
if (own.some(Boolean))
|
|
62
|
+
why.push(...own.map((x) => ["text", x]));
|
|
63
|
+
else if (texts.some(Boolean))
|
|
64
|
+
why.push(...texts.map((x) => ["summary", x]));
|
|
65
|
+
else
|
|
66
|
+
why.push(["encrypted", ""]);
|
|
67
|
+
}
|
|
68
|
+
else if (t === "message") {
|
|
69
|
+
for (const s of Array.isArray(item.content) ? item.content : [])
|
|
70
|
+
if (isObj(s) && typeof s.text === "string")
|
|
71
|
+
said.push(s.text);
|
|
72
|
+
}
|
|
73
|
+
else if ((t === "function_call" || t === "custom_tool_call") &&
|
|
74
|
+
typeof item.name === "string") {
|
|
75
|
+
let args = t === "function_call" ? item.arguments : item.input;
|
|
76
|
+
if (typeof args === "string" && t === "function_call") {
|
|
77
|
+
try {
|
|
78
|
+
args = JSON.parse(args);
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
if (why.length || said.length)
|
|
83
|
+
last = [why, said];
|
|
84
|
+
out.push(action(c, item.name, item.call_id, args, ...last));
|
|
85
|
+
why = [];
|
|
86
|
+
said = [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** The actions in a verified record, each with the reasoning before it (spec/replay.md §6). */
|
|
93
|
+
export function actionsOf(lines, bodies) {
|
|
94
|
+
return actionsFromCalls(callsOf(lines, bodies));
|
|
95
|
+
}
|
|
96
|
+
/** Cut to `most` code points (as Python counts them), then `…`. */
|
|
97
|
+
function cut(text, most) {
|
|
98
|
+
if (text === null)
|
|
99
|
+
return null;
|
|
100
|
+
const points = Array.from(text);
|
|
101
|
+
return points.length <= most ? text : `${points.slice(0, most).join("")}…`;
|
|
102
|
+
}
|
|
103
|
+
/** The actions for a reader: the first `limit`, each reasoning and `said` cut to `textMax` characters. */
|
|
104
|
+
export function actionsPage(actions, limit = 1000, textMax = 8000) {
|
|
105
|
+
return {
|
|
106
|
+
total: actions.length,
|
|
107
|
+
capped: actions.length > limit,
|
|
108
|
+
actions: actions.slice(0, limit).map((a) => ({
|
|
109
|
+
...a,
|
|
110
|
+
reasoning: { ...a.reasoning, text: cut(a.reasoning.text, textMax) },
|
|
111
|
+
said: cut(a.said, textMax),
|
|
112
|
+
})),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const REGISTER_TYPE = "blackbox.service-register";
|
|
2
|
+
export declare const ACCREDITATION_TYPE = "blackbox.accreditation";
|
|
3
|
+
export declare const SERVICE_ID: RegExp;
|
|
4
|
+
type Doc = Record<string, unknown>;
|
|
5
|
+
export interface ServiceRow {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
name_ar?: string;
|
|
9
|
+
tier: number;
|
|
10
|
+
}
|
|
11
|
+
/** `sha256:` + hex of the RFC 8785 JSON of the whole signed document. */
|
|
12
|
+
export declare const docHash: (doc: Doc) => string;
|
|
13
|
+
/** §1: null when the register is well formed and signed by one of `signers`, else why not. */
|
|
14
|
+
export declare function checkRegister(doc: unknown, signers: readonly string[]): string | null;
|
|
15
|
+
/** §1: a service's tier and its row; an unknown service (or none) gets `default_tier`. */
|
|
16
|
+
export declare function serviceTier(register: Doc, serviceId: string | null | undefined): {
|
|
17
|
+
tier: number;
|
|
18
|
+
service: ServiceRow | null;
|
|
19
|
+
};
|
|
20
|
+
export type AccreditationRefusal = "malformed" | "unsigned" | "wrong_issuer" | "revoked" | "not_yet" | "expired" | "wrong_vendor" | "agent_not_listed" | "tier_too_high";
|
|
21
|
+
/** §2: null when `cert` lets this vendor's agent open a session for a service of `tier` now, else
|
|
22
|
+
* the reason. */
|
|
23
|
+
export declare function checkAccreditation(cert: unknown, issuers: readonly string[], opts: {
|
|
24
|
+
now: string;
|
|
25
|
+
vendor?: string | null;
|
|
26
|
+
agentId?: string | null;
|
|
27
|
+
tier?: number | null;
|
|
28
|
+
revoked?: readonly string[];
|
|
29
|
+
}): AccreditationRefusal | null;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Who and what may open a session (spec/admission.md): the signed service risk register (each
|
|
2
|
+
// service's tier: 1 allowed, 2 a person approves each action, 3 off limits) and a vendor's signed
|
|
3
|
+
// accreditation. Both are signed like certificates (spec/fleet.md §5) and checked offline. Pure;
|
|
4
|
+
// mirrors sdks/python/src/zanii_blackbox/admission.py; pinned by spec/vectors/admission.json.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { verifyCertificate } from "../fleet/index.js";
|
|
7
|
+
import { canonical } from "../reconcile/shared.js";
|
|
8
|
+
export const REGISTER_TYPE = "blackbox.service-register";
|
|
9
|
+
export const ACCREDITATION_TYPE = "blackbox.accreditation";
|
|
10
|
+
export const SERVICE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
11
|
+
const VENDOR_ID = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
12
|
+
const AGENT_ID = /^(?:[A-Za-z0-9._:@/-]{1,128}|\*)$/;
|
|
13
|
+
const MAX_SERVICES = 10_000;
|
|
14
|
+
const UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z$/;
|
|
15
|
+
/** `sha256:` + hex of the RFC 8785 JSON of the whole signed document. */
|
|
16
|
+
export const docHash = (doc) => `sha256:${createHash("sha256").update(canonical(doc)).digest("hex")}`;
|
|
17
|
+
const isTier = (x) => Number.isInteger(x) && x >= 1 && x <= 3;
|
|
18
|
+
const when = (x) => {
|
|
19
|
+
if (typeof x !== "string" || !UTC.test(x))
|
|
20
|
+
return null;
|
|
21
|
+
const t = Date.parse(x);
|
|
22
|
+
return Number.isNaN(t) ? null : t;
|
|
23
|
+
};
|
|
24
|
+
const isObject = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
25
|
+
/** §1: null when the register is well formed and signed by one of `signers`, else why not. */
|
|
26
|
+
export function checkRegister(doc, signers) {
|
|
27
|
+
if (!isObject(doc))
|
|
28
|
+
return "the register must be a JSON object";
|
|
29
|
+
if (doc.v !== 1 || doc.type !== REGISTER_TYPE)
|
|
30
|
+
return `not a ${REGISTER_TYPE} v1`;
|
|
31
|
+
if (typeof doc.name !== "string" || !doc.name)
|
|
32
|
+
return "name is required";
|
|
33
|
+
if (!isTier(doc.default_tier))
|
|
34
|
+
return "default_tier must be 1, 2 or 3";
|
|
35
|
+
const services = doc.services;
|
|
36
|
+
if (!Array.isArray(services) || services.length > MAX_SERVICES)
|
|
37
|
+
return `services must be a list of at most ${MAX_SERVICES}`;
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const [i, s] of services.entries()) {
|
|
40
|
+
if (!isObject(s))
|
|
41
|
+
return `services[${i}] must be an object`;
|
|
42
|
+
if (typeof s.id !== "string" || !SERVICE_ID.test(s.id))
|
|
43
|
+
return `services[${i}].id must be 1-64 of a-z 0-9 . _ - (like visa-renewal)`;
|
|
44
|
+
if (seen.has(s.id))
|
|
45
|
+
return `services[${i}].id ${s.id} is listed twice`;
|
|
46
|
+
seen.add(s.id);
|
|
47
|
+
if (typeof s.name !== "string" || !s.name)
|
|
48
|
+
return `services[${i}].name is required`;
|
|
49
|
+
if (!isTier(s.tier))
|
|
50
|
+
return `services[${i}].tier must be 1, 2 or 3`;
|
|
51
|
+
}
|
|
52
|
+
const v = verifyCertificate(doc);
|
|
53
|
+
if (!v.ok)
|
|
54
|
+
return `the register's signature doesn't verify (${v.reason})`;
|
|
55
|
+
if (!signers.includes(v.by))
|
|
56
|
+
return `the register is signed by ${v.by}, who isn't a trusted signer`;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** §1: a service's tier and its row; an unknown service (or none) gets `default_tier`. */
|
|
60
|
+
export function serviceTier(register, serviceId) {
|
|
61
|
+
const found = register.services.find((s) => s.id === serviceId);
|
|
62
|
+
return found
|
|
63
|
+
? { tier: found.tier, service: found }
|
|
64
|
+
: { tier: register.default_tier, service: null };
|
|
65
|
+
}
|
|
66
|
+
/** §2: null when `cert` lets this vendor's agent open a session for a service of `tier` now, else
|
|
67
|
+
* the reason. */
|
|
68
|
+
export function checkAccreditation(cert, issuers, opts) {
|
|
69
|
+
if (!isObject(cert))
|
|
70
|
+
return "malformed";
|
|
71
|
+
const agents = cert.agents;
|
|
72
|
+
const issued = when(cert.issued_at);
|
|
73
|
+
const expires = when(cert.expires_at);
|
|
74
|
+
const at = when(opts.now);
|
|
75
|
+
if (cert.v !== 1 ||
|
|
76
|
+
cert.type !== ACCREDITATION_TYPE ||
|
|
77
|
+
typeof cert.vendor !== "string" ||
|
|
78
|
+
!VENDOR_ID.test(cert.vendor) ||
|
|
79
|
+
!Array.isArray(agents) ||
|
|
80
|
+
!agents.length ||
|
|
81
|
+
!agents.every((a) => typeof a === "string" && AGENT_ID.test(a)) ||
|
|
82
|
+
(cert.max_tier !== 1 && cert.max_tier !== 2) ||
|
|
83
|
+
issued === null ||
|
|
84
|
+
expires === null ||
|
|
85
|
+
at === null)
|
|
86
|
+
return "malformed";
|
|
87
|
+
const v = verifyCertificate(cert);
|
|
88
|
+
if (!v.ok)
|
|
89
|
+
return "unsigned";
|
|
90
|
+
if (!issuers.includes(v.by))
|
|
91
|
+
return "wrong_issuer";
|
|
92
|
+
if ((opts.revoked ?? []).includes(docHash(cert)))
|
|
93
|
+
return "revoked";
|
|
94
|
+
if (at < issued)
|
|
95
|
+
return "not_yet";
|
|
96
|
+
if (at >= expires)
|
|
97
|
+
return "expired";
|
|
98
|
+
if (opts.vendor != null && cert.vendor !== opts.vendor)
|
|
99
|
+
return "wrong_vendor";
|
|
100
|
+
const listed = agents;
|
|
101
|
+
if (!listed.includes("*") && (opts.agentId == null || !listed.includes(opts.agentId)))
|
|
102
|
+
return "agent_not_listed";
|
|
103
|
+
if (opts.tier != null && opts.tier > cert.max_tier)
|
|
104
|
+
return "tier_too_high";
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ const USAGE = `usage (spec/cli.md):
|
|
|
43
43
|
blackbox investigate <bundle.json> [--bodies <dir>] [--directives <file>] [--identity <file>] [--proofs] [--out <prefix>]
|
|
44
44
|
blackbox evidence-pack <bundle.json> [--proofs] [--identity <file>] [--lang en|ar|both] [--out <prefix>]
|
|
45
45
|
blackbox sign-constitution <draft.json> --identity <owner.json> [--out <file>]
|
|
46
|
+
blackbox sign <draft.json> --identity <owner.json> [--out <file>] (a service register or an accreditation)
|
|
46
47
|
blackbox policy-from-zanii <zanii-policy.json> [--out <file>]
|
|
47
48
|
blackbox policy-to-zanii <policy.json> [--out <file>]
|
|
48
49
|
blackbox statement --provider anthropic|openai --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--out <file>]
|
|
@@ -111,6 +112,8 @@ async function main(argv) {
|
|
|
111
112
|
return evidencePackCommand(rest);
|
|
112
113
|
if (command === "sign-constitution")
|
|
113
114
|
return signConstitutionCommand(rest);
|
|
115
|
+
if (command === "sign")
|
|
116
|
+
return signCommand(rest);
|
|
114
117
|
if (command === "policy-from-zanii" || command === "policy-to-zanii")
|
|
115
118
|
return policyZaniiCommand(command, rest);
|
|
116
119
|
if (command === "statement")
|
|
@@ -863,6 +866,30 @@ async function signConstitutionCommand(rest) {
|
|
|
863
866
|
return 2;
|
|
864
867
|
}
|
|
865
868
|
}
|
|
869
|
+
/** spec/admission.md §3: signs a draft (a service register, an accreditation) as the identity. */
|
|
870
|
+
async function signCommand(rest) {
|
|
871
|
+
const { flags, positional } = parse(rest, []);
|
|
872
|
+
if (positional.length !== 1 || !flags.identity)
|
|
873
|
+
return usage();
|
|
874
|
+
const { signCertificate } = await import("./fleet/index.js");
|
|
875
|
+
const { docHash } = await import("./admission/index.js");
|
|
876
|
+
try {
|
|
877
|
+
const draft = JSON.parse(readFileSync(positional[0], "utf8"));
|
|
878
|
+
if (!draft || typeof draft !== "object" || Array.isArray(draft))
|
|
879
|
+
throw new Error("the draft must be a JSON object");
|
|
880
|
+
const { signed: _old, ...rest_ } = draft;
|
|
881
|
+
const identity = await readIdentity(flags.identity);
|
|
882
|
+
const doc = signCertificate(rest_, identity.did, identity.seed);
|
|
883
|
+
const out = flags.out || "signed.json";
|
|
884
|
+
writeFileSync(out, `${JSON.stringify(doc, null, 2)}\n`);
|
|
885
|
+
process.stdout.write(`${JSON.stringify({ ok: true, file: out, sha256: docHash(doc), by: identity.did })}\n`);
|
|
886
|
+
return 0;
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
|
|
890
|
+
return 2;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
866
893
|
/** spec/investigations.md §7: the court evidence pack, `<prefix>.json` (signed) and `<prefix>.md`. */
|
|
867
894
|
async function evidencePackCommand(rest) {
|
|
868
895
|
const { flags, positional } = parse(rest, ["--proofs"]);
|
package/dist/cost/index.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface ModelPrice {
|
|
|
17
17
|
/** Audio in and out (else `input` / `output`). */
|
|
18
18
|
input_audio?: number;
|
|
19
19
|
output_audio?: number;
|
|
20
|
+
/** Image tokens in and out (else `input` / `output`): OpenAI's and Gemini's image models. */
|
|
21
|
+
input_image?: number;
|
|
22
|
+
output_image?: number;
|
|
20
23
|
/** µUSD per 1,000 web-search requests (Anthropic's server tool); absent: not priced. */
|
|
21
24
|
web_search_per_1k?: number;
|
|
22
25
|
/** Prices once the call's prompt is over `above` tokens (Gemini's >200k tier), highest first. */
|
|
@@ -56,6 +59,8 @@ export interface Tokens {
|
|
|
56
59
|
cache_write_1h?: number;
|
|
57
60
|
input_audio?: number;
|
|
58
61
|
output_audio?: number;
|
|
62
|
+
input_image?: number;
|
|
63
|
+
output_image?: number;
|
|
59
64
|
}
|
|
60
65
|
export interface CallCost {
|
|
61
66
|
seq: number;
|
|
@@ -74,6 +79,8 @@ export interface CallCost {
|
|
|
74
79
|
/** spec/cost.md §1a: the uplift applied, when one matched. */
|
|
75
80
|
uplift_percent?: number;
|
|
76
81
|
source?: "reported";
|
|
82
|
+
/** The answer carried no usage (`usage_missing`): unpriced, never counted as free. */
|
|
83
|
+
no_usage?: true;
|
|
77
84
|
}
|
|
78
85
|
export interface CostReport {
|
|
79
86
|
v: 1;
|
package/dist/cost/index.js
CHANGED
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
// Mirrors sdks/python/src/zanii_blackbox/cost.py.
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
const CLASSES = ["input", "output", "cache_read", "cache_write"];
|
|
5
|
-
const EXTRA = [
|
|
5
|
+
const EXTRA = [
|
|
6
|
+
"cache_write_1h",
|
|
7
|
+
"input_audio",
|
|
8
|
+
"output_audio",
|
|
9
|
+
"input_image",
|
|
10
|
+
"output_image",
|
|
11
|
+
];
|
|
6
12
|
const OPTIONAL_PRICES = [
|
|
7
13
|
"context_window",
|
|
8
14
|
"cache_write_1h",
|
|
9
15
|
"input_audio",
|
|
10
16
|
"output_audio",
|
|
17
|
+
"input_image",
|
|
18
|
+
"output_image",
|
|
11
19
|
"web_search_per_1k",
|
|
12
20
|
];
|
|
13
21
|
/** Parses and checks a price table; its sha256 is of the exact file bytes. */
|
|
@@ -99,12 +107,20 @@ export function tokensOf(usage) {
|
|
|
99
107
|
const cached = n("cachedContentTokenCount");
|
|
100
108
|
const audioIn = n("promptTokensDetails_AUDIO");
|
|
101
109
|
const audioOut = n("candidatesTokensDetails_AUDIO");
|
|
110
|
+
const imageIn = n("promptTokensDetails_IMAGE");
|
|
111
|
+
const imageOut = n("candidatesTokensDetails_IMAGE");
|
|
102
112
|
return extras({
|
|
103
|
-
input: Math.max(0, n("promptTokenCount") - cached - audioIn) +
|
|
104
|
-
|
|
113
|
+
input: Math.max(0, n("promptTokenCount") - cached - audioIn - imageIn) +
|
|
114
|
+
n("toolUsePromptTokenCount"),
|
|
115
|
+
output: Math.max(0, n("candidatesTokenCount") - audioOut - imageOut) + n("thoughtsTokenCount"),
|
|
105
116
|
cache_read: cached,
|
|
106
117
|
cache_write: 0,
|
|
107
|
-
}, {
|
|
118
|
+
}, {
|
|
119
|
+
input_audio: audioIn,
|
|
120
|
+
output_audio: audioOut,
|
|
121
|
+
input_image: imageIn,
|
|
122
|
+
output_image: imageOut,
|
|
123
|
+
});
|
|
108
124
|
}
|
|
109
125
|
if ("inputTokens" in usage)
|
|
110
126
|
// Bedrock Converse (spec/providers.md §5)
|
|
@@ -130,18 +146,28 @@ export function tokensOf(usage) {
|
|
|
130
146
|
// Anthropic's cache names are its own: they win over the Responses shape, since Anthropic also
|
|
131
147
|
// sends output_tokens_details (thinking_tokens) since Claude Sonnet 5
|
|
132
148
|
const anthropicCache = "cache_read_input_tokens" in usage || "cache_creation_input_tokens" in usage;
|
|
149
|
+
// OpenAI's transcriptions write input_token_details (singular); its images, image_tokens
|
|
133
150
|
if (!anthropicCache &&
|
|
134
|
-
Object.keys(usage).some((k) => k.startsWith("input_tokens_details_") ||
|
|
151
|
+
Object.keys(usage).some((k) => k.startsWith("input_tokens_details_") ||
|
|
152
|
+
k.startsWith("output_tokens_details_") ||
|
|
153
|
+
k.startsWith("input_token_details_"))) {
|
|
135
154
|
const cached = n("input_tokens_details_cached_tokens");
|
|
136
155
|
const written = n("input_tokens_details_cache_write_tokens");
|
|
137
|
-
const audioIn = n("input_tokens_details_audio_tokens");
|
|
156
|
+
const audioIn = n("input_tokens_details_audio_tokens") + n("input_token_details_audio_tokens");
|
|
138
157
|
const audioOut = n("output_tokens_details_audio_tokens");
|
|
158
|
+
const imageIn = n("input_tokens_details_image_tokens");
|
|
159
|
+
const imageOut = n("output_tokens_details_image_tokens");
|
|
139
160
|
return extras({
|
|
140
|
-
input: Math.max(0, n("input_tokens") - cached - written - audioIn),
|
|
141
|
-
output: Math.max(0, n("output_tokens") - audioOut),
|
|
161
|
+
input: Math.max(0, n("input_tokens") - cached - written - audioIn - imageIn),
|
|
162
|
+
output: Math.max(0, n("output_tokens") - audioOut - imageOut),
|
|
142
163
|
cache_read: cached,
|
|
143
164
|
cache_write: written,
|
|
144
|
-
}, {
|
|
165
|
+
}, {
|
|
166
|
+
input_audio: audioIn,
|
|
167
|
+
output_audio: audioOut,
|
|
168
|
+
input_image: imageIn,
|
|
169
|
+
output_image: imageOut,
|
|
170
|
+
});
|
|
145
171
|
}
|
|
146
172
|
// Anthropic: cache_creation_input_tokens is both lifetimes; the 1-hour part is priced apart
|
|
147
173
|
const hour = n("cache_creation_ephemeral_1h_input_tokens");
|
|
@@ -167,7 +193,12 @@ export function requestsOf(usage) {
|
|
|
167
193
|
}
|
|
168
194
|
/** A call's prompt, in tokens: what a context tier is decided on. */
|
|
169
195
|
export function promptTokens(t) {
|
|
170
|
-
return t.input +
|
|
196
|
+
return (t.input +
|
|
197
|
+
t.cache_read +
|
|
198
|
+
t.cache_write +
|
|
199
|
+
(t.cache_write_1h ?? 0) +
|
|
200
|
+
(t.input_audio ?? 0) +
|
|
201
|
+
(t.input_image ?? 0));
|
|
171
202
|
}
|
|
172
203
|
/** The prices a call pays: a context tier when its prompt is over one, then its service tier. */
|
|
173
204
|
export function effectivePrice(price, tokens, serviceTier) {
|
|
@@ -203,6 +234,8 @@ export function callMicroUsd(tokens, price, requests) {
|
|
|
203
234
|
sum += (tokens.cache_write_1h ?? 0) * (price.cache_write_1h ?? price.cache_write);
|
|
204
235
|
sum += (tokens.input_audio ?? 0) * (price.input_audio ?? price.input);
|
|
205
236
|
sum += (tokens.output_audio ?? 0) * (price.output_audio ?? price.output);
|
|
237
|
+
sum += (tokens.input_image ?? 0) * (price.input_image ?? price.input);
|
|
238
|
+
sum += (tokens.output_image ?? 0) * (price.output_image ?? price.output);
|
|
206
239
|
sum += (requests?.web_search ?? 0) * (price.web_search_per_1k ?? 0) * 1000;
|
|
207
240
|
if (!Number.isSafeInteger(sum))
|
|
208
241
|
throw new RangeError("cost overflow: tokens × price beyond 2^53");
|
|
@@ -288,9 +321,19 @@ export function sessionCost(lines, prices) {
|
|
|
288
321
|
if (replayed.has(e.meta.request_seq))
|
|
289
322
|
continue;
|
|
290
323
|
const usage = e.meta.usage;
|
|
291
|
-
if (typeof usage !== "object" || usage === null)
|
|
292
|
-
continue;
|
|
293
324
|
const model = typeof e.meta.model === "string" ? e.meta.model : null;
|
|
325
|
+
if (typeof usage !== "object" || usage === null) {
|
|
326
|
+
// an answer that said nothing of what it cost is unpriced, never silently free
|
|
327
|
+
if (e.meta.usage_missing === true)
|
|
328
|
+
calls.push({
|
|
329
|
+
seq: e.seq,
|
|
330
|
+
model,
|
|
331
|
+
tokens: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
332
|
+
micro_usd: null,
|
|
333
|
+
no_usage: true,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
294
337
|
const priced = priceCall({
|
|
295
338
|
model,
|
|
296
339
|
usage: usage,
|
package/dist/evidence/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { actionsPage } from "../actions/index.ts";
|
|
1
2
|
import type { Bundle, BundleReport } from "../verify/index.ts";
|
|
2
3
|
type Text = {
|
|
3
4
|
en: string;
|
|
@@ -7,7 +8,10 @@ export interface EvidencePackOptions {
|
|
|
7
8
|
generated_at: string;
|
|
8
9
|
bundle_sha256: string;
|
|
9
10
|
proofs: "checked" | "not_checked";
|
|
11
|
+
/** §7a: an `actionsPage`, when the bodies are at hand: each action with the reasoning before it. */
|
|
12
|
+
actions?: ActionsPage;
|
|
10
13
|
}
|
|
14
|
+
type ActionsPage = ReturnType<typeof actionsPage>;
|
|
11
15
|
/** spec/investigations.md §7. */
|
|
12
16
|
export declare function evidencePack(bundle: Bundle, report: BundleReport, opts: EvidencePackOptions): {
|
|
13
17
|
v: 1;
|
|
@@ -53,6 +57,11 @@ export declare function evidencePack(bundle: Bundle, report: BundleReport, opts:
|
|
|
53
57
|
reproduce: string[];
|
|
54
58
|
generated_at: string;
|
|
55
59
|
bundle_sha256: string;
|
|
60
|
+
actions?: {
|
|
61
|
+
total: number;
|
|
62
|
+
capped: boolean;
|
|
63
|
+
actions: import("../actions/index.ts").Action[];
|
|
64
|
+
};
|
|
56
65
|
};
|
|
57
66
|
export type EvidencePack = ReturnType<typeof evidencePack>;
|
|
58
67
|
/** The Markdown copy (spec/investigations.md §7): `en`, `ar` or `both`. */
|
package/dist/evidence/index.js
CHANGED
|
@@ -97,6 +97,7 @@ export function evidencePack(bundle, report, opts) {
|
|
|
97
97
|
reproduce: REPRODUCE,
|
|
98
98
|
generated_at: opts.generated_at,
|
|
99
99
|
bundle_sha256: opts.bundle_sha256,
|
|
100
|
+
...(opts.actions ? { actions: opts.actions } : {}),
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
// U+2067 … U+2069 isolate the Arabic paragraphs, so they read right to left in any viewer.
|
|
@@ -147,5 +148,54 @@ export function renderEvidencePack(pack, lang = "both") {
|
|
|
147
148
|
...pack.reproduce,
|
|
148
149
|
"```",
|
|
149
150
|
"",
|
|
151
|
+
...actionsMd(pack.actions, label),
|
|
150
152
|
].join("\n");
|
|
151
153
|
}
|
|
154
|
+
const KIND = {
|
|
155
|
+
text: ["the model's own reasoning", "تفكير النموذج نفسه"],
|
|
156
|
+
summary: ["the provider's summary of it", "ملخّص المزوّد له"],
|
|
157
|
+
encrypted: ["recorded, but encrypted by the provider", "مسجّل لكنه مشفّر من المزوّد"],
|
|
158
|
+
none: ["none recorded", "لم يُسجَّل شيء"],
|
|
159
|
+
};
|
|
160
|
+
const quote = (text) => text
|
|
161
|
+
.split(/\r?\n|\r/)
|
|
162
|
+
.map((l) => ` > ${l}`)
|
|
163
|
+
.join("\n") || ">";
|
|
164
|
+
/** Python's json.dumps(…, ensure_ascii=False, sort_keys=True): keys sorted, ", " and ": ". */
|
|
165
|
+
function pyJson(v) {
|
|
166
|
+
if (Array.isArray(v))
|
|
167
|
+
return `[${v.map(pyJson).join(", ")}]`;
|
|
168
|
+
if (v && typeof v === "object")
|
|
169
|
+
return `{${Object.keys(v)
|
|
170
|
+
.sort()
|
|
171
|
+
.map((k) => `${JSON.stringify(k)}: ${pyJson(v[k])}`)
|
|
172
|
+
.join(", ")}}`;
|
|
173
|
+
return JSON.stringify(v) ?? "null";
|
|
174
|
+
}
|
|
175
|
+
/** §7a: each action, then the reasoning recorded before it. */
|
|
176
|
+
function actionsMd(page, label) {
|
|
177
|
+
if (!page)
|
|
178
|
+
return [];
|
|
179
|
+
const out = [
|
|
180
|
+
`## ${label("Actions and the reasoning before each", "الإجراءات والتفكير الذي سبق كلًّا منها")}`,
|
|
181
|
+
"",
|
|
182
|
+
];
|
|
183
|
+
if (!page.actions.length)
|
|
184
|
+
out.push(label("No actions recorded.", "لم تُسجَّل أي إجراءات."), "");
|
|
185
|
+
page.actions.forEach((a, i) => {
|
|
186
|
+
const all = pyJson(a.args);
|
|
187
|
+
const points = Array.from(all);
|
|
188
|
+
const args = points.length <= 300 ? all : `${points.slice(0, 300).join("")}…`;
|
|
189
|
+
const [en, ar] = KIND[a.reasoning.kind] ?? KIND.none;
|
|
190
|
+
out.push(`${i + 1}. **\`${a.tool}\`** · ${label("event", "الحدث")} ${a.seq} · \`${args}\``, "", ` ${label("Reasoning", "التفكير")}: ${label(en, ar)}`, "");
|
|
191
|
+
if (a.reasoning.text)
|
|
192
|
+
out.push(quote(a.reasoning.text), "");
|
|
193
|
+
if (a.said)
|
|
194
|
+
out.push(` ${label("It wrote", "كتب")}:`, "", quote(a.said), "");
|
|
195
|
+
});
|
|
196
|
+
if (page.capped) {
|
|
197
|
+
const n = page.actions.length;
|
|
198
|
+
out.push(label(`The first ${n} of ${page.total} actions.`, `أول ${n} من أصل ${page.total} إجراءات.`), "");
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { A2A_STREAMING, type AgentCardReport, a2aMethod, a2aRequestMeta, a2aResponseMeta, a2aRestMethod, a2aRestRequestMeta, a2aRestResponseMeta, agentCardDigest, agentCardPayload, gatewayCardKey, rewriteAgentCard, verifyAgentCard, } from "./a2a/index.ts";
|
|
2
|
+
export { type Action, actionsFromCalls, actionsOf, actionsPage, type Reasoning, } from "./actions/index.ts";
|
|
3
|
+
export { ACCREDITATION_TYPE, type AccreditationRefusal, checkAccreditation, checkRegister, docHash, REGISTER_TYPE, SERVICE_ID, type ServiceRow, serviceTier, } from "./admission/index.ts";
|
|
2
4
|
export { causality, memoryXray } from "./agents/index.ts";
|
|
3
5
|
export { type DetectorOptions, detectors, fingerprint } from "./analysis/detectors.ts";
|
|
4
6
|
export { FAULTS, type Fault, faultOf } from "./analysis/faults.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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
3
|
export { A2A_STREAMING, a2aMethod, a2aRequestMeta, a2aResponseMeta, a2aRestMethod, a2aRestRequestMeta, a2aRestResponseMeta, agentCardDigest, agentCardPayload, gatewayCardKey, rewriteAgentCard, verifyAgentCard, } from "./a2a/index.js";
|
|
4
|
+
export { actionsFromCalls, actionsOf, actionsPage, } from "./actions/index.js";
|
|
5
|
+
export { ACCREDITATION_TYPE, checkAccreditation, checkRegister, docHash, REGISTER_TYPE, SERVICE_ID, serviceTier, } from "./admission/index.js";
|
|
4
6
|
export { causality, memoryXray } from "./agents/index.js";
|
|
5
7
|
export { detectors, fingerprint } from "./analysis/detectors.js";
|
|
6
8
|
export { FAULTS, faultOf } from "./analysis/faults.js";
|
|
@@ -35,6 +35,8 @@ export interface Call {
|
|
|
35
35
|
blocks: Map<number, Block>;
|
|
36
36
|
/** OpenAI Responses output items, in order. */
|
|
37
37
|
items: Obj[];
|
|
38
|
+
/** A Chat Completions message's own reasoning (`reasoning_content` or `reasoning`), when given. */
|
|
39
|
+
reasoning?: string;
|
|
38
40
|
}
|
|
39
41
|
export type Bodies = (hash: string) => Uint8Array | undefined;
|
|
40
42
|
export declare function parseJson(text: string): unknown;
|
package/dist/reconcile/record.js
CHANGED
|
@@ -125,9 +125,13 @@ function rebuild(call, body) {
|
|
|
125
125
|
if (typeof json.id === "string")
|
|
126
126
|
call.messageId ??= json.id;
|
|
127
127
|
const message = json.choices.find(isObj)?.message;
|
|
128
|
-
if (isObj(message))
|
|
128
|
+
if (isObj(message)) {
|
|
129
129
|
chatBlocks(call, message.content, message.tool_calls);
|
|
130
|
+
chatReasoning(call, message);
|
|
131
|
+
}
|
|
130
132
|
}
|
|
133
|
+
if (Array.isArray(json.candidates))
|
|
134
|
+
geminiParts(call, json);
|
|
131
135
|
return;
|
|
132
136
|
}
|
|
133
137
|
const partial = new Map();
|
|
@@ -141,6 +145,10 @@ function rebuild(call, body) {
|
|
|
141
145
|
if (!isObj(event))
|
|
142
146
|
continue;
|
|
143
147
|
const type = event.type;
|
|
148
|
+
if (Array.isArray(event.candidates)) {
|
|
149
|
+
geminiParts(call, event);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
144
152
|
if (Array.isArray(event.choices)) {
|
|
145
153
|
if (typeof event.id === "string")
|
|
146
154
|
call.messageId ??= event.id;
|
|
@@ -149,6 +157,7 @@ function rebuild(call, body) {
|
|
|
149
157
|
continue;
|
|
150
158
|
if (typeof delta.content === "string")
|
|
151
159
|
chatText = (chatText ?? "") + delta.content;
|
|
160
|
+
chatReasoning(call, delta);
|
|
152
161
|
for (const tc of Array.isArray(delta.tool_calls) ? delta.tool_calls.filter(isObj) : []) {
|
|
153
162
|
const k = typeof tc.index === "number" ? tc.index : 0;
|
|
154
163
|
const acc = chatCalls.get(k) ?? { arguments: "" };
|
|
@@ -210,6 +219,43 @@ function rebuild(call, body) {
|
|
|
210
219
|
* its text and tool calls: a text block (when there's text), then one tool_use per tool call, its
|
|
211
220
|
* arguments parsed from their JSON string.
|
|
212
221
|
*/
|
|
222
|
+
/** spec/reconcile.md §2: a Chat Completions message's (or a stream delta's) own reasoning. */
|
|
223
|
+
function chatReasoning(call, m) {
|
|
224
|
+
for (const key of ["reasoning_content", "reasoning"]) {
|
|
225
|
+
const v = m[key];
|
|
226
|
+
if (typeof v === "string" && v) {
|
|
227
|
+
call.reasoning = (call.reasoning ?? "") + v;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** spec/reconcile.md §2: Gemini's parts (a whole answer, or one streamed chunk) as blocks: its
|
|
233
|
+
* thoughts as thinking, its text as text, each functionCall as a tool_use. */
|
|
234
|
+
function geminiParts(call, event) {
|
|
235
|
+
if (typeof event.responseId === "string")
|
|
236
|
+
call.messageId ??= event.responseId;
|
|
237
|
+
const first = event.candidates.find(isObj) ?? {};
|
|
238
|
+
const content = first.content;
|
|
239
|
+
const parts = isObj(content) && Array.isArray(content.parts) ? content.parts.filter(isObj) : [];
|
|
240
|
+
for (const p of parts) {
|
|
241
|
+
const fc = p.functionCall;
|
|
242
|
+
if (isObj(fc) && typeof fc.name === "string") {
|
|
243
|
+
const block = { type: "tool_use", name: fc.name, input: fc.args ?? {} };
|
|
244
|
+
if (typeof fc.id === "string")
|
|
245
|
+
block.id = fc.id;
|
|
246
|
+
call.blocks.set(call.blocks.size, block);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (typeof p.text !== "string")
|
|
250
|
+
continue;
|
|
251
|
+
const kind = p.thought === true ? "thinking" : "text";
|
|
252
|
+
const last = call.blocks.get(call.blocks.size - 1);
|
|
253
|
+
if (last && last.type === kind)
|
|
254
|
+
last[kind] = (last[kind] ?? "") + p.text; // a streamed chunk
|
|
255
|
+
else
|
|
256
|
+
call.blocks.set(call.blocks.size, { type: kind, [kind]: p.text });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
213
259
|
function chatBlocks(call, content, toolCalls) {
|
|
214
260
|
let i = 0;
|
|
215
261
|
if (typeof content === "string" && content !== "")
|
package/dist/verify/chain.d.ts
CHANGED
|
@@ -23,5 +23,9 @@ export interface VerifyOptions {
|
|
|
23
23
|
count: number;
|
|
24
24
|
head: string;
|
|
25
25
|
}>;
|
|
26
|
+
/** A continuation (spec/tower.md §3): the first line's seq, and the event_hash it follows.
|
|
27
|
+
* Anchors need the whole chain. */
|
|
28
|
+
startSeq?: number;
|
|
29
|
+
startPrev?: string;
|
|
26
30
|
}
|
|
27
31
|
export declare function verifyChain(lines: readonly string[], options?: VerifyOptions): VerifyResult;
|
package/dist/verify/chain.js
CHANGED
|
@@ -7,7 +7,8 @@ const SESSION = /^ses_[A-Za-z0-9]{16,64}$/;
|
|
|
7
7
|
const TS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z$/;
|
|
8
8
|
const KINDS = new Set(EVENT_KINDS);
|
|
9
9
|
export function verifyChain(lines, options = {}) {
|
|
10
|
-
let prev = GENESIS;
|
|
10
|
+
let prev = options.startPrev ?? GENESIS;
|
|
11
|
+
const start = options.startSeq ?? 0;
|
|
11
12
|
const hashes = [];
|
|
12
13
|
let session = options.sessionId;
|
|
13
14
|
const fail = (code, index, message) => ({
|
|
@@ -34,8 +35,8 @@ export function verifyChain(lines, options = {}) {
|
|
|
34
35
|
session ??= env.session_id;
|
|
35
36
|
if (env.session_id !== session)
|
|
36
37
|
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}`);
|
|
38
|
+
if (env.seq !== start + i)
|
|
39
|
+
return fail("SEQ_GAP", i, `expected seq ${start + i}, found ${env.seq}`);
|
|
39
40
|
if (env.prev !== prev)
|
|
40
41
|
return fail("BROKEN_LINK", i, "prev does not match the previous event_hash");
|
|
41
42
|
const body = options.body?.(env.body_hash);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.22.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.
|
|
2
|
+
export const VERSION = "0.22.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.
|
|
4
|
+
"version": "0.22.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",
|