@ekairos/sandbox 1.22.39-beta.development.0 → 1.22.39

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.
Files changed (57) hide show
  1. package/dist/action-steps.d.ts +156 -0
  2. package/dist/action-steps.d.ts.map +1 -0
  3. package/dist/action-steps.js +153 -0
  4. package/dist/action-steps.js.map +1 -0
  5. package/dist/actions.d.ts +259 -169
  6. package/dist/actions.d.ts.map +1 -1
  7. package/dist/actions.js +176 -269
  8. package/dist/actions.js.map +1 -1
  9. package/dist/contract.d.ts +86 -0
  10. package/dist/contract.d.ts.map +1 -0
  11. package/dist/contract.js +83 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/domain.d.ts +2 -0
  14. package/dist/domain.d.ts.map +1 -0
  15. package/dist/domain.js +2 -0
  16. package/dist/domain.js.map +1 -0
  17. package/dist/index.d.ts +5 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/providers/daytona.d.ts +14 -0
  22. package/dist/providers/daytona.d.ts.map +1 -0
  23. package/dist/providers/daytona.js +153 -0
  24. package/dist/providers/daytona.js.map +1 -0
  25. package/dist/providers/provider.d.ts +3 -0
  26. package/dist/providers/provider.d.ts.map +1 -0
  27. package/dist/providers/provider.js +18 -0
  28. package/dist/providers/provider.js.map +1 -0
  29. package/dist/providers/sprites.d.ts +39 -0
  30. package/dist/providers/sprites.d.ts.map +1 -0
  31. package/dist/providers/sprites.js +234 -0
  32. package/dist/providers/sprites.js.map +1 -0
  33. package/dist/providers/types.d.ts +15 -0
  34. package/dist/providers/types.d.ts.map +1 -0
  35. package/dist/providers/types.js +9 -0
  36. package/dist/providers/types.js.map +1 -0
  37. package/dist/providers/vercel.d.ts +26 -0
  38. package/dist/providers/vercel.d.ts.map +1 -0
  39. package/dist/providers/vercel.js +182 -0
  40. package/dist/providers/vercel.js.map +1 -0
  41. package/dist/public.d.ts +55 -3
  42. package/dist/public.d.ts.map +1 -1
  43. package/dist/public.js +2 -5
  44. package/dist/public.js.map +1 -1
  45. package/dist/sandbox.d.ts +76 -0
  46. package/dist/sandbox.d.ts.map +1 -0
  47. package/dist/sandbox.js +154 -0
  48. package/dist/sandbox.js.map +1 -0
  49. package/dist/schema.d.ts +18 -1
  50. package/dist/schema.d.ts.map +1 -1
  51. package/dist/schema.js +66 -1
  52. package/dist/schema.js.map +1 -1
  53. package/dist/service.d.ts +10 -44
  54. package/dist/service.d.ts.map +1 -1
  55. package/dist/service.js +38 -565
  56. package/dist/service.js.map +1 -1
  57. package/package.json +25 -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, Image } from "@daytonaio/sdk";
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 = SandboxService.resolveProvider(config);
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 = SandboxService.resolveProvider(config);
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(SandboxService.getDaytonaConfig());
1084
- const language = SandboxService.resolveDaytonaLanguage(config);
577
+ const daytona = new Daytona(getDaytonaConfig());
578
+ const language = resolveDaytonaLanguage(config);
1085
579
  const requestedVolumes = config.daytona?.volumes ?? [];
1086
- const volumes = await SandboxService.resolveDaytonaVolumes(daytona, requestedVolumes);
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 = SandboxService.buildDeclarativeImage(config);
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 SandboxService.provisionSpritesSandbox({
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 SandboxService.provisionVercelSandbox(config, {
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(SandboxService.getDaytonaConfig());
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 SandboxService.getSpritesByName(name);
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 = SandboxService.asSpritesSandbox({
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 SandboxService.resolveVercelCredentials(record?.params ?? {});
835
+ const creds = await resolveVercelCredentials(record?.params ?? {});
1342
836
  try {
1343
837
  const maxAttempts = 20;
1344
838
  const delayMs = 500;
@@ -1348,6 +842,7 @@ export class SandboxService {
1348
842
  teamId: creds.teamId,
1349
843
  projectId: creds.projectId,
1350
844
  token: creds.token,
845
+ resume: true,
1351
846
  });
1352
847
  if (!sandbox)
1353
848
  return { ok: false, error: "Sandbox not found" };
@@ -1375,19 +870,21 @@ export class SandboxService {
1375
870
  }
1376
871
  }
1377
872
  async getSandboxRecord(sandboxId) {
1378
- const recordResult = await this.adminDb.query({
873
+ const query = {
1379
874
  sandbox_sandboxes: { $: { where: { id: sandboxId }, limit: 1 }, user: {} },
1380
- });
875
+ };
876
+ const recordResult = await this.adminDb.query(query);
1381
877
  return recordResult?.sandbox_sandboxes?.[0] ?? null;
1382
878
  }
1383
879
  async getProcessSnapshot(processId) {
1384
880
  try {
1385
- const processResult = await this.adminDb.query({
881
+ const query = {
1386
882
  sandbox_processes: {
1387
883
  $: { where: { id: processId }, limit: 1 },
1388
884
  sandbox: {},
1389
885
  },
1390
- });
886
+ };
887
+ const processResult = await this.adminDb.query(query);
1391
888
  const processRow = processResult?.sandbox_processes?.[0];
1392
889
  if (!processRow)
1393
890
  return { ok: false, error: "sandbox_process_not_found" };
@@ -1454,12 +951,13 @@ export class SandboxService {
1454
951
  }));
1455
952
  }
1456
953
  async readProcessRow(processId) {
1457
- const result = await this.adminDb.query({
954
+ const query = {
1458
955
  sandbox_processes: {
1459
956
  $: { where: { id: processId }, limit: 1 },
1460
957
  sandbox: {},
1461
958
  },
1462
- });
959
+ };
960
+ const result = await this.adminDb.query(query);
1463
961
  return result?.sandbox_processes?.[0] ?? null;
1464
962
  }
1465
963
  async writeProcessChunkByProcessId(processId, type, data, opts) {
@@ -1655,7 +1153,7 @@ export class SandboxService {
1655
1153
  else if (sandbox?.__provider === "sprites") {
1656
1154
  // Sprites does not have a reliable "stop" semantic; deleting is the durable cleanup primitive.
1657
1155
  try {
1658
- await SandboxService.spritesFetch(`/v1/sprites/${encodeURIComponent(String(sandbox.name))}`, {
1156
+ await spritesFetch(`/v1/sprites/${encodeURIComponent(String(sandbox.name))}`, {
1659
1157
  method: "DELETE",
1660
1158
  });
1661
1159
  }
@@ -1664,7 +1162,7 @@ export class SandboxService {
1664
1162
  }
1665
1163
  }
1666
1164
  else {
1667
- const daytona = new Daytona(SandboxService.getDaytonaConfig());
1165
+ const daytona = new Daytona(getDaytonaConfig());
1668
1166
  await daytona.stop(sandbox);
1669
1167
  if (deleteOnStop) {
1670
1168
  try {
@@ -1739,7 +1237,7 @@ export class SandboxService {
1739
1237
  }
1740
1238
  if (sandbox.__provider === "sprites") {
1741
1239
  const fullCommand = args.length > 0 ? [command, ...args].join(" ") : command;
1742
- const res = await SandboxService.spritesExec({
1240
+ const res = await spritesExec({
1743
1241
  spriteName: String(sandbox.name ?? ""),
1744
1242
  command,
1745
1243
  args,
@@ -2063,7 +1561,7 @@ export class SandboxService {
2063
1561
  const dirPath = filePath.includes("/") ? filePath.split("/").slice(0, -1).join("/") : "";
2064
1562
  const dirCmd = dirPath ? `mkdir -p ${SandboxService.shellEscapeArg(dirPath)} && ` : "";
2065
1563
  const cmd = `${dirCmd}printf %s ${SandboxService.shellEscapeArg(String(f.contentBase64 ?? ""))} | base64 -d > ${SandboxService.shellEscapeArg(filePath)}`;
2066
- await SandboxService.spritesExec({
1564
+ await spritesExec({
2067
1565
  spriteName,
2068
1566
  command: "sh",
2069
1567
  args: ["-lc", cmd],
@@ -2116,7 +1614,7 @@ export class SandboxService {
2116
1614
  return { ok: false, error: "sprites_name_required" };
2117
1615
  const filePath = String(path ?? "").trim();
2118
1616
  const cmd = `if [ -f ${SandboxService.shellEscapeArg(filePath)} ]; then base64 ${SandboxService.shellEscapeArg(filePath)} | tr -d '\\n'; fi`;
2119
- const res = await SandboxService.spritesExec({
1617
+ const res = await spritesExec({
2120
1618
  spriteName,
2121
1619
  command: "sh",
2122
1620
  args: ["-lc", cmd],
@@ -2162,31 +1660,6 @@ export class SandboxService {
2162
1660
  return { ok: false, error: formatInstantSchemaError(e) };
2163
1661
  }
2164
1662
  }
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
1663
  async createCheckpoint(sandboxId, params) {
2191
1664
  try {
2192
1665
  const recordResult = await this.adminDb.query({
@@ -2226,7 +1699,7 @@ export class SandboxService {
2226
1699
  const name = String(record.externalSandboxId).trim();
2227
1700
  const comment = String(params?.comment ?? "").trim();
2228
1701
  const body = comment ? { comment } : {};
2229
- const res = await SandboxService.spritesFetch(`/v1/sprites/${encodeURIComponent(name)}/checkpoint`, {
1702
+ const res = await spritesFetch(`/v1/sprites/${encodeURIComponent(name)}/checkpoint`, {
2230
1703
  method: "POST",
2231
1704
  headers: { "Content-Type": "application/json" },
2232
1705
  body: JSON.stringify(body),
@@ -2235,7 +1708,7 @@ export class SandboxService {
2235
1708
  if (!res?.ok) {
2236
1709
  return { ok: false, error: text || `sprites_checkpoint_http_${res?.status ?? "unknown"}` };
2237
1710
  }
2238
- const checkpointId = SandboxService.parseSpritesCheckpointIdFromNdjson(text);
1711
+ const checkpointId = parseSpritesCheckpointIdFromNdjson(text);
2239
1712
  if (!checkpointId) {
2240
1713
  return { ok: false, error: "sprites_checkpoint_id_missing" };
2241
1714
  }
@@ -2262,7 +1735,7 @@ export class SandboxService {
2262
1735
  });
2263
1736
  const record = recordResult?.sandbox_sandboxes?.[0];
2264
1737
  if (record?.externalSandboxId && record.provider === "vercel") {
2265
- const creds = await SandboxService.resolveVercelCredentials(record?.params ?? {});
1738
+ const creds = await resolveVercelCredentials(record?.params ?? {});
2266
1739
  const listed = await VercelSnapshot.list({
2267
1740
  teamId: creds.teamId,
2268
1741
  projectId: creds.projectId,
@@ -2280,7 +1753,7 @@ export class SandboxService {
2280
1753
  return { ok: false, error: "checkpoint_not_supported" };
2281
1754
  }
2282
1755
  const name = String(record.externalSandboxId).trim();
2283
- const json = await SandboxService.spritesJson(`/v1/sprites/${encodeURIComponent(name)}/checkpoints`, {
1756
+ const json = await spritesJson(`/v1/sprites/${encodeURIComponent(name)}/checkpoints`, {
2284
1757
  method: "GET",
2285
1758
  headers: { Accept: "application/json" },
2286
1759
  });
@@ -2307,7 +1780,7 @@ export class SandboxService {
2307
1780
  const cp = String(checkpointId ?? "").trim();
2308
1781
  if (!cp)
2309
1782
  return { ok: false, error: "checkpoint_id_required" };
2310
- const res = await SandboxService.spritesFetch(`/v1/sprites/${encodeURIComponent(name)}/checkpoints/${encodeURIComponent(cp)}/restore`, { method: "POST" });
1783
+ const res = await spritesFetch(`/v1/sprites/${encodeURIComponent(name)}/checkpoints/${encodeURIComponent(cp)}/restore`, { method: "POST" });
2311
1784
  const text = await res?.text?.().catch(() => "");
2312
1785
  if (!res?.ok) {
2313
1786
  return { ok: false, error: text || `sprites_restore_http_${res?.status ?? "unknown"}` };