@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,287 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { CODESANDBOX_TIERS, DEFAULT_MEMORY_GB, DEFAULT_VCPU, codesandboxTier } from "../catalog.js";
3
+ export { CODESANDBOX_TIERS, codesandboxTier };
4
+ import { ProviderError } from "../types.js";
5
+ import { dirname, optionalImport, shq, toBytes, wrapCommand } from "../util.js";
6
+ /**
7
+ * CodeSandbox SDK (Together AI's "code sandbox", legacy edition) via `@codesandbox/sdk` (checked
8
+ * against 2.4.2). Auth: CSB_API_KEY, a token from https://codesandbox.io/t/api.
9
+ * new CodeSandbox(key).sandboxes.create({ id: template, vmTier, hibernationTimeoutSeconds, privacy })
10
+ * sandboxes.resume(id) -> Sandbox (wakes a hibernated one), sandbox.connect() -> SandboxClient
11
+ * client.commands.runBackground(cmd) -> Command (.waitUntilComplete(), .kill())
12
+ * client.fs.writeFile / readFile / mkdir / remove
13
+ * sandboxes.hibernate(id) memory + disk snapshot; resume continues where it left off
14
+ * sandboxes.shutdown(id), sandboxes.delete(id)
15
+ * Sizes are fixed VM tiers billed in credits per minute (CODESANDBOX_TIERS below); the router asks
16
+ * for the smallest tier that fits. timeoutMinutes becomes hibernationTimeoutSeconds (60 s .. 1 day),
17
+ * which is an inactivity timeout that hibernates, not a hard kill.
18
+ * Commands run in a PTY that merges stdout and stderr, so exec redirects both to files in /tmp and
19
+ * reads them back; onStdout/onStderr are fed by polling those files. spec.env is written to
20
+ * $HOME/.sandboxrouter.env and sourced by every exec, so it survives connect().
21
+ * Successor: Together's `together-sandbox` SDK (https://docs.together.ai/docs/together-code-sandbox)
22
+ * needs sales enablement and publishes no prices, so it isn't used here.
23
+ * Docs: https://docs.together.ai/docs/together-code-sandbox-legacy, https://codesandbox.io/docs/sdk,
24
+ * https://github.com/codesandbox/codesandbox-sdk, https://codesandbox.io/pricing
25
+ */
26
+ const ID = "codesandbox";
27
+ /**
28
+ * VM tiers and list prices ($0.01486 per credit), from
29
+ * https://docs.together.ai/docs/together-code-sandbox-legacy ("VM credit prices by VM size").
30
+ * Pico: the docs table says 2 cores / 1 GB while the SDK's VMTier says 1 CPU / 2 GiB, so it is only
31
+ * picked for requests that fit both (1 vCPU, 1 GB).
32
+ */
33
+ /** Hibernation timeout bounds: below 60 s the SDK warns of unstable sessions; 86400 s is the max. */
34
+ const MIN_HIBERNATE_S = 60;
35
+ const MAX_HIBERNATE_S = 86_400;
36
+ const POLL_MS = 250;
37
+ const ENV_FILE = "$HOME/.sandboxrouter.env";
38
+ function fail(err, what) {
39
+ if (err instanceof ProviderError)
40
+ return err;
41
+ const status = typeof err?.status === "number" ? err.status : typeof err?.response?.status === "number" ? err.response.status : undefined;
42
+ return new ProviderError(ID, `${what}: ${err?.message ?? String(err)}`, status);
43
+ }
44
+ export class CodeSandboxProvider {
45
+ apiKey;
46
+ baseUrl;
47
+ id = ID;
48
+ sdk;
49
+ constructor(apiKey = process.env.CSB_API_KEY, baseUrl = process.env.CSB_BASE_URL) {
50
+ this.apiKey = apiKey;
51
+ this.baseUrl = baseUrl;
52
+ }
53
+ configured() {
54
+ return !!this.apiKey;
55
+ }
56
+ /** @internal */
57
+ async module() {
58
+ return optionalImport("@codesandbox/sdk", ID);
59
+ }
60
+ /** @internal */
61
+ async client() {
62
+ if (!this.apiKey)
63
+ throw new ProviderError(ID, "CSB_API_KEY is not set");
64
+ if (!this.sdk) {
65
+ const { CodeSandbox } = await this.module();
66
+ this.sdk = new CodeSandbox(this.apiKey, this.baseUrl ? { baseUrl: this.baseUrl } : undefined);
67
+ }
68
+ return this.sdk;
69
+ }
70
+ async create(spec) {
71
+ const [sdk, { VMTier }] = await Promise.all([this.client(), this.module()]);
72
+ const tier = codesandboxTier(spec.vcpu ?? DEFAULT_VCPU, spec.memoryGb ?? DEFAULT_MEMORY_GB);
73
+ const hibernate = Math.min(MAX_HIBERNATE_S, Math.max(MIN_HIBERNATE_S, Math.round((spec.timeoutMinutes ?? 30) * 60)));
74
+ let sandbox;
75
+ try {
76
+ sandbox = await sdk.sandboxes.create({
77
+ ...(spec.image?.codesandbox ? { id: spec.image.codesandbox } : {}),
78
+ vmTier: VMTier.fromName(tier.name),
79
+ hibernationTimeoutSeconds: hibernate,
80
+ // The SDK defaults to "public-hosts" (open preview URLs); keep router sandboxes private.
81
+ privacy: "private",
82
+ tags: ["sdk", "sandboxrouter"],
83
+ ...(spec.metadata?.name ? { title: spec.metadata.name } : {}),
84
+ });
85
+ }
86
+ catch (err) {
87
+ throw fail(err, "create");
88
+ }
89
+ const box = new CodeSandboxBox(this, sandbox.id);
90
+ try {
91
+ await box.attach(sandbox);
92
+ if (spec.env && Object.keys(spec.env).length)
93
+ await box.saveEnv(spec.env);
94
+ return box;
95
+ }
96
+ catch (err) {
97
+ await box.kill().catch(() => { });
98
+ throw fail(err, "create");
99
+ }
100
+ }
101
+ /** Reattach by id; a hibernated sandbox is resumed (shut-down ones boot clean). */
102
+ async connect(nativeId) {
103
+ const box = new CodeSandboxBox(this, nativeId);
104
+ await box.session();
105
+ return box;
106
+ }
107
+ }
108
+ class CodeSandboxBox {
109
+ p;
110
+ nativeId;
111
+ conn;
112
+ constructor(p, nativeId) {
113
+ this.p = p;
114
+ this.nativeId = nativeId;
115
+ }
116
+ /** @internal */
117
+ async attach(sandbox) {
118
+ this.conn = await sandbox.connect();
119
+ return this.conn;
120
+ }
121
+ /** The live session, resuming the sandbox first if this box has none (fresh connect or after pause). */
122
+ async session() {
123
+ if (this.conn)
124
+ return this.conn;
125
+ try {
126
+ const sdk = await this.p.client();
127
+ return await this.attach(await sdk.sandboxes.resume(this.nativeId));
128
+ }
129
+ catch (err) {
130
+ throw fail(err, `resume ${this.nativeId}`);
131
+ }
132
+ }
133
+ /** @internal Persist env vars as `export` lines in the sandbox; every exec sources the file. */
134
+ async saveEnv(env) {
135
+ const lines = Object.entries(env).map(([k, v]) => {
136
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k))
137
+ throw new ProviderError(ID, `invalid env var name: ${k}`);
138
+ return `export ${k}=${shq(v)}\n`;
139
+ });
140
+ const b64 = Buffer.from(lines.join("")).toString("base64");
141
+ const r = await this.exec(`echo ${b64} | base64 -d > "${ENV_FILE}"`, { timeoutSeconds: 30 });
142
+ if (r.exitCode !== 0)
143
+ throw new ProviderError(ID, `could not write env file: ${r.stderr.trim()}`);
144
+ }
145
+ async exec(cmd, opts = {}) {
146
+ const t0 = Date.now();
147
+ const client = await this.session();
148
+ const limit = (opts.timeoutSeconds ?? 60) * 1000;
149
+ const dir = `/tmp/.sbr-${randomBytes(8).toString("hex")}`;
150
+ const user = wrapCommand(cmd, { cwd: opts.cwd, env: opts.env });
151
+ const script = [
152
+ `mkdir -p ${dir} || exit 1`,
153
+ `[ -f "${ENV_FILE}" ] && . "${ENV_FILE}"`,
154
+ `( ${user}\n) > ${dir}/out 2> ${dir}/err < /dev/null`,
155
+ ].join("\n");
156
+ // base64 so the script passes the SDK's own `bash -c '...'` quoting untouched.
157
+ const line = `bash -c "$(echo ${Buffer.from(script).toString("base64")} | base64 -d)"`;
158
+ let command;
159
+ try {
160
+ command = await client.commands.runBackground(line);
161
+ }
162
+ catch (err) {
163
+ throw fail(err, "exec");
164
+ }
165
+ let done = false;
166
+ const finished = command.waitUntilComplete().then(() => 0, (err) => {
167
+ // CommandError carries the exit code for a nonzero exit: that's a result, not a failure.
168
+ if (typeof err?.exitCode === "number")
169
+ return err.exitCode;
170
+ throw err;
171
+ });
172
+ finished.finally(() => (done = true)).catch(() => { });
173
+ const out = new Tail(client, `${dir}/out`, opts.onStdout);
174
+ const err = new Tail(client, `${dir}/err`, opts.onStderr);
175
+ const streaming = !!(opts.onStdout || opts.onStderr);
176
+ const poller = (async () => {
177
+ while (streaming && !done) {
178
+ await Promise.all([out.poll(), err.poll()]);
179
+ if (!done)
180
+ await sleep(POLL_MS);
181
+ }
182
+ })();
183
+ let timer;
184
+ const timeout = new Promise((resolve) => (timer = setTimeout(() => resolve("timeout"), limit)));
185
+ try {
186
+ const r = await Promise.race([finished, timeout]);
187
+ if (r === "timeout") {
188
+ done = true;
189
+ finished.catch(() => { });
190
+ await command.kill().catch(() => { });
191
+ throw new ProviderError(ID, `command timed out after ${limit / 1000}s`);
192
+ }
193
+ await poller;
194
+ const [stdout, stderr] = await Promise.all([out.poll(), err.poll()]).then(() => [out.text(), err.text()]);
195
+ return { exitCode: r, stdout, stderr, durationMs: Date.now() - t0 };
196
+ }
197
+ catch (e) {
198
+ throw fail(e, "exec");
199
+ }
200
+ finally {
201
+ clearTimeout(timer);
202
+ done = true;
203
+ client.fs.remove(dir, true).catch(() => { });
204
+ }
205
+ }
206
+ async writeFile(path, content) {
207
+ const client = await this.session();
208
+ try {
209
+ const parent = dirname(path);
210
+ if (parent !== "/")
211
+ await client.fs.mkdir(parent, true).catch(() => { });
212
+ await client.fs.writeFile(path, toBytes(content), { create: true, overwrite: true });
213
+ }
214
+ catch (err) {
215
+ throw fail(err, `write ${path}`);
216
+ }
217
+ }
218
+ async readFile(path) {
219
+ const client = await this.session();
220
+ try {
221
+ return await client.fs.readFile(path);
222
+ }
223
+ catch (err) {
224
+ throw fail(err, `read ${path}`);
225
+ }
226
+ }
227
+ /** Hibernate: memory and disk are snapshotted. router.get() (connect) resumes it. */
228
+ async pause() {
229
+ const sdk = await this.p.client();
230
+ await this.conn?.disconnect().catch(() => { });
231
+ this.conn = undefined;
232
+ try {
233
+ await sdk.sandboxes.hibernate(this.nativeId);
234
+ }
235
+ catch (err) {
236
+ throw fail(err, "hibernate");
237
+ }
238
+ }
239
+ /** Shut the VM down (billing stops), then delete the sandbox so it doesn't pile up in the workspace. */
240
+ async kill() {
241
+ const sdk = await this.p.client();
242
+ await this.conn?.disconnect().catch(() => { });
243
+ this.conn = undefined;
244
+ await sdk.sandboxes.shutdown(this.nativeId).catch(() => { });
245
+ try {
246
+ await sdk.sandboxes.delete(this.nativeId);
247
+ }
248
+ catch (err) {
249
+ throw fail(err, "delete");
250
+ }
251
+ }
252
+ }
253
+ /** Follows a file in the sandbox: each poll reads it and passes on the bytes added since the last. */
254
+ class Tail {
255
+ client;
256
+ path;
257
+ on;
258
+ bytes = new Uint8Array(0);
259
+ decoder = new TextDecoder();
260
+ constructor(client, path, on) {
261
+ this.client = client;
262
+ this.path = path;
263
+ this.on = on;
264
+ }
265
+ async poll() {
266
+ let data;
267
+ try {
268
+ data = await this.client.fs.readFile(this.path);
269
+ }
270
+ catch {
271
+ return; // not created yet
272
+ }
273
+ if (data.length <= this.bytes.length)
274
+ return;
275
+ const fresh = data.subarray(this.bytes.length);
276
+ this.bytes = data;
277
+ const text = this.decoder.decode(fresh, { stream: true });
278
+ if (text)
279
+ this.on?.(text);
280
+ }
281
+ text() {
282
+ return new TextDecoder().decode(this.bytes);
283
+ }
284
+ }
285
+ function sleep(ms) {
286
+ return new Promise((r) => setTimeout(r, ms));
287
+ }
@@ -1,13 +1,34 @@
1
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 {
2
+ import type { ComputerOs, ComputerProvider, ComputerSpec, ProviderComputer } from "../computers.js";
3
+ /**
4
+ * Daytona via `@daytonaio/sdk`. Default snapshot; set image.daytona to start from an image.
5
+ *
6
+ * Computers are Linux container sandboxes from a default snapshot (they ship Xvfb, xfce4,
7
+ * x11vnc and noVNC) with the desktop started through `computerUse.start()`. Windows sandboxes
8
+ * exist but need a Tier 3 organization plus a support request, so they aren't routed here.
9
+ * Pass `computerSnapshot` (e.g. "daytona-medium") for a bigger desktop than the 1 vCPU / 1 GiB default.
10
+ * Docs: https://www.daytona.io/docs/en/computer-use
11
+ */
12
+ export declare class DaytonaProvider implements Provider, ComputerProvider {
4
13
  private apiKey;
14
+ private computerSnapshot?;
5
15
  readonly id: "daytona";
16
+ readonly computerOs: ComputerOs[];
6
17
  private client?;
7
- constructor(apiKey?: string | undefined);
18
+ constructor(apiKey?: string | undefined, computerSnapshot?: string | undefined);
8
19
  configured(): boolean;
9
20
  private daytona;
10
21
  create(spec: SandboxSpec): Promise<ProviderSandbox>;
11
- /** Reattach; a paused (stopped) sandbox is started again. */
22
+ private discardTagged;
23
+ /**
24
+ * Reattach. Daytona stops, archives or pauses idle sandboxes on its own schedule (and pause()
25
+ * stops them), so whatever state the sandbox is in, it is brought back to started first.
26
+ */
12
27
  connect(nativeId: string): Promise<ProviderSandbox>;
28
+ /** A new Linux desktop. `externalUserId` is stored as a label only: every call creates a new sandbox. */
29
+ createComputer(spec: ComputerSpec): Promise<ProviderComputer>;
30
+ /** Reattach; a stopped, paused or archived computer is started again, desktop included. */
31
+ connectComputer(nativeId: string): Promise<ProviderComputer>;
13
32
  }
33
+ /** @internal "ctrl+Return" -> ["ctrl", "enter"]. */
34
+ export declare function daytonaKeys(chord: string): string[];