@sandboxrouter/sdk 0.0.0-stage → 0.2.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -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 +105 -0
  8. package/dist/catalog.js +320 -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 +23 -0
  14. package/dist/index.js +23 -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/boat.d.ts +25 -0
  20. package/dist/providers/boat.js +258 -0
  21. package/dist/providers/cloudflare.d.ts +30 -0
  22. package/dist/providers/cloudflare.js +147 -0
  23. package/dist/providers/daytona.d.ts +13 -0
  24. package/dist/providers/daytona.js +121 -0
  25. package/dist/providers/docker.d.ts +17 -0
  26. package/dist/providers/docker.js +148 -0
  27. package/dist/providers/e2b.d.ts +10 -0
  28. package/dist/providers/e2b.js +69 -0
  29. package/dist/providers/exedev.d.ts +23 -0
  30. package/dist/providers/exedev.js +278 -0
  31. package/dist/providers/hopx.d.ts +15 -0
  32. package/dist/providers/hopx.js +168 -0
  33. package/dist/providers/maritime.d.ts +38 -0
  34. package/dist/providers/maritime.js +240 -0
  35. package/dist/providers/modal.d.ts +10 -0
  36. package/dist/providers/modal.js +82 -0
  37. package/dist/providers/morph.d.ts +19 -0
  38. package/dist/providers/morph.js +181 -0
  39. package/dist/providers/northflank.d.ts +21 -0
  40. package/dist/providers/northflank.js +261 -0
  41. package/dist/providers/runloop.d.ts +16 -0
  42. package/dist/providers/runloop.js +145 -0
  43. package/dist/providers/sprites.d.ts +15 -0
  44. package/dist/providers/sprites.js +135 -0
  45. package/dist/providers/tensorlake.d.ts +14 -0
  46. package/dist/providers/tensorlake.js +192 -0
  47. package/dist/providers/vercel.d.ts +12 -0
  48. package/dist/providers/vercel.js +87 -0
  49. package/dist/router.d.ts +65 -0
  50. package/dist/router.js +199 -0
  51. package/dist/server.d.ts +31 -0
  52. package/dist/server.js +257 -0
  53. package/dist/sse.d.ts +6 -0
  54. package/dist/sse.js +33 -0
  55. package/dist/types.d.ts +87 -0
  56. package/dist/types.js +19 -0
  57. package/dist/util.d.ts +11 -0
  58. package/dist/util.js +37 -0
  59. package/package.json +127 -4
@@ -0,0 +1,258 @@
1
+ import { BOAT_SIZES } from "../catalog.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { dirname, shq, toBytes, wrapCommand } from "../util.js";
4
+ /**
5
+ * Boat (boat.dev) over plain REST. One host, one key: Authorization: Bearer boat_... (BOAT_API_KEY).
6
+ * POST /sandboxes create (202, state "provisioning"); poll GET until ready
7
+ * GET /sandboxes/{id} state: provisioning ... ready | idle | running | archived | error | cancelled
8
+ * POST /sandboxes/{id}/commands {command, timeoutSeconds<=600, stream}; stream: true answers NDJSON frames
9
+ * GET|PUT /sandboxes/{id}/files JSON {path, content, encoding: base64}; 5 MiB; only under /home/user or /tmp
10
+ * POST /sandboxes/{id}/files/folder mkdir -p
11
+ * POST /sandboxes/{id}/stop | /resume stop keeps the disk as a snapshot; resume boots it again
12
+ * DELETE /sandboxes/{id} permanent; needs X-Ascii-Confirm-Delete: {id}
13
+ * Docs: https://docs.boat.dev/llms.txt
14
+ * API reference (OpenAPI 3.1): https://boat.dev/openapi.yaml (server https://boat.dev/api/v1)
15
+ * Sizes/pricing: https://docs.boat.dev/machines.md, https://docs.boat.dev/pricing.md
16
+ * Limits: https://docs.boat.dev/limits.md
17
+ */
18
+ const ID = "boat";
19
+ export class BoatProvider {
20
+ apiKey;
21
+ baseUrl;
22
+ id = ID;
23
+ constructor(apiKey = process.env.BOAT_API_KEY, baseUrl = process.env.BOAT_API_URL ?? "https://boat.dev/api/v1") {
24
+ this.apiKey = apiKey;
25
+ this.baseUrl = baseUrl;
26
+ }
27
+ configured() {
28
+ return !!this.apiKey;
29
+ }
30
+ /** @internal */
31
+ async request(method, path, init = {}) {
32
+ if (!this.apiKey)
33
+ throw new ProviderError(this.id, "BOAT_API_KEY is not set");
34
+ const headers = { Authorization: `Bearer ${this.apiKey}`, ...init.headers };
35
+ let body;
36
+ if (init.json !== undefined) {
37
+ headers["Content-Type"] = "application/json";
38
+ body = JSON.stringify(init.json);
39
+ }
40
+ const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body });
41
+ if (!res.ok) {
42
+ const text = await res.text();
43
+ let detail = text.slice(0, 300);
44
+ let code;
45
+ try {
46
+ // ErrorEnvelope: {ok:false, status, code, message, requestId, error:{code,message,status}}
47
+ const j = JSON.parse(text);
48
+ code = j?.code ?? j?.error?.code;
49
+ const message = j?.message ?? j?.error?.message ?? "";
50
+ if (code)
51
+ detail = `${code}: ${message}`;
52
+ }
53
+ catch {
54
+ /* non-JSON body */
55
+ }
56
+ throw new BoatError(`${method} ${path.split("?")[0]} -> ${res.status} ${detail}`, res.status, code);
57
+ }
58
+ return res;
59
+ }
60
+ /** @internal */
61
+ async json(method, path, json, headers) {
62
+ const text = await (await this.request(method, path, { json, headers })).text();
63
+ return (text ? JSON.parse(text) : {});
64
+ }
65
+ /** @internal Poll until the sandbox can take commands. Resumes it first when it is stopped. */
66
+ async ready(id, timeoutMs = 300_000) {
67
+ const deadline = Date.now() + timeoutMs;
68
+ let resumed = false;
69
+ for (;;) {
70
+ const { sandbox } = await this.json("GET", `/sandboxes/${encodeURIComponent(id)}`);
71
+ const state = sandbox.state;
72
+ if (READY.has(state))
73
+ return sandbox;
74
+ if (state === "error" || state === "cancelled") {
75
+ throw new ProviderError(this.id, `sandbox ${id} is ${state}${sandbox.error ? `: ${sandbox.error}` : ""}`);
76
+ }
77
+ if (STOPPED.has(state) && !resumed) {
78
+ await this.json("POST", `/sandboxes/${encodeURIComponent(id)}/resume`, {});
79
+ resumed = true;
80
+ }
81
+ if (Date.now() > deadline)
82
+ throw new ProviderError(this.id, `sandbox ${id} not ready after ${timeoutMs / 1000}s (state: ${state})`);
83
+ await sleep(1000);
84
+ }
85
+ }
86
+ async create(spec) {
87
+ const body = {
88
+ type: machineType(spec.vcpu ?? 2, spec.memoryGb ?? 4),
89
+ // Auto-stop (archive) after the router's hard lifetime. Max 30 days.
90
+ ttlSeconds: Math.min(2_592_000, Math.max(60, Math.round((spec.timeoutMinutes ?? 30) * 60))),
91
+ };
92
+ const from = spec.image?.boat;
93
+ if (from)
94
+ body.from = from; // a saved snapshot name or share code
95
+ if (spec.env)
96
+ body.env = spec.env;
97
+ if (spec.metadata)
98
+ body.labels = spec.metadata;
99
+ const created = await this.json("POST", "/sandboxes", body);
100
+ const id = created.sandbox.id;
101
+ try {
102
+ await this.ready(id);
103
+ }
104
+ catch (err) {
105
+ await this.request("DELETE", `/sandboxes/${encodeURIComponent(id)}`, { headers: { "X-Ascii-Confirm-Delete": id } }).catch(() => { });
106
+ throw err;
107
+ }
108
+ return new BoatSandbox(this, id);
109
+ }
110
+ async connect(nativeId) {
111
+ await this.ready(nativeId);
112
+ return new BoatSandbox(this, nativeId);
113
+ }
114
+ }
115
+ class BoatError extends ProviderError {
116
+ code;
117
+ constructor(message, status, code) {
118
+ super(ID, message, status);
119
+ this.code = code;
120
+ }
121
+ }
122
+ const READY = new Set(["ready", "idle", "running"]);
123
+ const STOPPED = new Set(["archived", "stopped"]);
124
+ /** Boat has four fixed sizes (catalog.ts). Pick the smallest that covers the request. */
125
+ function machineType(vcpu, memoryGb) {
126
+ const fit = BOAT_SIZES.find((s) => s.vcpu >= vcpu && s.memoryGb >= memoryGb);
127
+ if (!fit)
128
+ throw new ProviderError(ID, `no Boat size has ${vcpu} vCPU / ${memoryGb} GB (max xlarge: 16 vCPU / 32 GB)`);
129
+ return fit.type;
130
+ }
131
+ /** The files API only reaches these two trees; anything else goes through a command with sudo. */
132
+ function filesApiPath(path) {
133
+ if (!path.startsWith("/"))
134
+ return !path.split("/").includes("..");
135
+ return /^\/(home\/user|tmp)(\/|$)/.test(path) && !path.split("/").includes("..");
136
+ }
137
+ class BoatSandbox {
138
+ p;
139
+ nativeId;
140
+ base;
141
+ constructor(p, nativeId) {
142
+ this.p = p;
143
+ this.nativeId = nativeId;
144
+ this.base = `/sandboxes/${encodeURIComponent(nativeId)}`;
145
+ }
146
+ /** Data call; when the sandbox is stopped (409 sandbox_not_ready) or still starting, wait/resume once and retry. */
147
+ async call(method, path, json) {
148
+ try {
149
+ return await this.p.request(method, `${this.base}${path}`, { json });
150
+ }
151
+ catch (err) {
152
+ if (!(err instanceof BoatError && err.status === 409 && (err.code === "sandbox_not_ready" || err.code === "boat_starting")))
153
+ throw err;
154
+ await this.p.ready(this.nativeId);
155
+ return this.p.request(method, `${this.base}${path}`, { json });
156
+ }
157
+ }
158
+ async exec(cmd, opts = {}) {
159
+ const t0 = Date.now();
160
+ const streaming = !!(opts.onStdout || opts.onStderr);
161
+ const res = await this.call("POST", "/commands", {
162
+ command: wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }),
163
+ timeoutSeconds: Math.min(600, Math.max(1, Math.round(opts.timeoutSeconds ?? 60))),
164
+ stream: streaming,
165
+ });
166
+ let result;
167
+ if (streaming && res.headers.get("content-type")?.includes("ndjson")) {
168
+ let stdout = "";
169
+ let stderr = "";
170
+ let exit;
171
+ for await (const line of readLines(res)) {
172
+ const frame = JSON.parse(line);
173
+ if (frame.type === "stdout") {
174
+ stdout += frame.data ?? "";
175
+ opts.onStdout?.(frame.data ?? "");
176
+ }
177
+ else if (frame.type === "stderr") {
178
+ stderr += frame.data ?? "";
179
+ opts.onStderr?.(frame.data ?? "");
180
+ }
181
+ else if (frame.type === "exit")
182
+ exit = frame;
183
+ else if (frame.type === "error")
184
+ throw new ProviderError(ID, `${frame.error ?? "error"}: ${frame.message ?? ""}`);
185
+ }
186
+ if (!exit)
187
+ throw new ProviderError(ID, "output stream closed before the command finished");
188
+ result = { exitCode: exit.exitCode ?? null, stdout, stderr, timedOut: exit.timedOut };
189
+ }
190
+ else {
191
+ result = (await res.json());
192
+ if (result.stdout)
193
+ opts.onStdout?.(result.stdout);
194
+ if (result.stderr)
195
+ opts.onStderr?.(result.stderr);
196
+ }
197
+ return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", durationMs: Date.now() - t0 };
198
+ }
199
+ async writeFile(path, content) {
200
+ const b64 = Buffer.from(toBytes(content)).toString("base64");
201
+ if (filesApiPath(path)) {
202
+ const dir = dirname(path);
203
+ if (path.includes("/") && !/^\/(home\/user|tmp)$/.test(dir))
204
+ await this.call("POST", "/files/folder", { path: dir });
205
+ await this.call("PUT", "/files", { path, content: b64, encoding: "base64" });
206
+ return;
207
+ }
208
+ // Outside /home/user and /tmp: passwordless sudo through a command.
209
+ const r = await this.exec(`sudo mkdir -p ${shq(dirname(path))} && printf %s ${shq(b64)} | base64 -d | sudo tee ${shq(path)} > /dev/null`);
210
+ if (r.exitCode !== 0)
211
+ throw new ProviderError(ID, `writeFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
212
+ }
213
+ async readFile(path) {
214
+ if (filesApiPath(path)) {
215
+ const res = await this.call("GET", `/files?path=${encodeURIComponent(path)}&encoding=base64`);
216
+ const j = (await res.json());
217
+ return j.encoding === "utf8" ? new TextEncoder().encode(j.content) : new Uint8Array(Buffer.from(j.content, "base64"));
218
+ }
219
+ const r = await this.exec(`sudo base64 -w0 ${shq(path)}`);
220
+ if (r.exitCode !== 0)
221
+ throw new ProviderError(ID, `readFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
222
+ return new Uint8Array(Buffer.from(r.stdout.trim(), "base64"));
223
+ }
224
+ /** Stop: the disk is kept as a snapshot (>= 14 days), running processes end. connect() or the next call resumes it. */
225
+ async pause() {
226
+ await this.p.request("POST", `${this.base}/stop`, { json: {} });
227
+ }
228
+ async kill() {
229
+ await this.p.request("DELETE", this.base, { headers: { "X-Ascii-Confirm-Delete": this.nativeId } });
230
+ }
231
+ }
232
+ /** Newline-delimited JSON reader for a fetch Response. */
233
+ async function* readLines(res) {
234
+ if (!res.body)
235
+ return;
236
+ const reader = res.body.getReader();
237
+ const decoder = new TextDecoder();
238
+ let buffer = "";
239
+ for (;;) {
240
+ const { done, value } = await reader.read();
241
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
242
+ let nl;
243
+ while ((nl = buffer.indexOf("\n")) !== -1) {
244
+ const line = buffer.slice(0, nl).trim();
245
+ buffer = buffer.slice(nl + 1);
246
+ if (line)
247
+ yield line;
248
+ }
249
+ if (done) {
250
+ if (buffer.trim())
251
+ yield buffer.trim();
252
+ return;
253
+ }
254
+ }
255
+ }
256
+ function sleep(ms) {
257
+ return new Promise((r) => setTimeout(r, ms));
258
+ }
@@ -0,0 +1,30 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ /**
3
+ * Cloudflare Sandboxes through a bridge Worker you deploy to your own account
4
+ * (bridges/cloudflare in this repo). Cloudflare sandboxes are Containers started from a
5
+ * Durable Object, so they can only be driven from inside a Worker; the bridge exposes them over HTTPS.
6
+ * POST /sandboxes {env, instance, image?, timeoutMinutes} -> {id}
7
+ * GET /sandboxes/:id -> {id, running, expiresAt}
8
+ * POST /sandboxes/:id/exec with Accept: text/event-stream it streams stdout/stderr events
9
+ * GET|PUT /sandboxes/:id/files?path= raw bytes
10
+ * DELETE /sandboxes/:id
11
+ * Auth: Authorization: Bearer CLOUDFLARE_SANDBOX_TOKEN (the bridge's BRIDGE_TOKEN secret).
12
+ * Docs: https://developers.cloudflare.com/sandbox/
13
+ */
14
+ export declare class CloudflareProvider implements Provider {
15
+ private baseUrl;
16
+ private token;
17
+ readonly id: "cloudflare";
18
+ constructor(baseUrl?: string | undefined, token?: string | undefined);
19
+ configured(): boolean;
20
+ /** @internal */
21
+ request(method: string, path: string, init?: {
22
+ json?: unknown;
23
+ bytes?: Uint8Array;
24
+ headers?: Record<string, string>;
25
+ }): Promise<Response>;
26
+ /** @internal */
27
+ json<T>(method: string, path: string, json?: unknown): Promise<T>;
28
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
29
+ connect(nativeId: string): Promise<ProviderSandbox>;
30
+ }
@@ -0,0 +1,147 @@
1
+ import { CLOUDFLARE_INSTANCES, DEFAULT_MEMORY_GB, DEFAULT_VCPU, cloudflareInstance } from "../catalog.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { toBytes } from "../util.js";
4
+ import { readSSE } from "../sse.js";
5
+ /**
6
+ * Cloudflare Sandboxes through a bridge Worker you deploy to your own account
7
+ * (bridges/cloudflare in this repo). Cloudflare sandboxes are Containers started from a
8
+ * Durable Object, so they can only be driven from inside a Worker; the bridge exposes them over HTTPS.
9
+ * POST /sandboxes {env, instance, image?, timeoutMinutes} -> {id}
10
+ * GET /sandboxes/:id -> {id, running, expiresAt}
11
+ * POST /sandboxes/:id/exec with Accept: text/event-stream it streams stdout/stderr events
12
+ * GET|PUT /sandboxes/:id/files?path= raw bytes
13
+ * DELETE /sandboxes/:id
14
+ * Auth: Authorization: Bearer CLOUDFLARE_SANDBOX_TOKEN (the bridge's BRIDGE_TOKEN secret).
15
+ * Docs: https://developers.cloudflare.com/sandbox/
16
+ */
17
+ export class CloudflareProvider {
18
+ baseUrl;
19
+ token;
20
+ id = "cloudflare";
21
+ constructor(baseUrl = process.env.CLOUDFLARE_SANDBOX_URL, token = process.env.CLOUDFLARE_SANDBOX_TOKEN) {
22
+ this.baseUrl = baseUrl;
23
+ this.token = token;
24
+ }
25
+ configured() {
26
+ return !!(this.baseUrl && this.token);
27
+ }
28
+ /** @internal */
29
+ async request(method, path, init = {}) {
30
+ if (!this.baseUrl)
31
+ throw new ProviderError(this.id, "CLOUDFLARE_SANDBOX_URL is not set");
32
+ if (!this.token)
33
+ throw new ProviderError(this.id, "CLOUDFLARE_SANDBOX_TOKEN is not set");
34
+ const headers = { ...init.headers, Authorization: `Bearer ${this.token}` };
35
+ let body;
36
+ if (init.json !== undefined) {
37
+ headers["Content-Type"] = "application/json";
38
+ body = JSON.stringify(init.json);
39
+ }
40
+ else if (init.bytes) {
41
+ headers["Content-Type"] = "application/octet-stream";
42
+ body = init.bytes;
43
+ }
44
+ const res = await fetch(`${this.baseUrl.replace(/\/+$/, "")}${path}`, { method, headers, body });
45
+ if (!res.ok) {
46
+ const text = await res.text();
47
+ let detail = text.slice(0, 300);
48
+ try {
49
+ const j = JSON.parse(text);
50
+ if (j?.code)
51
+ detail = `${j.code}: ${j.message ?? ""}`;
52
+ }
53
+ catch {
54
+ /* non-JSON body */
55
+ }
56
+ throw new ProviderError(this.id, `${method} ${path.split("?")[0]} -> ${res.status} ${detail}`, res.status);
57
+ }
58
+ return res;
59
+ }
60
+ /** @internal */
61
+ async json(method, path, json) {
62
+ const text = await (await this.request(method, path, { json })).text();
63
+ return (text ? JSON.parse(text) : {});
64
+ }
65
+ async create(spec) {
66
+ const body = {
67
+ instance: instanceFor(spec.vcpu ?? DEFAULT_VCPU, spec.memoryGb ?? DEFAULT_MEMORY_GB),
68
+ timeoutMinutes: clamp(Math.round(spec.timeoutMinutes ?? 30), 1, 1440),
69
+ };
70
+ if (spec.env)
71
+ body.env = spec.env;
72
+ // Names an entry under containers[].images in the bridge's wrangler.jsonc (default "sandbox").
73
+ if (spec.image?.cloudflare)
74
+ body.image = spec.image.cloudflare;
75
+ const { id } = await this.json("POST", "/sandboxes", body);
76
+ return new CloudflareSandbox(this, id);
77
+ }
78
+ async connect(nativeId) {
79
+ const sbx = await this.json("GET", `/sandboxes/${encodeURIComponent(nativeId)}`);
80
+ return new CloudflareSandbox(this, sbx.id);
81
+ }
82
+ }
83
+ /**
84
+ * Smallest predefined instance type that covers the request, else the largest.
85
+ * https://developers.cloudflare.com/containers/platform/limits/#instance-types
86
+ */
87
+ void CLOUDFLARE_INSTANCES; // instance types live in the catalog so pricing and sizing agree
88
+ function instanceFor(vcpu, memoryGb) {
89
+ return cloudflareInstance(vcpu, memoryGb).name;
90
+ }
91
+ class CloudflareSandbox {
92
+ p;
93
+ nativeId;
94
+ base;
95
+ constructor(p, nativeId) {
96
+ this.p = p;
97
+ this.nativeId = nativeId;
98
+ this.base = `/sandboxes/${encodeURIComponent(nativeId)}`;
99
+ }
100
+ async exec(cmd, opts = {}) {
101
+ const t0 = Date.now();
102
+ const body = { cmd, cwd: opts.cwd, env: opts.env, timeoutSeconds: opts.timeoutSeconds ?? 60 };
103
+ const streaming = !!(opts.onStdout || opts.onStderr);
104
+ const res = await this.p.request("POST", `${this.base}/exec`, {
105
+ json: body,
106
+ headers: streaming ? { Accept: "text/event-stream" } : {},
107
+ });
108
+ let result;
109
+ if (streaming && res.headers.get("content-type")?.includes("text/event-stream")) {
110
+ for await (const ev of readSSE(res)) {
111
+ const payload = JSON.parse(ev.data);
112
+ if (ev.event === "stdout")
113
+ opts.onStdout?.(payload);
114
+ else if (ev.event === "stderr")
115
+ opts.onStderr?.(payload);
116
+ else if (ev.event === "result")
117
+ result = payload;
118
+ else if (ev.event === "error")
119
+ throw new ProviderError("cloudflare", `${payload?.code ?? "error"}: ${payload?.message ?? ev.data}`);
120
+ }
121
+ if (!result)
122
+ throw new ProviderError("cloudflare", "output stream closed before the command finished");
123
+ }
124
+ else {
125
+ result = JSON.parse(await res.text());
126
+ // Bridge answered without streaming: deliver the output in one chunk.
127
+ if (result.stdout)
128
+ opts.onStdout?.(result.stdout);
129
+ if (result.stderr)
130
+ opts.onStderr?.(result.stderr);
131
+ }
132
+ return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", durationMs: Date.now() - t0 };
133
+ }
134
+ async writeFile(path, content) {
135
+ await this.p.request("PUT", `${this.base}/files?path=${encodeURIComponent(path)}`, { bytes: toBytes(content) });
136
+ }
137
+ async readFile(path) {
138
+ const res = await this.p.request("GET", `${this.base}/files?path=${encodeURIComponent(path)}`);
139
+ return new Uint8Array(await res.arrayBuffer());
140
+ }
141
+ async kill() {
142
+ await this.p.request("DELETE", this.base);
143
+ }
144
+ }
145
+ function clamp(n, lo, hi) {
146
+ return Math.min(hi, Math.max(lo, n));
147
+ }
@@ -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,17 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ /**
3
+ * Local sandboxes in Docker containers, through the `docker` CLI. Free and needs no account, so it
4
+ * is good for development and tests. A container shares your machine's kernel: it is weaker
5
+ * isolation than a cloud micro-VM, so don't point untrusted code at it with secrets mounted.
6
+ * Never chosen automatically; route to it with { provider: "docker" }.
7
+ */
8
+ export declare class DockerProvider implements Provider {
9
+ private bin;
10
+ readonly id: "docker";
11
+ private available?;
12
+ constructor(bin?: string);
13
+ /** The docker CLI exists and its daemon answers. Cached for the life of the process. */
14
+ configured(): boolean;
15
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
16
+ connect(nativeId: string): Promise<ProviderSandbox>;
17
+ }