@agent-relay/sandbox 0.1.1 → 0.1.3

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 (121) hide show
  1. package/README.md +226 -5
  2. package/dist/agent37/client.d.ts +115 -0
  3. package/dist/agent37/client.d.ts.map +1 -0
  4. package/dist/agent37/client.js +270 -0
  5. package/dist/agent37/client.js.map +1 -0
  6. package/dist/agent37/runtime.d.ts +415 -0
  7. package/dist/agent37/runtime.d.ts.map +1 -0
  8. package/dist/agent37/runtime.js +855 -0
  9. package/dist/agent37/runtime.js.map +1 -0
  10. package/dist/agentcore/bench.d.ts +93 -0
  11. package/dist/agentcore/bench.d.ts.map +1 -0
  12. package/dist/agentcore/bench.js +125 -0
  13. package/dist/agentcore/bench.js.map +1 -0
  14. package/dist/agentcore/capabilities.d.ts +169 -0
  15. package/dist/agentcore/capabilities.d.ts.map +1 -0
  16. package/dist/agentcore/capabilities.js +153 -0
  17. package/dist/agentcore/capabilities.js.map +1 -0
  18. package/dist/agentcore/config.d.ts +156 -0
  19. package/dist/agentcore/config.d.ts.map +1 -0
  20. package/dist/agentcore/config.js +12 -0
  21. package/dist/agentcore/config.js.map +1 -0
  22. package/dist/agentcore/internal/sdk.d.ts +181 -0
  23. package/dist/agentcore/internal/sdk.d.ts.map +1 -0
  24. package/dist/agentcore/internal/sdk.js +223 -0
  25. package/dist/agentcore/internal/sdk.js.map +1 -0
  26. package/dist/agentcore/runtime.d.ts +177 -0
  27. package/dist/agentcore/runtime.d.ts.map +1 -0
  28. package/dist/agentcore/runtime.js +706 -0
  29. package/dist/agentcore/runtime.js.map +1 -0
  30. package/dist/daytona/runtime.d.ts +23 -0
  31. package/dist/daytona/runtime.d.ts.map +1 -1
  32. package/dist/daytona/runtime.js +292 -10
  33. package/dist/daytona/runtime.js.map +1 -1
  34. package/dist/daytona/wire-supplement.d.ts +25 -0
  35. package/dist/daytona/wire-supplement.d.ts.map +1 -0
  36. package/dist/daytona/wire-supplement.js +16 -0
  37. package/dist/daytona/wire-supplement.js.map +1 -0
  38. package/dist/e2b/runtime.d.ts +107 -72
  39. package/dist/e2b/runtime.d.ts.map +1 -1
  40. package/dist/e2b/runtime.js +675 -190
  41. package/dist/e2b/runtime.js.map +1 -1
  42. package/dist/freestyle/capabilities.d.ts +28 -0
  43. package/dist/freestyle/capabilities.d.ts.map +1 -0
  44. package/dist/freestyle/capabilities.js +42 -0
  45. package/dist/freestyle/capabilities.js.map +1 -0
  46. package/dist/freestyle/config.d.ts +36 -0
  47. package/dist/freestyle/config.d.ts.map +1 -0
  48. package/dist/freestyle/config.js +2 -0
  49. package/dist/freestyle/config.js.map +1 -0
  50. package/dist/freestyle/internal/sdk.d.ts +69 -0
  51. package/dist/freestyle/internal/sdk.d.ts.map +1 -0
  52. package/dist/freestyle/internal/sdk.js +19 -0
  53. package/dist/freestyle/internal/sdk.js.map +1 -0
  54. package/dist/freestyle/runtime.d.ts +163 -0
  55. package/dist/freestyle/runtime.d.ts.map +1 -0
  56. package/dist/freestyle/runtime.js +798 -0
  57. package/dist/freestyle/runtime.js.map +1 -0
  58. package/dist/index.d.ts +35 -4
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +16 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/microsandbox/runtime.d.ts +793 -0
  63. package/dist/microsandbox/runtime.d.ts.map +1 -0
  64. package/dist/microsandbox/runtime.js +2303 -0
  65. package/dist/microsandbox/runtime.js.map +1 -0
  66. package/dist/modal/bench.d.ts +199 -0
  67. package/dist/modal/bench.d.ts.map +1 -0
  68. package/dist/modal/bench.js +450 -0
  69. package/dist/modal/bench.js.map +1 -0
  70. package/dist/modal/capabilities.d.ts +180 -0
  71. package/dist/modal/capabilities.d.ts.map +1 -0
  72. package/dist/modal/capabilities.js +159 -0
  73. package/dist/modal/capabilities.js.map +1 -0
  74. package/dist/modal/config.d.ts +192 -0
  75. package/dist/modal/config.d.ts.map +1 -0
  76. package/dist/modal/config.js +169 -0
  77. package/dist/modal/config.js.map +1 -0
  78. package/dist/modal/internal/sdk.d.ts +141 -0
  79. package/dist/modal/internal/sdk.d.ts.map +1 -0
  80. package/dist/modal/internal/sdk.js +51 -0
  81. package/dist/modal/internal/sdk.js.map +1 -0
  82. package/dist/modal/runtime.d.ts +271 -0
  83. package/dist/modal/runtime.d.ts.map +1 -0
  84. package/dist/modal/runtime.js +871 -0
  85. package/dist/modal/runtime.js.map +1 -0
  86. package/dist/mount-script.d.ts +16 -1
  87. package/dist/mount-script.d.ts.map +1 -1
  88. package/dist/mount-script.js +76 -15
  89. package/dist/mount-script.js.map +1 -1
  90. package/dist/orchestrator.d.ts +15 -0
  91. package/dist/orchestrator.d.ts.map +1 -1
  92. package/dist/orchestrator.js +13 -1
  93. package/dist/orchestrator.js.map +1 -1
  94. package/dist/port.d.ts +144 -6
  95. package/dist/port.d.ts.map +1 -1
  96. package/dist/port.js +27 -5
  97. package/dist/port.js.map +1 -1
  98. package/dist/types.d.ts +55 -1
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/vercel/bench.d.ts +169 -0
  101. package/dist/vercel/bench.d.ts.map +1 -0
  102. package/dist/vercel/bench.js +337 -0
  103. package/dist/vercel/bench.js.map +1 -0
  104. package/dist/vercel/capabilities.d.ts +106 -0
  105. package/dist/vercel/capabilities.d.ts.map +1 -0
  106. package/dist/vercel/capabilities.js +128 -0
  107. package/dist/vercel/capabilities.js.map +1 -0
  108. package/dist/vercel/config.d.ts +119 -0
  109. package/dist/vercel/config.d.ts.map +1 -0
  110. package/dist/vercel/config.js +8 -0
  111. package/dist/vercel/config.js.map +1 -0
  112. package/dist/vercel/internal/sdk.d.ts +173 -0
  113. package/dist/vercel/internal/sdk.d.ts.map +1 -0
  114. package/dist/vercel/internal/sdk.js +117 -0
  115. package/dist/vercel/internal/sdk.js.map +1 -0
  116. package/dist/vercel/runtime.d.ts +305 -0
  117. package/dist/vercel/runtime.d.ts.map +1 -0
  118. package/dist/vercel/runtime.js +1169 -0
  119. package/dist/vercel/runtime.js.map +1 -0
  120. package/docs/freestyle.md +110 -0
  121. package/package.json +35 -4
@@ -0,0 +1,798 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { freestyleObservedCapabilities, freestyleSandboxCapabilities, freestyleWorkflowCapabilities, } from "./capabilities.js";
5
+ import { createOfficialFreestyleClient, } from "./internal/sdk.js";
6
+ const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
7
+ const DEFAULT_CREATE_TIMEOUT_MS = 120_000;
8
+ const DEFAULT_LOOKUP_TIMEOUT_MS = 10_000;
9
+ const DEFAULT_LIFECYCLE_SETTLE_TIMEOUT_MS = 120_000;
10
+ const DEFAULT_POLL_INTERVAL_MS = 500;
11
+ const COMMAND_REQUEST_GRACE_MS = 5_000;
12
+ export class FreestyleCreateTimeoutError extends Error {
13
+ timeoutMs;
14
+ constructor(timeoutMs) {
15
+ super(`Freestyle create did not complete within ${timeoutMs}ms`);
16
+ this.timeoutMs = timeoutMs;
17
+ this.name = "FreestyleCreateTimeoutError";
18
+ }
19
+ }
20
+ export class FreestyleLookupTimeoutError extends Error {
21
+ timeoutMs;
22
+ operation;
23
+ constructor(timeoutMs, operation) {
24
+ super(`Freestyle ${operation} did not complete within ${timeoutMs}ms`);
25
+ this.timeoutMs = timeoutMs;
26
+ this.operation = operation;
27
+ this.name = "FreestyleLookupTimeoutError";
28
+ }
29
+ }
30
+ export class FreestyleLifecycleTimeoutError extends Error {
31
+ sandboxId;
32
+ expected;
33
+ lastState;
34
+ timeoutMs;
35
+ constructor(sandboxId, expected, lastState, timeoutMs) {
36
+ super(`Freestyle VM "${sandboxId}" did not reach ${expected} within ${timeoutMs}ms `
37
+ + `(last state: "${lastState}")`);
38
+ this.sandboxId = sandboxId;
39
+ this.expected = expected;
40
+ this.lastState = lastState;
41
+ this.timeoutMs = timeoutMs;
42
+ this.name = "FreestyleLifecycleTimeoutError";
43
+ }
44
+ }
45
+ export class FreestyleDestroyVerificationError extends Error {
46
+ sandboxId;
47
+ timeoutMs;
48
+ constructor(sandboxId, timeoutMs) {
49
+ super(`Freestyle VM "${sandboxId}" delete was accepted but absence was not verified `
50
+ + `within ${timeoutMs}ms`);
51
+ this.sandboxId = sandboxId;
52
+ this.timeoutMs = timeoutMs;
53
+ this.name = "FreestyleDestroyVerificationError";
54
+ }
55
+ }
56
+ export class FreestyleUnknownExitCodeError extends Error {
57
+ constructor() {
58
+ super("Freestyle exec response omitted statusCode; refusing to treat it as success");
59
+ this.name = "FreestyleUnknownExitCodeError";
60
+ }
61
+ }
62
+ export class FreestyleLaunchEnvironmentUnsupportedError extends Error {
63
+ constructor() {
64
+ super("Freestyle vms.create has no environment field; launch env must be provisioned "
65
+ + "provider-neutrally after create");
66
+ this.name = "FreestyleLaunchEnvironmentUnsupportedError";
67
+ }
68
+ }
69
+ export class FreestyleCapabilityMismatchError extends Error {
70
+ constructor(message) {
71
+ super(`Freestyle capability declaration mismatch: ${message}`);
72
+ this.name = "FreestyleCapabilityMismatchError";
73
+ }
74
+ }
75
+ export class FreestyleRuntime {
76
+ id = "freestyle";
77
+ capabilities = freestyleWorkflowCapabilities;
78
+ declaredCapabilities = freestyleSandboxCapabilities;
79
+ observedCapabilities = freestyleObservedCapabilities;
80
+ apiKey;
81
+ baseUrl;
82
+ defaultHomeDir;
83
+ namePrefix;
84
+ snapshotId;
85
+ persistence;
86
+ idleTimeoutSeconds;
87
+ requestTimeoutMs;
88
+ createTimeoutMs;
89
+ lookupTimeoutMs;
90
+ lifecycleSettleTimeoutMs;
91
+ pollIntervalMs;
92
+ injectedClientFactory;
93
+ registrations = new Map();
94
+ reconciliations = new Set();
95
+ constructor(options, dependencies = {}) {
96
+ this.apiKey = required(options.apiKey, "Freestyle API key");
97
+ this.defaultHomeDir = required(options.defaultHomeDir, "Freestyle default home directory");
98
+ this.namePrefix = validateNamePrefix(options.namePrefix);
99
+ this.baseUrl = optionalTrimmed(options.baseUrl);
100
+ this.snapshotId = optionalTrimmed(options.snapshotId);
101
+ this.persistence = validatePersistence(options.persistence);
102
+ this.idleTimeoutSeconds = validateIdleTimeout(options.idleTimeoutSeconds);
103
+ this.requestTimeoutMs = positiveDuration(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
104
+ this.createTimeoutMs = positiveDuration(options.createTimeoutMs, DEFAULT_CREATE_TIMEOUT_MS);
105
+ this.lookupTimeoutMs = positiveDuration(options.lookupTimeoutMs, DEFAULT_LOOKUP_TIMEOUT_MS);
106
+ this.lifecycleSettleTimeoutMs = positiveDuration(options.lifecycleSettleTimeoutMs, DEFAULT_LIFECYCLE_SETTLE_TIMEOUT_MS);
107
+ this.pollIntervalMs = positiveDuration(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
108
+ this.injectedClientFactory = dependencies.clientFactory;
109
+ assertFreestyleCapabilityImplementation(this);
110
+ }
111
+ async launch(options = {}) {
112
+ if (hasEntries(options.env)) {
113
+ throw new FreestyleLaunchEnvironmentUnsupportedError();
114
+ }
115
+ const timeoutMs = options.createTimeoutSeconds && options.createTimeoutSeconds > 0
116
+ ? Math.ceil(options.createTimeoutSeconds * 1_000)
117
+ : this.createTimeoutMs;
118
+ const callerName = options.name?.trim() || options.label?.trim();
119
+ const name = this.ownedName(callerName);
120
+ // Only a name we generated with a fresh UUID is safe to reconcile by name.
121
+ // Deterministic slug names (derived from caller-supplied input) can collide
122
+ // across concurrent launches — matching on such a name could destroy a
123
+ // sibling's live VM. Leave those documented as unreconciled.
124
+ const reconcilableName = callerName ? null : name;
125
+ const client = await this.client(timeoutMs);
126
+ // Hold the raw create promise so it outlives the client-side deadline
127
+ // race. `withDeadline` only abandons the wait — the SDK call can still
128
+ // complete after we reject, and Freestyle may then hand back a billed VM
129
+ // that this adapter never registered. Reconcile that outcome in the
130
+ // background so the caller sees a clean rejection without leaking a VM.
131
+ const pending = client.vms.create({
132
+ name,
133
+ persistence: this.persistence,
134
+ ...(this.snapshotId ? { snapshotId: this.snapshotId } : {}),
135
+ ...(this.idleTimeoutSeconds !== undefined
136
+ ? { idleTimeoutSeconds: this.idleTimeoutSeconds }
137
+ : {}),
138
+ });
139
+ let created;
140
+ try {
141
+ created = await withDeadline(pending, timeoutMs, () => new FreestyleCreateTimeoutError(timeoutMs));
142
+ }
143
+ catch (error) {
144
+ if (error instanceof FreestyleCreateTimeoutError && reconcilableName) {
145
+ this.trackReconciliation(this.reconcileOrphanedCreate(pending, reconcilableName));
146
+ }
147
+ else {
148
+ // Nothing to reconcile — either not our timeout, or a caller-supplied
149
+ // name we cannot uniquely correlate. Swallow any late rejection so
150
+ // the runtime does not emit an unhandledRejection warning.
151
+ this.trackReconciliation(swallow(pending));
152
+ }
153
+ throw error;
154
+ }
155
+ if (!created || typeof created.vmId !== "string" || !created.vmId.trim()) {
156
+ throw new Error("Freestyle create response is missing vmId");
157
+ }
158
+ const id = created.vmId.trim();
159
+ this.register(id, {
160
+ owned: true,
161
+ labels: options.labels ?? {},
162
+ state: "STARTED",
163
+ });
164
+ return {
165
+ id,
166
+ state: "STARTED",
167
+ homeDir: this.defaultHomeDir,
168
+ ...(options.workdir ? { workdir: options.workdir } : {}),
169
+ };
170
+ }
171
+ async findByLabels(_labels, _options = {}) {
172
+ // Freestyle vms.create has no label field. Returning a name-based guess here
173
+ // would turn ownership naming into a false warm-lease capability.
174
+ return null;
175
+ }
176
+ async findAllByLabels(_labels, _options = {}) {
177
+ return [];
178
+ }
179
+ async countByLabels(_labels, _options = {}) {
180
+ return 0;
181
+ }
182
+ async listOwned(options = {}) {
183
+ const items = await this.listRemote(options.timeoutMs ?? this.lookupTimeoutMs, "owned VM list");
184
+ const states = options.states === undefined ? null : options.states;
185
+ return items
186
+ .filter((item) => typeof item.name === "string" && this.isOwnedName(item.name))
187
+ .filter((item) => options.includeDeleted || !item.deleted)
188
+ .filter((item) => matchesListedState(item, states, options.includeDeleted === true))
189
+ .map((item) => ({
190
+ id: item.id,
191
+ name: item.name,
192
+ state: normalizeState(item.state),
193
+ deleted: Boolean(item.deleted),
194
+ ...(item.createdAt ? { createdAt: item.createdAt } : {}),
195
+ ...(item.sizing
196
+ ? {
197
+ sizing: {
198
+ ...(typeof item.sizing.vcpuCount === "number"
199
+ ? { vcpus: item.sizing.vcpuCount }
200
+ : {}),
201
+ ...(typeof item.sizing.memSizeMib === "number"
202
+ ? { memoryMiB: item.sizing.memSizeMib }
203
+ : {}),
204
+ ...(typeof item.sizing.rootfsSizeMb === "number"
205
+ ? { storageMiB: item.sizing.rootfsSizeMb }
206
+ : {}),
207
+ },
208
+ }
209
+ : {}),
210
+ }));
211
+ }
212
+ async getById(id, options = {}) {
213
+ const item = await this.remoteById(id, this.lookupTimeoutMs);
214
+ if (!item || item.deleted) {
215
+ return null;
216
+ }
217
+ const states = options.states === undefined ? null : options.states;
218
+ if (!matchesState(item, states)) {
219
+ return null;
220
+ }
221
+ this.register(id, {
222
+ owned: options.owned ?? false,
223
+ labels: {},
224
+ state: normalizeState(item.state),
225
+ });
226
+ return {
227
+ id,
228
+ state: normalizeState(item.state),
229
+ homeDir: options.homeDir ?? this.defaultHomeDir,
230
+ ...(options.workdir ? { workdir: options.workdir } : {}),
231
+ ...(item.createdAt ? { createdAt: item.createdAt } : {}),
232
+ ...(item.lastNetworkActivity ? { lastActivityAt: item.lastNetworkActivity } : {}),
233
+ };
234
+ }
235
+ async runScript(handle, options) {
236
+ this.requireRegistered(handle);
237
+ const command = buildFreestyleCommand(options.command, options);
238
+ const commandTimeoutMs = options.timeoutMs && options.timeoutMs > 0
239
+ ? Math.ceil(options.timeoutMs)
240
+ : undefined;
241
+ const requestTimeoutMs = commandTimeoutMs
242
+ ? Math.max(this.requestTimeoutMs, commandTimeoutMs + COMMAND_REQUEST_GRACE_MS)
243
+ : this.requestTimeoutMs;
244
+ const vm = await this.vm(handle.id, requestTimeoutMs);
245
+ const result = await withDeadline(vm.exec({
246
+ command,
247
+ ...(commandTimeoutMs ? { timeoutMs: commandTimeoutMs } : {}),
248
+ }), requestTimeoutMs, () => new FreestyleLookupTimeoutError(requestTimeoutMs, "exec request"));
249
+ const stdout = result.stdout ?? "";
250
+ const stderr = result.stderr ?? "";
251
+ return {
252
+ output: combineOutput(stdout, stderr),
253
+ ...(stdout ? { stdout } : {}),
254
+ ...(stderr ? { stderr } : {}),
255
+ exitCode: typeof result.statusCode === "number" ? result.statusCode : null,
256
+ };
257
+ }
258
+ async exec(handle, command, options = {}) {
259
+ const result = await this.runScript(handle, { command, ...options });
260
+ if (result.exitCode === null) {
261
+ throw new FreestyleUnknownExitCodeError();
262
+ }
263
+ return { output: result.output, exitCode: result.exitCode };
264
+ }
265
+ async uploadFile(handle, source, destination) {
266
+ this.requireRegistered(handle);
267
+ const bytes = typeof source === "string" ? await readFile(source) : source;
268
+ const parent = parentDirectory(destination);
269
+ if (parent) {
270
+ const mkdir = await this.runScript(handle, {
271
+ command: `mkdir -p ${shellSingleQuote(parent)}`,
272
+ timeoutMs: 30_000,
273
+ });
274
+ if (mkdir.exitCode !== 0) {
275
+ throw new Error(`Failed to create Freestyle upload directory "${parent}"`);
276
+ }
277
+ }
278
+ const vm = await this.vm(handle.id, this.requestTimeoutMs);
279
+ await withDeadline(vm.fs.writeFile(destination, bytes), this.requestTimeoutMs, () => new FreestyleLookupTimeoutError(this.requestTimeoutMs, "file upload"));
280
+ }
281
+ async uploadBundle(handle, options) {
282
+ const destinations = [];
283
+ for (const file of options.files) {
284
+ await this.uploadFile(handle, file.source, file.destination);
285
+ destinations.push(file.destination);
286
+ }
287
+ if (options.manifest !== undefined) {
288
+ const path = options.manifestPath ?? "/workspace/manifest.json";
289
+ await this.uploadFile(handle, Buffer.from(JSON.stringify(options.manifest, null, 2), "utf8"), path);
290
+ destinations.push(path);
291
+ }
292
+ if (destinations.length > 0) {
293
+ const verified = await this.runScript(handle, {
294
+ command: destinations
295
+ .map((path) => `test -f ${shellSingleQuote(path)}`)
296
+ .join(" && "),
297
+ timeoutMs: 30_000,
298
+ });
299
+ if (verified.exitCode !== 0) {
300
+ throw new Error("Failed to verify uploaded Freestyle bundle files");
301
+ }
302
+ }
303
+ }
304
+ async downloadFile(handle, source, destination) {
305
+ this.requireRegistered(handle);
306
+ const vm = await this.vm(handle.id, this.requestTimeoutMs);
307
+ const bytes = Buffer.from(await withDeadline(vm.fs.readFile(source), this.requestTimeoutMs, () => new FreestyleLookupTimeoutError(this.requestTimeoutMs, "file download")));
308
+ if (destination) {
309
+ await writeFile(destination, bytes);
310
+ return;
311
+ }
312
+ return bytes;
313
+ }
314
+ async getHomeDir(handle) {
315
+ this.requireRegistered(handle);
316
+ handle.homeDir = handle.homeDir ?? this.defaultHomeDir;
317
+ return handle.homeDir;
318
+ }
319
+ async stop(handle) {
320
+ const entry = this.registrations.get(handle.id);
321
+ if (!entry || !entry.owned) {
322
+ return;
323
+ }
324
+ const current = await this.requireRemote(handle.id);
325
+ if (current.state === "stopped" || current.state === "suspended") {
326
+ entry.state = "STOPPED";
327
+ handle.state = "STOPPED";
328
+ return;
329
+ }
330
+ if (current.state === "stopping") {
331
+ await this.waitForState(handle.id, new Set(["stopped"]), "stopped");
332
+ }
333
+ else if (current.state === "suspending") {
334
+ await this.waitForState(handle.id, new Set(["suspended"]), "suspended");
335
+ }
336
+ else {
337
+ const vm = await this.vm(handle.id, this.requestTimeoutMs);
338
+ await withDeadline(vm.stop(), this.requestTimeoutMs, () => new FreestyleLookupTimeoutError(this.requestTimeoutMs, "stop request"));
339
+ await this.waitForState(handle.id, new Set(["stopped"]), "stopped");
340
+ }
341
+ entry.state = "STOPPED";
342
+ handle.state = "STOPPED";
343
+ }
344
+ async start(handle) {
345
+ const entry = this.registrations.get(handle.id);
346
+ if (!entry || !entry.owned) {
347
+ return handle;
348
+ }
349
+ let current = await this.requireRemote(handle.id);
350
+ if (current.state === "running") {
351
+ handle.state = "STARTED";
352
+ entry.state = "STARTED";
353
+ return handle;
354
+ }
355
+ if (current.state === "starting" || current.state === "building") {
356
+ await this.waitForState(handle.id, new Set(["running"]), "running");
357
+ handle.state = "STARTED";
358
+ entry.state = "STARTED";
359
+ return handle;
360
+ }
361
+ if (current.state === "suspending") {
362
+ await this.waitForState(handle.id, new Set(["suspended"]), "suspended");
363
+ current = await this.requireRemote(handle.id);
364
+ }
365
+ else if (current.state === "stopping") {
366
+ await this.waitForState(handle.id, new Set(["stopped"]), "stopped");
367
+ current = await this.requireRemote(handle.id);
368
+ }
369
+ if (current.state === "lost") {
370
+ throw new Error(`Freestyle VM "${handle.id}" is lost and cannot be started`);
371
+ }
372
+ const vm = await this.vm(handle.id, this.requestTimeoutMs);
373
+ await withDeadline(vm.start(this.idleTimeoutSeconds !== undefined
374
+ ? { idleTimeoutSeconds: this.idleTimeoutSeconds }
375
+ : {}), this.requestTimeoutMs, () => new FreestyleLookupTimeoutError(this.requestTimeoutMs, "start request"));
376
+ await this.waitForState(handle.id, new Set(["running"]), "running");
377
+ entry.state = "STARTED";
378
+ handle.state = "STARTED";
379
+ return handle;
380
+ }
381
+ async destroy(handle) {
382
+ const entry = this.registrations.get(handle.id);
383
+ if (!entry) {
384
+ return;
385
+ }
386
+ if (!entry.owned) {
387
+ this.registrations.delete(handle.id);
388
+ return;
389
+ }
390
+ const client = await this.client(this.requestTimeoutMs);
391
+ // The delete call and the verification share one teardown, so a lost
392
+ // response or a request that crosses `requestTimeoutMs` must not report
393
+ // failure without first checking the authoritative list. If the provider
394
+ // actually performed the delete, `waitUntilDeleted` will confirm it and
395
+ // teardown resolves cleanly; only an unverified failure retains ownership.
396
+ let deleteError;
397
+ try {
398
+ await withDeadline(client.vms.delete({ vmId: handle.id }), this.requestTimeoutMs, () => new FreestyleLookupTimeoutError(this.requestTimeoutMs, "delete request"));
399
+ }
400
+ catch (error) {
401
+ deleteError = error;
402
+ }
403
+ const verified = await this.waitUntilDeleted(handle.id);
404
+ if (!verified) {
405
+ if (deleteError !== undefined) {
406
+ // Delete request failed and the VM is still visible: surface the
407
+ // original transport failure so the caller can decide whether to
408
+ // retry, and retain the registration so cleanup remains reachable.
409
+ throw deleteError;
410
+ }
411
+ // Retain the registration so a caller can retry cleanup.
412
+ throw new FreestyleDestroyVerificationError(handle.id, this.lifecycleSettleTimeoutMs);
413
+ }
414
+ this.registrations.delete(handle.id);
415
+ }
416
+ /**
417
+ * Drain in-flight orphaned-create reconciliations.
418
+ *
419
+ * A `launch()` whose client-side deadline fires may schedule a background
420
+ * delete for a VM the provider still allocates. Short-lived processes and
421
+ * tests must wait for those before exiting, or the reconciliation is what
422
+ * gets leaked. Long-lived hosts can ignore it; either way, this method is
423
+ * safe to call more than once and does not surface reconciliation errors.
424
+ */
425
+ async close() {
426
+ if (this.reconciliations.size === 0) {
427
+ return;
428
+ }
429
+ await Promise.allSettled([...this.reconciliations]);
430
+ }
431
+ /**
432
+ * Terminate a create that outlived its client-side deadline.
433
+ *
434
+ * Called only for names this runtime generated with a fresh UUID, so a
435
+ * match in the provider listing is unambiguously our own late allocation.
436
+ * All failure modes are silent by design: `launch()` has already rejected,
437
+ * so there is no caller to raise to. `close()` awaits pending
438
+ * reconciliations before the process exits.
439
+ */
440
+ async reconcileOrphanedCreate(pending, name) {
441
+ let vmId;
442
+ try {
443
+ const settled = await pending;
444
+ if (settled
445
+ && typeof settled === "object"
446
+ && typeof settled.vmId === "string"
447
+ && settled.vmId.trim()) {
448
+ vmId = settled.vmId.trim();
449
+ }
450
+ }
451
+ catch {
452
+ // Create ultimately rejected — nothing was allocated, nothing to clean.
453
+ return;
454
+ }
455
+ if (!vmId) {
456
+ // No id came back. Fall back to the authoritative listing to find any
457
+ // row the provider allocated under the name we asked for.
458
+ try {
459
+ const owned = await this.listOwned({ states: null });
460
+ const match = owned.find((vm) => vm.name === name);
461
+ if (!match) {
462
+ return;
463
+ }
464
+ vmId = match.id;
465
+ }
466
+ catch {
467
+ return;
468
+ }
469
+ }
470
+ // Register just long enough to satisfy destroy()'s ownership gate and
471
+ // then reuse the same verified-absence teardown the normal path uses.
472
+ this.register(vmId, { owned: true, labels: {}, state: "STARTED" });
473
+ try {
474
+ await this.destroy({ id: vmId, state: "STARTED", homeDir: this.defaultHomeDir });
475
+ }
476
+ catch {
477
+ // Best effort. The next `listOwned` sweep or a follow-up cleanup pass
478
+ // is the safety net; there is no useful action from here.
479
+ }
480
+ }
481
+ trackReconciliation(work) {
482
+ this.reconciliations.add(work);
483
+ void work.finally(() => this.reconciliations.delete(work));
484
+ }
485
+ async client(requestTimeoutMs) {
486
+ if (this.injectedClientFactory) {
487
+ return this.injectedClientFactory(requestTimeoutMs);
488
+ }
489
+ return createOfficialFreestyleClient({ apiKey: this.apiKey, ...(this.baseUrl ? { baseUrl: this.baseUrl } : {}) }, requestTimeoutMs);
490
+ }
491
+ async vm(id, requestTimeoutMs) {
492
+ const client = await this.client(requestTimeoutMs);
493
+ return client.vms.ref({ vmId: id });
494
+ }
495
+ register(id, next) {
496
+ const existing = this.registrations.get(id);
497
+ this.registrations.set(id, {
498
+ owned: existing?.owned === true || next.owned,
499
+ labels: hasEntries(next.labels) ? { ...next.labels } : existing?.labels ?? {},
500
+ state: next.state ?? existing?.state,
501
+ });
502
+ }
503
+ requireRegistered(handle) {
504
+ const entry = this.registrations.get(handle.id);
505
+ if (!entry) {
506
+ throw new Error(`Freestyle runtime handle "${handle.id}" is not attached`);
507
+ }
508
+ return entry;
509
+ }
510
+ async listRemote(timeoutMs, operation) {
511
+ const client = await this.client(timeoutMs);
512
+ const response = await withDeadline(client.vms.list(), timeoutMs, () => new FreestyleLookupTimeoutError(timeoutMs, operation));
513
+ if (!response || !Array.isArray(response.vms)) {
514
+ throw new Error("Freestyle VM list response is malformed");
515
+ }
516
+ const malformedIndex = response.vms.findIndex((item) => !isFreestyleVmListItem(item));
517
+ if (malformedIndex !== -1) {
518
+ throw new Error(`Freestyle VM list item ${malformedIndex} is malformed`);
519
+ }
520
+ return response.vms;
521
+ }
522
+ async remoteById(id, timeoutMs) {
523
+ const items = await this.listRemote(timeoutMs, `lookup for VM "${id}"`);
524
+ return items.find((item) => item.id === id) ?? null;
525
+ }
526
+ async requireRemote(id) {
527
+ const item = await this.remoteById(id, this.lookupTimeoutMs);
528
+ if (!item || item.deleted) {
529
+ throw new Error(`Freestyle VM "${id}" is no longer available`);
530
+ }
531
+ return item;
532
+ }
533
+ async waitForState(id, expected, description) {
534
+ const deadline = Date.now() + this.lifecycleSettleTimeoutMs;
535
+ let lastState = "unknown";
536
+ for (;;) {
537
+ const remainingMs = Math.max(1, deadline - Date.now());
538
+ const item = await this.remoteById(id, Math.min(this.lookupTimeoutMs, remainingMs));
539
+ if (!item || item.deleted) {
540
+ throw new Error(`Freestyle VM "${id}" disappeared while waiting for ${description}`);
541
+ }
542
+ lastState = item.state;
543
+ if (expected.has(item.state)) {
544
+ return item;
545
+ }
546
+ if (item.state === "lost") {
547
+ throw new Error(`Freestyle VM "${id}" became lost while waiting for ${description}`);
548
+ }
549
+ if (Date.now() >= deadline) {
550
+ throw new FreestyleLifecycleTimeoutError(id, description, lastState, this.lifecycleSettleTimeoutMs);
551
+ }
552
+ await delay(this.pollIntervalMs);
553
+ }
554
+ }
555
+ async waitUntilDeleted(id) {
556
+ const deadline = Date.now() + this.lifecycleSettleTimeoutMs;
557
+ for (;;) {
558
+ const remainingMs = Math.max(1, deadline - Date.now());
559
+ const item = await this.remoteById(id, Math.min(this.lookupTimeoutMs, remainingMs));
560
+ // Freestyle may keep a soft-deleted row in vms.list; deleted:true is as
561
+ // authoritative as absence and must not be mistaken for a leaked VM.
562
+ if (!item || item.deleted === true) {
563
+ return true;
564
+ }
565
+ if (Date.now() >= deadline) {
566
+ return false;
567
+ }
568
+ await delay(this.pollIntervalMs);
569
+ }
570
+ }
571
+ ownedName(requested) {
572
+ if (!requested) {
573
+ return `${this.namePrefix}-${randomUUID().replaceAll("-", "").slice(0, 16)}`;
574
+ }
575
+ if (this.isOwnedName(requested)) {
576
+ return requested;
577
+ }
578
+ const slug = requested
579
+ .normalize("NFKC")
580
+ .replace(/[^A-Za-z0-9._-]+/gu, "-")
581
+ .replace(/^-+|-+$/gu, "") || "sandbox";
582
+ const digest = createHash("sha256").update(requested).digest("hex").slice(0, 8);
583
+ return `${this.namePrefix}-${slug}-${digest}`;
584
+ }
585
+ isOwnedName(name) {
586
+ return name === this.namePrefix || name.startsWith(`${this.namePrefix}-`);
587
+ }
588
+ }
589
+ /**
590
+ * Construction-time reconciliation between SDK-free declarations and the
591
+ * implemented surface. The registry is exhaustive over every capability key.
592
+ * Behavioral false claims (warmLease/lifecycle) may retain probe methods; a
593
+ * true claim is never allowed without the necessary implementation.
594
+ */
595
+ function assertFreestyleCapabilityImplementation(runtime) {
596
+ const workflowRegistry = {
597
+ pty: false,
598
+ snapshots: false,
599
+ isolation: "strong",
600
+ persistentHandle: typeof runtime.getById === "function",
601
+ streamingLogs: false,
602
+ };
603
+ for (const key of Object.keys(workflowRegistry)) {
604
+ if (workflowRegistry[key] !== freestyleWorkflowCapabilities[key]) {
605
+ throw new FreestyleCapabilityMismatchError(String(key));
606
+ }
607
+ }
608
+ const outerRegistry = {
609
+ warmLease: false,
610
+ lifecycle: typeof runtime.start === "function" && typeof runtime.stop === "function",
611
+ };
612
+ if (freestyleSandboxCapabilities.warmLease && !outerRegistry.warmLease) {
613
+ throw new FreestyleCapabilityMismatchError("warmLease");
614
+ }
615
+ if (freestyleSandboxCapabilities.lifecycle && !outerRegistry.lifecycle) {
616
+ throw new FreestyleCapabilityMismatchError("lifecycle");
617
+ }
618
+ const observedRegistry = {
619
+ cleanupVerified: typeof runtime.destroy === "function" && typeof runtime.listOwned === "function",
620
+ fork: false,
621
+ lifecycle: outerRegistry.lifecycle,
622
+ neverIdle: false,
623
+ ptySurvival: false,
624
+ snapshotCapture: false,
625
+ streamingExec: false,
626
+ warmLease: outerRegistry.warmLease,
627
+ };
628
+ for (const key of Object.keys(observedRegistry)) {
629
+ if (freestyleObservedCapabilities[key] && !observedRegistry[key]) {
630
+ throw new FreestyleCapabilityMismatchError(String(key));
631
+ }
632
+ }
633
+ }
634
+ export function buildFreestyleCommand(command, options = {}) {
635
+ const statements = [];
636
+ for (const [name, value] of Object.entries(options.env ?? {})) {
637
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
638
+ throw new Error(`Invalid Freestyle environment variable name "${name}"`);
639
+ }
640
+ statements.push(`export ${name}=${shellSingleQuote(value)}`);
641
+ }
642
+ if (options.cwd) {
643
+ statements.push(`cd ${shellSingleQuote(options.cwd)} || exit $?`);
644
+ }
645
+ statements.push(command);
646
+ return statements.join("\n");
647
+ }
648
+ function validatePersistence(value) {
649
+ if (!value || typeof value !== "object") {
650
+ throw new Error("Freestyle persistence policy is required");
651
+ }
652
+ if (value.type === "persistent" || value.type === "ephemeral") {
653
+ return { type: value.type };
654
+ }
655
+ if (value.type === "sticky"
656
+ && Number.isFinite(value.priority)
657
+ && Number.isInteger(value.priority)) {
658
+ return { type: "sticky", priority: value.priority };
659
+ }
660
+ throw new Error("Freestyle persistence policy is invalid");
661
+ }
662
+ function validateIdleTimeout(value) {
663
+ if (value === undefined || value === null) {
664
+ return value;
665
+ }
666
+ if (!Number.isFinite(value) || value < 0) {
667
+ throw new Error("Freestyle idleTimeoutSeconds must be null or a non-negative number");
668
+ }
669
+ return Math.ceil(value);
670
+ }
671
+ function validateNamePrefix(value) {
672
+ const prefix = required(value, "Freestyle name prefix");
673
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(prefix)) {
674
+ throw new Error("Freestyle name prefix must contain only letters, numbers, dot, underscore, or hyphen");
675
+ }
676
+ return prefix;
677
+ }
678
+ function required(value, description) {
679
+ const normalized = value?.trim();
680
+ if (!normalized) {
681
+ throw new Error(`${description} is required`);
682
+ }
683
+ return normalized;
684
+ }
685
+ function optionalTrimmed(value) {
686
+ const normalized = value?.trim();
687
+ return normalized || undefined;
688
+ }
689
+ function positiveDuration(value, fallback) {
690
+ return value !== undefined && Number.isFinite(value) && value > 0
691
+ ? Math.ceil(value)
692
+ : fallback;
693
+ }
694
+ function hasEntries(value) {
695
+ return !!value && Object.keys(value).length > 0;
696
+ }
697
+ function isFreestyleVmListItem(value) {
698
+ return Boolean(value
699
+ && typeof value === "object"
700
+ && typeof value.id === "string"
701
+ && value.id.trim()
702
+ && typeof value.state === "string"
703
+ && value.state.trim());
704
+ }
705
+ function normalizeState(state) {
706
+ switch (state.toLowerCase()) {
707
+ case "running":
708
+ return "STARTED";
709
+ case "stopped":
710
+ case "suspended":
711
+ return "STOPPED";
712
+ case "building":
713
+ case "starting":
714
+ return "STARTING";
715
+ case "stopping":
716
+ case "suspending":
717
+ return "STOPPING";
718
+ case "lost":
719
+ return "FAILED";
720
+ default:
721
+ return state.toUpperCase();
722
+ }
723
+ }
724
+ function normalizeRequestedState(state) {
725
+ const normalized = state.toUpperCase();
726
+ if (normalized === "RUNNING")
727
+ return "STARTED";
728
+ if (normalized === "PAUSED" || normalized === "SUSPENDED")
729
+ return "STOPPED";
730
+ return normalized;
731
+ }
732
+ function matchesState(item, states) {
733
+ if (item.deleted)
734
+ return false;
735
+ if (states === null)
736
+ return true;
737
+ const actual = normalizeState(item.state);
738
+ return states.some((state) => normalizeRequestedState(state) === actual);
739
+ }
740
+ function matchesListedState(item, states, includeDeleted) {
741
+ if (item.deleted && !includeDeleted)
742
+ return false;
743
+ if (states === null)
744
+ return true;
745
+ const actual = normalizeState(item.state);
746
+ return states.some((state) => normalizeRequestedState(state) === actual);
747
+ }
748
+ function combineOutput(stdout, stderr) {
749
+ if (stdout && stderr) {
750
+ return stdout.endsWith("\n") || stderr.startsWith("\n")
751
+ ? `${stdout}${stderr}`
752
+ : `${stdout}\n${stderr}`;
753
+ }
754
+ return stdout || stderr || "";
755
+ }
756
+ function parentDirectory(destination) {
757
+ const normalized = destination.trim().replace(/\/+$/gu, "");
758
+ const index = normalized.lastIndexOf("/");
759
+ return index > 0 ? normalized.slice(0, index) : null;
760
+ }
761
+ function shellSingleQuote(value) {
762
+ return `'${value.replace(/'/gu, `'\\''`)}'`;
763
+ }
764
+ async function withDeadline(operation, timeoutMs, timeoutError) {
765
+ let timer;
766
+ try {
767
+ return await Promise.race([
768
+ operation,
769
+ new Promise((_resolve, reject) => {
770
+ timer = setTimeout(() => reject(timeoutError()), timeoutMs);
771
+ }),
772
+ ]);
773
+ }
774
+ finally {
775
+ if (timer !== undefined)
776
+ clearTimeout(timer);
777
+ }
778
+ }
779
+ function delay(timeoutMs) {
780
+ return new Promise((resolve) => setTimeout(resolve, timeoutMs));
781
+ }
782
+ /**
783
+ * Consume a promise's eventual settlement without acting on it.
784
+ *
785
+ * Used when `launch()` has already rejected and there is no productive
786
+ * reconciliation to run (e.g. a caller-supplied name we cannot uniquely
787
+ * correlate). Without this, a late create rejection would surface as an
788
+ * unhandledRejection warning even though nothing wants the result.
789
+ */
790
+ async function swallow(operation) {
791
+ try {
792
+ await operation;
793
+ }
794
+ catch {
795
+ // Ignored on purpose.
796
+ }
797
+ }
798
+ //# sourceMappingURL=runtime.js.map