agenttwin 0.3.6 → 0.3.7
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 +17 -6
- package/dist/api.js +1515 -217
- package/dist/cli.js +2757 -642
- package/dist/dashboard/assets/dashboard/agent-home.css +130 -0
- package/dist/dashboard/assets/dashboard/golden-gate-portrait.png +0 -0
- package/dist/dashboard/assets/next/static/P_sIw5q7daW7e4IJEGveg/_buildManifest.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/549-f8859485ba130d9c.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/787-e53be69d23ff02f5.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/_not-found/{page-5dc6a60918e8652d.js → page-95856eb81db7799b.js} +1 -1
- package/dist/dashboard/assets/next/static/chunks/app/account/password/page-ff06be670b822cfe.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-64afdd8a8798e3b6.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-a11f5eeef34fdff9.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/demo/{page-760a47a5e4dee43f.js → page-8a71f24127eb1e63.js} +1 -1
- package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-df1468fc904db4f7.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/layout-9da6df58ef40be60.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/login/page-caa5c579f14ec911.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-e58bc195fd4bc31c.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/page-df1d4cae513d5eed.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/pricing/page-21130c3524f7e45e.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/signup/page-caa5c579f14ec911.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/layout-2936df21408f38fd.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-04e82fd8f3044022.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/layout-dc60420b4d88dd63.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-d3a27a2684498e13.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-b48a6cab88bb7f78.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/page-ca672c49c2235ce2.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-d3a27a2684498e13.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/main-app-def27ec5d496405f.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/webpack-29cc60ad58d8aefe.js +1 -0
- package/dist/dashboard/assets/next/static/css/0071f19b19d3d0c1.css +1 -0
- package/dist/dashboard/assets/next/static/css/138b575a572b2958.css +1 -0
- package/dist/dashboard/assets/next/static/css/1f0bd0855bbd1a14.css +1 -0
- package/dist/dashboard/assets/next/static/css/311920319a51fc90.css +1 -0
- package/dist/dashboard/assets/next/static/css/32d869ddee6d2dc1.css +1 -0
- package/dist/dashboard/assets/next/static/css/3f003235629d65a0.css +1 -0
- package/dist/dashboard/assets/next/static/css/4af3c8ca0da0f48b.css +1 -0
- package/dist/dashboard/assets/next/static/css/4fa51ab33fbc7e1b.css +1 -0
- package/dist/dashboard/assets/next/static/css/807e07644631d8e2.css +1 -0
- package/dist/dashboard/assets/next/static/css/97d686ac80c4018f.css +1 -0
- package/dist/dashboard/assets/next/static/css/98cc7bbbb43b84df.css +1 -0
- package/dist/dashboard/assets/next/static/css/c1117da59daa6ba9.css +1 -0
- package/dist/dashboard/assets/next/static/css/d99eecb7633d1200.css +1 -0
- package/dist/dashboard/assets/next/static/media/golden-gate-portrait.7941528f.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/gateway.js +1534 -218
- package/dist/worker.js +316 -22
- package/dist/world-host.js +1090 -0
- package/package.json +6 -1
- package/dist/dashboard/assets/next/static/HZwGKKvvx_oO6caYggjFh/_buildManifest.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-74603c2c8eaf0760.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-b79ed135f6bac5f7.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-80a0c2c82d5b4efd.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/layout-1a40c59e535513d4.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/page-de4735052adab906.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/pricing/page-7703fc31d6dff0f7.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/main-app-99295a8cc37e8a47.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/webpack-f3b6dd27998e84ce.js +0 -1
- package/dist/dashboard/assets/next/static/css/1323d7da89b3fa5e.css +0 -1
- package/dist/dashboard/assets/next/static/css/9282330a2fd1ab73.css +0 -1
- package/dist/dashboard/assets/next/static/css/96e4f356a1159071.css +0 -1
- /package/dist/dashboard/assets/next/static/{HZwGKKvvx_oO6caYggjFh → P_sIw5q7daW7e4IJEGveg}/_ssgManifest.js +0 -0
package/dist/worker.js
CHANGED
|
@@ -9,6 +9,79 @@ import { spawn } from "child_process";
|
|
|
9
9
|
// src/runtime/http-world.ts
|
|
10
10
|
import { createServer } from "http";
|
|
11
11
|
import { randomBytes } from "crypto";
|
|
12
|
+
import { isDeepStrictEqual } from "util";
|
|
13
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
14
|
+
|
|
15
|
+
// src/runtime/request-source.ts
|
|
16
|
+
import { mkdtemp, writeFile, rm, realpath } from "fs/promises";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { join, resolve } from "path";
|
|
19
|
+
function parseRequestSource(raw, spec) {
|
|
20
|
+
if (typeof raw !== "string" || raw.length > 1024) return;
|
|
21
|
+
try {
|
|
22
|
+
const source = JSON.parse(raw);
|
|
23
|
+
if (!source || typeof source.file !== "string" || !spec.files.includes(source.file) || !Number.isSafeInteger(source.line) || source.line < 1 || source.line > 1e6 || !Number.isSafeInteger(source.column) || source.column < 1 || source.column > 1e6) return;
|
|
24
|
+
return { file: source.file, line: source.line, column: source.column, provenance: "application-reported" };
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/runtime/stripe-adapter.ts
|
|
31
|
+
function stripeOperation(method, path, params, data, sequence) {
|
|
32
|
+
const error = (status, code, message, param) => ({ status, body: { error: { type: "invalid_request_error", code, message, ...param ? { param } : {} } } });
|
|
33
|
+
const match = /^\/v1\/(customers|payment_intents)(?:\/([^/]+))?(?:\/(confirm))?$/.exec(path);
|
|
34
|
+
if (!match || !["GET", "POST"].includes(method)) return void 0;
|
|
35
|
+
const [, resource, id, action] = match, collection = `/v1/${resource}`, rows = data[collection];
|
|
36
|
+
const allowed = method === "GET" ? id ? [] : ["limit", "starting_after"] : resource === "customers" && !id ? ["email", "name", "description"] : resource === "payment_intents" && !id ? ["amount", "currency", "customer", "payment_method", "confirm"] : action === "confirm" ? ["payment_method"] : void 0;
|
|
37
|
+
if (!allowed || resource === "customers" && action || resource === "payment_intents" && method === "GET" && !id) return void 0;
|
|
38
|
+
if (Object.keys(params).some((k) => !allowed.includes(k))) return void 0;
|
|
39
|
+
if (method === "GET" && !id) {
|
|
40
|
+
const limit = params.limit === void 0 ? 10 : Number(params.limit);
|
|
41
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) return { ...error(400, "parameter_invalid_integer", "limit must be 1\u2013100", "limit"), collection };
|
|
42
|
+
const ordered = [...rows].reverse();
|
|
43
|
+
const index = params.starting_after === void 0 ? -1 : ordered.findIndex((r) => r.id === params.starting_after);
|
|
44
|
+
if (params.starting_after !== void 0 && index === -1) return { ...error(400, "resource_missing", "No such starting_after customer", "starting_after"), collection };
|
|
45
|
+
const page = ordered.slice(index + 1, index + 1 + limit);
|
|
46
|
+
return { status: 200, body: { object: "list", data: page, has_more: ordered.length > index + 1 + limit, url: collection }, collection, operation: "read" };
|
|
47
|
+
}
|
|
48
|
+
const row = id ? rows.find((r) => r.id === id) : void 0;
|
|
49
|
+
if (id && !row) return { ...error(404, "resource_missing", `No such ${resource}: '${id}'`), collection };
|
|
50
|
+
if (method === "GET") return { status: 200, body: row, collection, operation: "read" };
|
|
51
|
+
if (action === "confirm") {
|
|
52
|
+
if (row.status === "succeeded") return { status: 200, body: row, collection, operation: "replay" };
|
|
53
|
+
const payment = params.payment_method ?? row.payment_method;
|
|
54
|
+
if (payment !== "pm_card_visa" && payment !== "pm_card_chargeDeclined") return { ...error(400, "parameter_invalid", "Only pm_card_visa and pm_card_chargeDeclined are supported", "payment_method"), collection };
|
|
55
|
+
row.payment_method = payment;
|
|
56
|
+
row.status = payment === "pm_card_visa" ? "succeeded" : "requires_payment_method";
|
|
57
|
+
row.last_payment_error = payment === "pm_card_chargeDeclined" ? { code: "card_declined", decline_code: "generic_decline", message: "Your card was declined." } : null;
|
|
58
|
+
return { status: payment === "pm_card_chargeDeclined" ? 402 : 200, body: payment === "pm_card_chargeDeclined" ? { error: { type: "card_error", code: "card_declined", decline_code: "generic_decline", message: "Your card was declined.", payment_intent: structuredClone(row) } } : row, collection, record: structuredClone(row), operation: "write" };
|
|
59
|
+
}
|
|
60
|
+
const prefix = resource === "customers" ? "cus" : "pi";
|
|
61
|
+
let suffix = sequence;
|
|
62
|
+
while (rows.some((r) => r.id === `${prefix}_at_${suffix}`)) suffix++;
|
|
63
|
+
const created = { id: `${prefix}_at_${suffix}`, object: resource === "customers" ? "customer" : "payment_intent", created: 17e8, livemode: false };
|
|
64
|
+
if (resource === "customers") {
|
|
65
|
+
if (params.email !== void 0 && typeof params.email !== "string") return { ...error(400, "parameter_invalid", "email must be a string", "email"), collection };
|
|
66
|
+
Object.assign(created, { email: null, name: null, description: null, ...params });
|
|
67
|
+
} else {
|
|
68
|
+
const amount = Number(params.amount);
|
|
69
|
+
if (!Number.isSafeInteger(amount) || amount < 1) return { ...error(400, "parameter_invalid_integer", "amount must be a positive integer", "amount"), collection };
|
|
70
|
+
if (typeof params.currency !== "string" || !/^[a-z]{3}$/.test(params.currency)) return { ...error(400, "parameter_invalid", "currency must be a lowercase three-letter code", "currency"), collection };
|
|
71
|
+
if (params.customer && !data["/v1/customers"].some((r) => r.id === params.customer)) return { ...error(400, "resource_missing", "No such customer", "customer"), collection };
|
|
72
|
+
if (params.payment_method !== void 0 && !["pm_card_visa", "pm_card_chargeDeclined"].includes(String(params.payment_method))) return { ...error(400, "parameter_invalid", "Unsupported test payment method", "payment_method"), collection };
|
|
73
|
+
if (params.confirm !== void 0 && !["true", "false", true, false].includes(params.confirm)) return { ...error(400, "parameter_invalid", "confirm must be boolean", "confirm"), collection };
|
|
74
|
+
const confirm = params.confirm === "true" || params.confirm === true;
|
|
75
|
+
if (confirm && !params.payment_method) return { ...error(400, "parameter_missing", "Missing payment_method", "payment_method"), collection };
|
|
76
|
+
Object.assign(created, { amount, currency: params.currency, customer: params.customer ?? null, payment_method: params.payment_method ?? null, status: confirm && params.payment_method === "pm_card_visa" ? "succeeded" : !confirm && params.payment_method ? "requires_confirmation" : "requires_payment_method", last_payment_error: confirm && params.payment_method === "pm_card_chargeDeclined" ? { code: "card_declined", message: "Your card was declined." } : null });
|
|
77
|
+
}
|
|
78
|
+
if (rows.length >= 1e4) return { ...error(429, "world_capacity", "World record capacity reached"), collection };
|
|
79
|
+
rows.push(created);
|
|
80
|
+
const declined = resource === "payment_intents" && (params.confirm === "true" || params.confirm === true) && params.payment_method === "pm_card_chargeDeclined";
|
|
81
|
+
return { status: declined ? 402 : 200, body: declined ? { error: { type: "card_error", code: "card_declined", decline_code: "generic_decline", message: "Your card was declined.", payment_intent: structuredClone(created) } } : created, collection, record: structuredClone(created), operation: "write" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/runtime/http-world.ts
|
|
12
85
|
var HttpWorld = class {
|
|
13
86
|
constructor(spec) {
|
|
14
87
|
this.spec = spec;
|
|
@@ -21,21 +94,32 @@ var HttpWorld = class {
|
|
|
21
94
|
servers = [];
|
|
22
95
|
timers = /* @__PURE__ */ new Set();
|
|
23
96
|
fired = false;
|
|
24
|
-
|
|
97
|
+
epoch = 0;
|
|
98
|
+
replayKeys = /* @__PURE__ */ new Map();
|
|
99
|
+
sourceContext = new AsyncLocalStorage();
|
|
100
|
+
record(service, operation, path, detail, record) {
|
|
25
101
|
if (this.events.length >= 5e4) return;
|
|
26
|
-
|
|
102
|
+
const source = this.sourceContext.getStore();
|
|
103
|
+
this.events.push({ sequence: this.events.length + 1, service, operation, path, detail, ...record ? { record: structuredClone(record) } : {}, ...source ? { source } : {} });
|
|
104
|
+
}
|
|
105
|
+
rejectUnsupported(service, method, path, detail) {
|
|
106
|
+
this.unsupported.push(`${service}: ${method} ${path}: ${detail}`);
|
|
107
|
+
this.record(service, "unsupported", path, detail);
|
|
27
108
|
}
|
|
28
109
|
async start() {
|
|
110
|
+
if (this.servers.length) throw new Error("World is already started");
|
|
29
111
|
for (const service of this.spec.services) {
|
|
30
112
|
const token = `at_world_${randomBytes(24).toString("hex")}`;
|
|
31
113
|
const data = /* @__PURE__ */ Object.create(null);
|
|
32
114
|
this.state[service.name] = data;
|
|
33
115
|
for (const c of service.collections) data[c.path] = structuredClone(c.seed);
|
|
34
116
|
const keys = /* @__PURE__ */ new Map();
|
|
35
|
-
|
|
117
|
+
this.replayKeys.set(service.name, keys);
|
|
118
|
+
const server = createServer((req, res) => this.sourceContext.run(parseRequestSource(req.headers["x-agenttwin-source"], this.spec), async () => {
|
|
119
|
+
const epoch = this.epoch;
|
|
36
120
|
const send = (code, body) => {
|
|
37
121
|
if (!res.destroyed) {
|
|
38
|
-
res.writeHead(code, { "content-type": "application/json" });
|
|
122
|
+
res.writeHead(code, { "content-type": "application/json", ...code === 501 ? { "X-AgentTwin-Unsupported": "true" } : {} });
|
|
39
123
|
res.end(JSON.stringify(body));
|
|
40
124
|
}
|
|
41
125
|
};
|
|
@@ -68,7 +152,7 @@ var HttpWorld = class {
|
|
|
68
152
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
69
153
|
let parsed;
|
|
70
154
|
try {
|
|
71
|
-
parsed = slack && req.headers["content-type"]?.startsWith("application/x-www-form-urlencoded") ? Object.fromEntries(new URLSearchParams(text)) : JSON.parse(text);
|
|
155
|
+
parsed = (slack || service.adapter === "stripe") && req.headers["content-type"]?.startsWith("application/x-www-form-urlencoded") ? Object.fromEntries(new URLSearchParams(text)) : JSON.parse(text);
|
|
72
156
|
} catch {
|
|
73
157
|
send(400, { error: "expected_json_object" });
|
|
74
158
|
return;
|
|
@@ -79,11 +163,50 @@ var HttpWorld = class {
|
|
|
79
163
|
}
|
|
80
164
|
payload = parsed;
|
|
81
165
|
}
|
|
166
|
+
if (epoch !== this.epoch) {
|
|
167
|
+
send(409, { error: "world_reset" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (service.adapter === "stripe") {
|
|
171
|
+
const params = payload ?? Object.fromEntries(url.searchParams);
|
|
172
|
+
const keyHeader2 = req.headers["idempotency-key"];
|
|
173
|
+
const key2 = req.method === "POST" && typeof keyHeader2 === "string" ? `${path}:${keyHeader2}` : void 0;
|
|
174
|
+
const signature2 = JSON.stringify(params);
|
|
175
|
+
const previous2 = key2 ? keys.get(key2) : void 0;
|
|
176
|
+
if (previous2) {
|
|
177
|
+
if (previous2.body !== signature2) {
|
|
178
|
+
send(400, { error: { type: "idempotency_error", message: "Keys for idempotent requests can only be used with the same parameters" } });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.record(service.name, "replay", path, "same-key response replay", previous2.record);
|
|
182
|
+
send(200, previous2.record);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const result = stripeOperation(req.method ?? "", path, params, data, this.events.length + 1);
|
|
186
|
+
if (!result) {
|
|
187
|
+
this.rejectUnsupported(service.name, req.method ?? "", path, "unsupported Stripe operation or parameters");
|
|
188
|
+
send(501, { error: { type: "invalid_request_error", code: "unsupported_operation", message: "Outside the verified AgentTwin Stripe subset" }, forwardedToProduction: false });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (result.operation) this.record(service.name, result.operation, result.collection, path, "record" in result ? result.record : result.body);
|
|
192
|
+
if (key2 && result.status === 200) keys.set(key2, { body: signature2, record: structuredClone(result.body) });
|
|
193
|
+
const fault2 = this.spec.fault;
|
|
194
|
+
if (!this.fired && result.operation === "write" && fault2?.service === service.name && fault2.path === result.collection) {
|
|
195
|
+
this.fired = true;
|
|
196
|
+
this.record(service.name, "fault", result.collection, `write committed; response delayed ${fault2.delayMs}ms`);
|
|
197
|
+
const timer = setTimeout(() => {
|
|
198
|
+
this.timers.delete(timer);
|
|
199
|
+
send(result.status, result.body);
|
|
200
|
+
}, fault2.delayMs);
|
|
201
|
+
this.timers.add(timer);
|
|
202
|
+
} else send(result.status, result.body);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
82
205
|
if (slack) {
|
|
83
206
|
const params = payload ?? Object.fromEntries(url.searchParams);
|
|
84
207
|
const allowed = slackMethod === "chat.postMessage" ? ["channel", "text"] : slackMethod === "conversations.history" ? ["channel"] : [];
|
|
85
208
|
if (Object.keys(params).some((k) => !allowed.includes(k))) {
|
|
86
|
-
this.
|
|
209
|
+
this.rejectUnsupported(service.name, req.method ?? "", url.pathname, `unsupported parameters for ${slackMethod}`);
|
|
87
210
|
send(501, { ok: false, error: "unsupported_parameters", forwardedToProduction: false });
|
|
88
211
|
return;
|
|
89
212
|
}
|
|
@@ -91,13 +214,13 @@ var HttpWorld = class {
|
|
|
91
214
|
else if (slackMethod === "conversations.history" && ["GET", "POST"].includes(req.method ?? "")) path = "/messages";
|
|
92
215
|
else if (slackMethod === "conversations.list" && ["GET", "POST"].includes(req.method ?? "")) path = "/channels";
|
|
93
216
|
else {
|
|
94
|
-
this.
|
|
95
|
-
send(501, { ok: false, error: "unsupported_operation" });
|
|
217
|
+
this.rejectUnsupported(service.name, req.method ?? "", url.pathname, "unsupported operation");
|
|
218
|
+
send(501, { ok: false, error: "unsupported_operation", forwardedToProduction: false });
|
|
96
219
|
return;
|
|
97
220
|
}
|
|
98
221
|
}
|
|
99
222
|
if (!Object.hasOwn(data, path) || !["GET", "POST"].includes(req.method ?? "")) {
|
|
100
|
-
this.
|
|
223
|
+
this.rejectUnsupported(service.name, req.method ?? "", path, "unsupported operation");
|
|
101
224
|
send(501, { error: "unsupported_operation", forwardedToProduction: false });
|
|
102
225
|
return;
|
|
103
226
|
}
|
|
@@ -136,7 +259,7 @@ var HttpWorld = class {
|
|
|
136
259
|
send(409, { error: "idempotency_conflict" });
|
|
137
260
|
return;
|
|
138
261
|
}
|
|
139
|
-
this.record(service.name, "replay", path, "same-key response replay");
|
|
262
|
+
this.record(service.name, "replay", path, "same-key response replay", previous.record);
|
|
140
263
|
send(200, previous.record);
|
|
141
264
|
return;
|
|
142
265
|
}
|
|
@@ -147,7 +270,7 @@ var HttpWorld = class {
|
|
|
147
270
|
const reply = slack ? { ok: true, channel: row.channel, ts: row.ts, message: row } : row;
|
|
148
271
|
data[path].push(row);
|
|
149
272
|
if (key) keys.set(key, { body: signature, record: row });
|
|
150
|
-
this.record(service.name, "write", path, String(row.id));
|
|
273
|
+
this.record(service.name, "write", path, String(row.id), row);
|
|
151
274
|
const fault = this.spec.fault;
|
|
152
275
|
if (!this.fired && fault?.service === service.name && fault.path === path) {
|
|
153
276
|
this.fired = true;
|
|
@@ -161,11 +284,11 @@ var HttpWorld = class {
|
|
|
161
284
|
} catch {
|
|
162
285
|
send(500, { error: "world_request_error" });
|
|
163
286
|
}
|
|
164
|
-
});
|
|
287
|
+
}));
|
|
165
288
|
server.requestTimeout = 1e4;
|
|
166
|
-
await new Promise((
|
|
289
|
+
await new Promise((resolve2, reject) => {
|
|
167
290
|
server.once("error", reject);
|
|
168
|
-
server.listen(0, "127.0.0.1",
|
|
291
|
+
server.listen(0, "127.0.0.1", resolve2);
|
|
169
292
|
});
|
|
170
293
|
this.servers.push(server);
|
|
171
294
|
this.credentials[service.name] = { endpoint: `http://127.0.0.1:${server.address().port}`, token };
|
|
@@ -174,20 +297,42 @@ var HttpWorld = class {
|
|
|
174
297
|
}
|
|
175
298
|
assertions() {
|
|
176
299
|
return this.spec.assertions.map((a) => {
|
|
177
|
-
const rows = this.state[a.service][a.path].filter((row) => !a.where || Object.entries(a.where).every(([k, v]) =>
|
|
300
|
+
const rows = this.state[a.service][a.path].filter((row) => !a.where || Object.entries(a.where).every(([k, v]) => isDeepStrictEqual(row[k], v)));
|
|
178
301
|
return { ...a, actual: rows.length, passed: rows.length === a.count };
|
|
179
302
|
});
|
|
180
303
|
}
|
|
181
304
|
get faultTriggered() {
|
|
182
305
|
return this.fired;
|
|
183
306
|
}
|
|
307
|
+
snapshot() {
|
|
308
|
+
return structuredClone(this.state);
|
|
309
|
+
}
|
|
310
|
+
quiesce() {
|
|
311
|
+
this.epoch++;
|
|
312
|
+
for (const server of this.servers) server.closeAllConnections();
|
|
313
|
+
for (const timer of this.timers) clearTimeout(timer);
|
|
314
|
+
this.timers.clear();
|
|
315
|
+
}
|
|
316
|
+
reset() {
|
|
317
|
+
this.quiesce();
|
|
318
|
+
for (const service of this.spec.services) {
|
|
319
|
+
for (const collection of service.collections) this.state[service.name][collection.path] = structuredClone(collection.seed);
|
|
320
|
+
this.replayKeys.get(service.name)?.clear();
|
|
321
|
+
}
|
|
322
|
+
this.events.length = 0;
|
|
323
|
+
this.unsupported.length = 0;
|
|
324
|
+
this.fired = false;
|
|
325
|
+
return this.snapshot();
|
|
326
|
+
}
|
|
184
327
|
async close() {
|
|
328
|
+
this.epoch++;
|
|
185
329
|
for (const timer of this.timers) clearTimeout(timer);
|
|
186
330
|
this.timers.clear();
|
|
187
|
-
await Promise.all(this.servers.map((server) => new Promise((
|
|
188
|
-
server.close(() =>
|
|
331
|
+
await Promise.all(this.servers.map((server) => new Promise((resolve2) => {
|
|
332
|
+
server.close(() => resolve2());
|
|
189
333
|
server.closeAllConnections();
|
|
190
334
|
})));
|
|
335
|
+
this.servers.length = 0;
|
|
191
336
|
}
|
|
192
337
|
};
|
|
193
338
|
|
|
@@ -212,7 +357,7 @@ function parseWorld(value) {
|
|
|
212
357
|
if (!object(service) || typeof service.name !== "string" || !namePattern.test(service.name) || services.has(service.name)) invalid("service names must be unique lowercase identifiers");
|
|
213
358
|
const item = service;
|
|
214
359
|
if (!Array.isArray(item.collections) || !item.collections.length || item.collections.length > 20) invalid("each service needs 1\u201320 collections");
|
|
215
|
-
if (item.adapter !== void 0 && !["json-collection", "slack"].includes(String(item.adapter))) invalid("unsupported service adapter");
|
|
360
|
+
if (item.adapter !== void 0 && !["json-collection", "slack", "stripe"].includes(String(item.adapter))) invalid("unsupported service adapter");
|
|
216
361
|
const paths = /* @__PURE__ */ new Set();
|
|
217
362
|
for (const entry of item.collections) {
|
|
218
363
|
if (!object(entry) || typeof entry.path !== "string" || !/^\/[a-zA-Z0-9_/-]+$/.test(entry.path) || entry.path.includes("//") || paths.has(entry.path)) invalid("collection paths must be unique absolute HTTP paths");
|
|
@@ -221,6 +366,7 @@ function parseWorld(value) {
|
|
|
221
366
|
paths.add(collection.path);
|
|
222
367
|
}
|
|
223
368
|
if (item.adapter === "slack" && (!paths.has("/messages") || !paths.has("/channels") || paths.size !== 2)) invalid("slack adapter requires /messages and /channels collections");
|
|
369
|
+
if (item.adapter === "stripe" && (!paths.has("/v1/customers") || !paths.has("/v1/payment_intents") || paths.size !== 2)) invalid("stripe adapter requires /v1/customers and /v1/payment_intents collections");
|
|
224
370
|
services.set(item.name, paths);
|
|
225
371
|
}
|
|
226
372
|
const known = (entry) => typeof entry.service === "string" && typeof entry.path === "string" && services.get(entry.service)?.has(entry.path);
|
|
@@ -228,6 +374,19 @@ function parseWorld(value) {
|
|
|
228
374
|
for (const assertion of v.assertions) {
|
|
229
375
|
if (!object(assertion) || !known(assertion) || !Number.isSafeInteger(assertion.count) || Number(assertion.count) < 0 || assertion.where !== void 0 && !object(assertion.where)) invalid("assertions need an existing service/path, nonnegative count, and optional where object");
|
|
230
376
|
}
|
|
377
|
+
const policyIds = /* @__PURE__ */ new Set();
|
|
378
|
+
for (const field of ["forbiddenEffects", "preservedState"]) {
|
|
379
|
+
if (v[field] === void 0) continue;
|
|
380
|
+
if (!Array.isArray(v[field]) || v[field].length > 100) invalid(`${field} must contain at most 100 checks`);
|
|
381
|
+
for (const entry of v[field]) {
|
|
382
|
+
if (!object(entry) || typeof entry.id !== "string" || !namePattern.test(entry.id) || policyIds.has(entry.id) || !known(entry) || entry.where !== void 0 && !object(entry.where)) invalid(`${field} requires unique ids, known collections and an optional where object`);
|
|
383
|
+
const item = entry;
|
|
384
|
+
if (field === "forbiddenEffects" && !["write", "read", "replay"].includes(String(item.operation))) invalid("forbiddenEffects operation must be write, read or replay");
|
|
385
|
+
if (Object.keys(item).some((k) => !["id", "service", "path", "where", ...field === "forbiddenEffects" ? ["operation"] : []].includes(k))) invalid(`unknown ${field} field`);
|
|
386
|
+
policyIds.add(item.id);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (v.scenario !== void 0 && (!object(v.scenario) || typeof v.scenario.id !== "string" || !namePattern.test(v.scenario.id) || !Number.isSafeInteger(v.scenario.version) || Number(v.scenario.version) < 1 || typeof v.scenario.digest !== "string" || !/^[a-f0-9]{64}$/.test(v.scenario.digest) || Object.keys(v.scenario).some((k) => !["id", "version", "digest"].includes(k)))) invalid("scenario requires id, positive version and SHA-256 digest");
|
|
231
390
|
if (v.postgres !== void 0) {
|
|
232
391
|
if (!object(v.postgres) || v.postgres.seedFile !== void 0 && !v.files.includes(String(v.postgres.seedFile)) || !Array.isArray(v.postgres.assertions) || !v.postgres.assertions.length || v.postgres.assertions.length > 100) invalid("postgres requires assertions and seedFile must be listed in files");
|
|
233
392
|
for (const a of v.postgres.assertions) {
|
|
@@ -241,15 +400,145 @@ function parseWorld(value) {
|
|
|
241
400
|
}
|
|
242
401
|
}
|
|
243
402
|
if (v.fault !== void 0 && (!object(v.fault) || !known(v.fault) || v.fault.type !== "timeout-after-write" || !Number.isInteger(v.fault.delayMs) || Number(v.fault.delayMs) < 1 || Number(v.fault.delayMs) > 1e4)) invalid("fault requires an existing service/path and delayMs 1\u201310000");
|
|
244
|
-
|
|
403
|
+
if (v.browser !== void 0) {
|
|
404
|
+
if (!object(v.browser) || typeof v.browser.baseUrl !== "string") invalid("browser requires baseUrl");
|
|
405
|
+
const browser = v.browser;
|
|
406
|
+
let u;
|
|
407
|
+
try {
|
|
408
|
+
u = new URL(String(v.browser.baseUrl));
|
|
409
|
+
} catch {
|
|
410
|
+
invalid("browser baseUrl must be a loopback HTTP URL");
|
|
411
|
+
}
|
|
412
|
+
if (u.protocol !== "http:" || u.hostname !== "127.0.0.1" || !u.port || u.username || u.password || u.pathname !== "/" || u.search || u.hash) invalid("browser baseUrl must be http://127.0.0.1:<port>");
|
|
413
|
+
if (!Array.isArray(browser.steps) || !browser.steps.length || browser.steps.length > 100 || Object.keys(browser).some((k) => !["baseUrl", "steps"].includes(k))) invalid("browser needs 1\u2013100 steps");
|
|
414
|
+
for (const raw of browser.steps) {
|
|
415
|
+
const step = raw;
|
|
416
|
+
if (!object(step) || !["goto", "click", "fill", "expectText"].includes(String(step.action))) invalid("unsupported browser action");
|
|
417
|
+
if (step.action === "goto") {
|
|
418
|
+
if (typeof step.path !== "string" || !step.path.startsWith("/") || step.path.startsWith("//") || step.path.includes("\\") || Object.keys(step).some((k) => !["action", "path"].includes(k))) invalid("goto requires a relative application path");
|
|
419
|
+
} else {
|
|
420
|
+
if (typeof step.selector !== "string" || !step.selector || step.action === "fill" && typeof step.value !== "string" || step.action === "expectText" && typeof step.text !== "string" || Object.keys(step).some((k) => !["action", "selector", ...step.action === "fill" ? ["value"] : step.action === "expectText" ? ["text"] : []].includes(k))) invalid("invalid browser selector/action parameters");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (v.background !== void 0 && (!object(v.background) || !known(v.background) || !Number.isSafeInteger(v.background.count) || Number(v.background.count) < 0 || !Number.isInteger(v.background.timeoutMs) || Number(v.background.timeoutMs) < 1 || Number(v.background.timeoutMs) > Number(v.timeoutSeconds) * 1e3 || v.background.where !== void 0 && !object(v.background.where) || Object.keys(v.background).some((k) => !["service", "path", "count", "where", "timeoutMs"].includes(k)))) invalid("background requires a known collection, expected count, optional where, and bounded timeoutMs");
|
|
425
|
+
const keys = ["schema", "name", "workflowId", "files", "command", "timeoutSeconds", "browser", "background", "services", "assertions", "forbiddenEffects", "preservedState", "scenario", "postgres", "redis", "fault"];
|
|
245
426
|
if (Object.keys(v).some((k) => !keys.includes(k))) invalid("unknown world field");
|
|
246
427
|
return v;
|
|
247
428
|
}
|
|
248
429
|
|
|
430
|
+
// src/runtime/evaluation.ts
|
|
431
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
432
|
+
var matches = (row, where) => !where || Object.entries(where).every(([k, v]) => isDeepStrictEqual2(row[k], v));
|
|
433
|
+
function evaluateState(spec, initial, final, events) {
|
|
434
|
+
const checks = spec.assertions.map((a, index) => {
|
|
435
|
+
const before = initial[a.service]?.[a.path];
|
|
436
|
+
const after = final[a.service]?.[a.path];
|
|
437
|
+
const actual = after?.filter((row) => matches(row, a.where)).length;
|
|
438
|
+
return {
|
|
439
|
+
id: `assertion_${index + 1}`,
|
|
440
|
+
kind: "required-outcome",
|
|
441
|
+
service: a.service,
|
|
442
|
+
path: a.path,
|
|
443
|
+
passed: actual === a.count,
|
|
444
|
+
expected: a.count,
|
|
445
|
+
actual: actual ?? null,
|
|
446
|
+
evidence: {
|
|
447
|
+
initial: `/initialState/${escape(a.service)}/${escape(a.path)}`,
|
|
448
|
+
final: `/finalState/${escape(a.service)}/${escape(a.path)}`,
|
|
449
|
+
beforeCount: before?.filter((row) => matches(row, a.where)).length ?? null,
|
|
450
|
+
eventSequences: events.filter((e) => e.service === a.service && e.path === a.path).map((e) => e.sequence)
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
const forbidden = (spec.forbiddenEffects ?? []).map((policy) => {
|
|
455
|
+
const hits = events.filter((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && (!policy.where || e.record !== void 0 && matches(e.record, policy.where)));
|
|
456
|
+
const unknown = events.some((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && policy.where && !e.record);
|
|
457
|
+
return {
|
|
458
|
+
id: policy.id,
|
|
459
|
+
kind: "forbidden-effect",
|
|
460
|
+
service: policy.service,
|
|
461
|
+
path: policy.path,
|
|
462
|
+
passed: hits.length === 0 && !unknown,
|
|
463
|
+
expected: 0,
|
|
464
|
+
actual: hits.length,
|
|
465
|
+
evidence: { eventSequences: hits.map((e) => e.sequence), evidenceGap: unknown }
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
const preserved = (spec.preservedState ?? []).map((policy) => {
|
|
469
|
+
const before = initial[policy.service]?.[policy.path];
|
|
470
|
+
const after = final[policy.service]?.[policy.path];
|
|
471
|
+
const passed = !!before && !!after && isDeepStrictEqual2(before.filter((row) => matches(row, policy.where)), after.filter((row) => matches(row, policy.where)));
|
|
472
|
+
return {
|
|
473
|
+
id: policy.id,
|
|
474
|
+
kind: "preserved-state",
|
|
475
|
+
service: policy.service,
|
|
476
|
+
path: policy.path,
|
|
477
|
+
passed,
|
|
478
|
+
expected: "unchanged",
|
|
479
|
+
actual: passed ? "unchanged" : "changed or missing",
|
|
480
|
+
evidence: { initial: `/initialState/${escape(policy.service)}/${escape(policy.path)}`, final: `/finalState/${escape(policy.service)}/${escape(policy.path)}` }
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
const taskPassed = checks.every((c) => c.passed);
|
|
484
|
+
const safe = [...forbidden, ...preserved].every((c) => c.passed);
|
|
485
|
+
return { schema: "agenttwin/evaluation-v1", outcome: !safe ? "unsafe" : taskPassed ? "passed" : "failed", taskPassed, safe, checks: [...checks, ...forbidden, ...preserved] };
|
|
486
|
+
}
|
|
487
|
+
var escape = (value) => value.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
488
|
+
|
|
489
|
+
// src/runtime/contracts.ts
|
|
490
|
+
var CONTRACTS = {
|
|
491
|
+
"json-collection": {
|
|
492
|
+
version: "1.0.0",
|
|
493
|
+
authentication: "Bearer run-scoped test token",
|
|
494
|
+
operations: [
|
|
495
|
+
{ method: "GET", endpoint: "<collection path>", effect: "read", response: "{data: records}", errors: ["401 invalid_test_credential", "429 world_capacity"] },
|
|
496
|
+
{ method: "POST", endpoint: "<collection path>", effect: "append", response: "201 record; 200 idempotency replay", errors: ["400 expected_json_object", "409 idempotency_conflict", "413 body_too_large", "429 world_capacity"] }
|
|
497
|
+
],
|
|
498
|
+
limitations: ["Custom API contract, not a vendor replica", "No update/delete, pagination, OAuth, webhooks or schema validation", "Idempotency compares the exact parsed JSON serialization"]
|
|
499
|
+
},
|
|
500
|
+
stripe: {
|
|
501
|
+
version: "1.0.0",
|
|
502
|
+
authentication: "Bearer run-scoped test token; official Stripe SDK configured with test endpoint",
|
|
503
|
+
operations: [
|
|
504
|
+
{ method: "POST", endpoint: "/v1/customers", effect: "append customer", parameters: ["email", "name", "description"], errors: ["400 invalid parameters"] },
|
|
505
|
+
{ method: "GET", endpoint: "/v1/customers[/<id>]", effect: "retrieve or list customers", parameters: ["limit", "starting_after"], errors: ["404 resource_missing", "400 invalid pagination"] },
|
|
506
|
+
{ method: "POST", endpoint: "/v1/payment_intents", effect: "create test payment intent", parameters: ["amount", "currency", "customer", "payment_method", "confirm"], errors: ["400 invalid parameters", "400 resource_missing", "402 card_declined"] },
|
|
507
|
+
{ method: "GET", endpoint: "/v1/payment_intents/<id>", effect: "retrieve intent", parameters: [], errors: ["404 resource_missing"] },
|
|
508
|
+
{ method: "POST", endpoint: "/v1/payment_intents/<id>/confirm", effect: "test success or decline", parameters: ["payment_method"], errors: ["404 resource_missing", "400 unsupported test method", "402 card_declined"] }
|
|
509
|
+
],
|
|
510
|
+
limitations: ["Bounded subset, not full Stripe parity; minimal object fields only; no real money", "Only pm_card_visa and pm_card_chargeDeclined test methods", "No webhooks, refunds, subscriptions, payment-method APIs, metadata, updates or deletes", "Customers list supports limit/starting_after; payment intent listing unsupported", "POST idempotency supported; reset clears records and keys; unsupported operations/parameters return 501 and make coverage incomplete"]
|
|
511
|
+
},
|
|
512
|
+
slack: {
|
|
513
|
+
version: "1.0.0",
|
|
514
|
+
authentication: "Bearer run-scoped test token; no Slack OAuth",
|
|
515
|
+
operations: [
|
|
516
|
+
{ method: "POST", endpoint: "/api/chat.postMessage", effect: "append /messages", parameters: ["channel", "text"], errors: ["channel_not_found", "no_text"] },
|
|
517
|
+
{ method: "GET|POST", endpoint: "/api/conversations.history", effect: "read /messages", parameters: ["channel"], errors: ["channel_not_found"] },
|
|
518
|
+
{ method: "GET|POST", endpoint: "/api/conversations.list", effect: "read /channels", parameters: [], errors: [] }
|
|
519
|
+
],
|
|
520
|
+
limitations: ["JSON and form bodies only", "No OAuth, scopes, pagination, threads, edits, deletes, attachments or webhooks", "Errors channel_not_found/no_text use HTTP 200 with ok:false", "Unmodeled parameters and operations return HTTP 501 and make coverage incomplete"]
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
function operationMatrix(spec) {
|
|
524
|
+
return {
|
|
525
|
+
schema: "agenttwin/contracts-v1",
|
|
526
|
+
unsupported: { status: 501, marker: "X-AgentTwin-Unsupported", coverage: "incomplete", forwardedToProduction: false },
|
|
527
|
+
adapters: CONTRACTS,
|
|
528
|
+
services: spec?.services.map((s) => ({
|
|
529
|
+
name: s.name,
|
|
530
|
+
adapter: s.adapter ?? "json-collection",
|
|
531
|
+
version: CONTRACTS[s.adapter ?? "json-collection"].version,
|
|
532
|
+
operations: s.adapter === "slack" ? CONTRACTS.slack.operations : s.adapter === "stripe" ? CONTRACTS.stripe.operations : s.collections.flatMap((c) => CONTRACTS["json-collection"].operations.map((op) => ({ ...op, endpoint: c.path })))
|
|
533
|
+
})) ?? []
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
249
537
|
// src/runtime/worker.ts
|
|
250
538
|
var secrets = [];
|
|
251
539
|
async function main() {
|
|
252
540
|
const spec = parseWorld(JSON.parse(await readFile("/input/world.json", "utf8")));
|
|
541
|
+
if (spec.browser || spec.background) throw new Error("Browser and background execution require the local runtime; this hosted worker does not provision Chromium.");
|
|
253
542
|
const world = new HttpWorld(spec);
|
|
254
543
|
const started = Date.now();
|
|
255
544
|
try {
|
|
@@ -282,9 +571,9 @@ async function main() {
|
|
|
282
571
|
timedOut = true;
|
|
283
572
|
kill();
|
|
284
573
|
}, spec.timeoutSeconds * 1e3);
|
|
285
|
-
const exitCode = await new Promise((
|
|
574
|
+
const exitCode = await new Promise((resolve2, reject) => {
|
|
286
575
|
child.once("error", reject);
|
|
287
|
-
child.once("exit",
|
|
576
|
+
child.once("exit", resolve2);
|
|
288
577
|
}).finally(() => {
|
|
289
578
|
clearTimeout(timer);
|
|
290
579
|
kill();
|
|
@@ -303,7 +592,9 @@ async function main() {
|
|
|
303
592
|
const actual = exists.trim() === "0" ? null : output.replace(/\n$/, "");
|
|
304
593
|
infrastructureAssertions.push({ service: "redis", key: assertion.key, expected: assertion.equals, actual, passed: actual === assertion.equals });
|
|
305
594
|
}
|
|
306
|
-
const
|
|
595
|
+
const initialState = Object.fromEntries(spec.services.map((s) => [s.name, Object.fromEntries(s.collections.map((c) => [c.path, c.seed]))]));
|
|
596
|
+
const evaluation = evaluateState(spec, initialState, world.state, world.events);
|
|
597
|
+
const status = world.unsupported.length || spec.fault && !world.faultTriggered ? "incomplete" : timedOut || exitCode !== 0 ? "error" : evaluation.outcome === "passed" && infrastructureAssertions.every((a) => a.passed) ? "passed" : "failed";
|
|
307
598
|
for (const c of Object.values(credentials)) {
|
|
308
599
|
stdout = stdout.split(c.token).join("[test-token]");
|
|
309
600
|
stderr = stderr.split(c.token).join("[test-token]");
|
|
@@ -320,6 +611,9 @@ async function main() {
|
|
|
320
611
|
application: { command: spec.command, exitCode, timedOut, stdout, stderr },
|
|
321
612
|
assertions,
|
|
322
613
|
infrastructureAssertions,
|
|
614
|
+
evaluation,
|
|
615
|
+
contracts: operationMatrix(spec),
|
|
616
|
+
scenario: spec.scenario,
|
|
323
617
|
fault: spec.fault ? { ...spec.fault, triggered: world.faultTriggered } : null,
|
|
324
618
|
initialState: Object.fromEntries(spec.services.map((s) => [s.name, Object.fromEntries(s.collections.map((c) => [c.path, c.seed]))])),
|
|
325
619
|
finalState: world.state,
|