@sandboxrouter/sdk 0.2.0 → 0.3.1

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 (91) hide show
  1. package/README.md +229 -50
  2. package/dist/bench.js +4 -4
  3. package/dist/catalog.d.ts +159 -0
  4. package/dist/catalog.js +450 -15
  5. package/dist/check.d.ts +28 -0
  6. package/dist/check.js +185 -0
  7. package/dist/cli.js +171 -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 +28 -2
  17. package/dist/index.js +28 -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.js +8 -3
  35. package/dist/providers/cloudflare.js +8 -3
  36. package/dist/providers/codesandbox.d.ts +59 -0
  37. package/dist/providers/codesandbox.js +287 -0
  38. package/dist/providers/daytona.d.ts +25 -4
  39. package/dist/providers/daytona.js +449 -28
  40. package/dist/providers/dedalus.d.ts +12 -0
  41. package/dist/providers/dedalus.js +244 -0
  42. package/dist/providers/deno.d.ts +50 -0
  43. package/dist/providers/deno.js +170 -0
  44. package/dist/providers/docker.js +11 -5
  45. package/dist/providers/e2b.d.ts +70 -3
  46. package/dist/providers/e2b.js +376 -8
  47. package/dist/providers/exedev.js +17 -5
  48. package/dist/providers/freestyle.d.ts +49 -0
  49. package/dist/providers/freestyle.js +261 -0
  50. package/dist/providers/hopx.js +27 -18
  51. package/dist/providers/instabox.d.ts +76 -0
  52. package/dist/providers/instabox.js +360 -0
  53. package/dist/providers/koyeb.d.ts +71 -0
  54. package/dist/providers/koyeb.js +379 -0
  55. package/dist/providers/lambda.d.ts +51 -0
  56. package/dist/providers/lambda.js +467 -0
  57. package/dist/providers/langsmith.d.ts +16 -0
  58. package/dist/providers/langsmith.js +149 -0
  59. package/dist/providers/limrun.d.ts +77 -0
  60. package/dist/providers/limrun.js +626 -0
  61. package/dist/providers/maritime.d.ts +5 -1
  62. package/dist/providers/maritime.js +39 -8
  63. package/dist/providers/mobilerun.d.ts +44 -0
  64. package/dist/providers/mobilerun.js +417 -0
  65. package/dist/providers/modal.js +12 -4
  66. package/dist/providers/morph.js +12 -4
  67. package/dist/providers/northflank.js +11 -13
  68. package/dist/providers/novita.d.ts +12 -0
  69. package/dist/providers/novita.js +81 -0
  70. package/dist/providers/openscale.d.ts +17 -0
  71. package/dist/providers/openscale.js +87 -0
  72. package/dist/providers/orgo.d.ts +45 -0
  73. package/dist/providers/orgo.js +414 -0
  74. package/dist/providers/sail.d.ts +32 -0
  75. package/dist/providers/sail.js +372 -0
  76. package/dist/providers/superserve.d.ts +25 -0
  77. package/dist/providers/superserve.js +337 -0
  78. package/dist/providers/tensorlake.js +17 -14
  79. package/dist/providers/upstash.d.ts +30 -0
  80. package/dist/providers/upstash.js +258 -0
  81. package/dist/providers/vercel.js +14 -3
  82. package/dist/providers/workspaces.d.ts +60 -0
  83. package/dist/providers/workspaces.js +662 -0
  84. package/dist/router.d.ts +41 -5
  85. package/dist/router.js +179 -23
  86. package/dist/server.d.ts +1 -1
  87. package/dist/server.js +22 -3
  88. package/dist/types.d.ts +13 -5
  89. package/dist/util.d.ts +28 -1
  90. package/dist/util.js +56 -7
  91. package/package.json +81 -5
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
3
3
  import { ProviderError } from "../types.js";
4
- import { dirname, optionalImport, shq, toBytes, wrapCommand } from "../util.js";
4
+ import { DEFAULT_EXEC_TIMEOUT_S, dirname, guardTimeout, killedByTimeout, optionalImport, shq, timeoutError, toBytes, wrapCommand } from "../util.js";
5
5
  const DEFAULT_IMAGE = "ubuntu:22.04";
6
6
  /** Northflank's createOptions.expiryTime bounds: 5 minutes to 7 days. */
7
7
  const MIN_EXPIRY_SECONDS = 300;
@@ -128,19 +128,17 @@ class NorthflankBox {
128
128
  }
129
129
  }
130
130
  /**
131
- * Exec has no cwd/env/timeout, so cwd/env are wrapped into the command, the command runs
132
- * under `timeout` when the image has it, and the deadline is also enforced here.
131
+ * Exec has no cwd/env/timeout, so cwd/env are wrapped into the command, the sandbox kills the
132
+ * command at the deadline (`guardTimeout`), and this side gives up a little later as a fallback.
133
133
  */
134
134
  async exec(cmd, opts = {}) {
135
135
  const t0 = Date.now();
136
- const secs = opts.timeoutSeconds ?? 60;
137
- const script = shq(wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }));
138
- const guarded = `if command -v timeout >/dev/null 2>&1; then exec timeout ${secs} /bin/sh -c ${script}; else exec /bin/sh -c ${script}; fi`;
139
- const r = await this.run(["/bin/sh", "-c", guarded], secs * 1000 + 10_000, opts);
136
+ const secs = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
137
+ const guarded = guardTimeout(wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }), secs);
138
+ const r = await this.run(["/bin/sh", "-c", guarded], secs * 1000 + 10_000, opts, undefined, () => timeoutError("northflank", secs));
140
139
  const durationMs = Date.now() - t0;
141
- if (durationMs >= secs * 1000 && [124, 137, 143].includes(r.exitCode)) {
142
- throw new ProviderError("northflank", `command timed out after ${secs}s`);
143
- }
140
+ if (killedByTimeout(r.exitCode, durationMs, secs))
141
+ throw timeoutError("northflank", secs);
144
142
  return { ...r, durationMs };
145
143
  }
146
144
  /** Stream base64 over stdin and decode in the sandbox. */
@@ -159,8 +157,8 @@ class NorthflankBox {
159
157
  async kill() {
160
158
  await call(() => this.nf.delete.service({ parameters: this.params }));
161
159
  }
162
- /** One exec session: optional stdin, streamed stdout/stderr, exit code. */
163
- async run(command, limitMs, opts = {}, stdin) {
160
+ /** One exec session: optional stdin, streamed stdout/stderr, exit code. `expired` is thrown when limitMs passes. */
161
+ async run(command, limitMs, opts = {}, stdin, expired = () => timeoutError("northflank", Math.round(limitMs / 1000))) {
164
162
  const t0 = Date.now();
165
163
  let session;
166
164
  try {
@@ -197,7 +195,7 @@ class NorthflankBox {
197
195
  try {
198
196
  const res = await Promise.race([session.waitForCommandResult(), failed, timedOut]);
199
197
  if (res === "timeout")
200
- throw new ProviderError("northflank", `command timed out after ${Math.round((Date.now() - t0) / 1000)}s`);
198
+ throw expired();
201
199
  return { exitCode: typeof res.exitCode === "number" ? res.exitCode : -1, stdout, stderr };
202
200
  }
203
201
  catch (err) {
@@ -0,0 +1,12 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ export declare class NovitaProvider implements Provider {
3
+ private apiKey;
4
+ private domain;
5
+ readonly id: "novita";
6
+ constructor(apiKey?: string | undefined, domain?: string | undefined);
7
+ configured(): boolean;
8
+ private conn;
9
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
10
+ /** Reattach by sandbox id; a paused sandbox is resumed. */
11
+ connect(nativeId: string): Promise<ProviderSandbox>;
12
+ }
@@ -0,0 +1,81 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { optionalImport } from "../util.js";
3
+ import { E2BBox } from "./e2b.js";
4
+ /**
5
+ * Novita AI Agent Sandbox via the official `novita-sandbox` SDK (2.x), a fork of E2B's SDK with
6
+ * the same surface (Sandbox.create/connect, commands.run with onStdout/onStderr, files.read/write,
7
+ * pause, kill), so the E2B box wrapper is reused as-is.
8
+ *
9
+ * Why not the `e2b` SDK with Novita's domain: that was Novita's documented route for its v1
10
+ * cluster (sandbox.novita.ai), but since the 2026-06-08 SDK 2.0 release the E2B-compatible page
11
+ * is a "Legacy SDKs & CLI" page for the 1.x line, the default cluster moved to
12
+ * us-phx-1.sandbox.novita.ai, and the docs only describe `novita-sandbox` for it.
13
+ *
14
+ * Auth: NOVITA_API_KEY (sent as X-API-KEY by the SDK). NOVITA_DOMAIN optionally picks the cluster
15
+ * (default us-phx-1.sandbox.novita.ai; sandbox.novita.ai is the legacy v1 cluster). Resources are
16
+ * not shared between clusters.
17
+ * Size: vCPU and memory come from the template (set at template build time: cpuCount/memoryMB),
18
+ * not from the create call, exactly like E2B. image.novita = template name or ID (default "base").
19
+ * Pause keeps memory and disk; Sandbox.connect resumes it. Billing is per second for CPU and RAM
20
+ * while running; paused sandboxes pay only persistent storage beyond 60 GB free.
21
+ *
22
+ * Docs: https://docs.novita.ai/guides/sandbox-overview.md (regions/domains)
23
+ * https://docs.novita.ai/guides/sandbox-your-first-agent-sandbox.md (NOVITA_API_KEY, SDK)
24
+ * https://docs.novita.ai/guides/sandbox-metadata.md (create: template, timeoutMs, envs, metadata)
25
+ * https://docs.novita.ai/guides/sandbox-persistence.md (pause / connect resumes)
26
+ * https://docs.novita.ai/guides/sandbox-quota-limit.md (vCPU/memory/session limits)
27
+ * https://docs.novita.ai/guides/sandbox-pricing.md
28
+ * https://docs.novita.ai/changelog/08-06-26 (SDK 2.0, new default cluster)
29
+ * https://docs.novita.ai/guides/sandbox-legacy-e2b-compatible (legacy 1.x / E2B route)
30
+ */
31
+ const ID = "novita";
32
+ export class NovitaProvider {
33
+ apiKey;
34
+ domain;
35
+ id = ID;
36
+ constructor(apiKey = process.env.NOVITA_API_KEY, domain = process.env.NOVITA_DOMAIN) {
37
+ this.apiKey = apiKey;
38
+ this.domain = domain;
39
+ }
40
+ configured() {
41
+ return !!this.apiKey;
42
+ }
43
+ conn() {
44
+ if (!this.apiKey)
45
+ throw new ProviderError(ID, "NOVITA_API_KEY is not set");
46
+ return { apiKey: this.apiKey, ...(this.domain ? { domain: this.domain } : {}) };
47
+ }
48
+ async create(spec) {
49
+ const { Sandbox } = await optionalImport("novita-sandbox", "novita");
50
+ const opts = {
51
+ ...this.conn(),
52
+ timeoutMs: (spec.timeoutMinutes ?? 30) * 60_000,
53
+ envs: spec.env,
54
+ metadata: spec.metadata,
55
+ };
56
+ const template = spec.image?.novita;
57
+ try {
58
+ const sbx = template ? await Sandbox.create(template, opts) : await Sandbox.create(opts);
59
+ return new E2BBox(sbx, ID);
60
+ }
61
+ catch (err) {
62
+ throw wrap(err);
63
+ }
64
+ }
65
+ /** Reattach by sandbox id; a paused sandbox is resumed. */
66
+ async connect(nativeId) {
67
+ const { Sandbox } = await optionalImport("novita-sandbox", "novita");
68
+ try {
69
+ return new E2BBox(await Sandbox.connect(nativeId, this.conn()), ID);
70
+ }
71
+ catch (err) {
72
+ throw wrap(err);
73
+ }
74
+ }
75
+ }
76
+ function wrap(err) {
77
+ if (err instanceof ProviderError)
78
+ return err;
79
+ const status = err?.name === "SandboxNotFoundError" || err?.name === "NotFoundError" ? 404 : err?.name === "AuthenticationError" ? 401 : undefined;
80
+ return new ProviderError(ID, err?.message ?? String(err), status);
81
+ }
@@ -0,0 +1,17 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ export declare const OPENSCALE_DOMAIN = "sandbox.openscale.so";
3
+ export declare class OpenScaleProvider implements Provider {
4
+ private apiKey;
5
+ readonly id: "openscale";
6
+ constructor(apiKey?: string | undefined);
7
+ configured(): boolean;
8
+ /** @internal Connection options for the e2b SDK. */
9
+ conn(): {
10
+ apiKey: string;
11
+ domain: string;
12
+ apiUrl: string;
13
+ };
14
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
15
+ /** Reattach by sandbox id; a paused sandbox is resumed. */
16
+ connect(nativeId: string): Promise<ProviderSandbox>;
17
+ }
@@ -0,0 +1,87 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { optionalImport } from "../util.js";
3
+ import { E2BBox } from "./e2b.js";
4
+ /**
5
+ * OpenScale Sandboxes (YC S22) through their E2B-compatible API, driven with the official `e2b`
6
+ * SDK pointed at OpenScale's domain:
7
+ * API URL https://api.sandbox.openscale.so (the SDK's https://api.<domain> default)
8
+ * domain sandbox.openscale.so (data plane: <port>-<sandboxId>.<domain>)
9
+ * OpenScale's own instructions are E2B_DOMAIN=sandbox.openscale.so plus the OpenScale key as
10
+ * E2B_API_KEY. This adapter does not read E2B_API_KEY, so a real E2B key is never sent to
11
+ * OpenScale (and an OpenScale key never to E2B): it reads OPENSCALE_API_KEY (the name OpenScale's
12
+ * model docs use) and passes key, domain and apiUrl explicitly, so a stray E2B_DOMAIN or
13
+ * E2B_API_URL can't redirect calls. E2B_SANDBOX_URL would still override the data plane, so don't
14
+ * set it alongside this adapter.
15
+ *
16
+ * Auth: a pay-as-you-go OpenScale key (sk-openscale-...), the same account and credits as their
17
+ * model API. Plan (subscription) keys are refused: the API answers 401 "Use a pay-as-you-go
18
+ * OpenScale API key; plan keys don't run sandboxes". A new key can take ~30 s to start working.
19
+ * Region: Singapore only for now.
20
+ *
21
+ * Size: vCPU and memory come from the template, as with E2B (1-8 vCPU, up to 16 GB, 25 GB disk
22
+ * included, no GPU). image.openscale = template name or ID; default "base", the template the
23
+ * quick start says a sandbox starts from. Custom templates are built with the E2B template
24
+ * builder or CLI (Dockerfile) against the same domain.
25
+ * Lifetime: timeoutMs as given; OpenScale advertises no session time limit or 24 h cap, so no cap
26
+ * is applied here.
27
+ * Pause/resume is advertised as built in (memory and files kept, paused sandboxes are free), so
28
+ * the box offers pause(); Sandbox.connect resumes. Not supported yet per OpenScale: volumes,
29
+ * secrets, fork, snapshots API.
30
+ *
31
+ * Docs: https://openscale.so/sandboxes (setup, limits, pricing, FAQ)
32
+ * https://openscale.so/app/sandboxes (keys and quick start, signed in)
33
+ * https://openscale.so/docs (API keys: pay-as-you-go vs plan keys)
34
+ */
35
+ const ID = "openscale";
36
+ export const OPENSCALE_DOMAIN = "sandbox.openscale.so";
37
+ const DEFAULT_TEMPLATE = "base";
38
+ export class OpenScaleProvider {
39
+ apiKey;
40
+ id = ID;
41
+ constructor(apiKey = process.env.OPENSCALE_API_KEY) {
42
+ this.apiKey = apiKey;
43
+ }
44
+ configured() {
45
+ return !!this.apiKey;
46
+ }
47
+ /** @internal Connection options for the e2b SDK. */
48
+ conn() {
49
+ if (!this.apiKey)
50
+ throw new ProviderError(ID, "OPENSCALE_API_KEY is not set");
51
+ return { apiKey: this.apiKey, domain: OPENSCALE_DOMAIN, apiUrl: `https://api.${OPENSCALE_DOMAIN}` };
52
+ }
53
+ async create(spec) {
54
+ const conn = this.conn();
55
+ const { Sandbox } = await optionalImport("e2b", "openscale");
56
+ const timeoutMs = Math.max(1, spec.timeoutMinutes ?? 30) * 60_000;
57
+ try {
58
+ const sbx = await Sandbox.create(spec.image?.openscale ?? DEFAULT_TEMPLATE, {
59
+ ...conn,
60
+ timeoutMs,
61
+ envs: spec.env,
62
+ metadata: spec.metadata,
63
+ });
64
+ return new E2BBox(sbx, ID);
65
+ }
66
+ catch (err) {
67
+ throw wrap(err);
68
+ }
69
+ }
70
+ /** Reattach by sandbox id; a paused sandbox is resumed. */
71
+ async connect(nativeId) {
72
+ const conn = this.conn();
73
+ const { Sandbox } = await optionalImport("e2b", "openscale");
74
+ try {
75
+ return new E2BBox((await Sandbox.connect(nativeId, conn)), ID);
76
+ }
77
+ catch (err) {
78
+ throw wrap(err);
79
+ }
80
+ }
81
+ }
82
+ function wrap(err) {
83
+ if (err instanceof ProviderError)
84
+ return err;
85
+ const status = err?.name === "SandboxNotFoundError" || err?.name === "NotFoundError" ? 404 : err?.name === "AuthenticationError" ? 401 : undefined;
86
+ return new ProviderError(ID, err?.message ?? String(err), status);
87
+ }
@@ -0,0 +1,45 @@
1
+ import type { ComputerOs, ComputerProvider, ComputerSpec, ProviderComputer } from "../computers.js";
2
+ type Json = Record<string, any>;
3
+ export declare class OrgoProvider implements ComputerProvider {
4
+ private apiKey;
5
+ private baseUrl;
6
+ /** Workspace to create computers in. Unset: the first workspace this key can write to. */
7
+ private workspaceId;
8
+ readonly id: "orgo";
9
+ /**
10
+ * Linux on every account. Windows is created with `os: "windows"` but needs a Scale plan or a
11
+ * purchased Windows licence (else 403 WINDOWS_REQUIRES_SCALE) and is never offered by the hour.
12
+ * Android, macOS and iPhone are devices set up on request, not self-serve.
13
+ */
14
+ readonly computerOs: ComputerOs[];
15
+ private workspace?;
16
+ constructor(apiKey?: string | undefined, baseUrl?: string,
17
+ /** Workspace to create computers in. Unset: the first workspace this key can write to. */
18
+ workspaceId?: string | undefined);
19
+ configured(): boolean;
20
+ /** @internal Authenticated call to the Orgo API. Throws ProviderError with the HTTP status. */
21
+ request(method: string, path: string, init?: {
22
+ json?: unknown;
23
+ form?: FormData;
24
+ query?: Record<string, string>;
25
+ }): Promise<Response>;
26
+ /** @internal */
27
+ json<T = Json>(method: string, path: string, init?: {
28
+ json?: unknown;
29
+ form?: FormData;
30
+ query?: Record<string, string>;
31
+ }): Promise<T>;
32
+ /** @internal The workspace new computers go in: ORGO_WORKSPACE_ID, else the first one the key can write to. */
33
+ resolveWorkspace(): Promise<string>;
34
+ createComputer(spec: ComputerSpec): Promise<ProviderComputer>;
35
+ /**
36
+ * Reattach to a computer. A stopped (frozen) one is started, a suspended one resumed. Both answer
37
+ * 202 while the computer comes back, so it is polled until it reports running.
38
+ */
39
+ connectComputer(nativeId: string): Promise<ProviderComputer>;
40
+ /** Poll a computer until it is running; a computer that errored or was deleted meanwhile is reported. */
41
+ private waitRunning;
42
+ /** @internal wss:// origin for the computer's VNC WebSocket (same host as the API). */
43
+ wsOrigin(): string;
44
+ }
45
+ export {};