@zanii/blackbox 0.21.2 → 0.23.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 +1 -0
- 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/analysis/accepted.d.ts +26 -0
- package/dist/analysis/accepted.js +61 -0
- package/dist/analysis/detectors.js +12 -9
- package/dist/analysis/faults.js +71 -0
- package/dist/analysis/grounding.js +5 -9
- package/dist/analysis/guard.d.ts +72 -0
- package/dist/analysis/guard.js +240 -0
- package/dist/analysis/hallucination.js +2 -2
- package/dist/analysis/index.d.ts +21 -1
- package/dist/analysis/index.js +101 -19
- package/dist/analysis/injection.d.ts +2 -1
- package/dist/analysis/injection.js +16 -16
- package/dist/analysis/landing.js +60 -14
- package/dist/analysis/plan-checks.d.ts +55 -0
- package/dist/analysis/plan-checks.js +748 -0
- package/dist/analysis/taxonomy.js +1 -0
- package/dist/analysis/waste.js +2 -3
- package/dist/approvals/index.d.ts +12 -3
- package/dist/approvals/index.js +36 -10
- package/dist/authority/index.js +3 -3
- package/dist/aviation/index.d.ts +422 -0
- package/dist/aviation/index.js +1014 -0
- package/dist/aviation/qrh.d.ts +4 -0
- package/dist/aviation/qrh.js +171 -0
- package/dist/budgets/index.d.ts +63 -0
- package/dist/budgets/index.js +161 -0
- package/dist/cli.js +199 -24
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.js +26 -2
- package/dist/cost/batch.d.ts +26 -0
- package/dist/cost/batch.js +173 -0
- package/dist/cost/index.d.ts +21 -0
- package/dist/cost/index.js +115 -30
- package/dist/cv/index.js +1 -1
- package/dist/duty/index.js +2 -1
- package/dist/evals/index.d.ts +233 -0
- package/dist/evals/index.js +488 -0
- package/dist/evidence/index.d.ts +9 -0
- package/dist/evidence/index.js +50 -0
- package/dist/federation/index.d.ts +9 -0
- package/dist/federation/index.js +8 -1
- package/dist/finance/index.js +3 -4
- package/dist/fleet/index.d.ts +25 -0
- package/dist/fleet/index.js +57 -17
- package/dist/fleet/insights.js +4 -1
- package/dist/health/index.js +2 -2
- package/dist/hooks/ai-sdk.d.ts +17 -19
- package/dist/hooks/ai-sdk.js +13 -7
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/mastra.d.ts +26 -0
- package/dist/hooks/mastra.js +63 -0
- package/dist/index.d.ts +24 -9
- package/dist/index.js +25 -9
- package/dist/mcp-requests/index.d.ts +26 -0
- package/dist/mcp-requests/index.js +154 -0
- package/dist/mcp-server/index.d.ts +12 -0
- package/dist/mcp-server/index.js +84 -2
- package/dist/mcp-wrap/index.js +8 -4
- package/dist/otlp/index.d.ts +155 -0
- package/dist/otlp/index.js +468 -0
- package/dist/otlp/protobuf.d.ts +7 -0
- package/dist/otlp/protobuf.js +314 -0
- package/dist/packs/index.d.ts +2 -0
- package/dist/packs/index.js +5 -1
- package/dist/pdf/arabic.d.ts +45 -0
- package/dist/pdf/arabic.js +373 -0
- package/dist/pdf/index.d.ts +77 -0
- package/dist/pdf/index.js +673 -0
- package/dist/policy/index.d.ts +0 -4
- package/dist/policy/index.js +13 -5
- package/dist/realtime/index.d.ts +33 -0
- package/dist/realtime/index.js +204 -0
- package/dist/reconcile/record.d.ts +14 -0
- package/dist/reconcile/record.js +161 -66
- package/dist/session/index.d.ts +43 -0
- package/dist/session/index.js +83 -0
- package/dist/undo/index.js +3 -3
- package/dist/verify/anchor.d.ts +5 -1
- package/dist/verify/anchor.js +13 -4
- package/dist/verify/chain.d.ts +4 -0
- package/dist/verify/chain.js +4 -3
- package/dist/verify/envelope.d.ts +1 -1
- package/dist/verify/envelope.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.js +2 -2
- package/dist/warehouse/index.d.ts +62 -0
- package/dist/warehouse/index.js +142 -0
- package/package.json +6 -3
package/dist/session/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { SELF_CHANGE_KINDS } from "../analysis/learning.js";
|
|
10
10
|
import { argsHash } from "../approvals/index.js";
|
|
11
11
|
import { attest, isReadOnly, parseCommand } from "../attest/index.js";
|
|
12
|
+
import { SAFETY_CATEGORIES, } from "../aviation/index.js";
|
|
12
13
|
import { buildPayment, ftaPayload, screeningPayload, } from "../finance/index.js";
|
|
13
14
|
import { certificateOf, taughtBy } from "../fleet/index.js";
|
|
14
15
|
import { decisionPayload } from "../gov/index.js";
|
|
@@ -459,6 +460,38 @@ export class BlackboxSession {
|
|
|
459
460
|
nearMiss(description) {
|
|
460
461
|
this.event("near_miss", undefined, { description });
|
|
461
462
|
}
|
|
463
|
+
/** spec/aviation.md §6: a voluntary, non-punitive safety report ("I nearly..."): a
|
|
464
|
+
* SAFETY_REPORT finding that never blocks. `category`: near_miss, hazard, procedure, tooling or
|
|
465
|
+
* other (the default). */
|
|
466
|
+
safetyReport(description, options = {}) {
|
|
467
|
+
const category = SAFETY_CATEGORIES.includes(options.category ?? "")
|
|
468
|
+
? options.category
|
|
469
|
+
: "other";
|
|
470
|
+
this.event("safety_report", category, { category, description: description.slice(0, 4000) });
|
|
471
|
+
}
|
|
472
|
+
/** spec/aviation.md §10: the agent declares distress (it's stuck, unsure, or about to do harm): a
|
|
473
|
+
* MAYDAY warning, routed to a person at once. Shipped now, not with the next batch. */
|
|
474
|
+
mayday(reason, options = {}) {
|
|
475
|
+
this.distress("mayday", reason, options.kind);
|
|
476
|
+
}
|
|
477
|
+
/** spec/aviation.md §10: urgency short of distress: a PAN_PAN caution, shipped now. */
|
|
478
|
+
panPan(reason, options = {}) {
|
|
479
|
+
this.distress("pan_pan", reason, options.kind);
|
|
480
|
+
}
|
|
481
|
+
distress(type, reason, kind) {
|
|
482
|
+
const k = kind === "stuck" || kind === "harm" ? kind : "unsure";
|
|
483
|
+
this.event(type, k, { kind: k, reason: reason.slice(0, 500) });
|
|
484
|
+
void this.flush().catch(() => { });
|
|
485
|
+
}
|
|
486
|
+
/** spec/aviation.md §8: the agent reports running a QRH procedure (`started`, `completed`). */
|
|
487
|
+
procedure(code, status) {
|
|
488
|
+
this.event("procedure", code.slice(0, 64), { code: code.slice(0, 64), status });
|
|
489
|
+
}
|
|
490
|
+
/** spec/aviation.md §9: the run passes its commit point (V1): past it, with the gateway's
|
|
491
|
+
* BLACKBOX_COMMIT_POINT on, a non-critical finding doesn't stop it. Only the first counts. */
|
|
492
|
+
commitPoint(note) {
|
|
493
|
+
this.event("commit_point", undefined, note ? { note: note.slice(0, 500) } : {});
|
|
494
|
+
}
|
|
462
495
|
/** spec/attestation.md: re-runs a read-only command and records whether the output matches. */
|
|
463
496
|
attest(argv, claimed, cwd, claimedExit) {
|
|
464
497
|
if (!isReadOnly(argv))
|
|
@@ -618,6 +651,48 @@ export class BlackboxSession {
|
|
|
618
651
|
}
|
|
619
652
|
return null;
|
|
620
653
|
}
|
|
654
|
+
/** One of this session's gateway routes: the answer on 200 or 201, else null. Never throws. */
|
|
655
|
+
async route(method, path, what, body) {
|
|
656
|
+
if (!this.enabled)
|
|
657
|
+
return null;
|
|
658
|
+
try {
|
|
659
|
+
const r = await send(method, this.options.url, `/v1/sessions/${this.id}/${path}`, this.token, body);
|
|
660
|
+
if (r.status === 200 || r.status === 201)
|
|
661
|
+
return r.json;
|
|
662
|
+
this.fail("rejected", `${what}: the gateway answered ${r.status}`);
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
this.fail("network", `${what}: ${message(error)}`);
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
/** spec/aviation.md §1-§2: asks for a clearance on a shared resource before changing it. The
|
|
670
|
+
* answer is `granted` (with its `clearance_id`) or `hold` (wait `retry_after_s`, then ask again).
|
|
671
|
+
* `null` when the gateway can't be asked. */
|
|
672
|
+
async clearance(resource, options = {}) {
|
|
673
|
+
return (await this.route("POST", "clearances", "asking for a clearance", {
|
|
674
|
+
resource,
|
|
675
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
676
|
+
...(options.ttlS !== undefined ? { ttl_s: options.ttlS } : {}),
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
/** spec/aviation.md §1: gives a clearance back. True when the gateway released it. */
|
|
680
|
+
async releaseClearance(clearanceId) {
|
|
681
|
+
const path = `clearances/${encodeURIComponent(clearanceId)}`;
|
|
682
|
+
return (await this.route("DELETE", path, "releasing a clearance")) !== null;
|
|
683
|
+
}
|
|
684
|
+
/** spec/aviation.md §8: the quick reference handbook's procedures for this session's
|
|
685
|
+
* findings. The gateway records that the agent read them. `null` when it can't be asked. */
|
|
686
|
+
async qrh() {
|
|
687
|
+
const r = await this.route("GET", "qrh", "reading the handbook");
|
|
688
|
+
return r ? r.procedures : null;
|
|
689
|
+
}
|
|
690
|
+
/** spec/aviation.md §3: the operator's notices in force now, to read before a run. The gateway
|
|
691
|
+
* records that the agent read them. `null` when it can't be asked. */
|
|
692
|
+
async notams() {
|
|
693
|
+
const r = await this.route("GET", "notams", "reading the notams");
|
|
694
|
+
return r ? r.notams : null;
|
|
695
|
+
}
|
|
621
696
|
/**
|
|
622
697
|
* spec/findings.md §1c: the agent changed itself (its prompt, a tool, its policy, its memory, its
|
|
623
698
|
* model). Only hashes of `before` and `after` are sent, never the content. A change that gives
|
|
@@ -640,6 +715,14 @@ export class BlackboxSession {
|
|
|
640
715
|
: {}),
|
|
641
716
|
});
|
|
642
717
|
}
|
|
718
|
+
/** spec/evals.md §4: the version of a registered prompt this run uses, for when the gateway
|
|
719
|
+
* can't tell it from the system prompt's hash (the prompt is assembled per run). A bad name or
|
|
720
|
+
* version is dropped. */
|
|
721
|
+
promptVersion(name, version) {
|
|
722
|
+
if (!/^[A-Za-z0-9._-]{1,128}$/.test(name) || !Number.isSafeInteger(version) || version < 1)
|
|
723
|
+
return;
|
|
724
|
+
this.event("prompt_version", name, { name, version });
|
|
725
|
+
}
|
|
643
726
|
/** spec/findings.md §13.1: what in an answer (the latest, or the one at `seq`) would hold it,
|
|
644
727
|
* for an app that streams the answer by itself: `{seq, risks, hold}`. `hold` says whether the
|
|
645
728
|
* server would hold it. `null` when there's no answer yet or the gateway can't be asked. */
|
package/dist/undo/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Undo (spec/undo.md): the completed risky actions of a session, newest first, each with the call
|
|
2
2
|
// that reverses it. Pure; mirrors sdks/python/src/zanii_blackbox/undo.py; pinned by
|
|
3
3
|
// spec/vectors/undo.json.
|
|
4
|
+
import { envelopesOf } from "../reconcile/record.js";
|
|
4
5
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
6
|
const glob = (pattern) => new RegExp(`^${pattern.split("*").map(escapeRe).join(".*")}$`);
|
|
6
7
|
const REDACTED = /\[REDACTED:[a-z_]+\]/;
|
|
@@ -40,7 +41,7 @@ function resolve(value, args, result) {
|
|
|
40
41
|
}
|
|
41
42
|
/** Completed, successful tool calls (not policy-denied, not undos), and the seqs already undone. */
|
|
42
43
|
function callsOf(lines, bodies) {
|
|
43
|
-
const events = lines
|
|
44
|
+
const events = envelopesOf(lines);
|
|
44
45
|
const calls = [];
|
|
45
46
|
const undone = new Set();
|
|
46
47
|
const results = new Map();
|
|
@@ -144,8 +145,7 @@ export function undoPlan(lines, bodies, compensations) {
|
|
|
144
145
|
/** spec/undo.md §3: an undo that the gateway ran and that failed. */
|
|
145
146
|
export function undoFindings(lines, bodies) {
|
|
146
147
|
const out = [];
|
|
147
|
-
for (const
|
|
148
|
-
const e = JSON.parse(line);
|
|
148
|
+
for (const e of envelopesOf(lines)) {
|
|
149
149
|
if (e.kind !== "tool.result" || typeof e.meta.undo_of !== "number")
|
|
150
150
|
continue;
|
|
151
151
|
const res = bodyJson(bodies, e.body_hash);
|
package/dist/verify/anchor.d.ts
CHANGED
|
@@ -35,11 +35,15 @@ export interface BundleReport {
|
|
|
35
35
|
ok: boolean;
|
|
36
36
|
message?: string;
|
|
37
37
|
};
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* One entry per anchor when proofs were checked; null when they weren't. `legacy`: the receipt
|
|
40
|
+
* predates JCS payloads (no `payload_encoding`, Zanii SDKs before 0.25.1).
|
|
41
|
+
*/
|
|
39
42
|
proofs: Array<{
|
|
40
43
|
receipt_hash: string;
|
|
41
44
|
ok: boolean;
|
|
42
45
|
problems: string[];
|
|
46
|
+
legacy: boolean;
|
|
43
47
|
}> | null;
|
|
44
48
|
}
|
|
45
49
|
/**
|
package/dist/verify/anchor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Anchors and audit bundles (spec/anchoring.md). Mirrors sdks/python/src/zanii_blackbox/verify/anchor.py.
|
|
2
|
-
import { canonicalBytes } from "@zanii/core";
|
|
2
|
+
import { canonicalBytes, verifyPayloadLegacy } from "@zanii/core";
|
|
3
3
|
import { fetchAndVerifyProof, verifyPayload } from "@zanii/sdk";
|
|
4
4
|
import { verifyChain } from "./chain.js";
|
|
5
5
|
export const ANCHOR_TARGET = "blackbox.anchor";
|
|
@@ -43,6 +43,7 @@ export async function verifyBundle(bundle, options = {}) {
|
|
|
43
43
|
proofs = [];
|
|
44
44
|
for (const a of bundle.anchors) {
|
|
45
45
|
const problems = [];
|
|
46
|
+
let legacy = false;
|
|
46
47
|
if (!bundle.ledger_url)
|
|
47
48
|
problems.push("no ledger_url in the bundle");
|
|
48
49
|
else {
|
|
@@ -60,12 +61,20 @@ export async function verifyBundle(bundle, options = {}) {
|
|
|
60
61
|
problems.push("receipt was not signed by the pinned gateway identity");
|
|
61
62
|
if (r.target !== ANCHOR_TARGET)
|
|
62
63
|
problems.push(`receipt target is ${r.target}, not ${ANCHOR_TARGET}`);
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const sid = bundle.session.session_id;
|
|
65
|
+
let committed;
|
|
66
|
+
if (r.payload_encoding === "jcs")
|
|
67
|
+
committed = verifyPayload(anchorPayloadBytes(sid, a.count, a.head), a.nonce, r.payload_hash);
|
|
68
|
+
else {
|
|
69
|
+
// recorded before JCS payloads: try each older SDK's encoding
|
|
70
|
+
legacy = true;
|
|
71
|
+
committed = verifyPayloadLegacy(anchorPayload(sid, a.count, a.head), a.nonce, r.payload_hash).ok;
|
|
65
72
|
}
|
|
73
|
+
if (!committed)
|
|
74
|
+
problems.push("receipt payload does not commit to this (count, head)");
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
|
-
proofs.push({ receipt_hash: a.receipt_hash, ok: problems.length === 0, problems });
|
|
77
|
+
proofs.push({ receipt_hash: a.receipt_hash, ok: problems.length === 0, problems, legacy });
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
return {
|
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);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const GENESIS = "genesis";
|
|
2
|
-
export declare const EVENT_KINDS: readonly ["session.open", "session.close", "session.lost_contact", "llm.request", "llm.chunk", "llm.response", "llm.incomplete", "tool.call", "tool.result", "tool.notification", "workspace.receipt", "sdk.event", "control", "outcome", "finding", "capture.gap"];
|
|
2
|
+
export declare const EVENT_KINDS: readonly ["session.open", "session.close", "session.lost_contact", "llm.request", "llm.chunk", "llm.response", "llm.incomplete", "llm.realtime.client", "llm.realtime.server", "tool.call", "tool.result", "tool.notification", "tool.sampling", "tool.elicitation", "tool.roots", "workspace.receipt", "sdk.event", "control", "outcome", "finding", "capture.gap"];
|
|
3
3
|
export type EventKind = (typeof EVENT_KINDS)[number];
|
|
4
4
|
export type Json = null | boolean | number | string | Json[] | {
|
|
5
5
|
[key: string]: Json;
|
package/dist/verify/envelope.js
CHANGED
|
@@ -10,9 +10,14 @@ export const EVENT_KINDS = [
|
|
|
10
10
|
"llm.chunk",
|
|
11
11
|
"llm.response",
|
|
12
12
|
"llm.incomplete",
|
|
13
|
+
"llm.realtime.client",
|
|
14
|
+
"llm.realtime.server",
|
|
13
15
|
"tool.call",
|
|
14
16
|
"tool.result",
|
|
15
17
|
"tool.notification",
|
|
18
|
+
"tool.sampling",
|
|
19
|
+
"tool.elicitation",
|
|
20
|
+
"tool.roots",
|
|
16
21
|
"workspace.receipt",
|
|
17
22
|
"sdk.event",
|
|
18
23
|
"control",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.23.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.23.0";
|
package/dist/walls/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Zanii's UAE vertical walls (spec/governance.md §5): the table from spec/walls/v1.json, embedded
|
|
2
2
|
// (a test checks it matches), and the output check. Mirrors sdks/python/src/zanii_blackbox/walls.py.
|
|
3
3
|
import { jcsHash } from "@zanii/core";
|
|
4
|
+
import { envelopesOf } from "../reconcile/record.js";
|
|
4
5
|
export const WALLS = {
|
|
5
6
|
"sca-trading": {
|
|
6
7
|
id: "sca-trading",
|
|
@@ -90,8 +91,7 @@ export function wallFindings(lines, bodies, wallIds) {
|
|
|
90
91
|
return out;
|
|
91
92
|
// a response's bytes are its chunks' bodies, joined in order (spec/api.md: llm.chunk)
|
|
92
93
|
const chunks = new Map();
|
|
93
|
-
for (const
|
|
94
|
-
const e = JSON.parse(l);
|
|
94
|
+
for (const e of envelopesOf(lines)) {
|
|
95
95
|
if (e.kind === "llm.chunk") {
|
|
96
96
|
const body = bodies(e.body_hash);
|
|
97
97
|
const list = chunks.get(e.meta.request_seq) ?? [];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare const WAREHOUSE_TABLES: readonly ["sessions", "events", "findings", "costs"];
|
|
2
|
+
export type WarehouseTable = (typeof WAREHOUSE_TABLES)[number];
|
|
3
|
+
export type WarehouseRow = Record<string, string | number | boolean | null>;
|
|
4
|
+
export interface WarehouseSession {
|
|
5
|
+
id: string;
|
|
6
|
+
tenant_id: string;
|
|
7
|
+
label: string | null;
|
|
8
|
+
created_at: string;
|
|
9
|
+
closed_at: string;
|
|
10
|
+
}
|
|
11
|
+
/** The part of a cost report (spec/cost.md) the rows use. */
|
|
12
|
+
export interface WarehouseCost {
|
|
13
|
+
total_micro_usd: number;
|
|
14
|
+
calls_priced: number;
|
|
15
|
+
calls_unpriced: number;
|
|
16
|
+
calls: Array<{
|
|
17
|
+
seq: number;
|
|
18
|
+
model: string | null;
|
|
19
|
+
tokens: {
|
|
20
|
+
input: number;
|
|
21
|
+
output: number;
|
|
22
|
+
cache_read: number;
|
|
23
|
+
cache_write: number;
|
|
24
|
+
};
|
|
25
|
+
micro_usd: number | null;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/** spec/warehouse.md §2: the session's rows per table. `bodies` (hash → text) only when the
|
|
29
|
+
* tenant opted in (§4); then every event row has `body`. */
|
|
30
|
+
export declare function warehouseRows(input: {
|
|
31
|
+
session: WarehouseSession;
|
|
32
|
+
lines: readonly string[];
|
|
33
|
+
cost: WarehouseCost;
|
|
34
|
+
bodies?: Record<string, string | null> | null;
|
|
35
|
+
}): Record<WarehouseTable, WarehouseRow[]>;
|
|
36
|
+
/** spec/warehouse.md §3: batches of at most `maxRows` rows and `maxBytes` bytes of JSON (a row
|
|
37
|
+
* bigger than that goes alone). */
|
|
38
|
+
export declare function warehouseBatches(rows: readonly WarehouseRow[], maxRows?: number, maxBytes?: number): WarehouseRow[][];
|
|
39
|
+
export type WarehouseDestination = {
|
|
40
|
+
type: "clickhouse";
|
|
41
|
+
database: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "bigquery";
|
|
44
|
+
project: string;
|
|
45
|
+
dataset: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: "snowflake";
|
|
48
|
+
database: string;
|
|
49
|
+
schema: string;
|
|
50
|
+
warehouse: string;
|
|
51
|
+
role?: string;
|
|
52
|
+
};
|
|
53
|
+
export interface WarehouseRequest {
|
|
54
|
+
/** The path and query, after the destination's base URL. */
|
|
55
|
+
path: string;
|
|
56
|
+
headers: Record<string, string>;
|
|
57
|
+
body: string;
|
|
58
|
+
}
|
|
59
|
+
/** spec/warehouse.md §3: a UUID from the batch key, so a retry is the same Snowflake request. */
|
|
60
|
+
export declare function warehouseRequestId(key: string): string;
|
|
61
|
+
/** spec/warehouse.md §3: one batch as the destination takes it, without credentials. */
|
|
62
|
+
export declare function warehouseRequest(dest: WarehouseDestination, table: WarehouseTable, rows: readonly WarehouseRow[], key: string): WarehouseRequest;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Warehouse export (spec/warehouse.md): a closed session's rows for the four tables, cut into
|
|
2
|
+
// batches, and each batch as a ClickHouse, BigQuery or Snowflake request. Pure: the server adds the
|
|
3
|
+
// base URL and the credentials and sends it. Mirrors sdks/python/src/zanii_blackbox/warehouse.py;
|
|
4
|
+
// pinned by spec/vectors/warehouse.json.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { canonical } from "../reconcile/shared.js";
|
|
7
|
+
export const WAREHOUSE_TABLES = ["sessions", "events", "findings", "costs"];
|
|
8
|
+
/** spec/warehouse.md §2: the session's rows per table. `bodies` (hash → text) only when the
|
|
9
|
+
* tenant opted in (§4); then every event row has `body`. */
|
|
10
|
+
export function warehouseRows(input) {
|
|
11
|
+
const { session: s, cost } = input;
|
|
12
|
+
const bodies = input.bodies ?? null;
|
|
13
|
+
const base = { tenant_id: s.tenant_id, session_id: s.id };
|
|
14
|
+
const events = [];
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const line of input.lines) {
|
|
17
|
+
const e = JSON.parse(line);
|
|
18
|
+
const meta = e.meta ?? {};
|
|
19
|
+
events.push({
|
|
20
|
+
insert_id: `${s.id}:${e.seq}`,
|
|
21
|
+
...base,
|
|
22
|
+
seq: e.seq,
|
|
23
|
+
ts: e.ts,
|
|
24
|
+
kind: e.kind,
|
|
25
|
+
body_hash: e.body_hash ?? null,
|
|
26
|
+
meta: canonical(meta),
|
|
27
|
+
...(bodies ? { body: (e.body_hash && bodies[e.body_hash]) ?? null } : {}),
|
|
28
|
+
});
|
|
29
|
+
if (e.kind === "finding")
|
|
30
|
+
findings.push({
|
|
31
|
+
insert_id: `${s.id}:${e.seq}`,
|
|
32
|
+
...base,
|
|
33
|
+
seq: e.seq,
|
|
34
|
+
ts: e.ts,
|
|
35
|
+
code: typeof meta.code === "string" ? meta.code : "",
|
|
36
|
+
source: typeof meta.source === "string" ? meta.source : "",
|
|
37
|
+
severity: typeof meta.severity === "string" ? meta.severity : "",
|
|
38
|
+
ref: canonical(meta.ref ?? {}),
|
|
39
|
+
detail: typeof meta.detail === "string" ? meta.detail : null,
|
|
40
|
+
accepted: meta.accepted !== undefined && meta.accepted !== null,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const costs = cost.calls.map((c) => ({
|
|
44
|
+
insert_id: `${s.id}:${c.seq}`,
|
|
45
|
+
...base,
|
|
46
|
+
seq: c.seq,
|
|
47
|
+
model: c.model,
|
|
48
|
+
input_tokens: c.tokens.input,
|
|
49
|
+
output_tokens: c.tokens.output,
|
|
50
|
+
cache_read_tokens: c.tokens.cache_read,
|
|
51
|
+
cache_write_tokens: c.tokens.cache_write,
|
|
52
|
+
micro_usd: c.micro_usd,
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
sessions: [
|
|
56
|
+
{
|
|
57
|
+
insert_id: s.id,
|
|
58
|
+
...base,
|
|
59
|
+
label: s.label,
|
|
60
|
+
created_at: s.created_at,
|
|
61
|
+
closed_at: s.closed_at,
|
|
62
|
+
events: input.lines.length,
|
|
63
|
+
findings: findings.length,
|
|
64
|
+
cost_micro_usd: cost.total_micro_usd,
|
|
65
|
+
calls_priced: cost.calls_priced,
|
|
66
|
+
calls_unpriced: cost.calls_unpriced,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
events,
|
|
70
|
+
findings,
|
|
71
|
+
costs,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** spec/warehouse.md §3: batches of at most `maxRows` rows and `maxBytes` bytes of JSON (a row
|
|
75
|
+
* bigger than that goes alone). */
|
|
76
|
+
export function warehouseBatches(rows, maxRows = 500, maxBytes = 5_000_000) {
|
|
77
|
+
const out = [];
|
|
78
|
+
let batch = [];
|
|
79
|
+
let bytes = 0;
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const size = Buffer.byteLength(JSON.stringify(row));
|
|
82
|
+
if (batch.length > 0 && (batch.length >= maxRows || bytes + size > maxBytes)) {
|
|
83
|
+
out.push(batch);
|
|
84
|
+
batch = [];
|
|
85
|
+
bytes = 0;
|
|
86
|
+
}
|
|
87
|
+
batch.push(row);
|
|
88
|
+
bytes += size;
|
|
89
|
+
}
|
|
90
|
+
if (batch.length > 0)
|
|
91
|
+
out.push(batch);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** spec/warehouse.md §3: a UUID from the batch key, so a retry is the same Snowflake request. */
|
|
95
|
+
export function warehouseRequestId(key) {
|
|
96
|
+
const h = createHash("sha256").update(key).digest("hex");
|
|
97
|
+
const variant = ((Number.parseInt(h[16], 16) & 3) | 8).toString(16);
|
|
98
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-5${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
99
|
+
}
|
|
100
|
+
/** spec/warehouse.md §3: one batch as the destination takes it, without credentials. */
|
|
101
|
+
export function warehouseRequest(dest, table, rows, key) {
|
|
102
|
+
const name = `blackbox_${table}`;
|
|
103
|
+
if (dest.type === "clickhouse") {
|
|
104
|
+
const query = encodeURIComponent(`INSERT INTO ${dest.database}.${name} FORMAT JSONEachRow`);
|
|
105
|
+
return {
|
|
106
|
+
path: `/?query=${query}&insert_deduplication_token=${encodeURIComponent(key)}`,
|
|
107
|
+
headers: { "content-type": "application/x-ndjson" },
|
|
108
|
+
body: rows.map((r) => `${JSON.stringify(r)}\n`).join(""),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (dest.type === "bigquery")
|
|
112
|
+
return {
|
|
113
|
+
path: `/bigquery/v2/projects/${encodeURIComponent(dest.project)}/datasets/${encodeURIComponent(dest.dataset)}/tables/${name}/insertAll`,
|
|
114
|
+
headers: { "content-type": "application/json" },
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
kind: "bigquery#tableDataInsertAllRequest",
|
|
117
|
+
skipInvalidRows: false,
|
|
118
|
+
ignoreUnknownValues: false,
|
|
119
|
+
rows: rows.map((r) => ({ insertId: r.insert_id, json: r })),
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
const values = rows.map(() => "(?)").join(", ");
|
|
123
|
+
const bindings = {};
|
|
124
|
+
rows.forEach((r, i) => {
|
|
125
|
+
bindings[String(i + 1)] = { type: "TEXT", value: JSON.stringify(r) };
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
path: `/api/v2/statements?requestId=${warehouseRequestId(key)}&retry=true`,
|
|
129
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
statement: `MERGE INTO ${name} t USING (SELECT PARSE_JSON(column1) AS r FROM VALUES ${values}) s` +
|
|
132
|
+
" ON t.insert_id = s.r:insert_id::STRING WHEN NOT MATCHED THEN" +
|
|
133
|
+
" INSERT (insert_id, record) VALUES (s.r:insert_id::STRING, s.r)",
|
|
134
|
+
timeout: 60,
|
|
135
|
+
database: dest.database,
|
|
136
|
+
schema: dest.schema,
|
|
137
|
+
warehouse: dest.warehouse,
|
|
138
|
+
...(dest.role ? { role: dest.role } : {}),
|
|
139
|
+
bindings,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
}
|
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.23.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",
|
|
@@ -40,14 +40,17 @@
|
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@zanii/core": "^0.5.
|
|
44
|
-
"@zanii/sdk": "^0.5.
|
|
43
|
+
"@zanii/core": "^0.5.6",
|
|
44
|
+
"@zanii/sdk": "^0.5.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@anthropic-ai/claude-agent-sdk": "0.3.283",
|
|
48
48
|
"@langchain/core": "1.2.13",
|
|
49
49
|
"@langchain/langgraph": "1.4.18",
|
|
50
|
+
"@mastra/core": "1.78.0",
|
|
51
|
+
"@mastra/observability": "1.20.1",
|
|
50
52
|
"@openai/agents": "0.18.0",
|
|
53
|
+
"ai": "6.0.283",
|
|
51
54
|
"langchain": "1.5.14",
|
|
52
55
|
"zod": "^4.6.5"
|
|
53
56
|
},
|