@sandboxrouter/sdk 0.0.0-stage → 0.2.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 +293 -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 +105 -0
- package/dist/catalog.js +320 -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 +23 -0
- package/dist/index.js +23 -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/boat.d.ts +25 -0
- package/dist/providers/boat.js +258 -0
- package/dist/providers/cloudflare.d.ts +30 -0
- package/dist/providers/cloudflare.js +147 -0
- package/dist/providers/daytona.d.ts +13 -0
- package/dist/providers/daytona.js +121 -0
- package/dist/providers/docker.d.ts +17 -0
- package/dist/providers/docker.js +148 -0
- package/dist/providers/e2b.d.ts +10 -0
- package/dist/providers/e2b.js +69 -0
- package/dist/providers/exedev.d.ts +23 -0
- package/dist/providers/exedev.js +278 -0
- package/dist/providers/hopx.d.ts +15 -0
- package/dist/providers/hopx.js +168 -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/northflank.d.ts +21 -0
- package/dist/providers/northflank.js +261 -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 +199 -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 +87 -0
- package/dist/types.js +19 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.js +37 -0
- package/package.json +127 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
import { ProviderError } from "../types.js";
|
|
4
|
+
import { optionalImport, toBytes } from "../util.js";
|
|
5
|
+
/**
|
|
6
|
+
* Fly.io Sprites via `@fly/sprites`. A sprite is a persistent machine named by the caller, so the
|
|
7
|
+
* name is the native id. Sprites idle on their own and have no lifetime knob: kill() deletes it.
|
|
8
|
+
*/
|
|
9
|
+
export class SpritesProvider {
|
|
10
|
+
token;
|
|
11
|
+
id = "sprites";
|
|
12
|
+
client;
|
|
13
|
+
constructor(token = process.env.SPRITES_TOKEN) {
|
|
14
|
+
this.token = token;
|
|
15
|
+
}
|
|
16
|
+
configured() {
|
|
17
|
+
return !!this.token;
|
|
18
|
+
}
|
|
19
|
+
async sprites() {
|
|
20
|
+
if (!this.client) {
|
|
21
|
+
const { SpritesClient } = await optionalImport("@fly/sprites", "sprites");
|
|
22
|
+
this.client = new SpritesClient(this.token);
|
|
23
|
+
}
|
|
24
|
+
return this.client;
|
|
25
|
+
}
|
|
26
|
+
async create(spec) {
|
|
27
|
+
const client = await this.sprites();
|
|
28
|
+
const name = `sbr-${randomUUID().slice(0, 12)}`;
|
|
29
|
+
try {
|
|
30
|
+
const sprite = await client.createSprite(name, {
|
|
31
|
+
config: { cpus: spec.vcpu, ramMB: spec.memoryGb ? Math.round(spec.memoryGb * 1024) : undefined },
|
|
32
|
+
environment: spec.env,
|
|
33
|
+
});
|
|
34
|
+
return new SpritesBox(sprite);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw new ProviderError("sprites", err?.message ?? String(err), err?.statusCode);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async connect(nativeId) {
|
|
41
|
+
const client = await this.sprites();
|
|
42
|
+
try {
|
|
43
|
+
return new SpritesBox(await client.getSprite(nativeId));
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new ProviderError("sprites", err?.message ?? String(err), err?.statusCode);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class SpritesBox {
|
|
51
|
+
sprite;
|
|
52
|
+
nativeId;
|
|
53
|
+
constructor(sprite) {
|
|
54
|
+
this.sprite = sprite;
|
|
55
|
+
this.nativeId = sprite.name;
|
|
56
|
+
}
|
|
57
|
+
/** `sprite.exec` splits on whitespace without a shell, so spawn `sh -c` and stream its output. */
|
|
58
|
+
async exec(cmd, opts = {}) {
|
|
59
|
+
const t0 = Date.now();
|
|
60
|
+
const limit = (opts.timeoutSeconds ?? 60) * 1000;
|
|
61
|
+
const proc = this.sprite.spawn("sh", ["-c", cmd], { cwd: opts.cwd, env: opts.env });
|
|
62
|
+
const out = new StringDecoder("utf8");
|
|
63
|
+
const errd = new StringDecoder("utf8");
|
|
64
|
+
let stdout = "";
|
|
65
|
+
let stderr = "";
|
|
66
|
+
proc.stdout.on("data", (b) => {
|
|
67
|
+
const c = out.write(b);
|
|
68
|
+
stdout += c;
|
|
69
|
+
if (c)
|
|
70
|
+
opts.onStdout?.(c);
|
|
71
|
+
});
|
|
72
|
+
proc.stderr.on("data", (b) => {
|
|
73
|
+
const c = errd.write(b);
|
|
74
|
+
stderr += c;
|
|
75
|
+
if (c)
|
|
76
|
+
opts.onStderr?.(c);
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
proc.kill("SIGKILL");
|
|
82
|
+
proc.close();
|
|
83
|
+
reject(new ProviderError("sprites", `command timed out after ${limit / 1000}s`));
|
|
84
|
+
}, limit);
|
|
85
|
+
timer.unref();
|
|
86
|
+
proc.on("exit", (code) => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
resolve(code);
|
|
89
|
+
});
|
|
90
|
+
proc.on("error", (e) => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
proc.close();
|
|
93
|
+
reject(e);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const so = out.end();
|
|
97
|
+
const se = errd.end();
|
|
98
|
+
if (so)
|
|
99
|
+
opts.onStdout?.(so);
|
|
100
|
+
if (se)
|
|
101
|
+
opts.onStderr?.(se);
|
|
102
|
+
return { exitCode, stdout: stdout + so, stderr: stderr + se, durationMs: Date.now() - t0 };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof ProviderError)
|
|
106
|
+
throw err;
|
|
107
|
+
throw new ProviderError("sprites", err?.message ?? String(err));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async writeFile(path, content) {
|
|
111
|
+
try {
|
|
112
|
+
await this.sprite.filesystem().writeFile(path, Buffer.from(toBytes(content)));
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw new ProviderError("sprites", err?.message ?? String(err));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async readFile(path) {
|
|
119
|
+
try {
|
|
120
|
+
return new Uint8Array(await this.sprite.filesystem().readFile(path, null));
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw new ProviderError("sprites", err?.message ?? String(err));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Sprites are persistent; delete so the router's disposable sandboxes don't linger. */
|
|
127
|
+
async kill() {
|
|
128
|
+
try {
|
|
129
|
+
await this.sprite.delete();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new ProviderError("sprites", err?.message ?? String(err), err?.statusCode);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tensorlake sandboxes via `@tensorlakeai/tensorlake`. Auth: TENSORLAKE_API_KEY.
|
|
4
|
+
* Set image.tensorlake to start from your own sandbox image; spec.gpu maps to a GPU model.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TensorlakeProvider implements Provider {
|
|
7
|
+
private apiKey;
|
|
8
|
+
readonly id: "tensorlake";
|
|
9
|
+
constructor(apiKey?: string | undefined);
|
|
10
|
+
configured(): boolean;
|
|
11
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
12
|
+
/** Reattach; a suspended sandbox is resumed. */
|
|
13
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
|
|
3
|
+
import { ProviderError } from "../types.js";
|
|
4
|
+
import { optionalImport, shq, toBytes, wrapCommand } from "../util.js";
|
|
5
|
+
/** Tensorlake has no sandbox-level env, so spec.env lives in this file and every exec sources it. */
|
|
6
|
+
const ENV_FILE = "/tmp/.sandboxrouter.env";
|
|
7
|
+
/**
|
|
8
|
+
* Tensorlake sandboxes via `@tensorlakeai/tensorlake`. Auth: TENSORLAKE_API_KEY.
|
|
9
|
+
* Set image.tensorlake to start from your own sandbox image; spec.gpu maps to a GPU model.
|
|
10
|
+
*/
|
|
11
|
+
export class TensorlakeProvider {
|
|
12
|
+
apiKey;
|
|
13
|
+
id = "tensorlake";
|
|
14
|
+
constructor(apiKey = process.env.TENSORLAKE_API_KEY) {
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
}
|
|
17
|
+
configured() {
|
|
18
|
+
return !!this.apiKey;
|
|
19
|
+
}
|
|
20
|
+
async create(spec) {
|
|
21
|
+
const { Sandbox } = await optionalImport("@tensorlakeai/tensorlake", "tensorlake");
|
|
22
|
+
const image = spec.image?.tensorlake;
|
|
23
|
+
let sbx;
|
|
24
|
+
try {
|
|
25
|
+
sbx = await Sandbox.create({
|
|
26
|
+
apiKey: this.apiKey,
|
|
27
|
+
image,
|
|
28
|
+
cpus: spec.vcpu ?? DEFAULT_VCPU,
|
|
29
|
+
memoryMb: Math.round((spec.memoryGb ?? DEFAULT_MEMORY_GB) * 1024),
|
|
30
|
+
timeoutSecs: Math.round((spec.timeoutMinutes ?? 30) * 60),
|
|
31
|
+
...(spec.gpu ? { gpuModel: spec.gpu } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw fail(err);
|
|
36
|
+
}
|
|
37
|
+
const box = new TensorlakeBox(sbx);
|
|
38
|
+
if (spec.env && Object.keys(spec.env).length > 0) {
|
|
39
|
+
try {
|
|
40
|
+
await box.writeFile(ENV_FILE, wrapCommand(":", { env: spec.env }) + "\n");
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
await sbx.terminate().catch(() => { });
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return box;
|
|
48
|
+
}
|
|
49
|
+
/** Reattach; a suspended sandbox is resumed. */
|
|
50
|
+
async connect(nativeId) {
|
|
51
|
+
const { Sandbox } = await optionalImport("@tensorlakeai/tensorlake", "tensorlake");
|
|
52
|
+
try {
|
|
53
|
+
const sbx = await Sandbox.connect({ sandboxId: nativeId, apiKey: this.apiKey });
|
|
54
|
+
let status = await sbx.status();
|
|
55
|
+
// resume() is rejected while a suspend is still in flight.
|
|
56
|
+
for (let i = 0; status === "suspending" && i < 300; i++) {
|
|
57
|
+
await sleep(1000);
|
|
58
|
+
status = await sbx.status();
|
|
59
|
+
}
|
|
60
|
+
if (status === "suspended")
|
|
61
|
+
await sbx.resume();
|
|
62
|
+
return new TensorlakeBox(sbx);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw fail(err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class TensorlakeBox {
|
|
70
|
+
sbx;
|
|
71
|
+
nativeId;
|
|
72
|
+
constructor(sbx) {
|
|
73
|
+
this.sbx = sbx;
|
|
74
|
+
this.nativeId = sbx.sandboxId;
|
|
75
|
+
}
|
|
76
|
+
/** `sh -c` so cmd is a shell line; spec.env is sourced first so per-call env wins. */
|
|
77
|
+
shellLine(cmd, opts) {
|
|
78
|
+
return `[ -f ${shq(ENV_FILE)} ] && . ${shq(ENV_FILE)}; ${wrapCommand(cmd, { cwd: opts.cwd, env: opts.env })}`;
|
|
79
|
+
}
|
|
80
|
+
async exec(cmd, opts = {}) {
|
|
81
|
+
if (opts.onStdout || opts.onStderr)
|
|
82
|
+
return this.execStreaming(cmd, opts);
|
|
83
|
+
const t0 = Date.now();
|
|
84
|
+
const limit = opts.timeoutSeconds ?? 60;
|
|
85
|
+
try {
|
|
86
|
+
const r = await this.sbx.run("/bin/sh", { args: ["-c", this.shellLine(cmd, opts)], timeout: limit });
|
|
87
|
+
if (r.timedOut)
|
|
88
|
+
throw new ProviderError("tensorlake", `command timed out after ${limit}s`);
|
|
89
|
+
// A nonzero exit is a result; the SDK only throws for transport/API failures.
|
|
90
|
+
return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, durationMs: Date.now() - t0 };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw fail(err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Start the process, then follow its combined output stream until it exits. */
|
|
97
|
+
async execStreaming(cmd, opts) {
|
|
98
|
+
const t0 = Date.now();
|
|
99
|
+
const limit = (opts.timeoutSeconds ?? 60) * 1000;
|
|
100
|
+
const abort = new AbortController();
|
|
101
|
+
const stdout = [];
|
|
102
|
+
const stderr = [];
|
|
103
|
+
let pid;
|
|
104
|
+
const timer = setTimeout(() => abort.abort(), limit);
|
|
105
|
+
timer.unref();
|
|
106
|
+
try {
|
|
107
|
+
({ pid } = await this.sbx.startProcess("/bin/sh", { args: ["-c", this.shellLine(cmd, opts)] }));
|
|
108
|
+
try {
|
|
109
|
+
for await (const ev of this.sbx.followOutput(pid, { signal: abort.signal })) {
|
|
110
|
+
if (ev.stream === "stderr") {
|
|
111
|
+
stderr.push(ev.line);
|
|
112
|
+
opts.onStderr?.(ev.line + "\n");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
stdout.push(ev.line);
|
|
116
|
+
opts.onStdout?.(ev.line + "\n");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (!abort.signal.aborted)
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
if (abort.signal.aborted) {
|
|
125
|
+
await this.sbx.killProcess(pid).catch(() => { });
|
|
126
|
+
throw new ProviderError("tensorlake", `command timed out after ${limit / 1000}s`);
|
|
127
|
+
}
|
|
128
|
+
// The stream can close a moment before the daemon records the exit.
|
|
129
|
+
let info = await this.sbx.getProcess(pid);
|
|
130
|
+
for (let i = 0; info.status === "running" && i < 20; i++) {
|
|
131
|
+
await sleep(250);
|
|
132
|
+
info = await this.sbx.getProcess(pid);
|
|
133
|
+
}
|
|
134
|
+
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 };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw fail(err);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async writeFile(path, content) {
|
|
145
|
+
try {
|
|
146
|
+
await this.sbx.writeFile(path, toBytes(content));
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
throw fail(err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async readFile(path) {
|
|
153
|
+
try {
|
|
154
|
+
return new Uint8Array(await this.sbx.readFile(path));
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
throw fail(err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Suspend, keeping memory and disk. Only named sandboxes can suspend, so an unnamed one is
|
|
162
|
+
* named first. router.get() resumes it.
|
|
163
|
+
*/
|
|
164
|
+
async pause() {
|
|
165
|
+
try {
|
|
166
|
+
const info = await this.sbx.info();
|
|
167
|
+
if (!info.name)
|
|
168
|
+
await this.sbx.update({ name: `sbr-${randomUUID()}` });
|
|
169
|
+
await this.sbx.suspend();
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
throw fail(err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async kill() {
|
|
176
|
+
try {
|
|
177
|
+
await this.sbx.terminate();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
throw fail(err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function fail(err) {
|
|
185
|
+
if (err instanceof ProviderError)
|
|
186
|
+
return err;
|
|
187
|
+
const status = typeof err?.statusCode === "number" ? err.statusCode : undefined;
|
|
188
|
+
return new ProviderError("tensorlake", err?.message ?? String(err), status);
|
|
189
|
+
}
|
|
190
|
+
function sleep(ms) {
|
|
191
|
+
return new Promise((r) => setTimeout(r, ms).unref());
|
|
192
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Vercel Sandbox via `@vercel/sandbox`. Auth: VERCEL_OIDC_TOKEN (from `vercel env pull`), or
|
|
4
|
+
* VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID. Memory is fixed at 2 GB per vCPU.
|
|
5
|
+
*/
|
|
6
|
+
export declare class VercelProvider implements Provider {
|
|
7
|
+
readonly id: "vercel";
|
|
8
|
+
configured(): boolean;
|
|
9
|
+
private creds;
|
|
10
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
11
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
|
|
2
|
+
import { ProviderError } from "../types.js";
|
|
3
|
+
import { optionalImport, toBytes, wrapCommand } from "../util.js";
|
|
4
|
+
/**
|
|
5
|
+
* Vercel Sandbox via `@vercel/sandbox`. Auth: VERCEL_OIDC_TOKEN (from `vercel env pull`), or
|
|
6
|
+
* VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID. Memory is fixed at 2 GB per vCPU.
|
|
7
|
+
*/
|
|
8
|
+
export class VercelProvider {
|
|
9
|
+
id = "vercel";
|
|
10
|
+
configured() {
|
|
11
|
+
const e = process.env;
|
|
12
|
+
return !!e.VERCEL_OIDC_TOKEN || (!!e.VERCEL_TOKEN && !!e.VERCEL_TEAM_ID && !!e.VERCEL_PROJECT_ID);
|
|
13
|
+
}
|
|
14
|
+
creds() {
|
|
15
|
+
const e = process.env;
|
|
16
|
+
return e.VERCEL_TOKEN && e.VERCEL_TEAM_ID && e.VERCEL_PROJECT_ID
|
|
17
|
+
? { token: e.VERCEL_TOKEN, teamId: e.VERCEL_TEAM_ID, projectId: e.VERCEL_PROJECT_ID }
|
|
18
|
+
: {};
|
|
19
|
+
}
|
|
20
|
+
async create(spec) {
|
|
21
|
+
const { Sandbox } = await optionalImport("@vercel/sandbox", "vercel");
|
|
22
|
+
const vcpus = Math.min(8, Math.max(spec.vcpu ?? DEFAULT_VCPU, Math.ceil((spec.memoryGb ?? DEFAULT_MEMORY_GB) / 2)));
|
|
23
|
+
const sbx = await Sandbox.create({
|
|
24
|
+
...this.creds(),
|
|
25
|
+
runtime: spec.image?.vercel ?? "node22",
|
|
26
|
+
timeout: (spec.timeoutMinutes ?? 30) * 60_000,
|
|
27
|
+
resources: { vcpus },
|
|
28
|
+
env: spec.env,
|
|
29
|
+
tags: spec.metadata,
|
|
30
|
+
});
|
|
31
|
+
return new VercelBox(sbx);
|
|
32
|
+
}
|
|
33
|
+
async connect(nativeId) {
|
|
34
|
+
const { Sandbox } = await optionalImport("@vercel/sandbox", "vercel");
|
|
35
|
+
return new VercelBox(await Sandbox.get({ ...this.creds(), name: nativeId }));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class VercelBox {
|
|
39
|
+
sbx;
|
|
40
|
+
nativeId;
|
|
41
|
+
constructor(sbx) {
|
|
42
|
+
this.sbx = sbx;
|
|
43
|
+
this.nativeId = sbx.name;
|
|
44
|
+
}
|
|
45
|
+
async exec(cmd, opts = {}) {
|
|
46
|
+
const t0 = Date.now();
|
|
47
|
+
const signal = AbortSignal.timeout((opts.timeoutSeconds ?? 60) * 1000);
|
|
48
|
+
try {
|
|
49
|
+
const params = { cmd: "sh", args: ["-c", wrapCommand(cmd)], cwd: opts.cwd, env: opts.env, signal };
|
|
50
|
+
if (opts.onStdout || opts.onStderr) {
|
|
51
|
+
const running = await this.sbx.runCommand({ ...params, detached: true });
|
|
52
|
+
let stdout = "";
|
|
53
|
+
let stderr = "";
|
|
54
|
+
for await (const line of running.logs({ signal })) {
|
|
55
|
+
if (line.stream === "stdout") {
|
|
56
|
+
stdout += line.data;
|
|
57
|
+
opts.onStdout?.(line.data);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
stderr += line.data;
|
|
61
|
+
opts.onStderr?.(line.data);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const done = await running.wait({ signal });
|
|
65
|
+
return { exitCode: done.exitCode, stdout, stderr, durationMs: Date.now() - t0 };
|
|
66
|
+
}
|
|
67
|
+
const r = await this.sbx.runCommand(params);
|
|
68
|
+
const [stdout, stderr] = await Promise.all([r.stdout(), r.stderr()]);
|
|
69
|
+
return { exitCode: r.exitCode, stdout, stderr, durationMs: Date.now() - t0 };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw new ProviderError("vercel", err?.message ?? String(err));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async writeFile(path, content) {
|
|
76
|
+
await this.sbx.writeFiles([{ path, content: Buffer.from(toBytes(content)) }]);
|
|
77
|
+
}
|
|
78
|
+
async readFile(path) {
|
|
79
|
+
const buf = await this.sbx.readFileToBuffer({ path });
|
|
80
|
+
if (!buf)
|
|
81
|
+
throw new ProviderError("vercel", `no such file: ${path}`);
|
|
82
|
+
return new Uint8Array(buf);
|
|
83
|
+
}
|
|
84
|
+
async kill() {
|
|
85
|
+
await this.sbx.stop();
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RoutedComputer, type ComputerProvider, type ComputerSpec } from "./computers.js";
|
|
2
|
+
import { type Attempt, type ExecOptions, type ExecResult, type Provider, type ProviderId, type ProviderSandbox, type RouteOptions, type SandboxSpec } from "./types.js";
|
|
3
|
+
export interface Candidate {
|
|
4
|
+
provider: ProviderId;
|
|
5
|
+
estimatedHourlyUsd: number;
|
|
6
|
+
medianCreateMs?: number;
|
|
7
|
+
/** Why this provider won't be tried, if it won't. */
|
|
8
|
+
skip?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RouterOptions {
|
|
11
|
+
/** Replace the built-in adapters (tests, custom providers). */
|
|
12
|
+
providers?: Provider[];
|
|
13
|
+
/** Path of the bench results file used by strategy "fastest". */
|
|
14
|
+
benchFile?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function defaultProviders(): Provider[];
|
|
17
|
+
export declare class Router {
|
|
18
|
+
private providers;
|
|
19
|
+
private benchFile?;
|
|
20
|
+
constructor(opts?: RouterOptions);
|
|
21
|
+
/** Registered and has credentials. False for providers this router doesn't know. */
|
|
22
|
+
isConfigured(id: ProviderId): boolean;
|
|
23
|
+
/** Catalog ids this router has an adapter for, in catalog order. */
|
|
24
|
+
registered(): ProviderId[];
|
|
25
|
+
provider(id: ProviderId): Provider;
|
|
26
|
+
/** The ordered list of providers a create would try, with the reason any are skipped. */
|
|
27
|
+
plan(spec?: SandboxSpec, route?: RouteOptions): Candidate[];
|
|
28
|
+
/** Start a sandbox on the best available provider, falling back down the list on failure. */
|
|
29
|
+
create(spec?: SandboxSpec, route?: RouteOptions): Promise<RoutedSandbox>;
|
|
30
|
+
/** Providers that also sell computers (desktops/phones). */
|
|
31
|
+
computerProviders(): ComputerProvider[];
|
|
32
|
+
/** Get or create a desktop/phone. With `externalUserId`, the same id returns the same computer. */
|
|
33
|
+
computer(spec?: ComputerSpec, opts?: {
|
|
34
|
+
provider?: ProviderId | ProviderId[];
|
|
35
|
+
}): Promise<RoutedComputer>;
|
|
36
|
+
getComputer(id: string): Promise<RoutedComputer>;
|
|
37
|
+
/** Reattach to a sandbox by its routed id (`provider:nativeId`). */
|
|
38
|
+
get(id: string): Promise<RoutedSandbox>;
|
|
39
|
+
}
|
|
40
|
+
export declare class RoutedSandbox {
|
|
41
|
+
readonly provider: ProviderId;
|
|
42
|
+
private box;
|
|
43
|
+
/** Every create attempt, including failed fallbacks. */
|
|
44
|
+
readonly attempts: Attempt[];
|
|
45
|
+
readonly estimatedHourlyUsd: number;
|
|
46
|
+
/** Stable id across processes: `provider:nativeId`. */
|
|
47
|
+
readonly id: string;
|
|
48
|
+
constructor(provider: ProviderId, box: ProviderSandbox,
|
|
49
|
+
/** Every create attempt, including failed fallbacks. */
|
|
50
|
+
attempts: Attempt[], estimatedHourlyUsd: number);
|
|
51
|
+
get nativeId(): string;
|
|
52
|
+
exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
53
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
54
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
55
|
+
readText(path: string): Promise<string>;
|
|
56
|
+
kill(): Promise<void>;
|
|
57
|
+
/** Pause, keeping the disk. Resume with router.get(id) or, on Maritime, any call. */
|
|
58
|
+
pause(): Promise<void>;
|
|
59
|
+
get canPause(): boolean;
|
|
60
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
export declare function parseId(id: string): {
|
|
63
|
+
provider: ProviderId;
|
|
64
|
+
nativeId: string;
|
|
65
|
+
};
|