@zanii/blackbox 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/faults.js +15 -0
- package/dist/analysis/index.d.ts +1 -0
- package/dist/analysis/index.js +4 -0
- package/dist/analysis/learning.d.ts +54 -0
- package/dist/analysis/learning.js +191 -0
- package/dist/analysis/taxonomy.js +2 -0
- package/dist/cli.js +83 -2
- package/dist/hooks/langchain-agent.d.ts +3 -2
- package/dist/hooks/langchain-agent.js +16 -0
- package/dist/hooks/openai-agents.d.ts +5 -1
- package/dist/hooks/openai-agents.js +26 -8
- package/dist/hooks/witness.d.ts +22 -0
- package/dist/hooks/witness.js +28 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/otlp/index.js +11 -3
- package/dist/session/index.d.ts +40 -0
- package/dist/session/index.js +78 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/analysis/faults.js
CHANGED
|
@@ -232,6 +232,21 @@ export const FAULTS = {
|
|
|
232
232
|
en: "A tool's definition changed mid-session",
|
|
233
233
|
ar: "تغيّر تعريف أداة أثناء الجلسة",
|
|
234
234
|
},
|
|
235
|
+
PROMPT_CHANGED: {
|
|
236
|
+
fault: "BBX-7405",
|
|
237
|
+
en: "The system prompt changed since the last run",
|
|
238
|
+
ar: "تغيّرت تعليمات النظام منذ التشغيل السابق",
|
|
239
|
+
},
|
|
240
|
+
SELF_CHANGE: {
|
|
241
|
+
fault: "BBX-7406",
|
|
242
|
+
en: "The agent changed itself",
|
|
243
|
+
ar: "غيّر الوكيل نفسه",
|
|
244
|
+
},
|
|
245
|
+
SELF_CHANGE_UNAPPROVED: {
|
|
246
|
+
fault: "BBX-7407",
|
|
247
|
+
en: "The agent gave itself more power without approval",
|
|
248
|
+
ar: "منح الوكيل نفسه صلاحيات أكبر دون موافقة",
|
|
249
|
+
},
|
|
235
250
|
USER_DOWNVOTE: {
|
|
236
251
|
fault: "BBX-4601",
|
|
237
252
|
en: "The person served said an answer was wrong",
|
package/dist/analysis/index.d.ts
CHANGED
|
@@ -103,3 +103,4 @@ export declare function verifyRegression(events: Ev[]): Finding[];
|
|
|
103
103
|
/** Stage 2 R4, judged at close: a run labelled (or claimed) a success that its own record contradicts:
|
|
104
104
|
* a check's last result failed, or the last tool call failed with nothing succeeding after it. */
|
|
105
105
|
export declare function falseSuccess(events: Ev[], now: number): Finding[];
|
|
106
|
+
export { type Lessons, lessons, POWER_KINDS, promptChanged, SELF_CHANGE_KINDS, selfChanges, systemPromptHash, } from "./learning.ts";
|
package/dist/analysis/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { groundingFindings, judgeFindings } from "./grounding.js";
|
|
|
16
16
|
import { toolHallucinations } from "./hallucination.js";
|
|
17
17
|
import { injectionFindings } from "./injection.js";
|
|
18
18
|
import { flightPlan } from "./landing.js";
|
|
19
|
+
import { promptChanged, selfChanges } from "./learning.js";
|
|
19
20
|
import { memoryFindings } from "./memory.js";
|
|
20
21
|
export { detectorAccuracy, wilsonLow } from "./accuracy.js";
|
|
21
22
|
export { answerClaims, answerCredential, verifyAnswerCredential, } from "./credential.js";
|
|
@@ -60,6 +61,8 @@ export function analyze(lines, options) {
|
|
|
60
61
|
...toolDrifted(events),
|
|
61
62
|
...secretSent(events),
|
|
62
63
|
...userDownvotes(events),
|
|
64
|
+
...selfChanges(events),
|
|
65
|
+
...promptChanged(events),
|
|
63
66
|
...injectionFindings(lines, options.bodies),
|
|
64
67
|
...verifyRegression(events),
|
|
65
68
|
...falseSuccess(events, now),
|
|
@@ -583,3 +586,4 @@ export function falseSuccess(events, now) {
|
|
|
583
586
|
: "The agent claimed it was done, but its last tool call failed and nothing succeeded after it.");
|
|
584
587
|
return [];
|
|
585
588
|
}
|
|
589
|
+
export { lessons, POWER_KINDS, promptChanged, SELF_CHANGE_KINDS, selfChanges, systemPromptHash, } from "./learning.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Summary } from "../fleet/index.ts";
|
|
2
|
+
import type { Ev, Finding } from "./index.ts";
|
|
3
|
+
/** What a self-change may be about. Anything else is recorded as `other`. */
|
|
4
|
+
export declare const SELF_CHANGE_KINDS: readonly ["prompt", "tool", "policy", "memory", "model", "other"];
|
|
5
|
+
/** Changes that give the agent more power: each needs an approved approval first. */
|
|
6
|
+
export declare const POWER_KINDS: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* The fingerprint of a model request's system prompt: `sha256:` + hex over the canonical JSON of
|
|
9
|
+
* every system part it carries (Anthropic `system`, OpenAI Responses `instructions`, the `system` and
|
|
10
|
+
* `developer` messages of Chat Completions or Responses `input`, Gemini `systemInstruction`), or
|
|
11
|
+
* null when it has none.
|
|
12
|
+
*/
|
|
13
|
+
export declare function systemPromptHash(body: unknown): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* spec/findings.md §1c: each self-change the agent reported (`self_change`) is SELF_CHANGE; one
|
|
16
|
+
* that gives it more power (a tool, a policy or a model) with no approval approved before it is
|
|
17
|
+
* SELF_CHANGE_UNAPPROVED.
|
|
18
|
+
*/
|
|
19
|
+
export declare function selfChanges(events: Ev[]): Finding[];
|
|
20
|
+
/**
|
|
21
|
+
* spec/findings.md §1c: the gateway stamps a request's system prompt fingerprint and, on the
|
|
22
|
+
* session's first one, the fingerprint the previous session with this label began with. A
|
|
23
|
+
* difference is PROMPT_CHANGED: advisory when the agent declared a prompt self-change before it,
|
|
24
|
+
* caution when nothing did.
|
|
25
|
+
*/
|
|
26
|
+
export declare function promptChanged(events: Ev[]): Finding[];
|
|
27
|
+
export interface Lessons {
|
|
28
|
+
label: string | null;
|
|
29
|
+
sessions: number;
|
|
30
|
+
outcomes: {
|
|
31
|
+
success: number;
|
|
32
|
+
failure: number;
|
|
33
|
+
abandoned: number;
|
|
34
|
+
unknown: number;
|
|
35
|
+
};
|
|
36
|
+
codes: Array<{
|
|
37
|
+
code: string;
|
|
38
|
+
title: string | null;
|
|
39
|
+
sessions: number;
|
|
40
|
+
failed_sessions: number;
|
|
41
|
+
findings: number;
|
|
42
|
+
last_seen: string;
|
|
43
|
+
}>;
|
|
44
|
+
claims_failed_sessions: number;
|
|
45
|
+
blocked_sessions: number;
|
|
46
|
+
brief: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* spec/fleet.md §8: what earlier runs of one agent (its label) hit, from their summaries, newest
|
|
50
|
+
* first: outcomes, the finding codes by how many runs they were in and how many of those failed,
|
|
51
|
+
* the runs whose completion claim the record contradicted, and a short brief an agent can put in its
|
|
52
|
+
* own prompt. Codes and counts only, never content.
|
|
53
|
+
*/
|
|
54
|
+
export declare function lessons(summaries: readonly Summary[], label: string | null): Lessons;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// The learning loop (spec/fleet.md §8, spec/findings.md §1c): what earlier runs of an agent hit,
|
|
2
|
+
// for it to read before a run, and the record of any change an agent makes to itself. The agent
|
|
3
|
+
// reads lessons; it never writes to the evidence. Mirrors analysis/learning.py.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { canonical } from "../reconcile/shared.js";
|
|
6
|
+
import { FAULTS } from "./faults.js";
|
|
7
|
+
/** What a self-change may be about. Anything else is recorded as `other`. */
|
|
8
|
+
export const SELF_CHANGE_KINDS = ["prompt", "tool", "policy", "memory", "model", "other"];
|
|
9
|
+
/** Changes that give the agent more power: each needs an approved approval first. */
|
|
10
|
+
export const POWER_KINDS = ["tool", "policy", "model"];
|
|
11
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
12
|
+
/**
|
|
13
|
+
* The fingerprint of a model request's system prompt: `sha256:` + hex over the canonical JSON of
|
|
14
|
+
* every system part it carries (Anthropic `system`, OpenAI Responses `instructions`, the `system` and
|
|
15
|
+
* `developer` messages of Chat Completions or Responses `input`, Gemini `systemInstruction`), or
|
|
16
|
+
* null when it has none.
|
|
17
|
+
*/
|
|
18
|
+
export function systemPromptHash(body) {
|
|
19
|
+
if (!isObj(body))
|
|
20
|
+
return null;
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (body.system !== undefined)
|
|
23
|
+
parts.push(["system", body.system]);
|
|
24
|
+
if (typeof body.instructions === "string")
|
|
25
|
+
parts.push(["instructions", body.instructions]);
|
|
26
|
+
for (const key of ["messages", "input"])
|
|
27
|
+
if (Array.isArray(body[key]))
|
|
28
|
+
for (const m of body[key])
|
|
29
|
+
if (isObj(m) && (m.role === "system" || m.role === "developer"))
|
|
30
|
+
parts.push([m.role, m.content ?? null]);
|
|
31
|
+
if ("systemInstruction" in body || "system_instruction" in body)
|
|
32
|
+
parts.push([
|
|
33
|
+
"systemInstruction",
|
|
34
|
+
"systemInstruction" in body ? body.systemInstruction : body.system_instruction,
|
|
35
|
+
]);
|
|
36
|
+
if (!parts.length)
|
|
37
|
+
return null;
|
|
38
|
+
return `sha256:${createHash("sha256").update(canonical(parts)).digest("hex")}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* spec/findings.md §1c: each self-change the agent reported (`self_change`) is SELF_CHANGE; one
|
|
42
|
+
* that gives it more power (a tool, a policy or a model) with no approval approved before it is
|
|
43
|
+
* SELF_CHANGE_UNAPPROVED.
|
|
44
|
+
*/
|
|
45
|
+
export function selfChanges(events) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const approved = new Map();
|
|
48
|
+
for (const e of events)
|
|
49
|
+
if (e.kind === "control" && e.meta.action === "approval" && e.meta.decision === "approve")
|
|
50
|
+
if (typeof e.meta.approval_id === "string" && !approved.has(e.meta.approval_id))
|
|
51
|
+
approved.set(e.meta.approval_id, e.seq);
|
|
52
|
+
for (const e of events) {
|
|
53
|
+
if (e.kind !== "sdk.event" || e.meta.type !== "self_change")
|
|
54
|
+
continue;
|
|
55
|
+
const d = e.data ?? {};
|
|
56
|
+
const kind = typeof d.kind === "string" && d.kind ? d.kind : "other";
|
|
57
|
+
out.push({
|
|
58
|
+
code: "SELF_CHANGE",
|
|
59
|
+
source: "detectors",
|
|
60
|
+
severity: "advisory",
|
|
61
|
+
ref: { seq: e.seq, kind },
|
|
62
|
+
detail: "The agent reported a change to itself.",
|
|
63
|
+
});
|
|
64
|
+
if (!POWER_KINDS.includes(kind))
|
|
65
|
+
continue;
|
|
66
|
+
const id = typeof d.approval_id === "string" ? d.approval_id : null;
|
|
67
|
+
const at = id === null ? undefined : approved.get(id);
|
|
68
|
+
if (at !== undefined && at < e.seq)
|
|
69
|
+
continue;
|
|
70
|
+
out.push({
|
|
71
|
+
code: "SELF_CHANGE_UNAPPROVED",
|
|
72
|
+
source: "detectors",
|
|
73
|
+
severity: "warning",
|
|
74
|
+
ref: { seq: e.seq, kind, ...(id ? { approval_id: id } : {}) },
|
|
75
|
+
detail: "The agent gave itself more power (a tool, a policy or a model) with no approval approved before it.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* spec/findings.md §1c: the gateway stamps a request's system prompt fingerprint and, on the
|
|
82
|
+
* session's first one, the fingerprint the previous session with this label began with. A
|
|
83
|
+
* difference is PROMPT_CHANGED: advisory when the agent declared a prompt self-change before it,
|
|
84
|
+
* caution when nothing did.
|
|
85
|
+
*/
|
|
86
|
+
export function promptChanged(events) {
|
|
87
|
+
for (const e of events) {
|
|
88
|
+
const { system_sha256: after, system_sha256_prev: before } = e.meta;
|
|
89
|
+
if (e.kind !== "llm.request" || typeof after !== "string" || typeof before !== "string")
|
|
90
|
+
continue;
|
|
91
|
+
if (after === before)
|
|
92
|
+
return [];
|
|
93
|
+
const declared = events.some((s) => s.seq < e.seq &&
|
|
94
|
+
s.kind === "sdk.event" &&
|
|
95
|
+
s.meta.type === "self_change" &&
|
|
96
|
+
s.data?.kind === "prompt");
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
code: "PROMPT_CHANGED",
|
|
100
|
+
source: "detectors",
|
|
101
|
+
severity: declared ? "advisory" : "caution",
|
|
102
|
+
ref: { seq: e.seq, before, after, declared },
|
|
103
|
+
detail: declared
|
|
104
|
+
? "The system prompt differs from the one the last run of this agent began with; the agent declared the change."
|
|
105
|
+
: "The system prompt differs from the one the last run of this agent began with, and nothing declared the change.",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
/** At most this many codes in a lesson: the ones seen in the most runs. */
|
|
112
|
+
const TOP = 10;
|
|
113
|
+
/**
|
|
114
|
+
* spec/fleet.md §8: what earlier runs of one agent (its label) hit, from their summaries, newest
|
|
115
|
+
* first: outcomes, the finding codes by how many runs they were in and how many of those failed,
|
|
116
|
+
* the runs whose completion claim the record contradicted, and a short brief an agent can put in its
|
|
117
|
+
* own prompt. Codes and counts only, never content.
|
|
118
|
+
*/
|
|
119
|
+
export function lessons(summaries, label) {
|
|
120
|
+
const outcomes = { success: 0, failure: 0, abandoned: 0, unknown: 0 };
|
|
121
|
+
const codes = new Map();
|
|
122
|
+
let claimsFailed = 0;
|
|
123
|
+
let blocked = 0;
|
|
124
|
+
for (const s of summaries) {
|
|
125
|
+
const o = s.outcome;
|
|
126
|
+
const failed = o === "failure";
|
|
127
|
+
if (o === "success" || o === "failure" || o === "abandoned")
|
|
128
|
+
outcomes[o]++;
|
|
129
|
+
else
|
|
130
|
+
outcomes.unknown++;
|
|
131
|
+
if ((s.claims?.failed ?? 0) > 0)
|
|
132
|
+
claimsFailed++;
|
|
133
|
+
if (s.blocked_by)
|
|
134
|
+
blocked++;
|
|
135
|
+
for (const [code, n] of Object.entries(s.finding_codes ?? {})) {
|
|
136
|
+
const c = codes.get(code) ?? { sessions: 0, failed_sessions: 0, findings: 0, last_seen: "" };
|
|
137
|
+
c.sessions++;
|
|
138
|
+
if (failed)
|
|
139
|
+
c.failed_sessions++;
|
|
140
|
+
c.findings += n;
|
|
141
|
+
if (s.started_at > c.last_seen)
|
|
142
|
+
c.last_seen = s.started_at;
|
|
143
|
+
codes.set(code, c);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const top = [...codes.entries()]
|
|
147
|
+
.sort((a, b) => b[1].sessions - a[1].sessions || (a[0] < b[0] ? -1 : 1))
|
|
148
|
+
.slice(0, TOP)
|
|
149
|
+
.map(([code, c]) => ({
|
|
150
|
+
code,
|
|
151
|
+
title: FAULTS[code]?.en ?? null,
|
|
152
|
+
...c,
|
|
153
|
+
}));
|
|
154
|
+
const n = summaries.length;
|
|
155
|
+
const name = label === null ? "this agent" : `"${label}"`;
|
|
156
|
+
let brief;
|
|
157
|
+
if (n === 0)
|
|
158
|
+
brief = `No earlier runs of ${name} on record.`;
|
|
159
|
+
else {
|
|
160
|
+
const parts = [`Lessons from the last ${n} run${n === 1 ? "" : "s"} of ${name}:`];
|
|
161
|
+
const counts = [`${outcomes.success} succeeded`, `${outcomes.failure} failed`];
|
|
162
|
+
if (outcomes.abandoned)
|
|
163
|
+
counts.push(`${outcomes.abandoned} abandoned`);
|
|
164
|
+
if (outcomes.unknown)
|
|
165
|
+
counts.push(`${outcomes.unknown} without an outcome`);
|
|
166
|
+
parts.push(`${counts.join(", ")}.`);
|
|
167
|
+
if (top.length)
|
|
168
|
+
parts.push(`Problems seen: ${top
|
|
169
|
+
.slice(0, 5)
|
|
170
|
+
.map((c) => `${c.code}${c.title ? ` (${c.title.toLowerCase()})` : ""} in ${c.sessions} run${c.sessions === 1 ? "" : "s"}${c.failed_sessions ? (c.sessions === 1 ? ", which failed" : `, ${c.failed_sessions} of them failed`) : ""}`)
|
|
171
|
+
.join("; ")}.`);
|
|
172
|
+
else
|
|
173
|
+
parts.push("No problems were found in them.");
|
|
174
|
+
if (claimsFailed)
|
|
175
|
+
parts.push(`${claimsFailed} run${claimsFailed === 1 ? "" : "s"} claimed to be done and the record disagreed.`);
|
|
176
|
+
if (blocked)
|
|
177
|
+
parts.push(`${blocked} run${blocked === 1 ? " was" : "s were"} stopped by the gateway.`);
|
|
178
|
+
if (top.length || claimsFailed)
|
|
179
|
+
parts.push("Avoid repeating these.");
|
|
180
|
+
brief = parts.join(" ");
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
label,
|
|
184
|
+
sessions: n,
|
|
185
|
+
outcomes,
|
|
186
|
+
codes: top,
|
|
187
|
+
claims_failed_sessions: claimsFailed,
|
|
188
|
+
blocked_sessions: blocked,
|
|
189
|
+
brief,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -56,6 +56,8 @@ export const TAXONOMY = {
|
|
|
56
56
|
TOOL_CHANGED: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
|
|
57
57
|
TOOL_DRIFTED: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
|
|
58
58
|
PROMPT_INJECTION: { owasp: ["ASI01"], atlas: ["AML.T0051"] },
|
|
59
|
+
PROMPT_CHANGED: { owasp: ["ASI04"] },
|
|
60
|
+
SELF_CHANGE_UNAPPROVED: { owasp: ["ASI10"] },
|
|
59
61
|
SECRET_SENT: { owasp: ["ASI03"], atlas: ["AML.T0057"] },
|
|
60
62
|
TOOL_OUTPUT_MISMATCH: { owasp: ["ASI04", "ASI06"], atlas: ["AML.T0110.002", "AML.T0099"] },
|
|
61
63
|
TOOL_WITNESS_INVALID: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The `blackbox` command. Mirrors the Python SDK's `python -m zanii_blackbox.<command>` entry points.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -25,6 +26,8 @@ const USAGE = `usage (spec/cli.md):
|
|
|
25
26
|
blackbox occurrence-pack <session_id> --framework <id>
|
|
26
27
|
blackbox file <session_id> --framework <id> --reference <ref> [--channel <c>] [--filed-at <iso>]
|
|
27
28
|
blackbox replay <session_id> [--fork] [--fork-after N] [--label L] [--drill <drill.json>]
|
|
29
|
+
blackbox scenario save <session_id> [--out <file>]
|
|
30
|
+
blackbox scenario run <scenario.json>... -- <agent command> (1 when an incident's findings recur)
|
|
28
31
|
blackbox tail <session_id> [--after-seq N]
|
|
29
32
|
blackbox close <session_id> [--outcome success|failure|abandoned] [--note <text>]
|
|
30
33
|
blackbox resume <session_id> [--token <resume token>] [--note <text>] [--force] (a paused session needs no token)
|
|
@@ -94,6 +97,8 @@ async function main(argv) {
|
|
|
94
97
|
return occurrenceCommand(rest);
|
|
95
98
|
if (command === "replay")
|
|
96
99
|
return replayCommand(rest);
|
|
100
|
+
if (command === "scenario")
|
|
101
|
+
return scenarioCommand(rest);
|
|
97
102
|
if (command === "analyze")
|
|
98
103
|
return analyzeCommand(rest);
|
|
99
104
|
if (command === "verify-certificate")
|
|
@@ -405,9 +410,10 @@ class ServerError extends Error {
|
|
|
405
410
|
/** GET from the gateway's admin API (BLACKBOX_URL + BLACKBOX_ADMIN_KEY). */
|
|
406
411
|
async function admin(path, body, allow = []) {
|
|
407
412
|
const url = process.env.BLACKBOX_URL;
|
|
408
|
-
|
|
413
|
+
// a tenant's key works too, for what a tenant may read (the scenarios in CI)
|
|
414
|
+
const key = process.env.BLACKBOX_ADMIN_KEY || process.env.BLACKBOX_API_KEY;
|
|
409
415
|
if (!url || !key)
|
|
410
|
-
throw new ServerError("BLACKBOX_URL and BLACKBOX_ADMIN_KEY must be set");
|
|
416
|
+
throw new ServerError("BLACKBOX_URL and BLACKBOX_ADMIN_KEY must be set (or a tenant's BLACKBOX_API_KEY)");
|
|
411
417
|
let res;
|
|
412
418
|
try {
|
|
413
419
|
res = await fetch(`${url.replace(/\/+$/, "")}${path}`, {
|
|
@@ -641,6 +647,81 @@ async function replayCommand(rest) {
|
|
|
641
647
|
process.stdout.write(`${JSON.stringify(doc)}\n`);
|
|
642
648
|
return 0;
|
|
643
649
|
}
|
|
650
|
+
/** spec/replay.md §3a: saves an incident as a regression test, or reruns the agent against each
|
|
651
|
+
* saved one and fails when any of the incident's findings comes back. */
|
|
652
|
+
async function scenarioCommand(rest) {
|
|
653
|
+
const at = rest.indexOf("--");
|
|
654
|
+
const command = at === -1 ? [] : rest.slice(at + 1);
|
|
655
|
+
const { flags, positional } = parse(at === -1 ? rest : rest.slice(0, at), []);
|
|
656
|
+
const [verb, ...args] = positional;
|
|
657
|
+
if (verb === "save" && args.length === 1 && SESSION_ID.test(args[0])) {
|
|
658
|
+
const doc = await (await admin(`/v1/sessions/${args[0]}/scenario`)).json();
|
|
659
|
+
const out = flags.out || `${args[0]}.scenario.json`;
|
|
660
|
+
writeFileSync(out, `${JSON.stringify(doc)}\n`);
|
|
661
|
+
process.stdout.write(`${out}\n`);
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
if (verb !== "run" || args.length === 0 || command.length === 0)
|
|
665
|
+
return usage();
|
|
666
|
+
let failed = false;
|
|
667
|
+
for (const file of args) {
|
|
668
|
+
const result = await scenarioRun(file, command);
|
|
669
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
670
|
+
failed ||= !result.ok;
|
|
671
|
+
}
|
|
672
|
+
return failed ? 1 : 0;
|
|
673
|
+
}
|
|
674
|
+
/** One scenario: a fork of its incident, the agent run against it (its output on stderr, so
|
|
675
|
+
* stdout stays the results), closed, and its findings compared with the incident's. */
|
|
676
|
+
async function scenarioRun(file, command) {
|
|
677
|
+
let scenario;
|
|
678
|
+
try {
|
|
679
|
+
scenario = JSON.parse(readFileSync(file, "utf8"));
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
throw new ServerError(`${file}: ${error instanceof Error ? error.message : error}`);
|
|
683
|
+
}
|
|
684
|
+
const source = scenario?.source;
|
|
685
|
+
const expect = scenario?.expect?.finding_codes;
|
|
686
|
+
if (scenario?.v !== 1 || typeof source !== "string" || !SESSION_ID.test(source))
|
|
687
|
+
throw new ServerError(`${file}: not a scenario (spec/replay.md §3)`);
|
|
688
|
+
const before = (expect && typeof expect === "object" ? expect : {});
|
|
689
|
+
const label = `regression: ${scenario.label || source}`.slice(0, 200);
|
|
690
|
+
const opened = (await (await admin("/v1/sessions", { label, replay: { of: source, fork: true } })).json());
|
|
691
|
+
const sid = String(opened.session_id);
|
|
692
|
+
const token = String(opened.token);
|
|
693
|
+
const url = String(process.env.BLACKBOX_URL).replace(/\/+$/, "");
|
|
694
|
+
const run = spawnSync(command[0], command.slice(1), {
|
|
695
|
+
stdio: ["inherit", 2, "inherit"], // the agent prints to stderr: stdout is the results
|
|
696
|
+
env: {
|
|
697
|
+
...process.env,
|
|
698
|
+
BLACKBOX_SESSION_ID: sid,
|
|
699
|
+
BLACKBOX_TOKEN: token,
|
|
700
|
+
BLACKBOX_SCENARIO: file,
|
|
701
|
+
// the official clients read these: an agent that doesn't set its own goes through the fork
|
|
702
|
+
ANTHROPIC_BASE_URL: `${url}/anthropic`,
|
|
703
|
+
ANTHROPIC_API_KEY: token,
|
|
704
|
+
OPENAI_BASE_URL: `${url}/openai/v1`,
|
|
705
|
+
OPENAI_API_KEY: token,
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
if (run.error)
|
|
709
|
+
process.stderr.write(`blackbox: can't run ${command[0]}: ${run.error.message}\n`);
|
|
710
|
+
const code = run.error ? 127 : (run.status ?? 1);
|
|
711
|
+
await admin(`/v1/sessions/${sid}/close`, { outcome: code === 0 ? "success" : "failure" });
|
|
712
|
+
const after = (await (await admin(`/v1/sessions/${sid}/scenario`)).json());
|
|
713
|
+
const got = (after?.expect?.finding_codes ?? {});
|
|
714
|
+
const recurred = Object.fromEntries(Object.keys(before).flatMap((c) => (got[c] ? [[c, got[c]]] : [])));
|
|
715
|
+
return {
|
|
716
|
+
scenario: file,
|
|
717
|
+
source,
|
|
718
|
+
session_id: sid,
|
|
719
|
+
exit_code: code,
|
|
720
|
+
ok: code === 0 && Object.keys(recurred).length === 0,
|
|
721
|
+
recurred,
|
|
722
|
+
new: Object.fromEntries(Object.entries(got).filter(([c]) => !(c in before))),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
644
725
|
/** Audit S12: an exported bundle, analysed offline: it must verify first, then the summary, the
|
|
645
726
|
* findings it recorded, and the findings this SDK's analyses find in it now. */
|
|
646
727
|
async function analyzeCommand(rest) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type CompiledPolicy } from "../policy/index.ts";
|
|
2
2
|
import { type EventSink } from "./shared.ts";
|
|
3
|
+
import { type WitnessOptions, type WitnessSink } from "./witness.ts";
|
|
3
4
|
/** A BlackboxSession, or anything with the same methods. */
|
|
4
|
-
export interface AgentSink extends EventSink {
|
|
5
|
+
export interface AgentSink extends EventSink, WitnessSink {
|
|
5
6
|
state?(): Promise<{
|
|
6
7
|
control?: {
|
|
7
8
|
state?: string;
|
|
@@ -29,7 +30,7 @@ export declare function blackboxAgentMiddleware(session: AgentSink, options?: {
|
|
|
29
30
|
checkBlock?: boolean;
|
|
30
31
|
/** Files with a machine-managed block (idea S5): file tools may not change it. */
|
|
31
32
|
protect?: readonly string[];
|
|
32
|
-
}): {
|
|
33
|
+
} & WitnessOptions): {
|
|
33
34
|
[x: symbol]: true;
|
|
34
35
|
name: string;
|
|
35
36
|
beforeAgent(): Promise<undefined>;
|
|
@@ -11,6 +11,7 @@ import { decide } from "../policy/index.js";
|
|
|
11
11
|
import { stableEventId } from "../session/index.js";
|
|
12
12
|
import { protectedEdit } from "./protect.js";
|
|
13
13
|
import { llmIds, neverThrow, providerOf } from "./shared.js";
|
|
14
|
+
import { shellCommand, toolText } from "./witness.js";
|
|
14
15
|
/** Thrown by `beforeAgent` when the gateway has stopped this session (spec/control.md). */
|
|
15
16
|
export class BlackboxStoppedError extends Error {
|
|
16
17
|
constructor(state) {
|
|
@@ -117,13 +118,28 @@ export function blackboxAgentMiddleware(session, options = {}) {
|
|
|
117
118
|
return (await refusal(call, `a second person's approval is needed (${answer})`));
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
const receipt = async (phase) => {
|
|
122
|
+
if (!options.workspace || !key || !session.workspaceReceipt)
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
await session.workspaceReceipt(phase, key, { tool: call.name, cwd: options.workspace });
|
|
126
|
+
}
|
|
127
|
+
catch { } // a receipt never replaces the tool's own result or error
|
|
128
|
+
};
|
|
129
|
+
await receipt("pre");
|
|
120
130
|
try {
|
|
121
131
|
const out = await handler(request);
|
|
132
|
+
await receipt("post");
|
|
133
|
+
const command = options.witnessShell ? shellCommand(call.args) : null;
|
|
134
|
+
const text = command ? toolText(out) : null;
|
|
135
|
+
if (command && text !== null)
|
|
136
|
+
neverThrow(() => session.witness?.(command, text, { cwd: options.workspace }));
|
|
122
137
|
const status = out?.status;
|
|
123
138
|
emit("tool.result", call.name, { ok: status !== "error", ...(key ? { run_id: key } : {}) }, key);
|
|
124
139
|
return out;
|
|
125
140
|
}
|
|
126
141
|
catch (error) {
|
|
142
|
+
await receipt("post");
|
|
127
143
|
if (isControl(error)) {
|
|
128
144
|
emit("interrupt", call.name, { ...(key ? { run_id: key } : {}) }, key);
|
|
129
145
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type EventSink } from "./shared.ts";
|
|
2
|
+
import { type WitnessOptions, type WitnessSink } from "./witness.ts";
|
|
2
3
|
interface HookEmitter {
|
|
3
4
|
on(event: any, listener: any): unknown;
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
+
/** `options.workspace`: a receipt of that git workspace before and after each tool call;
|
|
7
|
+
* `options.witnessShell`: re-run each read-only shell command and compare its output. The pre
|
|
8
|
+
* receipt's tree is taken synchronously in the listener, so before the tool runs. */
|
|
9
|
+
export declare function attachBlackbox(runner: HookEmitter, session: EventSink & WitnessSink, options?: WitnessOptions): void;
|
|
6
10
|
export {};
|
|
@@ -5,23 +5,37 @@
|
|
|
5
5
|
// The JS SDK's hooks are events on the Runner (agent / handoff / tool); it has no model-call event,
|
|
6
6
|
// so model-call ids come from the gateway alone here.
|
|
7
7
|
import { neverThrow } from "./shared.js";
|
|
8
|
+
import { shellCommand } from "./witness.js";
|
|
8
9
|
const nameOf = (x) => String(x?.name ?? "agent").slice(0, 256);
|
|
9
10
|
/** Audit K23: the SDK turns a tool's exception into this answer (its default error function), so a
|
|
10
11
|
* tool that failed isn't recorded as a success. */
|
|
11
12
|
const TOOL_ERROR = "An error occurred while running the tool.";
|
|
12
13
|
const failed = (result) => typeof result === "string" && result.startsWith(TOOL_ERROR);
|
|
13
|
-
|
|
14
|
+
const argsOf = (details) => {
|
|
15
|
+
const raw = details?.toolCall?.arguments;
|
|
16
|
+
try {
|
|
17
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return raw;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/** `options.workspace`: a receipt of that git workspace before and after each tool call;
|
|
24
|
+
* `options.witnessShell`: re-run each read-only shell command and compare its output. The pre
|
|
25
|
+
* receipt's tree is taken synchronously in the listener, so before the tool runs. */
|
|
26
|
+
export function attachBlackbox(runner, session, options = {}) {
|
|
27
|
+
const receipt = (phase, tool, details) => {
|
|
28
|
+
const callId = details?.toolCall?.callId;
|
|
29
|
+
if (options.workspace && callId && session.workspaceReceipt)
|
|
30
|
+
Promise.resolve(session.workspaceReceipt(phase, callId, { tool: nameOf(tool), cwd: options.workspace })).catch(() => { }); // never an unhandled rejection in the agent
|
|
31
|
+
};
|
|
14
32
|
const on = (event, listener) => runner.on(event, ((...args) => neverThrow(() => listener(...args))));
|
|
15
33
|
on("agent_start", (_context, agent) => session.event("step", nameOf(agent), {}));
|
|
16
34
|
on("agent_handoff", (_context, from, to) => session.event("step", nameOf(to), { handoff_from: nameOf(from) }));
|
|
17
35
|
on("agent_tool_start", (_context, agent, tool, details) => {
|
|
18
|
-
const call = details
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
args = typeof args === "string" ? JSON.parse(args) : args;
|
|
23
|
-
}
|
|
24
|
-
catch { }
|
|
36
|
+
const call = details?.toolCall;
|
|
37
|
+
const args = argsOf(details);
|
|
38
|
+
receipt("pre", tool, details);
|
|
25
39
|
session.event("tool.call", nameOf(tool), {
|
|
26
40
|
agent: nameOf(agent),
|
|
27
41
|
...(call?.callId ? { call_id: call.callId } : {}),
|
|
@@ -30,11 +44,15 @@ export function attachBlackbox(runner, session) {
|
|
|
30
44
|
});
|
|
31
45
|
on("agent_tool_end", (_context, agent, tool, result, details) => {
|
|
32
46
|
const callId = details?.toolCall?.callId;
|
|
47
|
+
receipt("post", tool, details);
|
|
33
48
|
session.event("tool.result", nameOf(tool), {
|
|
34
49
|
ok: !failed(result),
|
|
35
50
|
agent: nameOf(agent),
|
|
36
51
|
...(callId ? { call_id: callId } : {}),
|
|
37
52
|
...(failed(result) ? { error: String(result).slice(0, 500) } : {}),
|
|
38
53
|
});
|
|
54
|
+
const command = options.witnessShell ? shellCommand(argsOf(details)) : null;
|
|
55
|
+
if (command && typeof result === "string" && !failed(result))
|
|
56
|
+
session.witness?.(command, result, { cwd: options.workspace });
|
|
39
57
|
});
|
|
40
58
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** What a framework hook passes for the receipts and the witness. Both off unless set. */
|
|
2
|
+
export interface WitnessOptions {
|
|
3
|
+
/** A directory in a git repository: a receipt of its files before and after each tool call. */
|
|
4
|
+
workspace?: string;
|
|
5
|
+
/** Re-run each read-only shell command the agent ran and compare its output (attestation.md). */
|
|
6
|
+
witnessShell?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** The session methods the hooks call, when the sink has them (a BlackboxSession does). */
|
|
9
|
+
export interface WitnessSink {
|
|
10
|
+
workspaceReceipt?(phase: "pre" | "post", toolUseId: string, options?: {
|
|
11
|
+
tool?: string;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}): Promise<string | null>;
|
|
14
|
+
witness?(command: string, claimed: string, options?: {
|
|
15
|
+
cwd?: string | undefined;
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
}): unknown;
|
|
18
|
+
}
|
|
19
|
+
/** The shell command a tool call ran, from its arguments (`command`, `cmd`, or `commands`). */
|
|
20
|
+
export declare function shellCommand(args: unknown): string | null;
|
|
21
|
+
/** A tool's answer as text: a string, or a LangChain message's content (string or text parts). */
|
|
22
|
+
export declare function toolText(out: unknown): string | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// File receipts and a shell witness from any framework's tool hooks (spec/sdk.md §5.3). The
|
|
2
|
+
// session does the work (`workspaceReceipt`, `witness`); this finds what to give it in a tool call.
|
|
3
|
+
// Mirrors hooks/witness.py.
|
|
4
|
+
/** The shell command a tool call ran, from its arguments (`command`, `cmd`, or `commands`). */
|
|
5
|
+
export function shellCommand(args) {
|
|
6
|
+
const a = (args ?? {});
|
|
7
|
+
for (const k of ["command", "cmd", "commands"]) {
|
|
8
|
+
const v = a[k];
|
|
9
|
+
if (typeof v === "string" && v.trim())
|
|
10
|
+
return v;
|
|
11
|
+
if (Array.isArray(v) && v.length === 1 && typeof v[0] === "string")
|
|
12
|
+
return v[0];
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/** A tool's answer as text: a string, or a LangChain message's content (string or text parts). */
|
|
17
|
+
export function toolText(out) {
|
|
18
|
+
if (typeof out === "string")
|
|
19
|
+
return out;
|
|
20
|
+
const content = out?.content;
|
|
21
|
+
if (typeof content === "string")
|
|
22
|
+
return content;
|
|
23
|
+
if (Array.isArray(content))
|
|
24
|
+
return content
|
|
25
|
+
.map((p) => (typeof p === "string" ? p : (p?.text ?? "")))
|
|
26
|
+
.join("");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { A2A_STREAMING, type AgentCardReport, a2aMethod, a2aRequestMeta, a2aResp
|
|
|
2
2
|
export { causality, memoryXray } from "./agents/index.ts";
|
|
3
3
|
export { type DetectorOptions, detectors, fingerprint } from "./analysis/detectors.ts";
|
|
4
4
|
export { FAULTS, type Fault, faultOf } from "./analysis/faults.ts";
|
|
5
|
-
export { type Accuracy, type AnalyzeOptions, type AnswerClaims, analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, type ClaimEntry, type ClaimVerdict, type CriterionResult, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, type Fact, type Finding as AnalysisFinding, factsIn, findingKey, type GroundedClaim, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, type Label as AccuracyLabel, type Landing, landing, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, type Outcome, type OutcomeRollup, packsFor, parseGroundingAnswer, parseJudgeAnswer, type ReferenceFact, type ReferencePack, type ReferenceVerdict, referenceContext, rollup, type Severity, schemaProblem, secretSent, toolDrifted, toolHallucinations, userDownvotes, verifyAnswerCredential, type WasteReason, type WasteReport, waste, wilsonLow, } from "./analysis/index.ts";
|
|
5
|
+
export { type Accuracy, type AnalyzeOptions, type AnswerClaims, analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, type ClaimEntry, type ClaimVerdict, type CriterionResult, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, type Fact, type Finding as AnalysisFinding, factsIn, findingKey, type GroundedClaim, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, type Label as AccuracyLabel, type Landing, landing, lessons, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, type Outcome, type OutcomeRollup, packsFor, parseGroundingAnswer, parseJudgeAnswer, promptChanged, type ReferenceFact, type ReferencePack, type ReferenceVerdict, referenceContext, rollup, type Severity, schemaProblem, secretSent, selfChanges, systemPromptHash, toolDrifted, toolHallucinations, userDownvotes, verifyAnswerCredential, type WasteReason, type WasteReport, waste, wilsonLow, } from "./analysis/index.ts";
|
|
6
6
|
export { injectionFindings, injectionRule } from "./analysis/injection.ts";
|
|
7
7
|
export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, type Taxonomy, taxonomyOf, } from "./analysis/taxonomy.ts";
|
|
8
8
|
export { type ApprovalFinding, approvalConfirmation, approvalFindings, argsHash, verifyConfirmation, } from "./approvals/index.ts";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { A2A_STREAMING, a2aMethod, a2aRequestMeta, a2aResponseMeta, a2aRestMetho
|
|
|
4
4
|
export { causality, memoryXray } from "./agents/index.js";
|
|
5
5
|
export { detectors, fingerprint } from "./analysis/detectors.js";
|
|
6
6
|
export { FAULTS, faultOf } from "./analysis/faults.js";
|
|
7
|
-
export { analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, factsIn, findingKey, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, landing, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, packsFor, parseGroundingAnswer, parseJudgeAnswer, referenceContext, rollup, schemaProblem, secretSent, toolDrifted, toolHallucinations, userDownvotes, verifyAnswerCredential, waste, wilsonLow, } from "./analysis/index.js";
|
|
7
|
+
export { analyze, answerClaims, answerCredential, answerRisk, answersOf, answerText, canonicalNumber, checkAgainstPacks, checkFlightPlan, checkpointReplay, detectorAccuracy, dualWitness, Evidence, eventsOf, factsIn, findingKey, groundingFindings, groundingRequest, HOLD_TRIGGERS, identifiersIn, judgeFindings, judgeRequest, landing, lessons, loadReferencePacks, modelGroundingFindings, normaliseValue, OUTCOMES, packsFor, parseGroundingAnswer, parseJudgeAnswer, promptChanged, referenceContext, rollup, schemaProblem, secretSent, selfChanges, systemPromptHash, toolDrifted, toolHallucinations, userDownvotes, verifyAnswerCredential, waste, wilsonLow, } from "./analysis/index.js";
|
|
8
8
|
export { injectionFindings, injectionRule } from "./analysis/injection.js";
|
|
9
9
|
export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, taxonomyOf, } from "./analysis/taxonomy.js";
|
|
10
10
|
export { approvalConfirmation, approvalFindings, argsHash, verifyConfirmation, } from "./approvals/index.js";
|
package/dist/otlp/index.js
CHANGED
|
@@ -398,7 +398,8 @@ function toOtlpV2(lines) {
|
|
|
398
398
|
],
|
|
399
399
|
};
|
|
400
400
|
}
|
|
401
|
-
|
|
401
|
+
// Spring AI says `embedding` (singular) for the semconv `embeddings`
|
|
402
|
+
const LLM_OPS = new Set(["chat", "text_completion", "generate_content", "embeddings", "embedding"]);
|
|
402
403
|
const MAX_SPANS = 5000;
|
|
403
404
|
const MAX_DATA = 60_000;
|
|
404
405
|
function anyValue(v) {
|
|
@@ -433,13 +434,20 @@ function attributes(list) {
|
|
|
433
434
|
function spanType(a) {
|
|
434
435
|
const op = a["gen_ai.operation.name"];
|
|
435
436
|
const kind = a["openinference.span.kind"];
|
|
437
|
+
const spring = a["spring.ai.kind"]; // Spring AI's own span kinds
|
|
436
438
|
if ((typeof op === "string" && LLM_OPS.has(op)) || kind === "LLM" || kind === "EMBEDDING")
|
|
437
439
|
return "llm.call";
|
|
438
440
|
if (op === "execute_tool" || kind === "TOOL")
|
|
439
441
|
return "tool.call";
|
|
440
|
-
if (kind === "RETRIEVER")
|
|
442
|
+
if (kind === "RETRIEVER" || spring === "vector_store")
|
|
441
443
|
return "retrieval";
|
|
442
|
-
if (op === "invoke_agent" ||
|
|
444
|
+
if (op === "invoke_agent" ||
|
|
445
|
+
op === "create_agent" ||
|
|
446
|
+
op === "framework" ||
|
|
447
|
+
kind === "AGENT" ||
|
|
448
|
+
kind === "CHAIN" ||
|
|
449
|
+
spring === "chat_client" ||
|
|
450
|
+
spring === "advisor")
|
|
443
451
|
return "step";
|
|
444
452
|
return "otel.span";
|
|
445
453
|
}
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Lessons } from "../analysis/learning.ts";
|
|
1
2
|
import { type Attestation } from "../attest/index.ts";
|
|
2
3
|
import { buildPayment, type screenCounterparty } from "../finance/index.ts";
|
|
3
4
|
export interface SessionOptions {
|
|
@@ -315,6 +316,25 @@ export declare class BlackboxSession {
|
|
|
315
316
|
nearMiss(description: string): void;
|
|
316
317
|
/** spec/attestation.md: re-runs a read-only command and records whether the output matches. */
|
|
317
318
|
attest(argv: string[], claimed: string, cwd?: string, claimedExit?: number): Attestation | undefined;
|
|
319
|
+
/**
|
|
320
|
+
* The witness for a framework's shell tool (spec/attestation.md §2): parses `command` and, when
|
|
321
|
+
* it's one simple read-only command, re-runs it and records whether its output matches `claimed`.
|
|
322
|
+
* Anything else (a pipe, a write, a parse failure) records nothing. Never throws.
|
|
323
|
+
*/
|
|
324
|
+
witness(command: string, claimed: string, options?: {
|
|
325
|
+
cwd?: string | undefined;
|
|
326
|
+
exitCode?: number;
|
|
327
|
+
}): Attestation | undefined;
|
|
328
|
+
/**
|
|
329
|
+
* A workspace receipt (spec/reconcile.md §7) from any framework's tool hook: the git tree of
|
|
330
|
+
* `cwd` before (`pre`) or after (`post`) a tool call, so a change no tool claimed is
|
|
331
|
+
* UNATTRIBUTED_CHANGE. Resolves with null when posted, else the reason; never rejects.
|
|
332
|
+
*/
|
|
333
|
+
workspaceReceipt(phase: "pre" | "post", toolUseId: string, options?: {
|
|
334
|
+
tool?: string;
|
|
335
|
+
cwd?: string;
|
|
336
|
+
}): Promise<string | null>;
|
|
337
|
+
private receipts;
|
|
318
338
|
/**
|
|
319
339
|
* spec/sdk.md §1.1: a computer-use screenshot (PNG, JPEG or WebP, at most 2 MB), recorded as the
|
|
320
340
|
* body of a `screen` event, so its hash is in the chain. `action` says what the agent did or is
|
|
@@ -349,6 +369,26 @@ export declare class BlackboxSession {
|
|
|
349
369
|
/** Audit S17: the gateway's view of this session (blocked? findings?), or null if it can't be
|
|
350
370
|
* read. Lets an agent react to its own block before its next model call. Never throws. */
|
|
351
371
|
state(): Promise<SessionState | null>;
|
|
372
|
+
/**
|
|
373
|
+
* spec/fleet.md §8: what earlier runs of this agent (this session's label) hit, to read before a
|
|
374
|
+
* run: outcomes, finding codes and a `brief` to put in the agent's own prompt. Codes and counts
|
|
375
|
+
* only. The gateway records that the agent read them. `null` when it can't be asked.
|
|
376
|
+
*/
|
|
377
|
+
lessons(options?: {
|
|
378
|
+
limit?: number;
|
|
379
|
+
}): Promise<Lessons | null>;
|
|
380
|
+
/**
|
|
381
|
+
* spec/findings.md §1c: the agent changed itself (its prompt, a tool, its policy, its memory, its
|
|
382
|
+
* model). Only hashes of `before` and `after` are sent, never the content. A change that gives
|
|
383
|
+
* more power (tool, policy, model) needs an approval approved first (`requestApproval`, then
|
|
384
|
+
* pass its id), or it is SELF_CHANGE_UNAPPROVED. Never throws.
|
|
385
|
+
*/
|
|
386
|
+
selfChange(kind: string, options?: {
|
|
387
|
+
before?: string;
|
|
388
|
+
after?: string;
|
|
389
|
+
reason?: string;
|
|
390
|
+
approvalId?: string;
|
|
391
|
+
}): void;
|
|
352
392
|
/** spec/findings.md §13.1: what in an answer (the latest, or the one at `seq`) would hold it,
|
|
353
393
|
* for an app that streams the answer by itself: `{seq, risks, hold}`. `hold` says whether the
|
|
354
394
|
* server would hold it. `null` when there's no answer yet or the gateway can't be asked. */
|
package/dist/session/index.js
CHANGED
|
@@ -6,12 +6,14 @@ import { request as httpRequest } from "node:http";
|
|
|
6
6
|
import { request as httpsRequest } from "node:https";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { SELF_CHANGE_KINDS } from "../analysis/learning.js";
|
|
9
10
|
import { argsHash } from "../approvals/index.js";
|
|
10
|
-
import { attest, isReadOnly } from "../attest/index.js";
|
|
11
|
+
import { attest, isReadOnly, parseCommand } from "../attest/index.js";
|
|
11
12
|
import { buildPayment, ftaPayload, screeningPayload, } from "../finance/index.js";
|
|
12
13
|
import { decisionPayload } from "../gov/index.js";
|
|
13
14
|
import { accessPayload, checkEpisodeTag, recommendationPayload, verifyHealthSignature, } from "../health/index.js";
|
|
14
15
|
import { memoryEntry } from "../memory/index.js";
|
|
16
|
+
import { postReceipt, receiptFor, workspaceTree } from "../workspace-receipt/index.js";
|
|
15
17
|
/** spec/preflight.md §1 (L3): with an `egress` item, the SDK checks it here, on the agent's host. */
|
|
16
18
|
async function withEgress(p) {
|
|
17
19
|
if (![...(p.require ?? []), ...(p.optional ?? [])].includes("egress"))
|
|
@@ -464,6 +466,44 @@ export class BlackboxSession {
|
|
|
464
466
|
this.event("attestation", argv[0], { ...result });
|
|
465
467
|
return result;
|
|
466
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* The witness for a framework's shell tool (spec/attestation.md §2): parses `command` and, when
|
|
471
|
+
* it's one simple read-only command, re-runs it and records whether its output matches `claimed`.
|
|
472
|
+
* Anything else (a pipe, a write, a parse failure) records nothing. Never throws.
|
|
473
|
+
*/
|
|
474
|
+
witness(command, claimed, options = {}) {
|
|
475
|
+
const argv = parseCommand(command);
|
|
476
|
+
if (!argv)
|
|
477
|
+
return undefined;
|
|
478
|
+
try {
|
|
479
|
+
return this.attest(argv, claimed, options.cwd, options.exitCode);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* A workspace receipt (spec/reconcile.md §7) from any framework's tool hook: the git tree of
|
|
487
|
+
* `cwd` before (`pre`) or after (`post`) a tool call, so a change no tool claimed is
|
|
488
|
+
* UNATTRIBUTED_CHANGE. Resolves with null when posted, else the reason; never rejects.
|
|
489
|
+
*/
|
|
490
|
+
async workspaceReceipt(phase, toolUseId, options = {}) {
|
|
491
|
+
if (!this.enabled || this.closed)
|
|
492
|
+
return "the session is not recording";
|
|
493
|
+
const receipt = receiptFor({
|
|
494
|
+
hook_event_name: phase === "pre" ? "PreToolUse" : "PostToolUse",
|
|
495
|
+
tool_use_id: toolUseId.slice(0, 256),
|
|
496
|
+
...(options.tool ? { tool_name: options.tool } : {}),
|
|
497
|
+
}, workspaceTree(options.cwd ?? process.cwd()));
|
|
498
|
+
if (!receipt)
|
|
499
|
+
return "no tool use id";
|
|
500
|
+
// The tree is taken now; the posts go one after another, so a hook that doesn't wait (the
|
|
501
|
+
// OpenAI Agents runner's events) can't have a call's post land before its pre.
|
|
502
|
+
const posted = this.receipts.then(() => postReceipt(this.options.url, this.token, receipt));
|
|
503
|
+
this.receipts = posted;
|
|
504
|
+
return posted;
|
|
505
|
+
}
|
|
506
|
+
receipts = Promise.resolve();
|
|
467
507
|
/**
|
|
468
508
|
* spec/sdk.md §1.1: a computer-use screenshot (PNG, JPEG or WebP, at most 2 MB), recorded as the
|
|
469
509
|
* body of a `screen` event, so its hash is in the chain. `action` says what the agent did or is
|
|
@@ -557,6 +597,43 @@ export class BlackboxSession {
|
|
|
557
597
|
}
|
|
558
598
|
return null;
|
|
559
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* spec/fleet.md §8: what earlier runs of this agent (this session's label) hit, to read before a
|
|
602
|
+
* run: outcomes, finding codes and a `brief` to put in the agent's own prompt. Codes and counts
|
|
603
|
+
* only. The gateway records that the agent read them. `null` when it can't be asked.
|
|
604
|
+
*/
|
|
605
|
+
async lessons(options = {}) {
|
|
606
|
+
if (!this.enabled)
|
|
607
|
+
return null;
|
|
608
|
+
const q = options.limit === undefined ? "" : `?limit=${Math.trunc(options.limit)}`;
|
|
609
|
+
try {
|
|
610
|
+
const r = await send("GET", this.options.url, `/v1/sessions/${this.id}/lessons${q}`, this.token);
|
|
611
|
+
if (r.status === 200)
|
|
612
|
+
return r.json;
|
|
613
|
+
this.fail("rejected", `reading the lessons: the gateway answered ${r.status}`);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
this.fail("network", `reading the lessons: ${message(error)}`);
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* spec/findings.md §1c: the agent changed itself (its prompt, a tool, its policy, its memory, its
|
|
622
|
+
* model). Only hashes of `before` and `after` are sent, never the content. A change that gives
|
|
623
|
+
* more power (tool, policy, model) needs an approval approved first (`requestApproval`, then
|
|
624
|
+
* pass its id), or it is SELF_CHANGE_UNAPPROVED. Never throws.
|
|
625
|
+
*/
|
|
626
|
+
selfChange(kind, options = {}) {
|
|
627
|
+
const k = SELF_CHANGE_KINDS.includes(kind) ? kind : "other";
|
|
628
|
+
const sha = (text) => `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
|
629
|
+
this.event("self_change", k, {
|
|
630
|
+
kind: k,
|
|
631
|
+
...(options.before === undefined ? {} : { before_sha256: sha(options.before) }),
|
|
632
|
+
...(options.after === undefined ? {} : { after_sha256: sha(options.after) }),
|
|
633
|
+
...(options.reason ? { reason: options.reason.slice(0, 500) } : {}),
|
|
634
|
+
...(options.approvalId ? { approval_id: options.approvalId.slice(0, 128) } : {}),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
560
637
|
/** spec/findings.md §13.1: what in an answer (the latest, or the one at `seq`) would hold it,
|
|
561
638
|
* for an app that streams the answer by itself: `{seq, risks, hold}`. `hold` says whether the
|
|
562
639
|
* server would hold it. `null` when there's no answer yet or the gateway can't be asked. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.20.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The SDK version, on its own so the CLI can print it without loading the whole SDK.
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.20.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zanii/blackbox",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai-agents",
|