@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,33 @@
|
|
|
1
|
+
// The Memory X-ray (spec/agents.md §2). Mirrors analysis/memory.py.
|
|
2
|
+
/** spec/agents.md §2, over parsed events (sdk.event bodies attached). */
|
|
3
|
+
export function memoryXrayOf(events, revokedElsewhere = []) {
|
|
4
|
+
const revoked = new Set(revokedElsewhere);
|
|
5
|
+
const stale = [];
|
|
6
|
+
let writes = 0;
|
|
7
|
+
let reads = 0;
|
|
8
|
+
for (const e of events) {
|
|
9
|
+
if (e.kind !== "sdk.event")
|
|
10
|
+
continue;
|
|
11
|
+
const d = e.data ?? {};
|
|
12
|
+
if (e.meta.type === "memory.write")
|
|
13
|
+
writes++;
|
|
14
|
+
else if (e.meta.type === "memory.revoke" && typeof d.memory_id === "string")
|
|
15
|
+
revoked.add(d.memory_id);
|
|
16
|
+
else if (e.meta.type === "memory.read") {
|
|
17
|
+
reads++;
|
|
18
|
+
for (const id of Array.isArray(d.memory_ids) ? d.memory_ids : [])
|
|
19
|
+
if (typeof id === "string" && revoked.has(id))
|
|
20
|
+
stale.push({ seq: e.seq, memory_id: id });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { writes, reads, revoked: [...revoked].sort(), stale_reads: stale };
|
|
24
|
+
}
|
|
25
|
+
/** The findings part (this record's own revocations only). */
|
|
26
|
+
export function memoryFindings(events) {
|
|
27
|
+
return memoryXrayOf(events).stale_reads.map((r) => ({
|
|
28
|
+
code: "REVOKED_MEMORY_READ",
|
|
29
|
+
source: "memory",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
ref: { seq: r.seq, memory_id: r.memory_id },
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type LoadedPrices } from "../cost/index.ts";
|
|
2
|
+
import { type Bodies } from "../reconcile/record.ts";
|
|
3
|
+
export type WasteReason = "repeat" | "failed" | "outcome";
|
|
4
|
+
export type Outcome = "success" | "failure" | "abandoned";
|
|
5
|
+
export interface WasteReport {
|
|
6
|
+
v: 1;
|
|
7
|
+
outcome: Outcome | null;
|
|
8
|
+
total_micro_usd: number;
|
|
9
|
+
wasted_micro_usd: number;
|
|
10
|
+
wasted_aed_fils: number;
|
|
11
|
+
by_reason: Record<WasteReason, number>;
|
|
12
|
+
calls: Array<{
|
|
13
|
+
seq: number;
|
|
14
|
+
reason: WasteReason;
|
|
15
|
+
micro_usd: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export declare const OUTCOMES: readonly Outcome[];
|
|
19
|
+
export declare function waste(lines: readonly string[], bodies: Bodies, prices: LoadedPrices): WasteReport;
|
|
20
|
+
export interface OutcomeRollup {
|
|
21
|
+
sessions: number;
|
|
22
|
+
outcomes: Record<Outcome | "unlabelled", number>;
|
|
23
|
+
total_micro_usd: number;
|
|
24
|
+
wasted_micro_usd: number;
|
|
25
|
+
/** All the labelled sessions' cost over their successes (J5); null with no success. */
|
|
26
|
+
cost_per_success_micro_usd: number | null;
|
|
27
|
+
}
|
|
28
|
+
/** Rolls session waste reports up (spec/cost.md §5). Integer micro-USD, rounded half up. */
|
|
29
|
+
export declare function rollup(reports: readonly WasteReport[]): OutcomeRollup;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The waste ledger (spec/cost.md §5): which paid model calls were wasted, and why.
|
|
2
|
+
// Mirrors analysis/waste.py.
|
|
3
|
+
import { sessionCost, toAedFils } from "../cost/index.js";
|
|
4
|
+
import { callsOf } from "../reconcile/record.js";
|
|
5
|
+
import { toolCallsOf } from "./detectors.js";
|
|
6
|
+
export const OUTCOMES = ["success", "failure", "abandoned"];
|
|
7
|
+
export function waste(lines, bodies, prices) {
|
|
8
|
+
const cost = sessionCost(lines, prices);
|
|
9
|
+
const status = new Map();
|
|
10
|
+
let outcome = null;
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const e = JSON.parse(line);
|
|
13
|
+
if (e.kind === "llm.response" && typeof e.meta.status === "number")
|
|
14
|
+
status.set(e.seq, e.meta.status);
|
|
15
|
+
else if (e.kind === "llm.incomplete")
|
|
16
|
+
status.set(e.seq, "incomplete");
|
|
17
|
+
else if (e.kind === "outcome" && OUTCOMES.includes(e.meta.outcome))
|
|
18
|
+
outcome = e.meta.outcome; // the last one wins
|
|
19
|
+
}
|
|
20
|
+
// A response whose tool calls all repeat an identical earlier call (same tool + fingerprint).
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const byResponse = new Map();
|
|
23
|
+
for (const t of toolCallsOf(callsOf(lines, bodies))) {
|
|
24
|
+
const key = `${t.tool}\n${t.fp}`;
|
|
25
|
+
byResponse.set(t.seq, [...(byResponse.get(t.seq) ?? []), seen.has(key)]);
|
|
26
|
+
seen.add(key);
|
|
27
|
+
}
|
|
28
|
+
const by_reason = { repeat: 0, failed: 0, outcome: 0 };
|
|
29
|
+
const calls = [];
|
|
30
|
+
for (const c of cost.calls) {
|
|
31
|
+
if (!c.micro_usd)
|
|
32
|
+
continue;
|
|
33
|
+
const s = status.get(c.seq);
|
|
34
|
+
const repeats = byResponse.get(c.seq);
|
|
35
|
+
const reason = s === "incomplete" || (typeof s === "number" && s >= 400)
|
|
36
|
+
? "failed"
|
|
37
|
+
: repeats?.every(Boolean)
|
|
38
|
+
? "repeat"
|
|
39
|
+
: outcome === "failure" || outcome === "abandoned"
|
|
40
|
+
? "outcome"
|
|
41
|
+
: null;
|
|
42
|
+
if (!reason)
|
|
43
|
+
continue;
|
|
44
|
+
by_reason[reason] += c.micro_usd;
|
|
45
|
+
calls.push({ seq: c.seq, reason, micro_usd: c.micro_usd });
|
|
46
|
+
}
|
|
47
|
+
const wasted = by_reason.repeat + by_reason.failed + by_reason.outcome;
|
|
48
|
+
return {
|
|
49
|
+
v: 1,
|
|
50
|
+
outcome,
|
|
51
|
+
total_micro_usd: cost.total_micro_usd,
|
|
52
|
+
wasted_micro_usd: wasted,
|
|
53
|
+
wasted_aed_fils: toAedFils(wasted),
|
|
54
|
+
by_reason,
|
|
55
|
+
calls,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Rolls session waste reports up (spec/cost.md §5). Integer micro-USD, rounded half up. */
|
|
59
|
+
export function rollup(reports) {
|
|
60
|
+
const outcomes = { success: 0, failure: 0, abandoned: 0, unlabelled: 0 };
|
|
61
|
+
let total = 0;
|
|
62
|
+
let wasted = 0;
|
|
63
|
+
let labelledCost = 0;
|
|
64
|
+
for (const r of reports) {
|
|
65
|
+
outcomes[r.outcome ?? "unlabelled"]++;
|
|
66
|
+
total += r.total_micro_usd;
|
|
67
|
+
wasted += r.wasted_micro_usd;
|
|
68
|
+
if (r.outcome)
|
|
69
|
+
labelledCost += r.total_micro_usd;
|
|
70
|
+
}
|
|
71
|
+
const n = outcomes.success;
|
|
72
|
+
return {
|
|
73
|
+
sessions: reports.length,
|
|
74
|
+
outcomes,
|
|
75
|
+
total_micro_usd: total,
|
|
76
|
+
wasted_micro_usd: wasted,
|
|
77
|
+
cost_per_success_micro_usd: n ? Math.floor((labelledCost + Math.floor(n / 2)) / n) : null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ApprovalFinding {
|
|
2
|
+
code: "APPROVAL_REJECTED" | "APPROVAL_TIMEOUT";
|
|
3
|
+
source: "approval";
|
|
4
|
+
severity: "advisory" | "warning";
|
|
5
|
+
ref: {
|
|
6
|
+
seq: number;
|
|
7
|
+
approval_id: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** spec/approvals.md §3: a refused action (advisory), and one nobody confirmed in time (warning). */
|
|
11
|
+
export declare function approvalFindings(lines: readonly string[]): ApprovalFinding[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The two-person rule (spec/approvals.md): what the approval decisions say. Pure; mirrors
|
|
2
|
+
// sdks/python/src/zanii_blackbox/approvals.py; pinned by spec/vectors/approvals.json.
|
|
3
|
+
/** spec/approvals.md §3: a refused action (advisory), and one nobody confirmed in time (warning). */
|
|
4
|
+
export function approvalFindings(lines) {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
const e = JSON.parse(line);
|
|
8
|
+
if (e.kind !== "control" || e.meta.action !== "approval")
|
|
9
|
+
continue;
|
|
10
|
+
const id = typeof e.meta.approval_id === "string" ? e.meta.approval_id : "";
|
|
11
|
+
if (e.meta.decision === "reject")
|
|
12
|
+
out.push({
|
|
13
|
+
code: "APPROVAL_REJECTED",
|
|
14
|
+
source: "approval",
|
|
15
|
+
severity: "advisory",
|
|
16
|
+
ref: { seq: e.seq, approval_id: id },
|
|
17
|
+
});
|
|
18
|
+
else if (e.meta.decision === "timeout")
|
|
19
|
+
out.push({
|
|
20
|
+
code: "APPROVAL_TIMEOUT",
|
|
21
|
+
source: "approval",
|
|
22
|
+
severity: "warning",
|
|
23
|
+
ref: { seq: e.seq, approval_id: id },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Warnings for approval screens (spec/approvals.md §6, idea S5): text a person is asked to
|
|
2
|
+
// approve that may not say what it looks like it says. Mirrors approvals/warnings.py.
|
|
3
|
+
const BIDI = /[\u202A-\u202E\u2066-\u2069]/u;
|
|
4
|
+
const ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/u;
|
|
5
|
+
const URL_RE = /\bhttps?:\/\/([^\s/?#]+)/giu;
|
|
6
|
+
/** What might mislead a person about this text; empty when nothing does. */
|
|
7
|
+
export function textWarnings(text) {
|
|
8
|
+
const out = [];
|
|
9
|
+
if (BIDI.test(text))
|
|
10
|
+
out.push("bidi_control");
|
|
11
|
+
if (ZERO_WIDTH.test(text))
|
|
12
|
+
out.push("zero_width");
|
|
13
|
+
// A word that mixes Latin with Cyrillic or Greek letters: a look-alike (раypal).
|
|
14
|
+
for (const word of text.split(/[^\p{L}]+/u))
|
|
15
|
+
if (/\p{Script=Latin}/u.test(word) && /[\p{Script=Cyrillic}\p{Script=Greek}]/u.test(word)) {
|
|
16
|
+
out.push("mixed_script");
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
const hosts = [...text.matchAll(URL_RE)].map((m) => (m[1] ?? "").toLowerCase());
|
|
20
|
+
if (hosts.some((h) => h.includes("@")))
|
|
21
|
+
out.push("url_userinfo");
|
|
22
|
+
const bare = hosts.map((h) => h.slice(h.lastIndexOf("@") + 1).replace(/:\d+$/, ""));
|
|
23
|
+
if (bare.some((h) => h.split(".").some((label) => label.startsWith("xn--"))))
|
|
24
|
+
out.push("url_punycode");
|
|
25
|
+
if (bare.some((h) => /^\d{1,3}(\.\d{1,3}){3}$/.test(h) || h.startsWith("[")))
|
|
26
|
+
out.push("url_ip");
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A plain command's argv, or null (spec/attestation.md §1). */
|
|
2
|
+
export declare function parseCommand(text: string): string[] | null;
|
|
3
|
+
export declare function isReadOnly(argv: readonly string[]): boolean;
|
|
4
|
+
/** `\r\n` → `\n`; trailing spaces, tabs and line breaks removed (spec/attestation.md §2). */
|
|
5
|
+
export declare const normalise: (s: string) => string;
|
|
6
|
+
export interface Attestation {
|
|
7
|
+
command: string[];
|
|
8
|
+
claimed_sha256: string;
|
|
9
|
+
observed_sha256: string | null;
|
|
10
|
+
match: boolean;
|
|
11
|
+
exit_code: number | null;
|
|
12
|
+
/** The exit code the tool result reported, when it did (L7.3.6): then it must match too. */
|
|
13
|
+
claimed_exit_code?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Re-runs a read-only argv and compares (call isReadOnly first; this refuses anything else). */
|
|
17
|
+
export declare function attest(argv: readonly string[], claimed: string, cwd?: string, claimedExit?: number): Attestation;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Tool-output attestation (spec/attestation.md): re-run a read-only command, no shell, and compare
|
|
2
|
+
// hashes with the output the model was given. Mirrors sdks/python/src/zanii_blackbox/attest.py.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
const SPECIAL = /[|&;<>()$`\\"'*?[\]{}~!#\n\r]/;
|
|
6
|
+
/** A plain command's argv, or null (spec/attestation.md §1). */
|
|
7
|
+
export function parseCommand(text) {
|
|
8
|
+
if (text.length === 0 || text.length > 1000 || SPECIAL.test(text))
|
|
9
|
+
return null;
|
|
10
|
+
const argv = text.split(/[ \t]+/).filter((a) => a.length > 0);
|
|
11
|
+
return argv.length ? argv : null;
|
|
12
|
+
}
|
|
13
|
+
const ANY_ARGS = new Set([
|
|
14
|
+
"cat",
|
|
15
|
+
"head",
|
|
16
|
+
"tail",
|
|
17
|
+
"wc",
|
|
18
|
+
"ls",
|
|
19
|
+
"pwd",
|
|
20
|
+
"grep",
|
|
21
|
+
"rg",
|
|
22
|
+
"stat",
|
|
23
|
+
"du",
|
|
24
|
+
"file",
|
|
25
|
+
"sha256sum",
|
|
26
|
+
"md5sum",
|
|
27
|
+
]);
|
|
28
|
+
const GIT = new Set([
|
|
29
|
+
"status",
|
|
30
|
+
"log",
|
|
31
|
+
"diff",
|
|
32
|
+
"show",
|
|
33
|
+
"rev-parse",
|
|
34
|
+
"ls-files",
|
|
35
|
+
"blame",
|
|
36
|
+
"branch",
|
|
37
|
+
"remote",
|
|
38
|
+
"describe",
|
|
39
|
+
"cat-file",
|
|
40
|
+
]);
|
|
41
|
+
const LISTING = /^(-v|--list|-a|-r)/;
|
|
42
|
+
const programOf = (a) => (a.split(/[\\/]/).pop() ?? a).toLowerCase().replace(/\.exe$/, "");
|
|
43
|
+
export function isReadOnly(argv) {
|
|
44
|
+
const [first, ...args] = argv;
|
|
45
|
+
if (first === undefined)
|
|
46
|
+
return false;
|
|
47
|
+
const program = programOf(first);
|
|
48
|
+
const git = program === "git";
|
|
49
|
+
if (!ANY_ARGS.has(program) && !git)
|
|
50
|
+
return false;
|
|
51
|
+
if (args.some((a) => a.startsWith("--output") ||
|
|
52
|
+
a.startsWith("--exec") ||
|
|
53
|
+
a.startsWith("--ext-diff") ||
|
|
54
|
+
(git && a.startsWith("-o"))))
|
|
55
|
+
return false;
|
|
56
|
+
if (!git)
|
|
57
|
+
return true;
|
|
58
|
+
const [sub, ...rest] = args;
|
|
59
|
+
if (sub === undefined || !GIT.has(sub))
|
|
60
|
+
return false;
|
|
61
|
+
if (sub === "branch" || sub === "remote")
|
|
62
|
+
return rest.every((a) => LISTING.test(a));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** `\r\n` → `\n`; trailing spaces, tabs and line breaks removed (spec/attestation.md §2). */
|
|
66
|
+
export const normalise = (s) => s.replace(/\r\n/g, "\n").replace(/[ \t\n\r\f\v]+$/, "");
|
|
67
|
+
const sha = (s) => `sha256:${createHash("sha256").update(normalise(s), "utf8").digest("hex")}`;
|
|
68
|
+
/** Re-runs a read-only argv and compares (call isReadOnly first; this refuses anything else). */
|
|
69
|
+
export function attest(argv, claimed, cwd, claimedExit) {
|
|
70
|
+
const base = {
|
|
71
|
+
command: [...argv],
|
|
72
|
+
claimed_sha256: sha(claimed),
|
|
73
|
+
...(claimedExit === undefined ? {} : { claimed_exit_code: claimedExit }),
|
|
74
|
+
};
|
|
75
|
+
if (!isReadOnly(argv))
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
observed_sha256: null,
|
|
79
|
+
match: false,
|
|
80
|
+
exit_code: null,
|
|
81
|
+
error: "not read-only",
|
|
82
|
+
};
|
|
83
|
+
const r = spawnSync(argv[0], argv.slice(1), {
|
|
84
|
+
cwd,
|
|
85
|
+
shell: false,
|
|
86
|
+
timeout: 10_000,
|
|
87
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
});
|
|
91
|
+
if (r.error || r.status === null)
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
observed_sha256: null,
|
|
95
|
+
match: false,
|
|
96
|
+
exit_code: null,
|
|
97
|
+
error: r.error ? r.error.message : "killed",
|
|
98
|
+
};
|
|
99
|
+
const observed = sha(r.stdout);
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
observed_sha256: observed,
|
|
103
|
+
match: observed === base.claimed_sha256 && (claimedExit === undefined || claimedExit === r.status),
|
|
104
|
+
exit_code: r.status,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type Mode = "agent" | "supervised" | "human";
|
|
2
|
+
export interface Span {
|
|
3
|
+
mode: Mode;
|
|
4
|
+
from_seq: number;
|
|
5
|
+
/** The seq before the next change; null for the span still in force. */
|
|
6
|
+
to_seq: number | null;
|
|
7
|
+
by: string;
|
|
8
|
+
reason: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface AuthorityFinding {
|
|
11
|
+
code: "AUTOMATION_SURPRISE";
|
|
12
|
+
source: "authority";
|
|
13
|
+
severity: "warning";
|
|
14
|
+
ref: {
|
|
15
|
+
seq: number;
|
|
16
|
+
authority_seq: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** spec/authority.md §3: who was flying, span by span, oldest first. */
|
|
20
|
+
export declare function authorityTimeline(lines: readonly string[]): Span[];
|
|
21
|
+
/** The span that covers `seq`, or null before the session opened. */
|
|
22
|
+
export declare function authorityAt(lines: readonly string[], seq: number): Span | null;
|
|
23
|
+
/** spec/authority.md §4: the agent acted in a `human` span before it knew the mode had changed. */
|
|
24
|
+
export declare function automationSurprise(lines: readonly string[]): AuthorityFinding[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Who had the controls (spec/authority.md): the authority timeline, and automation surprise.
|
|
2
|
+
// Pure; mirrors sdks/python/src/zanii_blackbox/authority.py; pinned by spec/vectors/authority.json.
|
|
3
|
+
const MODES = new Set(["agent", "supervised", "human"]);
|
|
4
|
+
const ADMIN_BLOCKS = new Set(["EMERGENCY", "TAKEOVER"]);
|
|
5
|
+
/** The mode an event sets, or null for one that doesn't change who is flying (spec §2). */
|
|
6
|
+
function change(e) {
|
|
7
|
+
const m = e.meta;
|
|
8
|
+
if (e.kind === "session.open") {
|
|
9
|
+
const mode = typeof m.authority === "string" && MODES.has(m.authority) ? m.authority : "agent";
|
|
10
|
+
return { mode: mode, by: "open", reason: null };
|
|
11
|
+
}
|
|
12
|
+
if (e.kind !== "control")
|
|
13
|
+
return null;
|
|
14
|
+
if (m.action === "block") {
|
|
15
|
+
const code = String(m.code);
|
|
16
|
+
return { mode: "human", by: ADMIN_BLOCKS.has(code) ? "admin" : "gateway", reason: code };
|
|
17
|
+
}
|
|
18
|
+
if (m.action === "resume")
|
|
19
|
+
return { mode: "agent", by: typeof m.by === "string" ? m.by : "admin", reason: "resume" };
|
|
20
|
+
if (m.action === "authority" && typeof m.to === "string" && MODES.has(m.to))
|
|
21
|
+
return {
|
|
22
|
+
mode: m.to,
|
|
23
|
+
by: typeof m.by === "string" ? m.by : "admin",
|
|
24
|
+
reason: typeof m.reason === "string" ? m.reason : null,
|
|
25
|
+
};
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/** spec/authority.md §3: who was flying, span by span, oldest first. */
|
|
29
|
+
export function authorityTimeline(lines) {
|
|
30
|
+
const spans = [];
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const e = JSON.parse(line);
|
|
33
|
+
const c = change(e);
|
|
34
|
+
const current = spans.at(-1);
|
|
35
|
+
if (!c || current?.mode === c.mode)
|
|
36
|
+
continue;
|
|
37
|
+
if (current)
|
|
38
|
+
current.to_seq = e.seq - 1;
|
|
39
|
+
spans.push({ mode: c.mode, from_seq: e.seq, to_seq: null, by: c.by, reason: c.reason });
|
|
40
|
+
}
|
|
41
|
+
return spans;
|
|
42
|
+
}
|
|
43
|
+
/** The span that covers `seq`, or null before the session opened. */
|
|
44
|
+
export function authorityAt(lines, seq) {
|
|
45
|
+
for (const s of authorityTimeline(lines))
|
|
46
|
+
if (seq >= s.from_seq && (s.to_seq === null || seq <= s.to_seq))
|
|
47
|
+
return s;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/** spec/authority.md §4: the agent acted in a `human` span before it knew the mode had changed. */
|
|
51
|
+
export function automationSurprise(lines) {
|
|
52
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const span of authorityTimeline(lines)) {
|
|
55
|
+
if (span.mode !== "human")
|
|
56
|
+
continue;
|
|
57
|
+
for (const e of events) {
|
|
58
|
+
if (e.seq <= span.from_seq || (span.to_seq !== null && e.seq > span.to_seq))
|
|
59
|
+
continue;
|
|
60
|
+
if (e.kind === "control" && e.meta.action === "ack")
|
|
61
|
+
break; // it knew
|
|
62
|
+
const acted = (e.kind === "control" && e.meta.action === "refused") ||
|
|
63
|
+
(e.kind === "sdk.event" && e.meta.type === "tool.call") ||
|
|
64
|
+
e.kind === "tool.call";
|
|
65
|
+
if (acted) {
|
|
66
|
+
out.push({
|
|
67
|
+
code: "AUTOMATION_SURPRISE",
|
|
68
|
+
source: "authority",
|
|
69
|
+
severity: "warning",
|
|
70
|
+
ref: { seq: e.seq, authority_seq: span.from_seq },
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface Plan {
|
|
2
|
+
name: string;
|
|
3
|
+
monthly_fils: number;
|
|
4
|
+
/** Event metering: both or neither. */
|
|
5
|
+
included_events?: number;
|
|
6
|
+
per_1000_events_fils?: number;
|
|
7
|
+
/** Session metering (spec/billing.md §1): both or neither. */
|
|
8
|
+
included_sessions?: number;
|
|
9
|
+
per_session_fils?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface InvoiceLine {
|
|
12
|
+
description: string;
|
|
13
|
+
quantity: number;
|
|
14
|
+
unit_fils: number;
|
|
15
|
+
amount_fils: number;
|
|
16
|
+
}
|
|
17
|
+
export interface Invoice {
|
|
18
|
+
lines: InvoiceLine[];
|
|
19
|
+
subtotal_fils: number;
|
|
20
|
+
vat_percent: number;
|
|
21
|
+
vat_fils: number;
|
|
22
|
+
total_fils: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function invoice(plan: Plan, usage: {
|
|
25
|
+
events: number;
|
|
26
|
+
sessions?: number;
|
|
27
|
+
}, vatPercent?: number): Invoice;
|
|
28
|
+
export interface Plans {
|
|
29
|
+
version: string;
|
|
30
|
+
plans: Record<string, Plan>;
|
|
31
|
+
}
|
|
32
|
+
/** Parses a plans file (spec/billing.md §1); throws on anything that isn't integer fils. */
|
|
33
|
+
export declare function loadPlans(bytes: Uint8Array): Plans;
|
|
34
|
+
export interface TaxInvoiceInput {
|
|
35
|
+
number: string;
|
|
36
|
+
issued_at: string;
|
|
37
|
+
month: string;
|
|
38
|
+
supplier: {
|
|
39
|
+
name: string;
|
|
40
|
+
address: string;
|
|
41
|
+
trn: string;
|
|
42
|
+
};
|
|
43
|
+
customer: {
|
|
44
|
+
legal_name: string;
|
|
45
|
+
address: string;
|
|
46
|
+
trn?: string | null;
|
|
47
|
+
};
|
|
48
|
+
plan_id: string;
|
|
49
|
+
plans_version: string;
|
|
50
|
+
plan: Plan;
|
|
51
|
+
usage: {
|
|
52
|
+
events: number;
|
|
53
|
+
sessions: number;
|
|
54
|
+
};
|
|
55
|
+
vat_percent?: number;
|
|
56
|
+
}
|
|
57
|
+
/** spec/billing.md §5: the tax invoice document. */
|
|
58
|
+
export declare function taxInvoice(input: TaxInvoiceInput): {
|
|
59
|
+
lines: InvoiceLine[];
|
|
60
|
+
subtotal_fils: number;
|
|
61
|
+
vat_percent: number;
|
|
62
|
+
vat_fils: number;
|
|
63
|
+
total_fils: number;
|
|
64
|
+
v: 1;
|
|
65
|
+
title: "Tax Invoice";
|
|
66
|
+
number: string;
|
|
67
|
+
issued_at: string;
|
|
68
|
+
period: {
|
|
69
|
+
from: string;
|
|
70
|
+
to: string;
|
|
71
|
+
};
|
|
72
|
+
date_of_supply: string;
|
|
73
|
+
supplier: {
|
|
74
|
+
name: string;
|
|
75
|
+
address: string;
|
|
76
|
+
trn: string;
|
|
77
|
+
};
|
|
78
|
+
customer: {
|
|
79
|
+
legal_name: string;
|
|
80
|
+
address: string;
|
|
81
|
+
trn: string | null;
|
|
82
|
+
};
|
|
83
|
+
currency: "AED";
|
|
84
|
+
plan: {
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
version: string;
|
|
88
|
+
};
|
|
89
|
+
usage: {
|
|
90
|
+
events: number;
|
|
91
|
+
sessions: number;
|
|
92
|
+
};
|
|
93
|
+
total_aed: string;
|
|
94
|
+
};
|
|
95
|
+
export type TaxInvoice = ReturnType<typeof taxInvoice>;
|
|
96
|
+
/** spec/billing.md §5: the printable copy. */
|
|
97
|
+
export declare function renderTaxInvoice(d: TaxInvoice): string;
|
|
98
|
+
/** spec/billing.md §6: a `Stripe-Signature` header checked against the raw body. */
|
|
99
|
+
export declare function stripeSignatureOk(header: string, rawBody: Uint8Array, secret: string, nowSeconds: number, toleranceSeconds?: number): boolean;
|