@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/app.js +1 -1
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +46 -0
- package/dist/schema.js.map +1 -1
- package/dist/service.d.ts +82 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +561 -9
- package/dist/service.js.map +1 -1
- package/package.json +7 -5
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1922
|
+
if (isVercelSandbox(sandbox)) {
|
|
1371
1923
|
const stream = await sandbox.readFile({ path });
|
|
1372
1924
|
if (!stream) {
|
|
1373
1925
|
return { ok: true, data: { contentBase64: "" } };
|