@sandboxrouter/sdk 0.3.4 → 0.3.5

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/README.md CHANGED
@@ -41,39 +41,39 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
41
41
 
42
42
  | Provider | $/hr | Plan needed | Real-account check | Source |
43
43
  |---|---|---|---|---|
44
- | [Agent37](https://sandboxrouter.vercel.app/providers/agent37) | $0.007 | none | not run yet | [pricing](https://www.agent37.com/docs/agents-api/billing) |
44
+ | [Agent37](https://sandboxrouter.vercel.app/providers/agent37) | $0.007 | none | passed (2026-11-11) | [pricing](https://www.agent37.com/docs/agents-api/billing) |
45
45
  | [Boat](https://sandboxrouter.vercel.app/providers/boat) | $0.018 | $20/mo (credited) | not run yet | [pricing](https://docs.boat.dev/pricing.md) |
46
- | [Maritime](https://sandboxrouter.vercel.app/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-10) | [pricing](https://maritime.sh/pricing) |
47
- | [Sailboxes (Sail Research)](https://sandboxrouter.vercel.app/providers/sail) | up to $0.062 | none | passed (2026-11-10) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
46
+ | [Maritime](https://sandboxrouter.vercel.app/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-11) | [pricing](https://maritime.sh/pricing) |
47
+ | [Sailboxes (Sail Research)](https://sandboxrouter.vercel.app/providers/sail) | up to $0.062 | none | passed (2026-11-11) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
48
48
  | [Northflank](https://sandboxrouter.vercel.app/providers/northflank) | $0.067 | none | not run yet | [pricing](https://northflank.com/pricing) |
49
49
  | [Morph Cloud](https://sandboxrouter.vercel.app/providers/morph) | $0.100 | none | not run yet | [pricing](https://cloud.morph.so/web/pricing) |
50
50
  | [exe.dev](https://sandboxrouter.vercel.app/providers/exedev) | $0.105 | $15/mo | not run yet | [pricing](https://exe.dev/docs/standalone-vms.md) |
51
- | [Novita AI Agent Sandbox](https://sandboxrouter.vercel.app/providers/novita) | $0.117 | none | not run yet | [pricing](https://docs.novita.ai/guides/sandbox-pricing) |
52
- | [Freestyle](https://sandboxrouter.vercel.app/providers/freestyle) | $0.134 | none | not run yet | [pricing](https://www.freestyle.sh/docs/vms/pricing-and-limits) |
51
+ | [Novita AI Agent Sandbox](https://sandboxrouter.vercel.app/providers/novita) | $0.117 | none | passed (2026-11-11) | [pricing](https://docs.novita.ai/guides/sandbox-pricing) |
52
+ | [Freestyle](https://sandboxrouter.vercel.app/providers/freestyle) | $0.134 | none | passed (2026-11-11) | [pricing](https://www.freestyle.sh/docs/vms/pricing-and-limits) |
53
53
  | [Beam](https://sandboxrouter.vercel.app/providers/beam) | $0.140 | $20/mo (credited) | not run yet | [pricing](https://www.beam.cloud/pricing) |
54
54
  | [CodeSandbox SDK (Together)](https://sandboxrouter.vercel.app/providers/codesandbox) | $0.149 | none | not run yet | [pricing](https://docs.together.ai/docs/together-code-sandbox-legacy) |
55
- | [Fly.io Sprites](https://sandboxrouter.vercel.app/providers/sprites) | up to $0.164 | none | not run yet | [pricing](https://fly.io/sprites) |
56
- | [Blaxel](https://sandboxrouter.vercel.app/providers/blaxel) | $0.166 | none | passed (2026-11-10) | [pricing](https://blaxel.ai/pricing) |
55
+ | [Fly.io Sprites](https://sandboxrouter.vercel.app/providers/sprites) | up to $0.164 | none | passed (2026-11-11) | [pricing](https://fly.io/sprites) |
56
+ | [Blaxel](https://sandboxrouter.vercel.app/providers/blaxel) | $0.166 | none | passed (2026-11-11) | [pricing](https://blaxel.ai/pricing) |
57
57
  | [HOPX](https://sandboxrouter.vercel.app/providers/hopx) | $0.166 | none | not run yet | [pricing](https://hopx.ai/pricing) |
58
- | [Superserve](https://sandboxrouter.vercel.app/providers/superserve) | $0.166 | maybe $20/mo (credited) | not run yet | [pricing](https://superserve.ai/pricing) |
58
+ | [Superserve](https://sandboxrouter.vercel.app/providers/superserve) | $0.166 | maybe $20/mo (credited) | passed (2026-11-11) | [pricing](https://superserve.ai/pricing) |
59
59
  | [Tensorlake](https://sandboxrouter.vercel.app/providers/tensorlake) | up to $0.200 | none | not run yet | [pricing](https://www.tensorlake.ai/pricing) |
60
60
  | [Upstash Box](https://sandboxrouter.vercel.app/providers/upstash) | up to $0.200 | none | not run yet | [pricing](https://upstash.com/pricing/box) |
61
- | [Cloudflare Sandbox](https://sandboxrouter.vercel.app/providers/cloudflare) | up to $0.216 | $5/mo | not run yet | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
62
- | [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.vercel.app/providers/agentcore) | up to $0.217 | none | not run yet | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
63
- | [Modal](https://sandboxrouter.vercel.app/providers/modal) | up to $0.238 | none | not run yet | [pricing](https://modal.com/pricing) |
64
- | [AWS Lambda MicroVMs](https://sandboxrouter.vercel.app/providers/lambda) | $0.252 | none | not run yet | [pricing](https://aws.amazon.com/lambda/pricing/) |
61
+ | [Cloudflare Sandbox](https://sandboxrouter.vercel.app/providers/cloudflare) | up to $0.216 | $5/mo | passed (2026-11-11) | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
62
+ | [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.vercel.app/providers/agentcore) | up to $0.217 | none | passed (2026-11-11) | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
63
+ | [Modal](https://sandboxrouter.vercel.app/providers/modal) | up to $0.238 | none | passed (2026-11-11) | [pricing](https://modal.com/pricing) |
64
+ | [AWS Lambda MicroVMs](https://sandboxrouter.vercel.app/providers/lambda) | $0.252 | none | passed (2026-11-11) | [pricing](https://aws.amazon.com/lambda/pricing/) |
65
65
  | [Deno Sandbox](https://sandboxrouter.vercel.app/providers/deno) | up to $0.300 | $20/mo | not run yet | [pricing](https://deno.com/sandbox) |
66
66
  | [Runloop](https://sandboxrouter.vercel.app/providers/runloop) | $0.317 | none | not run yet | [pricing](https://www.runloop.ai/pricing) |
67
- | [Vercel Sandbox](https://sandboxrouter.vercel.app/providers/vercel) | up to $0.341 | none | not run yet | [pricing](https://vercel.com/docs/vercel-sandbox/pricing) |
67
+ | [Vercel Sandbox](https://sandboxrouter.vercel.app/providers/vercel) | up to $0.341 | none | passed (2026-11-11) | [pricing](https://vercel.com/docs/vercel-sandbox/pricing) |
68
68
  | [InstaBox (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/instabox) | $0.017 | none | not run yet | [pricing](https://instabox.sh/docs/pricing.md) |
69
69
  | [Koyeb Sandboxes (not taking new accounts)](https://sandboxrouter.vercel.app/providers/koyeb) | $0.058 | $29/mo | not run yet | [pricing](https://www.koyeb.com/docs/reference/instances) |
70
70
  | [Dedalus Machines (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/dedalus) | $0.149 | none | not run yet | [pricing](https://docs.dedaluslabs.ai/dcs/dm/pricing) |
71
71
  | [LangSmith Sandboxes (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/langsmith) | $0.189 | none | not run yet | [pricing](https://www.langchain.com/pricing) |
72
- | [OpenScale Sandboxes](https://sandboxrouter.vercel.app/providers/openscale) | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | failed 1 step (2026-11-10) | [pricing](https://openscale.so/sandboxes) |
72
+ | [OpenScale Sandboxes](https://sandboxrouter.vercel.app/providers/openscale) | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | passed (2026-11-11) | [pricing](https://openscale.so/sandboxes) |
73
73
  | [Azure Container Apps dynamic sessions](https://sandboxrouter.vercel.app/providers/azure) | skipped (max 1 vCPU) | none | not run yet | [pricing](https://learn.microsoft.com/en-us/azure/container-apps/billing#dynamic-sessions) |
74
- | [Daytona](https://sandboxrouter.vercel.app/providers/daytona) | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-10) | [pricing](https://www.daytona.io/pricing) |
74
+ | [Daytona](https://sandboxrouter.vercel.app/providers/daytona) | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-11) | [pricing](https://www.daytona.io/pricing) |
75
75
  | [Alibaba Cloud Agent Sandbox](https://sandboxrouter.vercel.app/providers/alibaba) | skipped (max 2 GB memory) | none | not run yet | [pricing](https://www.alibabacloud.com/help/en/agent-sandbox/product-overview/billing-overview) |
76
- | [E2B](https://sandboxrouter.vercel.app/providers/e2b) | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-10) | [pricing](https://e2b.dev/pricing) |
76
+ | [E2B](https://sandboxrouter.vercel.app/providers/e2b) | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-11) | [pricing](https://e2b.dev/pricing) |
77
77
  | [Docker (local)](https://sandboxrouter.vercel.app/providers/docker) | free | none | passed (2026-11-09) | [pricing](https://docs.docker.com/engine/) |
78
78
 
79
79
  Checked 2026-11-04 to 2026-11-06.
@@ -69,3 +69,12 @@ export declare class AgentCoreProvider implements Provider {
69
69
  create(spec: SandboxSpec): Promise<ProviderSandbox>;
70
70
  connect(nativeId: string): Promise<ProviderSandbox>;
71
71
  }
72
+ /**
73
+ * Splits exec's combined terminal output at the stderr marker: the command's stdout comes before
74
+ * it, its stderr (base64) after. If the terminal still translated newlines (stty unavailable), the
75
+ * marker is preceded by \r\n and stdout's \r\n are turned back into \n.
76
+ */
77
+ export declare function splitOutput(combined: string, marker: string): {
78
+ stdout: string;
79
+ stderr: string;
80
+ };
@@ -386,14 +386,39 @@ class AgentCoreSandbox {
386
386
  const timeout = Math.max(1, Math.round(opts.timeoutSeconds ?? 60));
387
387
  const full = wrapCommand(cmd, { cwd: opts.cwd, env: { ...this.env, ...opts.env } });
388
388
  const b64 = Buffer.from(full).toString("base64");
389
+ // The service runs commands in a terminal: stdout and stderr come back as one stream (labelled
390
+ // "stdout" on exit 0 and "stderr" otherwise), with \n turned into \r\n. So stderr goes to a file
391
+ // and is printed base64-encoded after a marker, and `stty -onlcr` keeps newlines as they are.
392
+ const nonce = randomBytes(6).toString("hex");
393
+ const errFile = `/tmp/.sbr-stderr-${nonce}`;
394
+ const marker = `__SBR_STDERR_${nonce}__`;
389
395
  // base64 keeps the user's command intact through whatever quoting the service applies.
390
- const command = `timeout -k 5 ${timeout} bash -c "$(echo ${b64} | base64 -d)"`;
391
- const r = await this.command(command, { onStdout: opts.onStdout, onStderr: opts.onStderr, timeoutMs: (timeout + 60) * 1000 });
396
+ const command = `stty -onlcr 2>/dev/null; timeout -k 5 ${timeout} bash -c "$(echo ${b64} | base64 -d)" 2>${errFile}; c=$?; ` +
397
+ `printf '\\n${marker}\\n'; base64 < ${errFile} | tr -d '\\r\\n'; rm -f ${errFile}; exit $c`;
398
+ // Stream stdout as it arrives, holding back only what could be the start of the marker.
399
+ let combined = "";
400
+ let sent = 0;
401
+ const feed = (chunk) => {
402
+ combined += chunk;
403
+ if (!opts.onStdout)
404
+ return;
405
+ const at = combined.indexOf(`\n${marker}`);
406
+ const safe = at >= 0 ? at : Math.max(sent, combined.length - marker.length - 2);
407
+ if (safe > sent)
408
+ opts.onStdout(combined.slice(sent, safe));
409
+ sent = Math.max(sent, safe);
410
+ };
411
+ const r = await this.command(command, { onStdout: feed, onStderr: feed, timeoutMs: (timeout + 60) * 1000 });
392
412
  const durationMs = Date.now() - t0;
413
+ const { stdout, stderr } = splitOutput(combined || r.stdout + r.stderr, marker);
414
+ if (opts.onStdout && stdout.length > sent)
415
+ opts.onStdout(stdout.slice(sent));
416
+ if (stderr)
417
+ opts.onStderr?.(stderr);
393
418
  // `timeout` leaves exit 124 (or 137 after SIGKILL) when it had to stop the command.
394
419
  if (killedByTimeout(r.exitCode, durationMs, timeout))
395
420
  throw timeoutError(ID, timeout);
396
- return { ...r, durationMs };
421
+ return { stdout, stderr, exitCode: r.exitCode, durationMs };
397
422
  }
398
423
  async writeFile(path, content) {
399
424
  const bytes = toBytes(content);
@@ -426,6 +451,22 @@ class AgentCoreSandbox {
426
451
  }
427
452
  }
428
453
  }
454
+ /**
455
+ * Splits exec's combined terminal output at the stderr marker: the command's stdout comes before
456
+ * it, its stderr (base64) after. If the terminal still translated newlines (stty unavailable), the
457
+ * marker is preceded by \r\n and stdout's \r\n are turned back into \n.
458
+ */
459
+ export function splitOutput(combined, marker) {
460
+ const at = combined.lastIndexOf(`\n${marker}`);
461
+ if (at < 0)
462
+ return { stdout: combined, stderr: "" };
463
+ const translated = combined[at - 1] === "\r";
464
+ let stdout = combined.slice(0, translated ? at - 1 : at);
465
+ if (translated)
466
+ stdout = stdout.replace(/\r\n/g, "\n");
467
+ const b64 = combined.slice(at + marker.length + 1).replace(/\s/g, "");
468
+ return { stdout, stderr: Buffer.from(b64, "base64").toString("utf8") };
469
+ }
429
470
  function exceptionStatus(name) {
430
471
  const n = name.toLowerCase();
431
472
  if (n.startsWith("accessdenied"))
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { ProviderError } from "../types.js";
3
- import { dirname, toBytes, wrapCommand } from "../util.js";
3
+ import { dirname, shq, toBytes, wrapCommand } from "../util.js";
4
4
  /**
5
5
  * Freestyle VMs over the public REST API (no SDK needed). Auth: Authorization: Bearer
6
6
  * <FREESTYLE_API_KEY>, base https://api.freestyle.sh. Errors are {code, message}.
@@ -230,11 +230,18 @@ class FreestyleBox {
230
230
  }
231
231
  async writeFile(path, content) {
232
232
  const bytes = toBytes(content);
233
- // Best effort (whether mkdir of an existing directory errors is not documented); the write
233
+ // The file API runs as root, so a folder it creates belongs to root and commands (which run as
234
+ // the image's user) can't write in it. Make the folder with a command instead, as that user;
235
+ // fall back to the file API where the user can't (e.g. under /root). Best effort: the write
234
236
  // itself reports any real problem.
235
237
  const dir = dirname(path);
236
- if (dir !== "/")
237
- await this.p.request("POST", `${this.path}/fs/mkdir`, { json: { path: dir }, background: true }).catch(() => { });
238
+ if (dir !== "/") {
239
+ const r = await this.p
240
+ .json("POST", `${this.path}/exec-await`, { json: { command: `mkdir -p ${shq(dir)}`, timeoutMs: 30_000 }, background: true })
241
+ .catch(() => undefined);
242
+ if (r?.statusCode !== 0)
243
+ await this.p.request("POST", `${this.path}/fs/mkdir`, { json: { path: dir }, background: true }).catch(() => { });
244
+ }
238
245
  const q = new URLSearchParams({ path, sha256: createHash("sha256").update(bytes).digest("hex") });
239
246
  await this.p.request("PUT", `${this.path}/fs/write?${q}`, { bytes, background: true });
240
247
  }
@@ -75,7 +75,11 @@ class ModalBox {
75
75
  catch (err) {
76
76
  if (err instanceof ProviderError)
77
77
  throw err;
78
- throw new ProviderError("modal", err?.message ?? String(err));
78
+ const message = err?.message ?? String(err);
79
+ // Newer SDKs end the output streams with "Deadline exceeded" at timeoutMs instead of a signal exit.
80
+ if (/deadline exceeded/i.test(message) && killedByTimeout(-1, Date.now() - t0, seconds))
81
+ throw timeoutError("modal", seconds);
82
+ throw new ProviderError("modal", message);
79
83
  }
80
84
  }
81
85
  async writeFile(path, content) {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { StringDecoder } from "node:string_decoder";
3
3
  import { ProviderError } from "../types.js";
4
- import { optionalImport, toBytes } from "../util.js";
4
+ import { optionalImport, timeoutError, toBytes } from "../util.js";
5
5
  /**
6
6
  * Fly.io Sprites via `@fly/sprites`. A sprite is a persistent machine named by the caller, so the
7
7
  * name is the native id. Sprites idle on their own and have no lifetime knob: kill() deletes it.
@@ -78,9 +78,10 @@ class SpritesBox {
78
78
  try {
79
79
  const exitCode = await new Promise((resolve, reject) => {
80
80
  const timer = setTimeout(() => {
81
+ // Reject first: killing can report an exit (-1) right away, and that must not count as the result.
82
+ reject(timeoutError("sprites", limit / 1000));
81
83
  proc.kill("SIGKILL");
82
84
  proc.close();
83
- reject(new ProviderError("sprites", `command timed out after ${limit / 1000}s`));
84
85
  }, limit);
85
86
  timer.unref();
86
87
  proc.on("exit", (code) => {
@@ -89,8 +90,8 @@ class SpritesBox {
89
90
  });
90
91
  proc.on("error", (e) => {
91
92
  clearTimeout(timer);
92
- proc.close();
93
93
  reject(e);
94
+ proc.close();
94
95
  });
95
96
  });
96
97
  const so = out.end();
@@ -27,6 +27,9 @@ export class VercelProvider {
27
27
  resources: { vcpus },
28
28
  env: spec.env,
29
29
  tags: spec.metadata,
30
+ // v3 keeps sandboxes by default and restarts a stopped one on the next command; a router
31
+ // sandbox is single-use, so nothing should outlive kill().
32
+ persistent: false,
30
33
  });
31
34
  return new VercelBox(sbx);
32
35
  }
@@ -93,6 +96,7 @@ class VercelBox {
93
96
  return new Uint8Array(buf);
94
97
  }
95
98
  async kill() {
96
- await this.sbx.stop();
99
+ // stop() only ends the session: the next SDK call would start it again. delete() makes it inert.
100
+ await this.sbx.delete({ deleteOrphanSnapshots: true });
97
101
  }
98
102
  }
package/dist/server.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type IncomingMessage, type ServerResponse } from "node:http";
2
2
  import { Router, type RoutedSandbox } from "./router.js";
3
3
  import { type RouteOptions, type SandboxSpec } from "./types.js";
4
4
  /**
5
- * Hooks for running this API for many users, as the hosted service does (hosted/). With none set,
5
+ * Hooks for running this API for many users (accounts, billing, per-user sandboxes). With none set,
6
6
  * the server is single-user: one SANDBOXROUTER_TOKEN, every sandbox reachable by anyone holding it.
7
7
  */
8
8
  export interface ServerHooks {
package/dist/verified.js CHANGED
@@ -1,7 +1,18 @@
1
1
  export const VERIFIED = {
2
- blaxel: "2026-11-10",
3
- daytona: "2026-11-10",
4
- e2b: "2026-11-10",
5
- maritime: "2026-11-10",
6
- sail: "2026-11-10",
2
+ agent37: "2026-11-11",
3
+ agentcore: "2026-11-11",
4
+ lambda: "2026-11-11",
5
+ blaxel: "2026-11-11",
6
+ cloudflare: "2026-11-11",
7
+ daytona: "2026-11-11",
8
+ e2b: "2026-11-11",
9
+ sprites: "2026-11-11",
10
+ freestyle: "2026-11-11",
11
+ maritime: "2026-11-11",
12
+ modal: "2026-11-11",
13
+ novita: "2026-11-11",
14
+ openscale: "2026-11-11",
15
+ sail: "2026-11-11",
16
+ superserve: "2026-11-11",
17
+ vercel: "2026-11-11",
7
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandboxrouter/sdk",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "One API for every sandbox provider: route AI agents' code by price, speed or capability, with automatic fallback. Free and open source.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",