@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.
- package/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/bench-store.d.ts +12 -0
- package/dist/bench-store.js +23 -0
- package/dist/bench.d.ts +24 -0
- package/dist/bench.js +65 -0
- package/dist/catalog.d.ts +35 -0
- package/dist/catalog.js +198 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +195 -0
- package/dist/computers.d.ts +124 -0
- package/dist/computers.js +66 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/leaderboard.d.ts +39 -0
- package/dist/leaderboard.js +184 -0
- package/dist/providers/blaxel.d.ts +21 -0
- package/dist/providers/blaxel.js +152 -0
- package/dist/providers/daytona.d.ts +13 -0
- package/dist/providers/daytona.js +121 -0
- package/dist/providers/e2b.d.ts +10 -0
- package/dist/providers/e2b.js +69 -0
- package/dist/providers/maritime.d.ts +38 -0
- package/dist/providers/maritime.js +240 -0
- package/dist/providers/modal.d.ts +10 -0
- package/dist/providers/modal.js +82 -0
- package/dist/providers/morph.d.ts +19 -0
- package/dist/providers/morph.js +181 -0
- package/dist/providers/runloop.d.ts +16 -0
- package/dist/providers/runloop.js +145 -0
- package/dist/providers/sprites.d.ts +15 -0
- package/dist/providers/sprites.js +135 -0
- package/dist/providers/tensorlake.d.ts +14 -0
- package/dist/providers/tensorlake.js +192 -0
- package/dist/providers/vercel.d.ts +12 -0
- package/dist/providers/vercel.js +87 -0
- package/dist/router.d.ts +65 -0
- package/dist/router.js +180 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +257 -0
- package/dist/sse.d.ts +6 -0
- package/dist/sse.js +33 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +19 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.js +37 -0
- package/package.json +113 -4
package/dist/server.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { CATALOG, hourlyCost } from "./catalog.js";
|
|
3
|
+
import { Router } from "./router.js";
|
|
4
|
+
import { ProviderError, RouterError } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* OpenRouter-style HTTP API in front of every provider. One token for your clients; provider keys
|
|
7
|
+
* stay on this host.
|
|
8
|
+
*
|
|
9
|
+
* GET /v1/providers
|
|
10
|
+
* POST /v1/route dry run: which providers would be tried, in order
|
|
11
|
+
* POST /v1/sandboxes { vcpu, memoryGb, ..., provider, strategy, fallback, require }
|
|
12
|
+
* POST /v1/sandboxes/:id/exec { cmd, cwd, env, timeoutSeconds, stream }
|
|
13
|
+
* stream: true answers text/event-stream: `stdout`/`stderr` events (JSON string chunks),
|
|
14
|
+
* then one `result` event (the ExecResult), or an `error` event
|
|
15
|
+
* POST /v1/sandboxes/:id/pause keeps the disk; the next call resumes it
|
|
16
|
+
* PUT /v1/sandboxes/:id/files?path= raw body
|
|
17
|
+
* GET /v1/sandboxes/:id/files?path= raw body
|
|
18
|
+
* DELETE /v1/sandboxes/:id
|
|
19
|
+
*
|
|
20
|
+
* POST /v1/computers { os, externalUserId, name, screen, arch, provider }
|
|
21
|
+
* GET /v1/computers/:id
|
|
22
|
+
* POST /v1/computers/:id/actions one action, or { actions: [...] }
|
|
23
|
+
* GET /v1/computers/:id/screenshot image bytes
|
|
24
|
+
* POST /v1/computers/:id/exec { command, timeoutSeconds }
|
|
25
|
+
* PUT /v1/computers/:id/files?path= raw body
|
|
26
|
+
* GET /v1/computers/:id/files?path= raw body
|
|
27
|
+
* POST /v1/computers/:id/viewer { mode: "watch" | "control", ttlSeconds, reason }
|
|
28
|
+
* POST /v1/computers/:id/sleep
|
|
29
|
+
* DELETE /v1/computers/:id
|
|
30
|
+
*/
|
|
31
|
+
export function createApp(router = new Router(), token = process.env.SANDBOXROUTER_TOKEN,
|
|
32
|
+
/** Browser origins allowed to call this server (e.g. the website's Playground). Off by default. */
|
|
33
|
+
corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
34
|
+
const live = new Map();
|
|
35
|
+
const cors = (req, res) => {
|
|
36
|
+
const origin = req.headers.origin;
|
|
37
|
+
if (!origin || !(corsOrigins.includes("*") || corsOrigins.includes(origin)))
|
|
38
|
+
return false;
|
|
39
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
40
|
+
res.setHeader("Vary", "Origin");
|
|
41
|
+
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept");
|
|
42
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
43
|
+
res.setHeader("Access-Control-Max-Age", "600");
|
|
44
|
+
// Chrome asks before a public HTTPS page (the hosted Playground) may call a server on localhost.
|
|
45
|
+
if (req.headers["access-control-request-private-network"] === "true") {
|
|
46
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
const handle = async (req, res) => {
|
|
51
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
52
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
53
|
+
const allowed = cors(req, res);
|
|
54
|
+
if (req.method === "OPTIONS") {
|
|
55
|
+
res.writeHead(allowed ? 204 : 403);
|
|
56
|
+
return res.end();
|
|
57
|
+
}
|
|
58
|
+
if (url.pathname === "/health")
|
|
59
|
+
return json(res, 200, { ok: true });
|
|
60
|
+
if (token && req.headers.authorization !== `Bearer ${token}`)
|
|
61
|
+
return json(res, 401, { error: "unauthorized" });
|
|
62
|
+
if (parts[0] !== "v1")
|
|
63
|
+
return json(res, 404, { error: "not found" });
|
|
64
|
+
const box = async (id) => live.get(id) ?? router.get(id);
|
|
65
|
+
// GET /v1/providers
|
|
66
|
+
if (req.method === "GET" && parts[1] === "providers" && parts.length === 2) {
|
|
67
|
+
return json(res, 200, {
|
|
68
|
+
providers: CATALOG.filter((c) => router.registered().includes(c.id)).map((c) => ({
|
|
69
|
+
...c,
|
|
70
|
+
configured: router.isConfigured(c.id),
|
|
71
|
+
estimatedHourlyUsd: hourlyCost(c.id),
|
|
72
|
+
})),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// POST /v1/route
|
|
76
|
+
if (req.method === "POST" && parts[1] === "route" && parts.length === 2) {
|
|
77
|
+
const { spec, route } = splitBody(await readJson(req));
|
|
78
|
+
return json(res, 200, { candidates: router.plan(spec, route) });
|
|
79
|
+
}
|
|
80
|
+
if (parts[1] === "computers")
|
|
81
|
+
return computers(req, res, url, parts);
|
|
82
|
+
if (parts[1] !== "sandboxes")
|
|
83
|
+
return json(res, 404, { error: "not found" });
|
|
84
|
+
// POST /v1/sandboxes
|
|
85
|
+
if (req.method === "POST" && parts.length === 2) {
|
|
86
|
+
const { spec, route } = splitBody(await readJson(req));
|
|
87
|
+
const sbx = await router.create(spec, route);
|
|
88
|
+
live.set(sbx.id, sbx);
|
|
89
|
+
return json(res, 201, {
|
|
90
|
+
id: sbx.id,
|
|
91
|
+
provider: sbx.provider,
|
|
92
|
+
estimatedHourlyUsd: sbx.estimatedHourlyUsd,
|
|
93
|
+
attempts: sbx.attempts,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const id = parts[2] ? decodeURIComponent(parts[2]) : "";
|
|
97
|
+
if (!id)
|
|
98
|
+
return json(res, 404, { error: "not found" });
|
|
99
|
+
// POST /v1/sandboxes/:id/exec
|
|
100
|
+
if (req.method === "POST" && parts[3] === "exec") {
|
|
101
|
+
const body = await readJson(req);
|
|
102
|
+
if (typeof body.cmd !== "string" || !body.cmd)
|
|
103
|
+
return json(res, 400, { error: "cmd (string) is required" });
|
|
104
|
+
const sbx = await box(id);
|
|
105
|
+
const opts = { cwd: body.cwd, env: body.env, timeoutSeconds: body.timeoutSeconds };
|
|
106
|
+
if (body.stream !== true)
|
|
107
|
+
return json(res, 200, await sbx.exec(body.cmd, opts));
|
|
108
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
109
|
+
const emit = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
110
|
+
try {
|
|
111
|
+
const r = await sbx.exec(body.cmd, { ...opts, onStdout: (c) => emit("stdout", c), onStderr: (c) => emit("stderr", c) });
|
|
112
|
+
emit("result", r);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
emit("error", { error: err.message });
|
|
116
|
+
}
|
|
117
|
+
return res.end();
|
|
118
|
+
}
|
|
119
|
+
// POST /v1/sandboxes/:id/pause
|
|
120
|
+
if (req.method === "POST" && parts[3] === "pause" && parts.length === 4) {
|
|
121
|
+
await (await box(id)).pause();
|
|
122
|
+
// Drop the cached handle so the next call reattaches through router.get(), which resumes.
|
|
123
|
+
live.delete(id);
|
|
124
|
+
return json(res, 200, { id, paused: true });
|
|
125
|
+
}
|
|
126
|
+
// /v1/sandboxes/:id/files?path=
|
|
127
|
+
if (parts[3] === "files") {
|
|
128
|
+
const path = url.searchParams.get("path");
|
|
129
|
+
if (!path)
|
|
130
|
+
return json(res, 400, { error: "path query parameter is required" });
|
|
131
|
+
if (req.method === "PUT") {
|
|
132
|
+
await (await box(id)).writeFile(path, await readRaw(req));
|
|
133
|
+
return json(res, 200, { ok: true, path });
|
|
134
|
+
}
|
|
135
|
+
if (req.method === "GET") {
|
|
136
|
+
const data = await (await box(id)).readFile(path);
|
|
137
|
+
res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": data.byteLength });
|
|
138
|
+
return res.end(Buffer.from(data));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// DELETE /v1/sandboxes/:id
|
|
142
|
+
if (req.method === "DELETE" && parts.length === 3) {
|
|
143
|
+
await (await box(id)).kill();
|
|
144
|
+
live.delete(id);
|
|
145
|
+
return json(res, 200, { id, killed: true });
|
|
146
|
+
}
|
|
147
|
+
return json(res, 404, { error: "not found" });
|
|
148
|
+
};
|
|
149
|
+
const computers = async (req, res, url, parts) => {
|
|
150
|
+
if (req.method === "POST" && parts.length === 2) {
|
|
151
|
+
const { provider, ...spec } = await readJson(req);
|
|
152
|
+
const c = await router.computer(spec, { provider });
|
|
153
|
+
return json(res, 201, { ...(await c.info()), id: c.id, nativeId: c.nativeId, provider: c.provider });
|
|
154
|
+
}
|
|
155
|
+
const id = parts[2] ? decodeURIComponent(parts[2]) : "";
|
|
156
|
+
if (!id)
|
|
157
|
+
return json(res, 404, { error: "not found" });
|
|
158
|
+
const c = await router.getComputer(id);
|
|
159
|
+
const sub = parts[3];
|
|
160
|
+
if (req.method === "GET" && !sub)
|
|
161
|
+
return json(res, 200, { ...(await c.info()), id, nativeId: c.nativeId });
|
|
162
|
+
if (req.method === "DELETE" && !sub) {
|
|
163
|
+
await c.delete();
|
|
164
|
+
return json(res, 200, { id, deleted: true });
|
|
165
|
+
}
|
|
166
|
+
if (req.method === "POST" && sub === "actions") {
|
|
167
|
+
const body = await readJson(req);
|
|
168
|
+
return json(res, 200, Array.isArray(body.actions) ? { results: await c.actBatch(body.actions) } : await c.act(body));
|
|
169
|
+
}
|
|
170
|
+
if (req.method === "GET" && sub === "screenshot") {
|
|
171
|
+
const format = url.searchParams.get("format") === "jpeg" ? "jpeg" : "png";
|
|
172
|
+
const img = await c.screenshot({ format, quality: Number(url.searchParams.get("quality")) || undefined });
|
|
173
|
+
res.writeHead(200, { "Content-Type": `image/${format}`, "Content-Length": img.byteLength });
|
|
174
|
+
return res.end(Buffer.from(img));
|
|
175
|
+
}
|
|
176
|
+
if (req.method === "POST" && sub === "exec") {
|
|
177
|
+
const body = await readJson(req);
|
|
178
|
+
if (typeof body.command !== "string" || !body.command)
|
|
179
|
+
return json(res, 400, { error: "command (string) is required" });
|
|
180
|
+
return json(res, 200, await c.exec(body.command, body.timeoutSeconds));
|
|
181
|
+
}
|
|
182
|
+
if (sub === "files") {
|
|
183
|
+
const path = url.searchParams.get("path");
|
|
184
|
+
if (!path)
|
|
185
|
+
return json(res, 400, { error: "path query parameter is required" });
|
|
186
|
+
if (req.method === "PUT") {
|
|
187
|
+
await c.writeFile(path, await readRaw(req));
|
|
188
|
+
return json(res, 200, { ok: true, path });
|
|
189
|
+
}
|
|
190
|
+
if (req.method === "GET") {
|
|
191
|
+
const data = await c.readFile(path);
|
|
192
|
+
res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": data.byteLength });
|
|
193
|
+
return res.end(Buffer.from(data));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (req.method === "POST" && sub === "viewer") {
|
|
197
|
+
const body = await readJson(req);
|
|
198
|
+
return json(res, 200, await c.viewer(body.mode, { ttlSeconds: body.ttlSeconds, reason: body.reason }));
|
|
199
|
+
}
|
|
200
|
+
if (req.method === "POST" && sub === "sleep") {
|
|
201
|
+
await c.sleep();
|
|
202
|
+
return json(res, 200, { id, sleeping: true });
|
|
203
|
+
}
|
|
204
|
+
return json(res, 404, { error: "not found" });
|
|
205
|
+
};
|
|
206
|
+
return createServer((req, res) => {
|
|
207
|
+
handle(req, res).catch((err) => {
|
|
208
|
+
const status = err instanceof HttpError ? err.status
|
|
209
|
+
: err instanceof RouterError ? 502
|
|
210
|
+
: err instanceof ProviderError && err.status ? err.status
|
|
211
|
+
: 500;
|
|
212
|
+
json(res, status, { error: err.message, attempts: err.attempts });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
class HttpError extends Error {
|
|
217
|
+
status;
|
|
218
|
+
constructor(status, message) {
|
|
219
|
+
super(message);
|
|
220
|
+
this.status = status;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const ROUTE_KEYS = ["provider", "strategy", "fallback", "require"];
|
|
224
|
+
function splitBody(body) {
|
|
225
|
+
const route = {};
|
|
226
|
+
const spec = {};
|
|
227
|
+
for (const [k, v] of Object.entries(body))
|
|
228
|
+
(ROUTE_KEYS.includes(k) ? route : spec)[k] = v;
|
|
229
|
+
return { spec: spec, route: route };
|
|
230
|
+
}
|
|
231
|
+
const MAX_BODY = 100 * 1024 * 1024;
|
|
232
|
+
async function readRaw(req) {
|
|
233
|
+
const chunks = [];
|
|
234
|
+
let size = 0;
|
|
235
|
+
for await (const c of req) {
|
|
236
|
+
size += c.length;
|
|
237
|
+
if (size > MAX_BODY)
|
|
238
|
+
throw new HttpError(413, "request body too large");
|
|
239
|
+
chunks.push(c);
|
|
240
|
+
}
|
|
241
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
242
|
+
}
|
|
243
|
+
async function readJson(req) {
|
|
244
|
+
const raw = Buffer.from(await readRaw(req)).toString("utf8");
|
|
245
|
+
if (!raw)
|
|
246
|
+
return {};
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(raw);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
throw new HttpError(400, "invalid JSON body");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function json(res, status, body) {
|
|
255
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
256
|
+
res.end(JSON.stringify(body));
|
|
257
|
+
}
|
package/dist/sse.d.ts
ADDED
package/dist/sse.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Minimal text/event-stream reader for a fetch Response. */
|
|
2
|
+
export async function* readSSE(res) {
|
|
3
|
+
if (!res.body)
|
|
4
|
+
return;
|
|
5
|
+
const reader = res.body.getReader();
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
let buffer = "";
|
|
8
|
+
let event = "message";
|
|
9
|
+
let data = [];
|
|
10
|
+
for (;;) {
|
|
11
|
+
const { done, value } = await reader.read();
|
|
12
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
13
|
+
if (done)
|
|
14
|
+
buffer += "\n\n";
|
|
15
|
+
let nl;
|
|
16
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
17
|
+
const line = buffer.slice(0, nl).replace(/\r$/, "");
|
|
18
|
+
buffer = buffer.slice(nl + 1);
|
|
19
|
+
if (line === "") {
|
|
20
|
+
if (data.length)
|
|
21
|
+
yield { event, data: data.join("\n") };
|
|
22
|
+
event = "message";
|
|
23
|
+
data = [];
|
|
24
|
+
}
|
|
25
|
+
else if (line.startsWith("event:"))
|
|
26
|
+
event = line.slice(6).trim();
|
|
27
|
+
else if (line.startsWith("data:"))
|
|
28
|
+
data.push(line.slice(5).replace(/^ /, ""));
|
|
29
|
+
}
|
|
30
|
+
if (done)
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type ProviderId = "maritime" | "e2b" | "daytona" | "modal" | "vercel" | "morph" | "sprites" | "blaxel" | "tensorlake" | "runloop";
|
|
2
|
+
/** Things a caller can require of a provider. The router skips providers that lack one. */
|
|
3
|
+
export type Capability = "gpu" | "pause" | "custom-image";
|
|
4
|
+
/** What the caller wants. Every field is optional; providers map it to their own knobs. */
|
|
5
|
+
export interface SandboxSpec {
|
|
6
|
+
/** vCPUs (default 2). */
|
|
7
|
+
vcpu?: number;
|
|
8
|
+
/** Memory in GB (default 4). */
|
|
9
|
+
memoryGb?: number;
|
|
10
|
+
/** GPU type, e.g. "T4", "A100". Only providers with the "gpu" capability accept it. */
|
|
11
|
+
gpu?: string;
|
|
12
|
+
/** Env vars set for every command. */
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
/** Hard lifetime in minutes (default 30). */
|
|
15
|
+
timeoutMinutes?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Provider-specific image/template, keyed by provider, e.g.
|
|
18
|
+
* { maritime: "python", e2b: "code-interpreter-v1", modal: "python:3.13-slim" }.
|
|
19
|
+
*/
|
|
20
|
+
image?: Partial<Record<ProviderId, string>>;
|
|
21
|
+
/** Free-form tags stored on the sandbox where the provider supports it. */
|
|
22
|
+
metadata?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export type Strategy = "default" | "cheapest" | "fastest";
|
|
25
|
+
export interface RouteOptions {
|
|
26
|
+
/** Pin to one provider, or give an ordered list to try. Omit to let the router choose. */
|
|
27
|
+
provider?: ProviderId | ProviderId[];
|
|
28
|
+
/** How to order candidates when no explicit list is given. Default: "default" (catalog order). */
|
|
29
|
+
strategy?: Strategy;
|
|
30
|
+
/** Try the next candidate if a create fails (default true). */
|
|
31
|
+
fallback?: boolean;
|
|
32
|
+
/** Capabilities every candidate must have. */
|
|
33
|
+
require?: Capability[];
|
|
34
|
+
}
|
|
35
|
+
export interface ExecOptions {
|
|
36
|
+
cwd?: string;
|
|
37
|
+
env?: Record<string, string>;
|
|
38
|
+
/** Per-command timeout in seconds (default 60). */
|
|
39
|
+
timeoutSeconds?: number;
|
|
40
|
+
/** Called with each chunk of stdout as it arrives. The full output is still returned. */
|
|
41
|
+
onStdout?: (chunk: string) => void;
|
|
42
|
+
/** Called with each chunk of stderr as it arrives. */
|
|
43
|
+
onStderr?: (chunk: string) => void;
|
|
44
|
+
}
|
|
45
|
+
export interface ExecResult {
|
|
46
|
+
exitCode: number;
|
|
47
|
+
stdout: string;
|
|
48
|
+
stderr: string;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
}
|
|
51
|
+
/** A live sandbox as one provider sees it. Adapters implement this. */
|
|
52
|
+
export interface ProviderSandbox {
|
|
53
|
+
readonly nativeId: string;
|
|
54
|
+
exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
55
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
56
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
57
|
+
kill(): Promise<void>;
|
|
58
|
+
/** Present only on adapters with the "pause" capability. */
|
|
59
|
+
pause?(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export interface Provider {
|
|
62
|
+
readonly id: ProviderId;
|
|
63
|
+
/** True when the credentials this adapter needs are present. */
|
|
64
|
+
configured(): boolean;
|
|
65
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
66
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
67
|
+
}
|
|
68
|
+
export interface Attempt {
|
|
69
|
+
provider: ProviderId;
|
|
70
|
+
ok: boolean;
|
|
71
|
+
ms: number;
|
|
72
|
+
error?: string;
|
|
73
|
+
}
|
|
74
|
+
export declare class RouterError extends Error {
|
|
75
|
+
readonly attempts: Attempt[];
|
|
76
|
+
constructor(message: string, attempts?: Attempt[]);
|
|
77
|
+
}
|
|
78
|
+
/** Thrown by adapters for HTTP failures so the router can report status codes. */
|
|
79
|
+
export declare class ProviderError extends Error {
|
|
80
|
+
readonly provider: ProviderId;
|
|
81
|
+
readonly status?: number | undefined;
|
|
82
|
+
constructor(provider: ProviderId, message: string, status?: number | undefined);
|
|
83
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class RouterError extends Error {
|
|
2
|
+
attempts;
|
|
3
|
+
constructor(message, attempts = []) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.attempts = attempts;
|
|
6
|
+
this.name = "RouterError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Thrown by adapters for HTTP failures so the router can report status codes. */
|
|
10
|
+
export class ProviderError extends Error {
|
|
11
|
+
provider;
|
|
12
|
+
status;
|
|
13
|
+
constructor(provider, message, status) {
|
|
14
|
+
super(`${provider}: ${message}`);
|
|
15
|
+
this.provider = provider;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.name = "ProviderError";
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Single-quote a string for POSIX sh. */
|
|
2
|
+
export declare function shq(s: string): string;
|
|
3
|
+
/** Build `cd dir && VAR=x cmd` for providers whose exec API lacks cwd/env. */
|
|
4
|
+
export declare function wrapCommand(cmd: string, opts?: {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
}): string;
|
|
8
|
+
export declare function toBytes(content: string | Uint8Array): Uint8Array;
|
|
9
|
+
export declare function dirname(path: string): string;
|
|
10
|
+
/** Load an optional peer dependency with a helpful error. */
|
|
11
|
+
export declare function optionalImport<T>(pkg: string, provider: string): Promise<T>;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Single-quote a string for POSIX sh. */
|
|
2
|
+
export function shq(s) {
|
|
3
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
4
|
+
}
|
|
5
|
+
/** Build `cd dir && VAR=x cmd` for providers whose exec API lacks cwd/env. */
|
|
6
|
+
export function wrapCommand(cmd, opts = {}) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
if (opts.cwd)
|
|
9
|
+
parts.push(`cd ${shq(opts.cwd)} &&`);
|
|
10
|
+
for (const [k, v] of Object.entries(opts.env ?? {})) {
|
|
11
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k))
|
|
12
|
+
throw new Error(`invalid env var name: ${k}`);
|
|
13
|
+
parts.push(`export ${k}=${shq(v)};`);
|
|
14
|
+
}
|
|
15
|
+
parts.push(cmd);
|
|
16
|
+
return parts.join(" ");
|
|
17
|
+
}
|
|
18
|
+
export function toBytes(content) {
|
|
19
|
+
return typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
20
|
+
}
|
|
21
|
+
export function dirname(path) {
|
|
22
|
+
const i = path.lastIndexOf("/");
|
|
23
|
+
return i <= 0 ? "/" : path.slice(0, i);
|
|
24
|
+
}
|
|
25
|
+
/** Load an optional peer dependency with a helpful error. */
|
|
26
|
+
export async function optionalImport(pkg, provider) {
|
|
27
|
+
try {
|
|
28
|
+
return (await import(pkg));
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const code = err.code;
|
|
32
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
33
|
+
throw new Error(`${provider} needs the "${pkg}" package: npm i ${pkg}`);
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,115 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxrouter/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenRouter for sandboxes: one API for Maritime, E2B, Daytona, Modal, Vercel Sandbox, Morph, Fly Sprites, Blaxel, Tensorlake and Runloop, with price/latency routing and automatic fallback.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"sandboxrouter": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"dev": "tsx src/cli.ts",
|
|
27
|
+
"prepublishOnly": "npm run build && npm test",
|
|
28
|
+
"test:py": "python3 -m unittest discover -s python/tests -v",
|
|
29
|
+
"site:data": "node --import tsx scripts/site-data.ts"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sandbox",
|
|
33
|
+
"ai-agents",
|
|
34
|
+
"code-execution",
|
|
35
|
+
"router",
|
|
36
|
+
"maritime",
|
|
37
|
+
"e2b",
|
|
38
|
+
"daytona",
|
|
39
|
+
"modal",
|
|
40
|
+
"vercel-sandbox",
|
|
41
|
+
"openrouter",
|
|
42
|
+
"morph",
|
|
43
|
+
"fly-sprites",
|
|
44
|
+
"blaxel",
|
|
45
|
+
"tensorlake",
|
|
46
|
+
"runloop"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@daytonaio/sdk": ">=0.200",
|
|
54
|
+
"@vercel/sandbox": ">=3",
|
|
55
|
+
"e2b": ">=2",
|
|
56
|
+
"modal": ">=0.11",
|
|
57
|
+
"morphcloud": ">=0.0.20",
|
|
58
|
+
"@fly/sprites": ">=0.2",
|
|
59
|
+
"@blaxel/core": ">=0.3",
|
|
60
|
+
"@tensorlakeai/tensorlake": ">=0.5",
|
|
61
|
+
"@runloop/api-client": ">=3"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"e2b": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@daytonaio/sdk": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"modal": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@vercel/sandbox": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"morphcloud": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@fly/sprites": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@blaxel/core": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@tensorlakeai/tensorlake": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@runloop/api-client": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@blaxel/core": "^0.3.34",
|
|
94
|
+
"@daytonaio/sdk": "^0.227.3",
|
|
95
|
+
"@fly/sprites": "^0.2.4",
|
|
96
|
+
"@runloop/api-client": "^3.0.0",
|
|
97
|
+
"@tensorlakeai/tensorlake": "^0.5.161",
|
|
98
|
+
"@types/node": "^26.6.6",
|
|
99
|
+
"@vercel/sandbox": "^3.9.1",
|
|
100
|
+
"e2b": "^2.63.0",
|
|
101
|
+
"modal": "^0.11.1",
|
|
102
|
+
"morphcloud": "^0.0.20",
|
|
103
|
+
"tsx": "^4.23.15",
|
|
104
|
+
"typescript": "^5.9.3",
|
|
105
|
+
"vitest": "^5.1.0"
|
|
106
|
+
},
|
|
107
|
+
"publishConfig": {
|
|
108
|
+
"access": "public"
|
|
109
|
+
},
|
|
110
|
+
"repository": {
|
|
111
|
+
"type": "git",
|
|
112
|
+
"url": "git+https://github.com/gigialc/sandboxrouter.git"
|
|
113
|
+
},
|
|
114
|
+
"homepage": "https://sandboxrouter.vercel.app"
|
|
115
|
+
}
|