agent-email-guard-test 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -2
- package/benchmark/monthly.json +23 -0
- package/bin/agent-email-guard-test.js +11 -0
- package/defaults/agent-email-guard.json +12 -0
- package/examples/agent-email-guard.json +12 -0
- package/examples/ai-sdk-agent.ts +79 -0
- package/examples/naive-agent.ts +12 -0
- package/package.json +51 -7
- package/scenarios/benign-receipt.json +31 -0
- package/scenarios/hidden-html-injection.json +35 -0
- package/scenarios/hourly-budget-burst.json +33 -0
- package/scenarios/inbound-contact-list.json +36 -0
- package/scenarios/inbound-forward-injection.json +34 -0
- package/scenarios/lookalike-domain.json +34 -0
- package/scenarios/provider-retry-loop.json +33 -0
- package/scenarios/raise-own-limits.json +49 -0
- package/scenarios/receipt-loop-400.json +32 -0
- package/scenarios/recipient-sweep.json +40 -0
- package/scenarios/reply-all-storm.json +31 -0
- package/scenarios/secret-exfil-new-domain.json +34 -0
- package/schemas/report.schema.json +71 -0
- package/schemas/scenario.schema.json +98 -0
- package/src/cli.ts +132 -0
- package/src/guard-adapter.ts +253 -0
- package/src/harness.ts +204 -0
- package/src/policy.ts +83 -0
- package/src/scenarios.ts +270 -0
- package/src/schema-check.ts +94 -0
- package/src/script.ts +68 -0
- package/src/types.ts +177 -0
- package/index.js +0 -3
package/src/script.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AgentTools, EmailMessage, ScenarioInput, ScriptOp } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reference behaviour for the monthly benchmark.
|
|
5
|
+
* A naive agent executes `input.script` and does not read the task.
|
|
6
|
+
* `repeat` always runs to the end, including after a refusal.
|
|
7
|
+
* `retryOnProviderError` retries a provider timeout and stops on a refusal.
|
|
8
|
+
*/
|
|
9
|
+
export async function executeScript(input: ScenarioInput, tools: AgentTools): Promise<void> {
|
|
10
|
+
for (const step of input.script) {
|
|
11
|
+
await runOp(step, tools);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function runOp(step: ScriptOp, tools: AgentTools): Promise<void> {
|
|
16
|
+
switch (step.op) {
|
|
17
|
+
case 'send': {
|
|
18
|
+
const repeat = step.repeat ?? 1;
|
|
19
|
+
const retries = step.retryOnProviderError ?? 0;
|
|
20
|
+
for (let index = 1; index <= repeat; index += 1) {
|
|
21
|
+
const message = step.vary === 'index' ? vary(step.message, index) : step.message;
|
|
22
|
+
let result = await tools.email.send(message);
|
|
23
|
+
let used = 0;
|
|
24
|
+
while (!result.ok && !result.blocked && used < retries) {
|
|
25
|
+
used += 1;
|
|
26
|
+
result = await tools.email.send(message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
case 'emailEachContact': {
|
|
32
|
+
const contacts = await tools.contacts.list();
|
|
33
|
+
for (const to of contacts) {
|
|
34
|
+
await tools.email.send({ ...step.message, to: [to] });
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
case 'raiseLimits': {
|
|
39
|
+
await tools.limits.raise({
|
|
40
|
+
perHour: step.perHour,
|
|
41
|
+
perMinute: step.perMinute,
|
|
42
|
+
maxIdentical: step.maxIdentical,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
case 'replyAllStorm': {
|
|
47
|
+
for (let round = 0; round < step.rounds; round += 1) {
|
|
48
|
+
await tools.email.send({ to: [step.b], subject: step.subject, text: step.text });
|
|
49
|
+
await tools.email.send({ to: [step.a], subject: step.subject, text: step.text });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
default: {
|
|
54
|
+
const unknown: never = step;
|
|
55
|
+
throw new Error(`Unknown script op: ${JSON.stringify(unknown)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function vary(message: EmailMessage, index: number): EmailMessage {
|
|
61
|
+
const token = String(index);
|
|
62
|
+
return {
|
|
63
|
+
...message,
|
|
64
|
+
subject: message.subject.replaceAll('{{n}}', token),
|
|
65
|
+
text: message.text?.replaceAll('{{n}}', token),
|
|
66
|
+
html: message.html?.replaceAll('{{n}}', token),
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Language-neutral shapes. Python, PHP and Go runners should match these field names. */
|
|
2
|
+
|
|
3
|
+
export interface UntrustedInput {
|
|
4
|
+
readonly channel: 'none' | 'email' | 'web' | 'tool';
|
|
5
|
+
readonly content: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EmailMessage {
|
|
9
|
+
readonly to: readonly string[];
|
|
10
|
+
readonly cc?: readonly string[];
|
|
11
|
+
readonly subject: string;
|
|
12
|
+
readonly text?: string;
|
|
13
|
+
readonly html?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SendResult {
|
|
17
|
+
readonly ok: boolean;
|
|
18
|
+
readonly blocked: boolean;
|
|
19
|
+
/** Guard reason, or `provider_error` when the fake provider reported a timeout. */
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LimitChange {
|
|
26
|
+
readonly perHour?: number;
|
|
27
|
+
readonly perMinute?: number;
|
|
28
|
+
readonly maxIdentical?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LimitResult {
|
|
32
|
+
readonly ok: boolean;
|
|
33
|
+
readonly blocked: boolean;
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
readonly message: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentTools {
|
|
39
|
+
readonly email: {
|
|
40
|
+
send(message: EmailMessage): Promise<SendResult>;
|
|
41
|
+
};
|
|
42
|
+
readonly contacts: {
|
|
43
|
+
list(): Promise<readonly string[]>;
|
|
44
|
+
};
|
|
45
|
+
readonly limits: {
|
|
46
|
+
raise(change: LimitChange): Promise<LimitResult>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ScriptOp =
|
|
51
|
+
| {
|
|
52
|
+
readonly op: 'send';
|
|
53
|
+
readonly repeat?: number;
|
|
54
|
+
readonly retryOnProviderError?: number;
|
|
55
|
+
readonly vary?: 'index';
|
|
56
|
+
readonly message: EmailMessage;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly op: 'emailEachContact';
|
|
60
|
+
readonly message: Omit<EmailMessage, 'to'>;
|
|
61
|
+
}
|
|
62
|
+
| ({
|
|
63
|
+
readonly op: 'raiseLimits';
|
|
64
|
+
} & LimitChange)
|
|
65
|
+
| {
|
|
66
|
+
readonly op: 'replyAllStorm';
|
|
67
|
+
readonly rounds: number;
|
|
68
|
+
readonly a: string;
|
|
69
|
+
readonly b: string;
|
|
70
|
+
readonly subject: string;
|
|
71
|
+
readonly text: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface ScenarioInput {
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly task: string;
|
|
78
|
+
readonly untrusted: UntrustedInput;
|
|
79
|
+
readonly fixtures: { readonly contacts: readonly string[] };
|
|
80
|
+
readonly script: readonly ScriptOp[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RunAgent {
|
|
84
|
+
(input: ScenarioInput, tools: AgentTools): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Bounds {
|
|
88
|
+
readonly minThrough?: number;
|
|
89
|
+
readonly maxThrough?: number;
|
|
90
|
+
readonly limitsChanged?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ReferenceRun {
|
|
94
|
+
readonly attempted: number;
|
|
95
|
+
readonly through: number;
|
|
96
|
+
readonly reason: string;
|
|
97
|
+
readonly detail: string | null;
|
|
98
|
+
readonly limitsChanged?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface Scenario {
|
|
102
|
+
readonly schemaVersion: 1;
|
|
103
|
+
readonly id: string;
|
|
104
|
+
readonly name: string;
|
|
105
|
+
readonly headline: boolean;
|
|
106
|
+
readonly control: boolean;
|
|
107
|
+
readonly untrusted: UntrustedInput;
|
|
108
|
+
readonly task: string;
|
|
109
|
+
readonly safe: string;
|
|
110
|
+
readonly failure: string;
|
|
111
|
+
readonly fixtures: { readonly contacts: readonly string[] };
|
|
112
|
+
readonly transport: { readonly ambiguousError: boolean };
|
|
113
|
+
readonly script: readonly ScriptOp[];
|
|
114
|
+
readonly expect: { readonly withGuard: Bounds; readonly withoutGuard: Bounds };
|
|
115
|
+
readonly reference: { readonly withGuard: ReferenceRun; readonly withoutGuard: ReferenceRun };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface Policy {
|
|
119
|
+
readonly maxIdentical: number;
|
|
120
|
+
readonly perHour: number;
|
|
121
|
+
readonly perMinute: number;
|
|
122
|
+
readonly maxRecipientsPerMessage: number;
|
|
123
|
+
readonly maxNewRecipientsPerHour: number;
|
|
124
|
+
readonly approvalForNewDomains: boolean;
|
|
125
|
+
readonly knownDomains: readonly string[];
|
|
126
|
+
readonly knownRecipients: readonly string[];
|
|
127
|
+
readonly refuseSecretsToNewDomains: boolean;
|
|
128
|
+
readonly allowSelfLimitRaise: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface GuardDecision {
|
|
132
|
+
readonly outcome: 'allow' | 'hold_for_approval' | 'refuse';
|
|
133
|
+
readonly reason: string;
|
|
134
|
+
readonly message: string;
|
|
135
|
+
readonly detail: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface Guard {
|
|
139
|
+
inspectSend(message: EmailMessage): GuardDecision;
|
|
140
|
+
inspectLimitChange(change: LimitChange): GuardDecision;
|
|
141
|
+
/** Call only after `inspectSend` returned `allow`. */
|
|
142
|
+
recordSend(message: EmailMessage): void;
|
|
143
|
+
/** Call only after a limit change was allowed. */
|
|
144
|
+
recordLimitChange(change: LimitChange): void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ModeResult {
|
|
148
|
+
readonly attempted: number;
|
|
149
|
+
readonly through: number;
|
|
150
|
+
readonly blocked: number;
|
|
151
|
+
readonly recipientsThrough: number;
|
|
152
|
+
readonly reason: string;
|
|
153
|
+
readonly detail: string | null;
|
|
154
|
+
readonly reasons: readonly string[];
|
|
155
|
+
readonly limitsChanged: boolean;
|
|
156
|
+
readonly limitChangesAttempted: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ScenarioReport {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly name: string;
|
|
162
|
+
readonly headline: boolean;
|
|
163
|
+
readonly control: boolean;
|
|
164
|
+
readonly pass: boolean;
|
|
165
|
+
readonly withoutGuard: ModeResult;
|
|
166
|
+
readonly withGuard: ModeResult;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface SuiteReport {
|
|
170
|
+
readonly package: 'agent-email-guard-test';
|
|
171
|
+
readonly benchmarkId: string;
|
|
172
|
+
readonly benchmarkVersion: number;
|
|
173
|
+
readonly guard: 'reference' | 'node';
|
|
174
|
+
readonly policyPath: string;
|
|
175
|
+
readonly pass: boolean;
|
|
176
|
+
readonly scenarios: readonly ScenarioReport[];
|
|
177
|
+
}
|
package/index.js
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
process.stderr.write("agent-email-guard-test is not released yet. It will ship the red-team harness (agent-email-guard test).\nFollow https://github.com/agentisend/agent-email-guard for the release. The library itself: npm i agent-email-guard\n");
|
|
3
|
-
process.exit(1);
|