@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/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { parseCommand } from "./attest/index.js";
|
|
8
|
-
import { serveMcp } from "./mcp-server/index.js";
|
|
8
|
+
import { serveMcp, serveMcpHttp } from "./mcp-server/index.js";
|
|
9
9
|
import { mcpWrap } from "./mcp-wrap/index.js";
|
|
10
10
|
import { RecordNotVerified, reconcile } from "./reconcile/index.js";
|
|
11
11
|
import { session } from "./session/index.js";
|
|
@@ -28,6 +28,11 @@ const USAGE = `usage (spec/cli.md):
|
|
|
28
28
|
blackbox replay <session_id> [--fork] [--fork-after N] [--label L] [--drill <drill.json>]
|
|
29
29
|
blackbox scenario save <session_id> [--out <file>]
|
|
30
30
|
blackbox scenario run <scenario.json>... -- <agent command> (1 when an incident's findings recur)
|
|
31
|
+
blackbox eval dataset create <name> [<cases.jsonl> | <scenario.json>]... [--label L [--outcome O] [--limit N]] [--scorer <json>]
|
|
32
|
+
blackbox eval dataset list | blackbox eval dataset get <dataset_id>
|
|
33
|
+
blackbox eval run <dataset_id> [--name N] -- <agent command> (1 when any case fails)
|
|
34
|
+
blackbox eval run <dataset_id> [--name N] <case_id>=<session_id>... (scores recorded sessions)
|
|
35
|
+
blackbox eval compare <run_a> <run_b> (1 when b is worse than a)
|
|
31
36
|
blackbox tail <session_id> [--after-seq N]
|
|
32
37
|
blackbox close <session_id> [--outcome success|failure|abandoned] [--note <text>]
|
|
33
38
|
blackbox resume <session_id> [--token <resume token>] [--note <text>] [--force] (a paused session needs no token)
|
|
@@ -43,6 +48,7 @@ const USAGE = `usage (spec/cli.md):
|
|
|
43
48
|
blackbox investigate <bundle.json> [--bodies <dir>] [--directives <file>] [--identity <file>] [--proofs] [--out <prefix>]
|
|
44
49
|
blackbox evidence-pack <bundle.json> [--proofs] [--identity <file>] [--lang en|ar|both] [--out <prefix>]
|
|
45
50
|
blackbox sign-constitution <draft.json> --identity <owner.json> [--out <file>]
|
|
51
|
+
blackbox sign <draft.json> --identity <owner.json> [--out <file>] (a service register or an accreditation)
|
|
46
52
|
blackbox policy-from-zanii <zanii-policy.json> [--out <file>]
|
|
47
53
|
blackbox policy-to-zanii <policy.json> [--out <file>]
|
|
48
54
|
blackbox statement --provider anthropic|openai --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--out <file>]
|
|
@@ -50,7 +56,7 @@ const USAGE = `usage (spec/cli.md):
|
|
|
50
56
|
blackbox statement --provider custom --connector <file> --from <d> --to <d> [--out <file>]
|
|
51
57
|
blackbox preflight --require <items> [--optional <items>]
|
|
52
58
|
blackbox --version
|
|
53
|
-
blackbox mcp (an MCP server on stdio: spec/mcp-server.md)
|
|
59
|
+
blackbox mcp [--http [host:]port] (an MCP server on stdio, or Streamable HTTP: spec/mcp-server.md)
|
|
54
60
|
blackbox mcp-wrap --server <name> -- <command> [args...]
|
|
55
61
|
blackbox reconcile --harness claude-code|codex --bundle <bundle.json> --bodies <dir> [--agent-session <id>]... [--require-receipts] <path>
|
|
56
62
|
blackbox reconcile-system --connector <connector.json> --log <export.csv|jsonl> [--pack <pack.json>] <session_id>
|
|
@@ -99,6 +105,8 @@ async function main(argv) {
|
|
|
99
105
|
return replayCommand(rest);
|
|
100
106
|
if (command === "scenario")
|
|
101
107
|
return scenarioCommand(rest);
|
|
108
|
+
if (command === "eval")
|
|
109
|
+
return evalCommand(rest);
|
|
102
110
|
if (command === "analyze")
|
|
103
111
|
return analyzeCommand(rest);
|
|
104
112
|
if (command === "verify-certificate")
|
|
@@ -111,6 +119,8 @@ async function main(argv) {
|
|
|
111
119
|
return evidencePackCommand(rest);
|
|
112
120
|
if (command === "sign-constitution")
|
|
113
121
|
return signConstitutionCommand(rest);
|
|
122
|
+
if (command === "sign")
|
|
123
|
+
return signCommand(rest);
|
|
114
124
|
if (command === "policy-from-zanii" || command === "policy-to-zanii")
|
|
115
125
|
return policyZaniiCommand(command, rest);
|
|
116
126
|
if (command === "statement")
|
|
@@ -393,17 +403,33 @@ function mcpCommand(rest) {
|
|
|
393
403
|
const url = process.env.BLACKBOX_URL?.replace(/\/+$/, "");
|
|
394
404
|
// audit, also found: a tenant's key (read-only works) instead of the operator's
|
|
395
405
|
const key = process.env.BLACKBOX_API_KEY || process.env.BLACKBOX_ADMIN_KEY;
|
|
396
|
-
|
|
397
|
-
process.stderr.write(`${USAGE}\n(mcp: BLACKBOX_URL and BLACKBOX_API_KEY or BLACKBOX_ADMIN_KEY must be set)\n`);
|
|
398
|
-
return 2;
|
|
399
|
-
}
|
|
400
|
-
return serveMcp(async (path) => {
|
|
406
|
+
const reader = (k) => async (path) => {
|
|
401
407
|
const res = await fetch(`${url}${path}`, {
|
|
402
|
-
headers: { authorization: `Bearer ${
|
|
408
|
+
headers: { authorization: `Bearer ${k}` },
|
|
403
409
|
signal: AbortSignal.timeout(30_000),
|
|
404
410
|
});
|
|
405
411
|
return { status: res.status, text: await res.text() };
|
|
406
|
-
}
|
|
412
|
+
};
|
|
413
|
+
// spec/mcp-server.md: `--http [host:]port` serves Streamable HTTP; each caller brings its own key
|
|
414
|
+
if (rest[0] === "--http") {
|
|
415
|
+
const m = /^(?:(.+):)?(\d{1,5})$/.exec(rest[1] ?? "");
|
|
416
|
+
const port = Number(m?.[2]);
|
|
417
|
+
if (rest.length !== 2 || !m || port > 65535 || !url) {
|
|
418
|
+
process.stderr.write(`${USAGE}\n(mcp --http [host:]port: BLACKBOX_URL must be set)\n`);
|
|
419
|
+
return 2;
|
|
420
|
+
}
|
|
421
|
+
const host = (m[1] ?? "127.0.0.1").replace(/^\[(.*)\]$/, "$1");
|
|
422
|
+
return serveMcpHttp(reader, {
|
|
423
|
+
host,
|
|
424
|
+
port,
|
|
425
|
+
onListening: (at) => process.stderr.write(`blackbox mcp: serving ${at}\n`),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (rest.length > 0 || !url || !key) {
|
|
429
|
+
process.stderr.write(`${USAGE}\n(mcp: BLACKBOX_URL and BLACKBOX_API_KEY or BLACKBOX_ADMIN_KEY must be set)\n`);
|
|
430
|
+
return 2;
|
|
431
|
+
}
|
|
432
|
+
return serveMcp(reader(key));
|
|
407
433
|
}
|
|
408
434
|
class ServerError extends Error {
|
|
409
435
|
}
|
|
@@ -689,16 +715,32 @@ async function scenarioRun(file, command) {
|
|
|
689
715
|
const label = `regression: ${scenario.label || source}`.slice(0, 200);
|
|
690
716
|
const opened = (await (await admin("/v1/sessions", { label, replay: { of: source, fork: true } })).json());
|
|
691
717
|
const sid = String(opened.session_id);
|
|
692
|
-
const
|
|
718
|
+
const code = await runAgent(command, sid, String(opened.token), { BLACKBOX_SCENARIO: file });
|
|
719
|
+
const after = (await (await admin(`/v1/sessions/${sid}/scenario`)).json());
|
|
720
|
+
const got = (after?.expect?.finding_codes ?? {});
|
|
721
|
+
const recurred = Object.fromEntries(Object.keys(before).flatMap((c) => (got[c] ? [[c, got[c]]] : [])));
|
|
722
|
+
return {
|
|
723
|
+
scenario: file,
|
|
724
|
+
source,
|
|
725
|
+
session_id: sid,
|
|
726
|
+
exit_code: code,
|
|
727
|
+
ok: code === 0 && Object.keys(recurred).length === 0,
|
|
728
|
+
recurred,
|
|
729
|
+
new: Object.fromEntries(Object.entries(got).filter(([c]) => !(c in before))),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** Runs the agent once in a session (no shell), its output on stderr so stdout stays the results,
|
|
733
|
+
* then closes the session: `success` when it exited 0. Returns its exit code. */
|
|
734
|
+
async function runAgent(command, sid, token, extra) {
|
|
693
735
|
const url = String(process.env.BLACKBOX_URL).replace(/\/+$/, "");
|
|
694
736
|
const run = spawnSync(command[0], command.slice(1), {
|
|
695
|
-
stdio: ["inherit", 2, "inherit"],
|
|
737
|
+
stdio: ["inherit", 2, "inherit"],
|
|
696
738
|
env: {
|
|
697
739
|
...process.env,
|
|
698
740
|
BLACKBOX_SESSION_ID: sid,
|
|
699
741
|
BLACKBOX_TOKEN: token,
|
|
700
|
-
|
|
701
|
-
// the official clients read these: an agent that doesn't set its own goes through
|
|
742
|
+
...extra,
|
|
743
|
+
// the official clients read these: an agent that doesn't set its own goes through Blackbox
|
|
702
744
|
ANTHROPIC_BASE_URL: `${url}/anthropic`,
|
|
703
745
|
ANTHROPIC_API_KEY: token,
|
|
704
746
|
OPENAI_BASE_URL: `${url}/openai/v1`,
|
|
@@ -709,18 +751,127 @@ async function scenarioRun(file, command) {
|
|
|
709
751
|
process.stderr.write(`blackbox: can't run ${command[0]}: ${run.error.message}\n`);
|
|
710
752
|
const code = run.error ? 127 : (run.status ?? 1);
|
|
711
753
|
await admin(`/v1/sessions/${sid}/close`, { outcome: code === 0 ? "success" : "failure" });
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
754
|
+
return code;
|
|
755
|
+
}
|
|
756
|
+
/** spec/evals.md §6: datasets, eval runs (the agent run against each case, or recorded sessions
|
|
757
|
+
* scored), and two runs compared. */
|
|
758
|
+
async function evalCommand(rest) {
|
|
759
|
+
const at = rest.indexOf("--");
|
|
760
|
+
const command = at === -1 ? [] : rest.slice(at + 1);
|
|
761
|
+
const { flags, positional } = parse(at === -1 ? rest : rest.slice(0, at), []);
|
|
762
|
+
const [what, ...args] = positional;
|
|
763
|
+
const enc = encodeURIComponent;
|
|
764
|
+
if (what === "dataset" && !command.length) {
|
|
765
|
+
const [verb, ...more] = args;
|
|
766
|
+
if (verb === "list" && !more.length)
|
|
767
|
+
return printJson("/v1/evals/datasets");
|
|
768
|
+
if (verb === "get" && more.length === 1)
|
|
769
|
+
return printJson(`/v1/evals/datasets/${enc(more[0])}`);
|
|
770
|
+
if (verb !== "create" || !more.length || (more.length === 1 && !flags.label))
|
|
771
|
+
return usage();
|
|
772
|
+
const [name, ...files] = more;
|
|
773
|
+
const { parseCasesJsonl } = await import("./evals/index.js");
|
|
774
|
+
const body = { name };
|
|
775
|
+
const cases = [];
|
|
776
|
+
const scenarios = [];
|
|
777
|
+
for (const f of files) {
|
|
778
|
+
let text;
|
|
779
|
+
try {
|
|
780
|
+
text = readFileSync(f, "utf8");
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
throw new ServerError(`${f}: ${error instanceof Error ? error.message : error}`);
|
|
784
|
+
}
|
|
785
|
+
let one;
|
|
786
|
+
try {
|
|
787
|
+
one = JSON.parse(text);
|
|
788
|
+
}
|
|
789
|
+
catch { }
|
|
790
|
+
if (one?.v === 1 && typeof one.source === "string" && one.expect !== undefined) {
|
|
791
|
+
scenarios.push(one);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const got = parseCasesJsonl(text);
|
|
795
|
+
if (typeof got === "string")
|
|
796
|
+
throw new ServerError(`${f}: ${got}`);
|
|
797
|
+
cases.push(...got);
|
|
798
|
+
}
|
|
799
|
+
if (cases.length)
|
|
800
|
+
body.cases = cases;
|
|
801
|
+
if (scenarios.length)
|
|
802
|
+
body.from_scenarios = scenarios;
|
|
803
|
+
if (flags.limit !== undefined && !/^\d+$/.test(flags.limit))
|
|
804
|
+
return usage();
|
|
805
|
+
if (flags.label)
|
|
806
|
+
body.from_sessions = {
|
|
807
|
+
label: flags.label,
|
|
808
|
+
...(flags.outcome ? { outcome: flags.outcome } : {}),
|
|
809
|
+
...(flags.limit ? { limit: Number(flags.limit) } : {}),
|
|
810
|
+
};
|
|
811
|
+
if (flags.scorer !== undefined)
|
|
812
|
+
try {
|
|
813
|
+
body.scorer = JSON.parse(flags.scorer);
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return usage();
|
|
817
|
+
}
|
|
818
|
+
const made = await (await admin("/v1/evals/datasets", body)).json();
|
|
819
|
+
process.stdout.write(`${JSON.stringify(made)}\n`);
|
|
820
|
+
return 0;
|
|
821
|
+
}
|
|
822
|
+
if (what === "compare" && args.length === 2 && !command.length) {
|
|
823
|
+
const [a, b] = args;
|
|
824
|
+
const doc = await adminJson(`/v1/evals/compare?a=${enc(a)}&b=${enc(b)}`);
|
|
825
|
+
process.stdout.write(`${JSON.stringify(doc)}\n`);
|
|
826
|
+
return doc.verdict === "worse" ? 1 : 0;
|
|
827
|
+
}
|
|
828
|
+
if (what !== "run" || args.length < 1)
|
|
829
|
+
return usage();
|
|
830
|
+
const [id, ...pairs] = args;
|
|
831
|
+
if (command.length ? pairs.length > 0 : pairs.length === 0)
|
|
832
|
+
return usage();
|
|
833
|
+
const sessions = [];
|
|
834
|
+
for (const p of pairs) {
|
|
835
|
+
const m = /^([^=]+)=(ses_[A-Za-z0-9]{16,64})$/.exec(p);
|
|
836
|
+
if (!m)
|
|
837
|
+
return usage();
|
|
838
|
+
sessions.push({ case: m[1], session_id: m[2] });
|
|
839
|
+
}
|
|
840
|
+
const ds = await adminJson(`/v1/evals/datasets/${enc(id)}`);
|
|
841
|
+
for (const c of command.length ? ds.cases : []) {
|
|
842
|
+
// a case made from a recorded run replays it, going live where the agent now differs
|
|
843
|
+
const of = typeof c.input?.replay_of === "string" ? c.input.replay_of : null;
|
|
844
|
+
const opened = (await (await admin("/v1/sessions", {
|
|
845
|
+
label: `eval: ${ds.name}`.slice(0, 128),
|
|
846
|
+
...(of ? { replay: { of, fork: true } } : {}),
|
|
847
|
+
})).json());
|
|
848
|
+
const sid = String(opened.session_id);
|
|
849
|
+
await runAgent(command, sid, String(opened.token), {
|
|
850
|
+
BLACKBOX_EVAL_DATASET: String(ds.id),
|
|
851
|
+
BLACKBOX_EVAL_CASE: String(c.id),
|
|
852
|
+
BLACKBOX_EVAL_INPUT: JSON.stringify(c.input),
|
|
853
|
+
});
|
|
854
|
+
sessions.push({ case: String(c.id), session_id: sid });
|
|
855
|
+
}
|
|
856
|
+
const body = { dataset: ds.id, ...(flags.name ? { name: flags.name } : {}), sessions };
|
|
857
|
+
const run = (await (await admin("/v1/evals/runs", body)).json());
|
|
858
|
+
const s = run.summary;
|
|
859
|
+
const line = {
|
|
860
|
+
run: run.id,
|
|
861
|
+
dataset: ds.id,
|
|
862
|
+
name: run.name,
|
|
863
|
+
total: s.total,
|
|
864
|
+
passed: s.passed,
|
|
865
|
+
failed: s.failed,
|
|
866
|
+
outages: s.outages,
|
|
867
|
+
pass_ppm: s.pass_ppm,
|
|
868
|
+
pass_low_ppm: s.pass_low_ppm,
|
|
869
|
+
pass_high_ppm: s.pass_high_ppm,
|
|
870
|
+
cost_micro_usd: s.cost_micro_usd,
|
|
871
|
+
failing: run.results.filter((r) => !r.pass).map((r) => r.case),
|
|
723
872
|
};
|
|
873
|
+
process.stdout.write(`${JSON.stringify(line)}\n`);
|
|
874
|
+
return s.failed ? 1 : 0;
|
|
724
875
|
}
|
|
725
876
|
/** Audit S12: an exported bundle, analysed offline: it must verify first, then the summary, the
|
|
726
877
|
* findings it recorded, and the findings this SDK's analyses find in it now. */
|
|
@@ -863,6 +1014,30 @@ async function signConstitutionCommand(rest) {
|
|
|
863
1014
|
return 2;
|
|
864
1015
|
}
|
|
865
1016
|
}
|
|
1017
|
+
/** spec/admission.md §3: signs a draft (a service register, an accreditation) as the identity. */
|
|
1018
|
+
async function signCommand(rest) {
|
|
1019
|
+
const { flags, positional } = parse(rest, []);
|
|
1020
|
+
if (positional.length !== 1 || !flags.identity)
|
|
1021
|
+
return usage();
|
|
1022
|
+
const { signCertificate } = await import("./fleet/index.js");
|
|
1023
|
+
const { docHash } = await import("./admission/index.js");
|
|
1024
|
+
try {
|
|
1025
|
+
const draft = JSON.parse(readFileSync(positional[0], "utf8"));
|
|
1026
|
+
if (!draft || typeof draft !== "object" || Array.isArray(draft))
|
|
1027
|
+
throw new Error("the draft must be a JSON object");
|
|
1028
|
+
const { signed: _old, ...rest_ } = draft;
|
|
1029
|
+
const identity = await readIdentity(flags.identity);
|
|
1030
|
+
const doc = signCertificate(rest_, identity.did, identity.seed);
|
|
1031
|
+
const out = flags.out || "signed.json";
|
|
1032
|
+
writeFileSync(out, `${JSON.stringify(doc, null, 2)}\n`);
|
|
1033
|
+
process.stdout.write(`${JSON.stringify({ ok: true, file: out, sha256: docHash(doc), by: identity.did })}\n`);
|
|
1034
|
+
return 0;
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
|
|
1038
|
+
return 2;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
866
1041
|
/** spec/investigations.md §7: the court evidence pack, `<prefix>.json` (signed) and `<prefix>.md`. */
|
|
867
1042
|
async function evidencePackCommand(rest) {
|
|
868
1043
|
const { flags, positional } = parse(rest, ["--proofs"]);
|
|
@@ -43,6 +43,8 @@ export declare function complianceReport(frameworkId: string, framework: Framewo
|
|
|
43
43
|
notes: string;
|
|
44
44
|
notes_ar?: string;
|
|
45
45
|
}, lang?: "en" | "ar"): Report;
|
|
46
|
+
/** spec/compliance.md §3: each fact's Arabic label (the key follows it, so tools still match). */
|
|
47
|
+
export declare const FACTS_AR: Record<string, string>;
|
|
46
48
|
export declare function renderReport(r: Report): string;
|
|
47
49
|
/** spec/compliance.md §1: the facts over sessions' records (the server adds verified / anchored). */
|
|
48
50
|
export declare function complianceFacts(sessions: ReadonlyArray<{
|
package/dist/compliance/index.js
CHANGED
|
@@ -32,6 +32,28 @@ const show = (v, ar = false) => {
|
|
|
32
32
|
}
|
|
33
33
|
return String(v);
|
|
34
34
|
};
|
|
35
|
+
/** spec/compliance.md §3: each fact's Arabic label (the key follows it, so tools still match). */
|
|
36
|
+
export const FACTS_AR = {
|
|
37
|
+
sessions: "الجلسات",
|
|
38
|
+
events: "الأحداث",
|
|
39
|
+
sessions_verified: "الجلسات التي تحققت سلاسلها",
|
|
40
|
+
sessions_anchored: "الجلسات المثبتة على السجل",
|
|
41
|
+
redactions: "الحجب",
|
|
42
|
+
findings_warning: "الملاحظات بمستوى تحذير",
|
|
43
|
+
bypasses: "تجاوزات البوابة",
|
|
44
|
+
false_claims: "الادعاءات غير الصحيحة",
|
|
45
|
+
blocks: "الإيقافات",
|
|
46
|
+
emergency_stops: "الإيقافات الطارئة",
|
|
47
|
+
resumes_with_note: "الاستئنافات المسببة",
|
|
48
|
+
outcomes: "النتائج",
|
|
49
|
+
data_region: "منطقة البيانات",
|
|
50
|
+
model_responses: "إجابات النموذج",
|
|
51
|
+
hallucination_findings: "ملاحظات الهلوسة",
|
|
52
|
+
grounding_checks: "فحوص الإسناد",
|
|
53
|
+
judge_checks: "فحوص المُقيِّم",
|
|
54
|
+
hallucination_controls: "ضوابط الهلوسة",
|
|
55
|
+
detector_accuracy: "دقة الكواشف",
|
|
56
|
+
};
|
|
35
57
|
export function renderReport(r) {
|
|
36
58
|
const ar = r.lang === "ar";
|
|
37
59
|
const out = [
|
|
@@ -47,9 +69,11 @@ export function renderReport(r) {
|
|
|
47
69
|
const head = ar ? "| الدليل | القيمة |" : "| Evidence | Value |";
|
|
48
70
|
out.push("", `## ${s.title}`, "", s.requirement, "", head, "|---|---|");
|
|
49
71
|
for (const [k, v] of Object.entries(s.evidence))
|
|
50
|
-
out.push(`| ${k} | ${show(v, ar)} |`);
|
|
72
|
+
out.push(`| ${ar && FACTS_AR[k] ? `${FACTS_AR[k]} (\`${k}\`)` : k} | ${show(v, ar)} |`);
|
|
51
73
|
}
|
|
52
|
-
|
|
74
|
+
const body = `${out.join("\n")}\n`;
|
|
75
|
+
// Right to left, as the incident report (spec/fleet.md §2)
|
|
76
|
+
return ar ? `<div dir="rtl" lang="ar">\n\n${body}\n</div>\n` : body;
|
|
53
77
|
}
|
|
54
78
|
/** spec/compliance.md §1: the facts over sessions' records (the server adds verified / anchored). */
|
|
55
79
|
export function complianceFacts(sessions, dataRegion) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Bodies } from "../reconcile/record.ts";
|
|
2
|
+
import { type LoadedPrices, type Tokens } from "./index.ts";
|
|
3
|
+
export interface BatchItem {
|
|
4
|
+
custom_id: string;
|
|
5
|
+
status: string;
|
|
6
|
+
model: string | null;
|
|
7
|
+
joined: boolean;
|
|
8
|
+
tokens: Tokens;
|
|
9
|
+
micro_usd: number | null;
|
|
10
|
+
}
|
|
11
|
+
export interface BatchEntry {
|
|
12
|
+
seq: number;
|
|
13
|
+
request_seq: number;
|
|
14
|
+
provider: string | null;
|
|
15
|
+
source_seq: number | null;
|
|
16
|
+
results: number;
|
|
17
|
+
joined: number;
|
|
18
|
+
micro_usd: number | null;
|
|
19
|
+
items: BatchItem[];
|
|
20
|
+
}
|
|
21
|
+
export interface BatchReport {
|
|
22
|
+
v: 1;
|
|
23
|
+
batches: BatchEntry[];
|
|
24
|
+
}
|
|
25
|
+
/** The session's batch results joined to their requests and priced (spec/cost.md §6). */
|
|
26
|
+
export declare function batchReport(lines: readonly string[], bodies: Bodies, prices: LoadedPrices): BatchReport;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Batch results joined to their requests (spec/cost.md §6, spec/providers.md §6): each results
|
|
2
|
+
// download read again from its recorded chunks, its lines joined by custom_id to the call that
|
|
3
|
+
// carried the batch's requests, each result priced at the batch rate.
|
|
4
|
+
// Mirrors sdks/python/src/zanii_blackbox/batch.py.
|
|
5
|
+
import { priceCall } from "./index.js";
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8
|
+
const ZERO = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
|
|
9
|
+
function jsonLines(text) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const line of text.split("\n")) {
|
|
12
|
+
if (!line.trim())
|
|
13
|
+
continue;
|
|
14
|
+
try {
|
|
15
|
+
const o = JSON.parse(line);
|
|
16
|
+
if (isObj(o))
|
|
17
|
+
out.push(o);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** One result line as {id, status, model, usage}, or null when it isn't one. */
|
|
24
|
+
function resultOf(o) {
|
|
25
|
+
if (typeof o.custom_id !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
if (isObj(o.result)) {
|
|
28
|
+
// Anthropic: {custom_id, result: {type, message?}}
|
|
29
|
+
const message = isObj(o.result.message) ? o.result.message : {};
|
|
30
|
+
const status = typeof o.result.type === "string" ? o.result.type : "errored";
|
|
31
|
+
return { id: o.custom_id, status, model: message.model, usage: message.usage };
|
|
32
|
+
}
|
|
33
|
+
if (!("response" in o) && !("error" in o))
|
|
34
|
+
return null;
|
|
35
|
+
// OpenAI: {custom_id, response: {status_code, body}, error}
|
|
36
|
+
const response = isObj(o.response) ? o.response : {};
|
|
37
|
+
const code = response.status_code;
|
|
38
|
+
const ok = typeof code === "number" && code >= 200 && code < 300 && o.error == null;
|
|
39
|
+
const body = isObj(response.body) ? response.body : {};
|
|
40
|
+
return {
|
|
41
|
+
id: o.custom_id,
|
|
42
|
+
status: ok ? "succeeded" : "errored",
|
|
43
|
+
model: body.model,
|
|
44
|
+
usage: body.usage,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Flattened integer usage, as the gateway records it (spec/api.md §3). */
|
|
48
|
+
function flatten(usage) {
|
|
49
|
+
const out = {};
|
|
50
|
+
const walk = (o, prefix) => {
|
|
51
|
+
for (const [k, v] of Object.entries(o)) {
|
|
52
|
+
if (typeof v === "number" && Number.isSafeInteger(v))
|
|
53
|
+
out[prefix + k] = v;
|
|
54
|
+
else if (isObj(v))
|
|
55
|
+
walk(v, `${prefix}${k}_`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
if (isObj(usage))
|
|
59
|
+
walk(usage, "");
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** The custom_ids a batch's source call carried: Anthropic's requests[], or OpenAI's JSONL file. */
|
|
63
|
+
function customIds(text) {
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
let whole;
|
|
66
|
+
try {
|
|
67
|
+
whole = JSON.parse(text);
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
if (isObj(whole) && Array.isArray(whole.requests)) {
|
|
71
|
+
for (const r of whole.requests)
|
|
72
|
+
if (isObj(r) && typeof r.custom_id === "string")
|
|
73
|
+
ids.add(r.custom_id);
|
|
74
|
+
return ids;
|
|
75
|
+
}
|
|
76
|
+
for (const o of jsonLines(text))
|
|
77
|
+
if (typeof o.custom_id === "string")
|
|
78
|
+
ids.add(o.custom_id);
|
|
79
|
+
return ids;
|
|
80
|
+
}
|
|
81
|
+
/** The session's batch results joined to their requests and priced (spec/cost.md §6). */
|
|
82
|
+
export function batchReport(lines, bodies, prices) {
|
|
83
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
84
|
+
const requests = new Map();
|
|
85
|
+
const answers = new Map(); // request seq → its llm.response meta
|
|
86
|
+
const chunks = new Map(); // decoded whole: a character can span chunks
|
|
87
|
+
for (const e of events) {
|
|
88
|
+
const owner = e.meta.request_seq;
|
|
89
|
+
if (e.kind === "llm.request")
|
|
90
|
+
requests.set(e.seq, e);
|
|
91
|
+
else if (e.kind === "llm.chunk" && typeof owner === "number") {
|
|
92
|
+
const list = chunks.get(owner) ?? [];
|
|
93
|
+
list.push(bodies(e.body_hash) ?? new Uint8Array());
|
|
94
|
+
chunks.set(owner, list);
|
|
95
|
+
}
|
|
96
|
+
else if (e.kind === "llm.response" && typeof owner === "number")
|
|
97
|
+
answers.set(owner, e.meta);
|
|
98
|
+
}
|
|
99
|
+
const pathOf = (seq) => {
|
|
100
|
+
const p = requests.get(seq)?.meta.path;
|
|
101
|
+
return typeof p === "string" ? p.split("?")[0] : "";
|
|
102
|
+
};
|
|
103
|
+
const postTo = (seq, tail) => requests.get(seq)?.meta.method === "POST" && tail.test(pathOf(seq));
|
|
104
|
+
/** The latest request before `before` whose answer has `field` = `value` and that passes `ok`. */
|
|
105
|
+
const answered = (field, value, before, ok) => {
|
|
106
|
+
let found = null;
|
|
107
|
+
for (const [seq, meta] of answers)
|
|
108
|
+
if (seq < before && meta[field] === value && ok(seq))
|
|
109
|
+
found = seq;
|
|
110
|
+
return found;
|
|
111
|
+
};
|
|
112
|
+
const batches = [];
|
|
113
|
+
for (const e of events) {
|
|
114
|
+
const requestSeq = e.meta.request_seq;
|
|
115
|
+
if (e.kind !== "llm.response" || !isObj(e.meta.batch) || typeof requestSeq !== "number")
|
|
116
|
+
continue;
|
|
117
|
+
const path = pathOf(requestSeq);
|
|
118
|
+
let source = null;
|
|
119
|
+
const anthropic = /\/messages\/batches\/([^/]+)\/results$/.exec(path);
|
|
120
|
+
const openai = /\/files\/([^/]+)\/content$/.exec(path);
|
|
121
|
+
if (anthropic)
|
|
122
|
+
source = answered("batch_id", anthropic[1], requestSeq, (s) => postTo(s, /\/messages\/batches$/));
|
|
123
|
+
else if (openai) {
|
|
124
|
+
const file = openai[1];
|
|
125
|
+
let input;
|
|
126
|
+
for (const [seq, meta] of answers)
|
|
127
|
+
if (seq < requestSeq && (meta.output_file_id === file || meta.error_file_id === file))
|
|
128
|
+
input = meta.input_file_id;
|
|
129
|
+
if (typeof input === "string")
|
|
130
|
+
source = answered("file_id", input, requestSeq, (s) => postTo(s, /\/files$/));
|
|
131
|
+
}
|
|
132
|
+
const sourceRequest = source === null ? undefined : requests.get(source);
|
|
133
|
+
const sourceBody = sourceRequest ? bodies(sourceRequest.body_hash) : undefined;
|
|
134
|
+
const ids = sourceBody ? customIds(decoder.decode(sourceBody)) : new Set();
|
|
135
|
+
const p = requests.get(requestSeq)?.meta.provider;
|
|
136
|
+
const provider = typeof p === "string" ? p : null;
|
|
137
|
+
const items = [];
|
|
138
|
+
let total = 0;
|
|
139
|
+
for (const line of jsonLines(decoder.decode(Buffer.concat(chunks.get(requestSeq) ?? [])))) {
|
|
140
|
+
const r = resultOf(line);
|
|
141
|
+
if (!r)
|
|
142
|
+
continue;
|
|
143
|
+
const model = typeof r.model === "string" ? r.model : null;
|
|
144
|
+
let tokens = ZERO;
|
|
145
|
+
let micro = 0;
|
|
146
|
+
if (r.status === "succeeded") {
|
|
147
|
+
const priced = priceCall({ model, usage: flatten(r.usage), provider, batch: { requests: 1 } }, prices.table);
|
|
148
|
+
tokens = priced.tokens;
|
|
149
|
+
micro = priced.micro_usd;
|
|
150
|
+
}
|
|
151
|
+
total = total === null || micro === null ? null : total + micro;
|
|
152
|
+
items.push({
|
|
153
|
+
custom_id: r.id,
|
|
154
|
+
status: r.status,
|
|
155
|
+
model,
|
|
156
|
+
joined: ids.has(r.id),
|
|
157
|
+
tokens,
|
|
158
|
+
micro_usd: micro,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
batches.push({
|
|
162
|
+
seq: e.seq,
|
|
163
|
+
request_seq: requestSeq,
|
|
164
|
+
provider,
|
|
165
|
+
source_seq: source,
|
|
166
|
+
results: items.length,
|
|
167
|
+
joined: items.filter((i) => i.joined).length,
|
|
168
|
+
micro_usd: total,
|
|
169
|
+
items,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return { v: 1, batches };
|
|
173
|
+
}
|
package/dist/cost/index.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface ModelPrice {
|
|
|
30
30
|
service_tiers?: Record<string, ClassPrices>;
|
|
31
31
|
/** `deprecated` feeds the MODEL_DEPRECATED finding; `beta` is informational. */
|
|
32
32
|
status?: "deprecated" | "beta";
|
|
33
|
+
/** The percent of the price a batch result pays (spec/cost.md §1); absent: no known batch rate. */
|
|
34
|
+
batch_percent?: number;
|
|
33
35
|
}
|
|
34
36
|
export interface PriceTable {
|
|
35
37
|
version: string;
|
|
@@ -81,6 +83,14 @@ export interface CallCost {
|
|
|
81
83
|
source?: "reported";
|
|
82
84
|
/** The answer carried no usage (`usage_missing`): unpriced, never counted as free. */
|
|
83
85
|
no_usage?: true;
|
|
86
|
+
/** A batch result (spec/cost.md §2): its result count, and the batch percent it was priced at. */
|
|
87
|
+
batch?: {
|
|
88
|
+
requests: number;
|
|
89
|
+
percent?: number;
|
|
90
|
+
};
|
|
91
|
+
/** spec/cost.md §2: an MCP client's answer to a server's `sampling/createMessage`, or a usage
|
|
92
|
+
* report in a realtime session (spec/providers.md §7). */
|
|
93
|
+
via?: "mcp_sampling" | "realtime";
|
|
84
94
|
}
|
|
85
95
|
export interface CostReport {
|
|
86
96
|
v: 1;
|
|
@@ -145,6 +155,10 @@ export declare function priceCall(call: {
|
|
|
145
155
|
provider?: string | null;
|
|
146
156
|
serviceTier?: string | null;
|
|
147
157
|
reportedMicroUsd?: number | null;
|
|
158
|
+
/** A batch result (spec/cost.md §2): priced at the model's batch_percent, or not at all. */
|
|
159
|
+
batch?: {
|
|
160
|
+
requests: number;
|
|
161
|
+
};
|
|
148
162
|
}, table: PriceTable): Omit<CallCost, "seq" | "model"> & {
|
|
149
163
|
key: string | null;
|
|
150
164
|
};
|
|
@@ -154,5 +168,12 @@ export declare function upliftPercent(table: PriceTable, provider: string | null
|
|
|
154
168
|
/** fils at the CBUAE peg (3.6725 AED / USD), rounded half up. */
|
|
155
169
|
export declare function toAedFils(microUsd: number): number;
|
|
156
170
|
export declare function formatAed(fils: number): string;
|
|
171
|
+
/** A results download's `meta.batch.models` (spec/providers.md §6), the well-formed entries; null
|
|
172
|
+
* when the response isn't one. */
|
|
173
|
+
export declare function batchModels(batch: unknown): Array<{
|
|
174
|
+
model: string;
|
|
175
|
+
requests: number;
|
|
176
|
+
usage: Record<string, unknown>;
|
|
177
|
+
}> | null;
|
|
157
178
|
/** The session's cost report from its recorded lines. */
|
|
158
179
|
export declare function sessionCost(lines: readonly string[], prices: LoadedPrices): CostReport;
|