@ekairos/sandbox 1.22.15-beta.feature-thread-unify.0 → 1.22.16-beta.development.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/service.js CHANGED
@@ -1,7 +1,33 @@
1
- import { Sandbox as VercelSandbox } from "@vercel/sandbox";
1
+ import { Sandbox as VercelSandbox, Snapshot as VercelSnapshot } from "@vercel/sandbox";
2
2
  import { Daytona, Image } from "@daytonaio/sdk";
3
3
  import { id } from "@instantdb/admin";
4
+ import { resolveRuntime } from "@ekairos/domain/runtime";
4
5
  import { runCommandInSandbox } from "./commands.js";
6
+ import { resolveVercelSandboxConfig, safeVercelConfigForRecord, } from "./vercel-options.js";
7
+ import { execFile } from "node:child_process";
8
+ import { randomUUID } from "node:crypto";
9
+ import { existsSync, promises as fs } from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { promisify } from "node:util";
13
+ const execFileAsync = promisify(execFile);
14
+ function isVercelSandbox(sandbox) {
15
+ return Boolean(sandbox &&
16
+ typeof sandbox === "object" &&
17
+ typeof sandbox.runCommand === "function" &&
18
+ typeof sandbox.currentSession === "function" &&
19
+ typeof sandbox.name === "string" &&
20
+ sandbox.__provider !== "sprites");
21
+ }
22
+ const EKAIROS_ROOT_DIR = "/vercel/sandbox/.ekairos";
23
+ const EKAIROS_RUNTIME_MANIFEST_PATH = `${EKAIROS_ROOT_DIR}/runtime.json`;
24
+ const EKAIROS_HTTP_HELPER_PATH = `${EKAIROS_ROOT_DIR}/instant-http.mjs`;
25
+ const EKAIROS_QUERY_SCRIPT_PATH = `${EKAIROS_ROOT_DIR}/query.mjs`;
26
+ const CODEX_HOME_DIR = "/vercel/sandbox/.codex";
27
+ const CODEX_SKILLS_DIR = `${CODEX_HOME_DIR}/skills`;
28
+ const INSTANT_API_BASE_URL = "https://api.instantdb.com";
29
+ const SANDBOX_PROCESS_STREAM_VERSION = 1;
30
+ const SANDBOX_PROCESS_TERMINAL_STATUSES = new Set(["exited", "failed", "killed", "lost"]);
5
31
  function formatInstantSchemaError(err) {
6
32
  const base = err instanceof Error ? err.message : String(err);
7
33
  const body = err?.body;
@@ -25,6 +51,174 @@ function formatInstantSchemaError(err) {
25
51
  // Keep it short + copy/paste friendly for debugging schema issues.
26
52
  return base + " | missing attributes: " + uniq.join(", ");
27
53
  }
54
+ function formatSandboxError(err) {
55
+ const base = err instanceof Error ? err.message : String(err);
56
+ const text = typeof err?.text === "string" ? err.text.trim() : "";
57
+ const json = err?.json ? JSON.stringify(err.json) : "";
58
+ const detail = text || json;
59
+ if (!detail)
60
+ return base;
61
+ return `${base}: ${detail}`;
62
+ }
63
+ function nowIso() {
64
+ return new Date().toISOString();
65
+ }
66
+ function asOptionalString(value) {
67
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
68
+ }
69
+ function sanitizeInstantString(value) {
70
+ return value.includes("\0") ? value.replace(/\0/g, "") : value;
71
+ }
72
+ function sanitizeInstantValue(value) {
73
+ if (typeof value === "string") {
74
+ return sanitizeInstantString(value);
75
+ }
76
+ if (Array.isArray(value)) {
77
+ return value.map((item) => sanitizeInstantValue(item));
78
+ }
79
+ if (value && typeof value === "object" && !(value instanceof Date)) {
80
+ const sanitized = {};
81
+ for (const [key, entry] of Object.entries(value)) {
82
+ sanitized[key] = sanitizeInstantValue(entry);
83
+ }
84
+ return sanitized;
85
+ }
86
+ return value;
87
+ }
88
+ function createSandboxProcessStreamClientId(processId) {
89
+ const normalized = String(processId ?? "").trim();
90
+ if (!normalized)
91
+ throw new Error("sandbox_process_id_required");
92
+ return `sandbox-process:${normalized}`;
93
+ }
94
+ function encodeSandboxProcessStreamChunk(chunk) {
95
+ return `${JSON.stringify(chunk)}\n`;
96
+ }
97
+ function parseSandboxProcessStreamChunk(value) {
98
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
99
+ if (!parsed || typeof parsed !== "object") {
100
+ throw new Error("invalid_sandbox_process_stream_chunk");
101
+ }
102
+ const record = parsed;
103
+ if (record.version !== SANDBOX_PROCESS_STREAM_VERSION) {
104
+ throw new Error(`invalid_sandbox_process_stream_version:${String(record.version)}`);
105
+ }
106
+ return record;
107
+ }
108
+ function sandboxProcessFinishedHookToken(processId) {
109
+ return `sandbox-process:${processId}:finished`;
110
+ }
111
+ async function resumeSandboxProcessHook(processId, payload) {
112
+ try {
113
+ const { resumeHook } = await import("workflow/api");
114
+ await resumeHook(sandboxProcessFinishedHookToken(processId), payload);
115
+ }
116
+ catch {
117
+ // No workflow may be listening; process metadata and streams remain the source of truth.
118
+ }
119
+ }
120
+ function commandResultFromProcessStream(params) {
121
+ const stdout = params.chunks
122
+ .filter((chunk) => chunk.type === "stdout")
123
+ .map((chunk) => String(chunk.data?.text ?? ""))
124
+ .join("");
125
+ const stderr = params.chunks
126
+ .filter((chunk) => chunk.type === "stderr" || chunk.type === "error")
127
+ .map((chunk) => String(chunk.data?.text ?? chunk.data?.message ?? ""))
128
+ .join("");
129
+ const exitChunk = [...params.chunks].reverse().find((chunk) => chunk.type === "exit");
130
+ const exitCode = Number(exitChunk?.data?.exitCode ?? params.processRow?.exitCode ?? 1);
131
+ const command = [
132
+ String(params.processRow?.command ?? ""),
133
+ ...(Array.isArray(params.processRow?.args) ? params.processRow.args : []),
134
+ ]
135
+ .filter(Boolean)
136
+ .join(" ");
137
+ return {
138
+ success: exitCode === 0,
139
+ exitCode,
140
+ output: stdout,
141
+ error: stderr,
142
+ command,
143
+ };
144
+ }
145
+ export class SandboxCommandRun {
146
+ constructor(data, service) {
147
+ this.service = null;
148
+ this.data = data;
149
+ this.service = service ?? null;
150
+ }
151
+ get sandboxId() {
152
+ return this.data.sandboxId;
153
+ }
154
+ get processId() {
155
+ return this.data.processId;
156
+ }
157
+ get streamId() {
158
+ return this.data.streamId;
159
+ }
160
+ get streamClientId() {
161
+ return this.data.streamClientId;
162
+ }
163
+ getService() {
164
+ if (!this.service) {
165
+ throw new Error("sandbox_command_run_service_required");
166
+ }
167
+ return this.service;
168
+ }
169
+ async readStream() {
170
+ const stream = await this.getService().readProcessStream(this.processId);
171
+ if (!stream.ok)
172
+ throw new Error(stream.error);
173
+ return stream.data;
174
+ }
175
+ async snapshot() {
176
+ const snapshot = await this.getService().getProcessSnapshot(this.processId);
177
+ if (!snapshot.ok)
178
+ throw new Error(snapshot.error);
179
+ return snapshot.data;
180
+ }
181
+ async wait(params) {
182
+ if (this.data.result)
183
+ return this.data.result;
184
+ const initial = await this.snapshot();
185
+ if (SANDBOX_PROCESS_TERMINAL_STATUSES.has(String(initial.status ?? ""))) {
186
+ const stream = await this.readStream();
187
+ const result = commandResultFromProcessStream({ processRow: initial, chunks: stream.chunks });
188
+ this.data.result = result;
189
+ return result;
190
+ }
191
+ try {
192
+ const { createHook } = await import("workflow");
193
+ const hook = createHook({
194
+ token: sandboxProcessFinishedHookToken(this.processId),
195
+ });
196
+ const result = await hook;
197
+ this.data.result = result;
198
+ return result;
199
+ }
200
+ catch {
201
+ // Outside workflow context, or if hooks are unavailable, poll the durable row.
202
+ }
203
+ const timeoutMs = Math.max(0, Number(params?.timeoutMs ?? 5 * 60 * 1000));
204
+ const pollMs = Math.max(50, Number(params?.pollMs ?? 500));
205
+ const deadline = Date.now() + timeoutMs;
206
+ while (Date.now() <= deadline) {
207
+ const row = await this.snapshot();
208
+ if (SANDBOX_PROCESS_TERMINAL_STATUSES.has(String(row.status ?? ""))) {
209
+ const stream = await this.readStream();
210
+ const result = commandResultFromProcessStream({ processRow: row, chunks: stream.chunks });
211
+ this.data.result = result;
212
+ return result;
213
+ }
214
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
215
+ }
216
+ throw new Error(`sandbox_process_wait_timeout:${this.processId}`);
217
+ }
218
+ then(onfulfilled, onrejected) {
219
+ return this.wait().then(onfulfilled, onrejected);
220
+ }
221
+ }
28
222
  export class SandboxService {
29
223
  constructor(db) {
30
224
  this.adminDb = db;
@@ -38,18 +232,423 @@ export class SandboxService {
38
232
  }
39
233
  return { teamId, projectId, token };
40
234
  }
41
- static async provisionVercelSandbox(config) {
42
- const creds = SandboxService.getVercelCredentials();
235
+ static getDomainName(domain) {
236
+ const metaName = typeof domain?.meta?.name === "string" ? domain.meta.name.trim() : "";
237
+ const contextName = typeof domain?.context === "function" ? String(domain.context()?.name ?? "").trim() : "";
238
+ return metaName || contextName || "domain";
239
+ }
240
+ static getDomainContextString(domain) {
241
+ if (typeof domain?.contextString !== "function")
242
+ return "";
243
+ try {
244
+ return String(domain.contextString() ?? "").trim();
245
+ }
246
+ catch {
247
+ return "";
248
+ }
249
+ }
250
+ static cloneJson(value) {
251
+ return JSON.parse(JSON.stringify(value));
252
+ }
253
+ static buildEkairosNetworkPolicy(params) {
254
+ const allow = {
255
+ "api.instantdb.com": [
256
+ {
257
+ transform: [
258
+ {
259
+ headers: {
260
+ "as-token": params.scopedToken,
261
+ },
262
+ },
263
+ ],
264
+ },
265
+ ],
266
+ };
267
+ if (params.datasetEnabled) {
268
+ allow["pypi.org"] = [];
269
+ allow["files.pythonhosted.org"] = [];
270
+ allow["*.pythonhosted.org"] = [];
271
+ }
272
+ return {
273
+ allow,
274
+ };
275
+ }
276
+ static buildEkairosManifest(params) {
277
+ const contextString = SandboxService.getDomainContextString(params.domain);
278
+ const schemaJson = SandboxService.cloneJson(params.domain.toInstantSchema());
279
+ return {
280
+ version: 1,
281
+ instant: {
282
+ apiBaseUrl: INSTANT_API_BASE_URL,
283
+ appId: params.appId,
284
+ },
285
+ sandbox: {
286
+ sandboxUserId: params.sandboxUserId,
287
+ },
288
+ domain: {
289
+ name: SandboxService.getDomainName(params.domain),
290
+ ...(contextString ? { contextString } : {}),
291
+ schemaJson,
292
+ },
293
+ ...(params.datasetEnabled ? { dataset: { enabled: true } } : {}),
294
+ };
295
+ }
296
+ static buildEkairosRuntimeFiles(manifest) {
297
+ const httpHelper = [
298
+ "import { readFile } from 'node:fs/promises'",
299
+ "import { randomUUID } from 'node:crypto'",
300
+ "",
301
+ "export async function readRuntimeManifest(manifestPath) {",
302
+ ` const resolvedPath = manifestPath || ${JSON.stringify(EKAIROS_RUNTIME_MANIFEST_PATH)}`,
303
+ " return JSON.parse(await readFile(resolvedPath, 'utf8'))",
304
+ "}",
305
+ "",
306
+ "export async function instantQuery(query, manifestPath) {",
307
+ " const manifest = await readRuntimeManifest(manifestPath)",
308
+ " const response = await fetch(`${manifest.instant.apiBaseUrl}/admin/query`, {",
309
+ " method: 'POST',",
310
+ " headers: {",
311
+ " 'content-type': 'application/json',",
312
+ " 'app-id': manifest.instant.appId,",
313
+ " },",
314
+ " body: JSON.stringify({ query }),",
315
+ " })",
316
+ " const text = await response.text()",
317
+ " if (!response.ok) {",
318
+ " throw new Error(JSON.stringify({ status: response.status, body: text }))",
319
+ " }",
320
+ " return text ? JSON.parse(text) : {}",
321
+ "}",
322
+ "",
323
+ "export async function instantTransact(steps, manifestPath) {",
324
+ " const manifest = await readRuntimeManifest(manifestPath)",
325
+ " const response = await fetch(`${manifest.instant.apiBaseUrl}/admin/transact`, {",
326
+ " method: 'POST',",
327
+ " headers: {",
328
+ " 'content-type': 'application/json',",
329
+ " 'app-id': manifest.instant.appId,",
330
+ " },",
331
+ " body: JSON.stringify({ steps, 'throw-on-missing-attrs?': true }),",
332
+ " })",
333
+ " const text = await response.text()",
334
+ " if (!response.ok) {",
335
+ " throw new Error(JSON.stringify({ status: response.status, body: text }))",
336
+ " }",
337
+ " return text ? JSON.parse(text) : {}",
338
+ "}",
339
+ "",
340
+ "export function newId() {",
341
+ " return randomUUID()",
342
+ "}",
343
+ "",
344
+ "export function decodeArg(encodedJson) {",
345
+ " return JSON.parse(Buffer.from(encodedJson, 'base64url').toString('utf8'))",
346
+ "}",
347
+ "",
348
+ ].join("\n");
349
+ const queryScript = [
350
+ `import { decodeArg, instantQuery } from ${JSON.stringify(EKAIROS_HTTP_HELPER_PATH)}`,
351
+ "",
352
+ "const encodedQuery = process.argv[2] ?? ''",
353
+ `const manifestPath = process.argv[3] ?? ${JSON.stringify(EKAIROS_RUNTIME_MANIFEST_PATH)}`,
354
+ "",
355
+ "if (!encodedQuery) {",
356
+ " console.error('ekairos_query_required')",
357
+ " process.exit(1)",
358
+ "}",
359
+ "",
360
+ "const query = decodeArg(encodedQuery)",
361
+ "try {",
362
+ " const result = await instantQuery(query, manifestPath)",
363
+ " process.stdout.write(JSON.stringify(result))",
364
+ "} catch (error) {",
365
+ " console.error(error instanceof Error ? error.message : String(error))",
366
+ " process.exit(1)",
367
+ "}",
368
+ "",
369
+ ].join("\n");
370
+ const files = [
371
+ {
372
+ path: EKAIROS_HTTP_HELPER_PATH,
373
+ content: Buffer.from(httpHelper, "utf8"),
374
+ },
375
+ {
376
+ path: EKAIROS_RUNTIME_MANIFEST_PATH,
377
+ content: Buffer.from(JSON.stringify(manifest, null, 2), "utf8"),
378
+ },
379
+ {
380
+ path: EKAIROS_QUERY_SCRIPT_PATH,
381
+ content: Buffer.from(queryScript, "utf8"),
382
+ },
383
+ ];
384
+ return files;
385
+ }
386
+ static async resolveInstantUserIdByRefreshToken(params) {
387
+ const response = await fetch(`${INSTANT_API_BASE_URL}/admin/users?refresh_token=${encodeURIComponent(params.refreshToken)}`, {
388
+ method: "GET",
389
+ headers: {
390
+ authorization: `Bearer ${params.adminToken}`,
391
+ "app-id": params.appId,
392
+ },
393
+ });
394
+ const text = await response.text();
395
+ let parsed = null;
396
+ try {
397
+ parsed = text ? JSON.parse(text) : null;
398
+ }
399
+ catch {
400
+ parsed = null;
401
+ }
402
+ if (!response.ok) {
403
+ throw new Error(parsed?.message || parsed?.error || text || "instant_refresh_token_lookup_failed");
404
+ }
405
+ const userId = String(parsed?.user?.id ?? "").trim();
406
+ if (!userId) {
407
+ throw new Error("instant_refresh_token_user_id_missing");
408
+ }
409
+ return userId;
410
+ }
411
+ static async resolveEkairosBootstrap(config) {
412
+ const hasRuntimeBinding = config.env !== undefined || config.domain !== undefined;
413
+ if (!hasRuntimeBinding)
414
+ return null;
415
+ if (!config.env || !config.domain) {
416
+ throw new Error("sandbox_runtime_requires_env_and_domain");
417
+ }
418
+ const provider = SandboxService.resolveProvider(config);
419
+ if (provider !== "vercel") {
420
+ throw new Error("ekairos_runtime_requires_vercel_provider");
421
+ }
422
+ const datasetEnabled = Boolean(config.dataset?.enabled);
423
+ const runtime = await resolveRuntime(config.domain, config.env);
424
+ const adminDb = runtime?.db;
425
+ const appId = String(adminDb?.config?.appId ?? "").trim();
426
+ const adminToken = String(adminDb?.config?.adminToken ?? "").trim();
427
+ if (!adminDb || !appId || !adminToken) {
428
+ throw new Error("ekairos_runtime_admin_db_required");
429
+ }
430
+ const provisionalSandboxUserId = randomUUID();
431
+ const scopedToken = await adminDb.auth.createToken({ id: provisionalSandboxUserId });
432
+ const sandboxUserId = await SandboxService.resolveInstantUserIdByRefreshToken({
433
+ appId,
434
+ adminToken,
435
+ refreshToken: scopedToken,
436
+ });
437
+ return {
438
+ appId,
439
+ sandboxUserId,
440
+ scopedToken,
441
+ manifest: SandboxService.buildEkairosManifest({
442
+ appId,
443
+ sandboxUserId,
444
+ domain: config.domain,
445
+ datasetEnabled,
446
+ }),
447
+ networkPolicy: SandboxService.buildEkairosNetworkPolicy({ scopedToken, datasetEnabled }),
448
+ env: {
449
+ EKAIROS_RUNTIME_MANIFEST_PATH: EKAIROS_RUNTIME_MANIFEST_PATH,
450
+ EKAIROS_SANDBOX_USER_ID: sandboxUserId,
451
+ EKAIROS_INSTANT_APP_ID: appId,
452
+ CODEX_HOME: CODEX_HOME_DIR,
453
+ },
454
+ };
455
+ }
456
+ static async bootstrapEkairosFiles(sandbox, manifest) {
457
+ await SandboxService.safeMkDir(sandbox, EKAIROS_ROOT_DIR);
458
+ await SandboxService.safeMkDir(sandbox, CODEX_HOME_DIR);
459
+ await SandboxService.safeMkDir(sandbox, CODEX_SKILLS_DIR);
460
+ await sandbox.writeFiles(SandboxService.buildEkairosRuntimeFiles(manifest));
461
+ }
462
+ static async safeMkDir(sandbox, dirPath) {
463
+ try {
464
+ await sandbox.mkDir(dirPath);
465
+ }
466
+ catch (error) {
467
+ const message = formatSandboxError(error);
468
+ if (message.includes("File exists")) {
469
+ return;
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ static buildSkillInstallSet(skills) {
475
+ return skills.map((skill) => {
476
+ const skillName = String(skill.name ?? "").trim();
477
+ if (!skillName) {
478
+ throw new Error("sandbox_skill_name_required");
479
+ }
480
+ const rootDir = `${CODEX_SKILLS_DIR}/${skillName}`;
481
+ const files = (skill.files ?? []).map((file) => {
482
+ const relativePath = String(file.path ?? "").replace(/\\/g, "/").replace(/^\/+/, "").trim();
483
+ if (!relativePath) {
484
+ throw new Error(`sandbox_skill_file_path_required:${skillName}`);
485
+ }
486
+ return {
487
+ path: `${rootDir}/${relativePath}`,
488
+ content: Buffer.from(String(file.contentBase64 ?? ""), "base64"),
489
+ };
490
+ });
491
+ return {
492
+ name: skillName,
493
+ rootDir,
494
+ files,
495
+ };
496
+ });
497
+ }
498
+ static async bootstrapSkills(sandbox, skills) {
499
+ const installSet = SandboxService.buildSkillInstallSet(skills);
500
+ if (installSet.length === 0)
501
+ return [];
502
+ await SandboxService.safeMkDir(sandbox, CODEX_HOME_DIR);
503
+ await SandboxService.safeMkDir(sandbox, CODEX_SKILLS_DIR);
504
+ for (const skill of installSet) {
505
+ await SandboxService.safeMkDir(sandbox, skill.rootDir);
506
+ const parentDirs = Array.from(new Set(skill.files
507
+ .map((file) => path.posix.dirname(file.path))
508
+ .filter((dirPath) => dirPath && dirPath !== "." && dirPath !== skill.rootDir))).sort((a, b) => a.length - b.length);
509
+ for (const dirPath of parentDirs) {
510
+ await SandboxService.safeMkDir(sandbox, dirPath);
511
+ }
512
+ await sandbox.writeFiles(skill.files);
513
+ }
514
+ return installSet.map((skill) => ({
515
+ name: skill.name,
516
+ rootDir: skill.rootDir,
517
+ fileCount: skill.files.length,
518
+ }));
519
+ }
520
+ static resolveVercelWorkingDirectory(config) {
521
+ const fromConfig = String(config.vercel?.cwd ?? "").trim();
522
+ if (fromConfig)
523
+ return path.resolve(fromConfig);
524
+ const fromEnv = String(process.env.SANDBOX_VERCEL_CWD ?? "").trim();
525
+ if (fromEnv)
526
+ return path.resolve(fromEnv);
527
+ return process.cwd();
528
+ }
529
+ static findLinkedVercelProjectFile(startDir) {
530
+ let current = path.resolve(startDir);
531
+ while (true) {
532
+ const candidate = path.join(current, ".vercel", "project.json");
533
+ if (existsSync(candidate))
534
+ return candidate;
535
+ const parent = path.dirname(current);
536
+ if (parent === current)
537
+ return null;
538
+ current = parent;
539
+ }
540
+ }
541
+ static async readLinkedVercelProject(config) {
542
+ const cwd = SandboxService.resolveVercelWorkingDirectory(config);
543
+ const file = SandboxService.findLinkedVercelProjectFile(cwd);
544
+ if (!file) {
545
+ return { cwd };
546
+ }
547
+ try {
548
+ const parsed = JSON.parse(await fs.readFile(file, "utf8"));
549
+ return {
550
+ cwd,
551
+ orgId: typeof parsed?.orgId === "string" ? parsed.orgId : undefined,
552
+ projectId: typeof parsed?.projectId === "string" ? parsed.projectId : undefined,
553
+ projectName: typeof parsed?.projectName === "string" ? parsed.projectName : undefined,
554
+ };
555
+ }
556
+ catch {
557
+ return { cwd };
558
+ }
559
+ }
560
+ static async pullVercelOidcToken(config) {
561
+ const cwd = SandboxService.resolveVercelWorkingDirectory(config);
562
+ const tmpPath = path.join(os.tmpdir(), `ekairos-vercel-env-${Date.now()}-${Math.random().toString(36).slice(2)}.env`);
563
+ const args = ["env", "pull", tmpPath, "--yes", "--environment", String(config.vercel?.environment ?? "development")];
564
+ const scope = String(config.vercel?.scope ?? process.env.SANDBOX_VERCEL_SCOPE ?? "").trim();
565
+ if (scope) {
566
+ args.push("--scope", scope);
567
+ }
568
+ const token = String(process.env.VERCEL_TOKEN ?? process.env.SANDBOX_VERCEL_TOKEN ?? "").trim();
569
+ if (token) {
570
+ args.push("--token", token);
571
+ }
572
+ const isWindows = process.platform === "win32";
573
+ const command = isWindows ? (process.env.COMSPEC || "cmd.exe") : "vercel";
574
+ const commandArgs = isWindows ? ["/c", "vercel", ...args] : args;
575
+ try {
576
+ await execFileAsync(command, commandArgs, {
577
+ cwd,
578
+ windowsHide: true,
579
+ timeout: 120000,
580
+ maxBuffer: 1024 * 1024 * 10,
581
+ });
582
+ const content = await fs.readFile(tmpPath, "utf8");
583
+ const match = content.match(/VERCEL_OIDC_TOKEN=\"?([^\r\n\"]+)\"?/);
584
+ const oidc = String(match?.[1] ?? "").trim();
585
+ if (!oidc) {
586
+ throw new Error("VERCEL_OIDC_TOKEN missing from vercel env pull output");
587
+ }
588
+ return oidc;
589
+ }
590
+ finally {
591
+ await fs.rm(tmpPath, { force: true }).catch(() => { });
592
+ }
593
+ }
594
+ static async resolveVercelCredentials(config) {
595
+ const explicitTeamId = String(config.vercel?.orgId ?? process.env.SANDBOX_VERCEL_TEAM_ID ?? "").trim();
596
+ const explicitProjectId = String(config.vercel?.projectId ?? process.env.SANDBOX_VERCEL_PROJECT_ID ?? "").trim();
597
+ const explicitToken = String(config.vercel?.token ?? process.env.SANDBOX_VERCEL_TOKEN ?? process.env.VERCEL_OIDC_TOKEN ?? "").trim();
598
+ if (explicitTeamId && explicitProjectId && explicitToken) {
599
+ return { teamId: explicitTeamId, projectId: explicitProjectId, token: explicitToken };
600
+ }
601
+ const linked = await SandboxService.readLinkedVercelProject(config);
602
+ const teamId = explicitTeamId || String(linked.orgId ?? "").trim();
603
+ const projectId = explicitProjectId || String(linked.projectId ?? "").trim();
604
+ let token = explicitToken;
605
+ if (!token) {
606
+ token = await SandboxService.pullVercelOidcToken(config);
607
+ }
608
+ if (!teamId || !projectId || !token) {
609
+ throw new Error("Missing Vercel sandbox credentials. Link the project (`vercel link`) and ensure `vercel env pull` can resolve VERCEL_OIDC_TOKEN, or provide explicit SANDBOX_VERCEL_* env vars.");
610
+ }
611
+ return { teamId, projectId, token };
612
+ }
613
+ static async provisionVercelSandbox(config, extra) {
614
+ const creds = await SandboxService.resolveVercelCredentials(config);
615
+ const resolved = extra?.resolved ?? resolveVercelSandboxConfig(config);
616
+ if (resolved.reuse && resolved.name) {
617
+ try {
618
+ return await VercelSandbox.get({
619
+ name: resolved.name,
620
+ teamId: creds.teamId,
621
+ projectId: creds.projectId,
622
+ token: creds.token,
623
+ resume: true,
624
+ });
625
+ }
626
+ catch (error) {
627
+ const status = Number(error?.response?.status ?? 0);
628
+ const message = formatSandboxError(error).toLowerCase();
629
+ if (status !== 404 && !message.includes("not found")) {
630
+ throw error;
631
+ }
632
+ }
633
+ }
43
634
  return await VercelSandbox.create({
44
635
  teamId: creds.teamId,
45
636
  projectId: creds.projectId,
46
637
  token: creds.token,
47
- timeout: config.timeoutMs ?? 30 * 60 * 1000,
48
- ports: Array.isArray(config.ports) ? config.ports : [],
638
+ ...(resolved.name ? { name: resolved.name } : {}),
639
+ timeout: resolved.timeoutMs,
640
+ ports: resolved.ports,
49
641
  // IMPORTANT: pass runtime as-is (e.g. "python3.13") to match provider expectations.
50
642
  // Don't normalize to "python3"/"node22" as that can cause provider-side 400s.
51
- runtime: (config.runtime ?? "node22"),
52
- resources: { vcpus: config.resources?.vcpus ?? 2 },
643
+ runtime: resolved.runtime,
644
+ resources: { vcpus: resolved.vcpus },
645
+ persistent: resolved.persistent,
646
+ ...(resolved.snapshotExpirationMs !== undefined
647
+ ? { snapshotExpiration: resolved.snapshotExpirationMs }
648
+ : {}),
649
+ ...(resolved.tags ? { tags: resolved.tags } : {}),
650
+ networkPolicy: extra?.networkPolicy,
651
+ env: extra?.env,
53
652
  });
54
653
  }
55
654
  static getDaytonaConfig() {
@@ -235,8 +834,8 @@ export class SandboxService {
235
834
  : "";
236
835
  return {
237
836
  exitCode: Number.isFinite(exitCode) ? exitCode : 0,
238
- stdout,
239
- stderr,
837
+ stdout: sanitizeInstantString(stdout),
838
+ stderr: sanitizeInstantString(stderr),
240
839
  };
241
840
  }
242
841
  static async spritesExec(params) {
@@ -428,22 +1027,55 @@ export class SandboxService {
428
1027
  const sandboxId = id();
429
1028
  const now = Date.now();
430
1029
  const provider = SandboxService.resolveProvider(config);
1030
+ const resolvedVercel = provider === "vercel" ? resolveVercelSandboxConfig(config, { sandboxId }) : undefined;
431
1031
  let daytonaEphemeral = undefined;
1032
+ let installedSkills = [];
432
1033
  try {
1034
+ const ekairos = await SandboxService.resolveEkairosBootstrap(config);
433
1035
  const baseParams = config.params && typeof config.params === "object" && !Array.isArray(config.params) ? config.params : {};
434
1036
  await this.adminDb.transact(this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
435
1037
  status: "creating",
1038
+ ...(ekairos ? { sandboxUserId: ekairos.sandboxUserId } : {}),
436
1039
  provider,
437
- timeout: config.timeoutMs,
438
- runtime: config.runtime,
439
- vcpus: config.resources?.vcpus,
440
- ports: config.ports,
1040
+ timeout: resolvedVercel?.timeoutMs ?? config.timeoutMs,
1041
+ runtime: resolvedVercel?.runtime ?? config.runtime,
1042
+ vcpus: resolvedVercel?.vcpus ?? config.resources?.vcpus,
1043
+ ports: (resolvedVercel?.ports ?? config.ports),
441
1044
  purpose: config.purpose,
442
- params: baseParams,
1045
+ params: {
1046
+ ...baseParams,
1047
+ ...(resolvedVercel ? { vercel: safeVercelConfigForRecord(config, resolvedVercel) } : {}),
1048
+ ...(ekairos
1049
+ ? {
1050
+ ekairos: {
1051
+ enabled: true,
1052
+ sandboxUserId: ekairos.sandboxUserId,
1053
+ instant: {
1054
+ appId: ekairos.appId,
1055
+ apiBaseUrl: ekairos.manifest.instant.apiBaseUrl,
1056
+ },
1057
+ bootstrap: {
1058
+ manifestPath: EKAIROS_RUNTIME_MANIFEST_PATH,
1059
+ queryScriptPath: EKAIROS_QUERY_SCRIPT_PATH,
1060
+ },
1061
+ domain: ekairos.manifest.domain,
1062
+ ...(config.dataset?.enabled ? { dataset: { enabled: true } } : {}),
1063
+ ...(Array.isArray(config.skills) && config.skills.length > 0
1064
+ ? {
1065
+ skills: config.skills.map((skill) => ({
1066
+ name: skill.name,
1067
+ fileCount: Array.isArray(skill.files) ? skill.files.length : 0,
1068
+ })),
1069
+ }
1070
+ : {}),
1071
+ },
1072
+ }
1073
+ : {}),
1074
+ },
443
1075
  createdAt: now,
444
1076
  updatedAt: now,
445
1077
  }));
446
- let sandbox;
1078
+ let sandbox = null;
447
1079
  try {
448
1080
  if (provider === "daytona") {
449
1081
  const daytona = new Daytona(SandboxService.getDaytonaConfig());
@@ -505,11 +1137,36 @@ export class SandboxService {
505
1137
  });
506
1138
  }
507
1139
  else {
508
- sandbox = await SandboxService.provisionVercelSandbox(config);
1140
+ const vercelEnv = {
1141
+ ...(Array.isArray(config.skills) && config.skills.length > 0 ? { CODEX_HOME: CODEX_HOME_DIR } : {}),
1142
+ ...(ekairos?.env ?? {}),
1143
+ };
1144
+ sandbox = await SandboxService.provisionVercelSandbox(config, {
1145
+ networkPolicy: ekairos?.networkPolicy,
1146
+ env: Object.keys(vercelEnv).length > 0 ? vercelEnv : undefined,
1147
+ resolved: resolvedVercel,
1148
+ });
1149
+ if (ekairos) {
1150
+ await SandboxService.bootstrapEkairosFiles(sandbox, ekairos.manifest);
1151
+ }
1152
+ if (Array.isArray(config.skills) && config.skills.length > 0) {
1153
+ installedSkills = await SandboxService.bootstrapSkills(sandbox, config.skills);
1154
+ }
509
1155
  }
510
1156
  }
511
1157
  catch (e) {
512
- const msg = e instanceof Error ? e.message : String(e);
1158
+ const msg = formatSandboxError(e);
1159
+ if (sandbox && provider === "vercel") {
1160
+ try {
1161
+ await sandbox.stop({ blocking: true });
1162
+ if (resolvedVercel?.deleteOnStop) {
1163
+ await sandbox.delete();
1164
+ }
1165
+ }
1166
+ catch {
1167
+ // ignore cleanup errors during failed bootstrap
1168
+ }
1169
+ }
513
1170
  await this.adminDb.transact(this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
514
1171
  status: "error",
515
1172
  updatedAt: Date.now(),
@@ -521,37 +1178,68 @@ export class SandboxService {
521
1178
  ? sandbox.id
522
1179
  : provider === "sprites"
523
1180
  ? String(sandbox.name)
524
- : sandbox.sandboxId;
1181
+ : sandbox.name;
525
1182
  const sandboxUrl = provider === "sprites" ? sandbox.url : undefined;
526
- await this.adminDb.transact(this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
527
- status: "active",
528
- externalSandboxId,
529
- ...(sandboxUrl ? { sandboxUrl } : {}),
530
- updatedAt: Date.now(),
531
- params: {
532
- ...baseParams,
533
- ...(provider === "daytona"
534
- ? {
535
- daytona: {
536
- ...baseParams?.daytona,
537
- ephemeral: daytonaEphemeral,
538
- },
539
- }
540
- : {}),
541
- ...(provider === "sprites"
542
- ? {
543
- sprites: {
544
- ...baseParams?.sprites,
545
- id: sandbox.id,
546
- name: sandbox.name,
547
- url: sandbox.url,
548
- urlSettings: config.sprites?.urlSettings ?? baseParams?.sprites?.urlSettings ?? undefined,
549
- deleteOnStop: config.sprites?.deleteOnStop ?? baseParams?.sprites?.deleteOnStop ?? true,
550
- },
551
- }
552
- : {}),
553
- },
554
- }));
1183
+ const activateMutations = [
1184
+ this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
1185
+ status: "active",
1186
+ externalSandboxId,
1187
+ ...(ekairos ? { sandboxUserId: ekairos.sandboxUserId } : {}),
1188
+ ...(sandboxUrl ? { sandboxUrl } : {}),
1189
+ updatedAt: Date.now(),
1190
+ params: {
1191
+ ...baseParams,
1192
+ ...(ekairos
1193
+ ? {
1194
+ ekairos: {
1195
+ enabled: true,
1196
+ sandboxUserId: ekairos.sandboxUserId,
1197
+ instant: {
1198
+ appId: ekairos.appId,
1199
+ apiBaseUrl: ekairos.manifest.instant.apiBaseUrl,
1200
+ },
1201
+ bootstrap: {
1202
+ manifestPath: EKAIROS_RUNTIME_MANIFEST_PATH,
1203
+ queryScriptPath: EKAIROS_QUERY_SCRIPT_PATH,
1204
+ },
1205
+ domain: ekairos.manifest.domain,
1206
+ ...(config.dataset?.enabled ? { dataset: { enabled: true } } : {}),
1207
+ ...(installedSkills.length > 0 ? { skills: installedSkills } : {}),
1208
+ },
1209
+ }
1210
+ : {}),
1211
+ ...(provider === "vercel"
1212
+ ? {
1213
+ vercel: resolvedVercel ? safeVercelConfigForRecord(config, resolvedVercel) : {},
1214
+ }
1215
+ : {}),
1216
+ ...(provider === "daytona"
1217
+ ? {
1218
+ daytona: {
1219
+ ...baseParams?.daytona,
1220
+ ephemeral: daytonaEphemeral,
1221
+ },
1222
+ }
1223
+ : {}),
1224
+ ...(provider === "sprites"
1225
+ ? {
1226
+ sprites: {
1227
+ ...baseParams?.sprites,
1228
+ id: sandbox.id,
1229
+ name: sandbox.name,
1230
+ url: sandbox.url,
1231
+ urlSettings: config.sprites?.urlSettings ?? baseParams?.sprites?.urlSettings ?? undefined,
1232
+ deleteOnStop: config.sprites?.deleteOnStop ?? baseParams?.sprites?.deleteOnStop ?? true,
1233
+ },
1234
+ }
1235
+ : {}),
1236
+ },
1237
+ }),
1238
+ ];
1239
+ if (ekairos) {
1240
+ activateMutations.push(this.adminDb.tx.sandbox_sandboxes[sandboxId].link({ user: ekairos.sandboxUserId }));
1241
+ }
1242
+ await this.adminDb.transact(activateMutations);
555
1243
  return { ok: true, data: { sandboxId } };
556
1244
  }
557
1245
  catch (e) {
@@ -648,13 +1336,13 @@ export class SandboxService {
648
1336
  if (record.provider !== "vercel") {
649
1337
  return { ok: false, error: "Valid sandbox record not found" };
650
1338
  }
651
- const creds = SandboxService.getVercelCredentials();
1339
+ const creds = await SandboxService.resolveVercelCredentials(record?.params ?? {});
652
1340
  try {
653
1341
  const maxAttempts = 20;
654
1342
  const delayMs = 500;
655
1343
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
656
1344
  const sandbox = await VercelSandbox.get({
657
- sandboxId: String(record.externalSandboxId),
1345
+ name: String(record.externalSandboxId),
658
1346
  teamId: creds.teamId,
659
1347
  projectId: creds.projectId,
660
1348
  token: creds.token,
@@ -684,6 +1372,260 @@ export class SandboxService {
684
1372
  return { ok: false, error: formatInstantSchemaError(e) };
685
1373
  }
686
1374
  }
1375
+ async getSandboxRecord(sandboxId) {
1376
+ const recordResult = await this.adminDb.query({
1377
+ sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 }, user: {} },
1378
+ });
1379
+ return recordResult?.sandbox_sandboxes?.[0] ?? null;
1380
+ }
1381
+ async getProcessSnapshot(processId) {
1382
+ try {
1383
+ const processResult = await this.adminDb.query({
1384
+ sandbox_processes: {
1385
+ $: { where: { id: processId }, limit: 1 },
1386
+ sandbox: {},
1387
+ },
1388
+ });
1389
+ const processRow = processResult?.sandbox_processes?.[0];
1390
+ if (!processRow)
1391
+ return { ok: false, error: "sandbox_process_not_found" };
1392
+ return { ok: true, data: processRow };
1393
+ }
1394
+ catch (e) {
1395
+ return { ok: false, error: formatInstantSchemaError(e) };
1396
+ }
1397
+ }
1398
+ async markOpenProcessesLost(sandboxId, reason) {
1399
+ try {
1400
+ const processResult = await this.adminDb.query({
1401
+ sandbox_processes: {
1402
+ $: {
1403
+ where: { "sandbox.id": sandboxId },
1404
+ limit: 500,
1405
+ },
1406
+ },
1407
+ });
1408
+ const rows = Array.isArray(processResult?.sandbox_processes)
1409
+ ? processResult.sandbox_processes
1410
+ : [];
1411
+ const now = Date.now();
1412
+ const txs = rows
1413
+ .filter((row) => !SANDBOX_PROCESS_TERMINAL_STATUSES.has(String(row?.status ?? "")))
1414
+ .map((row) => this.adminDb.tx.sandbox_processes[String(row.id)].update({
1415
+ status: "lost",
1416
+ streamFinishedAt: row.streamFinishedAt ?? now,
1417
+ streamAbortReason: reason,
1418
+ exitedAt: now,
1419
+ updatedAt: now,
1420
+ metadata: {
1421
+ ...(row.metadata ?? {}),
1422
+ lostReason: reason,
1423
+ },
1424
+ }));
1425
+ if (txs.length > 0) {
1426
+ await this.adminDb.transact(txs);
1427
+ }
1428
+ }
1429
+ catch {
1430
+ // Best-effort cleanup; stopping the sandbox should not fail because process metadata could not be marked.
1431
+ }
1432
+ }
1433
+ async createProcessStream(params) {
1434
+ const streams = this.adminDb?.streams;
1435
+ if (!streams?.createWriteStream) {
1436
+ throw new Error("sandbox_process_streams_unavailable");
1437
+ }
1438
+ const streamClientId = params.streamClientId || createSandboxProcessStreamClientId(params.processId);
1439
+ const stream = streams.createWriteStream({ clientId: streamClientId });
1440
+ const streamId = typeof stream.streamId === "function" ? await stream.streamId() : streamClientId;
1441
+ return { stream, streamId, streamClientId };
1442
+ }
1443
+ async writeProcessChunk(params) {
1444
+ await params.writer.write(encodeSandboxProcessStreamChunk({
1445
+ version: SANDBOX_PROCESS_STREAM_VERSION,
1446
+ at: nowIso(),
1447
+ seq: params.seq,
1448
+ type: params.type,
1449
+ sandboxId: params.sandboxId,
1450
+ processId: params.processId,
1451
+ ...(params.data ? { data: sanitizeInstantValue(params.data) } : {}),
1452
+ }));
1453
+ }
1454
+ async readProcessRow(processId) {
1455
+ const result = await this.adminDb.query({
1456
+ sandbox_processes: {
1457
+ $: { where: { id: processId }, limit: 1 },
1458
+ sandbox: {},
1459
+ },
1460
+ });
1461
+ return result?.sandbox_processes?.[0] ?? null;
1462
+ }
1463
+ async writeProcessChunkByProcessId(processId, type, data, opts) {
1464
+ const row = await this.readProcessRow(processId);
1465
+ if (!row)
1466
+ throw new Error("sandbox_process_not_found");
1467
+ const linkedSandbox = Array.isArray(row?.sandbox) ? row.sandbox[0] : row?.sandbox;
1468
+ const sandboxId = String(linkedSandbox?.id ?? row?.sandboxId ?? "").trim();
1469
+ if (!sandboxId)
1470
+ throw new Error("sandbox_process_sandbox_missing");
1471
+ const streamClientId = String(row.streamClientId ?? "").trim() || createSandboxProcessStreamClientId(processId);
1472
+ const streams = this.adminDb?.streams;
1473
+ if (!streams?.createWriteStream)
1474
+ throw new Error("sandbox_process_streams_unavailable");
1475
+ const stream = streams.createWriteStream({ clientId: streamClientId });
1476
+ const writer = stream.getWriter();
1477
+ try {
1478
+ const seq = Number(row.metadata?.lastSeq ?? row.metadata?.chunkCount ?? 0) + 1;
1479
+ await this.writeProcessChunk({
1480
+ writer,
1481
+ sandboxId,
1482
+ processId,
1483
+ seq,
1484
+ type,
1485
+ data,
1486
+ });
1487
+ if (opts?.close) {
1488
+ await writer.close();
1489
+ }
1490
+ await this.adminDb.transact([
1491
+ this.adminDb.tx.sandbox_processes[processId].update({
1492
+ updatedAt: Date.now(),
1493
+ metadata: sanitizeInstantValue({
1494
+ ...(row.metadata ?? {}),
1495
+ lastSeq: seq,
1496
+ chunkCount: seq,
1497
+ }),
1498
+ }),
1499
+ ]);
1500
+ }
1501
+ finally {
1502
+ try {
1503
+ writer.releaseLock();
1504
+ }
1505
+ catch {
1506
+ // ignore
1507
+ }
1508
+ }
1509
+ }
1510
+ async startObservedProcess(sandboxId, opts) {
1511
+ const processId = id();
1512
+ const now = Date.now();
1513
+ try {
1514
+ const record = await this.getSandboxRecord(sandboxId);
1515
+ if (!record)
1516
+ return { ok: false, error: "Valid sandbox record not found" };
1517
+ if (record.status !== "active")
1518
+ return { ok: false, error: `sandbox_not_active:${record.status}` };
1519
+ const streamSession = await this.createProcessStream({ sandboxId, processId });
1520
+ const stream = streamSession.stream;
1521
+ const writer = stream.getWriter();
1522
+ try {
1523
+ await this.adminDb.transact([
1524
+ this.adminDb.tx.sandbox_processes[processId]
1525
+ .update({
1526
+ kind: opts.kind ?? "command",
1527
+ mode: opts.mode ?? "foreground",
1528
+ status: "running",
1529
+ provider: String(record.provider ?? "unknown"),
1530
+ command: sanitizeInstantString(opts.command),
1531
+ args: sanitizeInstantValue(Array.isArray(opts.args) ? opts.args : []),
1532
+ cwd: asOptionalString(opts.cwd),
1533
+ env: sanitizeInstantValue(opts.env),
1534
+ externalProcessId: asOptionalString(opts.externalProcessId),
1535
+ streamId: streamSession.streamId,
1536
+ streamClientId: streamSession.streamClientId,
1537
+ streamStartedAt: now,
1538
+ startedAt: now,
1539
+ updatedAt: now,
1540
+ metadata: sanitizeInstantValue({
1541
+ ...(opts.metadata ?? {}),
1542
+ observed: true,
1543
+ lastSeq: 1,
1544
+ chunkCount: 1,
1545
+ }),
1546
+ })
1547
+ .link({ sandbox: sandboxId, stream: streamSession.streamId }),
1548
+ ]);
1549
+ await this.writeProcessChunk({
1550
+ writer,
1551
+ sandboxId,
1552
+ processId,
1553
+ seq: 1,
1554
+ type: "status",
1555
+ data: {
1556
+ status: "running",
1557
+ command: opts.command,
1558
+ args: Array.isArray(opts.args) ? opts.args : [],
1559
+ cwd: opts.cwd ?? null,
1560
+ externalProcessId: opts.externalProcessId ?? null,
1561
+ },
1562
+ });
1563
+ // Keep observed-process streams open across calls; finishObservedProcess closes them.
1564
+ }
1565
+ finally {
1566
+ try {
1567
+ writer.releaseLock();
1568
+ }
1569
+ catch {
1570
+ // ignore
1571
+ }
1572
+ }
1573
+ return {
1574
+ ok: true,
1575
+ data: {
1576
+ processId,
1577
+ streamId: streamSession.streamId,
1578
+ streamClientId: streamSession.streamClientId,
1579
+ },
1580
+ };
1581
+ }
1582
+ catch (e) {
1583
+ return { ok: false, error: formatInstantSchemaError(e) };
1584
+ }
1585
+ }
1586
+ async appendObservedProcessChunk(processId, type, data) {
1587
+ try {
1588
+ await this.writeProcessChunkByProcessId(processId, type, data);
1589
+ return { ok: true, data: undefined };
1590
+ }
1591
+ catch (e) {
1592
+ return { ok: false, error: formatInstantSchemaError(e) };
1593
+ }
1594
+ }
1595
+ async finishObservedProcess(processId, opts) {
1596
+ try {
1597
+ const row = await this.readProcessRow(processId);
1598
+ if (!row)
1599
+ return { ok: false, error: "sandbox_process_not_found" };
1600
+ const exitCode = Number.isFinite(Number(opts?.exitCode)) ? Number(opts?.exitCode) : undefined;
1601
+ const status = opts?.status ?? (exitCode === undefined || exitCode === 0 ? "exited" : "failed");
1602
+ await this.writeProcessChunkByProcessId(processId, status === "failed" ? "error" : "exit", {
1603
+ exitCode: exitCode ?? null,
1604
+ status,
1605
+ ...(opts?.errorText ? { message: opts.errorText } : {}),
1606
+ }, { close: true });
1607
+ const finishedAt = Date.now();
1608
+ await this.adminDb.transact([
1609
+ this.adminDb.tx.sandbox_processes[processId].update({
1610
+ status,
1611
+ ...(exitCode !== undefined ? { exitCode } : {}),
1612
+ streamFinishedAt: finishedAt,
1613
+ streamAbortReason: opts?.errorText ?? null,
1614
+ exitedAt: finishedAt,
1615
+ updatedAt: finishedAt,
1616
+ metadata: sanitizeInstantValue({
1617
+ ...(row.metadata ?? {}),
1618
+ ...(opts?.metadata ?? {}),
1619
+ ...(opts?.errorText ? { error: opts.errorText } : {}),
1620
+ }),
1621
+ }),
1622
+ ]);
1623
+ return { ok: true, data: undefined };
1624
+ }
1625
+ catch (e) {
1626
+ return { ok: false, error: formatInstantSchemaError(e) };
1627
+ }
1628
+ }
687
1629
  async stopSandbox(sandboxId) {
688
1630
  try {
689
1631
  const result = await this.reconnectToSandbox(sandboxId);
@@ -694,13 +1636,19 @@ export class SandboxService {
694
1636
  const deleteOnStop = record?.provider === "sprites"
695
1637
  ? SandboxService.parseOptionalBoolean(process.env.SANDBOX_SPRITES_DELETE_ON_STOP) ??
696
1638
  Boolean(record?.params?.sprites?.deleteOnStop ?? true)
697
- : SandboxService.parseOptionalBoolean(process.env.SANDBOX_DAYTONA_DELETE_ON_STOP) ??
698
- Boolean(record?.params?.daytona?.ephemeral);
1639
+ : record?.provider === "vercel"
1640
+ ? SandboxService.parseOptionalBoolean(process.env.SANDBOX_VERCEL_DELETE_ON_STOP) ??
1641
+ Boolean(record?.params?.vercel?.deleteOnStop ?? !record?.params?.vercel?.persistent)
1642
+ : SandboxService.parseOptionalBoolean(process.env.SANDBOX_DAYTONA_DELETE_ON_STOP) ??
1643
+ Boolean(record?.params?.daytona?.ephemeral);
699
1644
  if (result.ok) {
700
1645
  try {
701
1646
  const sandbox = result.data.sandbox;
702
- if (sandbox?.sandboxId) {
703
- await sandbox.stop();
1647
+ if (isVercelSandbox(sandbox)) {
1648
+ await sandbox.stop({ blocking: true });
1649
+ if (deleteOnStop) {
1650
+ await sandbox.delete();
1651
+ }
704
1652
  }
705
1653
  else if (sandbox?.__provider === "sprites") {
706
1654
  // Sprites does not have a reliable "stop" semantic; deleting is the durable cleanup primitive.
@@ -735,19 +1683,55 @@ export class SandboxService {
735
1683
  shutdownAt: Date.now(),
736
1684
  updatedAt: Date.now(),
737
1685
  }));
1686
+ await this.markOpenProcessesLost(sandboxId, "sandbox_stopped");
738
1687
  return { ok: true, data: undefined };
739
1688
  }
740
1689
  catch (e) {
741
1690
  return { ok: false, error: formatInstantSchemaError(e) };
742
1691
  }
743
1692
  }
1693
+ async query(sandboxId, query) {
1694
+ try {
1695
+ const record = await this.getSandboxRecord(sandboxId);
1696
+ if (!record) {
1697
+ return { ok: false, error: "Valid sandbox record not found" };
1698
+ }
1699
+ if (record.provider !== "vercel") {
1700
+ return { ok: false, error: "sandbox_query_requires_vercel_provider" };
1701
+ }
1702
+ const queryScriptPath = String(record?.params?.ekairos?.bootstrap?.queryScriptPath ?? "").trim();
1703
+ if (!queryScriptPath) {
1704
+ return { ok: false, error: "sandbox_query_not_configured" };
1705
+ }
1706
+ const manifestPath = String(record?.params?.ekairos?.bootstrap?.manifestPath ?? "").trim() || EKAIROS_RUNTIME_MANIFEST_PATH;
1707
+ const encodedQuery = Buffer.from(JSON.stringify(query), "utf8").toString("base64url");
1708
+ const result = await this.runCommand(sandboxId, "node", [queryScriptPath, encodedQuery, manifestPath]);
1709
+ if (!result.ok) {
1710
+ return result;
1711
+ }
1712
+ const stdout = String(result.data.output ?? "").trim();
1713
+ if (!stdout) {
1714
+ return { ok: false, error: "sandbox_query_empty_response" };
1715
+ }
1716
+ try {
1717
+ return { ok: true, data: JSON.parse(stdout) };
1718
+ }
1719
+ catch (error) {
1720
+ const message = error instanceof Error ? error.message : String(error);
1721
+ return { ok: false, error: `sandbox_query_invalid_json: ${message}` };
1722
+ }
1723
+ }
1724
+ catch (e) {
1725
+ return { ok: false, error: formatInstantSchemaError(e) };
1726
+ }
1727
+ }
744
1728
  async runCommand(sandboxId, command, args = []) {
745
1729
  try {
746
1730
  const sandboxResult = await this.reconnectToSandbox(sandboxId);
747
1731
  if (!sandboxResult.ok)
748
1732
  return { ok: false, error: sandboxResult.error };
749
1733
  const sandbox = sandboxResult.data.sandbox;
750
- if (sandbox.sandboxId) {
1734
+ if (isVercelSandbox(sandbox)) {
751
1735
  const result = await runCommandInSandbox(sandbox, command, args);
752
1736
  return { ok: true, data: result };
753
1737
  }
@@ -788,13 +1772,279 @@ export class SandboxService {
788
1772
  return { ok: false, error: formatInstantSchemaError(e) };
789
1773
  }
790
1774
  }
1775
+ async runCommandProcess(sandboxId, command, args = [], opts) {
1776
+ const processId = id();
1777
+ const now = Date.now();
1778
+ let writer = null;
1779
+ let stream = null;
1780
+ let seq = 0;
1781
+ try {
1782
+ const record = await this.getSandboxRecord(sandboxId);
1783
+ if (!record)
1784
+ return { ok: false, error: "Valid sandbox record not found" };
1785
+ if (record.status !== "active")
1786
+ return { ok: false, error: `sandbox_not_active:${record.status}` };
1787
+ const streamSession = await this.createProcessStream({ sandboxId, processId });
1788
+ stream = streamSession.stream;
1789
+ writer = stream.getWriter();
1790
+ await this.adminDb.transact([
1791
+ this.adminDb.tx.sandbox_processes[processId]
1792
+ .update({
1793
+ kind: opts?.kind ?? "command",
1794
+ mode: opts?.mode ?? "foreground",
1795
+ status: "running",
1796
+ provider: String(record.provider ?? "unknown"),
1797
+ command: sanitizeInstantString(command),
1798
+ args: sanitizeInstantValue(Array.isArray(args) ? args : []),
1799
+ cwd: asOptionalString(opts?.cwd),
1800
+ env: sanitizeInstantValue(opts?.env),
1801
+ streamId: streamSession.streamId,
1802
+ streamClientId: streamSession.streamClientId,
1803
+ streamStartedAt: now,
1804
+ startedAt: now,
1805
+ updatedAt: now,
1806
+ metadata: sanitizeInstantValue(opts?.metadata),
1807
+ })
1808
+ .link({ sandbox: sandboxId, stream: streamSession.streamId }),
1809
+ ]);
1810
+ seq += 1;
1811
+ await this.writeProcessChunk({
1812
+ writer,
1813
+ sandboxId,
1814
+ processId,
1815
+ seq,
1816
+ type: "status",
1817
+ data: {
1818
+ status: "running",
1819
+ command,
1820
+ args: Array.isArray(args) ? args : [],
1821
+ cwd: opts?.cwd ?? null,
1822
+ },
1823
+ });
1824
+ const result = await this.runCommand(sandboxId, command, args);
1825
+ const finishedAt = Date.now();
1826
+ let finalResult;
1827
+ let status;
1828
+ let exitCode;
1829
+ let errorText;
1830
+ if (result.ok) {
1831
+ finalResult = result.data;
1832
+ exitCode = Number(result.data.exitCode ?? (result.data.success === false ? 1 : 0));
1833
+ status = exitCode === 0 ? "exited" : "failed";
1834
+ const stdout = String(result.data.stdout ?? result.data.output ?? "");
1835
+ const stderr = String(result.data.stderr ?? result.data.error ?? "");
1836
+ if (stdout) {
1837
+ seq += 1;
1838
+ await this.writeProcessChunk({
1839
+ writer,
1840
+ sandboxId,
1841
+ processId,
1842
+ seq,
1843
+ type: "stdout",
1844
+ data: { text: stdout },
1845
+ });
1846
+ }
1847
+ if (stderr) {
1848
+ seq += 1;
1849
+ await this.writeProcessChunk({
1850
+ writer,
1851
+ sandboxId,
1852
+ processId,
1853
+ seq,
1854
+ type: "stderr",
1855
+ data: { text: stderr },
1856
+ });
1857
+ }
1858
+ }
1859
+ else {
1860
+ exitCode = 1;
1861
+ status = "failed";
1862
+ errorText = result.error;
1863
+ finalResult = {
1864
+ success: false,
1865
+ exitCode,
1866
+ output: "",
1867
+ error: result.error,
1868
+ command: [command, ...(Array.isArray(args) ? args : [])].join(" "),
1869
+ };
1870
+ seq += 1;
1871
+ await this.writeProcessChunk({
1872
+ writer,
1873
+ sandboxId,
1874
+ processId,
1875
+ seq,
1876
+ type: "error",
1877
+ data: { message: result.error },
1878
+ });
1879
+ }
1880
+ seq += 1;
1881
+ await this.writeProcessChunk({
1882
+ writer,
1883
+ sandboxId,
1884
+ processId,
1885
+ seq,
1886
+ type: "exit",
1887
+ data: { exitCode, status },
1888
+ });
1889
+ await writer.close();
1890
+ writer = null;
1891
+ await this.adminDb.transact([
1892
+ this.adminDb.tx.sandbox_processes[processId].update({
1893
+ status,
1894
+ exitCode,
1895
+ streamFinishedAt: finishedAt,
1896
+ streamAbortReason: null,
1897
+ exitedAt: finishedAt,
1898
+ updatedAt: finishedAt,
1899
+ metadata: sanitizeInstantValue({
1900
+ ...(opts?.metadata ?? {}),
1901
+ ...(errorText ? { error: errorText } : {}),
1902
+ chunkCount: seq,
1903
+ result: finalResult,
1904
+ }),
1905
+ }),
1906
+ ]);
1907
+ await resumeSandboxProcessHook(processId, finalResult);
1908
+ return {
1909
+ ok: true,
1910
+ data: new SandboxCommandRun({
1911
+ sandboxId,
1912
+ processId,
1913
+ streamId: streamSession.streamId,
1914
+ streamClientId: streamSession.streamClientId,
1915
+ result: finalResult,
1916
+ }, this),
1917
+ };
1918
+ }
1919
+ catch (e) {
1920
+ const message = formatInstantSchemaError(e);
1921
+ const failedAt = Date.now();
1922
+ try {
1923
+ if (writer) {
1924
+ seq += 1;
1925
+ await this.writeProcessChunk({
1926
+ writer,
1927
+ sandboxId,
1928
+ processId,
1929
+ seq,
1930
+ type: "error",
1931
+ data: { message },
1932
+ });
1933
+ await writer.abort(message);
1934
+ writer = null;
1935
+ }
1936
+ else if (stream) {
1937
+ await stream.abort(message);
1938
+ }
1939
+ }
1940
+ catch {
1941
+ // ignore stream cleanup errors
1942
+ }
1943
+ try {
1944
+ const finalResult = {
1945
+ success: false,
1946
+ exitCode: 1,
1947
+ output: "",
1948
+ error: message,
1949
+ command: [command, ...(Array.isArray(args) ? args : [])].join(" "),
1950
+ };
1951
+ await this.adminDb.transact([
1952
+ this.adminDb.tx.sandbox_processes[processId].update({
1953
+ status: "failed",
1954
+ streamFinishedAt: failedAt,
1955
+ streamAbortReason: message,
1956
+ exitedAt: failedAt,
1957
+ updatedAt: failedAt,
1958
+ metadata: sanitizeInstantValue({
1959
+ ...(opts?.metadata ?? {}),
1960
+ error: message,
1961
+ result: finalResult,
1962
+ }),
1963
+ }),
1964
+ ]);
1965
+ await resumeSandboxProcessHook(processId, finalResult);
1966
+ }
1967
+ catch {
1968
+ // ignore partial metadata failures
1969
+ }
1970
+ return { ok: false, error: message };
1971
+ }
1972
+ finally {
1973
+ try {
1974
+ writer?.releaseLock();
1975
+ }
1976
+ catch {
1977
+ // ignore
1978
+ }
1979
+ }
1980
+ }
1981
+ async runCommandWithProcessStream(sandboxId, command, args = [], opts) {
1982
+ const run = await this.runCommandProcess(sandboxId, command, args, opts);
1983
+ if (!run.ok)
1984
+ return run;
1985
+ const result = await run.data;
1986
+ return {
1987
+ ok: true,
1988
+ data: {
1989
+ processId: run.data.processId,
1990
+ streamId: run.data.streamId,
1991
+ streamClientId: run.data.streamClientId,
1992
+ result,
1993
+ },
1994
+ };
1995
+ }
1996
+ async readProcessStream(processId) {
1997
+ try {
1998
+ const processResult = await this.adminDb.query({
1999
+ sandbox_processes: {
2000
+ $: { where: { id: processId }, limit: 1 },
2001
+ },
2002
+ });
2003
+ const processRow = processResult?.sandbox_processes?.[0];
2004
+ if (!processRow)
2005
+ return { ok: false, error: "sandbox_process_not_found" };
2006
+ const streams = this.adminDb?.streams;
2007
+ if (!streams?.createReadStream)
2008
+ return { ok: false, error: "sandbox_process_streams_unavailable" };
2009
+ const clientId = String(processRow.streamClientId ?? "").trim() || undefined;
2010
+ const streamId = String(processRow.streamId ?? "").trim() || undefined;
2011
+ if (!clientId && !streamId)
2012
+ return { ok: false, error: "sandbox_process_stream_missing" };
2013
+ const stream = streams.createReadStream({ clientId, streamId });
2014
+ const chunks = [];
2015
+ let byteOffset = 0;
2016
+ let buffer = "";
2017
+ for await (const raw of stream) {
2018
+ const encoded = typeof raw === "string" ? raw : String(raw ?? "");
2019
+ if (!encoded)
2020
+ continue;
2021
+ byteOffset += new TextEncoder().encode(encoded).length;
2022
+ buffer += encoded;
2023
+ const lines = buffer.split("\n");
2024
+ buffer = lines.pop() ?? "";
2025
+ for (const line of lines) {
2026
+ const trimmed = line.trim();
2027
+ if (!trimmed)
2028
+ continue;
2029
+ chunks.push(parseSandboxProcessStreamChunk(trimmed));
2030
+ }
2031
+ }
2032
+ const trailing = buffer.trim();
2033
+ if (trailing)
2034
+ chunks.push(parseSandboxProcessStreamChunk(trailing));
2035
+ return { ok: true, data: { chunks, byteOffset } };
2036
+ }
2037
+ catch (e) {
2038
+ return { ok: false, error: formatInstantSchemaError(e) };
2039
+ }
2040
+ }
791
2041
  async writeFiles(sandboxId, files) {
792
2042
  try {
793
2043
  const sandboxResult = await this.reconnectToSandbox(sandboxId);
794
2044
  if (!sandboxResult.ok)
795
2045
  return { ok: false, error: sandboxResult.error };
796
2046
  const sandbox = sandboxResult.data.sandbox;
797
- if (sandbox.sandboxId) {
2047
+ if (isVercelSandbox(sandbox)) {
798
2048
  await sandbox.writeFiles(files.map((f) => ({
799
2049
  path: f.path,
800
2050
  content: Buffer.from(f.contentBase64, "base64"),
@@ -836,7 +2086,7 @@ export class SandboxService {
836
2086
  if (!sandboxResult.ok)
837
2087
  return { ok: false, error: sandboxResult.error };
838
2088
  const sandbox = sandboxResult.data.sandbox;
839
- if (sandbox.sandboxId) {
2089
+ if (isVercelSandbox(sandbox)) {
840
2090
  const stream = await sandbox.readFile({ path });
841
2091
  if (!stream) {
842
2092
  return { ok: true, data: { contentBase64: "" } };
@@ -878,6 +2128,38 @@ export class SandboxService {
878
2128
  return { ok: false, error: formatInstantSchemaError(e) };
879
2129
  }
880
2130
  }
2131
+ async getPortUrl(sandboxId, port) {
2132
+ try {
2133
+ const sandboxResult = await this.reconnectToSandbox(sandboxId);
2134
+ if (!sandboxResult.ok)
2135
+ return { ok: false, error: sandboxResult.error };
2136
+ const sandbox = sandboxResult.data.sandbox;
2137
+ const normalizedPort = Math.max(1, Math.floor(Number(port)));
2138
+ if (isVercelSandbox(sandbox)) {
2139
+ const url = sandbox.domain(normalizedPort);
2140
+ return { ok: true, data: { url: String(url ?? "").replace(/\/+$/, "") } };
2141
+ }
2142
+ if (sandbox.__provider === "sprites") {
2143
+ const base = String(sandbox.url ?? "").trim().replace(/\/+$/, "");
2144
+ if (!base)
2145
+ return { ok: false, error: "sprites_url_missing" };
2146
+ if (normalizedPort === 8080)
2147
+ return { ok: true, data: { url: base } };
2148
+ try {
2149
+ const u = new URL(base);
2150
+ u.port = String(normalizedPort);
2151
+ return { ok: true, data: { url: u.toString().replace(/\/+$/, "") } };
2152
+ }
2153
+ catch {
2154
+ return { ok: true, data: { url: `${base}:${normalizedPort}` } };
2155
+ }
2156
+ }
2157
+ return { ok: false, error: "sandbox_port_url_not_supported" };
2158
+ }
2159
+ catch (e) {
2160
+ return { ok: false, error: formatInstantSchemaError(e) };
2161
+ }
2162
+ }
881
2163
  static parseSpritesCheckpointIdFromNdjson(text) {
882
2164
  const lines = String(text ?? "")
883
2165
  .split("\n")
@@ -909,6 +2191,33 @@ export class SandboxService {
909
2191
  sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 } },
910
2192
  });
911
2193
  const record = recordResult?.sandbox_sandboxes?.[0];
2194
+ if (record?.externalSandboxId && record.provider === "vercel") {
2195
+ const sandboxResult = await this.reconnectToSandbox(sandboxId);
2196
+ if (!sandboxResult.ok)
2197
+ return { ok: false, error: sandboxResult.error };
2198
+ const sandbox = sandboxResult.data.sandbox;
2199
+ if (!isVercelSandbox(sandbox))
2200
+ return { ok: false, error: "checkpoint_not_supported" };
2201
+ const expiration = Number(record?.params?.vercel?.snapshotExpirationMs);
2202
+ const snapshot = await sandbox.snapshot({
2203
+ ...(Number.isFinite(expiration) ? { expiration } : {}),
2204
+ });
2205
+ const checkpointId = String(snapshot?.snapshotId ?? "").trim();
2206
+ if (!checkpointId)
2207
+ return { ok: false, error: "vercel_snapshot_id_missing" };
2208
+ await this.adminDb.transact(this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
2209
+ updatedAt: Date.now(),
2210
+ params: {
2211
+ ...(record.params ?? {}),
2212
+ vercel: {
2213
+ ...(record.params?.vercel ?? {}),
2214
+ lastCheckpointId: checkpointId,
2215
+ lastCheckpointComment: String(params?.comment ?? "").trim() || undefined,
2216
+ },
2217
+ },
2218
+ }));
2219
+ return { ok: true, data: { checkpointId } };
2220
+ }
912
2221
  if (!record?.externalSandboxId || record.provider !== "sprites") {
913
2222
  return { ok: false, error: "checkpoint_not_supported" };
914
2223
  }
@@ -950,6 +2259,21 @@ export class SandboxService {
950
2259
  sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 } },
951
2260
  });
952
2261
  const record = recordResult?.sandbox_sandboxes?.[0];
2262
+ if (record?.externalSandboxId && record.provider === "vercel") {
2263
+ const creds = await SandboxService.resolveVercelCredentials(record?.params ?? {});
2264
+ const listed = await VercelSnapshot.list({
2265
+ teamId: creds.teamId,
2266
+ projectId: creds.projectId,
2267
+ token: creds.token,
2268
+ name: String(record.externalSandboxId),
2269
+ limit: 50,
2270
+ sortOrder: "desc",
2271
+ });
2272
+ const checkpointIds = (listed.snapshots ?? [])
2273
+ .map((snapshot) => String(snapshot?.id ?? "").trim())
2274
+ .filter(Boolean);
2275
+ return { ok: true, data: { checkpointIds } };
2276
+ }
953
2277
  if (!record?.externalSandboxId || record.provider !== "sprites") {
954
2278
  return { ok: false, error: "checkpoint_not_supported" };
955
2279
  }