@ekairos/sandbox 1.22.33-beta.development.0 → 1.22.35-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/README.md +59 -452
- package/dist/app.js +1 -1
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +7 -1
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +170 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +290 -2
- package/dist/schema.js.map +1 -1
- package/dist/service.d.ts +89 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +818 -25
- package/dist/service.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vercel-options.d.ts +21 -0
- package/dist/vercel-options.d.ts.map +1 -0
- package/dist/vercel-options.js +149 -0
- package/dist/vercel-options.js.map +1 -0
- package/package.json +11 -7
package/dist/service.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
4
|
import { resolveRuntime } from "@ekairos/domain/runtime";
|
|
5
5
|
import { runCommandInSandbox } from "./commands.js";
|
|
6
|
+
import { resolveVercelSandboxConfig, safeVercelConfigForRecord, } from "./vercel-options.js";
|
|
6
7
|
import { execFile } from "node:child_process";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
8
9
|
import { existsSync, promises as fs } from "node:fs";
|
|
@@ -10,6 +11,14 @@ import os from "node:os";
|
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { promisify } from "node:util";
|
|
12
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
|
+
}
|
|
13
22
|
const EKAIROS_ROOT_DIR = "/vercel/sandbox/.ekairos";
|
|
14
23
|
const EKAIROS_RUNTIME_MANIFEST_PATH = `${EKAIROS_ROOT_DIR}/runtime.json`;
|
|
15
24
|
const EKAIROS_HTTP_HELPER_PATH = `${EKAIROS_ROOT_DIR}/instant-http.mjs`;
|
|
@@ -17,6 +26,8 @@ const EKAIROS_QUERY_SCRIPT_PATH = `${EKAIROS_ROOT_DIR}/query.mjs`;
|
|
|
17
26
|
const CODEX_HOME_DIR = "/vercel/sandbox/.codex";
|
|
18
27
|
const CODEX_SKILLS_DIR = `${CODEX_HOME_DIR}/skills`;
|
|
19
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"]);
|
|
20
31
|
function formatInstantSchemaError(err) {
|
|
21
32
|
const base = err instanceof Error ? err.message : String(err);
|
|
22
33
|
const body = err?.body;
|
|
@@ -49,6 +60,165 @@ function formatSandboxError(err) {
|
|
|
49
60
|
return base;
|
|
50
61
|
return `${base}: ${detail}`;
|
|
51
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
|
+
}
|
|
52
222
|
export class SandboxService {
|
|
53
223
|
constructor(db) {
|
|
54
224
|
this.adminDb = db;
|
|
@@ -442,16 +612,41 @@ export class SandboxService {
|
|
|
442
612
|
}
|
|
443
613
|
static async provisionVercelSandbox(config, extra) {
|
|
444
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
|
+
}
|
|
445
634
|
return await VercelSandbox.create({
|
|
446
635
|
teamId: creds.teamId,
|
|
447
636
|
projectId: creds.projectId,
|
|
448
637
|
token: creds.token,
|
|
449
|
-
|
|
450
|
-
|
|
638
|
+
...(resolved.name ? { name: resolved.name } : {}),
|
|
639
|
+
timeout: resolved.timeoutMs,
|
|
640
|
+
ports: resolved.ports,
|
|
451
641
|
// IMPORTANT: pass runtime as-is (e.g. "python3.13") to match provider expectations.
|
|
452
642
|
// Don't normalize to "python3"/"node22" as that can cause provider-side 400s.
|
|
453
|
-
runtime:
|
|
454
|
-
resources: { vcpus:
|
|
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 } : {}),
|
|
455
650
|
networkPolicy: extra?.networkPolicy,
|
|
456
651
|
env: extra?.env,
|
|
457
652
|
});
|
|
@@ -639,8 +834,8 @@ export class SandboxService {
|
|
|
639
834
|
: "";
|
|
640
835
|
return {
|
|
641
836
|
exitCode: Number.isFinite(exitCode) ? exitCode : 0,
|
|
642
|
-
stdout,
|
|
643
|
-
stderr,
|
|
837
|
+
stdout: sanitizeInstantString(stdout),
|
|
838
|
+
stderr: sanitizeInstantString(stderr),
|
|
644
839
|
};
|
|
645
840
|
}
|
|
646
841
|
static async spritesExec(params) {
|
|
@@ -832,6 +1027,7 @@ export class SandboxService {
|
|
|
832
1027
|
const sandboxId = id();
|
|
833
1028
|
const now = Date.now();
|
|
834
1029
|
const provider = SandboxService.resolveProvider(config);
|
|
1030
|
+
const resolvedVercel = provider === "vercel" ? resolveVercelSandboxConfig(config, { sandboxId }) : undefined;
|
|
835
1031
|
let daytonaEphemeral = undefined;
|
|
836
1032
|
let installedSkills = [];
|
|
837
1033
|
try {
|
|
@@ -841,13 +1037,14 @@ export class SandboxService {
|
|
|
841
1037
|
status: "creating",
|
|
842
1038
|
...(ekairos ? { sandboxUserId: ekairos.sandboxUserId } : {}),
|
|
843
1039
|
provider,
|
|
844
|
-
timeout: config.timeoutMs,
|
|
845
|
-
runtime: config.runtime,
|
|
846
|
-
vcpus: config.resources?.vcpus,
|
|
847
|
-
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),
|
|
848
1044
|
purpose: config.purpose,
|
|
849
1045
|
params: {
|
|
850
1046
|
...baseParams,
|
|
1047
|
+
...(resolvedVercel ? { vercel: safeVercelConfigForRecord(config, resolvedVercel) } : {}),
|
|
851
1048
|
...(ekairos
|
|
852
1049
|
? {
|
|
853
1050
|
ekairos: {
|
|
@@ -947,6 +1144,7 @@ export class SandboxService {
|
|
|
947
1144
|
sandbox = await SandboxService.provisionVercelSandbox(config, {
|
|
948
1145
|
networkPolicy: ekairos?.networkPolicy,
|
|
949
1146
|
env: Object.keys(vercelEnv).length > 0 ? vercelEnv : undefined,
|
|
1147
|
+
resolved: resolvedVercel,
|
|
950
1148
|
});
|
|
951
1149
|
if (ekairos) {
|
|
952
1150
|
await SandboxService.bootstrapEkairosFiles(sandbox, ekairos.manifest);
|
|
@@ -960,7 +1158,10 @@ export class SandboxService {
|
|
|
960
1158
|
const msg = formatSandboxError(e);
|
|
961
1159
|
if (sandbox && provider === "vercel") {
|
|
962
1160
|
try {
|
|
963
|
-
await sandbox.stop();
|
|
1161
|
+
await sandbox.stop({ blocking: true });
|
|
1162
|
+
if (resolvedVercel?.deleteOnStop) {
|
|
1163
|
+
await sandbox.delete();
|
|
1164
|
+
}
|
|
964
1165
|
}
|
|
965
1166
|
catch {
|
|
966
1167
|
// ignore cleanup errors during failed bootstrap
|
|
@@ -977,7 +1178,7 @@ export class SandboxService {
|
|
|
977
1178
|
? sandbox.id
|
|
978
1179
|
: provider === "sprites"
|
|
979
1180
|
? String(sandbox.name)
|
|
980
|
-
: sandbox.
|
|
1181
|
+
: sandbox.name;
|
|
981
1182
|
const sandboxUrl = provider === "sprites" ? sandbox.url : undefined;
|
|
982
1183
|
const activateMutations = [
|
|
983
1184
|
this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
|
|
@@ -1009,10 +1210,7 @@ export class SandboxService {
|
|
|
1009
1210
|
: {}),
|
|
1010
1211
|
...(provider === "vercel"
|
|
1011
1212
|
? {
|
|
1012
|
-
vercel: {
|
|
1013
|
-
...baseParams?.vercel,
|
|
1014
|
-
...(config.vercel ?? {}),
|
|
1015
|
-
},
|
|
1213
|
+
vercel: resolvedVercel ? safeVercelConfigForRecord(config, resolvedVercel) : {},
|
|
1016
1214
|
}
|
|
1017
1215
|
: {}),
|
|
1018
1216
|
...(provider === "daytona"
|
|
@@ -1144,7 +1342,7 @@ export class SandboxService {
|
|
|
1144
1342
|
const delayMs = 500;
|
|
1145
1343
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1146
1344
|
const sandbox = await VercelSandbox.get({
|
|
1147
|
-
|
|
1345
|
+
name: String(record.externalSandboxId),
|
|
1148
1346
|
teamId: creds.teamId,
|
|
1149
1347
|
projectId: creds.projectId,
|
|
1150
1348
|
token: creds.token,
|
|
@@ -1180,6 +1378,254 @@ export class SandboxService {
|
|
|
1180
1378
|
});
|
|
1181
1379
|
return recordResult?.sandbox_sandboxes?.[0] ?? null;
|
|
1182
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
|
+
}
|
|
1183
1629
|
async stopSandbox(sandboxId) {
|
|
1184
1630
|
try {
|
|
1185
1631
|
const result = await this.reconnectToSandbox(sandboxId);
|
|
@@ -1190,13 +1636,19 @@ export class SandboxService {
|
|
|
1190
1636
|
const deleteOnStop = record?.provider === "sprites"
|
|
1191
1637
|
? SandboxService.parseOptionalBoolean(process.env.SANDBOX_SPRITES_DELETE_ON_STOP) ??
|
|
1192
1638
|
Boolean(record?.params?.sprites?.deleteOnStop ?? true)
|
|
1193
|
-
:
|
|
1194
|
-
|
|
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);
|
|
1195
1644
|
if (result.ok) {
|
|
1196
1645
|
try {
|
|
1197
1646
|
const sandbox = result.data.sandbox;
|
|
1198
|
-
if (sandbox
|
|
1199
|
-
await sandbox.stop();
|
|
1647
|
+
if (isVercelSandbox(sandbox)) {
|
|
1648
|
+
await sandbox.stop({ blocking: true });
|
|
1649
|
+
if (deleteOnStop) {
|
|
1650
|
+
await sandbox.delete();
|
|
1651
|
+
}
|
|
1200
1652
|
}
|
|
1201
1653
|
else if (sandbox?.__provider === "sprites") {
|
|
1202
1654
|
// Sprites does not have a reliable "stop" semantic; deleting is the durable cleanup primitive.
|
|
@@ -1231,6 +1683,7 @@ export class SandboxService {
|
|
|
1231
1683
|
shutdownAt: Date.now(),
|
|
1232
1684
|
updatedAt: Date.now(),
|
|
1233
1685
|
}));
|
|
1686
|
+
await this.markOpenProcessesLost(sandboxId, "sandbox_stopped");
|
|
1234
1687
|
return { ok: true, data: undefined };
|
|
1235
1688
|
}
|
|
1236
1689
|
catch (e) {
|
|
@@ -1278,7 +1731,7 @@ export class SandboxService {
|
|
|
1278
1731
|
if (!sandboxResult.ok)
|
|
1279
1732
|
return { ok: false, error: sandboxResult.error };
|
|
1280
1733
|
const sandbox = sandboxResult.data.sandbox;
|
|
1281
|
-
if (sandbox
|
|
1734
|
+
if (isVercelSandbox(sandbox)) {
|
|
1282
1735
|
const result = await runCommandInSandbox(sandbox, command, args);
|
|
1283
1736
|
return { ok: true, data: result };
|
|
1284
1737
|
}
|
|
@@ -1319,13 +1772,279 @@ export class SandboxService {
|
|
|
1319
1772
|
return { ok: false, error: formatInstantSchemaError(e) };
|
|
1320
1773
|
}
|
|
1321
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
|
+
}
|
|
1322
2041
|
async writeFiles(sandboxId, files) {
|
|
1323
2042
|
try {
|
|
1324
2043
|
const sandboxResult = await this.reconnectToSandbox(sandboxId);
|
|
1325
2044
|
if (!sandboxResult.ok)
|
|
1326
2045
|
return { ok: false, error: sandboxResult.error };
|
|
1327
2046
|
const sandbox = sandboxResult.data.sandbox;
|
|
1328
|
-
if (sandbox
|
|
2047
|
+
if (isVercelSandbox(sandbox)) {
|
|
1329
2048
|
await sandbox.writeFiles(files.map((f) => ({
|
|
1330
2049
|
path: f.path,
|
|
1331
2050
|
content: Buffer.from(f.contentBase64, "base64"),
|
|
@@ -1367,7 +2086,7 @@ export class SandboxService {
|
|
|
1367
2086
|
if (!sandboxResult.ok)
|
|
1368
2087
|
return { ok: false, error: sandboxResult.error };
|
|
1369
2088
|
const sandbox = sandboxResult.data.sandbox;
|
|
1370
|
-
if (sandbox
|
|
2089
|
+
if (isVercelSandbox(sandbox)) {
|
|
1371
2090
|
const stream = await sandbox.readFile({ path });
|
|
1372
2091
|
if (!stream) {
|
|
1373
2092
|
return { ok: true, data: { contentBase64: "" } };
|
|
@@ -1409,6 +2128,38 @@ export class SandboxService {
|
|
|
1409
2128
|
return { ok: false, error: formatInstantSchemaError(e) };
|
|
1410
2129
|
}
|
|
1411
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
|
+
}
|
|
1412
2163
|
static parseSpritesCheckpointIdFromNdjson(text) {
|
|
1413
2164
|
const lines = String(text ?? "")
|
|
1414
2165
|
.split("\n")
|
|
@@ -1440,6 +2191,33 @@ export class SandboxService {
|
|
|
1440
2191
|
sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 } },
|
|
1441
2192
|
});
|
|
1442
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
|
+
}
|
|
1443
2221
|
if (!record?.externalSandboxId || record.provider !== "sprites") {
|
|
1444
2222
|
return { ok: false, error: "checkpoint_not_supported" };
|
|
1445
2223
|
}
|
|
@@ -1481,6 +2259,21 @@ export class SandboxService {
|
|
|
1481
2259
|
sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 } },
|
|
1482
2260
|
});
|
|
1483
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
|
+
}
|
|
1484
2277
|
if (!record?.externalSandboxId || record.provider !== "sprites") {
|
|
1485
2278
|
return { ok: false, error: "checkpoint_not_supported" };
|
|
1486
2279
|
}
|