@promigence/sdk 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +54 -0
- package/dist/blake3.d.ts +10 -0
- package/dist/blake3.js +111 -0
- package/dist/bundle.d.ts +96 -0
- package/dist/bundle.js +227 -0
- package/dist/ca-guard.d.ts +1 -0
- package/dist/ca-guard.js +166 -0
- package/dist/caller.d.ts +73 -0
- package/dist/caller.js +47 -0
- package/dist/cjs/blake3.d.ts +10 -0
- package/dist/cjs/blake3.js +116 -0
- package/dist/cjs/bundle.d.ts +96 -0
- package/dist/cjs/bundle.js +242 -0
- package/dist/cjs/ca-guard.d.ts +1 -0
- package/dist/cjs/ca-guard.js +174 -0
- package/dist/cjs/caller.d.ts +73 -0
- package/dist/cjs/caller.js +57 -0
- package/dist/cjs/client.d.ts +3015 -0
- package/dist/cjs/client.js +2617 -0
- package/dist/cjs/command-errors.d.ts +41 -0
- package/dist/cjs/command-errors.js +43 -0
- package/dist/cjs/context.d.ts +119 -0
- package/dist/cjs/context.js +598 -0
- package/dist/cjs/core.d.ts +285 -0
- package/dist/cjs/core.js +290 -0
- package/dist/cjs/credstore.d.ts +90 -0
- package/dist/cjs/credstore.js +219 -0
- package/dist/cjs/dispatcher.d.ts +58 -0
- package/dist/cjs/dispatcher.js +91 -0
- package/dist/cjs/github.d.ts +158 -0
- package/dist/cjs/github.js +84 -0
- package/dist/cjs/gitlab.d.ts +104 -0
- package/dist/cjs/gitlab.js +24 -0
- package/dist/cjs/handles.d.ts +82 -0
- package/dist/cjs/handles.js +139 -0
- package/dist/cjs/hint.d.ts +1 -0
- package/dist/cjs/hint.js +10 -0
- package/dist/cjs/http2.d.ts +206 -0
- package/dist/cjs/http2.js +1932 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/keyed.gen.d.ts +1 -0
- package/dist/cjs/keyed.gen.js +10 -0
- package/dist/cjs/models.gen.d.ts +6162 -0
- package/dist/cjs/models.gen.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/proxy.d.ts +40 -0
- package/dist/cjs/proxy.js +257 -0
- package/dist/cjs/pty.d.ts +171 -0
- package/dist/cjs/pty.js +504 -0
- package/dist/cjs/sandbox.d.ts +537 -0
- package/dist/cjs/sandbox.js +736 -0
- package/dist/cjs/ssh.d.ts +25 -0
- package/dist/cjs/ssh.js +117 -0
- package/dist/cjs/trace.d.ts +33 -0
- package/dist/cjs/trace.js +88 -0
- package/dist/cjs/transport-worker.d.ts +1 -0
- package/dist/cjs/transport-worker.js +468 -0
- package/dist/cjs/warm.d.ts +1 -0
- package/dist/cjs/warm.js +43 -0
- package/dist/client.d.ts +3015 -0
- package/dist/client.js +2583 -0
- package/dist/command-errors.d.ts +41 -0
- package/dist/command-errors.js +36 -0
- package/dist/context.d.ts +119 -0
- package/dist/context.js +588 -0
- package/dist/core.d.ts +285 -0
- package/dist/core.js +266 -0
- package/dist/credstore.d.ts +90 -0
- package/dist/credstore.js +206 -0
- package/dist/dispatcher.d.ts +58 -0
- package/dist/dispatcher.js +86 -0
- package/dist/github.d.ts +158 -0
- package/dist/github.js +78 -0
- package/dist/gitlab.d.ts +104 -0
- package/dist/gitlab.js +20 -0
- package/dist/handles.d.ts +82 -0
- package/dist/handles.js +135 -0
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +7 -0
- package/dist/http2.d.ts +206 -0
- package/dist/http2.js +1921 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +15 -0
- package/dist/keyed.gen.d.ts +1 -0
- package/dist/keyed.gen.js +7 -0
- package/dist/models.gen.d.ts +6162 -0
- package/dist/models.gen.js +1 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.js +249 -0
- package/dist/pty.d.ts +171 -0
- package/dist/pty.js +496 -0
- package/dist/sandbox.d.ts +537 -0
- package/dist/sandbox.js +725 -0
- package/dist/ssh.d.ts +25 -0
- package/dist/ssh.js +114 -0
- package/dist/trace.d.ts +33 -0
- package/dist/trace.js +80 -0
- package/dist/transport-worker.d.ts +1 -0
- package/dist/transport-worker.js +466 -0
- package/dist/warm.d.ts +1 -0
- package/dist/warm.js +40 -0
- package/package.json +54 -0
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
import { ApiError, CommandExitError, EnvFailedError, IMAGE_BUILD_TIMEOUT_MS, InvalidArgumentError, NotFoundError, TimeoutError, UnsupportedError, WAIT_TIMEOUT_MS, withPreviewsFix, } from "./core.js";
|
|
3
|
+
import { PromigenceClient, LONG_POLL_MS, checkImageRef, checkImageVersioned, checkSandboxId, errorFor, newIdempotencyKey, toCommandResult, isWaiting, } from "./client.js";
|
|
4
|
+
import { CommandHandle } from "./handles.js";
|
|
5
|
+
import { CommandTimeoutError, answerTimedOut } from "./command-errors.js";
|
|
6
|
+
import { hintArg } from "./hint.js";
|
|
7
|
+
const EXEC_TIMEOUT_DEFAULT_MS = 15 * 60_000;
|
|
8
|
+
function textSink(cb) {
|
|
9
|
+
if (!cb)
|
|
10
|
+
return undefined;
|
|
11
|
+
const dec = new TextDecoder();
|
|
12
|
+
return {
|
|
13
|
+
push: (b) => { const t = dec.decode(b, { stream: true }); if (t)
|
|
14
|
+
cb(t); },
|
|
15
|
+
end: () => { const t = dec.decode(); if (t)
|
|
16
|
+
cb(t); },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let uploadSeq = 0;
|
|
20
|
+
export const DEFAULT_PTY_CMD = "TERM=${TERM:-xterm-256color} exec /bin/sh -l";
|
|
21
|
+
export function shellQuote(s) {
|
|
22
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
23
|
+
}
|
|
24
|
+
export function ptyCommand(opts) {
|
|
25
|
+
const envs = Object.entries(opts.envs ?? {});
|
|
26
|
+
if (!opts.cwd && envs.length === 0)
|
|
27
|
+
return opts.cmd;
|
|
28
|
+
const parts = [];
|
|
29
|
+
if (opts.cwd)
|
|
30
|
+
parts.push(`cd ${shellQuote(opts.cwd)} &&`);
|
|
31
|
+
for (const [k, v] of envs) {
|
|
32
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
|
|
33
|
+
throw new InvalidArgumentError(`env name ${JSON.stringify(k)} is not a valid variable name`, "Use [A-Za-z_][A-Za-z0-9_]*.");
|
|
34
|
+
}
|
|
35
|
+
parts.push(`export ${k}=${shellQuote(v)};`);
|
|
36
|
+
}
|
|
37
|
+
parts.push(opts.cmd ?? DEFAULT_PTY_CMD);
|
|
38
|
+
return parts.join(" ");
|
|
39
|
+
}
|
|
40
|
+
function permString(mode) {
|
|
41
|
+
const r = (m) => `${m & 4 ? "r" : "-"}${m & 2 ? "w" : "-"}${m & 1 ? "x" : "-"}`;
|
|
42
|
+
return r((mode >> 6) & 7) + r((mode >> 3) & 7) + r(mode & 7);
|
|
43
|
+
}
|
|
44
|
+
export function toEntryInfo(e) {
|
|
45
|
+
const mode = parseInt(e.mode, 8) || 0;
|
|
46
|
+
return {
|
|
47
|
+
name: e.name,
|
|
48
|
+
path: e.path,
|
|
49
|
+
type: e.is_dir ? "dir" : "file",
|
|
50
|
+
size: e.size,
|
|
51
|
+
mode,
|
|
52
|
+
permissions: permString(mode),
|
|
53
|
+
modifiedTime: new Date(Math.floor(e.mtime_unix_ns / 1e6)),
|
|
54
|
+
isSymlink: e.is_symlink,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function toEntryInfoFromPath(f) {
|
|
58
|
+
const at = f.path.lastIndexOf("/");
|
|
59
|
+
return toEntryInfo({ ...f, name: at < 0 ? f.path : f.path.slice(at + 1) });
|
|
60
|
+
}
|
|
61
|
+
function buildIsFree(b) {
|
|
62
|
+
return b.deduped === true || b.joined === true || b.idempotent_replay === true || b.state === "verified" || b.state === "built";
|
|
63
|
+
}
|
|
64
|
+
function buildNotice(image) {
|
|
65
|
+
return (b) => {
|
|
66
|
+
if (buildIsFree(b))
|
|
67
|
+
return;
|
|
68
|
+
const price = typeof b.build_price === "number"
|
|
69
|
+
? typeof b.per_second_usd === "number"
|
|
70
|
+
? `billed per second while it builds ($${Number(b.per_second_usd.toPrecision(12))}/s), at most $${b.build_price.toFixed(2)}`
|
|
71
|
+
: `build_price $${b.build_price.toFixed(2)}, charged once it verifies`
|
|
72
|
+
: "billed at your plan's rates";
|
|
73
|
+
process.emitWarning(`building a snapshot of ${image}: ${price} (later creates from this image reuse it free)`, { code: "PROMIGENCE_BUILD" });
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function buildNotAllowed(image) {
|
|
77
|
+
return new InvalidArgumentError(`Sandbox.create({ image: ${JSON.stringify(image)} }) builds that image's snapshot unless one already exists, and a new build is billed per second while it runs (\`promigence pricing\` shows the rates; reusing a snapshot already built is free). Nothing was sent.`, `To allow the build: Sandbox.create({ image: ${JSON.stringify(image)}, allowBuild: true }). Or build it once, ` +
|
|
78
|
+
`\`promigence snapshot create --image ${hintArg(image)} --alias NAME\`, and create from its name: Sandbox.create("NAME").`);
|
|
79
|
+
}
|
|
80
|
+
export async function imageSnapshot(client, opts, hooks = {}) {
|
|
81
|
+
const create = { image: opts.image };
|
|
82
|
+
if (opts.setup !== undefined)
|
|
83
|
+
create.setup = opts.setup;
|
|
84
|
+
if (opts.verify !== undefined)
|
|
85
|
+
create.verify = opts.verify;
|
|
86
|
+
if (opts.start !== undefined)
|
|
87
|
+
create.start = opts.start;
|
|
88
|
+
if (opts.tier !== undefined)
|
|
89
|
+
create.tier = opts.tier;
|
|
90
|
+
if (opts.alias !== undefined)
|
|
91
|
+
create.alias = opts.alias;
|
|
92
|
+
if (opts.registrySecret !== undefined)
|
|
93
|
+
create.registrySecret = opts.registrySecret;
|
|
94
|
+
let build = (await client.snapshots.create(create)) ?? {};
|
|
95
|
+
if (hooks.onBuild)
|
|
96
|
+
await hooks.onBuild(build);
|
|
97
|
+
const buildId = build.build_id;
|
|
98
|
+
if (build.state === "queued" || build.state === "running") {
|
|
99
|
+
if (!buildId) {
|
|
100
|
+
throw new ApiError({
|
|
101
|
+
code: "internal", message: "POST /snapshots started a build but returned no build_id",
|
|
102
|
+
fix: "Not your fault. Re-run; if it repeats, include the request_id when reporting.", exitCode: 1,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const bound = opts.buildTimeoutMs ?? IMAGE_BUILD_TIMEOUT_MS;
|
|
106
|
+
const abortedBuild = () => new TimeoutError(`Sandbox.create was aborted by the caller's signal while image build ${buildId} ran; no sandbox was started, and the build goes on`, `Nothing to fix: the caller aborted it. The build is charged if it verifies, and later creates from this image reuse it: \`promigence snapshot status ${hintArg(buildId)} --wait\`.`);
|
|
107
|
+
let waited;
|
|
108
|
+
try {
|
|
109
|
+
waited = await client.snapshots.waitBuild(buildId, { waitTimeoutMs: bound, ...(hooks.signal ? { signal: hooks.signal } : {}) });
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
if (hooks.signal?.aborted)
|
|
113
|
+
throw abortedBuild();
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
if (hooks.signal?.aborted)
|
|
117
|
+
throw abortedBuild();
|
|
118
|
+
if (waited.timedOut) {
|
|
119
|
+
throw new TimeoutError(`image build ${buildId} was still running after ${bound} ms; nothing was cancelled`, `Wait for it: \`promigence snapshot status ${hintArg(buildId)} --wait\`, then call Sandbox.create({ image }) again — the same digest reuses the build.`);
|
|
120
|
+
}
|
|
121
|
+
build = waited.build ?? {};
|
|
122
|
+
}
|
|
123
|
+
const state = build.state;
|
|
124
|
+
if (build.snapshot_id && state !== "rejected" && state !== "failed")
|
|
125
|
+
return build.snapshot_id;
|
|
126
|
+
const err = build.error && typeof build.error === "object" ? build.error : {};
|
|
127
|
+
throw errorFor(err.code ?? "snapshot_failed", err.message ?? `image build ${buildId ?? "?"} ended ${state ?? "without a state"} with no snapshot`, buildId ? `Read the build log: \`promigence snapshot status ${hintArg(buildId)} --log 80\`.` : "Re-run; if it repeats, include the request_id when reporting.");
|
|
128
|
+
}
|
|
129
|
+
function deferred() {
|
|
130
|
+
let resolve;
|
|
131
|
+
let reject;
|
|
132
|
+
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
133
|
+
promise.catch(() => { });
|
|
134
|
+
const d = { promise, settled: false, resolve: (v) => { d.settled = true; resolve(v); }, reject: (e) => { d.settled = true; reject(e); } };
|
|
135
|
+
return d;
|
|
136
|
+
}
|
|
137
|
+
async function abandonQueued(client, sandboxId, runId, bound) {
|
|
138
|
+
const ids = { sandboxId, runId };
|
|
139
|
+
try {
|
|
140
|
+
await client.runs.kill(runId);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return new TimeoutError(`sandbox ${sandboxId} was still queued after ${bound} ms, and cancelling it failed: it may still start`, `Cancel it: \`promigence run kill ${hintArg(runId)}\`, or keep it: \`promigence run status ${hintArg(runId)} --wait\`.`, ids);
|
|
144
|
+
}
|
|
145
|
+
return new TimeoutError(`sandbox ${sandboxId} was still queued after ${bound} ms; it was cancelled, so it will not start later`, "Create again, or wait longer with `waitTimeoutMs`. A sandbox cancelled while queued is not billed.", ids);
|
|
146
|
+
}
|
|
147
|
+
const CREATE_CLEANUP_MS = 10_000;
|
|
148
|
+
async function endInterruptedCreate(client, runId, sent) {
|
|
149
|
+
try {
|
|
150
|
+
let id = runId;
|
|
151
|
+
if (id === undefined && sent !== undefined) {
|
|
152
|
+
const { waitMs: _park, signal: _gone, ...again } = sent;
|
|
153
|
+
id = (await client.runs.start({ ...again, signal: AbortSignal.timeout(CREATE_CLEANUP_MS) })).run_id;
|
|
154
|
+
}
|
|
155
|
+
if (id)
|
|
156
|
+
await client.runs.kill(id);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const creatingNow = new Set();
|
|
162
|
+
let sigintHeld = false;
|
|
163
|
+
function onSigint() {
|
|
164
|
+
sigintHeld = false;
|
|
165
|
+
const cleanups = [...creatingNow];
|
|
166
|
+
creatingNow.clear();
|
|
167
|
+
if (process.listenerCount("SIGINT") > 0)
|
|
168
|
+
return;
|
|
169
|
+
void Promise.allSettled(cleanups.map((f) => f())).finally(() => { process.kill(process.pid, "SIGINT"); });
|
|
170
|
+
}
|
|
171
|
+
function holdSigint(cleanup) {
|
|
172
|
+
if (typeof process === "undefined" || typeof process.listenerCount !== "function")
|
|
173
|
+
return () => { };
|
|
174
|
+
if (process.listenerCount("SIGINT") - (sigintHeld ? 1 : 0) > 0)
|
|
175
|
+
return () => { };
|
|
176
|
+
creatingNow.add(cleanup);
|
|
177
|
+
if (!sigintHeld) {
|
|
178
|
+
process.once("SIGINT", onSigint);
|
|
179
|
+
sigintHeld = true;
|
|
180
|
+
}
|
|
181
|
+
return () => {
|
|
182
|
+
creatingNow.delete(cleanup);
|
|
183
|
+
if (creatingNow.size === 0 && sigintHeld) {
|
|
184
|
+
process.removeListener("SIGINT", onSigint);
|
|
185
|
+
sigintHeld = false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export const DEFAULT_SNAPSHOT = "promigence/base";
|
|
190
|
+
export function spendCapFromEnv(spendCap) {
|
|
191
|
+
if (spendCap !== undefined)
|
|
192
|
+
return spendCap;
|
|
193
|
+
const env = process.env["PROMIGENCE_SPEND_CAP"];
|
|
194
|
+
if (env !== undefined && env !== "" && Number.isFinite(Number(env)))
|
|
195
|
+
return Number(env);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
export async function resolveSpendCap(client, spendCap) {
|
|
199
|
+
if (spendCap !== undefined)
|
|
200
|
+
return spendCap;
|
|
201
|
+
const env = process.env["PROMIGENCE_SPEND_CAP"];
|
|
202
|
+
if (env !== undefined && env !== "" && Number.isFinite(Number(env)))
|
|
203
|
+
return Number(env);
|
|
204
|
+
const me = await client.me();
|
|
205
|
+
if (typeof me.default_spend_cap === "number")
|
|
206
|
+
return me.default_spend_cap;
|
|
207
|
+
throw new InvalidArgumentError("no spend cap: every run must be capped before it starts", "Pass { spendCap: 1 } (USD), set PROMIGENCE_SPEND_CAP, or ask for an org default_spend_cap (GET /me).");
|
|
208
|
+
}
|
|
209
|
+
export class Sandbox {
|
|
210
|
+
sandboxId;
|
|
211
|
+
runId;
|
|
212
|
+
snapshot;
|
|
213
|
+
#client;
|
|
214
|
+
#envs;
|
|
215
|
+
timeoutMs;
|
|
216
|
+
dirty = false;
|
|
217
|
+
commands;
|
|
218
|
+
pty;
|
|
219
|
+
ports;
|
|
220
|
+
terminals = new Map();
|
|
221
|
+
files;
|
|
222
|
+
constructor(init) {
|
|
223
|
+
this.sandboxId = init.sandboxId;
|
|
224
|
+
this.runId = init.runId;
|
|
225
|
+
this.snapshot = init.snapshot;
|
|
226
|
+
this.#client = init.client;
|
|
227
|
+
this.#envs = init.envs;
|
|
228
|
+
this.timeoutMs = init.timeoutMs;
|
|
229
|
+
const runBackground = async (cmd, opts) => {
|
|
230
|
+
this.dirty = true;
|
|
231
|
+
const env = { ...(this.#envs ?? {}), ...(opts.envs ?? {}) };
|
|
232
|
+
const rec = await this.#client.sandboxes.execBackground(this.sandboxId, {
|
|
233
|
+
sh: cmd,
|
|
234
|
+
...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
|
|
235
|
+
...(Object.keys(env).length === 0 ? {} : { env }),
|
|
236
|
+
...(opts.user === undefined ? {} : { user: opts.user }),
|
|
237
|
+
...(opts.stdin ? { stdin: true } : {}),
|
|
238
|
+
...(opts.timeoutMs === undefined || opts.timeoutMs === 0 ? {} : { timeoutMs: opts.timeoutMs }),
|
|
239
|
+
...(opts.maxOutputBytes === undefined ? {} : { maxOutputBytes: opts.maxOutputBytes }),
|
|
240
|
+
});
|
|
241
|
+
return new CommandHandle(this.#client, rec, {
|
|
242
|
+
...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
|
|
243
|
+
...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
|
|
244
|
+
...(opts.timeoutMs === undefined || opts.timeoutMs === 0 ? {} : { timeoutMs: opts.timeoutMs }),
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
this.commands = {
|
|
248
|
+
run: (async (cmd, opts = {}) => {
|
|
249
|
+
if (opts.background)
|
|
250
|
+
return runBackground(cmd, opts);
|
|
251
|
+
const fg = opts;
|
|
252
|
+
this.dirty = true;
|
|
253
|
+
const env = this.#envs === undefined && fg.envs === undefined ? undefined : { ...(this.#envs ?? {}), ...(fg.envs ?? {}) };
|
|
254
|
+
const bound = fg.timeoutMs === undefined || fg.timeoutMs === 0 ? (this.timeoutMs || undefined) : fg.timeoutMs;
|
|
255
|
+
const exec = { sh: cmd };
|
|
256
|
+
if (fg.cwd !== undefined)
|
|
257
|
+
exec.cwd = fg.cwd;
|
|
258
|
+
if (env !== undefined && Object.keys(env).length !== 0)
|
|
259
|
+
exec.env = env;
|
|
260
|
+
if (fg.user !== undefined)
|
|
261
|
+
exec.user = fg.user;
|
|
262
|
+
if (bound !== undefined)
|
|
263
|
+
exec.timeoutMs = bound;
|
|
264
|
+
if (fg.maxOutputBytes !== undefined)
|
|
265
|
+
exec.maxOutputBytes = fg.maxOutputBytes;
|
|
266
|
+
let res;
|
|
267
|
+
const out = textSink(fg.onStdout);
|
|
268
|
+
const err = textSink(fg.onStderr);
|
|
269
|
+
if (out || err) {
|
|
270
|
+
res = await this.#client.sandboxes.execStream(this.sandboxId, exec, {
|
|
271
|
+
...(out ? { stdout: out.push } : {}),
|
|
272
|
+
...(err ? { stderr: err.push } : {}),
|
|
273
|
+
});
|
|
274
|
+
out?.end();
|
|
275
|
+
err?.end();
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
res = await this.#client.sandboxes.exec(this.sandboxId, exec);
|
|
279
|
+
}
|
|
280
|
+
const result = toCommandResult(res);
|
|
281
|
+
if (result.exitCode !== 0 && fg.throwOnExit !== false) {
|
|
282
|
+
if (answerTimedOut(res))
|
|
283
|
+
throw new CommandTimeoutError(result, bound ?? EXEC_TIMEOUT_DEFAULT_MS);
|
|
284
|
+
throw new CommandExitError(result);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}),
|
|
288
|
+
list: async () => {
|
|
289
|
+
const execs = await this.#client.sandboxes.execs.list(this.sandboxId);
|
|
290
|
+
return execs.map((e) => {
|
|
291
|
+
const code = e.exit ? (e.exit.exit_code ?? (e.exit.signal ? 128 + e.exit.signal : -1)) : undefined;
|
|
292
|
+
return { pid: e.exec_id, cmd: e.argv, state: e.state, ...(code === undefined ? {} : { exitCode: code }) };
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
kill: async (pid) => (await this.#client.sandboxes.execs.signal(this.sandboxId, pid, "SIGKILL")).delivered,
|
|
296
|
+
connect: async (pid, opts = {}) => {
|
|
297
|
+
const rec = await this.#client.sandboxes.execs.get(this.sandboxId, pid);
|
|
298
|
+
return new CommandHandle(this.#client, rec, opts);
|
|
299
|
+
},
|
|
300
|
+
sendStdin: async (pid, data) => (await this.#client.sandboxes.execs.stdin(this.sandboxId, pid, { data })).written,
|
|
301
|
+
closeStdin: async (pid) => {
|
|
302
|
+
await this.#client.sandboxes.execs.stdin(this.sandboxId, pid, { eof: true });
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
const terminal = (pid) => {
|
|
306
|
+
const t = this.terminals.get(pid);
|
|
307
|
+
if (!t) {
|
|
308
|
+
throw new NotFoundError(`no terminal with pid ${pid} is open from this Sandbox object`, "A terminal belongs to the connection that opened it: use the session pty.create() returned, or open a new one.");
|
|
309
|
+
}
|
|
310
|
+
return t;
|
|
311
|
+
};
|
|
312
|
+
this.pty = {
|
|
313
|
+
create: async (opts = {}) => {
|
|
314
|
+
const cmd = ptyCommand({
|
|
315
|
+
...(opts.cmd === undefined ? {} : { cmd: opts.cmd }),
|
|
316
|
+
...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
|
|
317
|
+
envs: { ...(this.#envs ?? {}), ...(opts.envs ?? {}) },
|
|
318
|
+
});
|
|
319
|
+
let pid = -1;
|
|
320
|
+
const session = await this.#client.sandboxes.pty(this.sandboxId, {
|
|
321
|
+
...(opts.cols === undefined ? {} : { cols: opts.cols }),
|
|
322
|
+
...(opts.rows === undefined ? {} : { rows: opts.rows }),
|
|
323
|
+
...(cmd === undefined ? {} : { cmd }),
|
|
324
|
+
...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
|
|
325
|
+
...(opts.onData ? { onData: opts.onData } : {}),
|
|
326
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
327
|
+
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
328
|
+
reconnect: opts.reconnect ?? opts.cmd === undefined,
|
|
329
|
+
...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
|
|
330
|
+
onExit: (e) => { this.terminals.delete(pid); opts.onExit?.(e); },
|
|
331
|
+
});
|
|
332
|
+
pid = session.pid;
|
|
333
|
+
this.dirty = true;
|
|
334
|
+
this.terminals.set(session.pid, session);
|
|
335
|
+
return session;
|
|
336
|
+
},
|
|
337
|
+
sendInput: async (pid, data) => { terminal(pid).write(data); },
|
|
338
|
+
resize: async (pid, size) => { terminal(pid).resize(size); },
|
|
339
|
+
kill: async (pid) => {
|
|
340
|
+
const t = this.terminals.get(pid);
|
|
341
|
+
if (!t)
|
|
342
|
+
return false;
|
|
343
|
+
t.signal(9);
|
|
344
|
+
return true;
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
this.ports = {
|
|
348
|
+
expose: (port) => this.#client.sandboxes.ports.expose(this.sandboxId, port),
|
|
349
|
+
list: () => this.#client.sandboxes.ports.list(this.sandboxId),
|
|
350
|
+
revoke: async (tokenOrPort) => (await this.#client.sandboxes.ports.revoke(this.sandboxId, tokenOrPort)).revoked,
|
|
351
|
+
};
|
|
352
|
+
this.files = {
|
|
353
|
+
read: (async (path, opts = {}) => {
|
|
354
|
+
const format = opts.format ?? "text";
|
|
355
|
+
if (format !== "text" && format !== "bytes") {
|
|
356
|
+
throw new InvalidArgumentError(`files.read format must be "text" or "bytes", got ${JSON.stringify(format)}`, "Pass { format: 'bytes' } and wrap the bytes yourself (new Blob([bytes]) for a Blob).");
|
|
357
|
+
}
|
|
358
|
+
const bytes = await this.#client.sandboxes.files.get(this.sandboxId, path);
|
|
359
|
+
return format === "bytes" ? bytes : new TextDecoder().decode(bytes);
|
|
360
|
+
}),
|
|
361
|
+
write: async (path, data, opts = {}) => {
|
|
362
|
+
let body;
|
|
363
|
+
if (typeof data === "string" || data instanceof Uint8Array)
|
|
364
|
+
body = data;
|
|
365
|
+
else if (data instanceof ArrayBuffer)
|
|
366
|
+
body = new Uint8Array(data);
|
|
367
|
+
else if (ArrayBuffer.isView(data))
|
|
368
|
+
body = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
369
|
+
else if (typeof Blob !== "undefined" && data instanceof Blob)
|
|
370
|
+
body = new Uint8Array(await data.arrayBuffer());
|
|
371
|
+
else {
|
|
372
|
+
throw new InvalidArgumentError(`files.write data must be a string, a typed array, an ArrayBuffer or a Blob, got ${data === null ? "null" : typeof data}`, "Pass text, or the bytes as a Uint8Array.");
|
|
373
|
+
}
|
|
374
|
+
this.dirty = true;
|
|
375
|
+
await this.#client.sandboxes.files.put(this.sandboxId, path, body, opts.mode === undefined ? {} : { mode: opts.mode });
|
|
376
|
+
},
|
|
377
|
+
remove: async (path, opts = {}) => {
|
|
378
|
+
this.dirty = true;
|
|
379
|
+
await this.#client.sandboxes.files.delete(this.sandboxId, path, opts);
|
|
380
|
+
},
|
|
381
|
+
exists: async (path) => {
|
|
382
|
+
try {
|
|
383
|
+
await this.#client.sandboxes.files.stat(this.sandboxId, path);
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
if (e instanceof NotFoundError)
|
|
388
|
+
return false;
|
|
389
|
+
throw e;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
list: async (path = "/", opts = {}) => {
|
|
393
|
+
const o = opts.depth !== undefined && opts.depth > 1 && opts.recursive === undefined ? { ...opts, recursive: true } : opts;
|
|
394
|
+
const r = await this.#client.sandboxes.files.list(this.sandboxId, path, o);
|
|
395
|
+
return r.entries.map(toEntryInfo);
|
|
396
|
+
},
|
|
397
|
+
mkdir: async (path, opts = {}) => {
|
|
398
|
+
this.dirty = true;
|
|
399
|
+
return toEntryInfoFromPath(await this.#client.sandboxes.files.mkdir(this.sandboxId, path, opts));
|
|
400
|
+
},
|
|
401
|
+
move: async (from, to, opts = {}) => {
|
|
402
|
+
this.dirty = true;
|
|
403
|
+
return toEntryInfoFromPath(await this.#client.sandboxes.files.move(this.sandboxId, from, to, opts));
|
|
404
|
+
},
|
|
405
|
+
rename: async (from, to, opts = {}) => this.files.move(from, to, opts),
|
|
406
|
+
copy: async (from, to, opts = {}) => {
|
|
407
|
+
this.dirty = true;
|
|
408
|
+
return toEntryInfoFromPath(await this.#client.sandboxes.files.copy(this.sandboxId, from, to, opts));
|
|
409
|
+
},
|
|
410
|
+
chmod: async (path, opts) => {
|
|
411
|
+
this.dirty = true;
|
|
412
|
+
return toEntryInfoFromPath(await this.#client.sandboxes.files.chmod(this.sandboxId, path, opts));
|
|
413
|
+
},
|
|
414
|
+
search: async (path, opts) => (await this.#client.sandboxes.files.search(this.sandboxId, path, opts)).matches,
|
|
415
|
+
downloadDir: async (path, opts = {}) => {
|
|
416
|
+
this.dirty = true;
|
|
417
|
+
const made = await this.#client.sandboxes.files.archive(this.sandboxId, path, opts);
|
|
418
|
+
try {
|
|
419
|
+
return await this.#client.sandboxes.files.get(this.sandboxId, made.archive_path);
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await this.#client.sandboxes.files.delete(this.sandboxId, made.archive_path).catch(() => { });
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
uploadDir: async (tar, into, opts = {}) => {
|
|
426
|
+
this.dirty = true;
|
|
427
|
+
const scratch = `/tmp/.promigence-upload/up-${Date.now().toString(36)}-${(uploadSeq++).toString(36)}.tar`;
|
|
428
|
+
await this.#client.sandboxes.files.put(this.sandboxId, scratch, tar, {});
|
|
429
|
+
try {
|
|
430
|
+
return await this.#client.sandboxes.files.extract(this.sandboxId, scratch, into, { strip: opts.strip ?? 1 });
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
await this.#client.sandboxes.files.delete(this.sandboxId, scratch).catch(() => { });
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
static async create(arg, second = {}) {
|
|
439
|
+
const opts = typeof arg === "object" && arg !== null ? arg : second;
|
|
440
|
+
let snapshotIdOrAlias = typeof arg === "string" ? arg : opts.snapshot;
|
|
441
|
+
if (snapshotIdOrAlias !== undefined && opts.image !== undefined) {
|
|
442
|
+
throw new InvalidArgumentError("Sandbox.create takes a snapshot or an image, not both", "Pass Sandbox.create('promigence/base') or Sandbox.create({ image: 'registry/name@sha256:…' }).");
|
|
443
|
+
}
|
|
444
|
+
if (opts.tier !== undefined && opts.image === undefined) {
|
|
445
|
+
throw new InvalidArgumentError(`Sandbox.create: tier "${opts.tier}" needs an image; a sandbox from a snapshot has the snapshot's size (the built-in base is small)`, "Pass image: 'registry/name@sha256:…' with the tier, or build a snapshot at that size once " +
|
|
446
|
+
"(promigence snapshot create --image REF --tier large) and create from its alias.");
|
|
447
|
+
}
|
|
448
|
+
if (opts.image !== undefined) {
|
|
449
|
+
checkImageRef(opts.image);
|
|
450
|
+
checkImageVersioned(opts.image);
|
|
451
|
+
if (opts.allowBuild !== true)
|
|
452
|
+
throw buildNotAllowed(opts.image.trim());
|
|
453
|
+
}
|
|
454
|
+
const client = opts.client ?? new PromigenceClient({
|
|
455
|
+
...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
|
|
456
|
+
...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
|
|
457
|
+
});
|
|
458
|
+
const aborted = () => new TimeoutError("Sandbox.create was aborted by the caller's signal; anything it had started was ended", "Nothing to fix: the caller aborted it.");
|
|
459
|
+
if (opts.signal?.aborted)
|
|
460
|
+
throw aborted();
|
|
461
|
+
if (opts.image !== undefined) {
|
|
462
|
+
try {
|
|
463
|
+
snapshotIdOrAlias = await imageSnapshot(client, opts, {
|
|
464
|
+
onBuild: opts.onBuild ?? buildNotice(opts.image.trim()),
|
|
465
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
if (opts.signal?.aborted && !(e instanceof TimeoutError))
|
|
470
|
+
throw aborted();
|
|
471
|
+
throw e;
|
|
472
|
+
}
|
|
473
|
+
if (opts.signal?.aborted)
|
|
474
|
+
throw aborted();
|
|
475
|
+
}
|
|
476
|
+
else if (snapshotIdOrAlias === undefined || snapshotIdOrAlias === "base") {
|
|
477
|
+
snapshotIdOrAlias = DEFAULT_SNAPSHOT;
|
|
478
|
+
}
|
|
479
|
+
const stop = new AbortController();
|
|
480
|
+
const key = newIdempotencyKey();
|
|
481
|
+
let sent;
|
|
482
|
+
let runId;
|
|
483
|
+
let ending;
|
|
484
|
+
const end = () => (ending ??= endInterruptedCreate(client, runId, sent));
|
|
485
|
+
const release = holdSigint(() => { stop.abort(); return end(); });
|
|
486
|
+
const onAbort = () => { stop.abort(); };
|
|
487
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
488
|
+
try {
|
|
489
|
+
let handles;
|
|
490
|
+
try {
|
|
491
|
+
({ runId, handles } = await Sandbox.forkHandlesWith(snapshotIdOrAlias, { ...opts, count: 1, client }, {
|
|
492
|
+
idempotencyKey: key, signal: stop.signal, onStart: (s) => { sent = s; },
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
if (stop.signal.aborted) {
|
|
497
|
+
await end();
|
|
498
|
+
throw aborted();
|
|
499
|
+
}
|
|
500
|
+
throw e;
|
|
501
|
+
}
|
|
502
|
+
const first = handles[0];
|
|
503
|
+
try {
|
|
504
|
+
if (!first)
|
|
505
|
+
throw new EnvFailedError("not_started", "no sandbox was started", "Re-run; if it repeats, check `promigence run report <run_id>`.");
|
|
506
|
+
return await first;
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
if (stop.signal.aborted) {
|
|
510
|
+
await end();
|
|
511
|
+
throw aborted();
|
|
512
|
+
}
|
|
513
|
+
if (e instanceof TimeoutError && e.runId === runId && e.sandboxId !== undefined) {
|
|
514
|
+
throw await abandonQueued(client, e.sandboxId, runId, opts.waitTimeoutMs ?? WAIT_TIMEOUT_MS);
|
|
515
|
+
}
|
|
516
|
+
await client.runs.kill(runId).catch(() => { });
|
|
517
|
+
throw e;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
release();
|
|
522
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
static async fork(snapshotIdOrAlias, opts = {}) {
|
|
526
|
+
const { handles } = await Sandbox.forkHandles(snapshotIdOrAlias, opts);
|
|
527
|
+
return Promise.all(handles);
|
|
528
|
+
}
|
|
529
|
+
static async forkHandles(snapshotIdOrAlias, opts = {}) {
|
|
530
|
+
return Sandbox.forkHandlesWith(snapshotIdOrAlias, opts, {});
|
|
531
|
+
}
|
|
532
|
+
static async forkHandlesWith(snapshotIdOrAlias, opts, extra) {
|
|
533
|
+
const client = opts.client ?? new PromigenceClient({
|
|
534
|
+
...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
|
|
535
|
+
...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
|
|
536
|
+
});
|
|
537
|
+
const count = opts.count ?? 1;
|
|
538
|
+
const spendCap = spendCapFromEnv(opts.spendCap);
|
|
539
|
+
const start = { snapshot: snapshotIdOrAlias, count };
|
|
540
|
+
if (spendCap !== undefined)
|
|
541
|
+
start.spendCap = spendCap;
|
|
542
|
+
if (count === 1)
|
|
543
|
+
start.waitMs = Math.min(opts.waitTimeoutMs ?? WAIT_TIMEOUT_MS, LONG_POLL_MS);
|
|
544
|
+
if (opts.timeoutMs !== undefined)
|
|
545
|
+
start.timeoutMs = opts.timeoutMs;
|
|
546
|
+
if (opts.allowInternetAccess === false)
|
|
547
|
+
start.network = "none";
|
|
548
|
+
if (opts.allowHosts?.length)
|
|
549
|
+
start.network = { allow: opts.allowHosts };
|
|
550
|
+
if (opts.metadata)
|
|
551
|
+
start.labels = opts.metadata;
|
|
552
|
+
if (opts.secrets?.length)
|
|
553
|
+
start.secrets = opts.secrets;
|
|
554
|
+
if (opts.optimizeFor !== undefined)
|
|
555
|
+
start.optimizeFor = opts.optimizeFor;
|
|
556
|
+
if (extra.idempotencyKey !== undefined)
|
|
557
|
+
start.idempotencyKey = extra.idempotencyKey;
|
|
558
|
+
extra.onStart?.({ ...start });
|
|
559
|
+
const run = await client.runs.start(extra.signal === undefined ? start : { ...start, signal: extra.signal });
|
|
560
|
+
const envs = opts.envs;
|
|
561
|
+
const mk = (ref) => {
|
|
562
|
+
const init = { sandboxId: ref.sandbox_id, runId: run.run_id, snapshot: snapshotIdOrAlias, client };
|
|
563
|
+
if (envs !== undefined)
|
|
564
|
+
init.envs = envs;
|
|
565
|
+
if (opts.timeoutMs !== undefined)
|
|
566
|
+
init.timeoutMs = opts.timeoutMs;
|
|
567
|
+
return new Sandbox(init);
|
|
568
|
+
};
|
|
569
|
+
const only = run.sandboxes?.length === 1 ? run.sandboxes[0] : undefined;
|
|
570
|
+
if (only !== undefined && !isWaiting(only.state) && only.state !== "ended") {
|
|
571
|
+
const sb = mk(only);
|
|
572
|
+
opts.onReady?.(sb, 0);
|
|
573
|
+
return { runId: run.run_id, handles: [Promise.resolve(sb)] };
|
|
574
|
+
}
|
|
575
|
+
const pending = new Map();
|
|
576
|
+
const order = [];
|
|
577
|
+
for (const ref of run.sandboxes ?? []) {
|
|
578
|
+
order.push(ref.sandbox_id);
|
|
579
|
+
pending.set(ref.sandbox_id, deferred());
|
|
580
|
+
}
|
|
581
|
+
const settle = (ref, index) => {
|
|
582
|
+
const d = pending.get(ref.sandbox_id);
|
|
583
|
+
if (!d || d.settled)
|
|
584
|
+
return;
|
|
585
|
+
if (ref.state === "ended") {
|
|
586
|
+
d.reject(new EnvFailedError("env_failed", `sandbox ${ref.sandbox_id} ended before hand-over`, `Not billed. Read the evidence: \`promigence run report ${hintArg(run.run_id)}\`.`));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const sb = mk(ref);
|
|
590
|
+
d.resolve(sb);
|
|
591
|
+
opts.onReady?.(sb, index);
|
|
592
|
+
};
|
|
593
|
+
(run.sandboxes ?? []).forEach((ref, i) => { if (!isWaiting(ref.state))
|
|
594
|
+
settle(ref, i); });
|
|
595
|
+
if ([...pending.values()].some((d) => !d.settled)) {
|
|
596
|
+
const bound = opts.waitTimeoutMs ?? WAIT_TIMEOUT_MS;
|
|
597
|
+
void client.runs
|
|
598
|
+
.waitReady(run.run_id, {
|
|
599
|
+
waitTimeoutMs: bound, onReady: (ref) => settle(ref, order.indexOf(ref.sandbox_id)),
|
|
600
|
+
...(extra.signal === undefined ? {} : { signal: extra.signal }),
|
|
601
|
+
})
|
|
602
|
+
.then((res) => {
|
|
603
|
+
if (!res.timedOut)
|
|
604
|
+
return;
|
|
605
|
+
for (const [id, d] of pending) {
|
|
606
|
+
if (d.settled)
|
|
607
|
+
continue;
|
|
608
|
+
d.reject(new TimeoutError(`sandbox ${id} was still queued after ${bound} ms; nothing was killed`, `Run the id-based command: \`promigence run status ${hintArg(run.run_id)} --wait\` or \`promigence run report ${hintArg(run.run_id)}\`.`, { sandboxId: id, runId: run.run_id }));
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
.catch((err) => {
|
|
612
|
+
for (const d of pending.values())
|
|
613
|
+
if (!d.settled)
|
|
614
|
+
d.reject(err);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return { runId: run.run_id, handles: order.map((id) => pending.get(id).promise) };
|
|
618
|
+
}
|
|
619
|
+
static async connect(sandboxId, opts = {}) {
|
|
620
|
+
checkSandboxId(sandboxId, "Sandbox.connect");
|
|
621
|
+
const client = opts.client ?? new PromigenceClient({
|
|
622
|
+
...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
|
|
623
|
+
...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
|
|
624
|
+
});
|
|
625
|
+
const rec = await client.sandboxes.get(sandboxId);
|
|
626
|
+
return new Sandbox({
|
|
627
|
+
sandboxId: rec.sandbox_id ?? sandboxId,
|
|
628
|
+
runId: rec.run_id ?? "",
|
|
629
|
+
snapshot: rec.snapshot_id ?? "",
|
|
630
|
+
client,
|
|
631
|
+
...(opts.envs === undefined ? {} : { envs: opts.envs }),
|
|
632
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
static async list(opts = {}) {
|
|
636
|
+
return (await Sandbox.listPage(opts)).items;
|
|
637
|
+
}
|
|
638
|
+
static async listPage(opts = {}) {
|
|
639
|
+
const c = opts.client ?? new PromigenceClient();
|
|
640
|
+
return c.sandboxes.listPage({
|
|
641
|
+
limit: opts.limit ?? 50,
|
|
642
|
+
...(opts.labels ? { labels: opts.labels } : {}),
|
|
643
|
+
...(opts.state?.length ? { state: opts.state } : {}),
|
|
644
|
+
...(opts.cursor ? { cursor: opts.cursor } : {}),
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async fork(opts = {}) {
|
|
648
|
+
if (this.dirty && opts.fromOrigin !== true) {
|
|
649
|
+
throw new UnsupportedError("sandbox.fork() on a sandbox that has changed since it started", `To fork it as it is now: \`const s = await client.sandboxes.snapshot(${JSON.stringify(this.sandboxId)})\` (a live snapshot; it ends background commands), then \`Sandbox.fork(s.snapshot_id, { count })\`. To fork the snapshot it came from instead, pass { fromOrigin: true }.`);
|
|
650
|
+
}
|
|
651
|
+
let origin = this.snapshot;
|
|
652
|
+
if (!origin && this.runId)
|
|
653
|
+
origin = (await this.#client.runs.get(this.runId)).snapshot_id ?? "";
|
|
654
|
+
if (!origin) {
|
|
655
|
+
throw new UnsupportedError("sandbox.fork() without a known origin snapshot", "Use Sandbox.fork('<snapshot>', { count }) or client.runs.start({ snapshot, count }).");
|
|
656
|
+
}
|
|
657
|
+
return Sandbox.fork(origin, {
|
|
658
|
+
client: this.#client,
|
|
659
|
+
count: opts.count ?? 1,
|
|
660
|
+
...(opts.waitTimeoutMs === undefined ? {} : { waitTimeoutMs: opts.waitTimeoutMs }),
|
|
661
|
+
...(opts.onReady === undefined ? {} : { onReady: opts.onReady }),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
toJSON() {
|
|
665
|
+
return { sandboxId: this.sandboxId, runId: this.runId, snapshot: this.snapshot };
|
|
666
|
+
}
|
|
667
|
+
[inspect.custom](_depth, opts) {
|
|
668
|
+
return `Sandbox ${inspect(this.toJSON(), opts)}`;
|
|
669
|
+
}
|
|
670
|
+
async kill() {
|
|
671
|
+
const r = await this.#client.sandboxes.kill(this.sandboxId);
|
|
672
|
+
return r?.already_ended !== true;
|
|
673
|
+
}
|
|
674
|
+
async getInfo() {
|
|
675
|
+
return this.#client.sandboxes.get(this.sandboxId);
|
|
676
|
+
}
|
|
677
|
+
probe(opts = {}) {
|
|
678
|
+
return this.#client.sandboxes.probe(this.sandboxId, opts);
|
|
679
|
+
}
|
|
680
|
+
async isRunning() {
|
|
681
|
+
const rec = await this.#client.sandboxes.get(this.sandboxId);
|
|
682
|
+
return rec.state === "ready" || rec.state === "running";
|
|
683
|
+
}
|
|
684
|
+
async setTimeout(ms) {
|
|
685
|
+
await this.#client.sandboxes.patch(this.sandboxId, { timeoutMs: ms });
|
|
686
|
+
this.timeoutMs = ms;
|
|
687
|
+
}
|
|
688
|
+
async setAutoPause(seconds, mode) {
|
|
689
|
+
await this.#client.sandboxes.patch(this.sandboxId, {
|
|
690
|
+
autoPauseS: seconds,
|
|
691
|
+
...(mode !== undefined ? { autoPauseMode: mode } : {}),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async getAutoPause() {
|
|
695
|
+
const rec = await this.#client.sandboxes.get(this.sandboxId);
|
|
696
|
+
return Number(rec.auto_pause_s ?? 0);
|
|
697
|
+
}
|
|
698
|
+
async disableInternet() {
|
|
699
|
+
await this.#client.sandboxes.setNetworkNone(this.sandboxId);
|
|
700
|
+
}
|
|
701
|
+
async getHost(port) {
|
|
702
|
+
const existing = (await withPreviewsFix(this.#client.sandboxes.ports.list(this.sandboxId), this.sandboxId, port)).find((p) => p.port === port);
|
|
703
|
+
const rec = existing ?? await this.#client.sandboxes.ports.expose(this.sandboxId, port);
|
|
704
|
+
return rec.url.replace(/^[a-z]+:\/\//i, "").replace(/\/$/, "");
|
|
705
|
+
}
|
|
706
|
+
async pause() {
|
|
707
|
+
const r = await this.#client.sandboxes.pause(this.sandboxId);
|
|
708
|
+
return {
|
|
709
|
+
pausedId: r.paused_id,
|
|
710
|
+
...(r.background_execs_killed === undefined ? {} : { backgroundExecsKilled: r.background_execs_killed }),
|
|
711
|
+
...(r.warnings?.length ? { warnings: r.warnings } : {}),
|
|
712
|
+
...(r.already_paused === true ? { alreadyPaused: true } : {}),
|
|
713
|
+
...(r.took_ms === undefined ? {} : { tookMs: r.took_ms }),
|
|
714
|
+
...(r.took_ms === undefined ? {} : { pauseMs: r.took_ms }),
|
|
715
|
+
...(r.bytes === undefined ? {} : { bytes: r.bytes }),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
async resume(opts = {}) {
|
|
719
|
+
const r = await this.#client.sandboxes.resume(this.sandboxId, opts);
|
|
720
|
+
return {
|
|
721
|
+
...(r.took_ms === undefined ? {} : { tookMs: r.took_ms }),
|
|
722
|
+
...(r.took_ms === undefined ? {} : { resumeMs: r.took_ms }),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|