@ekairos/sandbox 1.22.35-beta.development.0 → 1.22.35
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/action-steps.d.ts +156 -0
- package/dist/action-steps.d.ts.map +1 -0
- package/dist/action-steps.js +153 -0
- package/dist/action-steps.js.map +1 -0
- package/dist/actions.d.ts +263 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +208 -0
- package/dist/actions.js.map +1 -0
- package/dist/contract.d.ts +86 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +83 -0
- package/dist/contract.js.map +1 -0
- package/dist/domain.d.ts +2 -0
- package/dist/domain.d.ts.map +1 -0
- package/dist/domain.js +2 -0
- package/dist/domain.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/daytona.d.ts +14 -0
- package/dist/providers/daytona.d.ts.map +1 -0
- package/dist/providers/daytona.js +153 -0
- package/dist/providers/daytona.js.map +1 -0
- package/dist/providers/provider.d.ts +3 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +18 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/sprites.d.ts +39 -0
- package/dist/providers/sprites.d.ts.map +1 -0
- package/dist/providers/sprites.js +234 -0
- package/dist/providers/sprites.js.map +1 -0
- package/dist/providers/types.d.ts +15 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +9 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/vercel.d.ts +26 -0
- package/dist/providers/vercel.d.ts.map +1 -0
- package/dist/providers/vercel.js +182 -0
- package/dist/providers/vercel.js.map +1 -0
- package/dist/public.d.ts +56 -0
- package/dist/public.d.ts.map +1 -0
- package/dist/public.js +37 -0
- package/dist/public.js.map +1 -0
- package/dist/sandbox.d.ts +76 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +154 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schema.d.ts +18 -172
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +10 -270
- package/dist/schema.js.map +1 -1
- package/dist/service.d.ts +10 -44
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +41 -566
- package/dist/service.js.map +1 -1
- package/package.json +37 -5
package/dist/service.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { Sandbox as VercelSandbox, Snapshot as VercelSnapshot } from "@vercel/sandbox";
|
|
2
|
-
import { Daytona
|
|
2
|
+
import { Daytona } 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 { buildDeclarativeImage, getDaytonaConfig, resolveDaytonaLanguage, resolveDaytonaVolumes, } from "./providers/daytona.js";
|
|
7
|
+
import { resolveProvider } from "./providers/provider.js";
|
|
8
|
+
import { asSpritesSandbox, getSpritesByName, parseSpritesCheckpointIdFromNdjson, provisionSpritesSandbox, spritesExec, spritesFetch, spritesJson, } from "./providers/sprites.js";
|
|
9
|
+
import { isVercelSandbox, } from "./providers/types.js";
|
|
10
|
+
import { provisionVercelSandbox, resolveVercelCredentials, } from "./providers/vercel.js";
|
|
6
11
|
import { resolveVercelSandboxConfig, safeVercelConfigForRecord, } from "./vercel-options.js";
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
12
|
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { existsSync, promises as fs } from "node:fs";
|
|
10
|
-
import os from "node:os";
|
|
11
13
|
import path from "node:path";
|
|
12
|
-
import { promisify } from "node:util";
|
|
13
|
-
const execFileAsync = promisify(execFile);
|
|
14
|
-
function isVercelSandbox(sandbox) {
|
|
15
|
-
return Boolean(sandbox &&
|
|
16
|
-
typeof sandbox === "object" &&
|
|
17
|
-
typeof sandbox.runCommand === "function" &&
|
|
18
|
-
typeof sandbox.currentSession === "function" &&
|
|
19
|
-
typeof sandbox.name === "string" &&
|
|
20
|
-
sandbox.__provider !== "sprites");
|
|
21
|
-
}
|
|
22
14
|
const EKAIROS_ROOT_DIR = "/vercel/sandbox/.ekairos";
|
|
23
15
|
const EKAIROS_RUNTIME_MANIFEST_PATH = `${EKAIROS_ROOT_DIR}/runtime.json`;
|
|
24
16
|
const EKAIROS_HTTP_HELPER_PATH = `${EKAIROS_ROOT_DIR}/instant-http.mjs`;
|
|
@@ -223,15 +215,6 @@ export class SandboxService {
|
|
|
223
215
|
constructor(db) {
|
|
224
216
|
this.adminDb = db;
|
|
225
217
|
}
|
|
226
|
-
static getVercelCredentials() {
|
|
227
|
-
const teamId = String(process.env.SANDBOX_VERCEL_TEAM_ID ?? "").trim();
|
|
228
|
-
const projectId = String(process.env.SANDBOX_VERCEL_PROJECT_ID ?? "").trim();
|
|
229
|
-
const token = String(process.env.SANDBOX_VERCEL_TOKEN ?? "").trim();
|
|
230
|
-
if (!teamId || !projectId || !token) {
|
|
231
|
-
throw new Error("Missing required Vercel sandbox environment variables");
|
|
232
|
-
}
|
|
233
|
-
return { teamId, projectId, token };
|
|
234
|
-
}
|
|
235
218
|
static getDomainName(domain) {
|
|
236
219
|
const metaName = typeof domain?.meta?.name === "string" ? domain.meta.name.trim() : "";
|
|
237
220
|
const contextName = typeof domain?.context === "function" ? String(domain.context()?.name ?? "").trim() : "";
|
|
@@ -275,7 +258,9 @@ export class SandboxService {
|
|
|
275
258
|
}
|
|
276
259
|
static buildEkairosManifest(params) {
|
|
277
260
|
const contextString = SandboxService.getDomainContextString(params.domain);
|
|
278
|
-
const schemaJson = SandboxService.cloneJson(params.domain.
|
|
261
|
+
const schemaJson = SandboxService.cloneJson(typeof params.domain.instantSchema === "function"
|
|
262
|
+
? params.domain.instantSchema()
|
|
263
|
+
: params.domain.toInstantSchema());
|
|
279
264
|
return {
|
|
280
265
|
version: 1,
|
|
281
266
|
instant: {
|
|
@@ -415,7 +400,7 @@ export class SandboxService {
|
|
|
415
400
|
if (!config.env || !config.domain) {
|
|
416
401
|
throw new Error("sandbox_runtime_requires_env_and_domain");
|
|
417
402
|
}
|
|
418
|
-
const provider =
|
|
403
|
+
const provider = resolveProvider(config);
|
|
419
404
|
if (provider !== "vercel") {
|
|
420
405
|
throw new Error("ekairos_runtime_requires_vercel_provider");
|
|
421
406
|
}
|
|
@@ -517,447 +502,6 @@ export class SandboxService {
|
|
|
517
502
|
fileCount: skill.files.length,
|
|
518
503
|
}));
|
|
519
504
|
}
|
|
520
|
-
static resolveVercelWorkingDirectory(config) {
|
|
521
|
-
const fromConfig = String(config.vercel?.cwd ?? "").trim();
|
|
522
|
-
if (fromConfig)
|
|
523
|
-
return path.resolve(fromConfig);
|
|
524
|
-
const fromEnv = String(process.env.SANDBOX_VERCEL_CWD ?? "").trim();
|
|
525
|
-
if (fromEnv)
|
|
526
|
-
return path.resolve(fromEnv);
|
|
527
|
-
return process.cwd();
|
|
528
|
-
}
|
|
529
|
-
static findLinkedVercelProjectFile(startDir) {
|
|
530
|
-
let current = path.resolve(startDir);
|
|
531
|
-
while (true) {
|
|
532
|
-
const candidate = path.join(current, ".vercel", "project.json");
|
|
533
|
-
if (existsSync(candidate))
|
|
534
|
-
return candidate;
|
|
535
|
-
const parent = path.dirname(current);
|
|
536
|
-
if (parent === current)
|
|
537
|
-
return null;
|
|
538
|
-
current = parent;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
static async readLinkedVercelProject(config) {
|
|
542
|
-
const cwd = SandboxService.resolveVercelWorkingDirectory(config);
|
|
543
|
-
const file = SandboxService.findLinkedVercelProjectFile(cwd);
|
|
544
|
-
if (!file) {
|
|
545
|
-
return { cwd };
|
|
546
|
-
}
|
|
547
|
-
try {
|
|
548
|
-
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
549
|
-
return {
|
|
550
|
-
cwd,
|
|
551
|
-
orgId: typeof parsed?.orgId === "string" ? parsed.orgId : undefined,
|
|
552
|
-
projectId: typeof parsed?.projectId === "string" ? parsed.projectId : undefined,
|
|
553
|
-
projectName: typeof parsed?.projectName === "string" ? parsed.projectName : undefined,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
catch {
|
|
557
|
-
return { cwd };
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
static async pullVercelOidcToken(config) {
|
|
561
|
-
const cwd = SandboxService.resolveVercelWorkingDirectory(config);
|
|
562
|
-
const tmpPath = path.join(os.tmpdir(), `ekairos-vercel-env-${Date.now()}-${Math.random().toString(36).slice(2)}.env`);
|
|
563
|
-
const args = ["env", "pull", tmpPath, "--yes", "--environment", String(config.vercel?.environment ?? "development")];
|
|
564
|
-
const scope = String(config.vercel?.scope ?? process.env.SANDBOX_VERCEL_SCOPE ?? "").trim();
|
|
565
|
-
if (scope) {
|
|
566
|
-
args.push("--scope", scope);
|
|
567
|
-
}
|
|
568
|
-
const token = String(process.env.VERCEL_TOKEN ?? process.env.SANDBOX_VERCEL_TOKEN ?? "").trim();
|
|
569
|
-
if (token) {
|
|
570
|
-
args.push("--token", token);
|
|
571
|
-
}
|
|
572
|
-
const isWindows = process.platform === "win32";
|
|
573
|
-
const command = isWindows ? (process.env.COMSPEC || "cmd.exe") : "vercel";
|
|
574
|
-
const commandArgs = isWindows ? ["/c", "vercel", ...args] : args;
|
|
575
|
-
try {
|
|
576
|
-
await execFileAsync(command, commandArgs, {
|
|
577
|
-
cwd,
|
|
578
|
-
windowsHide: true,
|
|
579
|
-
timeout: 120000,
|
|
580
|
-
maxBuffer: 1024 * 1024 * 10,
|
|
581
|
-
});
|
|
582
|
-
const content = await fs.readFile(tmpPath, "utf8");
|
|
583
|
-
const match = content.match(/VERCEL_OIDC_TOKEN=\"?([^\r\n\"]+)\"?/);
|
|
584
|
-
const oidc = String(match?.[1] ?? "").trim();
|
|
585
|
-
if (!oidc) {
|
|
586
|
-
throw new Error("VERCEL_OIDC_TOKEN missing from vercel env pull output");
|
|
587
|
-
}
|
|
588
|
-
return oidc;
|
|
589
|
-
}
|
|
590
|
-
finally {
|
|
591
|
-
await fs.rm(tmpPath, { force: true }).catch(() => { });
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
static async resolveVercelCredentials(config) {
|
|
595
|
-
const explicitTeamId = String(config.vercel?.orgId ?? process.env.SANDBOX_VERCEL_TEAM_ID ?? "").trim();
|
|
596
|
-
const explicitProjectId = String(config.vercel?.projectId ?? process.env.SANDBOX_VERCEL_PROJECT_ID ?? "").trim();
|
|
597
|
-
const explicitToken = String(config.vercel?.token ?? process.env.SANDBOX_VERCEL_TOKEN ?? process.env.VERCEL_OIDC_TOKEN ?? "").trim();
|
|
598
|
-
if (explicitTeamId && explicitProjectId && explicitToken) {
|
|
599
|
-
return { teamId: explicitTeamId, projectId: explicitProjectId, token: explicitToken };
|
|
600
|
-
}
|
|
601
|
-
const linked = await SandboxService.readLinkedVercelProject(config);
|
|
602
|
-
const teamId = explicitTeamId || String(linked.orgId ?? "").trim();
|
|
603
|
-
const projectId = explicitProjectId || String(linked.projectId ?? "").trim();
|
|
604
|
-
let token = explicitToken;
|
|
605
|
-
if (!token) {
|
|
606
|
-
token = await SandboxService.pullVercelOidcToken(config);
|
|
607
|
-
}
|
|
608
|
-
if (!teamId || !projectId || !token) {
|
|
609
|
-
throw new Error("Missing Vercel sandbox credentials. Link the project (`vercel link`) and ensure `vercel env pull` can resolve VERCEL_OIDC_TOKEN, or provide explicit SANDBOX_VERCEL_* env vars.");
|
|
610
|
-
}
|
|
611
|
-
return { teamId, projectId, token };
|
|
612
|
-
}
|
|
613
|
-
static async provisionVercelSandbox(config, extra) {
|
|
614
|
-
const creds = await SandboxService.resolveVercelCredentials(config);
|
|
615
|
-
const resolved = extra?.resolved ?? resolveVercelSandboxConfig(config);
|
|
616
|
-
if (resolved.reuse && resolved.name) {
|
|
617
|
-
try {
|
|
618
|
-
return await VercelSandbox.get({
|
|
619
|
-
name: resolved.name,
|
|
620
|
-
teamId: creds.teamId,
|
|
621
|
-
projectId: creds.projectId,
|
|
622
|
-
token: creds.token,
|
|
623
|
-
resume: true,
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
catch (error) {
|
|
627
|
-
const status = Number(error?.response?.status ?? 0);
|
|
628
|
-
const message = formatSandboxError(error).toLowerCase();
|
|
629
|
-
if (status !== 404 && !message.includes("not found")) {
|
|
630
|
-
throw error;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
return await VercelSandbox.create({
|
|
635
|
-
teamId: creds.teamId,
|
|
636
|
-
projectId: creds.projectId,
|
|
637
|
-
token: creds.token,
|
|
638
|
-
...(resolved.name ? { name: resolved.name } : {}),
|
|
639
|
-
timeout: resolved.timeoutMs,
|
|
640
|
-
ports: resolved.ports,
|
|
641
|
-
// IMPORTANT: pass runtime as-is (e.g. "python3.13") to match provider expectations.
|
|
642
|
-
// Don't normalize to "python3"/"node22" as that can cause provider-side 400s.
|
|
643
|
-
runtime: resolved.runtime,
|
|
644
|
-
resources: { vcpus: resolved.vcpus },
|
|
645
|
-
persistent: resolved.persistent,
|
|
646
|
-
...(resolved.snapshotExpirationMs !== undefined
|
|
647
|
-
? { snapshotExpiration: resolved.snapshotExpirationMs }
|
|
648
|
-
: {}),
|
|
649
|
-
...(resolved.tags ? { tags: resolved.tags } : {}),
|
|
650
|
-
networkPolicy: extra?.networkPolicy,
|
|
651
|
-
env: extra?.env,
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
static getDaytonaConfig() {
|
|
655
|
-
const apiKey = String(process.env.DAYTONA_API_KEY ?? "").trim();
|
|
656
|
-
const apiUrl = String(process.env.DAYTONA_API_URL ?? "").trim() ||
|
|
657
|
-
String(process.env.DAYTONA_SERVER_URL ?? "").trim();
|
|
658
|
-
const jwtToken = String(process.env.DAYTONA_JWT_TOKEN ?? "").trim();
|
|
659
|
-
const organizationId = String(process.env.DAYTONA_ORGANIZATION_ID ?? "").trim();
|
|
660
|
-
const target = String(process.env.DAYTONA_TARGET ?? "").trim();
|
|
661
|
-
if (!apiUrl) {
|
|
662
|
-
throw new Error("Missing required Daytona env var: DAYTONA_API_URL (or DAYTONA_SERVER_URL)");
|
|
663
|
-
}
|
|
664
|
-
if (!apiKey && !(jwtToken && organizationId)) {
|
|
665
|
-
throw new Error("Missing required Daytona env vars: DAYTONA_API_KEY or DAYTONA_JWT_TOKEN + DAYTONA_ORGANIZATION_ID");
|
|
666
|
-
}
|
|
667
|
-
const config = {
|
|
668
|
-
apiUrl,
|
|
669
|
-
target: target || undefined,
|
|
670
|
-
apiKey: apiKey || undefined,
|
|
671
|
-
jwtToken: jwtToken || undefined,
|
|
672
|
-
organizationId: organizationId || undefined,
|
|
673
|
-
};
|
|
674
|
-
return config;
|
|
675
|
-
}
|
|
676
|
-
static normalizeBaseUrl(raw) {
|
|
677
|
-
const trimmed = String(raw ?? "").trim();
|
|
678
|
-
if (!trimmed)
|
|
679
|
-
return "";
|
|
680
|
-
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
681
|
-
}
|
|
682
|
-
static getSpritesConfig() {
|
|
683
|
-
const token = String(process.env.SPRITES_API_TOKEN ?? process.env.SPRITE_TOKEN ?? "").trim();
|
|
684
|
-
if (!token) {
|
|
685
|
-
throw new Error("Missing required Sprites token env var: SPRITES_API_TOKEN (or SPRITE_TOKEN)");
|
|
686
|
-
}
|
|
687
|
-
const baseUrl = SandboxService.normalizeBaseUrl(String(process.env.SPRITES_API_BASE_URL ?? process.env.SPRITES_API_URL ?? "").trim()) || "https://api.sprites.dev";
|
|
688
|
-
return { baseUrl, token };
|
|
689
|
-
}
|
|
690
|
-
static async spritesFetch(path, init) {
|
|
691
|
-
const { baseUrl, token } = SandboxService.getSpritesConfig();
|
|
692
|
-
const fetchFn = globalThis?.fetch;
|
|
693
|
-
if (typeof fetchFn !== "function") {
|
|
694
|
-
throw new Error("fetch_not_available");
|
|
695
|
-
}
|
|
696
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
697
|
-
const url = `${baseUrl}${normalizedPath}`;
|
|
698
|
-
return await fetchFn(url, {
|
|
699
|
-
...init,
|
|
700
|
-
headers: {
|
|
701
|
-
Authorization: `Bearer ${token}`,
|
|
702
|
-
...(init?.headers ?? {}),
|
|
703
|
-
},
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
static async spritesJson(path, init) {
|
|
707
|
-
const res = await SandboxService.spritesFetch(path, {
|
|
708
|
-
...init,
|
|
709
|
-
headers: {
|
|
710
|
-
Accept: "application/json",
|
|
711
|
-
...(init?.headers ?? {}),
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
if (!res?.ok) {
|
|
715
|
-
const text = await res?.text?.().catch(() => "");
|
|
716
|
-
throw new Error(`sprites_http_${res?.status ?? "unknown"}: ${text || "request_failed"}`);
|
|
717
|
-
}
|
|
718
|
-
return (await res.json().catch(() => ({})));
|
|
719
|
-
}
|
|
720
|
-
static async spritesText(path, init) {
|
|
721
|
-
const res = await SandboxService.spritesFetch(path, init);
|
|
722
|
-
const text = await res?.text?.().catch(() => "");
|
|
723
|
-
return { ok: Boolean(res?.ok), status: Number(res?.status ?? 0), text: String(text ?? "") };
|
|
724
|
-
}
|
|
725
|
-
static toSpritesPreviewUrl(spriteUrl, port) {
|
|
726
|
-
const base = String(spriteUrl ?? "").trim();
|
|
727
|
-
if (!base)
|
|
728
|
-
return "";
|
|
729
|
-
try {
|
|
730
|
-
const u = new URL(base);
|
|
731
|
-
if (Number.isFinite(port) && port > 0) {
|
|
732
|
-
u.port = String(Math.floor(port));
|
|
733
|
-
}
|
|
734
|
-
const next = u.toString();
|
|
735
|
-
return next.endsWith("/") ? next.slice(0, -1) : next;
|
|
736
|
-
}
|
|
737
|
-
catch {
|
|
738
|
-
// Best effort fallback: append port if missing.
|
|
739
|
-
if (!port)
|
|
740
|
-
return base;
|
|
741
|
-
return base.replace(/\/+$/, "") + ":" + String(Math.floor(port));
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
static asSpritesSandbox(sprite) {
|
|
745
|
-
const name = String(sprite?.name ?? "").trim();
|
|
746
|
-
const url = typeof sprite?.url === "string" ? sprite.url : undefined;
|
|
747
|
-
return {
|
|
748
|
-
__provider: "sprites",
|
|
749
|
-
name,
|
|
750
|
-
id: sprite?.id ? String(sprite.id) : undefined,
|
|
751
|
-
url,
|
|
752
|
-
getPreviewLink: async (port) => {
|
|
753
|
-
const base = url ?? "";
|
|
754
|
-
const next = SandboxService.toSpritesPreviewUrl(base, port);
|
|
755
|
-
return { url: next };
|
|
756
|
-
},
|
|
757
|
-
domain: async (port) => {
|
|
758
|
-
const base = url ?? "";
|
|
759
|
-
return SandboxService.toSpritesPreviewUrl(base, port);
|
|
760
|
-
},
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
static async getSpritesByName(name) {
|
|
764
|
-
const safeName = String(name ?? "").trim();
|
|
765
|
-
if (!safeName)
|
|
766
|
-
return { ok: false, status: 400, error: "sprites_name_required" };
|
|
767
|
-
const res = await SandboxService.spritesFetch(`/v1/sprites/${encodeURIComponent(safeName)}`, {
|
|
768
|
-
method: "GET",
|
|
769
|
-
headers: { Accept: "application/json" },
|
|
770
|
-
});
|
|
771
|
-
if (!res?.ok) {
|
|
772
|
-
const text = await res?.text?.().catch(() => "");
|
|
773
|
-
return { ok: false, status: Number(res?.status ?? 0), error: text || `sprites_http_${res?.status ?? "unknown"}` };
|
|
774
|
-
}
|
|
775
|
-
const json = await res.json().catch(() => ({}));
|
|
776
|
-
return { ok: true, sprite: json };
|
|
777
|
-
}
|
|
778
|
-
static async provisionSpritesSandbox(params) {
|
|
779
|
-
const requestedName = String(params.config?.sprites?.name ?? "").trim();
|
|
780
|
-
const name = requestedName || `ekairos-${params.sandboxId}`;
|
|
781
|
-
// Idempotent: if already exists, reuse.
|
|
782
|
-
const existing = await SandboxService.getSpritesByName(name);
|
|
783
|
-
if (existing.ok) {
|
|
784
|
-
const sprite = existing.sprite ?? {};
|
|
785
|
-
return SandboxService.asSpritesSandbox({
|
|
786
|
-
name: String(sprite?.name ?? name),
|
|
787
|
-
id: sprite?.id ? String(sprite.id) : undefined,
|
|
788
|
-
url: typeof sprite?.url === "string" ? sprite.url : undefined,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
const waitForCapacity = params.config?.sprites?.waitForCapacity ?? true;
|
|
792
|
-
const auth = params.config?.sprites?.urlSettings?.auth ?? "public";
|
|
793
|
-
const body = {
|
|
794
|
-
name,
|
|
795
|
-
wait_for_capacity: Boolean(waitForCapacity),
|
|
796
|
-
url_settings: { auth },
|
|
797
|
-
};
|
|
798
|
-
const created = await SandboxService.spritesJson("/v1/sprites", {
|
|
799
|
-
method: "POST",
|
|
800
|
-
headers: { "Content-Type": "application/json" },
|
|
801
|
-
body: JSON.stringify(body),
|
|
802
|
-
});
|
|
803
|
-
return SandboxService.asSpritesSandbox({
|
|
804
|
-
name: String(created?.name ?? name),
|
|
805
|
-
id: created?.id ? String(created.id) : undefined,
|
|
806
|
-
url: typeof created?.url === "string" ? created.url : undefined,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
static normalizeSpritesExecResult(payload) {
|
|
810
|
-
const exitCodeRaw = payload?.exit_code ??
|
|
811
|
-
payload?.exitCode ??
|
|
812
|
-
payload?.code ??
|
|
813
|
-
payload?.status ??
|
|
814
|
-
payload?.result?.exit_code ??
|
|
815
|
-
payload?.result?.exitCode;
|
|
816
|
-
const exitCode = Number(exitCodeRaw ?? 0);
|
|
817
|
-
const stdout = typeof payload?.stdout === "string"
|
|
818
|
-
? payload.stdout
|
|
819
|
-
: typeof payload?.output === "string"
|
|
820
|
-
? payload.output
|
|
821
|
-
: typeof payload?.out === "string"
|
|
822
|
-
? payload.out
|
|
823
|
-
: typeof payload?.result?.stdout === "string"
|
|
824
|
-
? payload.result.stdout
|
|
825
|
-
: "";
|
|
826
|
-
const stderr = typeof payload?.stderr === "string"
|
|
827
|
-
? payload.stderr
|
|
828
|
-
: typeof payload?.error === "string"
|
|
829
|
-
? payload.error
|
|
830
|
-
: typeof payload?.err === "string"
|
|
831
|
-
? payload.err
|
|
832
|
-
: typeof payload?.result?.stderr === "string"
|
|
833
|
-
? payload.result.stderr
|
|
834
|
-
: "";
|
|
835
|
-
return {
|
|
836
|
-
exitCode: Number.isFinite(exitCode) ? exitCode : 0,
|
|
837
|
-
stdout: sanitizeInstantString(stdout),
|
|
838
|
-
stderr: sanitizeInstantString(stderr),
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
static async spritesExec(params) {
|
|
842
|
-
const spriteName = String(params.spriteName ?? "").trim();
|
|
843
|
-
if (!spriteName)
|
|
844
|
-
throw new Error("sprites_name_required");
|
|
845
|
-
const parts = [String(params.command ?? "").trim(), ...(Array.isArray(params.args) ? params.args : [])].filter(Boolean);
|
|
846
|
-
if (parts.length === 0)
|
|
847
|
-
throw new Error("sprites_command_required");
|
|
848
|
-
const search = new URLSearchParams();
|
|
849
|
-
for (const part of parts) {
|
|
850
|
-
search.append("cmd", String(part));
|
|
851
|
-
}
|
|
852
|
-
const hasStdin = typeof params.stdin === "string" || Buffer.isBuffer(params.stdin);
|
|
853
|
-
if (hasStdin) {
|
|
854
|
-
search.set("stdin", "true");
|
|
855
|
-
}
|
|
856
|
-
const path = `/v1/sprites/${encodeURIComponent(spriteName)}/exec?${search.toString()}`;
|
|
857
|
-
const init = {
|
|
858
|
-
method: "POST",
|
|
859
|
-
};
|
|
860
|
-
if (hasStdin) {
|
|
861
|
-
init.body = params.stdin;
|
|
862
|
-
}
|
|
863
|
-
const res = await SandboxService.spritesFetch(path, init);
|
|
864
|
-
const text = await res?.text?.().catch(() => "");
|
|
865
|
-
const parsed = (() => {
|
|
866
|
-
try {
|
|
867
|
-
return text ? JSON.parse(text) : {};
|
|
868
|
-
}
|
|
869
|
-
catch {
|
|
870
|
-
return { stdout: String(text ?? "") };
|
|
871
|
-
}
|
|
872
|
-
})();
|
|
873
|
-
if (!res?.ok) {
|
|
874
|
-
const err = typeof parsed?.error === "string" ? parsed.error : text;
|
|
875
|
-
throw new Error(err || `sprites_exec_http_${res?.status ?? "unknown"}`);
|
|
876
|
-
}
|
|
877
|
-
return SandboxService.normalizeSpritesExecResult(parsed);
|
|
878
|
-
}
|
|
879
|
-
static resolveProvider(config) {
|
|
880
|
-
const explicit = String(config.provider ?? "").trim().toLowerCase();
|
|
881
|
-
if (explicit === "daytona")
|
|
882
|
-
return "daytona";
|
|
883
|
-
if (explicit === "vercel")
|
|
884
|
-
return "vercel";
|
|
885
|
-
if (explicit === "sprites")
|
|
886
|
-
return "sprites";
|
|
887
|
-
const env = String(process.env.SANDBOX_PROVIDER ?? "").trim().toLowerCase();
|
|
888
|
-
if (env === "daytona")
|
|
889
|
-
return "daytona";
|
|
890
|
-
if (env === "vercel")
|
|
891
|
-
return "vercel";
|
|
892
|
-
if (env === "sprites")
|
|
893
|
-
return "sprites";
|
|
894
|
-
return "sprites";
|
|
895
|
-
}
|
|
896
|
-
static resolveDaytonaLanguage(config) {
|
|
897
|
-
if (config.daytona?.language)
|
|
898
|
-
return config.daytona.language;
|
|
899
|
-
const runtime = String(config.runtime ?? "").toLowerCase();
|
|
900
|
-
if (runtime.startsWith("python"))
|
|
901
|
-
return "python";
|
|
902
|
-
if (runtime.startsWith("node"))
|
|
903
|
-
return "javascript";
|
|
904
|
-
if (runtime.startsWith("ts") || runtime.includes("typescript"))
|
|
905
|
-
return "typescript";
|
|
906
|
-
return undefined;
|
|
907
|
-
}
|
|
908
|
-
static async resolveDaytonaVolumes(daytona, volumes) {
|
|
909
|
-
if (!volumes || volumes.length === 0)
|
|
910
|
-
return [];
|
|
911
|
-
const resolved = [];
|
|
912
|
-
const shouldLog = SandboxService.parseOptionalBoolean(process.env.SANDBOX_DAYTONA_LOG_VOLUMES) ?? false;
|
|
913
|
-
for (const volume of volumes) {
|
|
914
|
-
const mountPath = String(volume.mountPath ?? "").trim();
|
|
915
|
-
if (!mountPath)
|
|
916
|
-
continue;
|
|
917
|
-
const volumeId = String(volume.volumeId ?? "").trim();
|
|
918
|
-
if (volumeId) {
|
|
919
|
-
resolved.push({ volumeId, mountPath });
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
const volumeName = String(volume.volumeName ?? "").trim();
|
|
923
|
-
if (!volumeName) {
|
|
924
|
-
throw new Error("Daytona volume requires volumeId or volumeName");
|
|
925
|
-
}
|
|
926
|
-
let resolvedVolume = await daytona.volume.get(volumeName, true);
|
|
927
|
-
const stateRaw = String(resolvedVolume?.state ?? "").trim().toLowerCase();
|
|
928
|
-
const waitStates = new Set(["creating", "provisioning", "pending", "pending_create", "pending-create", "initializing"]);
|
|
929
|
-
const readyStates = new Set(["available", "active", "ready"]);
|
|
930
|
-
if (waitStates.has(stateRaw)) {
|
|
931
|
-
const maxAttempts = 12;
|
|
932
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
933
|
-
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
|
934
|
-
resolvedVolume = await daytona.volume.get(volumeName, true);
|
|
935
|
-
const state = String(resolvedVolume?.state ?? "").trim().toLowerCase();
|
|
936
|
-
if (shouldLog) {
|
|
937
|
-
console.log(`[daytona:volume] name=${volumeName} state=${state} attempt=${attempt}/${maxAttempts}`);
|
|
938
|
-
}
|
|
939
|
-
if (readyStates.has(state))
|
|
940
|
-
break;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
const finalState = String(resolvedVolume?.state ?? "").trim().toLowerCase();
|
|
944
|
-
if (finalState && !readyStates.has(finalState)) {
|
|
945
|
-
if (shouldLog) {
|
|
946
|
-
console.log(`[daytona:volume] name=${volumeName} state=${finalState} mountPath=${mountPath} (not ready)`);
|
|
947
|
-
}
|
|
948
|
-
throw new Error(`Daytona volume not ready: ${volumeName} (state=${finalState})`);
|
|
949
|
-
}
|
|
950
|
-
const resolvedId = String(resolvedVolume?.id ?? "").trim();
|
|
951
|
-
if (!resolvedId) {
|
|
952
|
-
throw new Error(`Daytona volume not resolved: ${volumeName}`);
|
|
953
|
-
}
|
|
954
|
-
if (shouldLog) {
|
|
955
|
-
console.log(`[daytona:volume] name=${volumeName} id=${resolvedId} mountPath=${mountPath}`);
|
|
956
|
-
}
|
|
957
|
-
resolved.push({ volumeId: resolvedId, mountPath });
|
|
958
|
-
}
|
|
959
|
-
return resolved;
|
|
960
|
-
}
|
|
961
505
|
static shellEscapeArg(value) {
|
|
962
506
|
if (value.length === 0)
|
|
963
507
|
return "''";
|
|
@@ -975,58 +519,10 @@ export class SandboxService {
|
|
|
975
519
|
return false;
|
|
976
520
|
return undefined;
|
|
977
521
|
}
|
|
978
|
-
static parseCsvList(value) {
|
|
979
|
-
return String(value ?? "")
|
|
980
|
-
.split(",")
|
|
981
|
-
.map((entry) => entry.trim())
|
|
982
|
-
.filter(Boolean);
|
|
983
|
-
}
|
|
984
|
-
static resolvePythonVersion(runtime) {
|
|
985
|
-
const fromEnv = String(process.env.SANDBOX_DAYTONA_DECLARATIVE_PYTHON ?? "").trim() ||
|
|
986
|
-
String(process.env.STRUCTURE_DAYTONA_DECLARATIVE_PYTHON ?? "").trim();
|
|
987
|
-
if (fromEnv)
|
|
988
|
-
return fromEnv;
|
|
989
|
-
const match = String(runtime ?? "").match(/python\s*([0-9]+\.[0-9]+)/i);
|
|
990
|
-
if (match?.[1])
|
|
991
|
-
return match[1];
|
|
992
|
-
return "3.12";
|
|
993
|
-
}
|
|
994
|
-
static buildDeclarativeImage(config) {
|
|
995
|
-
const imageFlag = String(config.daytona?.image ?? "").trim();
|
|
996
|
-
const envFlag = SandboxService.parseOptionalBoolean(process.env.SANDBOX_DAYTONA_DECLARATIVE_IMAGE) ??
|
|
997
|
-
SandboxService.parseOptionalBoolean(process.env.STRUCTURE_DAYTONA_DECLARATIVE_IMAGE) ??
|
|
998
|
-
false;
|
|
999
|
-
const useDeclarative = envFlag || imageFlag.startsWith("declarative");
|
|
1000
|
-
if (!useDeclarative)
|
|
1001
|
-
return undefined;
|
|
1002
|
-
const baseImage = String(process.env.SANDBOX_DAYTONA_DECLARATIVE_BASE ?? "").trim() ||
|
|
1003
|
-
String(process.env.STRUCTURE_DAYTONA_DECLARATIVE_BASE ?? "").trim();
|
|
1004
|
-
const pythonVersion = SandboxService.resolvePythonVersion(config.runtime);
|
|
1005
|
-
const isStructureDataset = config.purpose === "structure.dataset" || typeof config.params?.datasetId === "string";
|
|
1006
|
-
const defaultPackages = isStructureDataset ? ["pandas", "openpyxl"] : [];
|
|
1007
|
-
const packages = [
|
|
1008
|
-
...SandboxService.parseCsvList(process.env.SANDBOX_DAYTONA_DECLARATIVE_PIP),
|
|
1009
|
-
...SandboxService.parseCsvList(process.env.STRUCTURE_DAYTONA_DECLARATIVE_PIP),
|
|
1010
|
-
...defaultPackages,
|
|
1011
|
-
];
|
|
1012
|
-
const uniquePackages = Array.from(new Set(packages));
|
|
1013
|
-
let image;
|
|
1014
|
-
if (baseImage) {
|
|
1015
|
-
image = Image.base(baseImage);
|
|
1016
|
-
}
|
|
1017
|
-
else {
|
|
1018
|
-
image = Image.debianSlim(pythonVersion);
|
|
1019
|
-
}
|
|
1020
|
-
if (uniquePackages.length > 0) {
|
|
1021
|
-
image = image.pipInstall(uniquePackages);
|
|
1022
|
-
}
|
|
1023
|
-
image = image.workdir("/home/daytona");
|
|
1024
|
-
return image;
|
|
1025
|
-
}
|
|
1026
522
|
async createSandbox(config) {
|
|
1027
523
|
const sandboxId = id();
|
|
1028
524
|
const now = Date.now();
|
|
1029
|
-
const provider =
|
|
525
|
+
const provider = resolveProvider(config);
|
|
1030
526
|
const resolvedVercel = provider === "vercel" ? resolveVercelSandboxConfig(config, { sandboxId }) : undefined;
|
|
1031
527
|
let daytonaEphemeral = undefined;
|
|
1032
528
|
let installedSkills = [];
|
|
@@ -1078,10 +574,10 @@ export class SandboxService {
|
|
|
1078
574
|
let sandbox = null;
|
|
1079
575
|
try {
|
|
1080
576
|
if (provider === "daytona") {
|
|
1081
|
-
const daytona = new Daytona(
|
|
1082
|
-
const language =
|
|
577
|
+
const daytona = new Daytona(getDaytonaConfig());
|
|
578
|
+
const language = resolveDaytonaLanguage(config);
|
|
1083
579
|
const requestedVolumes = config.daytona?.volumes ?? [];
|
|
1084
|
-
const volumes = await
|
|
580
|
+
const volumes = await resolveDaytonaVolumes(daytona, requestedVolumes);
|
|
1085
581
|
const envVars = config.daytona?.envVars;
|
|
1086
582
|
const isPublic = config.daytona?.public;
|
|
1087
583
|
const envEphemeral = SandboxService.parseOptionalBoolean(process.env.SANDBOX_DAYTONA_EPHEMERAL);
|
|
@@ -1092,7 +588,7 @@ export class SandboxService {
|
|
|
1092
588
|
const autoArchiveInterval = config.daytona?.autoArchiveIntervalMin;
|
|
1093
589
|
const autoDeleteInterval = config.daytona?.autoDeleteIntervalMin;
|
|
1094
590
|
const resolvedAutoDeleteInterval = ephemeral ? undefined : autoDeleteInterval;
|
|
1095
|
-
const declarativeImage =
|
|
591
|
+
const declarativeImage = buildDeclarativeImage(config);
|
|
1096
592
|
const image = declarativeImage ?? config.daytona?.image;
|
|
1097
593
|
const snapshot = config.daytona?.snapshot;
|
|
1098
594
|
const resources = config.resources?.vcpus ? { cpu: config.resources.vcpus } : undefined;
|
|
@@ -1131,7 +627,7 @@ export class SandboxService {
|
|
|
1131
627
|
}
|
|
1132
628
|
}
|
|
1133
629
|
else if (provider === "sprites") {
|
|
1134
|
-
sandbox = await
|
|
630
|
+
sandbox = await provisionSpritesSandbox({
|
|
1135
631
|
sandboxId,
|
|
1136
632
|
config,
|
|
1137
633
|
});
|
|
@@ -1141,7 +637,7 @@ export class SandboxService {
|
|
|
1141
637
|
...(Array.isArray(config.skills) && config.skills.length > 0 ? { CODEX_HOME: CODEX_HOME_DIR } : {}),
|
|
1142
638
|
...(ekairos?.env ?? {}),
|
|
1143
639
|
};
|
|
1144
|
-
sandbox = await
|
|
640
|
+
sandbox = await provisionVercelSandbox(config, {
|
|
1145
641
|
networkPolicy: ekairos?.networkPolicy,
|
|
1146
642
|
env: Object.keys(vercelEnv).length > 0 ? vercelEnv : undefined,
|
|
1147
643
|
resolved: resolvedVercel,
|
|
@@ -1256,7 +752,7 @@ export class SandboxService {
|
|
|
1256
752
|
return { ok: false, error: "Valid sandbox record not found" };
|
|
1257
753
|
}
|
|
1258
754
|
if (record.provider === "daytona") {
|
|
1259
|
-
const daytona = new Daytona(
|
|
755
|
+
const daytona = new Daytona(getDaytonaConfig());
|
|
1260
756
|
try {
|
|
1261
757
|
const sandbox = await daytona.get(String(record.externalSandboxId));
|
|
1262
758
|
const state = String(sandbox.state ?? "").toLowerCase();
|
|
@@ -1280,7 +776,7 @@ export class SandboxService {
|
|
|
1280
776
|
if (record.provider === "sprites") {
|
|
1281
777
|
const name = String(record.externalSandboxId ?? "").trim();
|
|
1282
778
|
try {
|
|
1283
|
-
const spriteRes = await
|
|
779
|
+
const spriteRes = await getSpritesByName(name);
|
|
1284
780
|
if (!spriteRes.ok) {
|
|
1285
781
|
if (record.status === "active") {
|
|
1286
782
|
await this.adminDb.transact(this.adminDb.tx.sandbox_sandboxes[sandboxId].update({
|
|
@@ -1292,7 +788,7 @@ export class SandboxService {
|
|
|
1292
788
|
return { ok: false, error: spriteRes.error || "sprites_not_found" };
|
|
1293
789
|
}
|
|
1294
790
|
const sprite = spriteRes.sprite ?? {};
|
|
1295
|
-
const spritesSandbox =
|
|
791
|
+
const spritesSandbox = asSpritesSandbox({
|
|
1296
792
|
name: String(sprite?.name ?? name),
|
|
1297
793
|
id: sprite?.id ? String(sprite.id) : undefined,
|
|
1298
794
|
url: typeof sprite?.url === "string" ? sprite.url : undefined,
|
|
@@ -1336,7 +832,7 @@ export class SandboxService {
|
|
|
1336
832
|
if (record.provider !== "vercel") {
|
|
1337
833
|
return { ok: false, error: "Valid sandbox record not found" };
|
|
1338
834
|
}
|
|
1339
|
-
const creds = await
|
|
835
|
+
const creds = await resolveVercelCredentials(record?.params ?? {});
|
|
1340
836
|
try {
|
|
1341
837
|
const maxAttempts = 20;
|
|
1342
838
|
const delayMs = 500;
|
|
@@ -1346,6 +842,7 @@ export class SandboxService {
|
|
|
1346
842
|
teamId: creds.teamId,
|
|
1347
843
|
projectId: creds.projectId,
|
|
1348
844
|
token: creds.token,
|
|
845
|
+
resume: true,
|
|
1349
846
|
});
|
|
1350
847
|
if (!sandbox)
|
|
1351
848
|
return { ok: false, error: "Sandbox not found" };
|
|
@@ -1373,19 +870,21 @@ export class SandboxService {
|
|
|
1373
870
|
}
|
|
1374
871
|
}
|
|
1375
872
|
async getSandboxRecord(sandboxId) {
|
|
1376
|
-
const
|
|
873
|
+
const query = {
|
|
1377
874
|
sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 }, user: {} },
|
|
1378
|
-
}
|
|
875
|
+
};
|
|
876
|
+
const recordResult = await this.adminDb.query(query);
|
|
1379
877
|
return recordResult?.sandbox_sandboxes?.[0] ?? null;
|
|
1380
878
|
}
|
|
1381
879
|
async getProcessSnapshot(processId) {
|
|
1382
880
|
try {
|
|
1383
|
-
const
|
|
881
|
+
const query = {
|
|
1384
882
|
sandbox_processes: {
|
|
1385
883
|
$: { where: { id: processId }, limit: 1 },
|
|
1386
884
|
sandbox: {},
|
|
1387
885
|
},
|
|
1388
|
-
}
|
|
886
|
+
};
|
|
887
|
+
const processResult = await this.adminDb.query(query);
|
|
1389
888
|
const processRow = processResult?.sandbox_processes?.[0];
|
|
1390
889
|
if (!processRow)
|
|
1391
890
|
return { ok: false, error: "sandbox_process_not_found" };
|
|
@@ -1452,12 +951,13 @@ export class SandboxService {
|
|
|
1452
951
|
}));
|
|
1453
952
|
}
|
|
1454
953
|
async readProcessRow(processId) {
|
|
1455
|
-
const
|
|
954
|
+
const query = {
|
|
1456
955
|
sandbox_processes: {
|
|
1457
956
|
$: { where: { id: processId }, limit: 1 },
|
|
1458
957
|
sandbox: {},
|
|
1459
958
|
},
|
|
1460
|
-
}
|
|
959
|
+
};
|
|
960
|
+
const result = await this.adminDb.query(query);
|
|
1461
961
|
return result?.sandbox_processes?.[0] ?? null;
|
|
1462
962
|
}
|
|
1463
963
|
async writeProcessChunkByProcessId(processId, type, data, opts) {
|
|
@@ -1653,7 +1153,7 @@ export class SandboxService {
|
|
|
1653
1153
|
else if (sandbox?.__provider === "sprites") {
|
|
1654
1154
|
// Sprites does not have a reliable "stop" semantic; deleting is the durable cleanup primitive.
|
|
1655
1155
|
try {
|
|
1656
|
-
await
|
|
1156
|
+
await spritesFetch(`/v1/sprites/${encodeURIComponent(String(sandbox.name))}`, {
|
|
1657
1157
|
method: "DELETE",
|
|
1658
1158
|
});
|
|
1659
1159
|
}
|
|
@@ -1662,7 +1162,7 @@ export class SandboxService {
|
|
|
1662
1162
|
}
|
|
1663
1163
|
}
|
|
1664
1164
|
else {
|
|
1665
|
-
const daytona = new Daytona(
|
|
1165
|
+
const daytona = new Daytona(getDaytonaConfig());
|
|
1666
1166
|
await daytona.stop(sandbox);
|
|
1667
1167
|
if (deleteOnStop) {
|
|
1668
1168
|
try {
|
|
@@ -1737,7 +1237,7 @@ export class SandboxService {
|
|
|
1737
1237
|
}
|
|
1738
1238
|
if (sandbox.__provider === "sprites") {
|
|
1739
1239
|
const fullCommand = args.length > 0 ? [command, ...args].join(" ") : command;
|
|
1740
|
-
const res = await
|
|
1240
|
+
const res = await spritesExec({
|
|
1741
1241
|
spriteName: String(sandbox.name ?? ""),
|
|
1742
1242
|
command,
|
|
1743
1243
|
args,
|
|
@@ -2061,7 +1561,7 @@ export class SandboxService {
|
|
|
2061
1561
|
const dirPath = filePath.includes("/") ? filePath.split("/").slice(0, -1).join("/") : "";
|
|
2062
1562
|
const dirCmd = dirPath ? `mkdir -p ${SandboxService.shellEscapeArg(dirPath)} && ` : "";
|
|
2063
1563
|
const cmd = `${dirCmd}printf %s ${SandboxService.shellEscapeArg(String(f.contentBase64 ?? ""))} | base64 -d > ${SandboxService.shellEscapeArg(filePath)}`;
|
|
2064
|
-
await
|
|
1564
|
+
await spritesExec({
|
|
2065
1565
|
spriteName,
|
|
2066
1566
|
command: "sh",
|
|
2067
1567
|
args: ["-lc", cmd],
|
|
@@ -2114,7 +1614,7 @@ export class SandboxService {
|
|
|
2114
1614
|
return { ok: false, error: "sprites_name_required" };
|
|
2115
1615
|
const filePath = String(path ?? "").trim();
|
|
2116
1616
|
const cmd = `if [ -f ${SandboxService.shellEscapeArg(filePath)} ]; then base64 ${SandboxService.shellEscapeArg(filePath)} | tr -d '\\n'; fi`;
|
|
2117
|
-
const res = await
|
|
1617
|
+
const res = await spritesExec({
|
|
2118
1618
|
spriteName,
|
|
2119
1619
|
command: "sh",
|
|
2120
1620
|
args: ["-lc", cmd],
|
|
@@ -2160,31 +1660,6 @@ export class SandboxService {
|
|
|
2160
1660
|
return { ok: false, error: formatInstantSchemaError(e) };
|
|
2161
1661
|
}
|
|
2162
1662
|
}
|
|
2163
|
-
static parseSpritesCheckpointIdFromNdjson(text) {
|
|
2164
|
-
const lines = String(text ?? "")
|
|
2165
|
-
.split("\n")
|
|
2166
|
-
.map((l) => l.trim())
|
|
2167
|
-
.filter(Boolean);
|
|
2168
|
-
const candidates = [];
|
|
2169
|
-
for (const line of lines) {
|
|
2170
|
-
try {
|
|
2171
|
-
const evt = JSON.parse(line);
|
|
2172
|
-
const data = typeof evt?.data === "string" ? evt.data : "";
|
|
2173
|
-
if (!data)
|
|
2174
|
-
continue;
|
|
2175
|
-
const m = data.match(/\bID:\s*(v[0-9]+)\b/i) || data.match(/\bCheckpoint\s+(v[0-9]+)\b/i);
|
|
2176
|
-
if (m?.[1]) {
|
|
2177
|
-
candidates.push(String(m[1]));
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
catch {
|
|
2181
|
-
// ignore invalid ndjson lines
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
if (candidates.length === 0)
|
|
2185
|
-
return null;
|
|
2186
|
-
return candidates[candidates.length - 1] ?? null;
|
|
2187
|
-
}
|
|
2188
1663
|
async createCheckpoint(sandboxId, params) {
|
|
2189
1664
|
try {
|
|
2190
1665
|
const recordResult = await this.adminDb.query({
|
|
@@ -2224,7 +1699,7 @@ export class SandboxService {
|
|
|
2224
1699
|
const name = String(record.externalSandboxId).trim();
|
|
2225
1700
|
const comment = String(params?.comment ?? "").trim();
|
|
2226
1701
|
const body = comment ? { comment } : {};
|
|
2227
|
-
const res = await
|
|
1702
|
+
const res = await spritesFetch(`/v1/sprites/${encodeURIComponent(name)}/checkpoint`, {
|
|
2228
1703
|
method: "POST",
|
|
2229
1704
|
headers: { "Content-Type": "application/json" },
|
|
2230
1705
|
body: JSON.stringify(body),
|
|
@@ -2233,7 +1708,7 @@ export class SandboxService {
|
|
|
2233
1708
|
if (!res?.ok) {
|
|
2234
1709
|
return { ok: false, error: text || `sprites_checkpoint_http_${res?.status ?? "unknown"}` };
|
|
2235
1710
|
}
|
|
2236
|
-
const checkpointId =
|
|
1711
|
+
const checkpointId = parseSpritesCheckpointIdFromNdjson(text);
|
|
2237
1712
|
if (!checkpointId) {
|
|
2238
1713
|
return { ok: false, error: "sprites_checkpoint_id_missing" };
|
|
2239
1714
|
}
|
|
@@ -2260,7 +1735,7 @@ export class SandboxService {
|
|
|
2260
1735
|
});
|
|
2261
1736
|
const record = recordResult?.sandbox_sandboxes?.[0];
|
|
2262
1737
|
if (record?.externalSandboxId && record.provider === "vercel") {
|
|
2263
|
-
const creds = await
|
|
1738
|
+
const creds = await resolveVercelCredentials(record?.params ?? {});
|
|
2264
1739
|
const listed = await VercelSnapshot.list({
|
|
2265
1740
|
teamId: creds.teamId,
|
|
2266
1741
|
projectId: creds.projectId,
|
|
@@ -2278,7 +1753,7 @@ export class SandboxService {
|
|
|
2278
1753
|
return { ok: false, error: "checkpoint_not_supported" };
|
|
2279
1754
|
}
|
|
2280
1755
|
const name = String(record.externalSandboxId).trim();
|
|
2281
|
-
const json = await
|
|
1756
|
+
const json = await spritesJson(`/v1/sprites/${encodeURIComponent(name)}/checkpoints`, {
|
|
2282
1757
|
method: "GET",
|
|
2283
1758
|
headers: { Accept: "application/json" },
|
|
2284
1759
|
});
|
|
@@ -2305,7 +1780,7 @@ export class SandboxService {
|
|
|
2305
1780
|
const cp = String(checkpointId ?? "").trim();
|
|
2306
1781
|
if (!cp)
|
|
2307
1782
|
return { ok: false, error: "checkpoint_id_required" };
|
|
2308
|
-
const res = await
|
|
1783
|
+
const res = await spritesFetch(`/v1/sprites/${encodeURIComponent(name)}/checkpoints/${encodeURIComponent(cp)}/restore`, { method: "POST" });
|
|
2309
1784
|
const text = await res?.text?.().catch(() => "");
|
|
2310
1785
|
if (!res?.ok) {
|
|
2311
1786
|
return { ok: false, error: text || `sprites_restore_http_${res?.status ?? "unknown"}` };
|