@ekairos/sandbox 1.22.10-beta.development.0 → 1.22.11-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,8 +1,10 @@
1
1
  import { Sandbox as VercelSandbox } from "@vercel/sandbox";
2
2
  import { Daytona, Image } from "@daytonaio/sdk";
3
- import { id } from "@instantdb/admin";
3
+ import { id, init } from "@instantdb/admin";
4
4
  import { resolveRuntime } from "@ekairos/domain/runtime";
5
+ import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
5
6
  import { runCommandInSandbox } from "./commands.js";
7
+ import { sandboxDomain } from "./schema.js";
6
8
  import { execFile } from "node:child_process";
7
9
  import { randomUUID } from "node:crypto";
8
10
  import { existsSync, promises as fs } from "node:fs";
@@ -10,6 +12,14 @@ import os from "node:os";
10
12
  import path from "node:path";
11
13
  import { promisify } from "node:util";
12
14
  const execFileAsync = promisify(execFile);
15
+ function isVercelSandbox(sandbox) {
16
+ return Boolean(sandbox &&
17
+ typeof sandbox === "object" &&
18
+ typeof sandbox.runCommand === "function" &&
19
+ typeof sandbox.currentSession === "function" &&
20
+ typeof sandbox.name === "string" &&
21
+ sandbox.__provider !== "sprites");
22
+ }
13
23
  const EKAIROS_ROOT_DIR = "/vercel/sandbox/.ekairos";
14
24
  const EKAIROS_RUNTIME_MANIFEST_PATH = `${EKAIROS_ROOT_DIR}/runtime.json`;
15
25
  const EKAIROS_HTTP_HELPER_PATH = `${EKAIROS_ROOT_DIR}/instant-http.mjs`;
@@ -17,6 +27,8 @@ const EKAIROS_QUERY_SCRIPT_PATH = `${EKAIROS_ROOT_DIR}/query.mjs`;
17
27
  const CODEX_HOME_DIR = "/vercel/sandbox/.codex";
18
28
  const CODEX_SKILLS_DIR = `${CODEX_HOME_DIR}/skills`;
19
29
  const INSTANT_API_BASE_URL = "https://api.instantdb.com";
30
+ const SANDBOX_PROCESS_STREAM_VERSION = 1;
31
+ const SANDBOX_PROCESS_TERMINAL_STATUSES = new Set(["exited", "failed", "killed", "lost"]);
20
32
  function formatInstantSchemaError(err) {
21
33
  const base = err instanceof Error ? err.message : String(err);
22
34
  const body = err?.body;
@@ -49,9 +61,200 @@ function formatSandboxError(err) {
49
61
  return base;
50
62
  return `${base}: ${detail}`;
51
63
  }
64
+ function nowIso() {
65
+ return new Date().toISOString();
66
+ }
67
+ function asOptionalString(value) {
68
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
69
+ }
70
+ function sanitizeInstantString(value) {
71
+ return value.includes("\0") ? value.replace(/\0/g, "") : value;
72
+ }
73
+ function sanitizeInstantValue(value) {
74
+ if (typeof value === "string") {
75
+ return sanitizeInstantString(value);
76
+ }
77
+ if (Array.isArray(value)) {
78
+ return value.map((item) => sanitizeInstantValue(item));
79
+ }
80
+ if (value && typeof value === "object" && !(value instanceof Date)) {
81
+ const sanitized = {};
82
+ for (const [key, entry] of Object.entries(value)) {
83
+ sanitized[key] = sanitizeInstantValue(entry);
84
+ }
85
+ return sanitized;
86
+ }
87
+ return value;
88
+ }
89
+ function createSandboxProcessStreamClientId(processId) {
90
+ const normalized = String(processId ?? "").trim();
91
+ if (!normalized)
92
+ throw new Error("sandbox_process_id_required");
93
+ return `sandbox-process:${normalized}`;
94
+ }
95
+ function encodeSandboxProcessStreamChunk(chunk) {
96
+ return `${JSON.stringify(chunk)}\n`;
97
+ }
98
+ function parseSandboxProcessStreamChunk(value) {
99
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
100
+ if (!parsed || typeof parsed !== "object") {
101
+ throw new Error("invalid_sandbox_process_stream_chunk");
102
+ }
103
+ const record = parsed;
104
+ if (record.version !== SANDBOX_PROCESS_STREAM_VERSION) {
105
+ throw new Error(`invalid_sandbox_process_stream_version:${String(record.version)}`);
106
+ }
107
+ return record;
108
+ }
109
+ function resolveDbConfig(db) {
110
+ const config = db?.config ?? {};
111
+ const appId = String(config.appId ?? "").trim();
112
+ const adminToken = String(config.adminToken ?? "").trim();
113
+ if (!appId || !adminToken) {
114
+ throw new Error("sandbox_service_db_config_required");
115
+ }
116
+ return { appId, adminToken };
117
+ }
118
+ function createAdminDbFromConfig(config) {
119
+ return init({
120
+ appId: config.appId,
121
+ adminToken: config.adminToken,
122
+ schema: sandboxDomain.toInstantSchema(),
123
+ useDateObjects: true,
124
+ });
125
+ }
126
+ function sandboxProcessFinishedHookToken(processId) {
127
+ return `sandbox-process:${processId}:finished`;
128
+ }
129
+ async function resumeSandboxProcessHook(processId, payload) {
130
+ try {
131
+ const { resumeHook } = await import("workflow/api");
132
+ await resumeHook(sandboxProcessFinishedHookToken(processId), payload);
133
+ }
134
+ catch {
135
+ // No workflow may be listening; process metadata and streams remain the source of truth.
136
+ }
137
+ }
138
+ function commandResultFromProcessStream(params) {
139
+ const stdout = params.chunks
140
+ .filter((chunk) => chunk.type === "stdout")
141
+ .map((chunk) => String(chunk.data?.text ?? ""))
142
+ .join("");
143
+ const stderr = params.chunks
144
+ .filter((chunk) => chunk.type === "stderr" || chunk.type === "error")
145
+ .map((chunk) => String(chunk.data?.text ?? chunk.data?.message ?? ""))
146
+ .join("");
147
+ const exitChunk = [...params.chunks].reverse().find((chunk) => chunk.type === "exit");
148
+ const exitCode = Number(exitChunk?.data?.exitCode ?? params.processRow?.exitCode ?? 1);
149
+ const command = [
150
+ String(params.processRow?.command ?? ""),
151
+ ...(Array.isArray(params.processRow?.args) ? params.processRow.args : []),
152
+ ]
153
+ .filter(Boolean)
154
+ .join(" ");
155
+ return {
156
+ success: exitCode === 0,
157
+ exitCode,
158
+ output: stdout,
159
+ error: stderr,
160
+ command,
161
+ };
162
+ }
163
+ export class SandboxCommandRun {
164
+ constructor(data, service) {
165
+ this.service = null;
166
+ this.data = data;
167
+ this.service = service ?? null;
168
+ }
169
+ static [WORKFLOW_SERIALIZE](instance) {
170
+ return instance.data;
171
+ }
172
+ static [WORKFLOW_DESERIALIZE](data) {
173
+ return new SandboxCommandRun(data);
174
+ }
175
+ get sandboxId() {
176
+ return this.data.sandboxId;
177
+ }
178
+ get processId() {
179
+ return this.data.processId;
180
+ }
181
+ get streamId() {
182
+ return this.data.streamId;
183
+ }
184
+ get streamClientId() {
185
+ return this.data.streamClientId;
186
+ }
187
+ getService() {
188
+ if (!this.service) {
189
+ this.service = new SandboxService(createAdminDbFromConfig(this.data.db));
190
+ }
191
+ return this.service;
192
+ }
193
+ async readStream() {
194
+ "use step";
195
+ const stream = await this.getService().readProcessStream(this.processId);
196
+ if (!stream.ok)
197
+ throw new Error(stream.error);
198
+ return stream.data;
199
+ }
200
+ async snapshot() {
201
+ "use step";
202
+ const snapshot = await this.getService().getProcessSnapshot(this.processId);
203
+ if (!snapshot.ok)
204
+ throw new Error(snapshot.error);
205
+ return snapshot.data;
206
+ }
207
+ async wait(params) {
208
+ if (this.data.result)
209
+ return this.data.result;
210
+ const initial = await this.snapshot();
211
+ if (SANDBOX_PROCESS_TERMINAL_STATUSES.has(String(initial.status ?? ""))) {
212
+ const stream = await this.readStream();
213
+ const result = commandResultFromProcessStream({ processRow: initial, chunks: stream.chunks });
214
+ this.data.result = result;
215
+ return result;
216
+ }
217
+ try {
218
+ const { createHook } = await import("workflow");
219
+ const hook = createHook({
220
+ token: sandboxProcessFinishedHookToken(this.processId),
221
+ });
222
+ const result = await hook;
223
+ this.data.result = result;
224
+ return result;
225
+ }
226
+ catch {
227
+ // Outside workflow context, or if hooks are unavailable, poll the durable row.
228
+ }
229
+ const timeoutMs = Math.max(0, Number(params?.timeoutMs ?? 5 * 60 * 1000));
230
+ const pollMs = Math.max(50, Number(params?.pollMs ?? 500));
231
+ const deadline = Date.now() + timeoutMs;
232
+ while (Date.now() <= deadline) {
233
+ const row = await this.snapshot();
234
+ if (SANDBOX_PROCESS_TERMINAL_STATUSES.has(String(row.status ?? ""))) {
235
+ const stream = await this.readStream();
236
+ const result = commandResultFromProcessStream({ processRow: row, chunks: stream.chunks });
237
+ this.data.result = result;
238
+ return result;
239
+ }
240
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
241
+ }
242
+ throw new Error(`sandbox_process_wait_timeout:${this.processId}`);
243
+ }
244
+ then(onfulfilled, onrejected) {
245
+ return this.wait().then(onfulfilled, onrejected);
246
+ }
247
+ }
52
248
  export class SandboxService {
53
249
  constructor(db) {
54
250
  this.adminDb = db;
251
+ this.dbConfig = resolveDbConfig(db);
252
+ }
253
+ static [WORKFLOW_SERIALIZE](instance) {
254
+ return { db: instance.dbConfig };
255
+ }
256
+ static [WORKFLOW_DESERIALIZE](data) {
257
+ return new SandboxService(createAdminDbFromConfig(data.db));
55
258
  }
56
259
  static getVercelCredentials() {
57
260
  const teamId = String(process.env.SANDBOX_VERCEL_TEAM_ID ?? "").trim();
@@ -639,8 +842,8 @@ export class SandboxService {
639
842
  : "";
640
843
  return {
641
844
  exitCode: Number.isFinite(exitCode) ? exitCode : 0,
642
- stdout,
643
- stderr,
845
+ stdout: sanitizeInstantString(stdout),
846
+ stderr: sanitizeInstantString(stderr),
644
847
  };
645
848
  }
646
849
  static async spritesExec(params) {
@@ -829,6 +1032,7 @@ export class SandboxService {
829
1032
  return image;
830
1033
  }
831
1034
  async createSandbox(config) {
1035
+ "use step";
832
1036
  const sandboxId = id();
833
1037
  const now = Date.now();
834
1038
  const provider = SandboxService.resolveProvider(config);
@@ -977,7 +1181,7 @@ export class SandboxService {
977
1181
  ? sandbox.id
978
1182
  : provider === "sprites"
979
1183
  ? String(sandbox.name)
980
- : sandbox.sandboxId;
1184
+ : sandbox.name;
981
1185
  const sandboxUrl = provider === "sprites" ? sandbox.url : undefined;
982
1186
  const activateMutations = [
983
1187
  this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
@@ -1144,7 +1348,7 @@ export class SandboxService {
1144
1348
  const delayMs = 500;
1145
1349
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1146
1350
  const sandbox = await VercelSandbox.get({
1147
- sandboxId: String(record.externalSandboxId),
1351
+ name: String(record.externalSandboxId),
1148
1352
  teamId: creds.teamId,
1149
1353
  projectId: creds.projectId,
1150
1354
  token: creds.token,
@@ -1180,7 +1384,82 @@ export class SandboxService {
1180
1384
  });
1181
1385
  return recordResult?.sandbox_sandboxes?.[0] ?? null;
1182
1386
  }
1387
+ async getProcessSnapshot(processId) {
1388
+ "use step";
1389
+ try {
1390
+ const processResult = await this.adminDb.query({
1391
+ sandbox_processes: {
1392
+ $: { where: { id: processId }, limit: 1 },
1393
+ sandbox: {},
1394
+ },
1395
+ });
1396
+ const processRow = processResult?.sandbox_processes?.[0];
1397
+ if (!processRow)
1398
+ return { ok: false, error: "sandbox_process_not_found" };
1399
+ return { ok: true, data: processRow };
1400
+ }
1401
+ catch (e) {
1402
+ return { ok: false, error: formatInstantSchemaError(e) };
1403
+ }
1404
+ }
1405
+ async markOpenProcessesLost(sandboxId, reason) {
1406
+ try {
1407
+ const processResult = await this.adminDb.query({
1408
+ sandbox_processes: {
1409
+ $: {
1410
+ where: { "sandbox.id": sandboxId },
1411
+ limit: 500,
1412
+ },
1413
+ },
1414
+ });
1415
+ const rows = Array.isArray(processResult?.sandbox_processes)
1416
+ ? processResult.sandbox_processes
1417
+ : [];
1418
+ const now = Date.now();
1419
+ const txs = rows
1420
+ .filter((row) => !SANDBOX_PROCESS_TERMINAL_STATUSES.has(String(row?.status ?? "")))
1421
+ .map((row) => this.adminDb.tx.sandbox_processes[String(row.id)].update({
1422
+ status: "lost",
1423
+ streamFinishedAt: row.streamFinishedAt ?? now,
1424
+ streamAbortReason: reason,
1425
+ exitedAt: now,
1426
+ updatedAt: now,
1427
+ metadata: {
1428
+ ...(row.metadata ?? {}),
1429
+ lostReason: reason,
1430
+ },
1431
+ }));
1432
+ if (txs.length > 0) {
1433
+ await this.adminDb.transact(txs);
1434
+ }
1435
+ }
1436
+ catch {
1437
+ // Best-effort cleanup; stopping the sandbox should not fail because process metadata could not be marked.
1438
+ }
1439
+ }
1440
+ async createProcessStream(params) {
1441
+ const streams = this.adminDb?.streams;
1442
+ if (!streams?.createWriteStream) {
1443
+ throw new Error("sandbox_process_streams_unavailable");
1444
+ }
1445
+ const streamClientId = params.streamClientId || createSandboxProcessStreamClientId(params.processId);
1446
+ const stream = streams.createWriteStream({ clientId: streamClientId });
1447
+ const streamId = typeof stream.streamId === "function" ? await stream.streamId() : streamClientId;
1448
+ return { stream, streamId, streamClientId };
1449
+ }
1450
+ async writeProcessChunk(params) {
1451
+ await params.writer.write(encodeSandboxProcessStreamChunk({
1452
+ version: SANDBOX_PROCESS_STREAM_VERSION,
1453
+ at: nowIso(),
1454
+ seq: params.seq,
1455
+ type: params.type,
1456
+ sandboxId: params.sandboxId,
1457
+ processId: params.processId,
1458
+ ...(params.data ? { data: sanitizeInstantValue(params.data) } : {}),
1459
+ }));
1460
+ }
1183
1461
  async stopSandbox(sandboxId) {
1462
+ "use step";
1184
1463
  try {
1185
1464
  const result = await this.reconnectToSandbox(sandboxId);
1186
1465
  const recordResult = await this.adminDb.query({
@@ -1195,7 +1474,7 @@ export class SandboxService {
1195
1474
  if (result.ok) {
1196
1475
  try {
1197
1476
  const sandbox = result.data.sandbox;
1198
- if (sandbox?.sandboxId) {
1477
+ if (isVercelSandbox(sandbox)) {
1199
1478
  await sandbox.stop();
1200
1479
  }
1201
1480
  else if (sandbox?.__provider === "sprites") {
@@ -1231,6 +1510,7 @@ export class SandboxService {
1231
1510
  shutdownAt: Date.now(),
1232
1511
  updatedAt: Date.now(),
1233
1512
  }));
1513
+ await this.markOpenProcessesLost(sandboxId, "sandbox_stopped");
1234
1514
  return { ok: true, data: undefined };
1235
1515
  }
1236
1516
  catch (e) {
@@ -1273,12 +1553,13 @@ export class SandboxService {
1273
1553
  }
1274
1554
  }
1275
1555
  async runCommand(sandboxId, command, args = []) {
1556
+ "use step";
1276
1557
  try {
1277
1558
  const sandboxResult = await this.reconnectToSandbox(sandboxId);
1278
1559
  if (!sandboxResult.ok)
1279
1560
  return { ok: false, error: sandboxResult.error };
1280
1561
  const sandbox = sandboxResult.data.sandbox;
1281
- if (sandbox.sandboxId) {
1562
+ if (isVercelSandbox(sandbox)) {
1282
1563
  const result = await runCommandInSandbox(sandbox, command, args);
1283
1564
  return { ok: true, data: result };
1284
1565
  }
@@ -1319,13 +1600,283 @@ export class SandboxService {
1319
1600
  return { ok: false, error: formatInstantSchemaError(e) };
1320
1601
  }
1321
1602
  }
1603
+ async runCommandProcess(sandboxId, command, args = [], opts) {
1604
+ "use step";
1605
+ const processId = id();
1606
+ const now = Date.now();
1607
+ let writer = null;
1608
+ let stream = null;
1609
+ let seq = 0;
1610
+ try {
1611
+ const record = await this.getSandboxRecord(sandboxId);
1612
+ if (!record)
1613
+ return { ok: false, error: "Valid sandbox record not found" };
1614
+ if (record.status !== "active")
1615
+ return { ok: false, error: `sandbox_not_active:${record.status}` };
1616
+ const streamSession = await this.createProcessStream({ sandboxId, processId });
1617
+ stream = streamSession.stream;
1618
+ writer = stream.getWriter();
1619
+ await this.adminDb.transact([
1620
+ this.adminDb.tx.sandbox_processes[processId]
1621
+ .update({
1622
+ kind: opts?.kind ?? "command",
1623
+ mode: opts?.mode ?? "foreground",
1624
+ status: "running",
1625
+ provider: String(record.provider ?? "unknown"),
1626
+ command: sanitizeInstantString(command),
1627
+ args: sanitizeInstantValue(Array.isArray(args) ? args : []),
1628
+ cwd: asOptionalString(opts?.cwd),
1629
+ env: sanitizeInstantValue(opts?.env),
1630
+ streamId: streamSession.streamId,
1631
+ streamClientId: streamSession.streamClientId,
1632
+ streamStartedAt: now,
1633
+ startedAt: now,
1634
+ updatedAt: now,
1635
+ metadata: sanitizeInstantValue(opts?.metadata),
1636
+ })
1637
+ .link({ sandbox: sandboxId, stream: streamSession.streamId }),
1638
+ ]);
1639
+ seq += 1;
1640
+ await this.writeProcessChunk({
1641
+ writer,
1642
+ sandboxId,
1643
+ processId,
1644
+ seq,
1645
+ type: "status",
1646
+ data: {
1647
+ status: "running",
1648
+ command,
1649
+ args: Array.isArray(args) ? args : [],
1650
+ cwd: opts?.cwd ?? null,
1651
+ },
1652
+ });
1653
+ const result = await this.runCommand(sandboxId, command, args);
1654
+ const finishedAt = Date.now();
1655
+ let finalResult;
1656
+ let status;
1657
+ let exitCode;
1658
+ let errorText;
1659
+ if (result.ok) {
1660
+ finalResult = result.data;
1661
+ exitCode = Number(result.data.exitCode ?? (result.data.success === false ? 1 : 0));
1662
+ status = exitCode === 0 ? "exited" : "failed";
1663
+ const stdout = String(result.data.stdout ?? result.data.output ?? "");
1664
+ const stderr = String(result.data.stderr ?? result.data.error ?? "");
1665
+ if (stdout) {
1666
+ seq += 1;
1667
+ await this.writeProcessChunk({
1668
+ writer,
1669
+ sandboxId,
1670
+ processId,
1671
+ seq,
1672
+ type: "stdout",
1673
+ data: { text: stdout },
1674
+ });
1675
+ }
1676
+ if (stderr) {
1677
+ seq += 1;
1678
+ await this.writeProcessChunk({
1679
+ writer,
1680
+ sandboxId,
1681
+ processId,
1682
+ seq,
1683
+ type: "stderr",
1684
+ data: { text: stderr },
1685
+ });
1686
+ }
1687
+ }
1688
+ else {
1689
+ exitCode = 1;
1690
+ status = "failed";
1691
+ errorText = result.error;
1692
+ finalResult = {
1693
+ success: false,
1694
+ exitCode,
1695
+ output: "",
1696
+ error: result.error,
1697
+ command: [command, ...(Array.isArray(args) ? args : [])].join(" "),
1698
+ };
1699
+ seq += 1;
1700
+ await this.writeProcessChunk({
1701
+ writer,
1702
+ sandboxId,
1703
+ processId,
1704
+ seq,
1705
+ type: "error",
1706
+ data: { message: result.error },
1707
+ });
1708
+ }
1709
+ seq += 1;
1710
+ await this.writeProcessChunk({
1711
+ writer,
1712
+ sandboxId,
1713
+ processId,
1714
+ seq,
1715
+ type: "exit",
1716
+ data: { exitCode, status },
1717
+ });
1718
+ await writer.close();
1719
+ writer = null;
1720
+ await this.adminDb.transact([
1721
+ this.adminDb.tx.sandbox_processes[processId].update({
1722
+ status,
1723
+ exitCode,
1724
+ streamFinishedAt: finishedAt,
1725
+ streamAbortReason: null,
1726
+ exitedAt: finishedAt,
1727
+ updatedAt: finishedAt,
1728
+ metadata: sanitizeInstantValue({
1729
+ ...(opts?.metadata ?? {}),
1730
+ ...(errorText ? { error: errorText } : {}),
1731
+ chunkCount: seq,
1732
+ result: finalResult,
1733
+ }),
1734
+ }),
1735
+ ]);
1736
+ await resumeSandboxProcessHook(processId, finalResult);
1737
+ return {
1738
+ ok: true,
1739
+ data: new SandboxCommandRun({
1740
+ db: this.dbConfig,
1741
+ sandboxId,
1742
+ processId,
1743
+ streamId: streamSession.streamId,
1744
+ streamClientId: streamSession.streamClientId,
1745
+ result: finalResult,
1746
+ }, this),
1747
+ };
1748
+ }
1749
+ catch (e) {
1750
+ const message = formatInstantSchemaError(e);
1751
+ const failedAt = Date.now();
1752
+ try {
1753
+ if (writer) {
1754
+ seq += 1;
1755
+ await this.writeProcessChunk({
1756
+ writer,
1757
+ sandboxId,
1758
+ processId,
1759
+ seq,
1760
+ type: "error",
1761
+ data: { message },
1762
+ });
1763
+ await writer.abort(message);
1764
+ writer = null;
1765
+ }
1766
+ else if (stream) {
1767
+ await stream.abort(message);
1768
+ }
1769
+ }
1770
+ catch {
1771
+ // ignore stream cleanup errors
1772
+ }
1773
+ try {
1774
+ const finalResult = {
1775
+ success: false,
1776
+ exitCode: 1,
1777
+ output: "",
1778
+ error: message,
1779
+ command: [command, ...(Array.isArray(args) ? args : [])].join(" "),
1780
+ };
1781
+ await this.adminDb.transact([
1782
+ this.adminDb.tx.sandbox_processes[processId].update({
1783
+ status: "failed",
1784
+ streamFinishedAt: failedAt,
1785
+ streamAbortReason: message,
1786
+ exitedAt: failedAt,
1787
+ updatedAt: failedAt,
1788
+ metadata: sanitizeInstantValue({
1789
+ ...(opts?.metadata ?? {}),
1790
+ error: message,
1791
+ result: finalResult,
1792
+ }),
1793
+ }),
1794
+ ]);
1795
+ await resumeSandboxProcessHook(processId, finalResult);
1796
+ }
1797
+ catch {
1798
+ // ignore partial metadata failures
1799
+ }
1800
+ return { ok: false, error: message };
1801
+ }
1802
+ finally {
1803
+ try {
1804
+ writer?.releaseLock();
1805
+ }
1806
+ catch {
1807
+ // ignore
1808
+ }
1809
+ }
1810
+ }
1811
+ async runCommandWithProcessStream(sandboxId, command, args = [], opts) {
1812
+ const run = await this.runCommandProcess(sandboxId, command, args, opts);
1813
+ if (!run.ok)
1814
+ return run;
1815
+ const result = await run.data;
1816
+ return {
1817
+ ok: true,
1818
+ data: {
1819
+ processId: run.data.processId,
1820
+ streamId: run.data.streamId,
1821
+ streamClientId: run.data.streamClientId,
1822
+ result,
1823
+ },
1824
+ };
1825
+ }
1826
+ async readProcessStream(processId) {
1827
+ "use step";
1828
+ try {
1829
+ const processResult = await this.adminDb.query({
1830
+ sandbox_processes: {
1831
+ $: { where: { id: processId }, limit: 1 },
1832
+ },
1833
+ });
1834
+ const processRow = processResult?.sandbox_processes?.[0];
1835
+ if (!processRow)
1836
+ return { ok: false, error: "sandbox_process_not_found" };
1837
+ const streams = this.adminDb?.streams;
1838
+ if (!streams?.createReadStream)
1839
+ return { ok: false, error: "sandbox_process_streams_unavailable" };
1840
+ const clientId = String(processRow.streamClientId ?? "").trim() || undefined;
1841
+ const streamId = String(processRow.streamId ?? "").trim() || undefined;
1842
+ if (!clientId && !streamId)
1843
+ return { ok: false, error: "sandbox_process_stream_missing" };
1844
+ const stream = streams.createReadStream({ clientId, streamId });
1845
+ const chunks = [];
1846
+ let byteOffset = 0;
1847
+ let buffer = "";
1848
+ for await (const raw of stream) {
1849
+ const encoded = typeof raw === "string" ? raw : String(raw ?? "");
1850
+ if (!encoded)
1851
+ continue;
1852
+ byteOffset += new TextEncoder().encode(encoded).length;
1853
+ buffer += encoded;
1854
+ const lines = buffer.split("\n");
1855
+ buffer = lines.pop() ?? "";
1856
+ for (const line of lines) {
1857
+ const trimmed = line.trim();
1858
+ if (!trimmed)
1859
+ continue;
1860
+ chunks.push(parseSandboxProcessStreamChunk(trimmed));
1861
+ }
1862
+ }
1863
+ const trailing = buffer.trim();
1864
+ if (trailing)
1865
+ chunks.push(parseSandboxProcessStreamChunk(trailing));
1866
+ return { ok: true, data: { chunks, byteOffset } };
1867
+ }
1868
+ catch (e) {
1869
+ return { ok: false, error: formatInstantSchemaError(e) };
1870
+ }
1871
+ }
1322
1872
  async writeFiles(sandboxId, files) {
1873
+ "use step";
1323
1874
  try {
1324
1875
  const sandboxResult = await this.reconnectToSandbox(sandboxId);
1325
1876
  if (!sandboxResult.ok)
1326
1877
  return { ok: false, error: sandboxResult.error };
1327
1878
  const sandbox = sandboxResult.data.sandbox;
1328
- if (sandbox.sandboxId) {
1879
+ if (isVercelSandbox(sandbox)) {
1329
1880
  await sandbox.writeFiles(files.map((f) => ({
1330
1881
  path: f.path,
1331
1882
  content: Buffer.from(f.contentBase64, "base64"),
@@ -1362,12 +1913,13 @@ export class SandboxService {
1362
1913
  }
1363
1914
  }
1364
1915
  async readFile(sandboxId, path) {
1916
+ "use step";
1365
1917
  try {
1366
1918
  const sandboxResult = await this.reconnectToSandbox(sandboxId);
1367
1919
  if (!sandboxResult.ok)
1368
1920
  return { ok: false, error: sandboxResult.error };
1369
1921
  const sandbox = sandboxResult.data.sandbox;
1370
- if (sandbox.sandboxId) {
1922
+ if (isVercelSandbox(sandbox)) {
1371
1923
  const stream = await sandbox.readFile({ path });
1372
1924
  if (!stream) {
1373
1925
  return { ok: true, data: { contentBase64: "" } };