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
@@ -0,0 +1,1090 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+
7
+ // src/runtime/contracts.ts
8
+ function operationMatrix(spec) {
9
+ return {
10
+ schema: "agenttwin/contracts-v1",
11
+ unsupported: { status: 501, marker: "X-AgentTwin-Unsupported", coverage: "incomplete", forwardedToProduction: false },
12
+ adapters: CONTRACTS,
13
+ services: spec?.services.map((s) => ({
14
+ name: s.name,
15
+ adapter: s.adapter ?? "json-collection",
16
+ version: CONTRACTS[s.adapter ?? "json-collection"].version,
17
+ 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 })))
18
+ })) ?? []
19
+ };
20
+ }
21
+ var CONTRACTS;
22
+ var init_contracts = __esm({
23
+ "src/runtime/contracts.ts"() {
24
+ "use strict";
25
+ CONTRACTS = {
26
+ "json-collection": {
27
+ version: "1.0.0",
28
+ authentication: "Bearer run-scoped test token",
29
+ operations: [
30
+ { method: "GET", endpoint: "<collection path>", effect: "read", response: "{data: records}", errors: ["401 invalid_test_credential", "429 world_capacity"] },
31
+ { 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"] }
32
+ ],
33
+ limitations: ["Custom API contract, not a vendor replica", "No update/delete, pagination, OAuth, webhooks or schema validation", "Idempotency compares the exact parsed JSON serialization"]
34
+ },
35
+ stripe: {
36
+ version: "1.0.0",
37
+ authentication: "Bearer run-scoped test token; official Stripe SDK configured with test endpoint",
38
+ operations: [
39
+ { method: "POST", endpoint: "/v1/customers", effect: "append customer", parameters: ["email", "name", "description"], errors: ["400 invalid parameters"] },
40
+ { method: "GET", endpoint: "/v1/customers[/<id>]", effect: "retrieve or list customers", parameters: ["limit", "starting_after"], errors: ["404 resource_missing", "400 invalid pagination"] },
41
+ { 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"] },
42
+ { method: "GET", endpoint: "/v1/payment_intents/<id>", effect: "retrieve intent", parameters: [], errors: ["404 resource_missing"] },
43
+ { 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"] }
44
+ ],
45
+ 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"]
46
+ },
47
+ slack: {
48
+ version: "1.0.0",
49
+ authentication: "Bearer run-scoped test token; no Slack OAuth",
50
+ operations: [
51
+ { method: "POST", endpoint: "/api/chat.postMessage", effect: "append /messages", parameters: ["channel", "text"], errors: ["channel_not_found", "no_text"] },
52
+ { method: "GET|POST", endpoint: "/api/conversations.history", effect: "read /messages", parameters: ["channel"], errors: ["channel_not_found"] },
53
+ { method: "GET|POST", endpoint: "/api/conversations.list", effect: "read /channels", parameters: [], errors: [] }
54
+ ],
55
+ 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"]
56
+ }
57
+ };
58
+ }
59
+ });
60
+
61
+ // src/runtime/git.ts
62
+ import { execFile } from "child_process";
63
+ import { promisify } from "util";
64
+ async function captureGit(cwd, previousHead) {
65
+ const exec = promisify(execFile);
66
+ const git = async (...args) => (await exec("git", args, { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 })).stdout.trim();
67
+ try {
68
+ const head = await git("rev-parse", "HEAD");
69
+ const [branch, total, status, history] = await Promise.all([git("branch", "--show-current"), git("rev-list", "--count", "HEAD"), git("status", "--porcelain", "--untracked-files=no"), git("log", "--since=14 days ago", "-100", "--format=%H %cI")]);
70
+ let changed = await git("diff", "--name-only", "HEAD");
71
+ if (previousHead && /^[a-f0-9]{40,64}$/.test(previousHead) && previousHead !== head) {
72
+ try {
73
+ changed += "\n" + await git("diff", "--name-only", previousHead, head);
74
+ } catch {
75
+ previousHead = void 0;
76
+ }
77
+ }
78
+ return { head, branch: branch || "detached HEAD", totalCommits: Number(total), dirty: !!status, capturedAt: (/* @__PURE__ */ new Date()).toISOString(), commits: history ? history.split("\n").map((line) => {
79
+ const [sha, date] = line.split(" ");
80
+ return { sha, date };
81
+ }) : [], changedFiles: [...new Set(changed.split("\n").filter(Boolean))].slice(0, 1e3), ...previousHead ? { previousHead } : {} };
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ var init_git = __esm({
87
+ "src/runtime/git.ts"() {
88
+ "use strict";
89
+ }
90
+ });
91
+
92
+ // src/runtime/world-session.ts
93
+ import { createServer as createServer2 } from "http";
94
+ import { randomBytes as randomBytes3, randomUUID as randomUUID2, createHash as createHash3 } from "crypto";
95
+ import { mkdir as mkdir4, mkdtemp as mkdtemp3, readFile as readFile6, readdir as readdir2, rename, rm as rm4, writeFile as writeFile5 } from "fs/promises";
96
+ import { dirname as dirname2, join as join7, resolve as resolve3, relative as relative2, sep as sep2 } from "path";
97
+ import { tmpdir as tmpdir3 } from "os";
98
+ import { fileURLToPath as fileURLToPath2 } from "url";
99
+ import { spawn as spawn2 } from "child_process";
100
+
101
+ // src/runtime/http-world.ts
102
+ import { createServer } from "http";
103
+ import { randomBytes } from "crypto";
104
+ import { isDeepStrictEqual } from "util";
105
+ import { AsyncLocalStorage } from "async_hooks";
106
+
107
+ // src/runtime/request-source.ts
108
+ import { mkdtemp, writeFile, rm, realpath } from "fs/promises";
109
+ import { tmpdir } from "os";
110
+ import { join, resolve } from "path";
111
+ function parseRequestSource(raw, spec) {
112
+ if (typeof raw !== "string" || raw.length > 1024) return;
113
+ try {
114
+ const source = JSON.parse(raw);
115
+ 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;
116
+ return { file: source.file, line: source.line, column: source.column, provenance: "application-reported" };
117
+ } catch {
118
+ return;
119
+ }
120
+ }
121
+ async function createRequestSourceHook(app, spec, credentials) {
122
+ const dir = await mkdtemp(join(tmpdir(), "agenttwin-source-hook-"));
123
+ const path = join(dir, "hook.cjs");
124
+ const config = { root: await realpath(resolve(app)), files: spec.files, origins: Object.values(credentials).map((c) => new URL(c.endpoint).origin) };
125
+ const hook = `const config=${JSON.stringify(config)};
126
+ ` + String.raw`
127
+ const http=require('node:http'),https=require('node:https'),path=require('node:path'),{fileURLToPath}=require('node:url');
128
+ const origins=new Set(config.origins),files=new Set(config.files);
129
+ function source(){for(const frame of new Error().stack.split('\n').slice(2)){const match=frame.match(/(?:\(|\s)(file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?$/);if(!match)continue;let file;try{file=match[1].startsWith('file:')?fileURLToPath(match[1]):match[1];}catch{continue;}file=path.relative(config.root,file);if(files.has(file))return JSON.stringify({file,line:Number(match[2]),column:Number(match[3])});}}
130
+ const originalFetch=globalThis.fetch;
131
+ if(originalFetch)globalThis.fetch=function(input,init){let url;try{url=new URL(typeof input==='string'||input instanceof URL?input:input.url);}catch{return originalFetch(input,init);}if(!origins.has(url.origin))return originalFetch(input,init);const location=source();if(!location)return originalFetch(input,init);const headers=new Headers(init?.headers??input?.headers);headers.set('x-agenttwin-source',location);return originalFetch(input,{...init,headers});};
132
+ for(const mod of [http,https])for(const method of ['request','get']){const original=mod[method];mod[method]=function(...args){let options,url,index;try{if(typeof args[0]==='string'||args[0] instanceof URL){url=new URL(args[0]);index=1;options=args[1]&&typeof args[1]==='object'?args[1]:{};}else{index=0;options=args[0]||{};url=new URL((options.protocol||(mod===https?'https:':'http:'))+'//'+(options.hostname||options.host||'localhost')+(options.port?':'+options.port:''));}}catch{return original.apply(this,args);}if(!origins.has(url.origin))return original.apply(this,args);const location=source();if(!location)return original.apply(this,args);const headers=Array.isArray(options.headers)?[...options.headers,'x-agenttwin-source',location]:{...options.headers,'x-agenttwin-source':location};const updated={...options,headers};if(index===1&&typeof args[1]==='function')args.splice(1,0,updated);else args[index]=updated;return original.apply(this,args);};}
133
+ require('node:module').syncBuiltinESMExports();
134
+ `;
135
+ await writeFile(path, hook, { mode: 384 });
136
+ return { path, close: () => rm(dir, { recursive: true, force: true }) };
137
+ }
138
+
139
+ // src/runtime/stripe-adapter.ts
140
+ function stripeOperation(method, path, params, data, sequence) {
141
+ const error = (status, code, message, param) => ({ status, body: { error: { type: "invalid_request_error", code, message, ...param ? { param } : {} } } });
142
+ const match = /^\/v1\/(customers|payment_intents)(?:\/([^/]+))?(?:\/(confirm))?$/.exec(path);
143
+ if (!match || !["GET", "POST"].includes(method)) return void 0;
144
+ const [, resource, id, action] = match, collection = `/v1/${resource}`, rows = data[collection];
145
+ 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;
146
+ if (!allowed || resource === "customers" && action || resource === "payment_intents" && method === "GET" && !id) return void 0;
147
+ if (Object.keys(params).some((k) => !allowed.includes(k))) return void 0;
148
+ if (method === "GET" && !id) {
149
+ const limit = params.limit === void 0 ? 10 : Number(params.limit);
150
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) return { ...error(400, "parameter_invalid_integer", "limit must be 1\u2013100", "limit"), collection };
151
+ const ordered = [...rows].reverse();
152
+ const index = params.starting_after === void 0 ? -1 : ordered.findIndex((r) => r.id === params.starting_after);
153
+ if (params.starting_after !== void 0 && index === -1) return { ...error(400, "resource_missing", "No such starting_after customer", "starting_after"), collection };
154
+ const page = ordered.slice(index + 1, index + 1 + limit);
155
+ return { status: 200, body: { object: "list", data: page, has_more: ordered.length > index + 1 + limit, url: collection }, collection, operation: "read" };
156
+ }
157
+ const row = id ? rows.find((r) => r.id === id) : void 0;
158
+ if (id && !row) return { ...error(404, "resource_missing", `No such ${resource}: '${id}'`), collection };
159
+ if (method === "GET") return { status: 200, body: row, collection, operation: "read" };
160
+ if (action === "confirm") {
161
+ if (row.status === "succeeded") return { status: 200, body: row, collection, operation: "replay" };
162
+ const payment = params.payment_method ?? row.payment_method;
163
+ 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 };
164
+ row.payment_method = payment;
165
+ row.status = payment === "pm_card_visa" ? "succeeded" : "requires_payment_method";
166
+ row.last_payment_error = payment === "pm_card_chargeDeclined" ? { code: "card_declined", decline_code: "generic_decline", message: "Your card was declined." } : null;
167
+ 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" };
168
+ }
169
+ const prefix = resource === "customers" ? "cus" : "pi";
170
+ let suffix = sequence;
171
+ while (rows.some((r) => r.id === `${prefix}_at_${suffix}`)) suffix++;
172
+ const created = { id: `${prefix}_at_${suffix}`, object: resource === "customers" ? "customer" : "payment_intent", created: 17e8, livemode: false };
173
+ if (resource === "customers") {
174
+ if (params.email !== void 0 && typeof params.email !== "string") return { ...error(400, "parameter_invalid", "email must be a string", "email"), collection };
175
+ Object.assign(created, { email: null, name: null, description: null, ...params });
176
+ } else {
177
+ const amount = Number(params.amount);
178
+ if (!Number.isSafeInteger(amount) || amount < 1) return { ...error(400, "parameter_invalid_integer", "amount must be a positive integer", "amount"), collection };
179
+ 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 };
180
+ if (params.customer && !data["/v1/customers"].some((r) => r.id === params.customer)) return { ...error(400, "resource_missing", "No such customer", "customer"), collection };
181
+ 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 };
182
+ if (params.confirm !== void 0 && !["true", "false", true, false].includes(params.confirm)) return { ...error(400, "parameter_invalid", "confirm must be boolean", "confirm"), collection };
183
+ const confirm = params.confirm === "true" || params.confirm === true;
184
+ if (confirm && !params.payment_method) return { ...error(400, "parameter_missing", "Missing payment_method", "payment_method"), collection };
185
+ 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 });
186
+ }
187
+ if (rows.length >= 1e4) return { ...error(429, "world_capacity", "World record capacity reached"), collection };
188
+ rows.push(created);
189
+ const declined = resource === "payment_intents" && (params.confirm === "true" || params.confirm === true) && params.payment_method === "pm_card_chargeDeclined";
190
+ 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" };
191
+ }
192
+
193
+ // src/runtime/http-world.ts
194
+ var HttpWorld = class {
195
+ constructor(spec) {
196
+ this.spec = spec;
197
+ }
198
+ spec;
199
+ events = [];
200
+ state = /* @__PURE__ */ Object.create(null);
201
+ credentials = /* @__PURE__ */ Object.create(null);
202
+ unsupported = [];
203
+ servers = [];
204
+ timers = /* @__PURE__ */ new Set();
205
+ fired = false;
206
+ epoch = 0;
207
+ replayKeys = /* @__PURE__ */ new Map();
208
+ sourceContext = new AsyncLocalStorage();
209
+ record(service, operation, path, detail, record) {
210
+ if (this.events.length >= 5e4) return;
211
+ const source = this.sourceContext.getStore();
212
+ this.events.push({ sequence: this.events.length + 1, service, operation, path, detail, ...record ? { record: structuredClone(record) } : {}, ...source ? { source } : {} });
213
+ }
214
+ rejectUnsupported(service, method, path, detail) {
215
+ this.unsupported.push(`${service}: ${method} ${path}: ${detail}`);
216
+ this.record(service, "unsupported", path, detail);
217
+ }
218
+ async start() {
219
+ if (this.servers.length) throw new Error("World is already started");
220
+ for (const service of this.spec.services) {
221
+ const token = `at_world_${randomBytes(24).toString("hex")}`;
222
+ const data = /* @__PURE__ */ Object.create(null);
223
+ this.state[service.name] = data;
224
+ for (const c of service.collections) data[c.path] = structuredClone(c.seed);
225
+ const keys = /* @__PURE__ */ new Map();
226
+ this.replayKeys.set(service.name, keys);
227
+ const server = createServer((req, res) => this.sourceContext.run(parseRequestSource(req.headers["x-agenttwin-source"], this.spec), async () => {
228
+ const epoch = this.epoch;
229
+ const send = (code, body) => {
230
+ if (!res.destroyed) {
231
+ res.writeHead(code, { "content-type": "application/json", ...code === 501 ? { "X-AgentTwin-Unsupported": "true" } : {} });
232
+ res.end(JSON.stringify(body));
233
+ }
234
+ };
235
+ try {
236
+ if (req.headers.authorization !== `Bearer ${token}`) {
237
+ this.record(service.name, "auth-denied", req.url ?? "", "invalid test credential");
238
+ send(401, { error: "invalid_test_credential" });
239
+ return;
240
+ }
241
+ if (this.events.length >= 5e4 || this.unsupported.length >= 1e3) {
242
+ send(429, { error: "world_capacity" });
243
+ return;
244
+ }
245
+ const url = new URL(req.url ?? "/", "http://localhost");
246
+ let path = url.pathname;
247
+ const slack = service.adapter === "slack";
248
+ const slackMethod = path.replace(/^\/api\//, "");
249
+ let payload;
250
+ if (req.method === "POST") {
251
+ const chunks = [];
252
+ let bytes = 0;
253
+ for await (const chunk of req) {
254
+ bytes += chunk.length;
255
+ if (bytes > 1024 * 1024) {
256
+ send(413, { error: "body_too_large" });
257
+ return;
258
+ }
259
+ chunks.push(Buffer.from(chunk));
260
+ }
261
+ const text = Buffer.concat(chunks).toString("utf8");
262
+ let parsed;
263
+ try {
264
+ parsed = (slack || service.adapter === "stripe") && req.headers["content-type"]?.startsWith("application/x-www-form-urlencoded") ? Object.fromEntries(new URLSearchParams(text)) : JSON.parse(text);
265
+ } catch {
266
+ send(400, { error: "expected_json_object" });
267
+ return;
268
+ }
269
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
270
+ send(400, { error: "expected_json_object" });
271
+ return;
272
+ }
273
+ payload = parsed;
274
+ }
275
+ if (epoch !== this.epoch) {
276
+ send(409, { error: "world_reset" });
277
+ return;
278
+ }
279
+ if (service.adapter === "stripe") {
280
+ const params = payload ?? Object.fromEntries(url.searchParams);
281
+ const keyHeader2 = req.headers["idempotency-key"];
282
+ const key2 = req.method === "POST" && typeof keyHeader2 === "string" ? `${path}:${keyHeader2}` : void 0;
283
+ const signature2 = JSON.stringify(params);
284
+ const previous2 = key2 ? keys.get(key2) : void 0;
285
+ if (previous2) {
286
+ if (previous2.body !== signature2) {
287
+ send(400, { error: { type: "idempotency_error", message: "Keys for idempotent requests can only be used with the same parameters" } });
288
+ return;
289
+ }
290
+ this.record(service.name, "replay", path, "same-key response replay", previous2.record);
291
+ send(200, previous2.record);
292
+ return;
293
+ }
294
+ const result = stripeOperation(req.method ?? "", path, params, data, this.events.length + 1);
295
+ if (!result) {
296
+ this.rejectUnsupported(service.name, req.method ?? "", path, "unsupported Stripe operation or parameters");
297
+ send(501, { error: { type: "invalid_request_error", code: "unsupported_operation", message: "Outside the verified AgentTwin Stripe subset" }, forwardedToProduction: false });
298
+ return;
299
+ }
300
+ if (result.operation) this.record(service.name, result.operation, result.collection, path, "record" in result ? result.record : result.body);
301
+ if (key2 && result.status === 200) keys.set(key2, { body: signature2, record: structuredClone(result.body) });
302
+ const fault2 = this.spec.fault;
303
+ if (!this.fired && result.operation === "write" && fault2?.service === service.name && fault2.path === result.collection) {
304
+ this.fired = true;
305
+ this.record(service.name, "fault", result.collection, `write committed; response delayed ${fault2.delayMs}ms`);
306
+ const timer = setTimeout(() => {
307
+ this.timers.delete(timer);
308
+ send(result.status, result.body);
309
+ }, fault2.delayMs);
310
+ this.timers.add(timer);
311
+ } else send(result.status, result.body);
312
+ return;
313
+ }
314
+ if (slack) {
315
+ const params = payload ?? Object.fromEntries(url.searchParams);
316
+ const allowed = slackMethod === "chat.postMessage" ? ["channel", "text"] : slackMethod === "conversations.history" ? ["channel"] : [];
317
+ if (Object.keys(params).some((k) => !allowed.includes(k))) {
318
+ this.rejectUnsupported(service.name, req.method ?? "", url.pathname, `unsupported parameters for ${slackMethod}`);
319
+ send(501, { ok: false, error: "unsupported_parameters", forwardedToProduction: false });
320
+ return;
321
+ }
322
+ if (slackMethod === "chat.postMessage" && req.method === "POST") path = "/messages";
323
+ else if (slackMethod === "conversations.history" && ["GET", "POST"].includes(req.method ?? "")) path = "/messages";
324
+ else if (slackMethod === "conversations.list" && ["GET", "POST"].includes(req.method ?? "")) path = "/channels";
325
+ else {
326
+ this.rejectUnsupported(service.name, req.method ?? "", url.pathname, "unsupported operation");
327
+ send(501, { ok: false, error: "unsupported_operation", forwardedToProduction: false });
328
+ return;
329
+ }
330
+ }
331
+ if (!Object.hasOwn(data, path) || !["GET", "POST"].includes(req.method ?? "")) {
332
+ this.rejectUnsupported(service.name, req.method ?? "", path, "unsupported operation");
333
+ send(501, { error: "unsupported_operation", forwardedToProduction: false });
334
+ return;
335
+ }
336
+ if (req.method === "GET" || slack && slackMethod !== "chat.postMessage") {
337
+ this.record(service.name, "read", path, `${data[path].length} records`);
338
+ if (slack && path === "/channels") send(200, { ok: true, channels: data[path], response_metadata: { next_cursor: "" } });
339
+ else if (slack) {
340
+ const channel = payload?.channel ?? url.searchParams.get("channel");
341
+ if (!data["/channels"].some((c) => c.id === channel)) send(200, { ok: false, error: "channel_not_found" });
342
+ else send(200, { ok: true, messages: data[path].filter((m) => m.channel === channel), has_more: false });
343
+ } else send(200, { data: data[path] });
344
+ return;
345
+ }
346
+ const body = payload;
347
+ if (slack) {
348
+ const message = body;
349
+ if (!data["/channels"].some((c) => c.id === message.channel)) {
350
+ send(200, { ok: false, error: "channel_not_found" });
351
+ return;
352
+ }
353
+ if (typeof message.text !== "string" || !message.text) {
354
+ send(200, { ok: false, error: "no_text" });
355
+ return;
356
+ }
357
+ }
358
+ if (data[path].length >= 1e4 || this.events.length >= 5e4) {
359
+ send(429, { error: "world_capacity" });
360
+ return;
361
+ }
362
+ const keyHeader = slack ? void 0 : req.headers["idempotency-key"];
363
+ const key = typeof keyHeader === "string" ? `${path}:${keyHeader}` : void 0;
364
+ const signature = JSON.stringify(body);
365
+ const previous = key ? keys.get(key) : void 0;
366
+ if (previous) {
367
+ if (previous.body !== signature) {
368
+ send(409, { error: "idempotency_conflict" });
369
+ return;
370
+ }
371
+ this.record(service.name, "replay", path, "same-key response replay", previous.record);
372
+ send(200, previous.record);
373
+ return;
374
+ }
375
+ let index = data[path].length + 1;
376
+ while (data[path].some((r) => r.id === `${service.name}_${index}`)) index++;
377
+ const row = { ...body, id: `${service.name}_${index}` };
378
+ if (slack) row.ts = `${index}.000001`;
379
+ const reply = slack ? { ok: true, channel: row.channel, ts: row.ts, message: row } : row;
380
+ data[path].push(row);
381
+ if (key) keys.set(key, { body: signature, record: row });
382
+ this.record(service.name, "write", path, String(row.id), row);
383
+ const fault = this.spec.fault;
384
+ if (!this.fired && fault?.service === service.name && fault.path === path) {
385
+ this.fired = true;
386
+ this.record(service.name, "fault", path, `write committed; response delayed ${fault.delayMs}ms`);
387
+ const timer = setTimeout(() => {
388
+ this.timers.delete(timer);
389
+ send(slack ? 200 : 201, reply);
390
+ }, fault.delayMs);
391
+ this.timers.add(timer);
392
+ } else send(slack ? 200 : 201, reply);
393
+ } catch {
394
+ send(500, { error: "world_request_error" });
395
+ }
396
+ }));
397
+ server.requestTimeout = 1e4;
398
+ await new Promise((resolve4, reject) => {
399
+ server.once("error", reject);
400
+ server.listen(0, "127.0.0.1", resolve4);
401
+ });
402
+ this.servers.push(server);
403
+ this.credentials[service.name] = { endpoint: `http://127.0.0.1:${server.address().port}`, token };
404
+ }
405
+ return this.credentials;
406
+ }
407
+ assertions() {
408
+ return this.spec.assertions.map((a) => {
409
+ const rows = this.state[a.service][a.path].filter((row) => !a.where || Object.entries(a.where).every(([k, v]) => isDeepStrictEqual(row[k], v)));
410
+ return { ...a, actual: rows.length, passed: rows.length === a.count };
411
+ });
412
+ }
413
+ get faultTriggered() {
414
+ return this.fired;
415
+ }
416
+ snapshot() {
417
+ return structuredClone(this.state);
418
+ }
419
+ quiesce() {
420
+ this.epoch++;
421
+ for (const server of this.servers) server.closeAllConnections();
422
+ for (const timer of this.timers) clearTimeout(timer);
423
+ this.timers.clear();
424
+ }
425
+ reset() {
426
+ this.quiesce();
427
+ for (const service of this.spec.services) {
428
+ for (const collection of service.collections) this.state[service.name][collection.path] = structuredClone(collection.seed);
429
+ this.replayKeys.get(service.name)?.clear();
430
+ }
431
+ this.events.length = 0;
432
+ this.unsupported.length = 0;
433
+ this.fired = false;
434
+ return this.snapshot();
435
+ }
436
+ async close() {
437
+ this.epoch++;
438
+ for (const timer of this.timers) clearTimeout(timer);
439
+ this.timers.clear();
440
+ await Promise.all(this.servers.map((server) => new Promise((resolve4) => {
441
+ server.close(() => resolve4());
442
+ server.closeAllConnections();
443
+ })));
444
+ this.servers.length = 0;
445
+ }
446
+ };
447
+
448
+ // src/runtime/browser-execution.ts
449
+ import { chromium } from "playwright";
450
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
451
+ async function browserExecution(spec, world, signal) {
452
+ signal?.throwIfAborted();
453
+ const recipe = spec.browser;
454
+ const browser = await chromium.launch({ headless: true, timeout: spec.timeoutSeconds * 1e3, ...process.env.AGENTTWIN_CHROMIUM_EXECUTABLE_PATH ? { executablePath: process.env.AGENTTWIN_CHROMIUM_EXECUTABLE_PATH } : {} });
455
+ const abort = () => {
456
+ void browser.close().catch(() => {
457
+ });
458
+ };
459
+ signal?.addEventListener("abort", abort, { once: true });
460
+ try {
461
+ signal?.throwIfAborted();
462
+ const context = await browser.newContext({ serviceWorkers: "block", viewport: { width: 1280, height: 720 } });
463
+ const origins = /* @__PURE__ */ new Set([new URL(recipe.baseUrl).origin, ...Object.values(world.credentials).map((c) => new URL(c.endpoint).origin)]);
464
+ const blocked = /* @__PURE__ */ new Set();
465
+ await context.route("**/*", (route) => {
466
+ const origin = new URL(route.request().url()).origin;
467
+ if (origins.has(origin)) return route.continue();
468
+ if (blocked.size < 100) blocked.add(origin);
469
+ return route.abort("blockedbyclient");
470
+ });
471
+ const page = await context.newPage();
472
+ page.setDefaultTimeout(Math.min(spec.timeoutSeconds * 500, 1e4));
473
+ const deadline = Date.now() + Math.min(spec.timeoutSeconds * 1e3, 1e4);
474
+ while (true) {
475
+ signal?.throwIfAborted();
476
+ try {
477
+ const r = await fetch(recipe.baseUrl, { signal: AbortSignal.timeout(300) });
478
+ if (r.ok) break;
479
+ } catch {
480
+ }
481
+ if (Date.now() > deadline) throw new Error("Application browser readiness timed out");
482
+ await new Promise((r) => setTimeout(r, 50));
483
+ }
484
+ const steps = [];
485
+ let error;
486
+ for (const step of recipe.steps) {
487
+ signal?.throwIfAborted();
488
+ try {
489
+ if (step.action === "goto") await page.goto(new URL(step.path, recipe.baseUrl).href);
490
+ else if (step.action === "click") await page.locator(step.selector).click();
491
+ else if (step.action === "fill") await page.locator(step.selector).fill(step.value);
492
+ else await page.locator(step.selector).filter({ hasText: step.text }).waitFor({ state: "visible" });
493
+ steps.push({ action: step.action, passed: true });
494
+ } catch (cause) {
495
+ error = cause instanceof Error ? cause.message : String(cause);
496
+ for (const s of recipe.steps) if (s.action === "fill" && s.value) error = error.split(s.value).join("[input]");
497
+ steps.push({ action: step.action, passed: false });
498
+ break;
499
+ }
500
+ }
501
+ let screenshot;
502
+ try {
503
+ const bytes = await page.screenshot({ type: "jpeg", quality: 65 });
504
+ if (bytes.length <= 768 * 1024) screenshot = { mimeType: "image/jpeg", base64: bytes.toString("base64") };
505
+ } catch {
506
+ }
507
+ for (const origin of blocked) world.unsupported.push(`browser: blocked unmodeled request origin ${origin}`);
508
+ return { engine: "playwright-chromium", steps, passed: !error, error, blockedOrigins: [...blocked], screenshot };
509
+ } finally {
510
+ signal?.removeEventListener("abort", abort);
511
+ await browser.close();
512
+ }
513
+ }
514
+ async function waitBackground(spec, world, signal) {
515
+ const check = spec.background, deadline = Date.now() + check.timeoutMs;
516
+ while (true) {
517
+ signal?.throwIfAborted();
518
+ const actual = world.state[check.service][check.path].filter((row) => !check.where || Object.entries(check.where).every(([k, v]) => isDeepStrictEqual2(row[k], v))).length;
519
+ if (actual === check.count) return { ...check, actual, passed: true };
520
+ if (Date.now() >= deadline) return { ...check, actual, passed: false };
521
+ await new Promise((r) => setTimeout(r, 25));
522
+ }
523
+ }
524
+
525
+ // src/runtime/local.ts
526
+ import { spawn } from "child_process";
527
+
528
+ // src/runtime/evaluation.ts
529
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
530
+ var matches = (row, where) => !where || Object.entries(where).every(([k, v]) => isDeepStrictEqual3(row[k], v));
531
+ function evaluateState(spec, initial, final, events) {
532
+ const checks = spec.assertions.map((a, index) => {
533
+ const before = initial[a.service]?.[a.path];
534
+ const after = final[a.service]?.[a.path];
535
+ const actual = after?.filter((row) => matches(row, a.where)).length;
536
+ return {
537
+ id: `assertion_${index + 1}`,
538
+ kind: "required-outcome",
539
+ service: a.service,
540
+ path: a.path,
541
+ passed: actual === a.count,
542
+ expected: a.count,
543
+ actual: actual ?? null,
544
+ evidence: {
545
+ initial: `/initialState/${escape(a.service)}/${escape(a.path)}`,
546
+ final: `/finalState/${escape(a.service)}/${escape(a.path)}`,
547
+ beforeCount: before?.filter((row) => matches(row, a.where)).length ?? null,
548
+ eventSequences: events.filter((e) => e.service === a.service && e.path === a.path).map((e) => e.sequence)
549
+ }
550
+ };
551
+ });
552
+ const forbidden = (spec.forbiddenEffects ?? []).map((policy) => {
553
+ 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)));
554
+ const unknown = events.some((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && policy.where && !e.record);
555
+ return {
556
+ id: policy.id,
557
+ kind: "forbidden-effect",
558
+ service: policy.service,
559
+ path: policy.path,
560
+ passed: hits.length === 0 && !unknown,
561
+ expected: 0,
562
+ actual: hits.length,
563
+ evidence: { eventSequences: hits.map((e) => e.sequence), evidenceGap: unknown }
564
+ };
565
+ });
566
+ const preserved = (spec.preservedState ?? []).map((policy) => {
567
+ const before = initial[policy.service]?.[policy.path];
568
+ const after = final[policy.service]?.[policy.path];
569
+ const passed = !!before && !!after && isDeepStrictEqual3(before.filter((row) => matches(row, policy.where)), after.filter((row) => matches(row, policy.where)));
570
+ return {
571
+ id: policy.id,
572
+ kind: "preserved-state",
573
+ service: policy.service,
574
+ path: policy.path,
575
+ passed,
576
+ expected: "unchanged",
577
+ actual: passed ? "unchanged" : "changed or missing",
578
+ evidence: { initial: `/initialState/${escape(policy.service)}/${escape(policy.path)}`, final: `/finalState/${escape(policy.service)}/${escape(policy.path)}` }
579
+ };
580
+ });
581
+ const taskPassed = checks.every((c) => c.passed);
582
+ const safe = [...forbidden, ...preserved].every((c) => c.passed);
583
+ return { schema: "agenttwin/evaluation-v1", outcome: !safe ? "unsafe" : taskPassed ? "passed" : "failed", taskPassed, safe, checks: [...checks, ...forbidden, ...preserved] };
584
+ }
585
+ var escape = (value) => value.replace(/~/g, "~0").replace(/\//g, "~1");
586
+
587
+ // src/runtime/local.ts
588
+ init_contracts();
589
+ async function executeLocal(app, spec, signal, existingWorld) {
590
+ if (spec.postgres || spec.redis) throw new Error("This world needs PostgreSQL or Redis. Use --runtime hosted or --runtime e2b, or prepare a workflow using HTTP twins only. No local containers are started.");
591
+ signal?.throwIfAborted();
592
+ const world = existingWorld ?? new HttpWorld(spec), started = Date.now();
593
+ try {
594
+ const credentials = existingWorld ? world.credentials : await world.start();
595
+ const initialState = world.snapshot();
596
+ const env = { PATH: process.env.PATH ?? "", NODE_ENV: "test" };
597
+ for (const [name, c] of Object.entries(credentials)) {
598
+ env[`AGENTTWIN_${name.toUpperCase()}_URL`] = c.endpoint;
599
+ env[`AGENTTWIN_${name.toUpperCase()}_TOKEN`] = c.token;
600
+ }
601
+ let stdout = "", stderr = "", timedOut = false;
602
+ const hook = await createRequestSourceHook(app, spec, credentials);
603
+ const child = spawn(process.execPath, ["--require", hook.path, ...spec.command.slice(1)], { cwd: app, env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
604
+ child.stdout.on("data", (b) => {
605
+ stdout = (stdout + b.toString()).slice(-65536);
606
+ });
607
+ child.stderr.on("data", (b) => {
608
+ stderr = (stderr + b.toString()).slice(-65536);
609
+ });
610
+ const kill = () => {
611
+ if (child.pid) {
612
+ try {
613
+ process.platform === "win32" ? child.kill("SIGKILL") : process.kill(-child.pid, "SIGKILL");
614
+ } catch {
615
+ }
616
+ }
617
+ };
618
+ const executionAbort = new AbortController();
619
+ const abort = () => {
620
+ executionAbort.abort();
621
+ kill();
622
+ };
623
+ signal?.addEventListener("abort", abort, { once: true });
624
+ const timer = setTimeout(() => {
625
+ timedOut = true;
626
+ executionAbort.abort();
627
+ kill();
628
+ }, spec.timeoutSeconds * 1e3);
629
+ const completion = new Promise((resolve4, reject) => {
630
+ child.once("error", reject);
631
+ child.once("close", resolve4);
632
+ });
633
+ let browser, background, executionError;
634
+ let exitCode;
635
+ try {
636
+ if (spec.browser || spec.background) {
637
+ const work = (async () => {
638
+ if (spec.browser) browser = await browserExecution(spec, world, executionAbort.signal);
639
+ if (spec.background) background = await waitBackground(spec, world, executionAbort.signal);
640
+ return 0;
641
+ })();
642
+ exitCode = await Promise.race([work, completion.then((code) => {
643
+ if (!spec.browser && code === 0) return work;
644
+ throw new Error(`Application exited before browser/background completion (${code})`);
645
+ })]);
646
+ } else exitCode = await completion;
647
+ } catch (error) {
648
+ executionError = error instanceof Error ? error.message : String(error);
649
+ exitCode = 1;
650
+ } finally {
651
+ executionAbort.abort();
652
+ clearTimeout(timer);
653
+ signal?.removeEventListener("abort", abort);
654
+ kill();
655
+ await completion.catch(() => {
656
+ });
657
+ await hook.close();
658
+ }
659
+ signal?.throwIfAborted();
660
+ if (existingWorld) world.quiesce();
661
+ else await world.close();
662
+ for (const c of Object.values(credentials)) {
663
+ stdout = stdout.split(c.token).join("[test-token]");
664
+ stderr = stderr.split(c.token).join("[test-token]");
665
+ }
666
+ const assertions = world.assertions();
667
+ const evaluation = evaluateState(spec, initialState, world.state, world.events);
668
+ const status = world.unsupported.length || spec.fault && !world.faultTriggered ? "incomplete" : timedOut || exitCode !== 0 ? "error" : background?.passed === false || browser?.passed === false ? "failed" : evaluation.outcome === "passed" ? "passed" : "failed";
669
+ return { schema: "agenttwin/run-v1", name: spec.name, workflowId: spec.workflowId, status, browser, background, executionError, application: { command: spec.command, exitCode, timedOut, stdout, stderr }, assertions, evaluation, contracts: operationMatrix(spec), scenario: spec.scenario, infrastructureAssertions: [], fault: spec.fault ? { ...spec.fault, triggered: world.faultTriggered } : null, initialState, finalState: world.snapshot(), events: structuredClone(world.events), unsupported: [...world.unsupported], durationMs: Date.now() - started, coverage: "CLI Node execution with fresh in-process HTTP collections and advertised provider subsets. Optional Chromium browser steps and bounded collection-state background completion. Not a VM or network/filesystem isolation boundary. No production environment variables inherited; reviewed code can still access the host filesystem and network.", isolation: { kind: "process", productionCredentialsInherited: false, network: "host network; not isolated", rootFilesystem: "host-accessible; explicit files staged in temporary directory" } };
670
+ } finally {
671
+ if (existingWorld) world.quiesce();
672
+ else await world.close();
673
+ }
674
+ }
675
+
676
+ // src/runtime/spec.ts
677
+ var object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
678
+ var namePattern = /^[a-z][a-z0-9_]{0,39}$/;
679
+ function parseWorld(value) {
680
+ const invalid = (message) => {
681
+ throw new Error(`Invalid world: ${message}`);
682
+ };
683
+ if (!object(value) || value.schema !== "agenttwin/world-v1") invalid("schema must be agenttwin/world-v1");
684
+ const v = value;
685
+ if (typeof v.name !== "string" || !namePattern.test(v.name)) invalid("name must be lowercase letters, digits or underscores");
686
+ if (!Array.isArray(v.files) || !v.files.length || v.files.length > 100 || !v.files.every((f) => typeof f === "string" && f.length > 0)) invalid("files must explicitly list 1\u2013100 application files");
687
+ if (new Set(v.files).size !== v.files.length) invalid("files must be unique");
688
+ if (!Array.isArray(v.command) || !v.command.length || !v.command.every((c) => typeof c === "string" && c.length > 0) || v.command[0] !== "node") invalid("command must be an argv array starting with node; supply a bundled Node application");
689
+ if (!Number.isInteger(v.timeoutSeconds) || Number(v.timeoutSeconds) < 1 || Number(v.timeoutSeconds) > 300) invalid("timeoutSeconds must be 1\u2013300");
690
+ if (v.workflowId !== void 0 && (typeof v.workflowId !== "string" || !v.workflowId)) invalid("workflowId must be a nonempty string");
691
+ if (!Array.isArray(v.services) || !v.services.length || v.services.length > 10) invalid("services must contain 1\u201310 HTTP services");
692
+ const services = /* @__PURE__ */ new Map();
693
+ for (const service of v.services) {
694
+ if (!object(service) || typeof service.name !== "string" || !namePattern.test(service.name) || services.has(service.name)) invalid("service names must be unique lowercase identifiers");
695
+ const item = service;
696
+ if (!Array.isArray(item.collections) || !item.collections.length || item.collections.length > 20) invalid("each service needs 1\u201320 collections");
697
+ if (item.adapter !== void 0 && !["json-collection", "slack", "stripe"].includes(String(item.adapter))) invalid("unsupported service adapter");
698
+ const paths = /* @__PURE__ */ new Set();
699
+ for (const entry of item.collections) {
700
+ 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");
701
+ const collection = entry;
702
+ if (!Array.isArray(collection.seed) || collection.seed.length > 1e3 || !collection.seed.every(object)) invalid("seed must be an array of up to 1000 JSON records");
703
+ paths.add(collection.path);
704
+ }
705
+ if (item.adapter === "slack" && (!paths.has("/messages") || !paths.has("/channels") || paths.size !== 2)) invalid("slack adapter requires /messages and /channels collections");
706
+ 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");
707
+ services.set(item.name, paths);
708
+ }
709
+ const known = (entry) => typeof entry.service === "string" && typeof entry.path === "string" && services.get(entry.service)?.has(entry.path);
710
+ if (!Array.isArray(v.assertions) || !v.assertions.length || v.assertions.length > 100) invalid("at least one final-state assertion is required (maximum 100)");
711
+ for (const assertion of v.assertions) {
712
+ 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");
713
+ }
714
+ const policyIds = /* @__PURE__ */ new Set();
715
+ for (const field of ["forbiddenEffects", "preservedState"]) {
716
+ if (v[field] === void 0) continue;
717
+ if (!Array.isArray(v[field]) || v[field].length > 100) invalid(`${field} must contain at most 100 checks`);
718
+ for (const entry of v[field]) {
719
+ 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`);
720
+ const item = entry;
721
+ if (field === "forbiddenEffects" && !["write", "read", "replay"].includes(String(item.operation))) invalid("forbiddenEffects operation must be write, read or replay");
722
+ if (Object.keys(item).some((k) => !["id", "service", "path", "where", ...field === "forbiddenEffects" ? ["operation"] : []].includes(k))) invalid(`unknown ${field} field`);
723
+ policyIds.add(item.id);
724
+ }
725
+ }
726
+ 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");
727
+ if (v.postgres !== void 0) {
728
+ 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");
729
+ for (const a of v.postgres.assertions) {
730
+ if (!object(a) || typeof a.query !== "string" || !/^select\s/i.test(a.query) || a.query.includes(";") || !Array.isArray(a.equals) || !a.equals.every(object)) invalid("postgres assertions require a SELECT query without semicolons and an equals array");
731
+ }
732
+ }
733
+ if (v.redis !== void 0) {
734
+ if (!object(v.redis) || !Array.isArray(v.redis.assertions) || !v.redis.assertions.length || v.redis.assertions.length > 100) invalid("redis requires assertions");
735
+ for (const a of v.redis.assertions) {
736
+ if (!object(a) || typeof a.key !== "string" || !a.key || a.equals !== null && typeof a.equals !== "string") invalid("redis assertions require key and string/null equals");
737
+ }
738
+ }
739
+ 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");
740
+ if (v.browser !== void 0) {
741
+ if (!object(v.browser) || typeof v.browser.baseUrl !== "string") invalid("browser requires baseUrl");
742
+ const browser = v.browser;
743
+ let u;
744
+ try {
745
+ u = new URL(String(v.browser.baseUrl));
746
+ } catch {
747
+ invalid("browser baseUrl must be a loopback HTTP URL");
748
+ }
749
+ 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>");
750
+ 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");
751
+ for (const raw of browser.steps) {
752
+ const step = raw;
753
+ if (!object(step) || !["goto", "click", "fill", "expectText"].includes(String(step.action))) invalid("unsupported browser action");
754
+ if (step.action === "goto") {
755
+ 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");
756
+ } else {
757
+ 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");
758
+ }
759
+ }
760
+ }
761
+ 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");
762
+ const keys = ["schema", "name", "workflowId", "files", "command", "timeoutSeconds", "browser", "background", "services", "assertions", "forbiddenEffects", "preservedState", "scenario", "postgres", "redis", "fault"];
763
+ if (Object.keys(v).some((k) => !keys.includes(k))) invalid("unknown world field");
764
+ return v;
765
+ }
766
+
767
+ // src/runtime/scenarios.ts
768
+ import { createHash } from "crypto";
769
+ import { mkdir, readFile, readdir, writeFile as writeFile2 } from "fs/promises";
770
+ import { join as join2 } from "path";
771
+ import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
772
+ var identifier = /^[a-z][a-z0-9_]{0,39}$/;
773
+ var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
774
+ var sort = (v) => Array.isArray(v) ? v.map(sort) : object2(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])])) : v;
775
+ var scenarioDigest = (scenario) => createHash("sha256").update(JSON.stringify(sort(scenario))).digest("hex");
776
+ function parseScenario(value) {
777
+ const fail = (detail) => {
778
+ throw new Error(`Invalid scenario: ${detail}`);
779
+ };
780
+ if (!object2(value) || value.schema !== "agenttwin/scenario-v1" || typeof value.id !== "string" || !identifier.test(value.id) || !Number.isSafeInteger(value.version) || Number(value.version) < 1 || typeof value.description !== "string" || value.description.length > 2e3 || !Array.isArray(value.services) || !Array.isArray(value.entities) || value.entities.length > 100 || Object.keys(value).some((k) => !["schema", "id", "version", "description", "services", "entities"].includes(k))) fail("schema, id, version, description, services and entities are required");
781
+ const scenario = value;
782
+ parseWorld({
783
+ schema: "agenttwin/world-v1",
784
+ name: scenario.id,
785
+ files: ["app.mjs"],
786
+ command: ["node", "app.mjs"],
787
+ timeoutSeconds: 1,
788
+ services: scenario.services,
789
+ assertions: [{ service: scenario.services[0]?.name, path: scenario.services[0]?.collections?.[0]?.path, count: 0 }]
790
+ });
791
+ const ids = /* @__PURE__ */ new Set();
792
+ for (const entity of scenario.entities) {
793
+ if (!object2(entity) || typeof entity.id !== "string" || !identifier.test(entity.id) || ids.has(entity.id) || !Array.isArray(entity.refs) || entity.refs.length < 2 || entity.refs.length > 20 || Object.keys(entity).some((k) => !["id", "refs"].includes(k))) fail("entities need unique ids and 2\u201320 cross-service references");
794
+ ids.add(entity.id);
795
+ const services = /* @__PURE__ */ new Set();
796
+ for (const ref of entity.refs) {
797
+ if (!object2(ref) || typeof ref.service !== "string" || typeof ref.path !== "string" || !object2(ref.where) || !Object.keys(ref.where).length || Object.keys(ref).some((k) => !["service", "path", "where"].includes(k))) fail("entity reference needs service, path and a nonempty where selector");
798
+ const rows = scenario.services.find((s) => s.name === ref.service)?.collections.find((c) => c.path === ref.path)?.seed;
799
+ if (!rows || rows.filter((row) => Object.entries(ref.where).every(([k, v]) => isDeepStrictEqual4(row[k], v))).length !== 1) fail(`entity ${entity.id} reference must resolve to exactly one seeded record`);
800
+ services.add(ref.service);
801
+ }
802
+ if (services.size < 2) fail(`entity ${entity.id} must link at least two services`);
803
+ }
804
+ return structuredClone(scenario);
805
+ }
806
+ function reference(ref) {
807
+ const match = /^([a-z][a-z0-9_]{0,39})@([1-9][0-9]*)$/.exec(ref);
808
+ if (!match || !Number.isSafeInteger(Number(match[2]))) throw new Error("Scenario reference must be id@version");
809
+ return { id: match[1], version: Number(match[2]) };
810
+ }
811
+ async function loadScenario(cwd, ref) {
812
+ const { id, version } = reference(ref);
813
+ const path = join2(cwd, ".agenttwin/scenarios", id, `${version}.json`);
814
+ const bytes = await readFile(path);
815
+ if (bytes.length > 2 * 1024 * 1024) throw new Error("Scenario exceeds 2 MiB");
816
+ const scenario = parseScenario(JSON.parse(bytes.toString()));
817
+ if (scenario.id !== id || scenario.version !== version) throw new Error("Scenario identity does not match its stored version");
818
+ return scenario;
819
+ }
820
+ function applyScenario(world, scenario) {
821
+ const shape = (services) => services.map((s) => ({ name: s.name, adapter: s.adapter ?? "json-collection", paths: s.collections.map((c) => c.path).sort() })).sort((a, b) => a.name.localeCompare(b.name));
822
+ if (!isDeepStrictEqual4(shape(world.services), shape(scenario.services))) throw new Error("Scenario services, adapters and collections must exactly match the world");
823
+ return parseWorld({ ...world, services: structuredClone(scenario.services), scenario: { id: scenario.id, version: scenario.version, digest: scenarioDigest(scenario) } });
824
+ }
825
+
826
+ // src/runtime/world-session.ts
827
+ init_contracts();
828
+
829
+ // src/commands/run.ts
830
+ import { mkdtemp as mkdtemp2, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4, realpath as realpath2, lstat, rm as rm3, copyFile } from "fs/promises";
831
+ import { tmpdir as tmpdir2 } from "os";
832
+ import { basename, dirname, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
833
+ import { fileURLToPath } from "url";
834
+ import { createHash as createHash2, randomUUID } from "crypto";
835
+
836
+ // src/runtime/credentials.ts
837
+ import { homedir } from "os";
838
+ import { join as join3 } from "path";
839
+ import { readFile as readFile2, mkdir as mkdir2, writeFile as writeFile3, chmod, rm as rm2 } from "fs/promises";
840
+
841
+ // src/runtime/remote.ts
842
+ import { readFile as readFile3 } from "fs/promises";
843
+ import { join as join4 } from "path";
844
+
845
+ // src/lib/exit.ts
846
+ var EXIT = {
847
+ OK: 0,
848
+ ERROR: 1,
849
+ BLOCKED: 2,
850
+ USAGE: 3
851
+ };
852
+ var CliError = class extends Error {
853
+ code;
854
+ action;
855
+ resume;
856
+ constructor(message, opts = {}) {
857
+ super(message);
858
+ this.name = "CliError";
859
+ this.code = opts.code ?? EXIT.ERROR;
860
+ this.action = opts.action;
861
+ this.resume = opts.resume;
862
+ }
863
+ };
864
+
865
+ // src/commands/run.ts
866
+ init_git();
867
+
868
+ // src/runtime/e2b.ts
869
+ import { Sandbox } from "e2b";
870
+ import { readFile as readFile4 } from "fs/promises";
871
+ import { randomBytes as randomBytes2 } from "crypto";
872
+ import { homedir as homedir2 } from "os";
873
+ import { join as join5 } from "path";
874
+
875
+ // src/commands/run.ts
876
+ async function stageApplication(root, files, destination) {
877
+ const canonicalRoot = await realpath2(root);
878
+ const digests = {};
879
+ let bytes = 0;
880
+ for (const file of files) {
881
+ if (isAbsolute(file) || file.includes("\\") || file.split("/").some((p) => p === ".." || /^(\.env.*|\.git|\.ssh|\.aws|\.azure|\.config|\.npmrc|\.netrc|credentials.*)$/i.test(p)) || /\.(pem|key|p12|pfx)$/i.test(file)) throw new CliError(`Unsafe application file: ${file}`, { code: EXIT.USAGE });
882
+ const source = await realpath2(resolve2(canonicalRoot, file));
883
+ const rel = relative(canonicalRoot, source);
884
+ if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel) || source !== resolve2(canonicalRoot, file) || !(await lstat(source)).isFile()) throw new CliError(`Files must be regular files within the project, without symlinks: ${file}`, { code: EXIT.USAGE });
885
+ const contents = await readFile5(source);
886
+ bytes += contents.length;
887
+ if (bytes > 20 * 1024 * 1024) throw new CliError("Application files exceed 20 MiB; supply a smaller built bundle", { code: EXIT.USAGE });
888
+ const target = join6(destination, file);
889
+ await mkdir3(dirname(target), { recursive: true });
890
+ await writeFile4(target, contents, { mode: 420 });
891
+ digests[file] = createHash2("sha256").update(contents).digest("hex");
892
+ }
893
+ return digests;
894
+ }
895
+
896
+ // src/runtime/world-session.ts
897
+ init_git();
898
+ var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
899
+ var hash = (spec) => createHash3("sha256").update(JSON.stringify(spec)).digest("hex");
900
+ function duration(value) {
901
+ if (!Number.isInteger(value) || value < 1 || value > 86400) throw new Error("ttlSeconds must be 1\u201386400");
902
+ return value;
903
+ }
904
+ async function persist(directory2, session) {
905
+ const temporary = join7(directory2, `${randomUUID2()}.tmp`);
906
+ await writeFile5(temporary, JSON.stringify(session, null, 2) + "\n", { mode: 384 });
907
+ await rename(temporary, join7(directory2, "session.json"));
908
+ }
909
+ var view = ({ controlToken: _token, cwd: _cwd, file: _file, controlUrl: _control, ...session }) => session;
910
+ async function serveWorldSession(directory2) {
911
+ const session = JSON.parse(await readFile6(join7(directory2, "session.json"), "utf8"));
912
+ if (session.status !== "provisioning" || !uuid.test(session.id)) throw new Error("Session is not awaiting provisioning");
913
+ const spec = parseWorld(JSON.parse(await readFile6(join7(directory2, "world.json"), "utf8")));
914
+ const world = new HttpWorld(spec);
915
+ let control;
916
+ let timer;
917
+ let active;
918
+ let busy = false;
919
+ let stopping = false;
920
+ let shutdownPromise;
921
+ let saving = Promise.resolve();
922
+ const save = () => {
923
+ const snapshot = { ...session };
924
+ const next = saving.catch(() => {
925
+ }).then(() => persist(directory2, snapshot));
926
+ saving = next;
927
+ return next;
928
+ };
929
+ const shutdown = (status) => {
930
+ if (shutdownPromise) return shutdownPromise;
931
+ stopping = true;
932
+ clearTimeout(timer);
933
+ active?.abort();
934
+ shutdownPromise = (async () => {
935
+ session.status = status;
936
+ session.credentials = void 0;
937
+ await world.close();
938
+ if (control) await new Promise((resolve4) => {
939
+ control.close(() => resolve4());
940
+ control.closeAllConnections();
941
+ });
942
+ await save();
943
+ })();
944
+ return shutdownPromise;
945
+ };
946
+ const arm = () => {
947
+ clearTimeout(timer);
948
+ timer = setTimeout(() => {
949
+ void shutdown("expired");
950
+ }, Math.max(1, Date.parse(session.expiresAt) - Date.now()));
951
+ };
952
+ try {
953
+ session.credentials = await world.start();
954
+ control = createServer2(async (req, res) => {
955
+ const reply = (status, value) => {
956
+ if (!res.destroyed) {
957
+ res.writeHead(status, { "content-type": "application/json" });
958
+ res.end(JSON.stringify(value));
959
+ }
960
+ };
961
+ if (req.headers.origin || req.headers.authorization !== `Bearer ${session.controlToken}`) {
962
+ reply(401, { error: "Unauthorized world control request" });
963
+ return;
964
+ }
965
+ if (stopping || Date.now() >= Date.parse(session.expiresAt)) {
966
+ reply(410, { error: "World expired or terminated" });
967
+ void shutdown("expired");
968
+ return;
969
+ }
970
+ const action = req.url?.slice(1);
971
+ if (action === "status" && req.method === "GET") {
972
+ reply(200, { ...view(session), contracts: operationMatrix(spec) });
973
+ return;
974
+ }
975
+ if (req.method !== "POST" || !["reset", "extend", "run", "teardown"].includes(action ?? "")) {
976
+ reply(404, { error: "Unknown world control operation" });
977
+ return;
978
+ }
979
+ if (busy && action !== "teardown") {
980
+ reply(409, { error: "World is busy; wait for the active operation to finish" });
981
+ return;
982
+ }
983
+ busy = true;
984
+ try {
985
+ const chunks = [];
986
+ let bytes = 0;
987
+ for await (const chunk of req) {
988
+ bytes += chunk.length;
989
+ if (bytes > 8192) throw new Error("Control request exceeds 8 KiB");
990
+ chunks.push(Buffer.from(chunk));
991
+ }
992
+ const input = JSON.parse(Buffer.concat(chunks).toString() || "{}");
993
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((k) => action !== "extend" || k !== "ttlSeconds")) throw new Error("Unknown control parameters");
994
+ if (action === "teardown") {
995
+ reply(200, { id: session.id, status: "terminated" });
996
+ setImmediate(() => {
997
+ void shutdown("terminated");
998
+ });
999
+ return;
1000
+ }
1001
+ if (action === "extend") {
1002
+ session.expiresAt = new Date(Date.now() + duration(Number(input.ttlSeconds ?? 3600)) * 1e3).toISOString();
1003
+ await save();
1004
+ if (!stopping) arm();
1005
+ reply(200, view(session));
1006
+ return;
1007
+ }
1008
+ if (action === "reset") {
1009
+ world.reset();
1010
+ reply(200, { ...view(session), state: world.snapshot() });
1011
+ return;
1012
+ }
1013
+ let current = parseWorld(JSON.parse(await readFile6(session.file, "utf8")));
1014
+ if (session.scenario) current = applyScenario(current, await loadScenario(session.cwd, session.scenario));
1015
+ if (hash(current) !== hash(spec)) throw new Error("World specification or scenario changed; provision a new world. Source patches are allowed.");
1016
+ const stage = await mkdtemp3(join7(tmpdir3(), "agenttwin-session-run-"));
1017
+ active = new AbortController();
1018
+ let result;
1019
+ try {
1020
+ if (stopping) throw new Error("World expired during execution");
1021
+ session.status = "running";
1022
+ await save();
1023
+ active.signal.throwIfAborted();
1024
+ const sourceDigests = await stageApplication(dirname2(session.file), spec.files, stage);
1025
+ active.signal.throwIfAborted();
1026
+ world.reset();
1027
+ const report = await executeLocal(stage, spec, active.signal, world);
1028
+ if (stopping) throw new Error("World expired during execution");
1029
+ const runId = randomUUID2();
1030
+ let project = "local-workspace";
1031
+ try {
1032
+ project = JSON.parse(await readFile6(join7(session.cwd, ".agenttwin/inventory.json"), "utf8")).name ?? project;
1033
+ } catch {
1034
+ }
1035
+ const receipt = { ...report, sourceDirectory: relative2(session.cwd, dirname2(session.file)).split(sep2).join("/"), runId, worldId: session.id, runtime: "local", project, git: await captureGit(session.cwd), createdAt: (/* @__PURE__ */ new Date()).toISOString(), sourceDigests, specDigest: hash(spec), provenance: "CLI-reported execution; not independently attested" };
1036
+ const out = join7(session.cwd, ".agenttwin/runs", runId);
1037
+ await mkdir4(out, { recursive: true, mode: 448 });
1038
+ const evidencePath = join7(out, "run.json");
1039
+ await writeFile5(evidencePath, JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
1040
+ await writeFile5(join7(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
1041
+ session.lastRunId = runId;
1042
+ result = { ...receipt, evidencePath };
1043
+ } finally {
1044
+ active = void 0;
1045
+ await rm4(stage, { recursive: true, force: true });
1046
+ if (!stopping) {
1047
+ session.status = "ready";
1048
+ await save();
1049
+ }
1050
+ }
1051
+ reply(200, result);
1052
+ } catch (error) {
1053
+ reply(400, { error: error instanceof Error ? error.message : "World operation failed" });
1054
+ } finally {
1055
+ busy = false;
1056
+ }
1057
+ });
1058
+ control.requestTimeout = 1e4;
1059
+ await new Promise((resolve4, reject) => {
1060
+ control.once("error", reject);
1061
+ control.listen(0, "127.0.0.1", resolve4);
1062
+ });
1063
+ const latest = JSON.parse(await readFile6(join7(directory2, "session.json"), "utf8"));
1064
+ if (latest.status !== "provisioning") throw new Error("Provisioning was abandoned");
1065
+ session.controlUrl = `http://127.0.0.1:${control.address().port}`;
1066
+ if (stopping) throw new Error("World expired during provisioning");
1067
+ session.status = "ready";
1068
+ await save();
1069
+ if (!stopping) arm();
1070
+ return { session: view(session), close: () => shutdown("terminated") };
1071
+ } catch (error) {
1072
+ session.error = error instanceof Error ? error.message : "Provisioning failed";
1073
+ await shutdown("failed");
1074
+ throw error;
1075
+ }
1076
+ }
1077
+
1078
+ // src/runtime/world-host.ts
1079
+ var directory = process.argv[2];
1080
+ if (!directory) throw new Error("World host requires a session directory");
1081
+ serveWorldSession(directory).then((host) => {
1082
+ const stop = () => {
1083
+ void host.close();
1084
+ };
1085
+ process.once("SIGTERM", stop);
1086
+ process.once("SIGINT", stop);
1087
+ }).catch((error) => {
1088
+ process.stderr.write(String(error) + "\n");
1089
+ process.exitCode = 1;
1090
+ });