@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.
- package/README.md +229 -50
- package/dist/bench.js +4 -4
- package/dist/catalog.d.ts +159 -0
- package/dist/catalog.js +450 -15
- package/dist/check.d.ts +28 -0
- package/dist/check.js +185 -0
- package/dist/cli.js +171 -12
- package/dist/computers.d.ts +55 -13
- package/dist/computers.js +132 -6
- package/dist/conformance.d.ts +46 -0
- package/dist/conformance.js +65 -0
- package/dist/cost.d.ts +67 -0
- package/dist/cost.js +153 -0
- package/dist/gc.d.ts +29 -0
- package/dist/gc.js +54 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +28 -2
- package/dist/leaderboard.d.ts +6 -0
- package/dist/leaderboard.js +5 -4
- package/dist/ledger.d.ts +38 -0
- package/dist/ledger.js +89 -0
- package/dist/providers/agent37.d.ts +40 -0
- package/dist/providers/agent37.js +224 -0
- package/dist/providers/agentcore.d.ts +71 -0
- package/dist/providers/agentcore.js +446 -0
- package/dist/providers/alibaba.d.ts +18 -0
- package/dist/providers/alibaba.js +106 -0
- package/dist/providers/azure.d.ts +43 -0
- package/dist/providers/azure.js +330 -0
- package/dist/providers/beam.d.ts +11 -0
- package/dist/providers/beam.js +179 -0
- package/dist/providers/blaxel.d.ts +3 -1
- package/dist/providers/blaxel.js +32 -22
- package/dist/providers/boat.js +8 -3
- package/dist/providers/cloudflare.js +8 -3
- package/dist/providers/codesandbox.d.ts +59 -0
- package/dist/providers/codesandbox.js +287 -0
- package/dist/providers/daytona.d.ts +25 -4
- package/dist/providers/daytona.js +449 -28
- package/dist/providers/dedalus.d.ts +12 -0
- package/dist/providers/dedalus.js +244 -0
- package/dist/providers/deno.d.ts +50 -0
- package/dist/providers/deno.js +170 -0
- package/dist/providers/docker.js +11 -5
- package/dist/providers/e2b.d.ts +70 -3
- package/dist/providers/e2b.js +376 -8
- package/dist/providers/exedev.js +17 -5
- package/dist/providers/freestyle.d.ts +49 -0
- package/dist/providers/freestyle.js +261 -0
- package/dist/providers/hopx.js +27 -18
- package/dist/providers/instabox.d.ts +76 -0
- package/dist/providers/instabox.js +360 -0
- package/dist/providers/koyeb.d.ts +71 -0
- package/dist/providers/koyeb.js +379 -0
- package/dist/providers/lambda.d.ts +51 -0
- package/dist/providers/lambda.js +467 -0
- package/dist/providers/langsmith.d.ts +16 -0
- package/dist/providers/langsmith.js +149 -0
- package/dist/providers/limrun.d.ts +77 -0
- package/dist/providers/limrun.js +626 -0
- package/dist/providers/maritime.d.ts +5 -1
- package/dist/providers/maritime.js +39 -8
- package/dist/providers/mobilerun.d.ts +44 -0
- package/dist/providers/mobilerun.js +417 -0
- package/dist/providers/modal.js +12 -4
- package/dist/providers/morph.js +12 -4
- package/dist/providers/northflank.js +11 -13
- package/dist/providers/novita.d.ts +12 -0
- package/dist/providers/novita.js +81 -0
- package/dist/providers/openscale.d.ts +17 -0
- package/dist/providers/openscale.js +87 -0
- package/dist/providers/orgo.d.ts +45 -0
- package/dist/providers/orgo.js +414 -0
- package/dist/providers/sail.d.ts +32 -0
- package/dist/providers/sail.js +372 -0
- package/dist/providers/superserve.d.ts +25 -0
- package/dist/providers/superserve.js +337 -0
- package/dist/providers/tensorlake.js +17 -14
- package/dist/providers/upstash.d.ts +30 -0
- package/dist/providers/upstash.js +258 -0
- package/dist/providers/vercel.js +14 -3
- package/dist/providers/workspaces.d.ts +60 -0
- package/dist/providers/workspaces.js +662 -0
- package/dist/router.d.ts +41 -5
- package/dist/router.js +179 -23
- package/dist/server.d.ts +1 -1
- package/dist/server.js +22 -3
- package/dist/types.d.ts +13 -5
- package/dist/util.d.ts +28 -1
- package/dist/util.js +56 -7
- package/package.json +81 -5
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readSSE } from "../sse.js";
|
|
3
|
+
import { ProviderError } from "../types.js";
|
|
4
|
+
import { toBytes } from "../util.js";
|
|
5
|
+
/**
|
|
6
|
+
* InstaBox (instabox.sh, by InsForge, YC P26) over plain REST. Full Linux (Debian 13, systemd,
|
|
7
|
+
* sudo) Firecracker microVMs with a persistent /data disk; billed per second while running.
|
|
8
|
+
*
|
|
9
|
+
* https://api.instabox.sh, X-API-Key: <INSTABOX_API_KEY> (ibk_...; Authorization: Bearer also works):
|
|
10
|
+
* POST /v1/machines {vcpus, memMiB, templateId|snapshotId, env, metadata, deleteAfterSeconds}
|
|
11
|
+
* -> 202 Operation {id, machineId, type, state, phase, error}
|
|
12
|
+
* GET /v1/operations/:id?wait=25s long poll; state queued | running | succeeded | failed
|
|
13
|
+
* GET /v1/machines/:id -> Machine {id, state, runtime:{ready}, operationId, lastError}
|
|
14
|
+
* POST /v1/machines/:id/suspend {} pause: memory and disk kept, compute billing stops
|
|
15
|
+
* POST /v1/machines/:id/start {} starts a stopped machine, or resumes a paused one
|
|
16
|
+
* DELETE /v1/machines/:id needs X-Confirm-Delete: <id>
|
|
17
|
+
* POST /v1/machines/:id/exec {cmd, cwd, envs, timeoutSeconds<=600} runs `bash -lc cmd`, blocks;
|
|
18
|
+
* -> ExecResult {stdout, stderr, exitCode, timedOut, truncated} (1 MiB per stream), or with
|
|
19
|
+
* Accept: text/event-stream: `stdout`/`stderr` events {data}, then `exit` (ExecResult) or `error`
|
|
20
|
+
* PUT /v1/machines/:id/files?path=&parents=true raw body, any bytes, up to 5 GiB
|
|
21
|
+
* GET /v1/machines/:id/files/content?path= raw bytes
|
|
22
|
+
* Create, start, suspend and delete need an Idempotency-Key header and answer an Operation.
|
|
23
|
+
* Machine state: creating, starting, running, stopping, stopped, suspending, suspended, resuming,
|
|
24
|
+
* deleting, deleted, error. Errors are {error:{code, message, retryable, retryAfterSeconds}}.
|
|
25
|
+
* Sizes are fixed at 1 vCPU : 2 GiB (Pro 2/4, 4/8, 8/16; Builder to 16/32; Team to 32/64; Free 4/8
|
|
26
|
+
* only). There is no lifetime timeout: deleteAfterSeconds (60-86400) deletes the machine in any state.
|
|
27
|
+
* An idle machine pauses itself after 30 minutes and wakes on the next exec or file call.
|
|
28
|
+
* Docs: https://instabox.sh/docs/api.md, https://instabox.sh/docs/machines.md,
|
|
29
|
+
* https://instabox.sh/docs/idle-suspend.md, https://instabox.sh/docs/limits.md,
|
|
30
|
+
* https://instabox.sh/docs/pricing.md, https://api.instabox.sh/v1/openapi.json
|
|
31
|
+
*/
|
|
32
|
+
const ID = "instabox";
|
|
33
|
+
const READY_TIMEOUT_MS = 5 * 60_000;
|
|
34
|
+
const POLL_MS = 250;
|
|
35
|
+
const MAX_EXEC_SECONDS = 600;
|
|
36
|
+
const HOME = "/data/home";
|
|
37
|
+
/** Sizes on api.instabox.sh (vCPUs / GiB); which ones an org may use depends on its plan. */
|
|
38
|
+
export const INSTABOX_SIZES = [
|
|
39
|
+
{ vcpu: 2, memoryGb: 4 },
|
|
40
|
+
{ vcpu: 4, memoryGb: 8 },
|
|
41
|
+
{ vcpu: 8, memoryGb: 16 },
|
|
42
|
+
{ vcpu: 16, memoryGb: 32 },
|
|
43
|
+
{ vcpu: 32, memoryGb: 64 },
|
|
44
|
+
];
|
|
45
|
+
/** The smallest size with at least the requested vCPUs and memory, or undefined if none fits. */
|
|
46
|
+
export function instaboxSize(vcpu, memoryGb) {
|
|
47
|
+
return INSTABOX_SIZES.find((s) => s.vcpu >= vcpu && s.memoryGb >= memoryGb);
|
|
48
|
+
}
|
|
49
|
+
/** The default size, and the only one on the free plan. */
|
|
50
|
+
const DEFAULT_SIZE = INSTABOX_SIZES[1];
|
|
51
|
+
class InstaboxError extends ProviderError {
|
|
52
|
+
code;
|
|
53
|
+
retryable;
|
|
54
|
+
retryAfterSeconds;
|
|
55
|
+
constructor(message, status, code, retryable = false, retryAfterSeconds) {
|
|
56
|
+
super(ID, message, status);
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.retryable = retryable;
|
|
59
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class InstaboxProvider {
|
|
63
|
+
apiKey;
|
|
64
|
+
baseUrl;
|
|
65
|
+
id = ID;
|
|
66
|
+
constructor(apiKey = process.env.INSTABOX_API_KEY, baseUrl = process.env.INSTABOX_API_URL ?? "https://api.instabox.sh") {
|
|
67
|
+
this.apiKey = apiKey;
|
|
68
|
+
this.baseUrl = baseUrl;
|
|
69
|
+
}
|
|
70
|
+
configured() {
|
|
71
|
+
return !!this.apiKey;
|
|
72
|
+
}
|
|
73
|
+
/** @internal Send a request; non-2xx answers throw InstaboxError. */
|
|
74
|
+
async request(method, path, init = {}) {
|
|
75
|
+
if (!this.apiKey)
|
|
76
|
+
throw new ProviderError(ID, "INSTABOX_API_KEY is not set");
|
|
77
|
+
const headers = { "X-API-Key": this.apiKey, ...init.headers };
|
|
78
|
+
if (init.idempotent)
|
|
79
|
+
headers["Idempotency-Key"] = randomUUID();
|
|
80
|
+
let body;
|
|
81
|
+
if (init.json !== undefined) {
|
|
82
|
+
headers["Content-Type"] = "application/json";
|
|
83
|
+
body = JSON.stringify(init.json);
|
|
84
|
+
}
|
|
85
|
+
else if (init.body) {
|
|
86
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
87
|
+
body = init.body;
|
|
88
|
+
}
|
|
89
|
+
const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body, signal: AbortSignal.timeout(init.timeoutMs ?? 120_000) });
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const text = await res.text();
|
|
92
|
+
let detail = text.slice(0, 300);
|
|
93
|
+
let err;
|
|
94
|
+
try {
|
|
95
|
+
err = JSON.parse(text)?.error;
|
|
96
|
+
if (err?.code)
|
|
97
|
+
detail = `${err.code}: ${err.message ?? ""}`;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* plain-text body */
|
|
101
|
+
}
|
|
102
|
+
const retryAfter = err?.retryAfterSeconds ?? (Number(res.headers.get("retry-after")) || undefined);
|
|
103
|
+
throw new InstaboxError(`${method} ${path.split("?")[0]} -> ${res.status} ${detail}`, res.status, err?.code, !!err?.retryable, retryAfter);
|
|
104
|
+
}
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
/** @internal */
|
|
108
|
+
async json(method, path, json, opts = {}) {
|
|
109
|
+
const text = await (await this.request(method, path, { json, ...opts })).text();
|
|
110
|
+
return (text ? JSON.parse(text) : {});
|
|
111
|
+
}
|
|
112
|
+
/** @internal Follow an operation until it succeeds; a failed one throws. */
|
|
113
|
+
async waitOperation(op, what) {
|
|
114
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
115
|
+
while (op.state !== "succeeded" && op.state !== "failed") {
|
|
116
|
+
if (Date.now() > deadline)
|
|
117
|
+
throw new ProviderError(ID, `${what} not done after ${READY_TIMEOUT_MS / 1000}s (${op.phase || op.state || "unknown"})`);
|
|
118
|
+
if (!op.id)
|
|
119
|
+
throw new ProviderError(ID, `${what} returned no operation id`);
|
|
120
|
+
// Long poll: answers as soon as the state or phase changes, or after 25 s.
|
|
121
|
+
op = await this.json("GET", `/v1/operations/${encodeURIComponent(op.id)}?wait=25s`);
|
|
122
|
+
if (op.state !== "succeeded" && op.state !== "failed")
|
|
123
|
+
await sleep(POLL_MS);
|
|
124
|
+
}
|
|
125
|
+
if (op.state === "failed") {
|
|
126
|
+
const e = op.error;
|
|
127
|
+
throw new InstaboxError(`${what} failed: ${e?.code ? `${e.code}: ` : ""}${e?.message ?? "no reason given"}`, e?.code === "no_capacity" ? 503 : undefined, e?.code, !!e?.retryable);
|
|
128
|
+
}
|
|
129
|
+
return op;
|
|
130
|
+
}
|
|
131
|
+
/** @internal Wait until the machine runs and takes commands, starting or resuming it when it is stopped or paused. */
|
|
132
|
+
async ensureRunning(id) {
|
|
133
|
+
const path = `/v1/machines/${encodeURIComponent(id)}`;
|
|
134
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
135
|
+
for (;;) {
|
|
136
|
+
const m = await this.json("GET", path);
|
|
137
|
+
const state = m.state ?? "";
|
|
138
|
+
if (state === "running" && m.runtime?.ready)
|
|
139
|
+
return;
|
|
140
|
+
if (state === "deleting" || state === "deleted")
|
|
141
|
+
throw new InstaboxError(`machine ${id} is ${state}`, 404, "not_found");
|
|
142
|
+
if (state === "error")
|
|
143
|
+
throw new InstaboxError(`machine ${id} is in error${m.lastError?.message ? `: ${m.lastError.message}` : ""}`, undefined, m.lastError?.code);
|
|
144
|
+
if (Date.now() > deadline)
|
|
145
|
+
throw new ProviderError(ID, `machine ${id} not ready after ${READY_TIMEOUT_MS / 1000}s (${state || "unknown"})`);
|
|
146
|
+
if (state === "stopped" || state === "suspended") {
|
|
147
|
+
try {
|
|
148
|
+
await this.waitOperation(await this.json("POST", `${path}/start`, {}, { idempotent: true }), `start of ${id}`);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
// Another operation (an automatic resume, say) got there first: look again.
|
|
152
|
+
if (!(err instanceof InstaboxError && err.code === "operation_conflict" && err.retryable))
|
|
153
|
+
throw err;
|
|
154
|
+
await sleep(1000);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (m.operationId) {
|
|
159
|
+
// Creating, starting, resuming, stopping or pausing: follow that operation, then look again.
|
|
160
|
+
await this.waitOperation({ id: m.operationId, state: "running" }, `operation on ${id}`).catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await sleep(POLL_MS);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async createMachine(body) {
|
|
168
|
+
const op = await this.json("POST", "/v1/machines", body, { idempotent: true });
|
|
169
|
+
const id = op.machineId;
|
|
170
|
+
if (!id)
|
|
171
|
+
throw new ProviderError(ID, "create returned no machineId");
|
|
172
|
+
try {
|
|
173
|
+
await this.waitOperation(op, `create of ${id}`);
|
|
174
|
+
// A machine from a snapshot with storage is created stopped; this starts it.
|
|
175
|
+
await this.ensureRunning(id);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
await new InstaboxBox(this, id).kill().catch(() => { });
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
return id;
|
|
182
|
+
}
|
|
183
|
+
async create(spec) {
|
|
184
|
+
const vcpu = spec.vcpu ?? 2;
|
|
185
|
+
const memoryGb = spec.memoryGb ?? 4;
|
|
186
|
+
const size = instaboxSize(vcpu, memoryGb);
|
|
187
|
+
if (!size)
|
|
188
|
+
throw new ProviderError(ID, `no InstaBox size has ${vcpu} vCPU / ${memoryGb} GB (largest: 32 vCPU / 64 GiB, Team plan)`);
|
|
189
|
+
const body = {
|
|
190
|
+
vcpus: size.vcpu,
|
|
191
|
+
memMiB: size.memoryGb * 1024,
|
|
192
|
+
// There is no lifetime timeout; this deletes the machine (in any state) so a leaked one stops billing.
|
|
193
|
+
deleteAfterSeconds: Math.min(86_400, Math.max(60, Math.round((spec.timeoutMinutes ?? 30) * 60))),
|
|
194
|
+
};
|
|
195
|
+
const image = spec.image?.instabox;
|
|
196
|
+
if (image) {
|
|
197
|
+
if (image.startsWith("snapshot:"))
|
|
198
|
+
body.snapshotId = image.slice("snapshot:".length);
|
|
199
|
+
else if (/^(sn|im)_/.test(image))
|
|
200
|
+
body.snapshotId = image;
|
|
201
|
+
else
|
|
202
|
+
body.templateId = image;
|
|
203
|
+
}
|
|
204
|
+
if (spec.env && Object.keys(spec.env).length)
|
|
205
|
+
body.env = spec.env;
|
|
206
|
+
if (spec.metadata && Object.keys(spec.metadata).length)
|
|
207
|
+
body.metadata = spec.metadata;
|
|
208
|
+
let id;
|
|
209
|
+
try {
|
|
210
|
+
id = await this.createMachine(body);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
// The free plan offers only the default 4 vCPU / 8 GiB; a smaller request fits in it.
|
|
214
|
+
const smaller = size.vcpu < DEFAULT_SIZE.vcpu;
|
|
215
|
+
if (!(smaller && err instanceof InstaboxError && err.code === "spec_not_allowed"))
|
|
216
|
+
throw err;
|
|
217
|
+
id = await this.createMachine({ ...body, vcpus: DEFAULT_SIZE.vcpu, memMiB: DEFAULT_SIZE.memoryGb * 1024 });
|
|
218
|
+
}
|
|
219
|
+
return new InstaboxBox(this, id);
|
|
220
|
+
}
|
|
221
|
+
async connect(nativeId) {
|
|
222
|
+
await this.ensureRunning(nativeId);
|
|
223
|
+
return new InstaboxBox(this, nativeId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
class InstaboxBox {
|
|
227
|
+
p;
|
|
228
|
+
nativeId;
|
|
229
|
+
constructor(p, nativeId) {
|
|
230
|
+
this.p = p;
|
|
231
|
+
this.nativeId = nativeId;
|
|
232
|
+
}
|
|
233
|
+
get path() {
|
|
234
|
+
return `/v1/machines/${encodeURIComponent(this.nativeId)}`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Retry calls the machine refused before doing anything: a paused or stopped machine is started,
|
|
238
|
+
* a machine busy resuming or restarting is waited for.
|
|
239
|
+
*/
|
|
240
|
+
async whenRunning(fn) {
|
|
241
|
+
const deadline = Date.now() + 120_000;
|
|
242
|
+
for (let attempt = 0;; attempt++) {
|
|
243
|
+
try {
|
|
244
|
+
return await fn();
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
if (!(err instanceof InstaboxError) || attempt >= 5 || Date.now() > deadline)
|
|
248
|
+
throw err;
|
|
249
|
+
if (err.code === "machine_not_running" && !err.retryable) {
|
|
250
|
+
await this.p.ensureRunning(this.nativeId);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const transient = ["machine_not_running", "operation_conflict", "host_unavailable", "quota_exceeded", "rate_limited"];
|
|
254
|
+
if (!err.retryable || !transient.includes(err.code ?? ""))
|
|
255
|
+
throw err;
|
|
256
|
+
await sleep(Math.min(10, err.retryAfterSeconds ?? 2) * 1000);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async exec(cmd, opts = {}) {
|
|
261
|
+
const t0 = Date.now();
|
|
262
|
+
const secs = Math.min(MAX_EXEC_SECONDS, Math.max(1, Math.round(opts.timeoutSeconds ?? 60)));
|
|
263
|
+
const body = {
|
|
264
|
+
cmd,
|
|
265
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
266
|
+
...(opts.env && Object.keys(opts.env).length ? { envs: opts.env } : {}),
|
|
267
|
+
timeoutSeconds: secs,
|
|
268
|
+
};
|
|
269
|
+
const streaming = !!(opts.onStdout || opts.onStderr);
|
|
270
|
+
const res = await this.whenRunning(() => this.p.request("POST", `${this.path}/exec`, {
|
|
271
|
+
json: body,
|
|
272
|
+
headers: streaming ? { Accept: "text/event-stream" } : {},
|
|
273
|
+
// The server kills the command at its timeout; allow for a wake in front of it.
|
|
274
|
+
timeoutMs: secs * 1000 + 60_000,
|
|
275
|
+
}));
|
|
276
|
+
let out;
|
|
277
|
+
let stdout = "";
|
|
278
|
+
let stderr = "";
|
|
279
|
+
if (streaming && (res.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
280
|
+
let exit;
|
|
281
|
+
for await (const ev of readSSE(res)) {
|
|
282
|
+
if (ev.event === "stdout" || ev.event === "stderr") {
|
|
283
|
+
const chunk = String(JSON.parse(ev.data).data ?? "");
|
|
284
|
+
if (!chunk)
|
|
285
|
+
continue;
|
|
286
|
+
if (ev.event === "stdout") {
|
|
287
|
+
stdout += chunk;
|
|
288
|
+
opts.onStdout?.(chunk);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
stderr += chunk;
|
|
292
|
+
opts.onStderr?.(chunk);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (ev.event === "exit") {
|
|
296
|
+
exit = JSON.parse(ev.data);
|
|
297
|
+
}
|
|
298
|
+
else if (ev.event === "error") {
|
|
299
|
+
const j = JSON.parse(ev.data);
|
|
300
|
+
const e = j?.error ?? j;
|
|
301
|
+
throw new InstaboxError(`exec: ${e?.code ? `${e.code}: ` : ""}${e?.message ?? ev.data.slice(0, 300)}`, undefined, e?.code, false);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!exit)
|
|
305
|
+
throw new ProviderError(ID, "exec stream ended without an exit event");
|
|
306
|
+
out = exit;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
out = (await res.json());
|
|
310
|
+
stdout = out.stdout ?? "";
|
|
311
|
+
stderr = out.stderr ?? "";
|
|
312
|
+
if (stdout)
|
|
313
|
+
opts.onStdout?.(stdout);
|
|
314
|
+
if (stderr)
|
|
315
|
+
opts.onStderr?.(stderr);
|
|
316
|
+
}
|
|
317
|
+
if (out.timedOut)
|
|
318
|
+
throw new ProviderError(ID, `command timed out after ${secs}s`);
|
|
319
|
+
if (typeof out.exitCode !== "number")
|
|
320
|
+
throw new ProviderError(ID, "exec returned no exit code");
|
|
321
|
+
return { exitCode: out.exitCode, stdout, stderr, durationMs: Date.now() - t0 };
|
|
322
|
+
}
|
|
323
|
+
async writeFile(path, content) {
|
|
324
|
+
const q = `?path=${encodeURIComponent(absolute(path))}&parents=true`;
|
|
325
|
+
await this.whenRunning(() => this.p.request("PUT", `${this.path}/files${q}`, { body: toBytes(content), timeoutMs: 30 * 60_000 }));
|
|
326
|
+
}
|
|
327
|
+
async readFile(path) {
|
|
328
|
+
const q = `?path=${encodeURIComponent(absolute(path))}`;
|
|
329
|
+
const res = await this.whenRunning(() => this.p.request("GET", `${this.path}/files/content${q}`, { timeoutMs: 30 * 60_000 }));
|
|
330
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
331
|
+
}
|
|
332
|
+
async pause() {
|
|
333
|
+
const op = await this.p.json("POST", `${this.path}/suspend`, {}, { idempotent: true });
|
|
334
|
+
await this.p.waitOperation(op, `pause of ${this.nativeId}`);
|
|
335
|
+
}
|
|
336
|
+
async kill() {
|
|
337
|
+
const deadline = Date.now() + 60_000;
|
|
338
|
+
for (;;) {
|
|
339
|
+
try {
|
|
340
|
+
await this.p.request("DELETE", this.path, { idempotent: true, headers: { "X-Confirm-Delete": this.nativeId } });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
if (err instanceof InstaboxError && err.status === 404)
|
|
345
|
+
return; // already gone
|
|
346
|
+
// A start or pause in progress blocks the delete until it ends.
|
|
347
|
+
if (!(err instanceof InstaboxError && err.code === "operation_conflict" && Date.now() < deadline))
|
|
348
|
+
throw err;
|
|
349
|
+
await sleep(1000);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** The files API takes absolute paths; relative ones are under HOME, where exec starts. */
|
|
355
|
+
function absolute(path) {
|
|
356
|
+
return path.startsWith("/") ? path : `${HOME}/${path.replace(/^\.\//, "")}`;
|
|
357
|
+
}
|
|
358
|
+
function sleep(ms) {
|
|
359
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
360
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Koyeb Standard instance types, from https://www.koyeb.com/docs/reference/instances
|
|
4
|
+
* (checked 2026-11-04). Billed per second for the whole instance.
|
|
5
|
+
*/
|
|
6
|
+
export declare const KOYEB_INSTANCES: readonly [{
|
|
7
|
+
readonly type: "nano";
|
|
8
|
+
readonly vcpu: 0.25;
|
|
9
|
+
readonly memoryGb: 0.25;
|
|
10
|
+
readonly usdHour: 0.0036;
|
|
11
|
+
}, {
|
|
12
|
+
readonly type: "micro";
|
|
13
|
+
readonly vcpu: 0.5;
|
|
14
|
+
readonly memoryGb: 0.5;
|
|
15
|
+
readonly usdHour: 0.0072;
|
|
16
|
+
}, {
|
|
17
|
+
readonly type: "small";
|
|
18
|
+
readonly vcpu: 1;
|
|
19
|
+
readonly memoryGb: 1;
|
|
20
|
+
readonly usdHour: 0.0144;
|
|
21
|
+
}, {
|
|
22
|
+
readonly type: "medium";
|
|
23
|
+
readonly vcpu: 2;
|
|
24
|
+
readonly memoryGb: 2;
|
|
25
|
+
readonly usdHour: 0.0288;
|
|
26
|
+
}, {
|
|
27
|
+
readonly type: "large";
|
|
28
|
+
readonly vcpu: 4;
|
|
29
|
+
readonly memoryGb: 4;
|
|
30
|
+
readonly usdHour: 0.0576;
|
|
31
|
+
}, {
|
|
32
|
+
readonly type: "xlarge";
|
|
33
|
+
readonly vcpu: 8;
|
|
34
|
+
readonly memoryGb: 8;
|
|
35
|
+
readonly usdHour: 0.1152;
|
|
36
|
+
}, {
|
|
37
|
+
readonly type: "2xlarge";
|
|
38
|
+
readonly vcpu: 16;
|
|
39
|
+
readonly memoryGb: 16;
|
|
40
|
+
readonly usdHour: 0.2304;
|
|
41
|
+
}, {
|
|
42
|
+
readonly type: "3xlarge";
|
|
43
|
+
readonly vcpu: 24;
|
|
44
|
+
readonly memoryGb: 32;
|
|
45
|
+
readonly usdHour: 0.4608;
|
|
46
|
+
}, {
|
|
47
|
+
readonly type: "4xlarge";
|
|
48
|
+
readonly vcpu: 32;
|
|
49
|
+
readonly memoryGb: 64;
|
|
50
|
+
readonly usdHour: 0.9216;
|
|
51
|
+
}, {
|
|
52
|
+
readonly type: "5xlarge";
|
|
53
|
+
readonly vcpu: 40;
|
|
54
|
+
readonly memoryGb: 128;
|
|
55
|
+
readonly usdHour: 1.8432;
|
|
56
|
+
}];
|
|
57
|
+
/** Smallest Standard instance with at least the requested vCPU and memory, or undefined. */
|
|
58
|
+
export declare function koyebInstance(vcpu: number, memoryGb: number): (typeof KOYEB_INSTANCES)[number] | undefined;
|
|
59
|
+
export declare class KoyebProvider implements Provider {
|
|
60
|
+
private token;
|
|
61
|
+
private baseUrl;
|
|
62
|
+
private region;
|
|
63
|
+
readonly id: "koyeb";
|
|
64
|
+
constructor(token?: string | undefined, baseUrl?: string, region?: string);
|
|
65
|
+
configured(): boolean;
|
|
66
|
+
/** @internal Control-plane JSON call. */
|
|
67
|
+
api<T>(method: string, path: string, json?: unknown): Promise<T>;
|
|
68
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
69
|
+
/** Reattach by service id. The executor secret is read back from the deployment's env. */
|
|
70
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
71
|
+
}
|