@ekairos/sandbox 1.22.15-beta.development.0 → 1.22.16-beta.development.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -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/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 +35 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +35 -0
- package/dist/schema.js.map +1 -1
- package/dist/service.d.ts +19 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +269 -17
- 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 +2 -2
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";
|
|
@@ -611,16 +612,41 @@ export class SandboxService {
|
|
|
611
612
|
}
|
|
612
613
|
static async provisionVercelSandbox(config, extra) {
|
|
613
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
|
+
}
|
|
614
634
|
return await VercelSandbox.create({
|
|
615
635
|
teamId: creds.teamId,
|
|
616
636
|
projectId: creds.projectId,
|
|
617
637
|
token: creds.token,
|
|
618
|
-
|
|
619
|
-
|
|
638
|
+
...(resolved.name ? { name: resolved.name } : {}),
|
|
639
|
+
timeout: resolved.timeoutMs,
|
|
640
|
+
ports: resolved.ports,
|
|
620
641
|
// IMPORTANT: pass runtime as-is (e.g. "python3.13") to match provider expectations.
|
|
621
642
|
// Don't normalize to "python3"/"node22" as that can cause provider-side 400s.
|
|
622
|
-
runtime:
|
|
623
|
-
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 } : {}),
|
|
624
650
|
networkPolicy: extra?.networkPolicy,
|
|
625
651
|
env: extra?.env,
|
|
626
652
|
});
|
|
@@ -1001,6 +1027,7 @@ export class SandboxService {
|
|
|
1001
1027
|
const sandboxId = id();
|
|
1002
1028
|
const now = Date.now();
|
|
1003
1029
|
const provider = SandboxService.resolveProvider(config);
|
|
1030
|
+
const resolvedVercel = provider === "vercel" ? resolveVercelSandboxConfig(config, { sandboxId }) : undefined;
|
|
1004
1031
|
let daytonaEphemeral = undefined;
|
|
1005
1032
|
let installedSkills = [];
|
|
1006
1033
|
try {
|
|
@@ -1010,13 +1037,14 @@ export class SandboxService {
|
|
|
1010
1037
|
status: "creating",
|
|
1011
1038
|
...(ekairos ? { sandboxUserId: ekairos.sandboxUserId } : {}),
|
|
1012
1039
|
provider,
|
|
1013
|
-
timeout: config.timeoutMs,
|
|
1014
|
-
runtime: config.runtime,
|
|
1015
|
-
vcpus: config.resources?.vcpus,
|
|
1016
|
-
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),
|
|
1017
1044
|
purpose: config.purpose,
|
|
1018
1045
|
params: {
|
|
1019
1046
|
...baseParams,
|
|
1047
|
+
...(resolvedVercel ? { vercel: safeVercelConfigForRecord(config, resolvedVercel) } : {}),
|
|
1020
1048
|
...(ekairos
|
|
1021
1049
|
? {
|
|
1022
1050
|
ekairos: {
|
|
@@ -1116,6 +1144,7 @@ export class SandboxService {
|
|
|
1116
1144
|
sandbox = await SandboxService.provisionVercelSandbox(config, {
|
|
1117
1145
|
networkPolicy: ekairos?.networkPolicy,
|
|
1118
1146
|
env: Object.keys(vercelEnv).length > 0 ? vercelEnv : undefined,
|
|
1147
|
+
resolved: resolvedVercel,
|
|
1119
1148
|
});
|
|
1120
1149
|
if (ekairos) {
|
|
1121
1150
|
await SandboxService.bootstrapEkairosFiles(sandbox, ekairos.manifest);
|
|
@@ -1129,7 +1158,10 @@ export class SandboxService {
|
|
|
1129
1158
|
const msg = formatSandboxError(e);
|
|
1130
1159
|
if (sandbox && provider === "vercel") {
|
|
1131
1160
|
try {
|
|
1132
|
-
await sandbox.stop();
|
|
1161
|
+
await sandbox.stop({ blocking: true });
|
|
1162
|
+
if (resolvedVercel?.deleteOnStop) {
|
|
1163
|
+
await sandbox.delete();
|
|
1164
|
+
}
|
|
1133
1165
|
}
|
|
1134
1166
|
catch {
|
|
1135
1167
|
// ignore cleanup errors during failed bootstrap
|
|
@@ -1178,10 +1210,7 @@ export class SandboxService {
|
|
|
1178
1210
|
: {}),
|
|
1179
1211
|
...(provider === "vercel"
|
|
1180
1212
|
? {
|
|
1181
|
-
vercel: {
|
|
1182
|
-
...baseParams?.vercel,
|
|
1183
|
-
...(config.vercel ?? {}),
|
|
1184
|
-
},
|
|
1213
|
+
vercel: resolvedVercel ? safeVercelConfigForRecord(config, resolvedVercel) : {},
|
|
1185
1214
|
}
|
|
1186
1215
|
: {}),
|
|
1187
1216
|
...(provider === "daytona"
|
|
@@ -1422,6 +1451,181 @@ export class SandboxService {
|
|
|
1422
1451
|
...(params.data ? { data: sanitizeInstantValue(params.data) } : {}),
|
|
1423
1452
|
}));
|
|
1424
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
|
+
}
|
|
1425
1629
|
async stopSandbox(sandboxId) {
|
|
1426
1630
|
try {
|
|
1427
1631
|
const result = await this.reconnectToSandbox(sandboxId);
|
|
@@ -1432,13 +1636,19 @@ export class SandboxService {
|
|
|
1432
1636
|
const deleteOnStop = record?.provider === "sprites"
|
|
1433
1637
|
? SandboxService.parseOptionalBoolean(process.env.SANDBOX_SPRITES_DELETE_ON_STOP) ??
|
|
1434
1638
|
Boolean(record?.params?.sprites?.deleteOnStop ?? true)
|
|
1435
|
-
:
|
|
1436
|
-
|
|
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);
|
|
1437
1644
|
if (result.ok) {
|
|
1438
1645
|
try {
|
|
1439
1646
|
const sandbox = result.data.sandbox;
|
|
1440
1647
|
if (isVercelSandbox(sandbox)) {
|
|
1441
|
-
await sandbox.stop();
|
|
1648
|
+
await sandbox.stop({ blocking: true });
|
|
1649
|
+
if (deleteOnStop) {
|
|
1650
|
+
await sandbox.delete();
|
|
1651
|
+
}
|
|
1442
1652
|
}
|
|
1443
1653
|
else if (sandbox?.__provider === "sprites") {
|
|
1444
1654
|
// Sprites does not have a reliable "stop" semantic; deleting is the durable cleanup primitive.
|
|
@@ -1981,6 +2191,33 @@ export class SandboxService {
|
|
|
1981
2191
|
sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 } },
|
|
1982
2192
|
});
|
|
1983
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
|
+
}
|
|
1984
2221
|
if (!record?.externalSandboxId || record.provider !== "sprites") {
|
|
1985
2222
|
return { ok: false, error: "checkpoint_not_supported" };
|
|
1986
2223
|
}
|
|
@@ -2022,6 +2259,21 @@ export class SandboxService {
|
|
|
2022
2259
|
sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 } },
|
|
2023
2260
|
});
|
|
2024
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
|
+
}
|
|
2025
2277
|
if (!record?.externalSandboxId || record.provider !== "sprites") {
|
|
2026
2278
|
return { ok: false, error: "checkpoint_not_supported" };
|
|
2027
2279
|
}
|