@sandboxrouter/sdk 0.0.0-stage → 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -2
  3. package/dist/bench-store.d.ts +12 -0
  4. package/dist/bench-store.js +23 -0
  5. package/dist/bench.d.ts +24 -0
  6. package/dist/bench.js +65 -0
  7. package/dist/catalog.d.ts +35 -0
  8. package/dist/catalog.js +198 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +195 -0
  11. package/dist/computers.d.ts +124 -0
  12. package/dist/computers.js +66 -0
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.js +17 -0
  15. package/dist/leaderboard.d.ts +39 -0
  16. package/dist/leaderboard.js +184 -0
  17. package/dist/providers/blaxel.d.ts +21 -0
  18. package/dist/providers/blaxel.js +152 -0
  19. package/dist/providers/daytona.d.ts +13 -0
  20. package/dist/providers/daytona.js +121 -0
  21. package/dist/providers/e2b.d.ts +10 -0
  22. package/dist/providers/e2b.js +69 -0
  23. package/dist/providers/maritime.d.ts +38 -0
  24. package/dist/providers/maritime.js +240 -0
  25. package/dist/providers/modal.d.ts +10 -0
  26. package/dist/providers/modal.js +82 -0
  27. package/dist/providers/morph.d.ts +19 -0
  28. package/dist/providers/morph.js +181 -0
  29. package/dist/providers/runloop.d.ts +16 -0
  30. package/dist/providers/runloop.js +145 -0
  31. package/dist/providers/sprites.d.ts +15 -0
  32. package/dist/providers/sprites.js +135 -0
  33. package/dist/providers/tensorlake.d.ts +14 -0
  34. package/dist/providers/tensorlake.js +192 -0
  35. package/dist/providers/vercel.d.ts +12 -0
  36. package/dist/providers/vercel.js +87 -0
  37. package/dist/router.d.ts +65 -0
  38. package/dist/router.js +180 -0
  39. package/dist/server.d.ts +31 -0
  40. package/dist/server.js +257 -0
  41. package/dist/sse.d.ts +6 -0
  42. package/dist/sse.js +33 -0
  43. package/dist/types.d.ts +83 -0
  44. package/dist/types.js +19 -0
  45. package/dist/util.d.ts +11 -0
  46. package/dist/util.js +37 -0
  47. package/package.json +113 -4
@@ -0,0 +1,152 @@
1
+ import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { optionalImport, toBytes } from "../util.js";
4
+ /** Blaxel's public base image (the SDK's own default). Set image.blaxel for another image or a `bl deploy` template. */
5
+ const DEFAULT_IMAGE = "blaxel/base-image:latest";
6
+ /** Blaxel rejects a max-age ttl under 5 minutes. */
7
+ const MIN_TTL_MINUTES = 5;
8
+ /**
9
+ * Blaxel sandboxes via `@blaxel/core`. Auth: BL_API_KEY + BL_WORKSPACE (or `bl login`).
10
+ * Region: BL_REGION, else us-pdx-1. Blaxel sizes by memory only and gives 1 vCPU per 2 GB,
11
+ * so memory is max(memoryGb, 2 × vcpu) GB. Idle sandboxes drop to standby on their own and
12
+ * wake on the next call; pause() archives explicitly.
13
+ */
14
+ export class BlaxelProvider {
15
+ apiKey;
16
+ workspace;
17
+ region;
18
+ id = "blaxel";
19
+ ready;
20
+ constructor(apiKey = process.env.BL_API_KEY, workspace = process.env.BL_WORKSPACE, region = process.env.BL_REGION ?? "us-pdx-1") {
21
+ this.apiKey = apiKey;
22
+ this.workspace = workspace;
23
+ this.region = region;
24
+ }
25
+ configured() {
26
+ return !!this.apiKey && !!this.workspace;
27
+ }
28
+ /** Load the SDK once and point its (global) settings at our credentials. */
29
+ sdk() {
30
+ this.ready ??= optionalImport("@blaxel/core", "blaxel").then((m) => {
31
+ if (this.apiKey && this.workspace)
32
+ m.initialize({ apiKey: this.apiKey, workspace: this.workspace });
33
+ return m;
34
+ });
35
+ return this.ready;
36
+ }
37
+ async create(spec) {
38
+ const { SandboxInstance } = await this.sdk();
39
+ const memoryGb = Math.max(spec.memoryGb ?? DEFAULT_MEMORY_GB, (spec.vcpu ?? DEFAULT_VCPU) * 2);
40
+ const ttlMinutes = Math.max(MIN_TTL_MINUTES, Math.round(spec.timeoutMinutes ?? 30));
41
+ try {
42
+ const sbx = await SandboxInstance.create({
43
+ image: spec.image?.blaxel ?? DEFAULT_IMAGE,
44
+ memory: Math.round(memoryGb * 1024),
45
+ region: this.region,
46
+ envs: Object.entries(spec.env ?? {}).map(([name, value]) => ({ name, value })),
47
+ labels: spec.metadata,
48
+ // Max-age from creation, not an idle timeout.
49
+ ttl: `${ttlMinutes}m`,
50
+ });
51
+ return new BlaxelBox(sbx);
52
+ }
53
+ catch (err) {
54
+ throw wrap(err);
55
+ }
56
+ }
57
+ /** Reattach by sandbox name; an archived (paused) sandbox is unarchived first. */
58
+ async connect(nativeId) {
59
+ const { SandboxInstance } = await this.sdk();
60
+ try {
61
+ let sbx = await SandboxInstance.get(nativeId);
62
+ if (sbx.status === "ARCHIVED" || sbx.status === "ARCHIVING")
63
+ sbx = await SandboxInstance.unarchive(nativeId);
64
+ return new BlaxelBox(sbx);
65
+ }
66
+ catch (err) {
67
+ throw wrap(err);
68
+ }
69
+ }
70
+ }
71
+ class BlaxelBox {
72
+ sbx;
73
+ nativeId;
74
+ constructor(sbx) {
75
+ this.sbx = sbx;
76
+ const name = sbx.metadata.name;
77
+ if (!name)
78
+ throw new ProviderError("blaxel", "sandbox has no name");
79
+ this.nativeId = name;
80
+ }
81
+ async exec(cmd, opts = {}) {
82
+ const t0 = Date.now();
83
+ const timeoutSeconds = opts.timeoutSeconds ?? 60;
84
+ let r;
85
+ try {
86
+ // With onStdout/onStderr the SDK streams logs from the same request as the command runs.
87
+ r = await this.sbx.process.exec({
88
+ command: cmd,
89
+ workingDir: opts.cwd,
90
+ env: opts.env,
91
+ timeout: timeoutSeconds,
92
+ waitForCompletion: true,
93
+ onStdout: opts.onStdout,
94
+ onStderr: opts.onStderr,
95
+ });
96
+ if (r.status === "running")
97
+ r = await this.sbx.process.wait(r.pid, { maxWait: timeoutSeconds * 1000 });
98
+ }
99
+ catch (err) {
100
+ throw wrap(err);
101
+ }
102
+ if (r.status === "killed" || r.status === "stopped") {
103
+ throw new ProviderError("blaxel", `command ${r.status} (timeout ${timeoutSeconds}s?)`);
104
+ }
105
+ // A nonzero exit is a result (status "failed"), not an error.
106
+ return { exitCode: r.exitCode, stdout: r.stdout ?? "", stderr: r.stderr ?? "", durationMs: Date.now() - t0 };
107
+ }
108
+ async writeFile(path, content) {
109
+ try {
110
+ await this.sbx.fs.writeBinary(path, toBytes(content));
111
+ }
112
+ catch (err) {
113
+ throw wrap(err);
114
+ }
115
+ }
116
+ async readFile(path) {
117
+ try {
118
+ return new Uint8Array(await (await this.sbx.fs.readBinary(path)).arrayBuffer());
119
+ }
120
+ catch (err) {
121
+ throw wrap(err);
122
+ }
123
+ }
124
+ /**
125
+ * Archive: the filesystem is kept, memory and processes are lost. The max-age ttl is cleared
126
+ * first so the archive survives until kill(). router.get() unarchives it.
127
+ */
128
+ async pause() {
129
+ const { SandboxInstance } = await optionalImport("@blaxel/core", "blaxel");
130
+ try {
131
+ await SandboxInstance.updateTtl(this.nativeId, null);
132
+ await this.sbx.archive();
133
+ }
134
+ catch (err) {
135
+ throw wrap(err);
136
+ }
137
+ }
138
+ async kill() {
139
+ try {
140
+ await this.sbx.delete();
141
+ }
142
+ catch (err) {
143
+ throw wrap(err);
144
+ }
145
+ }
146
+ }
147
+ function wrap(err) {
148
+ if (err instanceof ProviderError)
149
+ return err;
150
+ const status = typeof err?.status === "number" ? err.status : err?.response?.status;
151
+ return new ProviderError("blaxel", err?.message ?? String(err), status);
152
+ }
@@ -0,0 +1,13 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ /** Daytona via `@daytonaio/sdk`. Default snapshot; set image.daytona to start from an image. */
3
+ export declare class DaytonaProvider implements Provider {
4
+ private apiKey;
5
+ readonly id: "daytona";
6
+ private client?;
7
+ constructor(apiKey?: string | undefined);
8
+ configured(): boolean;
9
+ private daytona;
10
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
11
+ /** Reattach; a paused (stopped) sandbox is started again. */
12
+ connect(nativeId: string): Promise<ProviderSandbox>;
13
+ }
@@ -0,0 +1,121 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ProviderError } from "../types.js";
3
+ import { optionalImport, toBytes, wrapCommand } from "../util.js";
4
+ /** Daytona via `@daytonaio/sdk`. Default snapshot; set image.daytona to start from an image. */
5
+ export class DaytonaProvider {
6
+ apiKey;
7
+ id = "daytona";
8
+ client;
9
+ constructor(apiKey = process.env.DAYTONA_API_KEY) {
10
+ this.apiKey = apiKey;
11
+ }
12
+ configured() {
13
+ return !!this.apiKey;
14
+ }
15
+ async daytona() {
16
+ if (!this.client) {
17
+ const { Daytona } = await optionalImport("@daytonaio/sdk", "daytona");
18
+ this.client = new Daytona({ apiKey: this.apiKey });
19
+ }
20
+ return this.client;
21
+ }
22
+ async create(spec) {
23
+ const d = await this.daytona();
24
+ const base = {
25
+ envVars: spec.env,
26
+ labels: spec.metadata,
27
+ // Minutes of inactivity before Daytona stops it; delete right after stop.
28
+ autoStopInterval: Math.max(1, Math.round(spec.timeoutMinutes ?? 30)),
29
+ autoDeleteInterval: 0,
30
+ };
31
+ const sbx = spec.image?.daytona
32
+ ? await d.create({
33
+ ...base,
34
+ image: spec.image.daytona,
35
+ resources: { cpu: spec.vcpu, memory: spec.memoryGb },
36
+ })
37
+ : await d.create(base);
38
+ return new DaytonaBox(sbx);
39
+ }
40
+ /** Reattach; a paused (stopped) sandbox is started again. */
41
+ async connect(nativeId) {
42
+ const sbx = await (await this.daytona()).get(nativeId);
43
+ if (sbx.state === "stopped")
44
+ await sbx.start();
45
+ return new DaytonaBox(sbx);
46
+ }
47
+ }
48
+ class DaytonaBox {
49
+ sbx;
50
+ nativeId;
51
+ constructor(sbx) {
52
+ this.sbx = sbx;
53
+ this.nativeId = sbx.id;
54
+ }
55
+ async exec(cmd, opts = {}) {
56
+ if (opts.onStdout || opts.onStderr)
57
+ return this.execStreaming(cmd, opts);
58
+ const t0 = Date.now();
59
+ try {
60
+ const r = await this.sbx.process.executeCommand(cmd, opts.cwd, opts.env, opts.timeoutSeconds ?? 60);
61
+ // Daytona merges stdout and stderr into `result` here.
62
+ return { exitCode: r.exitCode, stdout: r.result, stderr: "", durationMs: Date.now() - t0 };
63
+ }
64
+ catch (err) {
65
+ throw new ProviderError("daytona", err?.message ?? String(err));
66
+ }
67
+ }
68
+ /** Streaming goes through a throwaway session, which also keeps stdout and stderr apart. */
69
+ async execStreaming(cmd, opts) {
70
+ const t0 = Date.now();
71
+ const session = `sbr-${randomUUID()}`;
72
+ const proc = this.sbx.process;
73
+ let stdout = "";
74
+ let stderr = "";
75
+ try {
76
+ await proc.createSession(session);
77
+ const { cmdId } = await proc.executeSessionCommand(session, {
78
+ command: wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }),
79
+ runAsync: true,
80
+ });
81
+ const logs = proc.getSessionCommandLogs(session, cmdId, (c) => {
82
+ stdout += c;
83
+ opts.onStdout?.(c);
84
+ }, (c) => {
85
+ stderr += c;
86
+ opts.onStderr?.(c);
87
+ });
88
+ const limit = (opts.timeoutSeconds ?? 60) * 1000;
89
+ const timedOut = await Promise.race([logs.then(() => false), sleep(limit).then(() => true)]);
90
+ if (timedOut)
91
+ throw new ProviderError("daytona", `command timed out after ${limit / 1000}s`);
92
+ const done = await proc.getSessionCommand(session, cmdId);
93
+ return { exitCode: done.exitCode ?? -1, stdout, stderr, durationMs: Date.now() - t0 };
94
+ }
95
+ catch (err) {
96
+ if (err instanceof ProviderError)
97
+ throw err;
98
+ throw new ProviderError("daytona", err?.message ?? String(err));
99
+ }
100
+ finally {
101
+ await proc.deleteSession(session).catch(() => { });
102
+ }
103
+ }
104
+ async writeFile(path, content) {
105
+ await this.sbx.fs.uploadFile(Buffer.from(toBytes(content)), path);
106
+ }
107
+ async readFile(path) {
108
+ return new Uint8Array(await this.sbx.fs.downloadFile(path));
109
+ }
110
+ /** Stop and keep the disk until kill(). router.get() starts it again. */
111
+ async pause() {
112
+ await this.sbx.setAutoDeleteInterval(-1);
113
+ await this.sbx.stop();
114
+ }
115
+ async kill() {
116
+ await this.sbx.delete();
117
+ }
118
+ }
119
+ function sleep(ms) {
120
+ return new Promise((r) => setTimeout(r, ms).unref());
121
+ }
@@ -0,0 +1,10 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ /** E2B via the official `e2b` SDK. vCPU and memory come from the template, not the create call. */
3
+ export declare class E2BProvider implements Provider {
4
+ private apiKey;
5
+ readonly id: "e2b";
6
+ constructor(apiKey?: string | undefined);
7
+ configured(): boolean;
8
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
9
+ connect(nativeId: string): Promise<ProviderSandbox>;
10
+ }
@@ -0,0 +1,69 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { optionalImport, toBytes } from "../util.js";
3
+ /** E2B via the official `e2b` SDK. vCPU and memory come from the template, not the create call. */
4
+ export class E2BProvider {
5
+ apiKey;
6
+ id = "e2b";
7
+ constructor(apiKey = process.env.E2B_API_KEY) {
8
+ this.apiKey = apiKey;
9
+ }
10
+ configured() {
11
+ return !!this.apiKey;
12
+ }
13
+ async create(spec) {
14
+ const { Sandbox } = await optionalImport("e2b", "e2b");
15
+ const opts = {
16
+ apiKey: this.apiKey,
17
+ timeoutMs: (spec.timeoutMinutes ?? 30) * 60_000,
18
+ envs: spec.env,
19
+ metadata: spec.metadata,
20
+ };
21
+ const sbx = spec.image?.e2b ? await Sandbox.create(spec.image.e2b, opts) : await Sandbox.create(opts);
22
+ return new E2BBox(sbx);
23
+ }
24
+ async connect(nativeId) {
25
+ const { Sandbox } = await optionalImport("e2b", "e2b");
26
+ return new E2BBox(await Sandbox.connect(nativeId, { apiKey: this.apiKey }));
27
+ }
28
+ }
29
+ class E2BBox {
30
+ sbx;
31
+ nativeId;
32
+ constructor(sbx) {
33
+ this.sbx = sbx;
34
+ this.nativeId = sbx.sandboxId;
35
+ }
36
+ async exec(cmd, opts = {}) {
37
+ const t0 = Date.now();
38
+ try {
39
+ const r = await this.sbx.commands.run(cmd, {
40
+ cwd: opts.cwd,
41
+ envs: opts.env,
42
+ timeoutMs: (opts.timeoutSeconds ?? 60) * 1000,
43
+ onStdout: opts.onStdout,
44
+ onStderr: opts.onStderr,
45
+ });
46
+ return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, durationMs: Date.now() - t0 };
47
+ }
48
+ catch (err) {
49
+ // The SDK throws CommandExitError for a nonzero exit; that's a result, not a failure.
50
+ if (err?.name === "CommandExitError" || typeof err?.exitCode === "number") {
51
+ return { exitCode: err.exitCode, stdout: err.stdout ?? "", stderr: err.stderr ?? "", durationMs: Date.now() - t0 };
52
+ }
53
+ throw new ProviderError("e2b", err?.message ?? String(err));
54
+ }
55
+ }
56
+ async writeFile(path, content) {
57
+ await this.sbx.files.write(path, toBytes(content));
58
+ }
59
+ async readFile(path) {
60
+ return await this.sbx.files.read(path, { format: "bytes" });
61
+ }
62
+ /** Snapshot memory and disk. Sandbox.connect (router.get) resumes it. */
63
+ async pause() {
64
+ await this.sbx.pause();
65
+ }
66
+ async kill() {
67
+ await this.sbx.kill();
68
+ }
69
+ }
@@ -0,0 +1,38 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ import type { ComputerOs, ComputerProvider, ComputerSpec, ProviderComputer } from "../computers.js";
3
+ /**
4
+ * Maritime over plain REST (no SDK needed).
5
+ * Control plane: https://api.maritime.sh (Bearer mk_ key)
6
+ * Data plane: sandbox.dataPlaneUrl (X-Maritime-Token: sandbox.secret)
7
+ * POST /commands run; with Accept: text/event-stream it streams stdout/stderr events
8
+ * GET|PUT /files?path= raw bytes
9
+ * Any data plane call resumes a paused sandbox.
10
+ * Docs: https://maritime.sh/docs/ephemeral-sandboxes
11
+ */
12
+ export declare class MaritimeProvider implements Provider, ComputerProvider {
13
+ private apiKey;
14
+ private baseUrl;
15
+ readonly id: "maritime";
16
+ readonly computerOs: ComputerOs[];
17
+ constructor(apiKey?: string | undefined, baseUrl?: string);
18
+ configured(): boolean;
19
+ /** @internal */
20
+ request(method: string, url: string, init?: {
21
+ json?: unknown;
22
+ bytes?: Uint8Array;
23
+ headers?: Record<string, string>;
24
+ }): Promise<Response>;
25
+ /** @internal */
26
+ json<T>(method: string, url: string, init?: {
27
+ json?: unknown;
28
+ headers?: Record<string, string>;
29
+ }): Promise<T>;
30
+ /** @internal */
31
+ control<T>(method: string, path: string, json?: unknown): Promise<T>;
32
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
33
+ /** @internal Control-plane call that returns the raw Response (binary bodies). */
34
+ controlRaw(method: string, path: string, bytes?: Uint8Array): Promise<Response>;
35
+ createComputer(spec: ComputerSpec): Promise<ProviderComputer>;
36
+ connectComputer(nativeId: string): Promise<ProviderComputer>;
37
+ connect(nativeId: string): Promise<ProviderSandbox>;
38
+ }
@@ -0,0 +1,240 @@
1
+ import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { toBytes } from "../util.js";
4
+ import { readSSE } from "../sse.js";
5
+ /**
6
+ * Maritime over plain REST (no SDK needed).
7
+ * Control plane: https://api.maritime.sh (Bearer mk_ key)
8
+ * Data plane: sandbox.dataPlaneUrl (X-Maritime-Token: sandbox.secret)
9
+ * POST /commands run; with Accept: text/event-stream it streams stdout/stderr events
10
+ * GET|PUT /files?path= raw bytes
11
+ * Any data plane call resumes a paused sandbox.
12
+ * Docs: https://maritime.sh/docs/ephemeral-sandboxes
13
+ */
14
+ export class MaritimeProvider {
15
+ apiKey;
16
+ baseUrl;
17
+ id = "maritime";
18
+ computerOs = ["linux", "windows", "android"];
19
+ constructor(apiKey = process.env.MARITIME_API_KEY, baseUrl = process.env.MARITIME_API_URL ?? "https://api.maritime.sh") {
20
+ this.apiKey = apiKey;
21
+ this.baseUrl = baseUrl;
22
+ }
23
+ configured() {
24
+ return !!this.apiKey;
25
+ }
26
+ /** @internal */
27
+ async request(method, url, init = {}) {
28
+ const headers = { ...init.headers };
29
+ let body;
30
+ if (init.json !== undefined) {
31
+ headers["Content-Type"] = "application/json";
32
+ body = JSON.stringify(init.json);
33
+ }
34
+ else if (init.bytes) {
35
+ headers["Content-Type"] = "application/octet-stream";
36
+ body = init.bytes;
37
+ }
38
+ const res = await fetch(url, { method, headers, body });
39
+ if (!res.ok) {
40
+ const text = await res.text();
41
+ let detail = text.slice(0, 300);
42
+ try {
43
+ const j = JSON.parse(text);
44
+ // Sandboxes: {code, message}. Computers: {error, message, retryAfterS}. Agents: {detail}.
45
+ const code = j?.code ?? (typeof j?.error === "string" ? j.error : undefined);
46
+ detail = code ? `${code}: ${j.message ?? ""}` : j?.detail ? JSON.stringify(j.detail) : detail;
47
+ }
48
+ catch {
49
+ /* non-JSON body */
50
+ }
51
+ throw new ProviderError(this.id, `${method} ${new URL(url).pathname} -> ${res.status} ${detail}`, res.status);
52
+ }
53
+ return res;
54
+ }
55
+ /** @internal */
56
+ async json(method, url, init = {}) {
57
+ const text = await (await this.request(method, url, init)).text();
58
+ return (text ? JSON.parse(text) : {});
59
+ }
60
+ /** @internal */
61
+ control(method, path, json) {
62
+ if (!this.apiKey)
63
+ throw new ProviderError(this.id, "MARITIME_API_KEY is not set");
64
+ return this.json(method, `${this.baseUrl}${path}`, { json, headers: { Authorization: `Bearer ${this.apiKey}` } });
65
+ }
66
+ async create(spec) {
67
+ const timeLimit = clamp(Math.round(spec.timeoutMinutes ?? 30), 5, 1440);
68
+ const body = {
69
+ from: spec.image?.maritime ?? "base",
70
+ vcpu: clamp(Math.round(spec.vcpu ?? DEFAULT_VCPU), 1, 16),
71
+ memoryMib: clamp(Math.ceil(spec.memoryGb ?? DEFAULT_MEMORY_GB), 1, 64) * 1024,
72
+ timeLimitMinutes: timeLimit,
73
+ // The router promises a hard lifetime, so don't let the idle timer end it much earlier.
74
+ idleMinutes: clamp(timeLimit, 1, 60),
75
+ };
76
+ if (spec.env)
77
+ body.env = spec.env;
78
+ if (spec.metadata)
79
+ body.metadata = spec.metadata;
80
+ return new MaritimeSandbox(this, await this.control("POST", "/api/sandboxes/ephemeral", body));
81
+ }
82
+ /** @internal Control-plane call that returns the raw Response (binary bodies). */
83
+ controlRaw(method, path, bytes) {
84
+ if (!this.apiKey)
85
+ throw new ProviderError(this.id, "MARITIME_API_KEY is not set");
86
+ return this.request(method, `${this.baseUrl}${path}`, { bytes, headers: { Authorization: `Bearer ${this.apiKey}` } });
87
+ }
88
+ async createComputer(spec) {
89
+ const body = { os: spec.os ?? "linux" };
90
+ for (const k of ["externalUserId", "name", "screen", "arch"])
91
+ if (spec[k] !== undefined)
92
+ body[k] = spec[k];
93
+ const c = await this.control("POST", "/api/v1/computers", body);
94
+ return new MaritimeComputer(this, c.id);
95
+ }
96
+ async connectComputer(nativeId) {
97
+ const c = await this.control("GET", `/api/v1/computers/${encodeURIComponent(nativeId)}`);
98
+ return new MaritimeComputer(this, c.id);
99
+ }
100
+ async connect(nativeId) {
101
+ const sbx = await this.control("GET", `/v1/sandboxes/${encodeURIComponent(nativeId)}`);
102
+ if (!sbx.dataPlaneUrl || !sbx.secret)
103
+ throw new ProviderError(this.id, `sandbox ${nativeId} has no data plane (state: ${sbx.state})`);
104
+ return new MaritimeSandbox(this, sbx);
105
+ }
106
+ }
107
+ class MaritimeSandbox {
108
+ p;
109
+ sbx;
110
+ nativeId;
111
+ constructor(p, sbx) {
112
+ this.p = p;
113
+ this.sbx = sbx;
114
+ this.nativeId = sbx.id;
115
+ }
116
+ /** Data plane call; on 401 re-reads the sandbox once in case the secret was rotated. */
117
+ async data(method, path, init = {}) {
118
+ const go = () => this.p.request(method, `${this.sbx.dataPlaneUrl}${path}`, {
119
+ ...init,
120
+ headers: { ...init.headers, "X-Maritime-Token": this.sbx.secret ?? "" },
121
+ });
122
+ try {
123
+ return await go();
124
+ }
125
+ catch (err) {
126
+ if (!(err instanceof ProviderError && err.status === 401))
127
+ throw err;
128
+ this.sbx = await this.p.control("GET", `/v1/sandboxes/${encodeURIComponent(this.nativeId)}`);
129
+ return go();
130
+ }
131
+ }
132
+ async exec(cmd, opts = {}) {
133
+ const t0 = Date.now();
134
+ const body = { cmd, cwd: opts.cwd, env: opts.env, timeoutSeconds: opts.timeoutSeconds ?? 60 };
135
+ const streaming = !!(opts.onStdout || opts.onStderr);
136
+ const res = await this.data("POST", "/commands", {
137
+ json: body,
138
+ headers: streaming ? { Accept: "text/event-stream" } : {},
139
+ });
140
+ let result;
141
+ if (streaming && res.headers.get("content-type")?.includes("text/event-stream")) {
142
+ for await (const ev of readSSE(res)) {
143
+ const payload = JSON.parse(ev.data);
144
+ if (ev.event === "stdout")
145
+ opts.onStdout?.(payload);
146
+ else if (ev.event === "stderr")
147
+ opts.onStderr?.(payload);
148
+ else if (ev.event === "result")
149
+ result = payload;
150
+ else if (ev.event === "error")
151
+ throw new ProviderError("maritime", `${payload?.code ?? "error"}: ${payload?.message ?? ev.data}`);
152
+ }
153
+ if (!result)
154
+ throw new ProviderError("maritime", "output stream closed before the command finished");
155
+ }
156
+ else {
157
+ result = JSON.parse(await res.text());
158
+ // Server answered without streaming: deliver the output in one chunk.
159
+ if (result.stdout)
160
+ opts.onStdout?.(result.stdout);
161
+ if (result.stderr)
162
+ opts.onStderr?.(result.stderr);
163
+ }
164
+ return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", durationMs: Date.now() - t0 };
165
+ }
166
+ async writeFile(path, content) {
167
+ await this.data("PUT", `/files?path=${encodeURIComponent(path)}`, { bytes: toBytes(content) });
168
+ }
169
+ async readFile(path) {
170
+ const res = await this.data("GET", `/files?path=${encodeURIComponent(path)}`);
171
+ return new Uint8Array(await res.arrayBuffer());
172
+ }
173
+ async pause() {
174
+ await this.p.control("POST", `/v1/sandboxes/${encodeURIComponent(this.nativeId)}/pause`);
175
+ }
176
+ async kill() {
177
+ await this.p.control("DELETE", `/api/sandboxes/ephemeral/${encodeURIComponent(this.nativeId)}`);
178
+ }
179
+ }
180
+ /** A Maritime desktop or phone. REST: /api/v1/computers/{id}/... (camelCase fields). */
181
+ class MaritimeComputer {
182
+ p;
183
+ nativeId;
184
+ base;
185
+ constructor(p, nativeId) {
186
+ this.p = p;
187
+ this.nativeId = nativeId;
188
+ this.base = `/api/v1/computers/${encodeURIComponent(nativeId)}`;
189
+ }
190
+ info() {
191
+ return this.p.control("GET", this.base);
192
+ }
193
+ act(action) {
194
+ return this.p.control("POST", `${this.base}/actions`, action);
195
+ }
196
+ async actBatch(actions) {
197
+ const r = await this.p.control("POST", `${this.base}/actions`, { actions });
198
+ return r.results;
199
+ }
200
+ async screenshot(opts = {}) {
201
+ const q = new URLSearchParams();
202
+ if (opts.format)
203
+ q.set("format", opts.format);
204
+ if (opts.quality)
205
+ q.set("quality", String(opts.quality));
206
+ const res = await this.p.controlRaw("GET", `${this.base}/screenshot${q.size ? `?${q}` : ""}`);
207
+ return new Uint8Array(await res.arrayBuffer());
208
+ }
209
+ async exec(command, timeoutSeconds = 30) {
210
+ const r = await this.p.control("POST", `${this.base}/exec`, {
211
+ command,
212
+ timeoutS: Math.min(30, timeoutSeconds),
213
+ });
214
+ return { exitCode: r.exitCode ?? -1, stdout: r.stdout, stderr: r.stderr };
215
+ }
216
+ async readFile(path) {
217
+ const res = await this.p.controlRaw("GET", `${this.base}/files?path=${encodeURIComponent(path)}`);
218
+ return new Uint8Array(await res.arrayBuffer());
219
+ }
220
+ async writeFile(path, content) {
221
+ await this.p.controlRaw("PUT", `${this.base}/files?path=${encodeURIComponent(path)}`, toBytes(content));
222
+ }
223
+ viewer(mode = "watch", opts = {}) {
224
+ const body = { mode };
225
+ if (opts.ttlSeconds !== undefined)
226
+ body.ttlS = opts.ttlSeconds;
227
+ if (opts.reason !== undefined)
228
+ body.reason = opts.reason;
229
+ return this.p.control("POST", `${this.base}/viewer`, body);
230
+ }
231
+ async sleep() {
232
+ await this.p.control("POST", `${this.base}/sleep`);
233
+ }
234
+ async delete() {
235
+ await this.p.control("DELETE", this.base);
236
+ }
237
+ }
238
+ function clamp(n, lo, hi) {
239
+ return Math.min(hi, Math.max(lo, n));
240
+ }
@@ -0,0 +1,10 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ /** Modal via the `modal` JS SDK. The only adapter here that takes `gpu`. */
3
+ export declare class ModalProvider implements Provider {
4
+ readonly id: "modal";
5
+ private client?;
6
+ configured(): boolean;
7
+ private modal;
8
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
9
+ connect(nativeId: string): Promise<ProviderSandbox>;
10
+ }