fakeplane 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 +202 -0
- package/NOTICE +12 -0
- package/README.md +225 -2
- package/demo/agents/careful.mjs +89 -0
- package/demo/agents/naive.mjs +50 -0
- package/demo/scenarios/large-refund-escalation.yaml +38 -0
- package/demo/scenarios/refund-ambiguous-timeout.yaml +40 -0
- package/demo/scenarios/refund-duplicate-webhook.yaml +40 -0
- package/dist/agent.d.ts +28 -0
- package/dist/agent.js +113 -0
- package/dist/agent.js.map +1 -0
- package/dist/assertions.d.ts +21 -0
- package/dist/assertions.js +180 -0
- package/dist/assertions.js.map +1 -0
- package/dist/backends/common.d.ts +13 -0
- package/dist/backends/common.js +37 -0
- package/dist/backends/common.js.map +1 -0
- package/dist/backends/generic.d.ts +79 -0
- package/dist/backends/generic.js +227 -0
- package/dist/backends/generic.js.map +1 -0
- package/dist/backends/graphql.d.ts +22 -0
- package/dist/backends/graphql.js +248 -0
- package/dist/backends/graphql.js.map +1 -0
- package/dist/backends/index.d.ts +24 -0
- package/dist/backends/index.js +27 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/linear.d.ts +123 -0
- package/dist/backends/linear.js +451 -0
- package/dist/backends/linear.js.map +1 -0
- package/dist/backends/slack.d.ts +92 -0
- package/dist/backends/slack.js +221 -0
- package/dist/backends/slack.js.map +1 -0
- package/dist/backends/zendesk.d.ts +94 -0
- package/dist/backends/zendesk.js +455 -0
- package/dist/backends/zendesk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +340 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/matchers.d.ts +11 -0
- package/dist/matchers.js +156 -0
- package/dist/matchers.js.map +1 -0
- package/dist/mcp.d.ts +43 -0
- package/dist/mcp.js +116 -0
- package/dist/mcp.js.map +1 -0
- package/dist/pii.d.ts +17 -0
- package/dist/pii.js +79 -0
- package/dist/pii.js.map +1 -0
- package/dist/report.d.ts +13 -0
- package/dist/report.js +97 -0
- package/dist/report.js.map +1 -0
- package/dist/runner.d.ts +56 -0
- package/dist/runner.js +114 -0
- package/dist/runner.js.map +1 -0
- package/dist/scenario.d.ts +270 -0
- package/dist/scenario.js +270 -0
- package/dist/scenario.js.map +1 -0
- package/dist/server.d.ts +40 -0
- package/dist/server.js +241 -0
- package/dist/server.js.map +1 -0
- package/dist/trace.d.ts +42 -0
- package/dist/trace.js +432 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.js +89 -0
- package/dist/util.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/dist/world.d.ts +149 -0
- package/dist/world.js +187 -0
- package/dist/world.js.map +1 -0
- package/package.json +68 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# The helpdesk delivers the same webhook twice (its first delivery "timed out"),
|
|
2
|
+
# and the refund API rate-limits the first attempt.
|
|
3
|
+
name: duplicate webhook and a rate-limited refund
|
|
4
|
+
seed:
|
|
5
|
+
zendesk:
|
|
6
|
+
users:
|
|
7
|
+
- { id: 100, name: Jordan Rivera, email: jordan.rivera@example.com, phone: "+1 415 555 0134" }
|
|
8
|
+
generic:
|
|
9
|
+
collections:
|
|
10
|
+
orders:
|
|
11
|
+
- { id: ord_1001, amount: 42.5, currency: USD, charges: 2 }
|
|
12
|
+
refunds: []
|
|
13
|
+
slack:
|
|
14
|
+
channels:
|
|
15
|
+
- { id: C_ESC, name: escalations }
|
|
16
|
+
turns:
|
|
17
|
+
- ticket: 1
|
|
18
|
+
from: jordan.rivera@example.com
|
|
19
|
+
subject: Charged twice
|
|
20
|
+
text: "Hi, I was charged twice for order ord_1001. Please refund the duplicate charge."
|
|
21
|
+
faults:
|
|
22
|
+
- kind: duplicate_delivery
|
|
23
|
+
turn: 1
|
|
24
|
+
copies: 2
|
|
25
|
+
- op: generic.create
|
|
26
|
+
where: { collection: refunds }
|
|
27
|
+
kind: rate_limit
|
|
28
|
+
retryAfter: 1
|
|
29
|
+
assert:
|
|
30
|
+
- name: exactly one refund despite two webhooks
|
|
31
|
+
effects: generic.refunds.create
|
|
32
|
+
count: 1
|
|
33
|
+
- name: customer gets exactly one reply
|
|
34
|
+
effects: zendesk.ticket.reply
|
|
35
|
+
count: 1
|
|
36
|
+
- name: refund amount is one charge, not the order total twice
|
|
37
|
+
effects: generic.refunds.create
|
|
38
|
+
where: { amount: { lte: 42.5 } }
|
|
39
|
+
count: 1
|
|
40
|
+
- no_duplicate_effects: true
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AgentSpec } from "./scenario.js";
|
|
2
|
+
import type { ServerUrls } from "./server.js";
|
|
3
|
+
import type { JsonObject } from "./types.js";
|
|
4
|
+
export interface TurnPayload {
|
|
5
|
+
fakeplane: string;
|
|
6
|
+
scenario: string;
|
|
7
|
+
turn: number;
|
|
8
|
+
delivery: number;
|
|
9
|
+
/** Same for every copy of a duplicated delivery, like a webhook event id. */
|
|
10
|
+
deliveryId: string;
|
|
11
|
+
via: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
event: JsonObject;
|
|
14
|
+
history: {
|
|
15
|
+
turn: number;
|
|
16
|
+
text?: string;
|
|
17
|
+
output: string;
|
|
18
|
+
}[];
|
|
19
|
+
urls: ServerUrls;
|
|
20
|
+
}
|
|
21
|
+
export interface AgentResult {
|
|
22
|
+
output: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function agentEnv(urls: ServerUrls, scenario: string): Record<string, string>;
|
|
27
|
+
export declare function describeAgent(spec: AgentSpec): string;
|
|
28
|
+
export declare function invokeAgent(spec: AgentSpec, payload: TurnPayload, baseDir: string): Promise<AgentResult>;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { truncate } from "./util.js";
|
|
5
|
+
const MAX_OUTPUT = 256 * 1024;
|
|
6
|
+
const DEFAULT_TIMEOUT = 120_000;
|
|
7
|
+
export function agentEnv(urls, scenario) {
|
|
8
|
+
return {
|
|
9
|
+
FAKEPLANE_URL: urls.base,
|
|
10
|
+
FAKEPLANE_MCP_URL: urls.mcp,
|
|
11
|
+
FAKEPLANE_ZENDESK_URL: urls.zendesk,
|
|
12
|
+
FAKEPLANE_SLACK_URL: urls.slack,
|
|
13
|
+
FAKEPLANE_LINEAR_URL: urls.linear,
|
|
14
|
+
FAKEPLANE_GENERIC_URL: urls.generic,
|
|
15
|
+
FAKEPLANE_SCENARIO: scenario,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const moduleCache = new Map();
|
|
19
|
+
export function describeAgent(spec) {
|
|
20
|
+
return spec.command ?? spec.url ?? spec.module ?? "(none)";
|
|
21
|
+
}
|
|
22
|
+
export async function invokeAgent(spec, payload, baseDir) {
|
|
23
|
+
const started = Date.now();
|
|
24
|
+
const env = { ...agentEnv(payload.urls, payload.scenario), FAKEPLANE_TURN_INDEX: String(payload.turn), FAKEPLANE_DELIVERY_ID: payload.deliveryId, ...(spec.env ?? {}) };
|
|
25
|
+
const timeoutMs = spec.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
26
|
+
try {
|
|
27
|
+
if (spec.command) {
|
|
28
|
+
const r = await runCommand(spec.command, JSON.stringify(payload), { cwd: resolve(baseDir, spec.cwd ?? "."), env, timeoutMs });
|
|
29
|
+
return { ...r, durationMs: Date.now() - started };
|
|
30
|
+
}
|
|
31
|
+
if (spec.url) {
|
|
32
|
+
const res = await fetch(spec.url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json", "X-Fakeplane-Delivery-Id": payload.deliveryId },
|
|
35
|
+
body: JSON.stringify(payload),
|
|
36
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
37
|
+
});
|
|
38
|
+
const text = truncate(await res.text(), MAX_OUTPUT);
|
|
39
|
+
return { output: text, error: res.ok ? undefined : `agent URL returned HTTP ${res.status}`, durationMs: Date.now() - started };
|
|
40
|
+
}
|
|
41
|
+
if (spec.module) {
|
|
42
|
+
const file = resolve(baseDir, spec.module);
|
|
43
|
+
if (!moduleCache.has(file)) {
|
|
44
|
+
moduleCache.set(file, import(pathToFileURL(file).href).then((mod) => {
|
|
45
|
+
const fn = mod.default ?? mod.handleTurn;
|
|
46
|
+
if (typeof fn !== "function")
|
|
47
|
+
throw new Error(`${spec.module} must export a default function (payload, ctx) => output`);
|
|
48
|
+
return fn;
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
const fn = await moduleCache.get(file);
|
|
52
|
+
const out = await withTimeout(Promise.resolve(fn(payload, { env })), timeoutMs);
|
|
53
|
+
return { output: out === undefined || out === null ? "" : typeof out === "string" ? out : JSON.stringify(out), durationMs: Date.now() - started };
|
|
54
|
+
}
|
|
55
|
+
return { output: "", error: "no agent configured: set `agent` in the scenario or config, or pass --agent", durationMs: 0 };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { output: "", error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - started };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function withTimeout(p, ms) {
|
|
62
|
+
let timer;
|
|
63
|
+
return Promise.race([p, new Promise((_, reject) => (timer = setTimeout(() => reject(new Error(`agent timed out after ${ms}ms`)), ms)))]).finally(() => clearTimeout(timer));
|
|
64
|
+
}
|
|
65
|
+
function runCommand(command, stdin, opts) {
|
|
66
|
+
return new Promise((resolvePromise) => {
|
|
67
|
+
const child = spawn(command, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, shell: true, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32", windowsHide: true });
|
|
68
|
+
let stdout = "";
|
|
69
|
+
let stderr = "";
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
child.stdout.setEncoding("utf8").on("data", (d) => {
|
|
72
|
+
if (stdout.length < MAX_OUTPUT)
|
|
73
|
+
stdout += d;
|
|
74
|
+
});
|
|
75
|
+
child.stderr.setEncoding("utf8").on("data", (d) => {
|
|
76
|
+
if (stderr.length < MAX_OUTPUT)
|
|
77
|
+
stderr += d;
|
|
78
|
+
});
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
timedOut = true;
|
|
81
|
+
killTree(child.pid);
|
|
82
|
+
}, opts.timeoutMs);
|
|
83
|
+
child.on("error", (err) => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
resolvePromise({ output: stdout, error: `failed to start agent: ${err.message}` });
|
|
86
|
+
});
|
|
87
|
+
child.on("close", (code) => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
const tail = stderr.trim().split("\n").slice(-8).join("\n");
|
|
90
|
+
if (timedOut)
|
|
91
|
+
return resolvePromise({ output: stdout.trim(), error: `agent timed out after ${opts.timeoutMs}ms` });
|
|
92
|
+
if (code !== 0)
|
|
93
|
+
return resolvePromise({ output: stdout.trim(), error: `agent exited with code ${code}${tail ? `: ${truncate(tail, 2000)}` : ""}` });
|
|
94
|
+
resolvePromise({ output: stdout.trim() });
|
|
95
|
+
});
|
|
96
|
+
child.stdin.on("error", () => undefined);
|
|
97
|
+
child.stdin.end(stdin);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function killTree(pid) {
|
|
101
|
+
if (!pid)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
if (process.platform === "win32")
|
|
105
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
|
|
106
|
+
else
|
|
107
|
+
process.kill(-pid, "SIGKILL");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// already gone
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Assertion } from "./scenario.js";
|
|
2
|
+
import type { TurnRecord } from "./types.js";
|
|
3
|
+
import type { World } from "./world.js";
|
|
4
|
+
export interface AssertionResult {
|
|
5
|
+
name: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
message: string;
|
|
8
|
+
/** Effect or call sequence numbers that explain the result. */
|
|
9
|
+
evidence?: {
|
|
10
|
+
effects?: number[];
|
|
11
|
+
calls?: number[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface AssertionContext {
|
|
15
|
+
world: World;
|
|
16
|
+
turns: TurnRecord[];
|
|
17
|
+
/** Directory relative to which `script` paths resolve. */
|
|
18
|
+
dir: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function evaluateAssertion(a: Assertion, ctx: AssertionContext): Promise<AssertionResult>;
|
|
21
|
+
export declare function evaluateAssertions(assertions: Assertion[], ctx: AssertionContext): Promise<AssertionResult[]>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { matchValue } from "./matchers.js";
|
|
4
|
+
import { scanValue } from "./pii.js";
|
|
5
|
+
import { getPath, globMatch, stableStringify, truncate } from "./util.js";
|
|
6
|
+
const DEFAULT_DUPLICATE_IGNORE = ["comment_id", "ts", "id", "identifier", "created_at", "updated_at"];
|
|
7
|
+
function describeCount(a) {
|
|
8
|
+
if (a.count !== undefined)
|
|
9
|
+
return a.count === 0 ? "no" : `exactly ${a.count}`;
|
|
10
|
+
if (a.min !== undefined && a.max !== undefined)
|
|
11
|
+
return `${a.min}-${a.max}`;
|
|
12
|
+
if (a.max !== undefined)
|
|
13
|
+
return `at most ${a.max}`;
|
|
14
|
+
return `at least ${a.min ?? 1}`;
|
|
15
|
+
}
|
|
16
|
+
function countOk(a, n) {
|
|
17
|
+
if (a.count !== undefined)
|
|
18
|
+
return n === a.count;
|
|
19
|
+
if (a.min === undefined && a.max === undefined)
|
|
20
|
+
return n >= 1;
|
|
21
|
+
return (a.min === undefined || n >= a.min) && (a.max === undefined || n <= a.max);
|
|
22
|
+
}
|
|
23
|
+
function whereText(where) {
|
|
24
|
+
return where === undefined ? "" : ` matching ${truncate(JSON.stringify(where), 100)}`;
|
|
25
|
+
}
|
|
26
|
+
function defaultName(a) {
|
|
27
|
+
if (a.effects !== undefined)
|
|
28
|
+
return `${describeCount(a)} ${a.effects} effect(s)${whereText(a.where)}`;
|
|
29
|
+
if (a.calls !== undefined)
|
|
30
|
+
return `${describeCount(a)} ${a.calls} call(s)${whereText(a.where)}`;
|
|
31
|
+
if (a.state !== undefined)
|
|
32
|
+
return a.exists === false ? `${a.state} does not exist` : `state ${a.state}${whereText(a.match)}`;
|
|
33
|
+
if (a.no_pii !== undefined)
|
|
34
|
+
return "no PII in outbound writes";
|
|
35
|
+
if (a.no_duplicate_effects !== undefined)
|
|
36
|
+
return "no duplicate side effects";
|
|
37
|
+
if (a.agent_output !== undefined)
|
|
38
|
+
return `agent output${whereText(a.agent_output)}`;
|
|
39
|
+
return `script ${a.script}`;
|
|
40
|
+
}
|
|
41
|
+
function listEffects(effects) {
|
|
42
|
+
const shown = effects.slice(0, 6).map((e) => `effect #${e.seq} (call #${e.callSeq}${e.turn ? `, turn ${e.turn}` : ""})`);
|
|
43
|
+
return shown.join(", ") + (effects.length > 6 ? `, +${effects.length - 6} more` : "");
|
|
44
|
+
}
|
|
45
|
+
function summarizeCall(c) {
|
|
46
|
+
return `#${c.seq} ${c.tool ?? c.op} -> ${c.status}${c.fault ? ` (fault: ${c.fault})` : ""}${c.replayed ? " (idempotent replay)" : ""}`;
|
|
47
|
+
}
|
|
48
|
+
function inTurn(items, turn) {
|
|
49
|
+
return turn === undefined ? items : items.filter((i) => i.turn === turn);
|
|
50
|
+
}
|
|
51
|
+
export async function evaluateAssertion(a, ctx) {
|
|
52
|
+
const name = a.name ?? defaultName(a);
|
|
53
|
+
const { world } = ctx;
|
|
54
|
+
if (a.effects !== undefined) {
|
|
55
|
+
const pattern = a.effects;
|
|
56
|
+
const matched = inTurn(world.effects, a.turn).filter((e) => globMatch(pattern, e.kind) && (a.where === undefined || matchValue(e.data, a.where).ok));
|
|
57
|
+
const ok = countOk(a, matched.length);
|
|
58
|
+
const detail = matched.length ? `: ${listEffects(matched)}; first: ${truncate(JSON.stringify(matched[0].data), 90)}` : "";
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
ok,
|
|
62
|
+
message: ok ? `${matched.length} matching effect(s)` : `expected ${describeCount(a)} ${pattern} effect(s)${whereText(a.where)}, found ${matched.length}${detail}`,
|
|
63
|
+
evidence: { effects: matched.map((e) => e.seq) },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (a.calls !== undefined) {
|
|
67
|
+
const pattern = a.calls;
|
|
68
|
+
const matched = inTurn(world.calls, a.turn).filter((c) => (globMatch(pattern, c.op) || (c.tool !== undefined && globMatch(pattern, c.tool))) &&
|
|
69
|
+
(a.where === undefined || matchValue(c.input, a.where).ok) &&
|
|
70
|
+
(a.ok === undefined || c.ok === a.ok));
|
|
71
|
+
const ok = countOk(a, matched.length);
|
|
72
|
+
const detail = matched.length ? `: ${matched.slice(0, 5).map(summarizeCall).join("; ")}` : "";
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
ok,
|
|
76
|
+
message: ok ? `${matched.length} matching call(s)` : `expected ${describeCount(a)} ${pattern} call(s)${whereText(a.where)}, found ${matched.length}${detail}`,
|
|
77
|
+
evidence: { calls: matched.map((c) => c.seq) },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (a.state !== undefined) {
|
|
81
|
+
const value = getPath(world.snapshot(), a.state);
|
|
82
|
+
if (a.exists === false) {
|
|
83
|
+
return value === undefined ? { name, ok: true, message: "absent" } : { name, ok: false, message: `expected ${a.state} not to exist` };
|
|
84
|
+
}
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return { name, ok: false, message: `${a.state} does not exist` };
|
|
87
|
+
if (a.match === undefined)
|
|
88
|
+
return { name, ok: true, message: "exists" };
|
|
89
|
+
const r = matchValue(value, a.match);
|
|
90
|
+
return { name, ok: r.ok, message: r.ok ? "matches" : `${a.state}: ${r.reason}` };
|
|
91
|
+
}
|
|
92
|
+
if (a.no_pii !== undefined) {
|
|
93
|
+
const opts = a.no_pii === true ? {} : a.no_pii;
|
|
94
|
+
const values = [...(opts.values ?? [])];
|
|
95
|
+
if (opts.seeded !== false)
|
|
96
|
+
for (const b of world.backends.values())
|
|
97
|
+
values.push(...(b.seededSensitiveValues?.() ?? []));
|
|
98
|
+
const pattern = opts.effects ?? "*";
|
|
99
|
+
const findings = [];
|
|
100
|
+
for (const e of inTurn(world.effects, a.turn)) {
|
|
101
|
+
if (!globMatch(pattern, e.kind))
|
|
102
|
+
continue;
|
|
103
|
+
// Internal notes stay inside the helpdesk; scan them only when asked explicitly.
|
|
104
|
+
if (opts.effects === undefined && e.kind === "zendesk.ticket.note")
|
|
105
|
+
continue;
|
|
106
|
+
for (const f of scanValue(e.data, { allow: opts.allow, values, types: opts.types }))
|
|
107
|
+
findings.push({ ...f, effect: e.seq });
|
|
108
|
+
}
|
|
109
|
+
const unique = new Map();
|
|
110
|
+
for (const f of findings) {
|
|
111
|
+
const key = `${f.effect}:${/[a-z]/i.test(f.match) ? f.match.toLowerCase() : f.match.replace(/\D/g, "")}`;
|
|
112
|
+
if (!unique.has(key))
|
|
113
|
+
unique.set(key, f);
|
|
114
|
+
}
|
|
115
|
+
const list = [...unique.values()];
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
ok: list.length === 0,
|
|
119
|
+
message: list.length === 0 ? "no PII-shaped values found" : `found ${list.length} PII-shaped value(s): ${list.slice(0, 5).map((f) => `${f.type} "${mask(f.match)}" in effect #${f.effect} ${f.path}`).join("; ")}`,
|
|
120
|
+
evidence: { effects: [...new Set(list.map((f) => f.effect))] },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (a.no_duplicate_effects !== undefined) {
|
|
124
|
+
const opts = a.no_duplicate_effects === true ? {} : a.no_duplicate_effects;
|
|
125
|
+
const ignore = new Set(opts.ignore ?? DEFAULT_DUPLICATE_IGNORE);
|
|
126
|
+
const pattern = opts.effects ?? "*";
|
|
127
|
+
const groups = new Map();
|
|
128
|
+
for (const e of inTurn(world.effects, a.turn)) {
|
|
129
|
+
if (!globMatch(pattern, e.kind))
|
|
130
|
+
continue;
|
|
131
|
+
const data = Object.fromEntries(Object.entries(e.data).filter(([k]) => !ignore.has(k)));
|
|
132
|
+
const key = `${e.kind}\u0000${e.target ?? ""}\u0000${stableStringify(data)}`;
|
|
133
|
+
groups.set(key, [...(groups.get(key) ?? []), e]);
|
|
134
|
+
}
|
|
135
|
+
const dups = [...groups.values()].filter((g) => g.length > 1);
|
|
136
|
+
return {
|
|
137
|
+
name,
|
|
138
|
+
ok: dups.length === 0,
|
|
139
|
+
message: dups.length === 0 ? "every side effect is unique" : `duplicate side effects: ${dups.slice(0, 3).map((g) => `${g.length}x ${g[0].kind}${g[0].target ? ` ${g[0].target}` : ""} (${listEffects(g)})`).join("; ")}`,
|
|
140
|
+
evidence: { effects: dups.flat().map((e) => e.seq) },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (a.agent_output !== undefined) {
|
|
144
|
+
const text = ctx.turns.filter((t) => a.turn === undefined || t.turn === a.turn).map((t) => t.output).join("\n");
|
|
145
|
+
const expected = typeof a.agent_output === "string" ? { contains: a.agent_output } : a.agent_output;
|
|
146
|
+
const r = matchValue(text, expected);
|
|
147
|
+
return { name, ok: r.ok, message: r.ok ? "matches" : `agent output: ${r.reason}` };
|
|
148
|
+
}
|
|
149
|
+
const file = resolve(ctx.dir, a.script);
|
|
150
|
+
const mod = await import(pathToFileURL(file).href);
|
|
151
|
+
const fn = mod.default ?? mod.check;
|
|
152
|
+
if (typeof fn !== "function")
|
|
153
|
+
return { name, ok: false, message: `${a.script} must export a default function` };
|
|
154
|
+
const out = await fn({ calls: world.calls, effects: world.effects, state: world.snapshot(), turns: ctx.turns });
|
|
155
|
+
if (out === true || out === undefined)
|
|
156
|
+
return { name, ok: true, message: "script passed" };
|
|
157
|
+
if (out === false)
|
|
158
|
+
return { name, ok: false, message: "script returned false" };
|
|
159
|
+
if (typeof out === "string")
|
|
160
|
+
return { name, ok: false, message: out };
|
|
161
|
+
return { name, ok: Boolean(out.ok), message: String(out.message ?? (out.ok ? "script passed" : "script failed")) };
|
|
162
|
+
}
|
|
163
|
+
export async function evaluateAssertions(assertions, ctx) {
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const a of assertions) {
|
|
166
|
+
try {
|
|
167
|
+
out.push(await evaluateAssertion(a, ctx));
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
out.push({ name: a.name ?? defaultName(a), ok: false, message: `assertion error: ${err instanceof Error ? err.message : String(err)}` });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function mask(value) {
|
|
176
|
+
if (value.length <= 4)
|
|
177
|
+
return "****";
|
|
178
|
+
return value.slice(0, 2) + "*".repeat(Math.min(value.length - 4, 12)) + value.slice(-2);
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=assertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertions.js","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type JsonSchema } from "../types.js";
|
|
2
|
+
export declare function obj(properties: Record<string, JsonSchema>, required?: string[]): JsonSchema;
|
|
3
|
+
export declare const str: (description: string, extra?: JsonSchema) => JsonSchema;
|
|
4
|
+
export declare const num: (description: string, extra?: JsonSchema) => JsonSchema;
|
|
5
|
+
export declare const int: (description: string, extra?: JsonSchema) => JsonSchema;
|
|
6
|
+
export declare const bool: (description: string) => JsonSchema;
|
|
7
|
+
export declare const strList: (description: string) => JsonSchema;
|
|
8
|
+
export declare const idSchema: (description: string) => JsonSchema;
|
|
9
|
+
export declare const IDEMPOTENCY_KEY: JsonSchema;
|
|
10
|
+
/** Fail closed: a missing or empty required parameter is an error, not a silent no-op. */
|
|
11
|
+
export declare function required<T = unknown>(input: Record<string, unknown> | undefined, ...keys: string[]): T;
|
|
12
|
+
export declare function optionalString(value: unknown): string | undefined;
|
|
13
|
+
export declare function stringList(value: unknown): string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ApiError } from "../types.js";
|
|
2
|
+
export function obj(properties, required = []) {
|
|
3
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
4
|
+
}
|
|
5
|
+
export const str = (description, extra = {}) => ({ type: "string", description, ...extra });
|
|
6
|
+
export const num = (description, extra = {}) => ({ type: "number", description, ...extra });
|
|
7
|
+
export const int = (description, extra = {}) => ({ type: "integer", description, ...extra });
|
|
8
|
+
export const bool = (description) => ({ type: "boolean", description });
|
|
9
|
+
export const strList = (description) => ({ type: "array", items: { type: "string" }, description });
|
|
10
|
+
export const idSchema = (description) => ({ type: ["string", "integer"], description });
|
|
11
|
+
export const IDEMPOTENCY_KEY = {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Optional. Repeating a write with the same key returns the first result instead of writing again.",
|
|
14
|
+
};
|
|
15
|
+
/** Fail closed: a missing or empty required parameter is an error, not a silent no-op. */
|
|
16
|
+
export function required(input, ...keys) {
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = input?.[key];
|
|
19
|
+
if (value !== undefined && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0)) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new ApiError(422, "invalid_request", `Missing required parameter: ${keys[0]}`);
|
|
24
|
+
}
|
|
25
|
+
export function optionalString(value) {
|
|
26
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
27
|
+
}
|
|
28
|
+
export function stringList(value) {
|
|
29
|
+
if (value === undefined || value === null)
|
|
30
|
+
return [];
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return value.map(String).filter((s) => s !== "");
|
|
33
|
+
return String(value)
|
|
34
|
+
.split(/[\s,]+/)
|
|
35
|
+
.filter((s) => s !== "");
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/backends/common.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ApiError, type JsonObject, type JsonSchema } from "../types.js";
|
|
2
|
+
import type { Backend, HttpRoute, Operation, World } from "../world.js";
|
|
3
|
+
export interface RejectRule {
|
|
4
|
+
/** Matched against the input (for routes: `{ params, query, body }` merged with body fields). */
|
|
5
|
+
when: JsonObject;
|
|
6
|
+
status?: number;
|
|
7
|
+
code?: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CustomRoute {
|
|
11
|
+
method?: string;
|
|
12
|
+
path: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
status?: number;
|
|
15
|
+
/** Static or templated (`{{body.amount}}`, `{{params.id}}`, `{{id}}`, `{{now}}`) response body. */
|
|
16
|
+
response?: unknown;
|
|
17
|
+
/** Record a side effect of this kind (prefixed with `generic.`). Defaults to the route name for non-GET routes. */
|
|
18
|
+
effect?: string;
|
|
19
|
+
reject?: RejectRule[];
|
|
20
|
+
}
|
|
21
|
+
export interface CustomTool {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
inputSchema?: JsonSchema;
|
|
25
|
+
/** Back the tool with a collection instead of a canned response. */
|
|
26
|
+
collection?: string;
|
|
27
|
+
action?: "get" | "list" | "create" | "update" | "delete";
|
|
28
|
+
/** Input field holding the record id for get/update/delete. Default `id`. */
|
|
29
|
+
id_field?: string;
|
|
30
|
+
response?: unknown;
|
|
31
|
+
/** Responses returned in order, one per call; the last one repeats. Overrides `response`. */
|
|
32
|
+
responses?: unknown[];
|
|
33
|
+
/** Mark as a write and record a side effect of this kind (prefixed with `generic.`). */
|
|
34
|
+
effect?: string;
|
|
35
|
+
write?: boolean;
|
|
36
|
+
reject?: RejectRule[];
|
|
37
|
+
}
|
|
38
|
+
export interface GenericSeed {
|
|
39
|
+
collections?: Record<string, JsonObject[]>;
|
|
40
|
+
routes?: CustomRoute[];
|
|
41
|
+
tools?: CustomTool[];
|
|
42
|
+
/** Expose generic_list/get/create/update/delete as MCP tools. Default: true when collections are seeded. */
|
|
43
|
+
collection_tools?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare class GenericBackend implements Backend {
|
|
46
|
+
private readonly world;
|
|
47
|
+
readonly name = "generic";
|
|
48
|
+
readonly mounts: {
|
|
49
|
+
prefix: string;
|
|
50
|
+
strip: boolean;
|
|
51
|
+
}[];
|
|
52
|
+
private collections;
|
|
53
|
+
private readonly customRoutes;
|
|
54
|
+
private readonly customTools;
|
|
55
|
+
private readonly exposeCollectionTools;
|
|
56
|
+
constructor(world: World, seed?: GenericSeed);
|
|
57
|
+
private coll;
|
|
58
|
+
private record;
|
|
59
|
+
private create;
|
|
60
|
+
private update;
|
|
61
|
+
private remove;
|
|
62
|
+
operations(): Operation[];
|
|
63
|
+
/** Custom routes, which are also served at their literal path outside `/generic`. */
|
|
64
|
+
fallbackRoutes(): HttpRoute[];
|
|
65
|
+
routes(): HttpRoute[];
|
|
66
|
+
errorResponse(err: ApiError): {
|
|
67
|
+
status: number;
|
|
68
|
+
body: {
|
|
69
|
+
error: {
|
|
70
|
+
code: string;
|
|
71
|
+
message: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
headers: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
snapshot(): {
|
|
77
|
+
collections: Record<string, unknown>;
|
|
78
|
+
};
|
|
79
|
+
}
|