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