@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
@@ -0,0 +1,337 @@
1
+ import { readSSE } from "../sse.js";
2
+ import { ProviderError } from "../types.js";
3
+ import { toBytes } from "../util.js";
4
+ /**
5
+ * Superserve (YC F26) over plain REST. Firecracker microVMs that pause (memory + disk kept,
6
+ * compute billing stops) and resume where they left off.
7
+ *
8
+ * Control plane https://api.superserve.ai, header X-API-Key: <SUPERSERVE_API_KEY> (ss_live_...):
9
+ * POST /sandboxes {name, from_template, env_vars, timeout_seconds, metadata}
10
+ * -> 201 {id, status, vcpu_count, memory_mib, access_token, routing_hint}
11
+ * GET /sandboxes/:id -> {id, status: starting|active|pausing|paused|resuming|migrating|failed}
12
+ * DELETE /sandboxes/:id -> 204
13
+ * POST /sandboxes/:id/pause -> 204 paused, or 202 {status:"pausing"} (then poll GET)
14
+ * POST /sandboxes/:id/activate -> {...sandbox, access_token, routing_hint}: resumes a paused
15
+ * sandbox and always mints a fresh token; 409 sandbox_pausing
16
+ * with Retry-After while a pause is still landing
17
+ * Data plane (one sandbox, header X-Access-Token from create/activate) on the shared host
18
+ * https://sandbox.superserve.ai with X-Superserve-Sandbox-Id: <id> (what the Node SDK uses; the
19
+ * per-box host boxd-<id>.sandbox.superserve.ai also works), plus X-Superserve-Routing-Hint if given:
20
+ * POST /exec/stream {command, env, working_dir, timeout_s} -> text/event-stream, each data: is
21
+ * {stdout} | {stderr} | {exit_code, finished:true} | {error, code, finished:true}.
22
+ * `command` runs under /bin/sh -c. At timeout_s the command is killed with exit code 124.
23
+ * GET /files?path=P -> raw bytes; POST /files?path=P (application/octet-stream) creates parents
24
+ * A paused sandbox answers the data plane with 503 sandbox_unavailable, and a stale token with 401:
25
+ * the adapter then calls /activate once and retries, as the SDK does (src/tokenRetry.ts).
26
+ * Errors are {error:{code, message}}.
27
+ * Size: a sandbox inherits vCPU / memory / disk from its template and cannot override them. Every
28
+ * system template (superserve/base, superserve/python-3.11, ...) is 2 vCPU / 4 GiB / 8 GiB, so
29
+ * specs up to 2 / 4 boot superserve/base and larger specs need image.superserve = a team template
30
+ * built at that size (paid plans; Free allows 0 team templates).
31
+ * Lifetime: spec.timeoutMinutes becomes timeout_seconds, an auto-pause (not delete) after that much
32
+ * active time. A paused box bills storage only and stays until kill().
33
+ * Regions: a key shaped ss_live_<region>_... picks that region's hosts (use, usw), like the SDK.
34
+ * Docs: https://docs.superserve.ai/llms.txt, https://docs.superserve.ai/sandbox/lifecycle.md,
35
+ * https://docs.superserve.ai/templates/overview.md, https://docs.superserve.ai/sdk-reference/commands.md,
36
+ * https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml,
37
+ * @superserve/sdk 0.11.6 on npm (dist/index.js: config.ts, commands.ts, files.ts, tokenRetry.ts),
38
+ * https://superserve.ai/pricing
39
+ */
40
+ const ID = "superserve";
41
+ const DEFAULT_TEMPLATE = "superserve/base";
42
+ /** Shape of every superserve/* system template. */
43
+ const SYSTEM_SHAPE = { vcpu: 2, memoryGb: 4 };
44
+ const PAUSE_TIMEOUT_MS = 2 * 60_000;
45
+ const ACTIVATE_TIMEOUT_MS = 2 * 60_000;
46
+ const POLL_MS = 500;
47
+ /** Extra time past the command timeout before the client gives up on its own. */
48
+ const CLIENT_GRACE_MS = 30_000;
49
+ const TIMEOUT_EXIT = 124;
50
+ /** API host -> data-plane host, from the SDK's KNOWN_REGIONS / deriveSandboxHost. */
51
+ const REGIONS = {
52
+ use: { api: "https://api.superserve.ai", sandbox: "https://sandbox.superserve.ai" },
53
+ usw: { api: "https://api-usw.superserve.ai", sandbox: "https://usw-sandbox.superserve.ai" },
54
+ };
55
+ const KNOWN_DATA_PLANES = {
56
+ "api.superserve.ai": "https://sandbox.superserve.ai",
57
+ "api-usw.superserve.ai": "https://usw-sandbox.superserve.ai",
58
+ "api-staging.superserve.ai": "https://staging-sandbox.superserve.ai",
59
+ };
60
+ export class SuperserveProvider {
61
+ apiKey;
62
+ id = ID;
63
+ /** @internal */
64
+ base;
65
+ /** @internal */
66
+ dataPlane;
67
+ /**
68
+ * @param baseUrl control-plane URL (SUPERSERVE_BASE_URL, as the SDK reads it); default from the key's region
69
+ * @param sandboxUrl data-plane URL (SUPERSERVE_SANDBOX_URL); default the region's shared host, or
70
+ * baseUrl itself when baseUrl is not a known Superserve host (a mock or proxy)
71
+ */
72
+ constructor(apiKey = process.env.SUPERSERVE_API_KEY, baseUrl = process.env.SUPERSERVE_BASE_URL, sandboxUrl = process.env.SUPERSERVE_SANDBOX_URL) {
73
+ this.apiKey = apiKey;
74
+ const region = /^ss_live_([a-z0-9]{1,17})_[A-Za-z0-9_-]{32}$/.exec(apiKey ?? "")?.[1];
75
+ const fromKey = region ? REGIONS[region] : undefined;
76
+ this.base = (baseUrl?.trim() || fromKey?.api || REGIONS.use.api).replace(/\/+$/, "");
77
+ let host = "";
78
+ try {
79
+ host = new URL(this.base).hostname;
80
+ }
81
+ catch {
82
+ /* invalid base URL: the first request reports it */
83
+ }
84
+ this.dataPlane = (sandboxUrl?.trim() || KNOWN_DATA_PLANES[host] || this.base).replace(/\/+$/, "");
85
+ }
86
+ configured() {
87
+ return !!this.apiKey;
88
+ }
89
+ /** @internal Control-plane call with the team API key. */
90
+ async api(method, path, json, headers = {}) {
91
+ if (!this.apiKey)
92
+ throw new ProviderError(ID, "SUPERSERVE_API_KEY is not set");
93
+ const res = await fetch(`${this.base}${path}`, {
94
+ method,
95
+ headers: { "X-API-Key": this.apiKey, ...(json !== undefined ? { "Content-Type": "application/json" } : {}), ...headers },
96
+ body: json !== undefined ? JSON.stringify(json) : undefined,
97
+ });
98
+ const text = await res.text();
99
+ if (!res.ok)
100
+ throw apiError(method, path, res.status, text);
101
+ return { status: res.status, body: (text ? JSON.parse(text) : {}), res };
102
+ }
103
+ async create(spec) {
104
+ const template = spec.image?.superserve;
105
+ const vcpu = spec.vcpu ?? SYSTEM_SHAPE.vcpu;
106
+ const mem = spec.memoryGb ?? SYSTEM_SHAPE.memoryGb;
107
+ if (!template && (vcpu > SYSTEM_SHAPE.vcpu || mem > SYSTEM_SHAPE.memoryGb)) {
108
+ throw new ProviderError(ID, `system templates are fixed at ${SYSTEM_SHAPE.vcpu} vCPU / ${SYSTEM_SHAPE.memoryGb} GB; for ${vcpu} vCPU / ${mem} GB set image.superserve to a team template built at that size`);
109
+ }
110
+ const minutes = spec.timeoutMinutes ?? 30;
111
+ const { body: sb } = await this.api("POST", "/sandboxes", {
112
+ name: `sbr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
113
+ from_template: template ?? DEFAULT_TEMPLATE,
114
+ // Auto-pause after this much active time; compute billing stops, the box is kept.
115
+ timeout_seconds: Math.min(604_800, Math.max(60, Math.round(minutes * 60))),
116
+ ...(spec.env && Object.keys(spec.env).length ? { env_vars: spec.env } : {}),
117
+ ...(spec.metadata && Object.keys(spec.metadata).length ? { metadata: spec.metadata } : {}),
118
+ });
119
+ if (!sb.id)
120
+ throw new ProviderError(ID, "create returned no sandbox id");
121
+ const box = new SuperserveBox(this, sb.id, sb.access_token, sb.routing_hint);
122
+ if (!sb.access_token) {
123
+ try {
124
+ await box.activate();
125
+ }
126
+ catch (err) {
127
+ await box.kill().catch(() => { });
128
+ throw err;
129
+ }
130
+ }
131
+ return box;
132
+ }
133
+ /** Resume (if paused) and fetch a fresh access token. 404 once the sandbox is deleted. */
134
+ async connect(nativeId) {
135
+ const box = new SuperserveBox(this, nativeId);
136
+ await box.activate();
137
+ return box;
138
+ }
139
+ }
140
+ class SuperserveBox {
141
+ p;
142
+ nativeId;
143
+ token;
144
+ routingHint;
145
+ constructor(p, nativeId, token, routingHint) {
146
+ this.p = p;
147
+ this.nativeId = nativeId;
148
+ this.token = token;
149
+ this.routingHint = routingHint;
150
+ }
151
+ get path() {
152
+ return `/sandboxes/${encodeURIComponent(this.nativeId)}`;
153
+ }
154
+ /** POST /activate, waiting out a pause that is still landing. */
155
+ async activate() {
156
+ const deadline = Date.now() + ACTIVATE_TIMEOUT_MS;
157
+ for (;;) {
158
+ try {
159
+ const { body } = await this.p.api("POST", `${this.path}/activate`);
160
+ if (!body.access_token)
161
+ throw new ProviderError(ID, `activate ${this.nativeId} returned no access_token`);
162
+ this.token = body.access_token;
163
+ this.routingHint = body.routing_hint;
164
+ return;
165
+ }
166
+ catch (err) {
167
+ // A pause still landing (409 sandbox_pausing, Retry-After ~1 s) or a box mid-move: retry.
168
+ const e = err;
169
+ const busy = e instanceof ProviderError && (e.status === 409 || e.status === 503) && /sandbox_pausing|secret_change_in_progress|resuming|migrating|sandbox_unavailable/.test(e.message);
170
+ if (!busy || Date.now() > deadline)
171
+ throw err;
172
+ await sleep(1_000);
173
+ }
174
+ }
175
+ }
176
+ /** Data-plane request; re-activates once on a stale token, a paused box, or a moved route. */
177
+ async data(method, path, init = {}) {
178
+ if (!this.token)
179
+ await this.activate();
180
+ const send = () => fetch(`${this.p.dataPlane}${path}`, {
181
+ method,
182
+ signal: init.signal,
183
+ headers: {
184
+ "X-Access-Token": this.token,
185
+ "X-Superserve-Sandbox-Id": this.nativeId,
186
+ ...(this.routingHint ? { "X-Superserve-Routing-Hint": this.routingHint } : {}),
187
+ ...(init.json !== undefined ? { "Content-Type": "application/json" } : {}),
188
+ ...(init.body !== undefined ? { "Content-Type": "application/octet-stream" } : {}),
189
+ },
190
+ body: init.json !== undefined ? JSON.stringify(init.json) : init.body,
191
+ });
192
+ let res = await send();
193
+ if (!res.ok) {
194
+ const text = await res.text();
195
+ const code = errorCode(text);
196
+ const resumable = res.status === 401 || res.status === 503 || (res.status === 404 && code === "sandbox_route_stale");
197
+ if (!resumable)
198
+ throw apiError(method, path, res.status, text);
199
+ await this.activate();
200
+ res = await send();
201
+ if (!res.ok)
202
+ throw apiError(method, path, res.status, await res.text());
203
+ }
204
+ return res;
205
+ }
206
+ async exec(cmd, opts = {}) {
207
+ const t0 = Date.now();
208
+ const secs = opts.timeoutSeconds ?? 60;
209
+ const ctl = new AbortController();
210
+ let clientTimedOut = false;
211
+ const timer = setTimeout(() => {
212
+ clientTimedOut = true;
213
+ ctl.abort();
214
+ }, secs * 1000 + CLIENT_GRACE_MS);
215
+ let stdout = "";
216
+ let stderr = "";
217
+ let final;
218
+ try {
219
+ const res = await this.data("POST", "/exec/stream", {
220
+ signal: ctl.signal,
221
+ json: {
222
+ command: cmd,
223
+ ...(opts.cwd ? { working_dir: opts.cwd } : {}),
224
+ ...(opts.env && Object.keys(opts.env).length ? { env: opts.env } : {}),
225
+ timeout_s: secs,
226
+ },
227
+ });
228
+ for await (const ev of readSSE(res)) {
229
+ let e;
230
+ try {
231
+ e = JSON.parse(ev.data);
232
+ }
233
+ catch {
234
+ continue;
235
+ }
236
+ if (e.stdout) {
237
+ stdout += e.stdout;
238
+ opts.onStdout?.(e.stdout);
239
+ }
240
+ if (e.stderr) {
241
+ stderr += e.stderr;
242
+ opts.onStderr?.(e.stderr);
243
+ }
244
+ if (e.finished || e.error) {
245
+ final = e;
246
+ break;
247
+ }
248
+ }
249
+ }
250
+ catch (err) {
251
+ if (clientTimedOut)
252
+ throw new ProviderError(ID, `command timed out after ${secs}s`);
253
+ throw err;
254
+ }
255
+ finally {
256
+ clearTimeout(timer);
257
+ ctl.abort();
258
+ }
259
+ const durationMs = Date.now() - t0;
260
+ if (!final)
261
+ throw new ProviderError(ID, "command stream ended without a finished event (connection dropped?)");
262
+ if (final.error) {
263
+ if (/time(d)?[ _-]?out|deadline/i.test(`${final.code ?? ""} ${final.error}`))
264
+ throw new ProviderError(ID, `command timed out after ${secs}s`);
265
+ throw new ProviderError(ID, `exec failed${final.code ? ` (${final.code})` : ""}: ${final.error}`);
266
+ }
267
+ const exitCode = final.exit_code ?? 0;
268
+ // The server kills the command at timeout_s and reports exit 124 (like coreutils `timeout`).
269
+ if (exitCode === TIMEOUT_EXIT && durationMs >= secs * 1000 - 250)
270
+ throw new ProviderError(ID, `command timed out after ${secs}s`);
271
+ return { exitCode, stdout, stderr, durationMs };
272
+ }
273
+ async writeFile(path, content) {
274
+ const res = await this.data("POST", `/files?path=${encodeURIComponent(path)}`, { body: toBytes(content) });
275
+ await res.arrayBuffer().catch(() => { });
276
+ }
277
+ async readFile(path) {
278
+ const res = await this.data("GET", `/files?path=${encodeURIComponent(path)}`);
279
+ return new Uint8Array(await res.arrayBuffer());
280
+ }
281
+ /** Pause (memory + disk kept) and wait until it lands, so connect() can resume right away. */
282
+ async pause() {
283
+ const { status } = await this.p.api("POST", `${this.path}/pause`);
284
+ if (status !== 202)
285
+ return;
286
+ const deadline = Date.now() + PAUSE_TIMEOUT_MS;
287
+ for (;;) {
288
+ const { body } = await this.p.api("GET", this.path);
289
+ if (body.status === "paused")
290
+ return;
291
+ if (body.status === "failed")
292
+ throw new ProviderError(ID, `sandbox ${this.nativeId} failed while pausing`);
293
+ if (Date.now() > deadline)
294
+ throw new ProviderError(ID, `sandbox ${this.nativeId} not paused after ${PAUSE_TIMEOUT_MS / 1000}s (${body.status ?? "unknown"})`);
295
+ await sleep(POLL_MS);
296
+ }
297
+ }
298
+ async kill() {
299
+ try {
300
+ await this.p.api("DELETE", this.path);
301
+ }
302
+ catch (err) {
303
+ // Already gone is fine (the SDK's kill() swallows 404 too).
304
+ if (err.status !== 404)
305
+ throw err;
306
+ }
307
+ this.token = undefined;
308
+ }
309
+ }
310
+ function errorCode(text) {
311
+ try {
312
+ return JSON.parse(text)?.error?.code;
313
+ }
314
+ catch {
315
+ return undefined;
316
+ }
317
+ }
318
+ function apiError(method, path, status, text) {
319
+ let detail = text.slice(0, 300);
320
+ try {
321
+ const j = JSON.parse(text);
322
+ const e = j?.error;
323
+ if (e && typeof e === "object")
324
+ detail = `${e.code ? `${e.code}: ` : ""}${e.message ?? ""}`.trim();
325
+ else if (typeof e === "string")
326
+ detail = e;
327
+ else if (j?.message)
328
+ detail = j.message;
329
+ }
330
+ catch {
331
+ /* plain-text body */
332
+ }
333
+ return new ProviderError(ID, `${method} ${path.split("?")[0]} -> ${status} ${detail}`, status);
334
+ }
335
+ function sleep(ms) {
336
+ return new Promise((r) => setTimeout(r, ms));
337
+ }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
3
3
  import { ProviderError } from "../types.js";
4
- import { optionalImport, shq, toBytes, wrapCommand } from "../util.js";
4
+ import { DEFAULT_EXEC_TIMEOUT_S, optionalImport, shq, timeoutError, toBytes, wrapCommand } from "../util.js";
5
5
  /** Tensorlake has no sandbox-level env, so spec.env lives in this file and every exec sources it. */
6
6
  const ENV_FILE = "/tmp/.sandboxrouter.env";
7
7
  /**
@@ -81,11 +81,11 @@ class TensorlakeBox {
81
81
  if (opts.onStdout || opts.onStderr)
82
82
  return this.execStreaming(cmd, opts);
83
83
  const t0 = Date.now();
84
- const limit = opts.timeoutSeconds ?? 60;
84
+ const seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
85
85
  try {
86
- const r = await this.sbx.run("/bin/sh", { args: ["-c", this.shellLine(cmd, opts)], timeout: limit });
86
+ const r = await this.sbx.run("/bin/sh", { args: ["-c", this.shellLine(cmd, opts)], timeout: seconds });
87
87
  if (r.timedOut)
88
- throw new ProviderError("tensorlake", `command timed out after ${limit}s`);
88
+ throw timeoutError("tensorlake", seconds);
89
89
  // A nonzero exit is a result; the SDK only throws for transport/API failures.
90
90
  return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, durationMs: Date.now() - t0 };
91
91
  }
@@ -96,24 +96,27 @@ class TensorlakeBox {
96
96
  /** Start the process, then follow its combined output stream until it exits. */
97
97
  async execStreaming(cmd, opts) {
98
98
  const t0 = Date.now();
99
- const limit = (opts.timeoutSeconds ?? 60) * 1000;
99
+ const seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
100
100
  const abort = new AbortController();
101
- const stdout = [];
102
- const stderr = [];
101
+ // Output arrives as lines without their line ending. Each is stored exactly as it is streamed
102
+ // to the callbacks, so the returned text equals what the callbacks saw.
103
+ let stdout = "";
104
+ let stderr = "";
103
105
  let pid;
104
- const timer = setTimeout(() => abort.abort(), limit);
106
+ const timer = setTimeout(() => abort.abort(), seconds * 1000);
105
107
  timer.unref();
106
108
  try {
107
109
  ({ pid } = await this.sbx.startProcess("/bin/sh", { args: ["-c", this.shellLine(cmd, opts)] }));
108
110
  try {
109
111
  for await (const ev of this.sbx.followOutput(pid, { signal: abort.signal })) {
112
+ const line = ev.line + "\n";
110
113
  if (ev.stream === "stderr") {
111
- stderr.push(ev.line);
112
- opts.onStderr?.(ev.line + "\n");
114
+ stderr += line;
115
+ opts.onStderr?.(line);
113
116
  }
114
117
  else {
115
- stdout.push(ev.line);
116
- opts.onStdout?.(ev.line + "\n");
118
+ stdout += line;
119
+ opts.onStdout?.(line);
117
120
  }
118
121
  }
119
122
  }
@@ -123,7 +126,7 @@ class TensorlakeBox {
123
126
  }
124
127
  if (abort.signal.aborted) {
125
128
  await this.sbx.killProcess(pid).catch(() => { });
126
- throw new ProviderError("tensorlake", `command timed out after ${limit / 1000}s`);
129
+ throw timeoutError("tensorlake", seconds);
127
130
  }
128
131
  // The stream can close a moment before the daemon records the exit.
129
132
  let info = await this.sbx.getProcess(pid);
@@ -132,7 +135,7 @@ class TensorlakeBox {
132
135
  info = await this.sbx.getProcess(pid);
133
136
  }
134
137
  const exitCode = info.exitCode ?? (info.signal != null ? -info.signal : -1);
135
- return { exitCode, stdout: stdout.join("\n"), stderr: stderr.join("\n"), durationMs: Date.now() - t0 };
138
+ return { exitCode, stdout, stderr, durationMs: Date.now() - t0 };
136
139
  }
137
140
  catch (err) {
138
141
  throw fail(err);
@@ -0,0 +1,30 @@
1
+ import { UPSTASH_SIZES, upstashSize } from "../catalog.js";
2
+ export { UPSTASH_SIZES, upstashSize };
3
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
4
+ export declare class UpstashProvider implements Provider {
5
+ private apiKey;
6
+ private baseUrl;
7
+ readonly id: "upstash";
8
+ constructor(apiKey?: string | undefined, baseUrl?: string);
9
+ configured(): boolean;
10
+ /** @internal One API call; JSON in and out. Non-2xx becomes a ProviderError with the status. */
11
+ request(method: string, path: string, body?: unknown, timeoutMs?: number, raw?: boolean): Promise<any>;
12
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
13
+ connect(nativeId: string): Promise<ProviderSandbox>;
14
+ }
15
+ type StreamEvent = {
16
+ type: "output";
17
+ data: string;
18
+ } | {
19
+ type: "exit";
20
+ exitCode: number;
21
+ } | {
22
+ type: "error";
23
+ message: string;
24
+ };
25
+ /**
26
+ * The exec-stream body as the official SDK reads it: raw output text, then a trailing SSE event
27
+ * `event: exit\ndata: {"exit_code":N,...}` (or `event: error\ndata: {"error":"..."}`).
28
+ * @internal exported for tests
29
+ */
30
+ export declare function parseExecStream(res: Response): AsyncGenerator<StreamEvent>;