@zanii/blackbox 0.0.0-stage → 0.2.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/LICENSE +202 -0
- package/README.md +109 -2
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.js +73 -0
- package/dist/analysis/detectors.d.ts +36 -0
- package/dist/analysis/detectors.js +339 -0
- package/dist/analysis/faults.d.ts +9 -0
- package/dist/analysis/faults.js +250 -0
- package/dist/analysis/index.d.ts +68 -0
- package/dist/analysis/index.js +388 -0
- package/dist/analysis/landing.d.ts +25 -0
- package/dist/analysis/landing.js +225 -0
- package/dist/analysis/memory.d.ts +13 -0
- package/dist/analysis/memory.js +33 -0
- package/dist/analysis/waste.d.ts +29 -0
- package/dist/analysis/waste.js +79 -0
- package/dist/approvals/index.d.ts +11 -0
- package/dist/approvals/index.js +27 -0
- package/dist/approvals/warnings.d.ts +2 -0
- package/dist/approvals/warnings.js +28 -0
- package/dist/attest/index.d.ts +17 -0
- package/dist/attest/index.js +106 -0
- package/dist/authority/index.d.ts +24 -0
- package/dist/authority/index.js +77 -0
- package/dist/billing/index.d.ts +99 -0
- package/dist/billing/index.js +174 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1057 -0
- package/dist/client/index.d.ts +146 -0
- package/dist/client/index.js +210 -0
- package/dist/compliance/index.d.ts +41 -0
- package/dist/compliance/index.js +96 -0
- package/dist/cost/index.d.ts +133 -0
- package/dist/cost/index.js +293 -0
- package/dist/data/index.d.ts +191 -0
- package/dist/data/index.js +762 -0
- package/dist/directives/index.d.ts +35 -0
- package/dist/directives/index.js +80 -0
- package/dist/drills/index.d.ts +43 -0
- package/dist/drills/index.js +101 -0
- package/dist/duty/index.d.ts +21 -0
- package/dist/duty/index.js +68 -0
- package/dist/fleet/index.d.ts +141 -0
- package/dist/fleet/index.js +454 -0
- package/dist/hooks/ai-sdk.d.ts +42 -0
- package/dist/hooks/ai-sdk.js +62 -0
- package/dist/hooks/claude-agent-sdk.d.ts +14 -0
- package/dist/hooks/claude-agent-sdk.js +70 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/langchain-agent.d.ts +69 -0
- package/dist/hooks/langchain-agent.js +163 -0
- package/dist/hooks/langchain.d.ts +41 -0
- package/dist/hooks/langchain.js +216 -0
- package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
- package/dist/hooks/langgraph-checkpoint.js +73 -0
- package/dist/hooks/memory.d.ts +17 -0
- package/dist/hooks/memory.js +64 -0
- package/dist/hooks/openai-agents.d.ts +6 -0
- package/dist/hooks/openai-agents.js +40 -0
- package/dist/hooks/protect.d.ts +7 -0
- package/dist/hooks/protect.js +39 -0
- package/dist/hooks/providers.d.ts +16 -0
- package/dist/hooks/providers.js +149 -0
- package/dist/hooks/shared.d.ts +11 -0
- package/dist/hooks/shared.js +39 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +48 -0
- package/dist/investigate/index.d.ts +66 -0
- package/dist/investigate/index.js +119 -0
- package/dist/mcp-server/index.d.ts +85 -0
- package/dist/mcp-server/index.js +216 -0
- package/dist/mcp-wrap/index.d.ts +17 -0
- package/dist/mcp-wrap/index.js +170 -0
- package/dist/money/index.d.ts +114 -0
- package/dist/money/index.js +622 -0
- package/dist/occurrence/index.d.ts +108 -0
- package/dist/occurrence/index.js +168 -0
- package/dist/ocsf/index.d.ts +22 -0
- package/dist/ocsf/index.js +168 -0
- package/dist/otlp/index.d.ts +24 -0
- package/dist/otlp/index.js +143 -0
- package/dist/packs/index.d.ts +48 -0
- package/dist/packs/index.js +343 -0
- package/dist/policy/delta.d.ts +11 -0
- package/dist/policy/delta.js +39 -0
- package/dist/policy/drafts.d.ts +34 -0
- package/dist/policy/drafts.js +129 -0
- package/dist/policy/index.d.ts +47 -0
- package/dist/policy/index.js +154 -0
- package/dist/precog/index.d.ts +96 -0
- package/dist/precog/index.js +167 -0
- package/dist/precog/intervention.d.ts +22 -0
- package/dist/precog/intervention.js +44 -0
- package/dist/precog/normal.d.ts +31 -0
- package/dist/precog/normal.js +89 -0
- package/dist/preflight/index.d.ts +11 -0
- package/dist/preflight/index.js +19 -0
- package/dist/ratings/index.d.ts +21 -0
- package/dist/ratings/index.js +48 -0
- package/dist/reconcile/claude-code.d.ts +19 -0
- package/dist/reconcile/claude-code.js +220 -0
- package/dist/reconcile/codex.d.ts +5 -0
- package/dist/reconcile/codex.js +191 -0
- package/dist/reconcile/index.d.ts +19 -0
- package/dist/reconcile/index.js +50 -0
- package/dist/reconcile/record.d.ts +49 -0
- package/dist/reconcile/record.js +225 -0
- package/dist/reconcile/shared.d.ts +65 -0
- package/dist/reconcile/shared.js +113 -0
- package/dist/replay/index.d.ts +11 -0
- package/dist/replay/index.js +64 -0
- package/dist/replay/repair.d.ts +10 -0
- package/dist/replay/repair.js +62 -0
- package/dist/session/drain.d.ts +13 -0
- package/dist/session/drain.js +35 -0
- package/dist/session/index.d.ts +275 -0
- package/dist/session/index.js +681 -0
- package/dist/undo/index.d.ts +45 -0
- package/dist/undo/index.js +212 -0
- package/dist/verify/anchor.d.ts +54 -0
- package/dist/verify/anchor.js +77 -0
- package/dist/verify/chain.d.ts +27 -0
- package/dist/verify/chain.js +105 -0
- package/dist/verify/envelope.d.ts +28 -0
- package/dist/verify/envelope.js +55 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/weather/index.d.ts +24 -0
- package/dist/weather/index.js +45 -0
- package/dist/workspace-receipt/index.d.ts +15 -0
- package/dist/workspace-receipt/index.js +121 -0
- package/package.json +56 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type Bundle, type BundleReport } from "../verify/anchor.ts";
|
|
2
|
+
export interface ClientOptions {
|
|
3
|
+
url: string;
|
|
4
|
+
/** The admin key or a tenant's `bbxt_` key. */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Per request (default 30 s). */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** Retries of a 429, a 503 or a network error, with backoff (default 2; 0 = none). Only reads and
|
|
9
|
+
* idempotent writes are retried. */
|
|
10
|
+
retries?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SessionQuery {
|
|
13
|
+
limit?: number;
|
|
14
|
+
/** The previous page's `next`. */
|
|
15
|
+
before?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
status?: "open" | "closed";
|
|
18
|
+
/** ISO dates on `created_at`; `to` is exclusive. */
|
|
19
|
+
from?: string;
|
|
20
|
+
to?: string;
|
|
21
|
+
}
|
|
22
|
+
export type Json = Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* The gateway answered with an error, or couldn't be reached. `code` is its stable error code
|
|
25
|
+
* (spec/api.md §3), or `network`, `timeout` or `integrity` from the client itself. `requestId` is
|
|
26
|
+
* the gateway's `x-request-id`, to find the call in its logs.
|
|
27
|
+
*/
|
|
28
|
+
export declare class BlackboxApiError extends Error {
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly requestId: string | null;
|
|
32
|
+
constructor(status: number, code: string, message: string, requestId?: string | null);
|
|
33
|
+
}
|
|
34
|
+
export declare function client(options: ClientOptions): {
|
|
35
|
+
/** Any JSON read route not covered below, e.g. `get("/v1/fleet/collisions")`. */
|
|
36
|
+
get: (path: string) => Promise<Json>;
|
|
37
|
+
version: () => Promise<Json>;
|
|
38
|
+
/** One page, newest first: `{sessions, next}`; pass `next` back as `before`. */
|
|
39
|
+
sessions: (q?: SessionQuery) => Promise<{
|
|
40
|
+
sessions: Json[];
|
|
41
|
+
next: string | null;
|
|
42
|
+
}>;
|
|
43
|
+
/** Every matching session, newest first, page by page. */
|
|
44
|
+
allSessions(q?: SessionQuery): AsyncGenerator<Json>;
|
|
45
|
+
session: (id: string) => Promise<Json>;
|
|
46
|
+
verify: (id: string) => Promise<Json>;
|
|
47
|
+
state: (id: string) => Promise<Json>;
|
|
48
|
+
/** The session's events, parsed; with `afterSeq`, only those after it (audit S4). */
|
|
49
|
+
events(id: string, o?: {
|
|
50
|
+
afterSeq?: number;
|
|
51
|
+
}): Promise<Json[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Audit K13: the session's events live, as they're recorded (Server-Sent Events), from
|
|
54
|
+
* `afterSeq`. Ends once the session is closed and everything is sent.
|
|
55
|
+
*/
|
|
56
|
+
tail(id: string, o?: {
|
|
57
|
+
afterSeq?: number;
|
|
58
|
+
}): AsyncGenerator<Json>;
|
|
59
|
+
/** The audit bundle, typed for verifyBundle (audit K22). */
|
|
60
|
+
bundle: (id: string) => Promise<Bundle>;
|
|
61
|
+
/** Audit K22: fetches the bundle and verifies it here, not trusting the gateway's own check. */
|
|
62
|
+
verifiedBundle(id: string, o?: {
|
|
63
|
+
proofs?: boolean;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
bundle: Bundle;
|
|
66
|
+
report: BundleReport;
|
|
67
|
+
}>;
|
|
68
|
+
summary: (id: string) => Promise<Json>;
|
|
69
|
+
cost: (id: string) => Promise<Json>;
|
|
70
|
+
findings: (id: string) => Promise<Json>;
|
|
71
|
+
landing: (id: string) => Promise<Json>;
|
|
72
|
+
waste: (id: string) => Promise<Json>;
|
|
73
|
+
/** Markdown, in English or Arabic. */
|
|
74
|
+
incident: (id: string, lang?: "en" | "ar") => Promise<string>;
|
|
75
|
+
fleet: () => Promise<Json>;
|
|
76
|
+
logbook: (o?: {
|
|
77
|
+
limit?: number;
|
|
78
|
+
}) => Promise<Json>;
|
|
79
|
+
certificate: (label: string) => Promise<Json>;
|
|
80
|
+
/**
|
|
81
|
+
* The exact body bytes, or null if unknown or erased. The bytes are checked against their
|
|
82
|
+
* hash (audit K24): a mismatch throws, code `integrity`.
|
|
83
|
+
*/
|
|
84
|
+
body(hash: string): Promise<Uint8Array | null>;
|
|
85
|
+
close: (id: string, o?: {
|
|
86
|
+
outcome?: "success" | "failure" | "abandoned";
|
|
87
|
+
note?: string;
|
|
88
|
+
}) => Promise<Json>;
|
|
89
|
+
/** PDPL / GDPR erasure of a closed session's content (spec/api.md). Idempotent. */
|
|
90
|
+
erase: (id: string) => Promise<Json>;
|
|
91
|
+
/** Unblocks a blocked session (spec/control.md §3). */
|
|
92
|
+
resume: (id: string, o?: {
|
|
93
|
+
token?: string;
|
|
94
|
+
force?: boolean;
|
|
95
|
+
note?: string;
|
|
96
|
+
}) => Promise<Json>;
|
|
97
|
+
/** Stops the session at its next call, resumable without a token (spec/control.md §3). */
|
|
98
|
+
pause: (id: string, o?: {
|
|
99
|
+
note?: string;
|
|
100
|
+
}) => Promise<Json>;
|
|
101
|
+
/** The emergency stop (squawk 7700, spec/control.md). */
|
|
102
|
+
emergency: (id: string, o?: {
|
|
103
|
+
note?: string;
|
|
104
|
+
}) => Promise<Json>;
|
|
105
|
+
/** Hands the controls over (spec/authority.md): `human` takes over; `agent`/`supervised` hand back. */
|
|
106
|
+
authority: (id: string, to: "human" | "agent" | "supervised", o?: {
|
|
107
|
+
reason?: string;
|
|
108
|
+
}) => Promise<Json>;
|
|
109
|
+
/** The pending approvals (spec/approvals.md). */
|
|
110
|
+
approvals: (id: string) => Promise<Json>;
|
|
111
|
+
/** Approves or rejects one (never with the session's own token). */
|
|
112
|
+
decide: (id: string, approvalId: string, decision: "approve" | "reject", o?: {
|
|
113
|
+
note?: string;
|
|
114
|
+
}) => Promise<Json>;
|
|
115
|
+
/** The undo plan (spec/undo.md); with `confirm: true`, runs the gateway's own steps. */
|
|
116
|
+
undo: (id: string, o?: {
|
|
117
|
+
confirm?: boolean;
|
|
118
|
+
}) => Promise<Json>;
|
|
119
|
+
/** Records a regulator filing (spec/occurrence.md). */
|
|
120
|
+
file: (id: string, filing: {
|
|
121
|
+
framework: string;
|
|
122
|
+
reference: string;
|
|
123
|
+
channel?: string;
|
|
124
|
+
filed_at?: string;
|
|
125
|
+
}) => Promise<Json>;
|
|
126
|
+
/** A cost reconciliation against the provider's statement (spec/money.md §2). */
|
|
127
|
+
reconcileCosts: (input: {
|
|
128
|
+
from: string;
|
|
129
|
+
to: string;
|
|
130
|
+
statement: Json;
|
|
131
|
+
tolerance_ppm?: number;
|
|
132
|
+
tenant?: string;
|
|
133
|
+
}) => Promise<Json>;
|
|
134
|
+
/** Operator only: tenants and their keys (spec/tenancy.md). */
|
|
135
|
+
tenants: () => Promise<Json>;
|
|
136
|
+
createTenant: (name: string) => Promise<Json>;
|
|
137
|
+
updateTenant: (tenantId: string, o: {
|
|
138
|
+
name?: string;
|
|
139
|
+
disabled?: boolean;
|
|
140
|
+
}) => Promise<Json>;
|
|
141
|
+
createKey: (tenantId: string, o?: {
|
|
142
|
+
scope?: "full" | "read";
|
|
143
|
+
}) => Promise<Json>;
|
|
144
|
+
revokeKey: (tenantId: string, keyId: string) => Promise<Json>;
|
|
145
|
+
};
|
|
146
|
+
export type Client = ReturnType<typeof client>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// A client for the gateway's API (audit S3, S4; spec/api.md): reads, and the operator's control
|
|
2
|
+
// and decision calls (audit K6). Unlike a session, it throws: a failed call is the caller's to
|
|
3
|
+
// handle, always as a BlackboxApiError (audit K11). Mirrors sdks/python/src/zanii_blackbox/client.py.
|
|
4
|
+
//
|
|
5
|
+
// const bb = client({ url, apiKey });
|
|
6
|
+
// for await (const s of bb.allSessions({ label: "billing-agent" })) console.log(s.session_id);
|
|
7
|
+
// for await (const line of bb.tail(id)) console.log(line.kind);
|
|
8
|
+
import { verifyBundle } from "../verify/anchor.js";
|
|
9
|
+
import { hashBody } from "../verify/envelope.js";
|
|
10
|
+
/**
|
|
11
|
+
* The gateway answered with an error, or couldn't be reached. `code` is its stable error code
|
|
12
|
+
* (spec/api.md §3), or `network`, `timeout` or `integrity` from the client itself. `requestId` is
|
|
13
|
+
* the gateway's `x-request-id`, to find the call in its logs.
|
|
14
|
+
*/
|
|
15
|
+
export class BlackboxApiError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
code;
|
|
18
|
+
requestId;
|
|
19
|
+
constructor(status, code, message, requestId = null) {
|
|
20
|
+
super(`${status} ${code}: ${message}${requestId ? ` (request ${requestId})` : ""}`);
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.requestId = requestId;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const RETRY_STATUSES = new Set([429, 503]);
|
|
27
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
28
|
+
export function client(options) {
|
|
29
|
+
const base = options.url.replace(/\/$/, "");
|
|
30
|
+
const retries = options.retries ?? 2;
|
|
31
|
+
async function once(method, path, body) {
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetch(`${base}${path}`, {
|
|
35
|
+
method,
|
|
36
|
+
headers: {
|
|
37
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
38
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
39
|
+
},
|
|
40
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
41
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const timedOut = error instanceof Error && error.name === "TimeoutError";
|
|
46
|
+
throw new BlackboxApiError(0, timedOut ? "timeout" : "network", error instanceof Error ? error.message : String(error));
|
|
47
|
+
}
|
|
48
|
+
if (res.status >= 400) {
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
let err = {};
|
|
51
|
+
try {
|
|
52
|
+
err = JSON.parse(text);
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
throw Object.assign(new BlackboxApiError(res.status, typeof err.code === "string" ? err.code : "error", typeof err.error === "string" ? err.error : text.slice(0, 200), res.headers.get("x-request-id")), { retryAfter: Number(res.headers.get("retry-after")) || 0 });
|
|
56
|
+
}
|
|
57
|
+
return res;
|
|
58
|
+
}
|
|
59
|
+
/** `retry` only for reads and idempotent writes: a retried POST must not act twice. */
|
|
60
|
+
async function request(method, path, body, retry = method === "GET") {
|
|
61
|
+
for (let attempt = 0;; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
return await once(method, path, body);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const e = error;
|
|
67
|
+
const again = e.status === 0 || RETRY_STATUSES.has(e.status);
|
|
68
|
+
if (!retry || !again || attempt >= retries)
|
|
69
|
+
throw error;
|
|
70
|
+
// the server's Retry-After, else 0.5 s, 1 s, 2 s ... with jitter, at most 10 s
|
|
71
|
+
const wait = e.retryAfter ? e.retryAfter * 1000 : 500 * 2 ** attempt;
|
|
72
|
+
await sleep(Math.min(10_000, wait) * (0.75 + Math.random() / 2));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const json = async (method, path, body, retry) => (await (await request(method, path, body, retry)).json());
|
|
77
|
+
const text = async (path) => (await request("GET", path)).text();
|
|
78
|
+
const ses = (id, action = "") => `/v1/sessions/${encodeURIComponent(id)}${action ? `/${action}` : ""}`;
|
|
79
|
+
const query = (q) => {
|
|
80
|
+
const p = new URLSearchParams();
|
|
81
|
+
for (const [k, v] of Object.entries(q))
|
|
82
|
+
if (v !== undefined)
|
|
83
|
+
p.set(k, String(v));
|
|
84
|
+
const s = p.toString();
|
|
85
|
+
return s ? `?${s}` : "";
|
|
86
|
+
};
|
|
87
|
+
const compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
88
|
+
const api = {
|
|
89
|
+
/** Any JSON read route not covered below, e.g. `get("/v1/fleet/collisions")`. */
|
|
90
|
+
get: (path) => json("GET", path),
|
|
91
|
+
version: () => json("GET", "/v1/version"),
|
|
92
|
+
/** One page, newest first: `{sessions, next}`; pass `next` back as `before`. */
|
|
93
|
+
sessions: (q = {}) => json("GET", `/v1/sessions${query({ ...q })}`),
|
|
94
|
+
/** Every matching session, newest first, page by page. */
|
|
95
|
+
async *allSessions(q = {}) {
|
|
96
|
+
let before = q.before;
|
|
97
|
+
for (;;) {
|
|
98
|
+
const page = await api.sessions({ ...q, ...(before ? { before } : {}) });
|
|
99
|
+
yield* page.sessions;
|
|
100
|
+
if (!page.next)
|
|
101
|
+
return;
|
|
102
|
+
before = page.next;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
session: (id) => json("GET", ses(id)),
|
|
106
|
+
verify: (id) => json("GET", ses(id, "verify")),
|
|
107
|
+
state: (id) => json("GET", ses(id, "state")),
|
|
108
|
+
/** The session's events, parsed; with `afterSeq`, only those after it (audit S4). */
|
|
109
|
+
async events(id, o = {}) {
|
|
110
|
+
const t = await text(`${ses(id, "events")}${query({ after_seq: o.afterSeq })}`);
|
|
111
|
+
return t
|
|
112
|
+
.split("\n")
|
|
113
|
+
.filter((l) => l)
|
|
114
|
+
.map((l) => JSON.parse(l));
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Audit K13: the session's events live, as they're recorded (Server-Sent Events), from
|
|
118
|
+
* `afterSeq`. Ends once the session is closed and everything is sent.
|
|
119
|
+
*/
|
|
120
|
+
async *tail(id, o = {}) {
|
|
121
|
+
const res = await once("GET", `${ses(id, "stream")}${query({ after_seq: o.afterSeq })}`);
|
|
122
|
+
const reader = res.body.getReader();
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let buffer = "";
|
|
125
|
+
for (;;) {
|
|
126
|
+
const { value, done } = await reader.read();
|
|
127
|
+
if (done)
|
|
128
|
+
return;
|
|
129
|
+
buffer += decoder.decode(value, { stream: true });
|
|
130
|
+
for (let end = buffer.indexOf("\n\n"); end !== -1; end = buffer.indexOf("\n\n")) {
|
|
131
|
+
const frame = buffer.slice(0, end);
|
|
132
|
+
buffer = buffer.slice(end + 2);
|
|
133
|
+
if (/^event: end$/m.test(frame))
|
|
134
|
+
return;
|
|
135
|
+
const data = frame
|
|
136
|
+
.split("\n")
|
|
137
|
+
.filter((l) => l.startsWith("data: "))
|
|
138
|
+
.map((l) => l.slice(6))
|
|
139
|
+
.join("\n");
|
|
140
|
+
if (data)
|
|
141
|
+
yield JSON.parse(data);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
/** The audit bundle, typed for verifyBundle (audit K22). */
|
|
146
|
+
bundle: async (id) => (await json("GET", ses(id, "bundle"))),
|
|
147
|
+
/** Audit K22: fetches the bundle and verifies it here, not trusting the gateway's own check. */
|
|
148
|
+
async verifiedBundle(id, o = {}) {
|
|
149
|
+
const bundle = await api.bundle(id);
|
|
150
|
+
return { bundle, report: await verifyBundle(bundle, o) };
|
|
151
|
+
},
|
|
152
|
+
summary: (id) => json("GET", ses(id, "summary")),
|
|
153
|
+
cost: (id) => json("GET", ses(id, "cost")),
|
|
154
|
+
findings: (id) => json("GET", ses(id, "findings")),
|
|
155
|
+
landing: (id) => json("GET", ses(id, "landing")),
|
|
156
|
+
waste: (id) => json("GET", ses(id, "waste")),
|
|
157
|
+
/** Markdown, in English or Arabic. */
|
|
158
|
+
incident: (id, lang = "en") => text(`${ses(id, "incident")}${query({ lang })}`),
|
|
159
|
+
fleet: () => json("GET", "/v1/fleet"),
|
|
160
|
+
logbook: (o = {}) => json("GET", `/v1/logbook${query({ ...o })}`),
|
|
161
|
+
certificate: (label) => json("GET", `/v1/logbook/certificate${query({ label })}`),
|
|
162
|
+
/**
|
|
163
|
+
* The exact body bytes, or null if unknown or erased. The bytes are checked against their
|
|
164
|
+
* hash (audit K24): a mismatch throws, code `integrity`.
|
|
165
|
+
*/
|
|
166
|
+
async body(hash) {
|
|
167
|
+
let bytes;
|
|
168
|
+
try {
|
|
169
|
+
bytes = new Uint8Array(await (await request("GET", `/v1/bodies/${encodeURIComponent(hash).replace(/%3A/gi, ":")}`)).arrayBuffer());
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error instanceof BlackboxApiError && (error.status === 404 || error.status === 410))
|
|
173
|
+
return null;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
if (hashBody(bytes) !== hash)
|
|
177
|
+
throw new BlackboxApiError(0, "integrity", `the gateway's bytes don't match ${hash}`);
|
|
178
|
+
return bytes;
|
|
179
|
+
},
|
|
180
|
+
// ---------------------------------------------------------------- control (audit K6)
|
|
181
|
+
close: (id, o = {}) => json("POST", ses(id, "close"), o, true), // idempotent (spec/api.md)
|
|
182
|
+
/** PDPL / GDPR erasure of a closed session's content (spec/api.md). Idempotent. */
|
|
183
|
+
erase: (id) => json("POST", ses(id, "erase"), {}, true),
|
|
184
|
+
/** Unblocks a blocked session (spec/control.md §3). */
|
|
185
|
+
resume: (id, o = {}) => json("POST", ses(id, "resume"), compact(o)),
|
|
186
|
+
/** Stops the session at its next call, resumable without a token (spec/control.md §3). */
|
|
187
|
+
pause: (id, o = {}) => json("POST", ses(id, "pause"), compact(o)),
|
|
188
|
+
/** The emergency stop (squawk 7700, spec/control.md). */
|
|
189
|
+
emergency: (id, o = {}) => json("POST", ses(id, "emergency"), compact(o)),
|
|
190
|
+
/** Hands the controls over (spec/authority.md): `human` takes over; `agent`/`supervised` hand back. */
|
|
191
|
+
authority: (id, to, o = {}) => json("POST", ses(id, "authority"), compact({ to, ...o })),
|
|
192
|
+
/** The pending approvals (spec/approvals.md). */
|
|
193
|
+
approvals: (id) => json("GET", ses(id, "approvals")),
|
|
194
|
+
/** Approves or rejects one (never with the session's own token). */
|
|
195
|
+
decide: (id, approvalId, decision, o = {}) => json("POST", `${ses(id, "approvals")}/${encodeURIComponent(approvalId)}`, compact({ decision, ...o })),
|
|
196
|
+
/** The undo plan (spec/undo.md); with `confirm: true`, runs the gateway's own steps. */
|
|
197
|
+
undo: (id, o = {}) => o.confirm ? json("POST", ses(id, "undo"), { confirm: true }) : json("GET", ses(id, "undo")),
|
|
198
|
+
/** Records a regulator filing (spec/occurrence.md). */
|
|
199
|
+
file: (id, filing) => json("POST", ses(id, "occurrence/filings"), compact(filing)),
|
|
200
|
+
/** A cost reconciliation against the provider's statement (spec/money.md §2). */
|
|
201
|
+
reconcileCosts: (input) => json("POST", "/v1/costs/reconcile", compact(input)),
|
|
202
|
+
/** Operator only: tenants and their keys (spec/tenancy.md). */
|
|
203
|
+
tenants: () => json("GET", "/v1/tenants"),
|
|
204
|
+
createTenant: (name) => json("POST", "/v1/tenants", { name }),
|
|
205
|
+
updateTenant: (tenantId, o) => json("PATCH", `/v1/tenants/${encodeURIComponent(tenantId)}`, compact(o)),
|
|
206
|
+
createKey: (tenantId, o = {}) => json("POST", `/v1/tenants/${encodeURIComponent(tenantId)}/keys`, compact(o)),
|
|
207
|
+
revokeKey: (tenantId, keyId) => json("POST", `/v1/tenants/${encodeURIComponent(tenantId)}/keys/${encodeURIComponent(keyId)}/revoke`, {}, true),
|
|
208
|
+
};
|
|
209
|
+
return api;
|
|
210
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type FactValue = number | string | null | Record<string, number>;
|
|
2
|
+
export type Facts = Record<string, FactValue>;
|
|
3
|
+
export interface Framework {
|
|
4
|
+
title: string;
|
|
5
|
+
sections: Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
requirement: string;
|
|
9
|
+
facts: string[];
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export interface Report {
|
|
13
|
+
framework: string;
|
|
14
|
+
title: string;
|
|
15
|
+
tenant_id: string;
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
generated_at: string;
|
|
19
|
+
facts: Facts;
|
|
20
|
+
sections: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
requirement: string;
|
|
24
|
+
evidence: Facts;
|
|
25
|
+
}>;
|
|
26
|
+
notes: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function complianceReport(frameworkId: string, framework: Framework, facts: Facts, meta: {
|
|
29
|
+
tenant_id: string;
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
generated_at: string;
|
|
33
|
+
notes: string;
|
|
34
|
+
}): Report;
|
|
35
|
+
export declare function renderReport(r: Report): string;
|
|
36
|
+
/** spec/compliance.md §1: the facts over sessions' records (the server adds verified / anchored). */
|
|
37
|
+
export declare function complianceFacts(sessions: ReadonlyArray<{
|
|
38
|
+
lines: readonly string[];
|
|
39
|
+
verified: boolean;
|
|
40
|
+
anchored: boolean;
|
|
41
|
+
}>, dataRegion: string | null): Facts;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Compliance reports (spec/compliance.md): facts from the record, mapped onto a framework's sections,
|
|
2
|
+
// and rendered as Markdown. Evidence mapping, not legal advice. Mirrors sdks/python/src/zanii_blackbox/compliance.py.
|
|
3
|
+
export function complianceReport(frameworkId, framework, facts, meta) {
|
|
4
|
+
return {
|
|
5
|
+
framework: frameworkId,
|
|
6
|
+
title: framework.title,
|
|
7
|
+
...meta,
|
|
8
|
+
facts,
|
|
9
|
+
sections: framework.sections.map((s) => ({
|
|
10
|
+
id: s.id,
|
|
11
|
+
title: s.title,
|
|
12
|
+
requirement: s.requirement,
|
|
13
|
+
evidence: Object.fromEntries(s.facts.map((f) => [f, facts[f] ?? null])),
|
|
14
|
+
})),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const show = (v) => {
|
|
18
|
+
if (v === null)
|
|
19
|
+
return "not set";
|
|
20
|
+
if (typeof v === "object") {
|
|
21
|
+
const keys = Object.keys(v).sort();
|
|
22
|
+
return keys.length === 0 ? "none" : keys.map((k) => `${k}: ${v[k]}`).join(", ");
|
|
23
|
+
}
|
|
24
|
+
return String(v);
|
|
25
|
+
};
|
|
26
|
+
export function renderReport(r) {
|
|
27
|
+
const out = [
|
|
28
|
+
`# ${r.title}`,
|
|
29
|
+
"",
|
|
30
|
+
`Tenant \`${r.tenant_id}\` · sessions created from ${r.from} to ${r.to} (exclusive) · generated ${r.generated_at}`,
|
|
31
|
+
"",
|
|
32
|
+
`> ${r.notes}`,
|
|
33
|
+
];
|
|
34
|
+
for (const s of r.sections) {
|
|
35
|
+
out.push("", `## ${s.title}`, "", s.requirement, "", "| Evidence | Value |", "|---|---|");
|
|
36
|
+
for (const [k, v] of Object.entries(s.evidence))
|
|
37
|
+
out.push(`| ${k} | ${show(v)} |`);
|
|
38
|
+
}
|
|
39
|
+
return `${out.join("\n")}\n`;
|
|
40
|
+
}
|
|
41
|
+
/** spec/compliance.md §1: the facts over sessions' records (the server adds verified / anchored). */
|
|
42
|
+
export function complianceFacts(sessions, dataRegion) {
|
|
43
|
+
const redactions = {};
|
|
44
|
+
const outcomes = { success: 0, failure: 0, abandoned: 0, unlabelled: 0 };
|
|
45
|
+
let events = 0;
|
|
46
|
+
let findingsWarning = 0;
|
|
47
|
+
let bypasses = 0;
|
|
48
|
+
let falseClaims = 0;
|
|
49
|
+
let blocks = 0;
|
|
50
|
+
let emergencies = 0;
|
|
51
|
+
let resumesWithNote = 0;
|
|
52
|
+
for (const s of sessions) {
|
|
53
|
+
let outcome = "unlabelled";
|
|
54
|
+
for (const line of s.lines) {
|
|
55
|
+
events++;
|
|
56
|
+
const e = JSON.parse(line);
|
|
57
|
+
const m = e.meta;
|
|
58
|
+
if (m.redacted && typeof m.redacted === "object")
|
|
59
|
+
for (const [k, v] of Object.entries(m.redacted))
|
|
60
|
+
if (typeof v === "number")
|
|
61
|
+
redactions[k] = (redactions[k] ?? 0) + v;
|
|
62
|
+
if (e.kind === "finding") {
|
|
63
|
+
if (m.severity === "warning")
|
|
64
|
+
findingsWarning++;
|
|
65
|
+
if (m.code === "BYPASS")
|
|
66
|
+
bypasses++;
|
|
67
|
+
if (m.code === "FALSE_CLAIM")
|
|
68
|
+
falseClaims++;
|
|
69
|
+
}
|
|
70
|
+
else if (e.kind === "control") {
|
|
71
|
+
if (m.action === "block")
|
|
72
|
+
m.code === "EMERGENCY" ? emergencies++ : blocks++;
|
|
73
|
+
if (m.action === "resume" && typeof m.note === "string" && m.note !== "")
|
|
74
|
+
resumesWithNote++;
|
|
75
|
+
}
|
|
76
|
+
else if (e.kind === "outcome" && typeof m.outcome === "string" && m.outcome in outcomes)
|
|
77
|
+
outcome = m.outcome;
|
|
78
|
+
}
|
|
79
|
+
outcomes[outcome]++;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
sessions: sessions.length,
|
|
83
|
+
events,
|
|
84
|
+
sessions_verified: sessions.filter((s) => s.verified).length,
|
|
85
|
+
sessions_anchored: sessions.filter((s) => s.anchored).length,
|
|
86
|
+
redactions,
|
|
87
|
+
findings_warning: findingsWarning,
|
|
88
|
+
bypasses,
|
|
89
|
+
false_claims: falseClaims,
|
|
90
|
+
blocks,
|
|
91
|
+
emergency_stops: emergencies,
|
|
92
|
+
resumes_with_note: resumesWithNote,
|
|
93
|
+
outcomes,
|
|
94
|
+
data_region: dataRegion,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** µUSD per million tokens, for the classes a tier or a service tier overrides. */
|
|
2
|
+
export interface ClassPrices {
|
|
3
|
+
input?: number;
|
|
4
|
+
output?: number;
|
|
5
|
+
cache_read?: number;
|
|
6
|
+
cache_write?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ModelPrice {
|
|
9
|
+
input: number;
|
|
10
|
+
output: number;
|
|
11
|
+
cache_read: number;
|
|
12
|
+
cache_write: number;
|
|
13
|
+
/** Tokens; optional (the context-bloat detector uses it). */
|
|
14
|
+
context_window?: number;
|
|
15
|
+
/** Anthropic's 1-hour cache writes (else `cache_write`). */
|
|
16
|
+
cache_write_1h?: number;
|
|
17
|
+
/** Audio in and out (else `input` / `output`). */
|
|
18
|
+
input_audio?: number;
|
|
19
|
+
output_audio?: number;
|
|
20
|
+
/** µUSD per 1,000 web-search requests (Anthropic's server tool); absent: not priced. */
|
|
21
|
+
web_search_per_1k?: number;
|
|
22
|
+
/** Prices once the call's prompt is over `above` tokens (Gemini's >200k tier), highest first. */
|
|
23
|
+
context_tiers?: Array<ClassPrices & {
|
|
24
|
+
above: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** Prices for a provider's service tier (OpenAI `flex`, `priority`; Gemini's traffic type). */
|
|
27
|
+
service_tiers?: Record<string, ClassPrices>;
|
|
28
|
+
/** `deprecated` feeds the MODEL_DEPRECATED finding; `beta` is informational. */
|
|
29
|
+
status?: "deprecated" | "beta";
|
|
30
|
+
}
|
|
31
|
+
export interface PriceTable {
|
|
32
|
+
version: string;
|
|
33
|
+
currency: "USD";
|
|
34
|
+
unit: "micro_usd_per_mtok";
|
|
35
|
+
models: Record<string, ModelPrice>;
|
|
36
|
+
}
|
|
37
|
+
export interface Tokens {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
cache_read: number;
|
|
41
|
+
cache_write: number;
|
|
42
|
+
/** Present only when non-zero (2026-10-01): each is also out of the class it's part of. */
|
|
43
|
+
cache_write_1h?: number;
|
|
44
|
+
input_audio?: number;
|
|
45
|
+
output_audio?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface CallCost {
|
|
48
|
+
seq: number;
|
|
49
|
+
model: string | null;
|
|
50
|
+
tokens: Tokens;
|
|
51
|
+
micro_usd: number | null;
|
|
52
|
+
/** Only when present: web-search requests; the tier the call was priced at; `reported` when
|
|
53
|
+
* the provider's own cost was used (no table price). */
|
|
54
|
+
requests?: {
|
|
55
|
+
web_search: number;
|
|
56
|
+
};
|
|
57
|
+
tier?: {
|
|
58
|
+
service?: string;
|
|
59
|
+
context_above?: number;
|
|
60
|
+
};
|
|
61
|
+
source?: "reported";
|
|
62
|
+
}
|
|
63
|
+
export interface CostReport {
|
|
64
|
+
v: 1;
|
|
65
|
+
/** `pinned`: whether this is the table the session opened with (absent: nothing was pinned). */
|
|
66
|
+
price_table: {
|
|
67
|
+
version: string;
|
|
68
|
+
sha256: string;
|
|
69
|
+
pinned?: boolean;
|
|
70
|
+
};
|
|
71
|
+
total_micro_usd: number;
|
|
72
|
+
total_aed_fils: number;
|
|
73
|
+
calls_priced: number;
|
|
74
|
+
calls_unpriced: number;
|
|
75
|
+
unknown_models: string[];
|
|
76
|
+
tokens: Tokens;
|
|
77
|
+
calls: CallCost[];
|
|
78
|
+
/** Calls served from a replay cassette, left out of the cost (present only when there are any). */
|
|
79
|
+
calls_replayed?: number;
|
|
80
|
+
/** Web-search requests, when there were any. */
|
|
81
|
+
requests?: {
|
|
82
|
+
web_search: number;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface LoadedPrices {
|
|
86
|
+
table: PriceTable;
|
|
87
|
+
sha256: string;
|
|
88
|
+
}
|
|
89
|
+
/** Parses and checks a price table; its sha256 is of the exact file bytes. */
|
|
90
|
+
export declare function loadPrices(bytes: Uint8Array): LoadedPrices;
|
|
91
|
+
/** The exact model, else the longest table key the model starts with; null when there's none. */
|
|
92
|
+
export declare function priceFor(model: string, table: PriceTable,
|
|
93
|
+
/** The configured provider id: `<provider>/<model>` keys are tried first (spec/cost.md §1). */
|
|
94
|
+
provider?: string | null): {
|
|
95
|
+
key: string;
|
|
96
|
+
price: ModelPrice;
|
|
97
|
+
} | null;
|
|
98
|
+
/** Token classes from a recorded, flattened `meta.usage` (spec/cost.md §2). */
|
|
99
|
+
export declare function tokensOf(usage: Record<string, unknown>): Tokens;
|
|
100
|
+
/** Requests a call made to a provider's server tools (Anthropic web search). */
|
|
101
|
+
export declare function requestsOf(usage: Record<string, unknown>): {
|
|
102
|
+
web_search: number;
|
|
103
|
+
} | undefined;
|
|
104
|
+
/** A call's prompt, in tokens: what a context tier is decided on. */
|
|
105
|
+
export declare function promptTokens(t: Tokens): number;
|
|
106
|
+
/** The prices a call pays: a context tier when its prompt is over one, then its service tier. */
|
|
107
|
+
export declare function effectivePrice(price: ModelPrice, tokens: Tokens, serviceTier?: string | null): {
|
|
108
|
+
price: ModelPrice;
|
|
109
|
+
tier?: {
|
|
110
|
+
service?: string;
|
|
111
|
+
context_above?: number;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/** round_half_up((tokens × price + requests × price per 1k × 1000) / 1e6), integers only. */
|
|
115
|
+
export declare function callMicroUsd(tokens: Tokens, price: ModelPrice, requests?: {
|
|
116
|
+
web_search: number;
|
|
117
|
+
}): number;
|
|
118
|
+
/** One call's cost as the cost report prices it: the table (provider-scoped, tiered), else the
|
|
119
|
+
* provider's own reported cost, else null. The spend and duty checks use the same. */
|
|
120
|
+
export declare function priceCall(call: {
|
|
121
|
+
model: string | null;
|
|
122
|
+
usage: Record<string, unknown>;
|
|
123
|
+
provider?: string | null;
|
|
124
|
+
serviceTier?: string | null;
|
|
125
|
+
reportedMicroUsd?: number | null;
|
|
126
|
+
}, table: PriceTable): Omit<CallCost, "seq" | "model"> & {
|
|
127
|
+
key: string | null;
|
|
128
|
+
};
|
|
129
|
+
/** fils at the CBUAE peg (3.6725 AED / USD), rounded half up. */
|
|
130
|
+
export declare function toAedFils(microUsd: number): number;
|
|
131
|
+
export declare function formatAed(fils: number): string;
|
|
132
|
+
/** The session's cost report from its recorded lines. */
|
|
133
|
+
export declare function sessionCost(lines: readonly string[], prices: LoadedPrices): CostReport;
|