agenttwin 0.3.5 → 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.
Files changed (65) hide show
  1. package/README.md +25 -6
  2. package/dist/api.js +1552 -214
  3. package/dist/cli.js +2857 -712
  4. package/dist/dashboard/assets/dashboard/agent-home.css +156 -1
  5. package/dist/dashboard/assets/dashboard/bay-morning.png +0 -0
  6. package/dist/dashboard/assets/dashboard/golden-gate-afternoon.png +0 -0
  7. package/dist/dashboard/assets/dashboard/golden-gate-portrait.png +0 -0
  8. package/dist/dashboard/assets/next/static/P_sIw5q7daW7e4IJEGveg/_buildManifest.js +1 -0
  9. package/dist/dashboard/assets/next/static/chunks/549-f8859485ba130d9c.js +1 -0
  10. package/dist/dashboard/assets/next/static/chunks/787-e53be69d23ff02f5.js +1 -0
  11. package/dist/dashboard/assets/next/static/chunks/app/_not-found/{page-5dc6a60918e8652d.js → page-95856eb81db7799b.js} +1 -1
  12. package/dist/dashboard/assets/next/static/chunks/app/account/password/page-ff06be670b822cfe.js +1 -0
  13. package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-64afdd8a8798e3b6.js +1 -0
  14. package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-a11f5eeef34fdff9.js +1 -0
  15. package/dist/dashboard/assets/next/static/chunks/app/demo/{page-760a47a5e4dee43f.js → page-8a71f24127eb1e63.js} +1 -1
  16. package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-df1468fc904db4f7.js +1 -0
  17. package/dist/dashboard/assets/next/static/chunks/app/layout-9da6df58ef40be60.js +1 -0
  18. package/dist/dashboard/assets/next/static/chunks/app/login/page-caa5c579f14ec911.js +1 -0
  19. package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-e58bc195fd4bc31c.js +1 -0
  20. package/dist/dashboard/assets/next/static/chunks/app/page-df1d4cae513d5eed.js +1 -0
  21. package/dist/dashboard/assets/next/static/chunks/app/pricing/page-21130c3524f7e45e.js +1 -0
  22. package/dist/dashboard/assets/next/static/chunks/app/signup/page-caa5c579f14ec911.js +1 -0
  23. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/layout-2936df21408f38fd.js +1 -0
  24. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-04e82fd8f3044022.js +1 -0
  25. package/dist/dashboard/assets/next/static/chunks/app/v2/layout-dc60420b4d88dd63.js +1 -0
  26. package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-d3a27a2684498e13.js +1 -0
  27. package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-b48a6cab88bb7f78.js +1 -0
  28. package/dist/dashboard/assets/next/static/chunks/app/v2/page-ca672c49c2235ce2.js +1 -0
  29. package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-d3a27a2684498e13.js +1 -0
  30. package/dist/dashboard/assets/next/static/chunks/main-app-def27ec5d496405f.js +1 -0
  31. package/dist/dashboard/assets/next/static/chunks/webpack-29cc60ad58d8aefe.js +1 -0
  32. package/dist/dashboard/assets/next/static/css/0071f19b19d3d0c1.css +1 -0
  33. package/dist/dashboard/assets/next/static/css/138b575a572b2958.css +1 -0
  34. package/dist/dashboard/assets/next/static/css/1f0bd0855bbd1a14.css +1 -0
  35. package/dist/dashboard/assets/next/static/css/311920319a51fc90.css +1 -0
  36. package/dist/dashboard/assets/next/static/css/32d869ddee6d2dc1.css +1 -0
  37. package/dist/dashboard/assets/next/static/css/3f003235629d65a0.css +1 -0
  38. package/dist/dashboard/assets/next/static/css/4af3c8ca0da0f48b.css +1 -0
  39. package/dist/dashboard/assets/next/static/css/4fa51ab33fbc7e1b.css +1 -0
  40. package/dist/dashboard/assets/next/static/css/807e07644631d8e2.css +1 -0
  41. package/dist/dashboard/assets/next/static/css/97d686ac80c4018f.css +1 -0
  42. package/dist/dashboard/assets/next/static/css/98cc7bbbb43b84df.css +1 -0
  43. package/dist/dashboard/assets/next/static/css/c1117da59daa6ba9.css +1 -0
  44. package/dist/dashboard/assets/next/static/css/d99eecb7633d1200.css +1 -0
  45. package/dist/dashboard/assets/next/static/media/bay-morning.1a9f358c.png +0 -0
  46. package/dist/dashboard/assets/next/static/media/golden-gate-afternoon.3e7cea58.png +0 -0
  47. package/dist/dashboard/assets/next/static/media/golden-gate-portrait.7941528f.png +0 -0
  48. package/dist/dashboard/index.html +1 -1
  49. package/dist/gateway.js +1571 -215
  50. package/dist/worker.js +316 -22
  51. package/dist/world-host.js +1090 -0
  52. package/package.json +6 -1
  53. package/dist/dashboard/assets/next/static/It81cabE0Vc-SUEO_z8CQ/_buildManifest.js +0 -1
  54. package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-fa1f6ba56d7af38a.js +0 -1
  55. package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-9b4372b1e47bc3bc.js +0 -1
  56. package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-80a0c2c82d5b4efd.js +0 -1
  57. package/dist/dashboard/assets/next/static/chunks/app/layout-1a40c59e535513d4.js +0 -1
  58. package/dist/dashboard/assets/next/static/chunks/app/page-de4735052adab906.js +0 -1
  59. package/dist/dashboard/assets/next/static/chunks/app/pricing/page-7703fc31d6dff0f7.js +0 -1
  60. package/dist/dashboard/assets/next/static/chunks/main-app-99295a8cc37e8a47.js +0 -1
  61. package/dist/dashboard/assets/next/static/chunks/webpack-330c9b9dd99c0d55.js +0 -1
  62. package/dist/dashboard/assets/next/static/css/1323d7da89b3fa5e.css +0 -1
  63. package/dist/dashboard/assets/next/static/css/62e3209163fa5e55.css +0 -1
  64. package/dist/dashboard/assets/next/static/css/9282330a2fd1ab73.css +0 -1
  65. /package/dist/dashboard/assets/next/static/{It81cabE0Vc-SUEO_z8CQ → 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
- record(service, operation, path, detail) {
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
- this.events.push({ sequence: this.events.length + 1, service, operation, path, detail });
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
- const server = createServer(async (req, res) => {
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.unsupported.push(`${service.name}: unsupported parameters for ${slackMethod}`);
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.unsupported.push(`${service.name}: ${req.method} ${url.pathname}`);
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.unsupported.push(`${service.name}: ${req.method} ${path}`);
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((resolve, reject) => {
289
+ await new Promise((resolve2, reject) => {
167
290
  server.once("error", reject);
168
- server.listen(0, "127.0.0.1", resolve);
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]) => JSON.stringify(row[k]) === JSON.stringify(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((resolve) => {
188
- server.close(() => resolve());
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
- const keys = ["schema", "name", "workflowId", "files", "command", "timeoutSeconds", "services", "assertions", "postgres", "redis", "fault"];
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((resolve, reject) => {
574
+ const exitCode = await new Promise((resolve2, reject) => {
286
575
  child.once("error", reject);
287
- child.once("exit", resolve);
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 status = world.unsupported.length || spec.fault && !world.faultTriggered ? "incomplete" : timedOut || exitCode !== 0 ? "error" : [...assertions, ...infrastructureAssertions].every((a) => a.passed) ? "passed" : "failed";
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,