@mastra/deployer-sandbox 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150

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 (63) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/LICENSE.md +30 -0
  3. package/README.md +104 -0
  4. package/dist/alias.d.ts +10 -0
  5. package/dist/alias.d.ts.map +1 -0
  6. package/dist/client/index.cjs +174 -0
  7. package/dist/client/index.cjs.map +1 -0
  8. package/dist/client/index.d.ts +98 -0
  9. package/dist/client/index.d.ts.map +1 -0
  10. package/dist/client/index.js +171 -0
  11. package/dist/client/index.js.map +1 -0
  12. package/dist/deployer.d.ts +57 -0
  13. package/dist/deployer.d.ts.map +1 -0
  14. package/dist/engine.d.ts +34 -0
  15. package/dist/engine.d.ts.map +1 -0
  16. package/dist/index.cjs +832 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +824 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/manifest.d.ts +7 -0
  23. package/dist/manifest.d.ts.map +1 -0
  24. package/dist/shared-C-piQAcU.js +115 -0
  25. package/dist/shared-C-piQAcU.js.map +1 -0
  26. package/dist/shared-H9GioaPM.cjs +192 -0
  27. package/dist/shared-H9GioaPM.cjs.map +1 -0
  28. package/dist/shared.d.ts +63 -0
  29. package/dist/shared.d.ts.map +1 -0
  30. package/dist/studio/assets/CommitMono-400-Regular-DzkyLZ26.woff2 +0 -0
  31. package/dist/studio/assets/CommitMono-700-Regular-DmOSN4kd.woff2 +0 -0
  32. package/dist/studio/assets/MonaSans-VariableFont_wdth-wght-CX-7s9jm.ttf +0 -0
  33. package/dist/studio/assets/babel-lWBcy2sH.js +16 -0
  34. package/dist/studio/assets/bash-atvbtKCR.js +1 -0
  35. package/dist/studio/assets/core-B-AlWOWa.js +12 -0
  36. package/dist/studio/assets/engine-compile-BkERmzkH.js +137 -0
  37. package/dist/studio/assets/engine-javascript-CSIo_1eZ.js +1 -0
  38. package/dist/studio/assets/estree-Dd9JfUIE.js +44 -0
  39. package/dist/studio/assets/github-dark-DHJKELXO.js +1 -0
  40. package/dist/studio/assets/github-light-DAi9KRSo.js +1 -0
  41. package/dist/studio/assets/index-yUiD89B4.js +1 -0
  42. package/dist/studio/assets/index-znlQ7_ex.js +2 -0
  43. package/dist/studio/assets/javascript-ySlJ1b_l.js +1 -0
  44. package/dist/studio/assets/json-BQoSv7ci.js +1 -0
  45. package/dist/studio/assets/jsx-BAng5TT0.js +1 -0
  46. package/dist/studio/assets/livekit-client.esm-CKIgC2IJ.js +39 -0
  47. package/dist/studio/assets/main-D5Yq7Fjg.js +769 -0
  48. package/dist/studio/assets/markdown-UIAJJxZW.js +1 -0
  49. package/dist/studio/assets/preload-helper-PPVm8Dsz.js +1 -0
  50. package/dist/studio/assets/python-DhUJRlN_.js +1 -0
  51. package/dist/studio/assets/shell-CjFT_Tl9.js +1 -0
  52. package/dist/studio/assets/standalone-Dfz2oS5J.js +29 -0
  53. package/dist/studio/assets/style-CAUk37Fq.css +1 -0
  54. package/dist/studio/assets/tsx-B6W0miNI.js +1 -0
  55. package/dist/studio/assets/typescript-Dj6nwHGl.js +1 -0
  56. package/dist/studio/index.html +100 -0
  57. package/dist/studio/mastra.svg +17 -0
  58. package/dist/studio/routes-manifest.json +27 -0
  59. package/dist/types.d.ts +215 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/worker.d.ts +3 -0
  62. package/dist/worker.d.ts.map +1 -0
  63. package/package.json +79 -0
package/dist/index.js ADDED
@@ -0,0 +1,824 @@
1
+ import { a as SERVER_SCRIPT, c as launchServer, d as shellQuote, f as tailServerLog, i as SERVER_PIDFILE, l as resolveRemoteDir, n as INSTALL_MARKER$1, o as getInfoSafe, p as waitForHealthy, r as SERVER_LOGFILE, s as killPreviousServer, t as DEFAULT_PORT, u as runInSandbox } from "./shared-C-piQAcU.js";
2
+ import { access, mkdtemp, readFile, rm, writeFile } from "fs/promises";
3
+ import { dirname, join, posix } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { Deployer } from "@mastra/deployer";
6
+ import { copy } from "fs-extra/esm";
7
+ import { execFile } from "child_process";
8
+ import { createHash } from "crypto";
9
+ import { existsSync } from "fs";
10
+ import { tmpdir } from "os";
11
+ import { promisify } from "util";
12
+ import { supportsNetworking } from "@mastra/core/workspace";
13
+ //#region src/alias.ts
14
+ /**
15
+ * Upsert a Vercel Edge Config item so a stable key always points at the
16
+ * current sandbox URL. Used for Tier 3 routing: apps read the key from Edge
17
+ * Config (e.g. in middleware) instead of hardcoding the rotating sandbox URL.
18
+ */
19
+ async function updateEdgeConfigAlias(options) {
20
+ const { token, teamId } = options;
21
+ if (!token) throw new Error("Updating the Edge Config alias requires a Vercel API token. Pass `alias.token`.");
22
+ const endpoint = new URL(`https://api.vercel.com/v1/edge-config/${options.edgeConfigId}/items`);
23
+ if (teamId) endpoint.searchParams.set("teamId", teamId);
24
+ const res = await fetch(endpoint, {
25
+ method: "PATCH",
26
+ headers: {
27
+ Authorization: `Bearer ${token}`,
28
+ "Content-Type": "application/json"
29
+ },
30
+ body: JSON.stringify({ items: [{
31
+ operation: "upsert",
32
+ key: options.key,
33
+ value: options.url
34
+ }] }),
35
+ signal: AbortSignal.timeout(3e4)
36
+ });
37
+ if (!res.ok) {
38
+ const body = await res.text().catch(() => "");
39
+ throw new Error(`Failed to update Edge Config alias "${options.key}" (${res.status}): ${body}`);
40
+ }
41
+ }
42
+ //#endregion
43
+ //#region src/engine.ts
44
+ const execFileAsync = promisify(execFile);
45
+ const noopLogger = {
46
+ debug: () => {},
47
+ info: () => {},
48
+ warn: () => {},
49
+ error: () => {}
50
+ };
51
+ /** Max shell-command payload per chunk for the base64 upload fallback. */
52
+ const UPLOAD_CHUNK_SIZE = 96e3;
53
+ /**
54
+ * Deploy a prebuilt Mastra server directory into any workspace sandbox that
55
+ * supports networking. Provider-agnostic: only uses the core WorkspaceSandbox
56
+ * contract (`executeCommand` + `networking`, with `writeFiles` / `processes`
57
+ * as fast paths).
58
+ */
59
+ async function deployToSandbox(options) {
60
+ const { sandbox, dir, port = DEFAULT_PORT, env = {}, studio = false, healthCheckPath = "/api", healthCheckTimeoutMs = 6e4, healthCheckIntervalMs = 1e3, installCommand = "npm install --omit=dev", logger = noopLogger } = options;
61
+ if (!existsSync(join(dir, "index.mjs"))) throw new Error(`No index.mjs found in "${dir}" — did the build succeed?`);
62
+ logger.info(`Starting ${sandbox.provider} sandbox...`);
63
+ await sandbox.start?.();
64
+ if (!supportsNetworking(sandbox)) throw new Error(`Sandbox provider "${sandbox.provider}" does not support networking (public port URLs), which is required for sandbox deploys.`);
65
+ if (!sandbox.executeCommand) throw new Error(`Sandbox provider "${sandbox.provider}" does not support executeCommand, which is required for sandbox deploys.`);
66
+ const url = await sandbox.networking.getPortUrl(port);
67
+ if (!url) throw new Error(`Sandbox provider "${sandbox.provider}" did not expose a public URL for port ${port}. Make sure the port is declared when constructing the sandbox (e.g. \`ports: [${port}]\`).`);
68
+ const remoteDir = await resolveRemoteDir(sandbox, options.remoteDir);
69
+ const mergedEnv = { ...env };
70
+ if (studio && mergedEnv.MASTRA_STUDIO_PATH === void 0) mergedEnv.MASTRA_STUDIO_PATH = `${remoteDir}/studio`;
71
+ logger.info(`Uploading build output from ${dir}...`);
72
+ const tarball = await createTarball(dir);
73
+ logger.debug(`Tarball size: ${(tarball.length / 1024 / 1024).toFixed(2)} MB`);
74
+ const remoteTarball = `${remoteDir}/.deploy.tgz`;
75
+ await runInSandbox(sandbox, `mkdir -p ${shellQuote(remoteDir)}`);
76
+ await uploadFile(sandbox, remoteTarball, tarball);
77
+ await killPreviousServer(sandbox, remoteDir);
78
+ await runInSandbox(sandbox, `cd ${shellQuote(remoteDir)} && tar -xzf .deploy.tgz && rm -f .deploy.tgz`, { timeout: 12e4 });
79
+ const installHash = await hashInstallInputs(dir, installCommand);
80
+ const marker = `${remoteDir}/${INSTALL_MARKER$1}`;
81
+ const markerCheck = await runInSandbox(sandbox, `cat ${shellQuote(marker)} 2>/dev/null || true`, { allowFailure: true });
82
+ if (installHash && markerCheck.stdout.trim() === installHash) logger.info("Dependencies unchanged — skipping install.");
83
+ else {
84
+ logger.info(`Installing dependencies (${installCommand})...`);
85
+ await runInSandbox(sandbox, `cd ${shellQuote(remoteDir)} && ${installCommand}`, {
86
+ timeout: 6e5,
87
+ label: `install dependencies (${installCommand})`
88
+ });
89
+ if (installHash) await runInSandbox(sandbox, `printf '%s' ${shellQuote(installHash)} > ${shellQuote(marker)}`);
90
+ }
91
+ const launchScript = buildLaunchScript({
92
+ remoteDir,
93
+ port,
94
+ env: mergedEnv
95
+ });
96
+ await uploadFile(sandbox, `${remoteDir}/${SERVER_SCRIPT}`, Buffer.from(launchScript));
97
+ await runInSandbox(sandbox, `chmod 700 ${shellQuote(`${remoteDir}/${SERVER_SCRIPT}`)}`);
98
+ logger.info("Starting Mastra server...");
99
+ await launchServer(sandbox, remoteDir);
100
+ if (!await waitForHealthy(url, {
101
+ path: healthCheckPath,
102
+ timeoutMs: healthCheckTimeoutMs,
103
+ intervalMs: healthCheckIntervalMs
104
+ })) {
105
+ const log = await tailServerLog(sandbox, remoteDir).catch(() => "");
106
+ throw new Error(`Mastra server did not become healthy at ${url}${healthCheckPath} within ${healthCheckTimeoutMs}ms.` + (log ? `\n\nServer log:\n${log}` : "\n\n(no server log output captured)"));
107
+ }
108
+ const info = await getInfoSafe(sandbox);
109
+ return {
110
+ url,
111
+ sandboxId: info?.id ?? sandbox.id,
112
+ expiresAt: info?.timeoutAt,
113
+ stop: async () => {
114
+ await sandbox.stop?.();
115
+ },
116
+ destroy: async () => {
117
+ await sandbox.destroy?.();
118
+ },
119
+ logs: (lines) => tailServerLog(sandbox, remoteDir, lines)
120
+ };
121
+ }
122
+ /**
123
+ * Build the POSIX launch script. Re-running the script restarts the server —
124
+ * the wake path uses this after a snapshot resume (which restores the
125
+ * filesystem but not processes).
126
+ */
127
+ function buildLaunchScript(opts) {
128
+ const lines = ["#!/bin/sh", `cd ${shellQuote(opts.remoteDir)}`];
129
+ const env = {
130
+ MASTRA_AUTO_DETECT_URL: "true",
131
+ ...opts.env,
132
+ PORT: String(opts.port),
133
+ MASTRA_HOST: "0.0.0.0"
134
+ };
135
+ for (const [key, value] of Object.entries(env)) {
136
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: "${key}"`);
137
+ lines.push(`export ${key}=${shellQuote(value)}`);
138
+ }
139
+ lines.push(`echo $$ > ${shellQuote(SERVER_PIDFILE)}`);
140
+ lines.push(`exec node index.mjs >> ${shellQuote(SERVER_LOGFILE)} 2>&1`);
141
+ return lines.join("\n") + "\n";
142
+ }
143
+ /** Create a gzipped tarball of the directory contents (excluding node_modules). */
144
+ async function createTarball(dir) {
145
+ const tmp = await mkdtemp(join(tmpdir(), "mastra-sandbox-"));
146
+ const tarPath = join(tmp, "deploy.tgz");
147
+ try {
148
+ await execFileAsync("tar", [
149
+ "-czf",
150
+ tarPath,
151
+ "--exclude=node_modules",
152
+ "-C",
153
+ dir,
154
+ "."
155
+ ]);
156
+ return await readFile(tarPath);
157
+ } finally {
158
+ await rm(tmp, {
159
+ recursive: true,
160
+ force: true
161
+ });
162
+ }
163
+ }
164
+ /**
165
+ * Upload a file into the sandbox. Uses the provider's native `writeFiles` fast
166
+ * path when available, otherwise falls back to base64 chunks over
167
+ * `executeCommand` — so `executeCommand` + `networking` is the minimum contract.
168
+ */
169
+ async function uploadFile(sandbox, remotePath, content) {
170
+ if (sandbox.writeFiles) {
171
+ await sandbox.writeFiles([{
172
+ path: remotePath,
173
+ content
174
+ }]);
175
+ return;
176
+ }
177
+ const b64 = content.toString("base64");
178
+ const tmpPath = `${remotePath}.b64`;
179
+ await runInSandbox(sandbox, `rm -f ${shellQuote(tmpPath)}`);
180
+ for (let i = 0; i < b64.length; i += UPLOAD_CHUNK_SIZE) await runInSandbox(sandbox, `printf '%s' ${shellQuote(b64.slice(i, i + UPLOAD_CHUNK_SIZE))} >> ${shellQuote(tmpPath)}`, { label: `upload chunk to ${remotePath}` });
181
+ await runInSandbox(sandbox, `base64 -d ${shellQuote(tmpPath)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(tmpPath)}`, { label: `decode upload at ${remotePath}` });
182
+ }
183
+ /** Lockfiles that, when present in the build output, participate in the install-skip hash. */
184
+ const LOCKFILES = [
185
+ "package-lock.json",
186
+ "npm-shrinkwrap.json",
187
+ "pnpm-lock.yaml",
188
+ "yarn.lock",
189
+ "bun.lock"
190
+ ];
191
+ /**
192
+ * Hash everything that determines the outcome of a dependency install:
193
+ * package.json, any bundled lockfile, and the install command itself. A
194
+ * matching hash means the previous `node_modules` can be reused.
195
+ */
196
+ async function hashInstallInputs(dir, installCommand) {
197
+ const hash = createHash("sha256");
198
+ try {
199
+ hash.update(await readFile(join(dir, "package.json")));
200
+ } catch {
201
+ return null;
202
+ }
203
+ for (const lockfile of LOCKFILES) {
204
+ let content;
205
+ try {
206
+ content = await readFile(join(dir, lockfile));
207
+ } catch {
208
+ continue;
209
+ }
210
+ hash.update(lockfile).update(content);
211
+ }
212
+ hash.update(installCommand);
213
+ return hash.digest("hex");
214
+ }
215
+ //#endregion
216
+ //#region src/manifest.ts
217
+ const MANIFEST_FILENAME = "sandbox-deployment.json";
218
+ /** Write `sandbox-deployment.json` into the build output directory. */
219
+ async function writeDeploymentManifest(outputDir, manifest) {
220
+ await writeFile(join(outputDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2));
221
+ }
222
+ /** Read `sandbox-deployment.json` from the build output directory, or null when absent. */
223
+ async function readDeploymentManifest(outputDir) {
224
+ let raw;
225
+ try {
226
+ raw = await readFile(join(outputDir, MANIFEST_FILENAME), "utf-8");
227
+ } catch (error) {
228
+ if (error.code === "ENOENT") return null;
229
+ throw error;
230
+ }
231
+ return JSON.parse(raw);
232
+ }
233
+ //#endregion
234
+ //#region src/deployer.ts
235
+ /**
236
+ * Deploy a full Mastra server into any workspace sandbox that supports
237
+ * networking (Vercel Sandbox, E2B, ...) and get a live public URL.
238
+ *
239
+ * Positioning: ephemeral environments — instant previews, PR/CI smoke deploys,
240
+ * agent-built-app verification. Not production hosting.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * import { SandboxDeployer } from '@mastra/deployer-sandbox';
245
+ * import { VercelSandbox } from '@mastra/vercel';
246
+ *
247
+ * export const mastra = new Mastra({
248
+ * deployer: new SandboxDeployer({
249
+ * sandbox: new VercelSandbox({ sandboxName: 'my-preview', timeout: 3_600_000, ports: [4111] }),
250
+ * }),
251
+ * });
252
+ * ```
253
+ */
254
+ var SandboxDeployer = class extends Deployer {
255
+ /** Sandbox deploys are push-style: `mastra build` runs `deploy()` after bundling. */
256
+ deployOnBuild = true;
257
+ sandbox;
258
+ port;
259
+ studio;
260
+ /** Explicit remote dir, when configured. The engine defaults to `$HOME/mastra-app` inside the sandbox. */
261
+ remoteDir;
262
+ env;
263
+ alias;
264
+ healthCheckTimeoutMs;
265
+ constructor(options) {
266
+ super({ name: "SANDBOX" });
267
+ this.sandbox = options.sandbox;
268
+ this.port = options.port ?? 4111;
269
+ this.studio = options.studio ?? true;
270
+ this.remoteDir = options.remoteDir;
271
+ this.env = options.env ?? {};
272
+ this.alias = options.alias;
273
+ this.healthCheckTimeoutMs = options.healthCheckTimeoutMs;
274
+ }
275
+ /**
276
+ * Merge all existing env files instead of only the first one (base behavior).
277
+ * Later files win in `loadEnvVars()`, so order least → most specific: a
278
+ * `.env.local` written by `vercel env pull` shouldn't shadow the `.env` that
279
+ * holds the app's own keys.
280
+ */
281
+ async getEnvFiles() {
282
+ const candidates = [
283
+ ".env",
284
+ ".env.production",
285
+ ".env.local"
286
+ ];
287
+ const existing = [];
288
+ for (const file of candidates) try {
289
+ await access(file);
290
+ existing.push(file);
291
+ } catch {}
292
+ return existing;
293
+ }
294
+ async getUserBundlerOptions(mastraEntryFile, outputDirectory) {
295
+ return {
296
+ ...await super.getUserBundlerOptions(mastraEntryFile, outputDirectory),
297
+ externals: true
298
+ };
299
+ }
300
+ getEntry() {
301
+ return `
302
+ // @ts-expect-error
303
+ import { scoreTracesWorkflow } from '@mastra/core/evals/scoreTraces';
304
+ import { mastra } from '#mastra';
305
+ import { createNodeServer, getToolExports } from '#server';
306
+ import { tools } from '#tools';
307
+
308
+ // @ts-expect-error
309
+ await createNodeServer(mastra, { tools: getToolExports(tools), studio: ${this.studio} });
310
+
311
+ const storage = mastra.getStorage();
312
+ if (storage) {
313
+ if (!storage.disableInit) {
314
+ storage.init();
315
+ }
316
+ mastra.__registerInternalWorkflow(scoreTracesWorkflow);
317
+ }
318
+ `;
319
+ }
320
+ async prepare(outputDirectory) {
321
+ await super.prepare(outputDirectory);
322
+ if (this.studio) {
323
+ const studioSource = join(dirname(dirname(fileURLToPath(import.meta.url))), "dist", "studio");
324
+ const studioServePath = join(outputDirectory, this.outputDir, "studio");
325
+ try {
326
+ await copy(studioSource, studioServePath, { overwrite: true });
327
+ } catch (err) {
328
+ throw new Error(`Failed to copy studio assets from "${studioSource}" to "${studioServePath}": ${err instanceof Error ? err.message : err}`);
329
+ }
330
+ }
331
+ }
332
+ async bundle(entryFile, outputDirectory, { toolsPaths, projectRoot }) {
333
+ return this._bundle(this.getEntry(), entryFile, {
334
+ outputDirectory,
335
+ projectRoot
336
+ }, toolsPaths);
337
+ }
338
+ /**
339
+ * Deploy the built output into the sandbox and wait for the server to come
340
+ * up on its public URL. Writes `sandbox-deployment.json` into the output
341
+ * directory and updates the Edge Config alias when configured.
342
+ */
343
+ async deploy(outputDirectory) {
344
+ const dir = join(outputDirectory, this.outputDir);
345
+ const envVars = await this.loadEnvVars();
346
+ const env = {
347
+ ...Object.fromEntries(envVars),
348
+ ...this.env
349
+ };
350
+ if (envVars.size > 0) this.logger.warn("Environment variables from your .env file are injected into the remote sandbox. Anyone with access to the sandbox can read them.");
351
+ const deployment = await deployToSandbox({
352
+ sandbox: this.sandbox,
353
+ dir,
354
+ port: this.port,
355
+ env,
356
+ studio: this.studio,
357
+ remoteDir: this.remoteDir,
358
+ healthCheckTimeoutMs: this.healthCheckTimeoutMs,
359
+ logger: this.logger
360
+ });
361
+ await writeDeploymentManifest(dir, {
362
+ provider: this.sandbox.provider,
363
+ sandboxId: deployment.sandboxId,
364
+ url: deployment.url,
365
+ port: this.port,
366
+ deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
367
+ expiresAt: deployment.expiresAt?.toISOString()
368
+ });
369
+ if (this.alias) {
370
+ await updateEdgeConfigAlias({
371
+ ...this.alias,
372
+ url: deployment.url
373
+ });
374
+ this.logger.info(`Edge Config alias "${this.alias.key}" now points at ${deployment.url}`);
375
+ }
376
+ this.logger.info(`Mastra server deployed: ${deployment.url}/api`);
377
+ if (this.studio) this.logger.info(`Studio: ${deployment.url}`);
378
+ if (deployment.expiresAt) this.logger.warn(`Sandbox expires at ${deployment.expiresAt.toISOString()} (provider runtime cap).`);
379
+ }
380
+ };
381
+ //#endregion
382
+ //#region src/worker.ts
383
+ const ARCHIVE = ".mastra-worker.tar.gz";
384
+ const RUNTIME_DIR = ".mastra/executions";
385
+ const INSTALL_MARKER = ".mastra-install-hash";
386
+ const INSTALL_LOCK = ".mastra-install-lock";
387
+ const ARTIFACT_LOCK = ".mastra-artifact-lock";
388
+ const EXECUTION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
389
+ const DEFAULT_INPUT_LIMIT = 16 * 1024 * 1024;
390
+ const DEFAULT_OUTPUT_READ_LIMIT = 1024 * 1024;
391
+ async function deployWorkerToSandbox(options) {
392
+ validateOptions(options);
393
+ const { sandbox, dir, executionId, command, mode = "worker", args = [], env = {}, workingDirectory = ".", installCommand = "npm install --omit=dev", startupTimeoutMs = 1e4, executionTimeoutMs, terminationGraceMs = 5e3, inputLimitBytes = DEFAULT_INPUT_LIMIT } = options;
394
+ const remoteDir = await resolveRemoteDir(sandbox, options.remoteDir);
395
+ const config = {
396
+ sandbox,
397
+ remoteDir,
398
+ command,
399
+ args,
400
+ env,
401
+ workingDirectory,
402
+ mode,
403
+ startupTimeoutMs,
404
+ executionTimeoutMs,
405
+ terminationGraceMs,
406
+ inputLimitBytes
407
+ };
408
+ const archive = `${remoteDir}/${ARCHIVE}`;
409
+ const tarball = await createTarball(dir);
410
+ const installHash = await hashInstallInputs(dir, installCommand);
411
+ const artifactLock = `${remoteDir}/${ARTIFACT_LOCK}`;
412
+ let artifactLockAcquired = false;
413
+ try {
414
+ await runInSandbox(sandbox, `mkdir -p ${shellQuote(remoteDir)}`);
415
+ await acquireLock(sandbox, artifactLock, options.installTimeoutMs, "worker artifact");
416
+ artifactLockAcquired = true;
417
+ await uploadFile(sandbox, archive, tarball);
418
+ await runInSandbox(sandbox, `tar -xzf ${shellQuote(archive)} -C ${shellQuote(remoteDir)} && rm -f ${shellQuote(archive)}`, { label: "extract worker artifact" });
419
+ } catch (error) {
420
+ throw workerPhaseError("upload", error);
421
+ } finally {
422
+ if (artifactLockAcquired) await runInSandbox(sandbox, `rm -rf ${shellQuote(artifactLock)}`, {
423
+ allowFailure: true,
424
+ label: "release worker artifact lock"
425
+ });
426
+ }
427
+ try {
428
+ await installDependencies(sandbox, remoteDir, installHash ?? void 0, installCommand, options.installTimeoutMs);
429
+ } catch (error) {
430
+ throw workerPhaseError("install", error);
431
+ }
432
+ return createExecution(config, executionId, options.input);
433
+ }
434
+ function validateOptions(options) {
435
+ if (!options.sandbox.executeCommand) throw new Error(`Sandbox provider "${options.sandbox.provider}" does not support executeCommand, which is required for worker deploys.`);
436
+ validateExecutionId(options.executionId);
437
+ if (!options.command || /[\0\r\n]/.test(options.command)) throw new Error("Worker command must be a non-empty executable path.");
438
+ if (options.args?.some((arg) => arg.includes("\0"))) throw new Error("Worker arguments must not contain NUL bytes.");
439
+ for (const key of Object.keys(options.env ?? {})) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid worker environment variable name: ${key}`);
440
+ validateRelativePath(options.workingDirectory ?? ".", "workingDirectory");
441
+ validateInput(options.input);
442
+ for (const [name, value] of [
443
+ ["inputLimitBytes", options.inputLimitBytes],
444
+ ["startupTimeoutMs", options.startupTimeoutMs],
445
+ ["executionTimeoutMs", options.executionTimeoutMs],
446
+ ["terminationGraceMs", options.terminationGraceMs]
447
+ ]) if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) throw new Error(`${name} must be greater than zero.`);
448
+ }
449
+ function validateRelativePath(value, label) {
450
+ if (!value || posix.isAbsolute(value) || posix.normalize(value).startsWith("..")) throw new Error(`Worker ${label} must stay within the deployed artifact root.`);
451
+ }
452
+ function validateInput(input) {
453
+ if (input?.type === "file") validateRelativePath(input.path, "input file path");
454
+ }
455
+ async function acquireLock(sandbox, lock, timeout, label) {
456
+ const timeoutMs = timeout ?? 6e5;
457
+ const attempts = Math.max(1, Math.ceil(timeoutMs / 1e3));
458
+ await runInSandbox(sandbox, [
459
+ "i=0",
460
+ `while ! mkdir ${shellQuote(lock)} 2>/dev/null; do`,
461
+ ` if [ "$i" -ge ${attempts} ]; then echo ${shellQuote(`${label} lock timeout`)} >&2; exit 1; fi`,
462
+ " sleep 1; i=$((i + 1))",
463
+ "done"
464
+ ].join("\n"), {
465
+ timeout: timeoutMs,
466
+ label: `acquire ${label} lock`
467
+ });
468
+ }
469
+ async function installDependencies(sandbox, remoteDir, installHash, installCommand, timeout) {
470
+ if (!installHash) return;
471
+ const marker = `${remoteDir}/${INSTALL_MARKER}`;
472
+ const lock = `${remoteDir}/${INSTALL_LOCK}`;
473
+ await acquireLock(sandbox, lock, timeout, "dependency install");
474
+ try {
475
+ await runInSandbox(sandbox, [
476
+ `current="$(cat ${shellQuote(marker)} 2>/dev/null || true)"`,
477
+ `if [ "$current" != ${shellQuote(installHash)} ]; then`,
478
+ ` cd ${shellQuote(remoteDir)} && ${installCommand}`,
479
+ ` printf %s ${shellQuote(installHash)} > ${shellQuote(`${marker}.tmp`)}`,
480
+ ` mv ${shellQuote(`${marker}.tmp`)} ${shellQuote(marker)}`,
481
+ "fi"
482
+ ].join("\n"), {
483
+ timeout: timeout ?? 6e5,
484
+ label: "install worker dependencies"
485
+ });
486
+ } finally {
487
+ await runInSandbox(sandbox, `rm -rf ${shellQuote(lock)}`, {
488
+ allowFailure: true,
489
+ label: "release dependency install lock"
490
+ });
491
+ }
492
+ }
493
+ async function createExecution(config, executionId, input) {
494
+ validateExecutionId(executionId);
495
+ const paths = executionPaths(config.remoteDir, executionId);
496
+ await runInSandbox(config.sandbox, `mkdir -p ${shellQuote(`${config.remoteDir}/${RUNTIME_DIR}`)} && mkdir -m 700 ${shellQuote(paths.dir)}`, { label: "create worker execution namespace" });
497
+ const script = buildExecutionScript(config, paths, await stageInput(config, paths, input));
498
+ await uploadFile(config.sandbox, paths.script, Buffer.from(script));
499
+ await runInSandbox(config.sandbox, `chmod 700 ${shellQuote(paths.script)}`);
500
+ try {
501
+ await launchExecution(config.sandbox, paths);
502
+ } catch (error) {
503
+ await writeFailedStatus(config.sandbox, paths, executionId, "launch", error);
504
+ throw workerPhaseError("launch", error);
505
+ }
506
+ const startup = await waitForStartup(config, executionId, paths);
507
+ if (startup.state === "timed_out") await cancelExecution(config, executionId, paths, "startup");
508
+ if (startup.state === "failed" || startup.state === "timed_out" || startup.state === "provider_unavailable") throw new Error(`Worker ${startup.state} during startup${"message" in startup && startup.message ? `: ${startup.message}` : ""}.`);
509
+ const info = await getInfoSafe(config.sandbox);
510
+ return deployment(config, executionId, paths, info?.id ?? config.sandbox.id ?? "unknown", info?.timeoutAt);
511
+ }
512
+ function deployment(config, executionId, paths, sandboxId, expiresAt) {
513
+ return {
514
+ sandboxId,
515
+ executionId,
516
+ expiresAt,
517
+ status: (options) => readWorkerStatus(config.sandbox, executionId, paths, options),
518
+ readOutput: (stream, options) => readOutput(config.sandbox, executionId, paths, stream, options),
519
+ cancel: () => cancelExecution(config, executionId, paths),
520
+ stop: async () => {
521
+ if (!config.sandbox.stop) throw new Error(`Sandbox provider "${config.sandbox.provider}" does not support stop.`);
522
+ await config.sandbox.stop();
523
+ },
524
+ destroy: (options) => destroyWithRetry(config.sandbox, options),
525
+ relaunch: async (options) => {
526
+ if (options.executionId === executionId) throw new Error("Relaunch requires a new executionId.");
527
+ validateInput(options.input);
528
+ return createExecution(config, options.executionId, options.input);
529
+ }
530
+ };
531
+ }
532
+ async function stageInput(config, paths, input) {
533
+ if (!input) return void 0;
534
+ const data = typeof input.data === "string" ? Buffer.from(input.data) : Buffer.from(input.data);
535
+ if (data.byteLength > config.inputLimitBytes) throw new Error(`Worker input exceeds inputLimitBytes (${data.byteLength} > ${config.inputLimitBytes}).`);
536
+ const path = input.type === "stdin" ? paths.stdin : posix.resolve(config.remoteDir, input.path);
537
+ await uploadFile(config.sandbox, path, data);
538
+ await runInSandbox(config.sandbox, `chmod 600 ${shellQuote(path)}`);
539
+ return input.type === "stdin" ? path : void 0;
540
+ }
541
+ function buildExecutionScript(config, paths, stdinPath) {
542
+ const cwd = posix.resolve(config.remoteDir, config.workingDirectory);
543
+ const envPrefix = Object.entries(config.env).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
544
+ const executable = [shellQuote(config.command), ...config.args.map(shellQuote)].join(" ");
545
+ const target = `${envPrefix ? `env ${envPrefix} ` : ""}${executable}`;
546
+ const graceAttempts = Math.max(1, Math.ceil(config.terminationGraceMs / 1e3));
547
+ const state = (value) => `tmp=${shellQuote(`${paths.status}.tmp.$$`)}; printf '%s\\n' ${shellQuote(value)} > "$tmp"; mv "$tmp" ${shellQuote(paths.status)};`;
548
+ return [
549
+ "#!/bin/sh",
550
+ `cd ${shellQuote(cwd)}`,
551
+ `execution_id=${shellQuote(paths.executionId)}`,
552
+ `stdout=${shellQuote(paths.stdout)}`,
553
+ `stderr=${shellQuote(paths.stderr)}`,
554
+ `pidfile=${shellQuote(paths.pid)}`,
555
+ `tokenfile=${shellQuote(paths.pidToken)}`,
556
+ `: > "$stdout"; : > "$stderr"`,
557
+ state(`starting|${paths.executionId}`),
558
+ `setsid sh -c ${shellQuote(`exec ${target}${stdinPath ? ` < ${shellQuote(stdinPath)}` : ""}`)} > "$stdout" 2> "$stderr" &`,
559
+ "child=$!",
560
+ "printf %s \"$child\" > \"$pidfile\"",
561
+ `if [ -r "/proc/$child/stat" ]; then awk '{print $22}' "/proc/$child/stat" > "$tokenfile"; else : > "$tokenfile"; fi`,
562
+ state(`running|${paths.executionId}`),
563
+ "cancelled=0",
564
+ `trap 'cancelled=1; kill -TERM -"$child" 2>/dev/null || kill -TERM "$child" 2>/dev/null || true' TERM INT`,
565
+ ...config.executionTimeoutMs ? [`(sleep ${Math.max(1, Math.ceil(config.executionTimeoutMs / 1e3))}; if kill -0 "$child" 2>/dev/null; then ${state(`timed_out|${paths.executionId}|execution`)} kill -TERM -"$child" 2>/dev/null || kill -TERM "$child" 2>/dev/null || true; i=0; while kill -0 "$child" 2>/dev/null && [ "$i" -lt ${graceAttempts} ]; do sleep 1; i=$((i + 1)); done; kill -KILL -"$child" 2>/dev/null || kill -KILL "$child" 2>/dev/null || true; fi) &`, "watchdog=$!"] : [],
566
+ "wait \"$child\"",
567
+ "code=$?",
568
+ ...config.executionTimeoutMs ? ["kill \"$watchdog\" 2>/dev/null || true"] : [],
569
+ `current="$(cat ${shellQuote(paths.status)} 2>/dev/null || true)"`,
570
+ `case "$current" in timed_out*) ;; *) if [ "$cancelled" -eq 1 ]; then ${state(`cancelled|${paths.executionId}|TERM`)} else signal=''; if [ "$code" -gt 128 ]; then signal="SIG$((code - 128))"; fi; tmp=${shellQuote(`${paths.status}.tmp.$$`)}; printf 'exited|%s|%s|%s\\n' "$execution_id" "$code" "$signal" > "$tmp"; mv "$tmp" ${shellQuote(paths.status)}; fi ;; esac`,
571
+ "rm -f \"$pidfile\" \"$tokenfile\"",
572
+ "exit \"$code\""
573
+ ].join("\n");
574
+ }
575
+ async function launchExecution(sandbox, paths) {
576
+ await runInSandbox(sandbox, `setsid nohup sh ${shellQuote(paths.script)} >/dev/null 2>&1 & echo $!`, { label: "launch worker execution" });
577
+ }
578
+ async function waitForStartup(config, executionId, paths) {
579
+ const deadline = Date.now() + config.startupTimeoutMs;
580
+ while (Date.now() < deadline) {
581
+ const status = await readWorkerStatus(config.sandbox, executionId, paths);
582
+ if (status.state !== "unknown" && status.state !== "starting") return status;
583
+ await new Promise((resolve) => setTimeout(resolve, 100));
584
+ }
585
+ return {
586
+ state: "timed_out",
587
+ executionId,
588
+ phase: "startup"
589
+ };
590
+ }
591
+ async function readWorkerStatus(sandbox, executionId, paths, options) {
592
+ const providerState = sandbox.status;
593
+ if (providerState === "destroyed" || providerState === "destroying") return {
594
+ state: "provider_unavailable",
595
+ executionId,
596
+ providerState
597
+ };
598
+ if (providerState === "stopped" || providerState === "stopping") {
599
+ if (!options?.wake || !sandbox.start) return {
600
+ state: "provider_unavailable",
601
+ executionId,
602
+ providerState
603
+ };
604
+ try {
605
+ await sandbox.start();
606
+ } catch (error) {
607
+ return {
608
+ state: "provider_unavailable",
609
+ executionId,
610
+ providerState,
611
+ message: errorMessage(error)
612
+ };
613
+ }
614
+ }
615
+ try {
616
+ const result = await runInSandbox(sandbox, [
617
+ `status="$(cat ${shellQuote(paths.status)} 2>/dev/null || true)"`,
618
+ `if [ -f ${shellQuote(paths.pid)} ]; then`,
619
+ ` pid="$(cat ${shellQuote(paths.pid)})"`,
620
+ ` expected="$(cat ${shellQuote(paths.pidToken)} 2>/dev/null || true)"`,
621
+ ` actual="$(if [ -r "/proc/$pid/stat" ]; then awk '{print $22}' "/proc/$pid/stat"; fi)"`,
622
+ ` if kill -0 "$pid" 2>/dev/null && { [ -z "$expected" ] || [ "$expected" = "$actual" ]; }; then echo "running|${executionId}"; exit 0; fi`,
623
+ ` if kill -0 "$pid" 2>/dev/null; then echo "stale|${executionId}"; exit 0; fi`,
624
+ "fi",
625
+ `if [ -n "$status" ]; then printf '%s\\n' "$status"; else echo "unknown|${executionId}"; fi`
626
+ ].join("\n"), {
627
+ allowFailure: true,
628
+ label: "read worker status"
629
+ });
630
+ if (result.exitCode !== 0) return {
631
+ state: "provider_unavailable",
632
+ executionId,
633
+ providerState: sandbox.status,
634
+ message: result.stderr || result.stdout || "Sandbox status inspection failed."
635
+ };
636
+ return parseStatus(executionId, result.stdout.trim());
637
+ } catch (error) {
638
+ return {
639
+ state: "provider_unavailable",
640
+ executionId,
641
+ providerState: sandbox.status,
642
+ message: errorMessage(error)
643
+ };
644
+ }
645
+ }
646
+ function parseStatus(executionId, value) {
647
+ const [state, recordedId, first, second] = value.split("|");
648
+ if (recordedId !== executionId || state === "stale") return {
649
+ state: "unknown",
650
+ executionId
651
+ };
652
+ if (state === "starting") return {
653
+ state,
654
+ executionId
655
+ };
656
+ if (state === "running") return {
657
+ state,
658
+ executionId
659
+ };
660
+ if (state === "exited") {
661
+ const exitCode = Number(first);
662
+ return Number.isInteger(exitCode) ? {
663
+ state,
664
+ executionId,
665
+ exitCode,
666
+ ...second ? { signal: second } : {}
667
+ } : {
668
+ state: "unknown",
669
+ executionId
670
+ };
671
+ }
672
+ if (state === "cancelled") return {
673
+ state,
674
+ executionId,
675
+ ...first ? { signal: first } : {}
676
+ };
677
+ if (state === "timed_out" && (first === "startup" || first === "execution")) return {
678
+ state,
679
+ executionId,
680
+ phase: first
681
+ };
682
+ if (state === "failed" && (first === "upload" || first === "install" || first === "launch")) return {
683
+ state,
684
+ executionId,
685
+ phase: first,
686
+ message: second ?? ""
687
+ };
688
+ return {
689
+ state: "unknown",
690
+ executionId
691
+ };
692
+ }
693
+ async function cancelExecution(config, executionId, paths, timeoutPhase) {
694
+ const current = await readWorkerStatus(config.sandbox, executionId, paths);
695
+ if (current.state !== "running" && current.state !== "starting") return current;
696
+ const attempts = Math.max(1, Math.ceil(config.terminationGraceMs / 1e3));
697
+ const terminal = timeoutPhase ? `timed_out|${executionId}|startup` : `cancelled|${executionId}|TERM`;
698
+ await runInSandbox(config.sandbox, [
699
+ `pid="$(cat ${shellQuote(paths.pid)} 2>/dev/null || true)"`,
700
+ "[ -n \"$pid\" ] || exit 0",
701
+ `expected="$(cat ${shellQuote(paths.pidToken)} 2>/dev/null || true)"`,
702
+ `actual="$(if [ -r "/proc/$pid/stat" ]; then awk '{print $22}' "/proc/$pid/stat"; fi)"`,
703
+ "[ -n \"$expected\" ] && [ \"$expected\" != \"$actual\" ] && exit 0",
704
+ "kill -TERM -\"$pid\" 2>/dev/null || kill -TERM \"$pid\" 2>/dev/null || true",
705
+ `i=0; while kill -0 "$pid" 2>/dev/null && [ "$i" -lt ${attempts} ]; do sleep 1; i=$((i + 1)); done`,
706
+ "kill -KILL -\"$pid\" 2>/dev/null || kill -KILL \"$pid\" 2>/dev/null || true",
707
+ `tmp=${shellQuote(`${paths.status}.tmp.$$`)}; printf '%s\\n' ${shellQuote(terminal)} > "$tmp"; mv "$tmp" ${shellQuote(paths.status)}`,
708
+ `rm -f ${shellQuote(paths.pid)} ${shellQuote(paths.pidToken)}`
709
+ ].join("\n"), {
710
+ allowFailure: true,
711
+ timeout: config.terminationGraceMs + 5e3,
712
+ label: "cancel worker execution"
713
+ });
714
+ return parseStatus(executionId, terminal);
715
+ }
716
+ async function readOutput(sandbox, executionId, paths, stream, options) {
717
+ const offset = Math.max(0, Math.floor(options?.offset ?? 0));
718
+ const maxBytes = Math.max(1, Math.floor(options?.maxBytes ?? DEFAULT_OUTPUT_READ_LIMIT));
719
+ const path = stream === "stdout" ? paths.stdout : paths.stderr;
720
+ try {
721
+ const result = await runInSandbox(sandbox, `size=$(wc -c < ${shellQuote(path)} 2>/dev/null || echo 0); printf '%s\\n' "$size"; tail -c +${offset + 1} ${shellQuote(path)} 2>/dev/null | head -c ${maxBytes} | base64`, {
722
+ allowFailure: true,
723
+ label: `read worker ${stream}`
724
+ });
725
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Unable to read worker ${stream}.`);
726
+ const newline = result.stdout.indexOf("\n");
727
+ const totalBytes = Number((newline === -1 ? result.stdout : result.stdout.slice(0, newline)).trim()) || 0;
728
+ const encoded = newline === -1 ? "" : result.stdout.slice(newline + 1).replace(/\s/g, "");
729
+ const data = Buffer.from(encoded, "base64");
730
+ const nextOffset = offset + data.byteLength;
731
+ const status = await readWorkerStatus(sandbox, executionId, paths);
732
+ const terminal = [
733
+ "exited",
734
+ "cancelled",
735
+ "timed_out",
736
+ "failed"
737
+ ].includes(status.state);
738
+ const interrupted = status.state === "provider_unavailable" || status.state === "unknown";
739
+ return {
740
+ stream,
741
+ data,
742
+ offset,
743
+ nextOffset,
744
+ totalBytes,
745
+ eof: terminal && nextOffset >= totalBytes,
746
+ truncated: nextOffset < totalBytes,
747
+ interrupted
748
+ };
749
+ } catch {
750
+ return {
751
+ stream,
752
+ data: /* @__PURE__ */ new Uint8Array(),
753
+ offset,
754
+ nextOffset: offset,
755
+ totalBytes: offset,
756
+ eof: false,
757
+ truncated: false,
758
+ interrupted: true
759
+ };
760
+ }
761
+ }
762
+ async function destroyWithRetry(sandbox, options) {
763
+ if (!sandbox.destroy) return {
764
+ state: "unsupported",
765
+ attempts: 0
766
+ };
767
+ const attempts = Math.max(1, Math.floor(options?.attempts ?? 3));
768
+ const delayMs = Math.max(0, Math.floor(options?.delayMs ?? 250));
769
+ let lastError;
770
+ for (let attempt = 1; attempt <= attempts; attempt++) try {
771
+ await sandbox.destroy();
772
+ return {
773
+ state: "destroyed",
774
+ attempts: attempt
775
+ };
776
+ } catch (error) {
777
+ lastError = error;
778
+ if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, delayMs));
779
+ }
780
+ return {
781
+ state: "exhausted",
782
+ attempts,
783
+ error: lastError
784
+ };
785
+ }
786
+ async function writeFailedStatus(sandbox, paths, executionId, phase, error) {
787
+ await writeStatus(sandbox, paths.status, `failed|${executionId}|${phase}|${sanitizeStatusValue(errorMessage(error))}`);
788
+ }
789
+ async function writeStatus(sandbox, path, value) {
790
+ await runInSandbox(sandbox, `tmp=${shellQuote(`${path}.tmp.$$`)}; printf '%s\\n' ${shellQuote(value)} > "$tmp"; mv "$tmp" ${shellQuote(path)}`, {
791
+ allowFailure: true,
792
+ label: "write worker status"
793
+ });
794
+ }
795
+ function executionPaths(remoteDir, executionId) {
796
+ const dir = `${remoteDir}/${RUNTIME_DIR}/${executionId}`;
797
+ return {
798
+ executionId,
799
+ dir,
800
+ script: `${dir}/launch.sh`,
801
+ pid: `${dir}/pid`,
802
+ pidToken: `${dir}/pid-start`,
803
+ status: `${dir}/status`,
804
+ stdin: `${dir}/stdin`,
805
+ stdout: `${dir}/stdout`,
806
+ stderr: `${dir}/stderr`
807
+ };
808
+ }
809
+ function validateExecutionId(executionId) {
810
+ if (!executionId || !EXECUTION_ID_PATTERN.test(executionId)) throw new Error("Worker executionId must contain only letters, numbers, dots, underscores, and hyphens.");
811
+ }
812
+ function workerPhaseError(phase, error) {
813
+ return new Error(`Worker ${phase} failed: ${errorMessage(error)}`, { cause: error });
814
+ }
815
+ function errorMessage(error) {
816
+ return error instanceof Error ? error.message : String(error);
817
+ }
818
+ function sanitizeStatusValue(value) {
819
+ return value.replace(/[|\r\n]/g, " ").slice(0, 500);
820
+ }
821
+ //#endregion
822
+ export { MANIFEST_FILENAME, SandboxDeployer, buildLaunchScript, deployToSandbox, deployWorkerToSandbox, readDeploymentManifest, updateEdgeConfigAlias, writeDeploymentManifest };
823
+
824
+ //# sourceMappingURL=index.js.map