@sandboxrouter/sdk 0.1.0 → 0.3.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 (97) hide show
  1. package/README.md +271 -62
  2. package/dist/bench.js +4 -4
  3. package/dist/catalog.d.ts +232 -3
  4. package/dist/catalog.js +569 -14
  5. package/dist/check.d.ts +28 -0
  6. package/dist/check.js +185 -0
  7. package/dist/cli.js +149 -12
  8. package/dist/computers.d.ts +55 -13
  9. package/dist/computers.js +132 -6
  10. package/dist/conformance.d.ts +46 -0
  11. package/dist/conformance.js +65 -0
  12. package/dist/cost.d.ts +67 -0
  13. package/dist/cost.js +153 -0
  14. package/dist/gc.d.ts +29 -0
  15. package/dist/gc.js +54 -0
  16. package/dist/index.d.ts +34 -2
  17. package/dist/index.js +34 -2
  18. package/dist/leaderboard.d.ts +6 -0
  19. package/dist/leaderboard.js +5 -4
  20. package/dist/ledger.d.ts +38 -0
  21. package/dist/ledger.js +89 -0
  22. package/dist/providers/agent37.d.ts +40 -0
  23. package/dist/providers/agent37.js +224 -0
  24. package/dist/providers/agentcore.d.ts +71 -0
  25. package/dist/providers/agentcore.js +446 -0
  26. package/dist/providers/alibaba.d.ts +18 -0
  27. package/dist/providers/alibaba.js +106 -0
  28. package/dist/providers/azure.d.ts +43 -0
  29. package/dist/providers/azure.js +330 -0
  30. package/dist/providers/beam.d.ts +11 -0
  31. package/dist/providers/beam.js +179 -0
  32. package/dist/providers/blaxel.d.ts +3 -1
  33. package/dist/providers/blaxel.js +32 -22
  34. package/dist/providers/boat.d.ts +25 -0
  35. package/dist/providers/boat.js +263 -0
  36. package/dist/providers/cloudflare.d.ts +30 -0
  37. package/dist/providers/cloudflare.js +152 -0
  38. package/dist/providers/codesandbox.d.ts +59 -0
  39. package/dist/providers/codesandbox.js +287 -0
  40. package/dist/providers/daytona.d.ts +25 -4
  41. package/dist/providers/daytona.js +449 -28
  42. package/dist/providers/dedalus.d.ts +12 -0
  43. package/dist/providers/dedalus.js +244 -0
  44. package/dist/providers/deno.d.ts +50 -0
  45. package/dist/providers/deno.js +170 -0
  46. package/dist/providers/docker.d.ts +17 -0
  47. package/dist/providers/docker.js +154 -0
  48. package/dist/providers/e2b.d.ts +70 -3
  49. package/dist/providers/e2b.js +376 -8
  50. package/dist/providers/exedev.d.ts +23 -0
  51. package/dist/providers/exedev.js +290 -0
  52. package/dist/providers/freestyle.d.ts +49 -0
  53. package/dist/providers/freestyle.js +261 -0
  54. package/dist/providers/hopx.d.ts +15 -0
  55. package/dist/providers/hopx.js +177 -0
  56. package/dist/providers/instabox.d.ts +76 -0
  57. package/dist/providers/instabox.js +360 -0
  58. package/dist/providers/koyeb.d.ts +71 -0
  59. package/dist/providers/koyeb.js +379 -0
  60. package/dist/providers/lambda.d.ts +51 -0
  61. package/dist/providers/lambda.js +467 -0
  62. package/dist/providers/langsmith.d.ts +16 -0
  63. package/dist/providers/langsmith.js +149 -0
  64. package/dist/providers/limrun.d.ts +77 -0
  65. package/dist/providers/limrun.js +626 -0
  66. package/dist/providers/maritime.d.ts +5 -1
  67. package/dist/providers/maritime.js +39 -8
  68. package/dist/providers/mobilerun.d.ts +44 -0
  69. package/dist/providers/mobilerun.js +417 -0
  70. package/dist/providers/modal.js +12 -4
  71. package/dist/providers/morph.js +12 -4
  72. package/dist/providers/northflank.d.ts +21 -0
  73. package/dist/providers/northflank.js +259 -0
  74. package/dist/providers/novita.d.ts +12 -0
  75. package/dist/providers/novita.js +81 -0
  76. package/dist/providers/openscale.d.ts +17 -0
  77. package/dist/providers/openscale.js +87 -0
  78. package/dist/providers/orgo.d.ts +45 -0
  79. package/dist/providers/orgo.js +414 -0
  80. package/dist/providers/sail.d.ts +32 -0
  81. package/dist/providers/sail.js +372 -0
  82. package/dist/providers/superserve.d.ts +25 -0
  83. package/dist/providers/superserve.js +337 -0
  84. package/dist/providers/tensorlake.js +17 -14
  85. package/dist/providers/upstash.d.ts +30 -0
  86. package/dist/providers/upstash.js +258 -0
  87. package/dist/providers/vercel.js +14 -3
  88. package/dist/providers/workspaces.d.ts +60 -0
  89. package/dist/providers/workspaces.js +662 -0
  90. package/dist/router.d.ts +36 -5
  91. package/dist/router.js +174 -25
  92. package/dist/server.d.ts +1 -1
  93. package/dist/server.js +22 -3
  94. package/dist/types.d.ts +18 -6
  95. package/dist/util.d.ts +28 -1
  96. package/dist/util.js +56 -7
  97. package/package.json +106 -17
@@ -0,0 +1,263 @@
1
+ import { BOAT_SIZES } from "../catalog.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { DEFAULT_EXEC_TIMEOUT_S, dirname, killedByTimeout, shq, timeoutError, 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
+ // Boat caps a command at 600 s; it stops the command at the deadline and flags it as timedOut.
162
+ const seconds = Math.min(600, Math.max(1, Math.round(opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S)));
163
+ const res = await this.call("POST", "/commands", {
164
+ command: wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }),
165
+ timeoutSeconds: seconds,
166
+ stream: streaming,
167
+ });
168
+ let result;
169
+ if (streaming && res.headers.get("content-type")?.includes("ndjson")) {
170
+ let stdout = "";
171
+ let stderr = "";
172
+ let exit;
173
+ for await (const line of readLines(res)) {
174
+ const frame = JSON.parse(line);
175
+ if (frame.type === "stdout") {
176
+ stdout += frame.data ?? "";
177
+ opts.onStdout?.(frame.data ?? "");
178
+ }
179
+ else if (frame.type === "stderr") {
180
+ stderr += frame.data ?? "";
181
+ opts.onStderr?.(frame.data ?? "");
182
+ }
183
+ else if (frame.type === "exit")
184
+ exit = frame;
185
+ else if (frame.type === "error")
186
+ throw new ProviderError(ID, `${frame.error ?? "error"}: ${frame.message ?? ""}`);
187
+ }
188
+ if (!exit)
189
+ throw new ProviderError(ID, "output stream closed before the command finished");
190
+ result = { exitCode: exit.exitCode ?? null, stdout, stderr, timedOut: exit.timedOut };
191
+ }
192
+ else {
193
+ result = (await res.json());
194
+ if (result.stdout)
195
+ opts.onStdout?.(result.stdout);
196
+ if (result.stderr)
197
+ opts.onStderr?.(result.stderr);
198
+ }
199
+ const durationMs = Date.now() - t0;
200
+ if (result.timedOut || killedByTimeout(result.exitCode ?? -1, durationMs, seconds))
201
+ throw timeoutError(ID, seconds);
202
+ return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", durationMs };
203
+ }
204
+ async writeFile(path, content) {
205
+ const b64 = Buffer.from(toBytes(content)).toString("base64");
206
+ if (filesApiPath(path)) {
207
+ const dir = dirname(path);
208
+ if (path.includes("/") && !/^\/(home\/user|tmp)$/.test(dir))
209
+ await this.call("POST", "/files/folder", { path: dir });
210
+ await this.call("PUT", "/files", { path, content: b64, encoding: "base64" });
211
+ return;
212
+ }
213
+ // Outside /home/user and /tmp: passwordless sudo through a command.
214
+ const r = await this.exec(`sudo mkdir -p ${shq(dirname(path))} && printf %s ${shq(b64)} | base64 -d | sudo tee ${shq(path)} > /dev/null`);
215
+ if (r.exitCode !== 0)
216
+ throw new ProviderError(ID, `writeFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
217
+ }
218
+ async readFile(path) {
219
+ if (filesApiPath(path)) {
220
+ const res = await this.call("GET", `/files?path=${encodeURIComponent(path)}&encoding=base64`);
221
+ const j = (await res.json());
222
+ return j.encoding === "utf8" ? new TextEncoder().encode(j.content) : new Uint8Array(Buffer.from(j.content, "base64"));
223
+ }
224
+ const r = await this.exec(`sudo base64 -w0 ${shq(path)}`);
225
+ if (r.exitCode !== 0)
226
+ throw new ProviderError(ID, `readFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
227
+ return new Uint8Array(Buffer.from(r.stdout.trim(), "base64"));
228
+ }
229
+ /** Stop: the disk is kept as a snapshot (>= 14 days), running processes end. connect() or the next call resumes it. */
230
+ async pause() {
231
+ await this.p.request("POST", `${this.base}/stop`, { json: {} });
232
+ }
233
+ async kill() {
234
+ await this.p.request("DELETE", this.base, { headers: { "X-Ascii-Confirm-Delete": this.nativeId } });
235
+ }
236
+ }
237
+ /** Newline-delimited JSON reader for a fetch Response. */
238
+ async function* readLines(res) {
239
+ if (!res.body)
240
+ return;
241
+ const reader = res.body.getReader();
242
+ const decoder = new TextDecoder();
243
+ let buffer = "";
244
+ for (;;) {
245
+ const { done, value } = await reader.read();
246
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
247
+ let nl;
248
+ while ((nl = buffer.indexOf("\n")) !== -1) {
249
+ const line = buffer.slice(0, nl).trim();
250
+ buffer = buffer.slice(nl + 1);
251
+ if (line)
252
+ yield line;
253
+ }
254
+ if (done) {
255
+ if (buffer.trim())
256
+ yield buffer.trim();
257
+ return;
258
+ }
259
+ }
260
+ }
261
+ function sleep(ms) {
262
+ return new Promise((r) => setTimeout(r, ms));
263
+ }
@@ -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,152 @@
1
+ import { CLOUDFLARE_INSTANCES, DEFAULT_MEMORY_GB, DEFAULT_VCPU, cloudflareInstance } from "../catalog.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { DEFAULT_EXEC_TIMEOUT_S, killedByTimeout, timeoutError, 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 seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
103
+ const body = { cmd, cwd: opts.cwd, env: opts.env, timeoutSeconds: seconds };
104
+ const streaming = !!(opts.onStdout || opts.onStderr);
105
+ const res = await this.p.request("POST", `${this.base}/exec`, {
106
+ json: body,
107
+ headers: streaming ? { Accept: "text/event-stream" } : {},
108
+ });
109
+ let result;
110
+ if (streaming && res.headers.get("content-type")?.includes("text/event-stream")) {
111
+ for await (const ev of readSSE(res)) {
112
+ const payload = JSON.parse(ev.data);
113
+ if (ev.event === "stdout")
114
+ opts.onStdout?.(payload);
115
+ else if (ev.event === "stderr")
116
+ opts.onStderr?.(payload);
117
+ else if (ev.event === "result")
118
+ result = payload;
119
+ else if (ev.event === "error")
120
+ throw new ProviderError("cloudflare", `${payload?.code ?? "error"}: ${payload?.message ?? ev.data}`);
121
+ }
122
+ if (!result)
123
+ throw new ProviderError("cloudflare", "output stream closed before the command finished");
124
+ }
125
+ else {
126
+ result = JSON.parse(await res.text());
127
+ // Bridge answered without streaming: deliver the output in one chunk.
128
+ if (result.stdout)
129
+ opts.onStdout?.(result.stdout);
130
+ if (result.stderr)
131
+ opts.onStderr?.(result.stderr);
132
+ }
133
+ const durationMs = Date.now() - t0;
134
+ // The bridge runs the command under coreutils `timeout`: a kill at the deadline is exit 124 (137 after SIGKILL).
135
+ if (killedByTimeout(result.exitCode ?? -1, durationMs, seconds))
136
+ throw timeoutError("cloudflare", seconds);
137
+ return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", durationMs };
138
+ }
139
+ async writeFile(path, content) {
140
+ await this.p.request("PUT", `${this.base}/files?path=${encodeURIComponent(path)}`, { bytes: toBytes(content) });
141
+ }
142
+ async readFile(path) {
143
+ const res = await this.p.request("GET", `${this.base}/files?path=${encodeURIComponent(path)}`);
144
+ return new Uint8Array(await res.arrayBuffer());
145
+ }
146
+ async kill() {
147
+ await this.p.request("DELETE", this.base);
148
+ }
149
+ }
150
+ function clamp(n, lo, hi) {
151
+ return Math.min(hi, Math.max(lo, n));
152
+ }
@@ -0,0 +1,59 @@
1
+ import { CODESANDBOX_TIERS, codesandboxTier } from "../catalog.js";
2
+ export { CODESANDBOX_TIERS, codesandboxTier };
3
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
4
+ interface CsbCommand {
5
+ waitUntilComplete(): Promise<string>;
6
+ kill(): Promise<void>;
7
+ }
8
+ interface CsbClient {
9
+ commands: {
10
+ runBackground(command: string, opts?: Record<string, unknown>): Promise<CsbCommand>;
11
+ };
12
+ fs: {
13
+ writeFile(path: string, content: Uint8Array, opts?: {
14
+ create?: boolean;
15
+ overwrite?: boolean;
16
+ }): Promise<void>;
17
+ readFile(path: string): Promise<Uint8Array>;
18
+ mkdir(path: string, recursive?: boolean): Promise<void>;
19
+ remove(path: string, recursive?: boolean): Promise<void>;
20
+ };
21
+ disconnect(): Promise<void>;
22
+ }
23
+ interface CsbSandbox {
24
+ id: string;
25
+ connect(): Promise<CsbClient>;
26
+ }
27
+ interface CsbSdk {
28
+ sandboxes: {
29
+ create(opts: Record<string, unknown>): Promise<CsbSandbox>;
30
+ resume(id: string): Promise<CsbSandbox>;
31
+ hibernate(id: string): Promise<void>;
32
+ shutdown(id: string): Promise<void>;
33
+ delete(id: string): Promise<void>;
34
+ };
35
+ }
36
+ /** The slice of `@codesandbox/sdk` this adapter uses. */
37
+ interface CsbModule {
38
+ CodeSandbox: new (apiToken?: string, opts?: {
39
+ baseUrl?: string;
40
+ }) => CsbSdk;
41
+ VMTier: {
42
+ fromName(name: string): unknown;
43
+ };
44
+ }
45
+ export declare class CodeSandboxProvider implements Provider {
46
+ private apiKey;
47
+ private baseUrl;
48
+ readonly id: "codesandbox";
49
+ private sdk?;
50
+ constructor(apiKey?: string | undefined, baseUrl?: string | undefined);
51
+ configured(): boolean;
52
+ /** @internal */
53
+ module(): Promise<CsbModule>;
54
+ /** @internal */
55
+ client(): Promise<CsbSdk>;
56
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
57
+ /** Reattach by id; a hibernated sandbox is resumed (shut-down ones boot clean). */
58
+ connect(nativeId: string): Promise<ProviderSandbox>;
59
+ }