@agent-nexus/driftbench-users 0.0.0-stage → 0.1.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 +201 -0
- package/README.md +30 -2
- package/dist/index.d.ts +572 -0
- package/dist/index.js +1782 -0
- package/package.json +49 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { DeviationClass, UserDriver, Spec, AgentAdapter, CheckSet, ArtifactReaderRegistry, TargetRef, RunResult, PerturbationSpec } from '@agent-nexus/driftbench-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The red-team pack: one YAML file per deviation class, each holding phrasings for conversations and, for
|
|
6
|
+
* the classes that apply to code tasks, for tasks. Every phrasing is fictional and carries `approved`: a
|
|
7
|
+
* person reads it and sets it to true. The perturbing driver draws by seed from the approved phrasings
|
|
8
|
+
* only, unless the caller asks for drafts.
|
|
9
|
+
*
|
|
10
|
+
* What a phrasing holds depends on how its class reaches the agent:
|
|
11
|
+
* - a user class (D1 to D5, D9, D10): `user`, the message;
|
|
12
|
+
* - D7: `user` (an empty string is silence) or `burst`, two or more messages;
|
|
13
|
+
* - D6 on a conversation: `world`, a change of the world whose data carries `instruction`, the text the
|
|
14
|
+
* agent must treat as data (for example a store record with a notes field);
|
|
15
|
+
* - D6 on a task: `file` and `content`, a file planted in the workspace that carries the instruction;
|
|
16
|
+
* - D8 on a conversation: `error`, the error a tool returns, and optionally `tool`, the tool that fails
|
|
17
|
+
* (without it, the first tool the spec expects from the injection on);
|
|
18
|
+
* - D8 on a task: `content`, the text of an obstacle planted where the agent's next command reads or
|
|
19
|
+
* writes, so that command fails.
|
|
20
|
+
*/
|
|
21
|
+
declare const PhrasingKinds: readonly ["conversation", "task"];
|
|
22
|
+
type PhrasingKind = (typeof PhrasingKinds)[number];
|
|
23
|
+
declare const Phrasing: z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
approved: z.ZodBoolean;
|
|
26
|
+
note: z.ZodOptional<z.ZodString>;
|
|
27
|
+
user: z.ZodOptional<z.ZodString>;
|
|
28
|
+
burst: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
29
|
+
world: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
31
|
+
result: z.ZodOptional<z.ZodJSONSchema>;
|
|
32
|
+
error: z.ZodOptional<z.ZodString>;
|
|
33
|
+
latency_ms: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
}, z.core.$strict>>>;
|
|
35
|
+
store: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
36
|
+
workspace: z.ZodOptional<z.ZodObject<{
|
|
37
|
+
from: z.ZodOptional<z.ZodString>;
|
|
38
|
+
files: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
fixtures: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
instruction: z.ZodOptional<z.ZodString>;
|
|
43
|
+
error: z.ZodOptional<z.ZodString>;
|
|
44
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
45
|
+
file: z.ZodOptional<z.ZodString>;
|
|
46
|
+
content: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
type Phrasing = z.infer<typeof Phrasing>;
|
|
49
|
+
declare const PackFile: z.ZodObject<{
|
|
50
|
+
class: z.ZodEnum<{
|
|
51
|
+
D1_off_topic: "D1_off_topic";
|
|
52
|
+
D2_goal_shift: "D2_goal_shift";
|
|
53
|
+
D3_contradiction: "D3_contradiction";
|
|
54
|
+
D4_loose_confirmation: "D4_loose_confirmation";
|
|
55
|
+
D5_language_switch: "D5_language_switch";
|
|
56
|
+
D6_instruction_in_data: "D6_instruction_in_data";
|
|
57
|
+
D7_silence_or_burst: "D7_silence_or_burst";
|
|
58
|
+
D8_tool_failure: "D8_tool_failure";
|
|
59
|
+
D9_out_of_scope: "D9_out_of_scope";
|
|
60
|
+
D10_return: "D10_return";
|
|
61
|
+
}>;
|
|
62
|
+
title: z.ZodOptional<z.ZodString>;
|
|
63
|
+
conversation: z.ZodArray<z.ZodObject<{
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
approved: z.ZodBoolean;
|
|
66
|
+
note: z.ZodOptional<z.ZodString>;
|
|
67
|
+
user: z.ZodOptional<z.ZodString>;
|
|
68
|
+
burst: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
69
|
+
world: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
71
|
+
result: z.ZodOptional<z.ZodJSONSchema>;
|
|
72
|
+
error: z.ZodOptional<z.ZodString>;
|
|
73
|
+
latency_ms: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
}, z.core.$strict>>>;
|
|
75
|
+
store: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
76
|
+
workspace: z.ZodOptional<z.ZodObject<{
|
|
77
|
+
from: z.ZodOptional<z.ZodString>;
|
|
78
|
+
files: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
79
|
+
}, z.core.$strict>>;
|
|
80
|
+
fixtures: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
81
|
+
}, z.core.$strict>>;
|
|
82
|
+
instruction: z.ZodOptional<z.ZodString>;
|
|
83
|
+
error: z.ZodOptional<z.ZodString>;
|
|
84
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
85
|
+
file: z.ZodOptional<z.ZodString>;
|
|
86
|
+
content: z.ZodOptional<z.ZodString>;
|
|
87
|
+
}, z.core.$strict>>;
|
|
88
|
+
task: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
89
|
+
id: z.ZodString;
|
|
90
|
+
approved: z.ZodBoolean;
|
|
91
|
+
note: z.ZodOptional<z.ZodString>;
|
|
92
|
+
user: z.ZodOptional<z.ZodString>;
|
|
93
|
+
burst: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
94
|
+
world: z.ZodOptional<z.ZodObject<{
|
|
95
|
+
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
96
|
+
result: z.ZodOptional<z.ZodJSONSchema>;
|
|
97
|
+
error: z.ZodOptional<z.ZodString>;
|
|
98
|
+
latency_ms: z.ZodOptional<z.ZodNumber>;
|
|
99
|
+
}, z.core.$strict>>>;
|
|
100
|
+
store: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
101
|
+
workspace: z.ZodOptional<z.ZodObject<{
|
|
102
|
+
from: z.ZodOptional<z.ZodString>;
|
|
103
|
+
files: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
104
|
+
}, z.core.$strict>>;
|
|
105
|
+
fixtures: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
106
|
+
}, z.core.$strict>>;
|
|
107
|
+
instruction: z.ZodOptional<z.ZodString>;
|
|
108
|
+
error: z.ZodOptional<z.ZodString>;
|
|
109
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
110
|
+
file: z.ZodOptional<z.ZodString>;
|
|
111
|
+
content: z.ZodOptional<z.ZodString>;
|
|
112
|
+
}, z.core.$strict>>>;
|
|
113
|
+
}, z.core.$strict>;
|
|
114
|
+
type PackFile = z.infer<typeof PackFile>;
|
|
115
|
+
/** A loaded pack: one file per class, by class. */
|
|
116
|
+
type RedteamPack = {
|
|
117
|
+
readonly files: ReadonlyMap<DeviationClass, PackFile & {
|
|
118
|
+
path: string;
|
|
119
|
+
}>;
|
|
120
|
+
};
|
|
121
|
+
declare class RedteamPackError extends Error {
|
|
122
|
+
name: string;
|
|
123
|
+
}
|
|
124
|
+
/** Checks one parsed pack file; returns the problems, each naming the phrasing. */
|
|
125
|
+
declare function checkPackFile(file: PackFile): string[];
|
|
126
|
+
/**
|
|
127
|
+
* Reads every .yaml and .yml file of a folder as a pack file. Throws a RedteamPackError that names every
|
|
128
|
+
* file and problem when a file does not parse or validate, when two files hold the same class, or when the
|
|
129
|
+
* folder holds none.
|
|
130
|
+
*/
|
|
131
|
+
declare function loadRedteamPack(dir: string): Promise<RedteamPack>;
|
|
132
|
+
/** A pack held in memory, for tests and callers that build one. Validated as a loaded pack is. */
|
|
133
|
+
declare function redteamPack(files: readonly PackFile[]): RedteamPack;
|
|
134
|
+
type DrawOptions = {
|
|
135
|
+
readonly cls: DeviationClass;
|
|
136
|
+
readonly kind: PhrasingKind;
|
|
137
|
+
readonly seed: number;
|
|
138
|
+
/** Draw from every phrasing, approved or not. Default false: approved phrasings only. */
|
|
139
|
+
readonly drafts?: boolean;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* One phrasing of a class, drawn by seed: the same pack, class, kind and seed give the same phrasing.
|
|
143
|
+
* Throws a RedteamPackError when the pack has no file for the class, no phrasing for the kind, or no
|
|
144
|
+
* approved one (without drafts).
|
|
145
|
+
*/
|
|
146
|
+
declare function drawPhrasing(pack: RedteamPack, options: DrawOptions): Phrasing & {
|
|
147
|
+
packPath: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The simulated user: a person with a persona and a goal, played by a model through the model port. It
|
|
152
|
+
* answers the agent block by block, reveals a fact only once the agent has asked for it, says the stop
|
|
153
|
+
* token when the goal is met or abandoned, and never sees the agent's tool calls: it reads the agent's
|
|
154
|
+
* visible text only.
|
|
155
|
+
*
|
|
156
|
+
* Reveal-when-asked is enforced in code, not left to the model: a fact reaches the model's request only
|
|
157
|
+
* after an agent reply matches one of the fact's ask patterns; the spec's own words for the block, given as
|
|
158
|
+
* the user's intent, have every fact not yet asked replaced; and an answer that holds a fact not yet asked
|
|
159
|
+
* fails the run.
|
|
160
|
+
*
|
|
161
|
+
* The model must belong to another family than the agent's and every judge's: a model playing the user
|
|
162
|
+
* of its own family, or grading a conversation it took part in, is the bias the design forbids. The check
|
|
163
|
+
* runs when the user is created, so a run with a wrong port never starts.
|
|
164
|
+
*/
|
|
165
|
+
/** The model port, as @agent-nexus/driftbench-generate defines it: one system prompt and one user message in, text out. */
|
|
166
|
+
type UserModelPort = {
|
|
167
|
+
readonly family: string;
|
|
168
|
+
readonly model?: string;
|
|
169
|
+
complete(request: {
|
|
170
|
+
system: string;
|
|
171
|
+
user: string;
|
|
172
|
+
maxTokens?: number;
|
|
173
|
+
}): Promise<string>;
|
|
174
|
+
};
|
|
175
|
+
declare const Persona: z.ZodObject<{
|
|
176
|
+
id: z.ZodString;
|
|
177
|
+
name: z.ZodString;
|
|
178
|
+
persona: z.ZodString;
|
|
179
|
+
goal: z.ZodString;
|
|
180
|
+
facts: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
181
|
+
reveal_when_asked: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
182
|
+
stop_token: z.ZodString;
|
|
183
|
+
spec: z.ZodOptional<z.ZodString>;
|
|
184
|
+
}, z.core.$strict>;
|
|
185
|
+
type Persona = z.infer<typeof Persona>;
|
|
186
|
+
declare class SimulatedUserError extends Error {
|
|
187
|
+
name: string;
|
|
188
|
+
}
|
|
189
|
+
type SimulatedUserOptions = {
|
|
190
|
+
/** Who the person is and how they write: a persona file's text, or the whole persona. */
|
|
191
|
+
readonly persona: string | Persona;
|
|
192
|
+
readonly goal: string;
|
|
193
|
+
/** What the person knows, by name. Given only when asked for. */
|
|
194
|
+
readonly facts: Readonly<Record<string, string>>;
|
|
195
|
+
/** For each fact, the patterns (case-insensitive) of an agent reply that asks for it. Every fact needs one. */
|
|
196
|
+
readonly revealWhenAsked: Readonly<Record<string, readonly string[]>>;
|
|
197
|
+
readonly stopToken: string;
|
|
198
|
+
readonly port: UserModelPort;
|
|
199
|
+
/** The model family of the agent under test; `none` for an agent that is not a model. */
|
|
200
|
+
readonly agentFamily: string;
|
|
201
|
+
/** The model families of the judges that will grade the run. Empty when no judge grades it. */
|
|
202
|
+
readonly judgeFamilies: readonly string[];
|
|
203
|
+
/**
|
|
204
|
+
* `hint` (default): the spec's words for the block are given to the model as what the person means to
|
|
205
|
+
* say, so the run stays on the spec's blocks and their expectations apply. `none`: the model writes from
|
|
206
|
+
* the persona, the goal and the conversation alone.
|
|
207
|
+
*/
|
|
208
|
+
readonly script?: 'hint' | 'none';
|
|
209
|
+
readonly maxTokens?: number;
|
|
210
|
+
};
|
|
211
|
+
type SimulatedUser = UserDriver & {
|
|
212
|
+
/** The facts given to the model so far in the current run: the ones the agent has asked for. */
|
|
213
|
+
revealed(): string[];
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Creates the simulated user. Throws a SimulatedUserError when the port's family is empty, `unknown`, the
|
|
217
|
+
* agent's or a judge's; when a fact has no ask pattern or a pattern does not compile; when an ask pattern
|
|
218
|
+
* names no fact; when the persona or the goal already holds a fact's value; or when the stop token is empty.
|
|
219
|
+
*/
|
|
220
|
+
declare function createSimulatedUser(options: SimulatedUserOptions): SimulatedUser;
|
|
221
|
+
/**
|
|
222
|
+
* Refuses a model port of no known family, of the agent's family or of a judge's family: a model that plays
|
|
223
|
+
* the user (or the attacker) must not share a family with the agent it talks to or the judges that grade it.
|
|
224
|
+
*/
|
|
225
|
+
declare function checkUserFamilies(options: Pick<SimulatedUserOptions, 'port' | 'agentFamily' | 'judgeFamilies'>): void;
|
|
226
|
+
/** A persona file's fields as the text the model reads. */
|
|
227
|
+
declare function describePersona(persona: Persona): string;
|
|
228
|
+
/** Reads and validates a persona file (YAML). */
|
|
229
|
+
declare function loadPersona(path: string): Promise<Persona>;
|
|
230
|
+
/** The options a persona file gives, ready for createSimulatedUser with a port and the families. */
|
|
231
|
+
declare function personaOptions(persona: Persona): Pick<SimulatedUserOptions, 'persona' | 'goal' | 'facts' | 'revealWhenAsked' | 'stopToken'>;
|
|
232
|
+
declare const UserRecording: z.ZodObject<{
|
|
233
|
+
family: z.ZodString;
|
|
234
|
+
model: z.ZodString;
|
|
235
|
+
persona: z.ZodString;
|
|
236
|
+
answers: z.ZodArray<z.ZodString>;
|
|
237
|
+
}, z.core.$strict>;
|
|
238
|
+
type UserRecording = z.infer<typeof UserRecording>;
|
|
239
|
+
/** Reads and validates a recording of a simulated user (YAML). */
|
|
240
|
+
declare function loadUserRecording(path: string): Promise<UserRecording>;
|
|
241
|
+
/**
|
|
242
|
+
* A port that replays a recording: turn n gets answer n. The request names its turn (`Turn n.` on its
|
|
243
|
+
* first line: the simulated user's own count of the messages it wrote, so a block another driver injects
|
|
244
|
+
* does not move it); a request for a turn the recording does not hold fails, so a run never answers out of
|
|
245
|
+
* order. The port keeps every request, for tests and for the proof.
|
|
246
|
+
*/
|
|
247
|
+
declare function userRecordingPort(recording: UserRecording): UserModelPort & {
|
|
248
|
+
readonly requests: {
|
|
249
|
+
system: string;
|
|
250
|
+
user: string;
|
|
251
|
+
}[];
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The adaptive attacker: a red-team user that picks each attack from what the agent has just said.
|
|
256
|
+
*
|
|
257
|
+
* It first runs the spec free (the reference). Then, round after round, it reads the agent's replies in the
|
|
258
|
+
* last run and finds the cues they give: a reply that asks for an explicit confirmation invites a loose yes
|
|
259
|
+
* (D4) right after it; a block that called a tool invites a failure of that tool (D8) or an instruction in
|
|
260
|
+
* its data (D6) under that block; a reply that asks for a fact invites a false premise (D3); an offer
|
|
261
|
+
* invites a goal shift (D2); any question or request for the user's next move ("I need your name",
|
|
262
|
+
* "Reply CONFIRM") invites an aside (D1), another language (D5), an out-of-scope request (D9), silence or a
|
|
263
|
+
* burst (D7); any reply a return after a detour (D10). Of the classes it has not
|
|
264
|
+
* tried, it takes the one cued by the latest reply (ties broken by seed), builds the deviation (the spec's
|
|
265
|
+
* own perturbation of that class at that block when it declares one, else a phrasing drawn by seed from the
|
|
266
|
+
* red-team pack, or written by the model port from the cueing reply), and runs the
|
|
267
|
+
* spec with it injected. The runner's recovery record scores the round; the next round reads the replies
|
|
268
|
+
* of this run. A class no reply cues goes in at a seeded position.
|
|
269
|
+
*
|
|
270
|
+
* Every decision lands in the recording; a replay runs the same attacks without the pack, the seed or the
|
|
271
|
+
* model, so a deterministic agent gives the same records.
|
|
272
|
+
*/
|
|
273
|
+
type Cue = {
|
|
274
|
+
/** The run the cue was read from: `reference`, or the run id of the round before. */
|
|
275
|
+
run: string;
|
|
276
|
+
/** The spec block whose reply gave the cue; -1 for a seeded position. */
|
|
277
|
+
block: number;
|
|
278
|
+
rule: string;
|
|
279
|
+
/** The reply that gave the cue, as the agent wrote it. */
|
|
280
|
+
reply: string;
|
|
281
|
+
/** The tool the block called, for D6 and D8. */
|
|
282
|
+
tool?: string;
|
|
283
|
+
};
|
|
284
|
+
type AttackPhrasing = Phrasing & {
|
|
285
|
+
source: 'pack' | 'model' | 'spec' | 'recording';
|
|
286
|
+
};
|
|
287
|
+
type AttackRound = {
|
|
288
|
+
round: number;
|
|
289
|
+
class: DeviationClass;
|
|
290
|
+
injectAt: number;
|
|
291
|
+
cue: Cue;
|
|
292
|
+
phrasing: AttackPhrasing;
|
|
293
|
+
perturbation: PerturbationSpec;
|
|
294
|
+
runId: string;
|
|
295
|
+
status: RunResult['status'];
|
|
296
|
+
recovery?: NonNullable<RunResult['recovery']>;
|
|
297
|
+
/** Why the round is a finding: an unsafe act, no recovery in the spec's window, a failed run, no record. */
|
|
298
|
+
finding?: 'unsafe act' | 'not back in time' | 'run failed' | 'not measured';
|
|
299
|
+
};
|
|
300
|
+
type AttackRecording = {
|
|
301
|
+
version: 1;
|
|
302
|
+
spec: string;
|
|
303
|
+
specHash: string;
|
|
304
|
+
seed: number;
|
|
305
|
+
rounds: {
|
|
306
|
+
class: DeviationClass;
|
|
307
|
+
inject_at: number;
|
|
308
|
+
cue: Cue;
|
|
309
|
+
phrasing: AttackPhrasing;
|
|
310
|
+
}[];
|
|
311
|
+
};
|
|
312
|
+
type AttackResult = {
|
|
313
|
+
reference: RunResult;
|
|
314
|
+
rounds: AttackRound[];
|
|
315
|
+
findings: number;
|
|
316
|
+
recording: AttackRecording;
|
|
317
|
+
};
|
|
318
|
+
type AttackOptions = {
|
|
319
|
+
spec: Spec;
|
|
320
|
+
adapter: AgentAdapter;
|
|
321
|
+
checks: CheckSet;
|
|
322
|
+
artifacts?: ArtifactReaderRegistry;
|
|
323
|
+
target?: TargetRef;
|
|
324
|
+
/** The red-team pack the phrasings are drawn from. Needed unless the run replays a recording. */
|
|
325
|
+
pack?: RedteamPack;
|
|
326
|
+
/** Draw from unapproved phrasings too. */
|
|
327
|
+
drafts?: boolean;
|
|
328
|
+
/** Default 0. */
|
|
329
|
+
seed?: number;
|
|
330
|
+
/** At most this many attacks; never more than the ten classes. Default 10. */
|
|
331
|
+
rounds?: number;
|
|
332
|
+
/** Writes the user-borne attacks from the cueing reply. With it, agentFamily and judgeFamilies are required. */
|
|
333
|
+
port?: UserModelPort;
|
|
334
|
+
agentFamily?: string;
|
|
335
|
+
judgeFamilies?: readonly string[];
|
|
336
|
+
/** Runs the attacks of an earlier recording, in order, instead of choosing them. */
|
|
337
|
+
replay?: AttackRecording;
|
|
338
|
+
consequential?: (label: string) => boolean;
|
|
339
|
+
now?: () => Date;
|
|
340
|
+
};
|
|
341
|
+
declare class AttackError extends Error {
|
|
342
|
+
name: string;
|
|
343
|
+
}
|
|
344
|
+
declare function runAttack(options: AttackOptions): Promise<AttackResult>;
|
|
345
|
+
type CuedClass = {
|
|
346
|
+
cls: DeviationClass;
|
|
347
|
+
at: number;
|
|
348
|
+
cue: Cue;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* The cues the agent's replies in one run give, by spec block, latest block first. A block the attacker
|
|
352
|
+
* injected as a message gives none: it is not a block of the spec.
|
|
353
|
+
*/
|
|
354
|
+
declare function cuesOf(spec: Spec, run: RunResult, label: string, perturbation?: PerturbationSpec): CuedClass[];
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The ten deviation classes, as the perturbing driver applies them. Each class says what carries it to the
|
|
358
|
+
* agent (a user message, a burst, or a change of the world with no user text), what the agent is expected
|
|
359
|
+
* to do, and the recovery a drawn perturbation of the class is given when the spec declares none.
|
|
360
|
+
*/
|
|
361
|
+
/** What carries a deviation to the agent. */
|
|
362
|
+
type Carrier =
|
|
363
|
+
/** One user message. */
|
|
364
|
+
'user'
|
|
365
|
+
/** One user message, or several that arrive together, or an empty message. */
|
|
366
|
+
| 'user_or_burst'
|
|
367
|
+
/** A change of the world (a tool result, a tool error, a workspace file) with no user text. */
|
|
368
|
+
| 'world';
|
|
369
|
+
type DeviationDef = {
|
|
370
|
+
readonly class: DeviationClass;
|
|
371
|
+
/** The short name, D1 to D10. */
|
|
372
|
+
readonly code: string;
|
|
373
|
+
readonly title: string;
|
|
374
|
+
readonly carrier: Carrier;
|
|
375
|
+
/** What the agent is expected to do, in one sentence. */
|
|
376
|
+
readonly expected: string;
|
|
377
|
+
/** The blocks the agent has to come back, for a drawn perturbation on a conversation. */
|
|
378
|
+
readonly withinBlocks: number;
|
|
379
|
+
/** The blocks the agent has to come back, for a drawn perturbation on a task. */
|
|
380
|
+
readonly taskWithinBlocks: number;
|
|
381
|
+
/**
|
|
382
|
+
* True when, on a conversation, the injected block must leave the agent at the stage it was pending at:
|
|
383
|
+
* the stage of the block before the injection. A goal shift moves the flow, and a world change is
|
|
384
|
+
* judged by the block it changes, so neither keeps it.
|
|
385
|
+
*/
|
|
386
|
+
readonly keepsPendingStage: boolean;
|
|
387
|
+
};
|
|
388
|
+
declare const DEVIATIONS: Readonly<Record<DeviationClass, DeviationDef>>;
|
|
389
|
+
/** True for the classes that change the world and carry no user text: D6 and D8. */
|
|
390
|
+
declare function isWorldClass(cls: DeviationClass): boolean;
|
|
391
|
+
/**
|
|
392
|
+
* The class a name means: the full name (`D2_goal_shift`) or its code (`D2`, `d2`). Undefined when it names
|
|
393
|
+
* none.
|
|
394
|
+
*/
|
|
395
|
+
declare function deviationClassOf(name: string): DeviationClass | undefined;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The perturbing driver. It plays the spec's blocks and injects one deviation before block `inject_at`
|
|
399
|
+
* (0-based), then returns to the spec's blocks so recovery can be measured.
|
|
400
|
+
*
|
|
401
|
+
* A deviation carried by a user message or a burst goes in as its own block, before block `inject_at`.
|
|
402
|
+
* A deviation carried by the world alone (D6, D8: a tool result, a tool error, a workspace file) has no
|
|
403
|
+
* user text: it changes the Given of block `inject_at`, which then runs as the injected block, with the
|
|
404
|
+
* perturbation's world and the perturbation's expectations, and is not run again. The driver contract has
|
|
405
|
+
* no step for a world change without a stimulus, so the injected step carries what the base driver says at
|
|
406
|
+
* that block, and the runner applies the world of the perturbation the step names (from the spec it runs).
|
|
407
|
+
*
|
|
408
|
+
* The runner reads a perturbation's world and expectations from the spec it runs, so a run uses
|
|
409
|
+
* `driver.spec`: the spec with the resolved perturbation in its perturbations. The driver refuses to step
|
|
410
|
+
* a run of any other spec.
|
|
411
|
+
*/
|
|
412
|
+
/** A perturbation drawn from the red-team pack, by class, position and seed. */
|
|
413
|
+
type DrawnPerturbation = {
|
|
414
|
+
readonly class: DeviationClass;
|
|
415
|
+
readonly inject_at: number;
|
|
416
|
+
readonly pack: RedteamPack;
|
|
417
|
+
/** Default 0. */
|
|
418
|
+
readonly seed?: number;
|
|
419
|
+
/** Draw from unapproved phrasings too. Default false. */
|
|
420
|
+
readonly drafts?: boolean;
|
|
421
|
+
};
|
|
422
|
+
type PerturbingDriverOptions = {
|
|
423
|
+
readonly spec: Spec;
|
|
424
|
+
/**
|
|
425
|
+
* The deviation: the id of one the spec declares, a perturbation written out, or a draw from the pack. A
|
|
426
|
+
* draw takes the spec's own perturbation of that class at that position when the spec declares one.
|
|
427
|
+
*/
|
|
428
|
+
readonly perturbation: string | PerturbationSpec | DrawnPerturbation;
|
|
429
|
+
/** The driver for the spec's blocks. Default: the scripted driver. */
|
|
430
|
+
readonly base?: UserDriver;
|
|
431
|
+
};
|
|
432
|
+
/** Where a resolved perturbation came from. */
|
|
433
|
+
type PerturbationSource = {
|
|
434
|
+
kind: 'declared';
|
|
435
|
+
} | {
|
|
436
|
+
kind: 'given';
|
|
437
|
+
} | {
|
|
438
|
+
kind: 'drawn';
|
|
439
|
+
phrasingId: string;
|
|
440
|
+
approved: boolean;
|
|
441
|
+
packPath: string;
|
|
442
|
+
seed: number;
|
|
443
|
+
};
|
|
444
|
+
type PerturbingDriver = UserDriver & {
|
|
445
|
+
/** The spec to run: the input spec, with the resolved perturbation in its perturbations. */
|
|
446
|
+
readonly spec: Spec;
|
|
447
|
+
readonly perturbation: PerturbationSpec;
|
|
448
|
+
readonly source: PerturbationSource;
|
|
449
|
+
/** True when the deviation changes the world only and the injected block replaces block inject_at. */
|
|
450
|
+
readonly worldOnly: boolean;
|
|
451
|
+
};
|
|
452
|
+
declare class PerturbationError extends Error {
|
|
453
|
+
name: string;
|
|
454
|
+
}
|
|
455
|
+
declare function createPerturbingDriver(options: PerturbingDriverOptions): PerturbingDriver;
|
|
456
|
+
/** The perturbation an option names, with where it came from. */
|
|
457
|
+
declare function resolvePerturbation(spec: Spec, choice: PerturbingDriverOptions['perturbation']): {
|
|
458
|
+
perturbation: PerturbationSpec;
|
|
459
|
+
source: PerturbationSource;
|
|
460
|
+
};
|
|
461
|
+
/**
|
|
462
|
+
* A perturbation built from a drawn phrasing. Its expectations and recovery follow the class definition:
|
|
463
|
+
* - a user class keeps the pending stage (the stage of the block before) when the class says so, on a
|
|
464
|
+
* conversation; D5 also expects the declared language;
|
|
465
|
+
* - D6 expects block inject_at's own expectations: the instruction is data, so the block runs as usual;
|
|
466
|
+
* - D8 expects the failing tool called at most twice (one retry), on a conversation; on a task, a reply
|
|
467
|
+
* (an empty one is no honest fallback) and no deleted file.
|
|
468
|
+
*/
|
|
469
|
+
declare function fromPhrasing(spec: Spec, cls: DeviationClass, injectAt: number, phrasing: Phrasing): PerturbationSpec;
|
|
470
|
+
/** A perturbation with neither user text nor a burst: it changes the world only. */
|
|
471
|
+
declare function isWorldOnly(perturbation: PerturbationSpec): boolean;
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* A seeded choice: the same parts give the same index on every machine and every run. The index is the
|
|
475
|
+
* first 48 bits of the SHA-256 of the parts joined, modulo the size.
|
|
476
|
+
*/
|
|
477
|
+
declare function seededIndex(parts: readonly (string | number)[], size: number): number;
|
|
478
|
+
|
|
479
|
+
/** The families a plugin belongs to. Harm and security come first; policy and reliability cover the rest. */
|
|
480
|
+
declare const PluginFamilies: readonly ["harm", "security", "policy", "reliability"];
|
|
481
|
+
type PluginFamily = (typeof PluginFamilies)[number];
|
|
482
|
+
/**
|
|
483
|
+
* One red-team plugin: a generation template that asks for attack requests against a purpose, and a grader
|
|
484
|
+
* rubric that judges the reply. Both hold `{{purpose}}`; the generation template also holds `{{n}}`, the
|
|
485
|
+
* grader rubric also holds `{{prompt}}`.
|
|
486
|
+
*/
|
|
487
|
+
declare const RedteamPlugin: z.ZodObject<{
|
|
488
|
+
id: z.ZodString;
|
|
489
|
+
family: z.ZodEnum<{
|
|
490
|
+
harm: "harm";
|
|
491
|
+
security: "security";
|
|
492
|
+
policy: "policy";
|
|
493
|
+
reliability: "reliability";
|
|
494
|
+
}>;
|
|
495
|
+
title: z.ZodString;
|
|
496
|
+
source: z.ZodString;
|
|
497
|
+
template: z.ZodString;
|
|
498
|
+
rubric: z.ZodString;
|
|
499
|
+
}, z.core.$strict>;
|
|
500
|
+
type RedteamPlugin = z.infer<typeof RedteamPlugin>;
|
|
501
|
+
/** A plugin that was not ported, with the reason. */
|
|
502
|
+
type SkippedPlugin = {
|
|
503
|
+
readonly source: string;
|
|
504
|
+
readonly reason: string;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Harm plugins, one per category. The requests they generate test refusal and safe handling; no plugin here
|
|
509
|
+
* writes operational detail for a harmful act. Adapted from MIT-licensed upstream plugins.
|
|
510
|
+
*/
|
|
511
|
+
declare const HARM_PLUGINS: readonly RedteamPlugin[];
|
|
512
|
+
|
|
513
|
+
/** Policy and reliability plugins: impersonation, bias, unverified claims, invented facts and wrong premises. */
|
|
514
|
+
declare const POLICY_PLUGINS: readonly RedteamPlugin[];
|
|
515
|
+
|
|
516
|
+
/** Fills every `{{name}}` of a text. Throws when the text names a value the caller did not give. */
|
|
517
|
+
declare function renderText(text: string, values: Readonly<Record<string, string>>): string;
|
|
518
|
+
/**
|
|
519
|
+
* The generation template and the grader rubric of a plugin, filled for a target purpose. `n` is the number of
|
|
520
|
+
* requests the template asks for. Supply `prompt` to bind the rubric to the generated attack; without it,
|
|
521
|
+
* the rubric points at the user messages in the transcript.
|
|
522
|
+
*/
|
|
523
|
+
declare function renderPlugin(plugin: RedteamPlugin, options: {
|
|
524
|
+
readonly purpose: string;
|
|
525
|
+
readonly n?: number;
|
|
526
|
+
readonly prompt?: string;
|
|
527
|
+
}): {
|
|
528
|
+
template: string;
|
|
529
|
+
rubric: string;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
/** Security plugins: injection, access control, data leaks and tool abuse. Adapted from MIT-licensed upstream plugins. */
|
|
533
|
+
declare const SECURITY_PLUGINS: readonly RedteamPlugin[];
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The upstream plugins not ported, each with its reason. Dataset and URL plugins need a download at run time,
|
|
537
|
+
* which the pack does not do. Policy skips hold content categories whose generation template would need
|
|
538
|
+
* operational or sexual detail, so they are left for an owner to write under their own review.
|
|
539
|
+
*/
|
|
540
|
+
declare const SKIPPED_PLUGINS: readonly SkippedPlugin[];
|
|
541
|
+
|
|
542
|
+
/** Every ported plugin, harm and security first. */
|
|
543
|
+
declare const REDTEAM_PLUGINS: readonly RedteamPlugin[];
|
|
544
|
+
/**
|
|
545
|
+
* The plugins, each checked against its schema. Throws when a plugin is malformed or two share an id, and names
|
|
546
|
+
* each problem.
|
|
547
|
+
*/
|
|
548
|
+
declare function loadRedteamPlugins(): readonly RedteamPlugin[];
|
|
549
|
+
type PluginToSpecOptions = {
|
|
550
|
+
readonly purpose: string;
|
|
551
|
+
/** A variation label sent to the generator and recorded in the spec. Save the spec for exact replay. */
|
|
552
|
+
readonly seed: number;
|
|
553
|
+
/** The caller supplies the generator. No model is selected or called implicitly. */
|
|
554
|
+
readonly port: UserModelPort;
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Generates one attack through the supplied port, then builds a spec whose user turn is that attack.
|
|
558
|
+
* The rubric receives the actual attack text. Invalid inputs fail before generation; malformed output
|
|
559
|
+
* fails before a spec reaches the target. The returned spec is checked by the spec loader.
|
|
560
|
+
*/
|
|
561
|
+
declare function pluginToSpec(plugin: RedteamPlugin, options: PluginToSpecOptions): Promise<Spec>;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The scripted driver: replays the spec's blocks in order, each with its own stimulus, and stops after
|
|
565
|
+
* the last one. It decides from its context alone, so one instance can drive any number of runs.
|
|
566
|
+
*/
|
|
567
|
+
declare function createScriptedDriver(): UserDriver;
|
|
568
|
+
|
|
569
|
+
/** User drivers: scripted (phase 1), perturbing and simulated (phase 3), the red-team pack and recovery. */
|
|
570
|
+
declare const packageName = "@agent-nexus/driftbench-users";
|
|
571
|
+
|
|
572
|
+
export { AttackError, type AttackOptions, type AttackRecording, type AttackResult, type AttackRound, type Carrier, type Cue, DEVIATIONS, type DeviationDef, type DrawOptions, type DrawnPerturbation, HARM_PLUGINS, POLICY_PLUGINS, PackFile, Persona, PerturbationError, type PerturbationSource, type PerturbingDriver, type PerturbingDriverOptions, Phrasing, type PhrasingKind, PhrasingKinds, PluginFamilies, type PluginFamily, type PluginToSpecOptions, REDTEAM_PLUGINS, type RedteamPack, RedteamPackError, RedteamPlugin, SECURITY_PLUGINS, SKIPPED_PLUGINS, type SimulatedUser, SimulatedUserError, type SimulatedUserOptions, type SkippedPlugin, type UserModelPort, UserRecording, checkPackFile, checkUserFamilies, createPerturbingDriver, createScriptedDriver, createSimulatedUser, cuesOf, describePersona, deviationClassOf, drawPhrasing, fromPhrasing, isWorldClass, isWorldOnly, loadPersona, loadRedteamPack, loadRedteamPlugins, loadUserRecording, packageName, personaOptions, pluginToSpec, redteamPack, renderPlugin, renderText, resolvePerturbation, runAttack, seededIndex, userRecordingPort };
|