tensorlake 0.4.50 → 0.5.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/dist/bin/darwin-arm64/tensorlake +0 -0
- package/dist/bin/darwin-arm64/tl +0 -0
- package/dist/bin/linux-x64/tensorlake +0 -0
- package/dist/bin/linux-x64/tl +0 -0
- package/dist/bin/win32-x64/tensorlake.exe +0 -0
- package/dist/bin/win32-x64/tl.exe +0 -0
- package/dist/index.cjs +388 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +210 -12
- package/dist/index.d.ts +210 -12
- package/dist/index.js +388 -49
- package/dist/index.js.map +1 -1
- package/dist/{sandbox-image-C79FXqQk.d.cts → sandbox-image-CEGsGg4V.d.cts} +22 -2
- package/dist/{sandbox-image-C79FXqQk.d.ts → sandbox-image-CEGsGg4V.d.ts} +22 -2
- package/dist/sandbox-image.cjs +375 -36
- package/dist/sandbox-image.cjs.map +1 -1
- package/dist/sandbox-image.d.cts +1 -1
- package/dist/sandbox-image.d.ts +1 -1
- package/dist/sandbox-image.js +380 -36
- package/dist/sandbox-image.js.map +1 -1
- package/package.json +1 -1
package/dist/sandbox-image.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
1
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
3
|
var __esm = (fn, res) => function __init() {
|
|
3
4
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
4
5
|
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
5
10
|
|
|
6
11
|
// src/models.ts
|
|
7
12
|
function snakeToCamel(str) {
|
|
@@ -52,10 +57,11 @@ var init_models = __esm({
|
|
|
52
57
|
});
|
|
53
58
|
|
|
54
59
|
// src/defaults.ts
|
|
55
|
-
var API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
|
|
60
|
+
var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
|
|
56
61
|
var init_defaults = __esm({
|
|
57
62
|
"src/defaults.ts"() {
|
|
58
63
|
"use strict";
|
|
64
|
+
SDK_VERSION = "0.4.49";
|
|
59
65
|
API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
|
|
60
66
|
API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
|
|
61
67
|
NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
|
|
@@ -134,6 +140,12 @@ import {
|
|
|
134
140
|
fetch as undiciFetch,
|
|
135
141
|
setGlobalDispatcher
|
|
136
142
|
} from "undici";
|
|
143
|
+
function withTraceId(value, traceId) {
|
|
144
|
+
if (value == null) {
|
|
145
|
+
return { traceId };
|
|
146
|
+
}
|
|
147
|
+
return Object.assign(value, { traceId });
|
|
148
|
+
}
|
|
137
149
|
function hasHeader(headers, name) {
|
|
138
150
|
const lowered = name.toLowerCase();
|
|
139
151
|
return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
|
|
@@ -202,7 +214,7 @@ var init_http = __esm({
|
|
|
202
214
|
allowH2: true
|
|
203
215
|
})
|
|
204
216
|
);
|
|
205
|
-
HttpClient = class {
|
|
217
|
+
HttpClient = class _HttpClient {
|
|
206
218
|
baseUrl;
|
|
207
219
|
headers;
|
|
208
220
|
maxRetries;
|
|
@@ -215,7 +227,9 @@ var init_http = __esm({
|
|
|
215
227
|
this.maxRetries = options.maxRetries ?? MAX_RETRIES;
|
|
216
228
|
this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
|
|
217
229
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
218
|
-
this.headers = {
|
|
230
|
+
this.headers = {
|
|
231
|
+
"User-Agent": `tensorlake-typescript-sdk/${SDK_VERSION}`
|
|
232
|
+
};
|
|
219
233
|
if (options.apiKey) {
|
|
220
234
|
this.headers["Authorization"] = `Bearer ${options.apiKey}`;
|
|
221
235
|
}
|
|
@@ -244,8 +258,8 @@ var init_http = __esm({
|
|
|
244
258
|
signal: options?.signal
|
|
245
259
|
});
|
|
246
260
|
const text = await response.text();
|
|
247
|
-
|
|
248
|
-
return
|
|
261
|
+
const data = text ? JSON.parse(text) : void 0;
|
|
262
|
+
return withTraceId(data, response.traceId);
|
|
249
263
|
}
|
|
250
264
|
/** Make a request returning raw bytes. */
|
|
251
265
|
async requestBytes(method, path2, options) {
|
|
@@ -259,7 +273,7 @@ var init_http = __esm({
|
|
|
259
273
|
signal: options?.signal
|
|
260
274
|
});
|
|
261
275
|
const buffer = await response.arrayBuffer();
|
|
262
|
-
return new Uint8Array(buffer);
|
|
276
|
+
return withTraceId(new Uint8Array(buffer), response.traceId);
|
|
263
277
|
}
|
|
264
278
|
/** Make a request and return the response body as an SSE stream. */
|
|
265
279
|
async requestStream(method, path2, options) {
|
|
@@ -274,7 +288,7 @@ var init_http = __esm({
|
|
|
274
288
|
"No response body for SSE stream"
|
|
275
289
|
);
|
|
276
290
|
}
|
|
277
|
-
return response.body;
|
|
291
|
+
return withTraceId(response.body, response.traceId);
|
|
278
292
|
}
|
|
279
293
|
/** Make a request and return the raw Response. */
|
|
280
294
|
async requestResponse(method, path2, options) {
|
|
@@ -287,7 +301,7 @@ var init_http = __esm({
|
|
|
287
301
|
headers["Content-Type"] = "application/json";
|
|
288
302
|
}
|
|
289
303
|
const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
|
|
290
|
-
|
|
304
|
+
const { response, traceId } = await this.doFetch(
|
|
291
305
|
method,
|
|
292
306
|
path2,
|
|
293
307
|
body,
|
|
@@ -295,9 +309,18 @@ var init_http = __esm({
|
|
|
295
309
|
options?.signal,
|
|
296
310
|
options?.allowHttpErrors ?? false
|
|
297
311
|
);
|
|
312
|
+
return withTraceId(response, traceId);
|
|
313
|
+
}
|
|
314
|
+
static makeTraceparent() {
|
|
315
|
+
const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
316
|
+
const traceId = randomHex(16);
|
|
317
|
+
const spanId = randomHex(8);
|
|
318
|
+
return { traceparent: `00-${traceId}-${spanId}-01`, traceId };
|
|
298
319
|
}
|
|
299
320
|
async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
|
|
300
321
|
const url = `${this.baseUrl}${path2}`;
|
|
322
|
+
const { traceparent, traceId } = _HttpClient.makeTraceparent();
|
|
323
|
+
headers["traceparent"] = traceparent;
|
|
301
324
|
let lastError;
|
|
302
325
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
303
326
|
if (attempt > 0) {
|
|
@@ -318,7 +341,7 @@ var init_http = __esm({
|
|
|
318
341
|
signal: combinedSignal
|
|
319
342
|
});
|
|
320
343
|
clearTimeout(timeoutId);
|
|
321
|
-
if (response.ok) return response;
|
|
344
|
+
if (response.ok) return { response, traceId };
|
|
322
345
|
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
|
|
323
346
|
lastError = new RemoteAPIError(
|
|
324
347
|
response.status,
|
|
@@ -327,7 +350,7 @@ var init_http = __esm({
|
|
|
327
350
|
continue;
|
|
328
351
|
}
|
|
329
352
|
if (allowHttpErrors) {
|
|
330
|
-
return response;
|
|
353
|
+
return { response, traceId };
|
|
331
354
|
}
|
|
332
355
|
const errorBody = await response.text().catch(() => "");
|
|
333
356
|
throwMappedError(response.status, errorBody, path2);
|
|
@@ -3074,13 +3097,17 @@ var init_sandbox = __esm({
|
|
|
3074
3097
|
};
|
|
3075
3098
|
Sandbox = class {
|
|
3076
3099
|
sandboxId;
|
|
3100
|
+
traceId = null;
|
|
3077
3101
|
http;
|
|
3078
3102
|
baseUrl;
|
|
3079
3103
|
wsHeaders;
|
|
3080
3104
|
ownsSandbox = false;
|
|
3081
3105
|
lifecycleClient = null;
|
|
3106
|
+
lifecycleIdentifier;
|
|
3107
|
+
sandboxName = null;
|
|
3082
3108
|
constructor(options) {
|
|
3083
3109
|
this.sandboxId = options.sandboxId;
|
|
3110
|
+
this.lifecycleIdentifier = options.sandboxId;
|
|
3084
3111
|
const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
|
|
3085
3112
|
const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
|
|
3086
3113
|
this.baseUrl = baseUrl;
|
|
@@ -3106,21 +3133,182 @@ var init_sandbox = __esm({
|
|
|
3106
3133
|
routingHint: options.routingHint
|
|
3107
3134
|
});
|
|
3108
3135
|
}
|
|
3136
|
+
get name() {
|
|
3137
|
+
return this.sandboxName;
|
|
3138
|
+
}
|
|
3139
|
+
/** @internal Used by client wiring to keep locally cached name in sync. */
|
|
3140
|
+
_setName(name) {
|
|
3141
|
+
this.sandboxName = name;
|
|
3142
|
+
}
|
|
3143
|
+
/** @internal Used by lifecycle operations to pin to canonical sandbox ID. */
|
|
3144
|
+
_setLifecycleIdentifier(identifier) {
|
|
3145
|
+
this.lifecycleIdentifier = identifier;
|
|
3146
|
+
}
|
|
3109
3147
|
/** @internal Used by SandboxClient.createAndConnect to set ownership. */
|
|
3110
3148
|
_setOwner(client) {
|
|
3111
3149
|
this.ownsSandbox = true;
|
|
3112
3150
|
this.lifecycleClient = client;
|
|
3113
3151
|
}
|
|
3152
|
+
// --- Static factory methods ---
|
|
3153
|
+
/**
|
|
3154
|
+
* Create a new sandbox and return a connected, running handle.
|
|
3155
|
+
*
|
|
3156
|
+
* Covers both fresh sandbox creation and restore-from-snapshot (set
|
|
3157
|
+
* `snapshotId`). Blocks until the sandbox is `Running`.
|
|
3158
|
+
*/
|
|
3159
|
+
static async create(options) {
|
|
3160
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3161
|
+
const client = new SandboxClient2(
|
|
3162
|
+
options,
|
|
3163
|
+
/* _internal */
|
|
3164
|
+
true
|
|
3165
|
+
);
|
|
3166
|
+
const sandbox = await client.createAndConnect(options);
|
|
3167
|
+
sandbox.lifecycleClient = client;
|
|
3168
|
+
return sandbox;
|
|
3169
|
+
}
|
|
3170
|
+
/**
|
|
3171
|
+
* Attach to an existing sandbox and return a connected handle.
|
|
3172
|
+
*
|
|
3173
|
+
* Verifies the sandbox exists via a server GET call, then returns a handle
|
|
3174
|
+
* in whatever state the sandbox is in. Does **not** auto-resume a suspended
|
|
3175
|
+
* sandbox — call `sandbox.resume()` explicitly.
|
|
3176
|
+
*/
|
|
3177
|
+
static async connect(options) {
|
|
3178
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3179
|
+
const client = new SandboxClient2(
|
|
3180
|
+
options,
|
|
3181
|
+
/* _internal */
|
|
3182
|
+
true
|
|
3183
|
+
);
|
|
3184
|
+
const info = await client.get(options.sandboxId);
|
|
3185
|
+
const sandbox = client.connect(
|
|
3186
|
+
info.sandboxId,
|
|
3187
|
+
options.proxyUrl,
|
|
3188
|
+
options.routingHint ?? info.routingHint
|
|
3189
|
+
);
|
|
3190
|
+
sandbox.lifecycleClient = client;
|
|
3191
|
+
sandbox._setLifecycleIdentifier(info.sandboxId);
|
|
3192
|
+
sandbox._setName(info.name ?? null);
|
|
3193
|
+
return sandbox;
|
|
3194
|
+
}
|
|
3195
|
+
// --- Static snapshot management ---
|
|
3196
|
+
/** Get information about a snapshot by ID. No sandbox handle needed. */
|
|
3197
|
+
static async getSnapshot(snapshotId, options) {
|
|
3198
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3199
|
+
const client = new SandboxClient2(
|
|
3200
|
+
options,
|
|
3201
|
+
/* _internal */
|
|
3202
|
+
true
|
|
3203
|
+
);
|
|
3204
|
+
return client.getSnapshot(snapshotId);
|
|
3205
|
+
}
|
|
3206
|
+
/** Delete a snapshot by ID. No sandbox handle needed. */
|
|
3207
|
+
static async deleteSnapshot(snapshotId, options) {
|
|
3208
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3209
|
+
const client = new SandboxClient2(
|
|
3210
|
+
options,
|
|
3211
|
+
/* _internal */
|
|
3212
|
+
true
|
|
3213
|
+
);
|
|
3214
|
+
await client.deleteSnapshot(snapshotId);
|
|
3215
|
+
}
|
|
3216
|
+
// --- Instance lifecycle methods ---
|
|
3217
|
+
requireLifecycleClient(operation) {
|
|
3218
|
+
if (!this.lifecycleClient) {
|
|
3219
|
+
throw new SandboxError(
|
|
3220
|
+
`Cannot ${operation}: no lifecycle client available. Use Sandbox.create() or Sandbox.connect() to get a lifecycle-aware handle.`
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
return this.lifecycleClient;
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Fetch the current sandbox status from the server.
|
|
3227
|
+
*
|
|
3228
|
+
* Always hits the network — the value is not cached locally because the
|
|
3229
|
+
* status changes over the sandbox's lifecycle.
|
|
3230
|
+
*/
|
|
3231
|
+
async status() {
|
|
3232
|
+
const client = this.requireLifecycleClient("read_status");
|
|
3233
|
+
const info = await client.get(this.lifecycleIdentifier);
|
|
3234
|
+
this._setLifecycleIdentifier(info.sandboxId);
|
|
3235
|
+
this._setName(info.name ?? null);
|
|
3236
|
+
return info.status;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Update this sandbox's properties (name, exposed ports, proxy auth).
|
|
3240
|
+
*
|
|
3241
|
+
* Naming an ephemeral sandbox makes it non-ephemeral and enables
|
|
3242
|
+
* suspend/resume.
|
|
3243
|
+
*/
|
|
3244
|
+
async update(options) {
|
|
3245
|
+
const client = this.requireLifecycleClient("update");
|
|
3246
|
+
const info = await client.update(this.lifecycleIdentifier, options);
|
|
3247
|
+
this._setLifecycleIdentifier(info.sandboxId);
|
|
3248
|
+
this._setName(info.name ?? null);
|
|
3249
|
+
return info;
|
|
3250
|
+
}
|
|
3251
|
+
/**
|
|
3252
|
+
* Suspend this sandbox.
|
|
3253
|
+
*
|
|
3254
|
+
* By default blocks until the sandbox is fully `Suspended`. Pass
|
|
3255
|
+
* `{ wait: false }` for fire-and-return.
|
|
3256
|
+
*/
|
|
3257
|
+
async suspend(options) {
|
|
3258
|
+
const client = this.requireLifecycleClient("suspend");
|
|
3259
|
+
await client.suspend(this.lifecycleIdentifier, options);
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Resume this sandbox.
|
|
3263
|
+
*
|
|
3264
|
+
* By default blocks until the sandbox is `Running` and routable. Pass
|
|
3265
|
+
* `{ wait: false }` for fire-and-return.
|
|
3266
|
+
*/
|
|
3267
|
+
async resume(options) {
|
|
3268
|
+
const client = this.requireLifecycleClient("resume");
|
|
3269
|
+
await client.resume(this.lifecycleIdentifier, options);
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Create a snapshot of this sandbox's filesystem and wait for it to
|
|
3273
|
+
* be committed.
|
|
3274
|
+
*
|
|
3275
|
+
* By default blocks until the snapshot artifact is ready and returns
|
|
3276
|
+
* the completed `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
|
|
3277
|
+
* (returns `undefined`).
|
|
3278
|
+
*/
|
|
3279
|
+
async checkpoint(options) {
|
|
3280
|
+
const client = this.requireLifecycleClient("checkpoint");
|
|
3281
|
+
if (options?.wait === false) {
|
|
3282
|
+
await client.snapshot(this.lifecycleIdentifier, { contentMode: options.contentMode });
|
|
3283
|
+
return void 0;
|
|
3284
|
+
}
|
|
3285
|
+
return client.snapshotAndWait(this.lifecycleIdentifier, {
|
|
3286
|
+
timeout: options?.timeout,
|
|
3287
|
+
pollInterval: options?.pollInterval,
|
|
3288
|
+
contentMode: options?.contentMode
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* List snapshots taken from this sandbox.
|
|
3293
|
+
*/
|
|
3294
|
+
async listSnapshots() {
|
|
3295
|
+
const client = this.requireLifecycleClient("listSnapshots");
|
|
3296
|
+
const all = await client.listSnapshots();
|
|
3297
|
+
const filtered = all.filter((s) => s.sandboxId === this.lifecycleIdentifier);
|
|
3298
|
+
return Object.assign(filtered, { traceId: all.traceId });
|
|
3299
|
+
}
|
|
3300
|
+
/** Close the HTTP client. The sandbox keeps running. */
|
|
3114
3301
|
close() {
|
|
3115
3302
|
this.http.close();
|
|
3116
3303
|
}
|
|
3304
|
+
/** Terminate the sandbox and release all resources. */
|
|
3117
3305
|
async terminate() {
|
|
3118
3306
|
const client = this.lifecycleClient;
|
|
3119
3307
|
this.ownsSandbox = false;
|
|
3120
3308
|
this.lifecycleClient = null;
|
|
3121
3309
|
this.close();
|
|
3122
3310
|
if (client) {
|
|
3123
|
-
await client.delete(this.
|
|
3311
|
+
await client.delete(this.lifecycleIdentifier);
|
|
3124
3312
|
}
|
|
3125
3313
|
}
|
|
3126
3314
|
// --- High-level convenience ---
|
|
@@ -3166,6 +3354,14 @@ var init_sandbox = __esm({
|
|
|
3166
3354
|
};
|
|
3167
3355
|
}
|
|
3168
3356
|
// --- Process management ---
|
|
3357
|
+
/**
|
|
3358
|
+
* Start a process in the sandbox without waiting for it to exit.
|
|
3359
|
+
*
|
|
3360
|
+
* Returns a `ProcessInfo` with the assigned `pid`. Use `getProcess()` to
|
|
3361
|
+
* poll status, or `followStdout()` / `followOutput()` to stream output
|
|
3362
|
+
* until the process exits. Use `run()` instead to block until completion
|
|
3363
|
+
* and get combined output in one call.
|
|
3364
|
+
*/
|
|
3169
3365
|
async startProcess(command, options) {
|
|
3170
3366
|
const payload = { command };
|
|
3171
3367
|
if (options?.args != null) payload.args = options.args;
|
|
@@ -3187,13 +3383,16 @@ var init_sandbox = __esm({
|
|
|
3187
3383
|
);
|
|
3188
3384
|
return fromSnakeKeys(raw);
|
|
3189
3385
|
}
|
|
3386
|
+
/** List all processes (running and exited) tracked by the sandbox daemon. */
|
|
3190
3387
|
async listProcesses() {
|
|
3191
3388
|
const raw = await this.http.requestJson(
|
|
3192
3389
|
"GET",
|
|
3193
3390
|
"/api/v1/processes"
|
|
3194
3391
|
);
|
|
3195
|
-
|
|
3392
|
+
const processes = (raw.processes ?? []).map((p) => fromSnakeKeys(p));
|
|
3393
|
+
return Object.assign(processes, { traceId: raw.traceId });
|
|
3196
3394
|
}
|
|
3395
|
+
/** Get current status and metadata for a process by PID. */
|
|
3197
3396
|
async getProcess(pid) {
|
|
3198
3397
|
const raw = await this.http.requestJson(
|
|
3199
3398
|
"GET",
|
|
@@ -3201,9 +3400,11 @@ var init_sandbox = __esm({
|
|
|
3201
3400
|
);
|
|
3202
3401
|
return fromSnakeKeys(raw);
|
|
3203
3402
|
}
|
|
3403
|
+
/** Send SIGKILL to a process. */
|
|
3204
3404
|
async killProcess(pid) {
|
|
3205
3405
|
await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
|
|
3206
3406
|
}
|
|
3407
|
+
/** Send an arbitrary signal to a process (e.g. `15` for SIGTERM, `9` for SIGKILL). */
|
|
3207
3408
|
async sendSignal(pid, signal) {
|
|
3208
3409
|
const raw = await this.http.requestJson(
|
|
3209
3410
|
"POST",
|
|
@@ -3213,15 +3414,18 @@ var init_sandbox = __esm({
|
|
|
3213
3414
|
return fromSnakeKeys(raw);
|
|
3214
3415
|
}
|
|
3215
3416
|
// --- Process I/O ---
|
|
3417
|
+
/** Write bytes to a process's stdin. The process must have been started with `stdinMode: StdinMode.PIPE`. */
|
|
3216
3418
|
async writeStdin(pid, data) {
|
|
3217
3419
|
await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
|
|
3218
3420
|
body: data,
|
|
3219
3421
|
contentType: "application/octet-stream"
|
|
3220
3422
|
});
|
|
3221
3423
|
}
|
|
3424
|
+
/** Close a process's stdin pipe, signalling EOF to the process. */
|
|
3222
3425
|
async closeStdin(pid) {
|
|
3223
3426
|
await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
|
|
3224
3427
|
}
|
|
3428
|
+
/** Return all captured stdout lines produced so far by a process. */
|
|
3225
3429
|
async getStdout(pid) {
|
|
3226
3430
|
const raw = await this.http.requestJson(
|
|
3227
3431
|
"GET",
|
|
@@ -3229,6 +3433,7 @@ var init_sandbox = __esm({
|
|
|
3229
3433
|
);
|
|
3230
3434
|
return fromSnakeKeys(raw);
|
|
3231
3435
|
}
|
|
3436
|
+
/** Return all captured stderr lines produced so far by a process. */
|
|
3232
3437
|
async getStderr(pid) {
|
|
3233
3438
|
const raw = await this.http.requestJson(
|
|
3234
3439
|
"GET",
|
|
@@ -3236,6 +3441,7 @@ var init_sandbox = __esm({
|
|
|
3236
3441
|
);
|
|
3237
3442
|
return fromSnakeKeys(raw);
|
|
3238
3443
|
}
|
|
3444
|
+
/** Return all captured stdout+stderr lines produced so far by a process. */
|
|
3239
3445
|
async getOutput(pid) {
|
|
3240
3446
|
const raw = await this.http.requestJson(
|
|
3241
3447
|
"GET",
|
|
@@ -3244,6 +3450,7 @@ var init_sandbox = __esm({
|
|
|
3244
3450
|
return fromSnakeKeys(raw);
|
|
3245
3451
|
}
|
|
3246
3452
|
// --- Streaming (SSE) ---
|
|
3453
|
+
/** Stream stdout events from a process until it exits. Yields one `OutputEvent` per line. */
|
|
3247
3454
|
async *followStdout(pid, options) {
|
|
3248
3455
|
const stream = await this.http.requestStream(
|
|
3249
3456
|
"GET",
|
|
@@ -3257,6 +3464,7 @@ var init_sandbox = __esm({
|
|
|
3257
3464
|
yield fromSnakeKeys(raw);
|
|
3258
3465
|
}
|
|
3259
3466
|
}
|
|
3467
|
+
/** Stream stderr events from a process until it exits. Yields one `OutputEvent` per line. */
|
|
3260
3468
|
async *followStderr(pid, options) {
|
|
3261
3469
|
const stream = await this.http.requestStream(
|
|
3262
3470
|
"GET",
|
|
@@ -3270,6 +3478,7 @@ var init_sandbox = __esm({
|
|
|
3270
3478
|
yield fromSnakeKeys(raw);
|
|
3271
3479
|
}
|
|
3272
3480
|
}
|
|
3481
|
+
/** Stream combined stdout+stderr events from a process until it exits. Yields one `OutputEvent` per line. */
|
|
3273
3482
|
async *followOutput(pid, options) {
|
|
3274
3483
|
const stream = await this.http.requestStream(
|
|
3275
3484
|
"GET",
|
|
@@ -3284,12 +3493,14 @@ var init_sandbox = __esm({
|
|
|
3284
3493
|
}
|
|
3285
3494
|
}
|
|
3286
3495
|
// --- File operations ---
|
|
3496
|
+
/** Read a file from the sandbox and return its raw bytes. */
|
|
3287
3497
|
async readFile(path2) {
|
|
3288
3498
|
return this.http.requestBytes(
|
|
3289
3499
|
"GET",
|
|
3290
3500
|
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
3291
3501
|
);
|
|
3292
3502
|
}
|
|
3503
|
+
/** Write raw bytes to a file in the sandbox, creating it if it does not exist. */
|
|
3293
3504
|
async writeFile(path2, content) {
|
|
3294
3505
|
await this.http.requestBytes(
|
|
3295
3506
|
"PUT",
|
|
@@ -3297,12 +3508,14 @@ var init_sandbox = __esm({
|
|
|
3297
3508
|
{ body: content, contentType: "application/octet-stream" }
|
|
3298
3509
|
);
|
|
3299
3510
|
}
|
|
3511
|
+
/** Delete a file from the sandbox. */
|
|
3300
3512
|
async deleteFile(path2) {
|
|
3301
3513
|
await this.http.requestJson(
|
|
3302
3514
|
"DELETE",
|
|
3303
3515
|
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
3304
3516
|
);
|
|
3305
3517
|
}
|
|
3518
|
+
/** List the contents of a directory in the sandbox. */
|
|
3306
3519
|
async listDirectory(path2) {
|
|
3307
3520
|
const raw = await this.http.requestJson(
|
|
3308
3521
|
"GET",
|
|
@@ -3311,6 +3524,7 @@ var init_sandbox = __esm({
|
|
|
3311
3524
|
return fromSnakeKeys(raw);
|
|
3312
3525
|
}
|
|
3313
3526
|
// --- PTY ---
|
|
3527
|
+
/** Create an interactive PTY session. Returns a `sessionId` and `token` for WebSocket connection via `connectPty()`. */
|
|
3314
3528
|
async createPtySession(options) {
|
|
3315
3529
|
const payload = {
|
|
3316
3530
|
command: options.command,
|
|
@@ -3327,6 +3541,7 @@ var init_sandbox = __esm({
|
|
|
3327
3541
|
);
|
|
3328
3542
|
return fromSnakeKeys(raw);
|
|
3329
3543
|
}
|
|
3544
|
+
/** Create a PTY session and connect to it immediately. Cleans up the session if the WebSocket connection fails. */
|
|
3330
3545
|
async createPty(options) {
|
|
3331
3546
|
const { onData, onExit, ...createOptions } = options;
|
|
3332
3547
|
const session = await this.createPtySession(createOptions);
|
|
@@ -3340,6 +3555,7 @@ var init_sandbox = __esm({
|
|
|
3340
3555
|
throw error;
|
|
3341
3556
|
}
|
|
3342
3557
|
}
|
|
3558
|
+
/** Attach to an existing PTY session by ID and token and return a connected `Pty` handle. */
|
|
3343
3559
|
async connectPty(sessionId, token, options) {
|
|
3344
3560
|
const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
|
|
3345
3561
|
const authToken = wsUrl.searchParams.get("token") ?? token;
|
|
@@ -3364,6 +3580,7 @@ var init_sandbox = __esm({
|
|
|
3364
3580
|
await pty.connect();
|
|
3365
3581
|
return pty;
|
|
3366
3582
|
}
|
|
3583
|
+
/** Open a TCP tunnel to a port inside the sandbox and return the local listener. */
|
|
3367
3584
|
async createTunnel(remotePort, options) {
|
|
3368
3585
|
return TcpTunnel.listen({
|
|
3369
3586
|
baseUrl: this.baseUrl,
|
|
@@ -3374,6 +3591,7 @@ var init_sandbox = __esm({
|
|
|
3374
3591
|
connectTimeout: options?.connectTimeout
|
|
3375
3592
|
});
|
|
3376
3593
|
}
|
|
3594
|
+
/** Connect to a sandbox VNC session for programmatic desktop control. */
|
|
3377
3595
|
async connectDesktop(options) {
|
|
3378
3596
|
return Desktop.connect({
|
|
3379
3597
|
baseUrl: this.baseUrl,
|
|
@@ -3396,6 +3614,7 @@ var init_sandbox = __esm({
|
|
|
3396
3614
|
return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
|
|
3397
3615
|
}
|
|
3398
3616
|
// --- Health ---
|
|
3617
|
+
/** Check the sandbox daemon health. */
|
|
3399
3618
|
async health() {
|
|
3400
3619
|
const raw = await this.http.requestJson(
|
|
3401
3620
|
"GET",
|
|
@@ -3403,6 +3622,7 @@ var init_sandbox = __esm({
|
|
|
3403
3622
|
);
|
|
3404
3623
|
return fromSnakeKeys(raw);
|
|
3405
3624
|
}
|
|
3625
|
+
/** Get sandbox daemon info (version, uptime, process counts). */
|
|
3406
3626
|
async info() {
|
|
3407
3627
|
const raw = await this.http.requestJson(
|
|
3408
3628
|
"GET",
|
|
@@ -3415,6 +3635,10 @@ var init_sandbox = __esm({
|
|
|
3415
3635
|
});
|
|
3416
3636
|
|
|
3417
3637
|
// src/client.ts
|
|
3638
|
+
var client_exports = {};
|
|
3639
|
+
__export(client_exports, {
|
|
3640
|
+
SandboxClient: () => SandboxClient
|
|
3641
|
+
});
|
|
3418
3642
|
function sleep2(ms) {
|
|
3419
3643
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3420
3644
|
}
|
|
@@ -3451,7 +3675,13 @@ var init_client = __esm({
|
|
|
3451
3675
|
projectId;
|
|
3452
3676
|
namespace;
|
|
3453
3677
|
local;
|
|
3454
|
-
|
|
3678
|
+
/** @internal Pass `true` to suppress the deprecation warning when used by `Sandbox.create()` / `Sandbox.connect()`. */
|
|
3679
|
+
constructor(options, _internal = false) {
|
|
3680
|
+
if (!_internal) {
|
|
3681
|
+
console.warn(
|
|
3682
|
+
"[tensorlake] SandboxClient is deprecated; use Sandbox.create() / Sandbox.connect() instead."
|
|
3683
|
+
);
|
|
3684
|
+
}
|
|
3455
3685
|
this.apiUrl = options?.apiUrl ?? API_URL;
|
|
3456
3686
|
this.apiKey = options?.apiKey ?? API_KEY;
|
|
3457
3687
|
this.organizationId = options?.organizationId;
|
|
@@ -3491,12 +3721,13 @@ var init_client = __esm({
|
|
|
3491
3721
|
return lifecyclePath(subpath, this.local, this.namespace);
|
|
3492
3722
|
}
|
|
3493
3723
|
// --- Sandbox CRUD ---
|
|
3724
|
+
/** Create a new sandbox. Returns immediately; the sandbox may still be starting. Use `createAndConnect()` for a blocking, ready-to-use handle. */
|
|
3494
3725
|
async create(options) {
|
|
3495
3726
|
const body = {
|
|
3496
3727
|
resources: {
|
|
3497
3728
|
cpus: options?.cpus ?? 1,
|
|
3498
3729
|
memory_mb: options?.memoryMb ?? 1024,
|
|
3499
|
-
|
|
3730
|
+
...options?.diskMb != null ? { disk_mb: options.diskMb } : {}
|
|
3500
3731
|
}
|
|
3501
3732
|
};
|
|
3502
3733
|
if (options?.image != null) body.image = options.image;
|
|
@@ -3517,8 +3748,10 @@ var init_client = __esm({
|
|
|
3517
3748
|
this.path("sandboxes"),
|
|
3518
3749
|
{ body }
|
|
3519
3750
|
);
|
|
3520
|
-
|
|
3751
|
+
const result = fromSnakeKeys(raw, "sandboxId");
|
|
3752
|
+
return Object.assign(result, { traceId: raw.traceId });
|
|
3521
3753
|
}
|
|
3754
|
+
/** Get current state and metadata for a sandbox by ID. */
|
|
3522
3755
|
async get(sandboxId) {
|
|
3523
3756
|
const raw = await this.http.requestJson(
|
|
3524
3757
|
"GET",
|
|
@@ -3526,15 +3759,18 @@ var init_client = __esm({
|
|
|
3526
3759
|
);
|
|
3527
3760
|
return fromSnakeKeys(raw, "sandboxId");
|
|
3528
3761
|
}
|
|
3762
|
+
/** List all sandboxes in the namespace. */
|
|
3529
3763
|
async list() {
|
|
3530
3764
|
const raw = await this.http.requestJson(
|
|
3531
3765
|
"GET",
|
|
3532
3766
|
this.path("sandboxes")
|
|
3533
3767
|
);
|
|
3534
|
-
|
|
3768
|
+
const sandboxes = (raw.sandboxes ?? []).map(
|
|
3535
3769
|
(s) => fromSnakeKeys(s, "sandboxId")
|
|
3536
3770
|
);
|
|
3771
|
+
return Object.assign(sandboxes, { traceId: raw.traceId });
|
|
3537
3772
|
}
|
|
3773
|
+
/** Update sandbox properties such as name, exposed ports, and proxy auth settings. */
|
|
3538
3774
|
async update(sandboxId, options) {
|
|
3539
3775
|
const body = {};
|
|
3540
3776
|
if (options.name != null) body.name = options.name;
|
|
@@ -3554,6 +3790,7 @@ var init_client = __esm({
|
|
|
3554
3790
|
);
|
|
3555
3791
|
return fromSnakeKeys(raw, "sandboxId");
|
|
3556
3792
|
}
|
|
3793
|
+
/** Get the current proxy port settings for a sandbox. */
|
|
3557
3794
|
async getPortAccess(sandboxId) {
|
|
3558
3795
|
const info = await this.get(sandboxId);
|
|
3559
3796
|
return {
|
|
@@ -3562,6 +3799,7 @@ var init_client = __esm({
|
|
|
3562
3799
|
sandboxUrl: info.sandboxUrl
|
|
3563
3800
|
};
|
|
3564
3801
|
}
|
|
3802
|
+
/** Add one or more user ports to the sandbox proxy allowlist. */
|
|
3565
3803
|
async exposePorts(sandboxId, ports, options) {
|
|
3566
3804
|
const requestedPorts = normalizeUserPorts(ports);
|
|
3567
3805
|
const current = await this.getPortAccess(sandboxId);
|
|
@@ -3574,6 +3812,7 @@ var init_client = __esm({
|
|
|
3574
3812
|
exposedPorts: desiredPorts
|
|
3575
3813
|
});
|
|
3576
3814
|
}
|
|
3815
|
+
/** Remove one or more user ports from the sandbox proxy allowlist. */
|
|
3577
3816
|
async unexposePorts(sandboxId, ports) {
|
|
3578
3817
|
const requestedPorts = normalizeUserPorts(ports);
|
|
3579
3818
|
const current = await this.getPortAccess(sandboxId);
|
|
@@ -3584,32 +3823,98 @@ var init_client = __esm({
|
|
|
3584
3823
|
exposedPorts: desiredPorts
|
|
3585
3824
|
});
|
|
3586
3825
|
}
|
|
3826
|
+
/** Terminate and delete a sandbox. */
|
|
3587
3827
|
async delete(sandboxId) {
|
|
3588
3828
|
await this.http.requestJson(
|
|
3589
3829
|
"DELETE",
|
|
3590
3830
|
this.path(`sandboxes/${sandboxId}`)
|
|
3591
3831
|
);
|
|
3592
3832
|
}
|
|
3593
|
-
|
|
3833
|
+
/**
|
|
3834
|
+
* Suspend a named sandbox, preserving its state for later resume.
|
|
3835
|
+
*
|
|
3836
|
+
* Only sandboxes created with a `name` can be suspended; ephemeral sandboxes
|
|
3837
|
+
* cannot. By default blocks until the sandbox is fully `Suspended`. Pass
|
|
3838
|
+
* `{ wait: false }` to return immediately after the request is sent
|
|
3839
|
+
* (fire-and-return); the server processes the suspend asynchronously.
|
|
3840
|
+
*
|
|
3841
|
+
* @param sandboxId - ID or name of the sandbox.
|
|
3842
|
+
* @param options.wait - If `true` (default), poll until `Suspended`. Pass `false` to fire-and-return.
|
|
3843
|
+
* @param options.timeout - Max seconds to wait when `wait=true` (default 300).
|
|
3844
|
+
* @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
|
|
3845
|
+
* @throws {SandboxError} If `wait=true` and the sandbox does not reach `Suspended` within `timeout`.
|
|
3846
|
+
*/
|
|
3847
|
+
async suspend(sandboxId, options) {
|
|
3594
3848
|
await this.http.requestResponse(
|
|
3595
3849
|
"POST",
|
|
3596
3850
|
this.path(`sandboxes/${sandboxId}/suspend`)
|
|
3597
3851
|
);
|
|
3852
|
+
if (options?.wait === false) return;
|
|
3853
|
+
const timeout = options?.timeout ?? 300;
|
|
3854
|
+
const pollInterval = options?.pollInterval ?? 1;
|
|
3855
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
3856
|
+
while (Date.now() < deadline) {
|
|
3857
|
+
const info = await this.get(sandboxId);
|
|
3858
|
+
if (info.status === "suspended" /* SUSPENDED */) return;
|
|
3859
|
+
if (info.status === "terminated" /* TERMINATED */) {
|
|
3860
|
+
throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for suspend`);
|
|
3861
|
+
}
|
|
3862
|
+
await sleep2(pollInterval * 1e3);
|
|
3863
|
+
}
|
|
3864
|
+
throw new SandboxError(`Sandbox ${sandboxId} did not suspend within ${timeout}s`);
|
|
3598
3865
|
}
|
|
3599
|
-
|
|
3866
|
+
/**
|
|
3867
|
+
* Resume a suspended sandbox and bring it back to `Running`.
|
|
3868
|
+
*
|
|
3869
|
+
* By default blocks until the sandbox is `Running` and routable. Pass
|
|
3870
|
+
* `{ wait: false }` to return immediately after the request is sent
|
|
3871
|
+
* (fire-and-return); the server processes the resume asynchronously.
|
|
3872
|
+
*
|
|
3873
|
+
* @param sandboxId - ID or name of the sandbox.
|
|
3874
|
+
* @param options.wait - If `true` (default), poll until `Running`. Pass `false` to fire-and-return.
|
|
3875
|
+
* @param options.timeout - Max seconds to wait when `wait=true` (default 300).
|
|
3876
|
+
* @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
|
|
3877
|
+
* @throws {SandboxError} If `wait=true` and the sandbox does not reach `Running` within `timeout`.
|
|
3878
|
+
*/
|
|
3879
|
+
async resume(sandboxId, options) {
|
|
3600
3880
|
await this.http.requestResponse(
|
|
3601
3881
|
"POST",
|
|
3602
3882
|
this.path(`sandboxes/${sandboxId}/resume`)
|
|
3603
3883
|
);
|
|
3884
|
+
if (options?.wait === false) return;
|
|
3885
|
+
const timeout = options?.timeout ?? 300;
|
|
3886
|
+
const pollInterval = options?.pollInterval ?? 1;
|
|
3887
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
3888
|
+
while (Date.now() < deadline) {
|
|
3889
|
+
const info = await this.get(sandboxId);
|
|
3890
|
+
if (info.status === "running" /* RUNNING */) return;
|
|
3891
|
+
if (info.status === "terminated" /* TERMINATED */) {
|
|
3892
|
+
throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for resume`);
|
|
3893
|
+
}
|
|
3894
|
+
await sleep2(pollInterval * 1e3);
|
|
3895
|
+
}
|
|
3896
|
+
throw new SandboxError(`Sandbox ${sandboxId} did not resume within ${timeout}s`);
|
|
3604
3897
|
}
|
|
3898
|
+
/** Claim a warm sandbox from a pool, creating one if no warm containers are available. */
|
|
3605
3899
|
async claim(poolId) {
|
|
3606
3900
|
const raw = await this.http.requestJson(
|
|
3607
3901
|
"POST",
|
|
3608
3902
|
this.path(`sandbox-pools/${poolId}/sandboxes`)
|
|
3609
3903
|
);
|
|
3610
|
-
|
|
3904
|
+
const result = fromSnakeKeys(raw, "sandboxId");
|
|
3905
|
+
return Object.assign(result, { traceId: raw.traceId });
|
|
3611
3906
|
}
|
|
3612
3907
|
// --- Snapshots ---
|
|
3908
|
+
/**
|
|
3909
|
+
* Request a snapshot of a running sandbox's filesystem.
|
|
3910
|
+
*
|
|
3911
|
+
* This call **returns immediately** with a `snapshotId` and `in_progress`
|
|
3912
|
+
* status — the snapshot is created asynchronously. Poll `getSnapshot()` until
|
|
3913
|
+
* `completed` or `failed`, or use `snapshotAndWait()` to block automatically.
|
|
3914
|
+
*
|
|
3915
|
+
* @param options.contentMode - `"filesystem_only"` for cold-boot snapshots (e.g. image builds).
|
|
3916
|
+
* Omit to use the server default (full VM snapshot).
|
|
3917
|
+
*/
|
|
3613
3918
|
async snapshot(sandboxId, options) {
|
|
3614
3919
|
const requestOptions = options?.contentMode != null ? { body: { snapshot_content_mode: options.contentMode } } : void 0;
|
|
3615
3920
|
const raw = await this.http.requestJson(
|
|
@@ -3619,6 +3924,7 @@ var init_client = __esm({
|
|
|
3619
3924
|
);
|
|
3620
3925
|
return fromSnakeKeys(raw, "snapshotId");
|
|
3621
3926
|
}
|
|
3927
|
+
/** Get current status and metadata for a snapshot by ID. */
|
|
3622
3928
|
async getSnapshot(snapshotId) {
|
|
3623
3929
|
const raw = await this.http.requestJson(
|
|
3624
3930
|
"GET",
|
|
@@ -3626,21 +3932,37 @@ var init_client = __esm({
|
|
|
3626
3932
|
);
|
|
3627
3933
|
return fromSnakeKeys(raw, "snapshotId");
|
|
3628
3934
|
}
|
|
3935
|
+
/** List all snapshots in the namespace. */
|
|
3629
3936
|
async listSnapshots() {
|
|
3630
3937
|
const raw = await this.http.requestJson(
|
|
3631
3938
|
"GET",
|
|
3632
3939
|
this.path("snapshots")
|
|
3633
3940
|
);
|
|
3634
|
-
|
|
3941
|
+
const snapshots = (raw.snapshots ?? []).map(
|
|
3635
3942
|
(s) => fromSnakeKeys(s, "snapshotId")
|
|
3636
3943
|
);
|
|
3944
|
+
return Object.assign(snapshots, { traceId: raw.traceId });
|
|
3637
3945
|
}
|
|
3946
|
+
/** Delete a snapshot by ID. */
|
|
3638
3947
|
async deleteSnapshot(snapshotId) {
|
|
3639
3948
|
await this.http.requestJson(
|
|
3640
3949
|
"DELETE",
|
|
3641
3950
|
this.path(`snapshots/${snapshotId}`)
|
|
3642
3951
|
);
|
|
3643
3952
|
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Create a snapshot and block until it is committed.
|
|
3955
|
+
*
|
|
3956
|
+
* Combines `snapshot()` with polling `getSnapshot()` until `completed`.
|
|
3957
|
+
* Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
|
|
3958
|
+
* without managing the client separately.
|
|
3959
|
+
*
|
|
3960
|
+
* @param sandboxId - ID of the running sandbox to snapshot.
|
|
3961
|
+
* @param options.timeout - Max seconds to wait (default 300).
|
|
3962
|
+
* @param options.pollInterval - Seconds between status polls (default 1).
|
|
3963
|
+
* @param options.contentMode - Content mode passed through to `snapshot()`.
|
|
3964
|
+
* @throws {SandboxError} If the snapshot fails or `timeout` elapses.
|
|
3965
|
+
*/
|
|
3644
3966
|
async snapshotAndWait(sandboxId, options) {
|
|
3645
3967
|
const timeout = options?.timeout ?? 300;
|
|
3646
3968
|
const pollInterval = options?.pollInterval ?? 1;
|
|
@@ -3663,6 +3985,7 @@ var init_client = __esm({
|
|
|
3663
3985
|
);
|
|
3664
3986
|
}
|
|
3665
3987
|
// --- Pools ---
|
|
3988
|
+
/** Create a new sandbox pool with warm pre-booted containers. */
|
|
3666
3989
|
async createPool(options) {
|
|
3667
3990
|
const body = {
|
|
3668
3991
|
image: options.image,
|
|
@@ -3684,6 +4007,7 @@ var init_client = __esm({
|
|
|
3684
4007
|
);
|
|
3685
4008
|
return fromSnakeKeys(raw, "poolId");
|
|
3686
4009
|
}
|
|
4010
|
+
/** Get current state and metadata for a sandbox pool by ID. */
|
|
3687
4011
|
async getPool(poolId) {
|
|
3688
4012
|
const raw = await this.http.requestJson(
|
|
3689
4013
|
"GET",
|
|
@@ -3691,15 +4015,18 @@ var init_client = __esm({
|
|
|
3691
4015
|
);
|
|
3692
4016
|
return fromSnakeKeys(raw, "poolId");
|
|
3693
4017
|
}
|
|
4018
|
+
/** List all sandbox pools in the namespace. */
|
|
3694
4019
|
async listPools() {
|
|
3695
4020
|
const raw = await this.http.requestJson(
|
|
3696
4021
|
"GET",
|
|
3697
4022
|
this.path("sandbox-pools")
|
|
3698
4023
|
);
|
|
3699
|
-
|
|
4024
|
+
const pools = (raw.pools ?? []).map(
|
|
3700
4025
|
(p) => fromSnakeKeys(p, "poolId")
|
|
3701
4026
|
);
|
|
4027
|
+
return Object.assign(pools, { traceId: raw.traceId });
|
|
3702
4028
|
}
|
|
4029
|
+
/** Replace the configuration of an existing sandbox pool. */
|
|
3703
4030
|
async updatePool(poolId, options) {
|
|
3704
4031
|
const body = {
|
|
3705
4032
|
image: options.image,
|
|
@@ -3721,6 +4048,7 @@ var init_client = __esm({
|
|
|
3721
4048
|
);
|
|
3722
4049
|
return fromSnakeKeys(raw, "poolId");
|
|
3723
4050
|
}
|
|
4051
|
+
/** Delete a sandbox pool. Fails if the pool has active containers. */
|
|
3724
4052
|
async deletePool(poolId) {
|
|
3725
4053
|
await this.http.requestJson(
|
|
3726
4054
|
"DELETE",
|
|
@@ -3728,6 +4056,7 @@ var init_client = __esm({
|
|
|
3728
4056
|
);
|
|
3729
4057
|
}
|
|
3730
4058
|
// --- Connect ---
|
|
4059
|
+
/** Return a `Sandbox` handle for an existing running sandbox without verifying it exists. */
|
|
3731
4060
|
connect(identifier, proxyUrl, routingHint) {
|
|
3732
4061
|
const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
|
|
3733
4062
|
return new Sandbox({
|
|
@@ -3739,26 +4068,35 @@ var init_client = __esm({
|
|
|
3739
4068
|
routingHint
|
|
3740
4069
|
});
|
|
3741
4070
|
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Create a sandbox, wait for it to reach `Running`, and return a connected handle.
|
|
4073
|
+
*
|
|
4074
|
+
* Blocks until the sandbox is ready or `startupTimeout` elapses. The returned
|
|
4075
|
+
* `Sandbox` auto-terminates when `terminate()` is called.
|
|
4076
|
+
*
|
|
4077
|
+
* @param options.startupTimeout - Max seconds to wait for `Running` status (default 60).
|
|
4078
|
+
* @throws {SandboxError} If the sandbox terminates during startup or the timeout elapses.
|
|
4079
|
+
*/
|
|
3742
4080
|
async createAndConnect(options) {
|
|
3743
4081
|
const startupTimeout = options?.startupTimeout ?? 60;
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
result = await this.create(options);
|
|
3749
|
-
}
|
|
3750
|
-
if (result.status === "running" /* RUNNING */) {
|
|
3751
|
-
const sandbox = this.connect(result.sandboxId, options?.proxyUrl, result.routingHint);
|
|
4082
|
+
const result = options?.poolId != null ? await this.claim(options.poolId) : await this.create(options);
|
|
4083
|
+
const requestedName = options?.poolId != null ? null : options?.name ?? null;
|
|
4084
|
+
const finishConnect = (routingHint, name) => {
|
|
4085
|
+
const sandbox = this.connect(result.sandboxId, options?.proxyUrl, routingHint);
|
|
3752
4086
|
sandbox._setOwner(this);
|
|
4087
|
+
sandbox.traceId = result.traceId;
|
|
4088
|
+
sandbox._setLifecycleIdentifier(result.sandboxId);
|
|
4089
|
+
sandbox._setName(name ?? requestedName);
|
|
3753
4090
|
return sandbox;
|
|
4091
|
+
};
|
|
4092
|
+
if (result.status === "running" /* RUNNING */) {
|
|
4093
|
+
return finishConnect(result.routingHint, result.name);
|
|
3754
4094
|
}
|
|
3755
4095
|
const deadline = Date.now() + startupTimeout * 1e3;
|
|
3756
4096
|
while (Date.now() < deadline) {
|
|
3757
4097
|
const info = await this.get(result.sandboxId);
|
|
3758
4098
|
if (info.status === "running" /* RUNNING */) {
|
|
3759
|
-
|
|
3760
|
-
sandbox._setOwner(this);
|
|
3761
|
-
return sandbox;
|
|
4099
|
+
return finishConnect(info.routingHint, info.name);
|
|
3762
4100
|
}
|
|
3763
4101
|
if (info.status === "terminated" /* TERMINATED */) {
|
|
3764
4102
|
throw new SandboxError(
|
|
@@ -4497,7 +4835,8 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
4497
4835
|
sandbox = await client.createAndConnect({
|
|
4498
4836
|
...plan.baseImage == null ? {} : { image: plan.baseImage },
|
|
4499
4837
|
cpus: options.cpus ?? 2,
|
|
4500
|
-
memoryMb: options.memoryMb ?? 4096
|
|
4838
|
+
memoryMb: options.memoryMb ?? 4096,
|
|
4839
|
+
...options.diskMb != null ? { diskMb: options.diskMb } : {}
|
|
4501
4840
|
});
|
|
4502
4841
|
emit({
|
|
4503
4842
|
type: "status",
|
|
@@ -4510,8 +4849,7 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
4510
4849
|
});
|
|
4511
4850
|
emit({
|
|
4512
4851
|
type: "snapshot_created",
|
|
4513
|
-
snapshot_id: snapshot.snapshotId
|
|
4514
|
-
snapshot_uri: snapshot.snapshotUri ?? null
|
|
4852
|
+
snapshot_id: snapshot.snapshotId
|
|
4515
4853
|
});
|
|
4516
4854
|
if (!snapshot.snapshotUri) {
|
|
4517
4855
|
throw new Error(
|
|
@@ -4555,27 +4893,33 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
4555
4893
|
name: { type: "string", short: "n" },
|
|
4556
4894
|
cpus: { type: "string" },
|
|
4557
4895
|
memory: { type: "string" },
|
|
4896
|
+
disk: { type: "string" },
|
|
4558
4897
|
public: { type: "boolean", default: false }
|
|
4559
4898
|
}
|
|
4560
4899
|
});
|
|
4561
4900
|
const dockerfilePath = parsed.positionals[0];
|
|
4562
4901
|
if (!dockerfilePath) {
|
|
4563
|
-
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
|
|
4902
|
+
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk GB] [--public]");
|
|
4564
4903
|
}
|
|
4565
4904
|
const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
|
|
4566
4905
|
const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
|
|
4906
|
+
const diskGb = parsed.values.disk != null ? Number(parsed.values.disk) : void 0;
|
|
4567
4907
|
if (cpus != null && !Number.isFinite(cpus)) {
|
|
4568
4908
|
throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
|
|
4569
4909
|
}
|
|
4570
4910
|
if (memoryMb != null && !Number.isInteger(memoryMb)) {
|
|
4571
4911
|
throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
|
|
4572
4912
|
}
|
|
4913
|
+
if (diskGb != null && !Number.isInteger(diskGb)) {
|
|
4914
|
+
throw new Error(`Invalid --disk value: ${parsed.values.disk}`);
|
|
4915
|
+
}
|
|
4573
4916
|
await createSandboxImage(
|
|
4574
4917
|
dockerfilePath,
|
|
4575
4918
|
{
|
|
4576
4919
|
registeredName: parsed.values.name,
|
|
4577
4920
|
cpus,
|
|
4578
4921
|
memoryMb,
|
|
4922
|
+
diskMb: diskGb != null ? diskGb * 1024 : void 0,
|
|
4579
4923
|
isPublic: parsed.values.public
|
|
4580
4924
|
},
|
|
4581
4925
|
{ emit: ndjsonStdoutEmit }
|