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/index.js
CHANGED
|
@@ -9,10 +9,11 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/defaults.ts
|
|
12
|
-
var API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
|
|
12
|
+
var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TIMEOUT_MS, MAX_RETRIES, RETRY_BACKOFF_MS, RETRYABLE_STATUS_CODES;
|
|
13
13
|
var init_defaults = __esm({
|
|
14
14
|
"src/defaults.ts"() {
|
|
15
15
|
"use strict";
|
|
16
|
+
SDK_VERSION = "0.4.49";
|
|
16
17
|
API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
|
|
17
18
|
API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
|
|
18
19
|
NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
|
|
@@ -115,6 +116,12 @@ import {
|
|
|
115
116
|
fetch as undiciFetch,
|
|
116
117
|
setGlobalDispatcher
|
|
117
118
|
} from "undici";
|
|
119
|
+
function withTraceId(value, traceId) {
|
|
120
|
+
if (value == null) {
|
|
121
|
+
return { traceId };
|
|
122
|
+
}
|
|
123
|
+
return Object.assign(value, { traceId });
|
|
124
|
+
}
|
|
118
125
|
function hasHeader(headers, name) {
|
|
119
126
|
const lowered = name.toLowerCase();
|
|
120
127
|
return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
|
|
@@ -183,7 +190,7 @@ var init_http = __esm({
|
|
|
183
190
|
allowH2: true
|
|
184
191
|
})
|
|
185
192
|
);
|
|
186
|
-
HttpClient = class {
|
|
193
|
+
HttpClient = class _HttpClient {
|
|
187
194
|
baseUrl;
|
|
188
195
|
headers;
|
|
189
196
|
maxRetries;
|
|
@@ -196,7 +203,9 @@ var init_http = __esm({
|
|
|
196
203
|
this.maxRetries = options.maxRetries ?? MAX_RETRIES;
|
|
197
204
|
this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
|
|
198
205
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
199
|
-
this.headers = {
|
|
206
|
+
this.headers = {
|
|
207
|
+
"User-Agent": `tensorlake-typescript-sdk/${SDK_VERSION}`
|
|
208
|
+
};
|
|
200
209
|
if (options.apiKey) {
|
|
201
210
|
this.headers["Authorization"] = `Bearer ${options.apiKey}`;
|
|
202
211
|
}
|
|
@@ -225,8 +234,8 @@ var init_http = __esm({
|
|
|
225
234
|
signal: options?.signal
|
|
226
235
|
});
|
|
227
236
|
const text = await response.text();
|
|
228
|
-
|
|
229
|
-
return
|
|
237
|
+
const data = text ? JSON.parse(text) : void 0;
|
|
238
|
+
return withTraceId(data, response.traceId);
|
|
230
239
|
}
|
|
231
240
|
/** Make a request returning raw bytes. */
|
|
232
241
|
async requestBytes(method, path2, options) {
|
|
@@ -240,7 +249,7 @@ var init_http = __esm({
|
|
|
240
249
|
signal: options?.signal
|
|
241
250
|
});
|
|
242
251
|
const buffer = await response.arrayBuffer();
|
|
243
|
-
return new Uint8Array(buffer);
|
|
252
|
+
return withTraceId(new Uint8Array(buffer), response.traceId);
|
|
244
253
|
}
|
|
245
254
|
/** Make a request and return the response body as an SSE stream. */
|
|
246
255
|
async requestStream(method, path2, options) {
|
|
@@ -255,7 +264,7 @@ var init_http = __esm({
|
|
|
255
264
|
"No response body for SSE stream"
|
|
256
265
|
);
|
|
257
266
|
}
|
|
258
|
-
return response.body;
|
|
267
|
+
return withTraceId(response.body, response.traceId);
|
|
259
268
|
}
|
|
260
269
|
/** Make a request and return the raw Response. */
|
|
261
270
|
async requestResponse(method, path2, options) {
|
|
@@ -268,7 +277,7 @@ var init_http = __esm({
|
|
|
268
277
|
headers["Content-Type"] = "application/json";
|
|
269
278
|
}
|
|
270
279
|
const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
|
|
271
|
-
|
|
280
|
+
const { response, traceId } = await this.doFetch(
|
|
272
281
|
method,
|
|
273
282
|
path2,
|
|
274
283
|
body,
|
|
@@ -276,9 +285,18 @@ var init_http = __esm({
|
|
|
276
285
|
options?.signal,
|
|
277
286
|
options?.allowHttpErrors ?? false
|
|
278
287
|
);
|
|
288
|
+
return withTraceId(response, traceId);
|
|
289
|
+
}
|
|
290
|
+
static makeTraceparent() {
|
|
291
|
+
const randomHex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
292
|
+
const traceId = randomHex(16);
|
|
293
|
+
const spanId = randomHex(8);
|
|
294
|
+
return { traceparent: `00-${traceId}-${spanId}-01`, traceId };
|
|
279
295
|
}
|
|
280
296
|
async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
|
|
281
297
|
const url = `${this.baseUrl}${path2}`;
|
|
298
|
+
const { traceparent, traceId } = _HttpClient.makeTraceparent();
|
|
299
|
+
headers["traceparent"] = traceparent;
|
|
282
300
|
let lastError;
|
|
283
301
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
284
302
|
if (attempt > 0) {
|
|
@@ -299,7 +317,7 @@ var init_http = __esm({
|
|
|
299
317
|
signal: combinedSignal
|
|
300
318
|
});
|
|
301
319
|
clearTimeout(timeoutId);
|
|
302
|
-
if (response.ok) return response;
|
|
320
|
+
if (response.ok) return { response, traceId };
|
|
303
321
|
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
|
|
304
322
|
lastError = new RemoteAPIError(
|
|
305
323
|
response.status,
|
|
@@ -308,7 +326,7 @@ var init_http = __esm({
|
|
|
308
326
|
continue;
|
|
309
327
|
}
|
|
310
328
|
if (allowHttpErrors) {
|
|
311
|
-
return response;
|
|
329
|
+
return { response, traceId };
|
|
312
330
|
}
|
|
313
331
|
const errorBody = await response.text().catch(() => "");
|
|
314
332
|
throwMappedError(response.status, errorBody, path2);
|
|
@@ -378,20 +396,20 @@ var SandboxStatus, SnapshotStatus, ProcessStatus, StdinMode, OutputMode, Contain
|
|
|
378
396
|
var init_models = __esm({
|
|
379
397
|
"src/models.ts"() {
|
|
380
398
|
"use strict";
|
|
381
|
-
SandboxStatus = /* @__PURE__ */ ((
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return
|
|
399
|
+
SandboxStatus = /* @__PURE__ */ ((SandboxStatus3) => {
|
|
400
|
+
SandboxStatus3["PENDING"] = "pending";
|
|
401
|
+
SandboxStatus3["RUNNING"] = "running";
|
|
402
|
+
SandboxStatus3["SNAPSHOTTING"] = "snapshotting";
|
|
403
|
+
SandboxStatus3["SUSPENDING"] = "suspending";
|
|
404
|
+
SandboxStatus3["SUSPENDED"] = "suspended";
|
|
405
|
+
SandboxStatus3["TERMINATED"] = "terminated";
|
|
406
|
+
return SandboxStatus3;
|
|
389
407
|
})(SandboxStatus || {});
|
|
390
|
-
SnapshotStatus = /* @__PURE__ */ ((
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return
|
|
408
|
+
SnapshotStatus = /* @__PURE__ */ ((SnapshotStatus3) => {
|
|
409
|
+
SnapshotStatus3["IN_PROGRESS"] = "in_progress";
|
|
410
|
+
SnapshotStatus3["COMPLETED"] = "completed";
|
|
411
|
+
SnapshotStatus3["FAILED"] = "failed";
|
|
412
|
+
return SnapshotStatus3;
|
|
395
413
|
})(SnapshotStatus || {});
|
|
396
414
|
ProcessStatus = /* @__PURE__ */ ((ProcessStatus2) => {
|
|
397
415
|
ProcessStatus2["RUNNING"] = "running";
|
|
@@ -3140,13 +3158,17 @@ var init_sandbox = __esm({
|
|
|
3140
3158
|
};
|
|
3141
3159
|
Sandbox = class {
|
|
3142
3160
|
sandboxId;
|
|
3161
|
+
traceId = null;
|
|
3143
3162
|
http;
|
|
3144
3163
|
baseUrl;
|
|
3145
3164
|
wsHeaders;
|
|
3146
3165
|
ownsSandbox = false;
|
|
3147
3166
|
lifecycleClient = null;
|
|
3167
|
+
lifecycleIdentifier;
|
|
3168
|
+
sandboxName = null;
|
|
3148
3169
|
constructor(options) {
|
|
3149
3170
|
this.sandboxId = options.sandboxId;
|
|
3171
|
+
this.lifecycleIdentifier = options.sandboxId;
|
|
3150
3172
|
const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
|
|
3151
3173
|
const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
|
|
3152
3174
|
this.baseUrl = baseUrl;
|
|
@@ -3172,21 +3194,182 @@ var init_sandbox = __esm({
|
|
|
3172
3194
|
routingHint: options.routingHint
|
|
3173
3195
|
});
|
|
3174
3196
|
}
|
|
3197
|
+
get name() {
|
|
3198
|
+
return this.sandboxName;
|
|
3199
|
+
}
|
|
3200
|
+
/** @internal Used by client wiring to keep locally cached name in sync. */
|
|
3201
|
+
_setName(name) {
|
|
3202
|
+
this.sandboxName = name;
|
|
3203
|
+
}
|
|
3204
|
+
/** @internal Used by lifecycle operations to pin to canonical sandbox ID. */
|
|
3205
|
+
_setLifecycleIdentifier(identifier) {
|
|
3206
|
+
this.lifecycleIdentifier = identifier;
|
|
3207
|
+
}
|
|
3175
3208
|
/** @internal Used by SandboxClient.createAndConnect to set ownership. */
|
|
3176
3209
|
_setOwner(client) {
|
|
3177
3210
|
this.ownsSandbox = true;
|
|
3178
3211
|
this.lifecycleClient = client;
|
|
3179
3212
|
}
|
|
3213
|
+
// --- Static factory methods ---
|
|
3214
|
+
/**
|
|
3215
|
+
* Create a new sandbox and return a connected, running handle.
|
|
3216
|
+
*
|
|
3217
|
+
* Covers both fresh sandbox creation and restore-from-snapshot (set
|
|
3218
|
+
* `snapshotId`). Blocks until the sandbox is `Running`.
|
|
3219
|
+
*/
|
|
3220
|
+
static async create(options) {
|
|
3221
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3222
|
+
const client = new SandboxClient2(
|
|
3223
|
+
options,
|
|
3224
|
+
/* _internal */
|
|
3225
|
+
true
|
|
3226
|
+
);
|
|
3227
|
+
const sandbox = await client.createAndConnect(options);
|
|
3228
|
+
sandbox.lifecycleClient = client;
|
|
3229
|
+
return sandbox;
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Attach to an existing sandbox and return a connected handle.
|
|
3233
|
+
*
|
|
3234
|
+
* Verifies the sandbox exists via a server GET call, then returns a handle
|
|
3235
|
+
* in whatever state the sandbox is in. Does **not** auto-resume a suspended
|
|
3236
|
+
* sandbox — call `sandbox.resume()` explicitly.
|
|
3237
|
+
*/
|
|
3238
|
+
static async connect(options) {
|
|
3239
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3240
|
+
const client = new SandboxClient2(
|
|
3241
|
+
options,
|
|
3242
|
+
/* _internal */
|
|
3243
|
+
true
|
|
3244
|
+
);
|
|
3245
|
+
const info = await client.get(options.sandboxId);
|
|
3246
|
+
const sandbox = client.connect(
|
|
3247
|
+
info.sandboxId,
|
|
3248
|
+
options.proxyUrl,
|
|
3249
|
+
options.routingHint ?? info.routingHint
|
|
3250
|
+
);
|
|
3251
|
+
sandbox.lifecycleClient = client;
|
|
3252
|
+
sandbox._setLifecycleIdentifier(info.sandboxId);
|
|
3253
|
+
sandbox._setName(info.name ?? null);
|
|
3254
|
+
return sandbox;
|
|
3255
|
+
}
|
|
3256
|
+
// --- Static snapshot management ---
|
|
3257
|
+
/** Get information about a snapshot by ID. No sandbox handle needed. */
|
|
3258
|
+
static async getSnapshot(snapshotId, options) {
|
|
3259
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3260
|
+
const client = new SandboxClient2(
|
|
3261
|
+
options,
|
|
3262
|
+
/* _internal */
|
|
3263
|
+
true
|
|
3264
|
+
);
|
|
3265
|
+
return client.getSnapshot(snapshotId);
|
|
3266
|
+
}
|
|
3267
|
+
/** Delete a snapshot by ID. No sandbox handle needed. */
|
|
3268
|
+
static async deleteSnapshot(snapshotId, options) {
|
|
3269
|
+
const { SandboxClient: SandboxClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3270
|
+
const client = new SandboxClient2(
|
|
3271
|
+
options,
|
|
3272
|
+
/* _internal */
|
|
3273
|
+
true
|
|
3274
|
+
);
|
|
3275
|
+
await client.deleteSnapshot(snapshotId);
|
|
3276
|
+
}
|
|
3277
|
+
// --- Instance lifecycle methods ---
|
|
3278
|
+
requireLifecycleClient(operation) {
|
|
3279
|
+
if (!this.lifecycleClient) {
|
|
3280
|
+
throw new SandboxError(
|
|
3281
|
+
`Cannot ${operation}: no lifecycle client available. Use Sandbox.create() or Sandbox.connect() to get a lifecycle-aware handle.`
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
return this.lifecycleClient;
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Fetch the current sandbox status from the server.
|
|
3288
|
+
*
|
|
3289
|
+
* Always hits the network — the value is not cached locally because the
|
|
3290
|
+
* status changes over the sandbox's lifecycle.
|
|
3291
|
+
*/
|
|
3292
|
+
async status() {
|
|
3293
|
+
const client = this.requireLifecycleClient("read_status");
|
|
3294
|
+
const info = await client.get(this.lifecycleIdentifier);
|
|
3295
|
+
this._setLifecycleIdentifier(info.sandboxId);
|
|
3296
|
+
this._setName(info.name ?? null);
|
|
3297
|
+
return info.status;
|
|
3298
|
+
}
|
|
3299
|
+
/**
|
|
3300
|
+
* Update this sandbox's properties (name, exposed ports, proxy auth).
|
|
3301
|
+
*
|
|
3302
|
+
* Naming an ephemeral sandbox makes it non-ephemeral and enables
|
|
3303
|
+
* suspend/resume.
|
|
3304
|
+
*/
|
|
3305
|
+
async update(options) {
|
|
3306
|
+
const client = this.requireLifecycleClient("update");
|
|
3307
|
+
const info = await client.update(this.lifecycleIdentifier, options);
|
|
3308
|
+
this._setLifecycleIdentifier(info.sandboxId);
|
|
3309
|
+
this._setName(info.name ?? null);
|
|
3310
|
+
return info;
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* Suspend this sandbox.
|
|
3314
|
+
*
|
|
3315
|
+
* By default blocks until the sandbox is fully `Suspended`. Pass
|
|
3316
|
+
* `{ wait: false }` for fire-and-return.
|
|
3317
|
+
*/
|
|
3318
|
+
async suspend(options) {
|
|
3319
|
+
const client = this.requireLifecycleClient("suspend");
|
|
3320
|
+
await client.suspend(this.lifecycleIdentifier, options);
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Resume this sandbox.
|
|
3324
|
+
*
|
|
3325
|
+
* By default blocks until the sandbox is `Running` and routable. Pass
|
|
3326
|
+
* `{ wait: false }` for fire-and-return.
|
|
3327
|
+
*/
|
|
3328
|
+
async resume(options) {
|
|
3329
|
+
const client = this.requireLifecycleClient("resume");
|
|
3330
|
+
await client.resume(this.lifecycleIdentifier, options);
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* Create a snapshot of this sandbox's filesystem and wait for it to
|
|
3334
|
+
* be committed.
|
|
3335
|
+
*
|
|
3336
|
+
* By default blocks until the snapshot artifact is ready and returns
|
|
3337
|
+
* the completed `SnapshotInfo`. Pass `{ wait: false }` to fire-and-return
|
|
3338
|
+
* (returns `undefined`).
|
|
3339
|
+
*/
|
|
3340
|
+
async checkpoint(options) {
|
|
3341
|
+
const client = this.requireLifecycleClient("checkpoint");
|
|
3342
|
+
if (options?.wait === false) {
|
|
3343
|
+
await client.snapshot(this.lifecycleIdentifier, { contentMode: options.contentMode });
|
|
3344
|
+
return void 0;
|
|
3345
|
+
}
|
|
3346
|
+
return client.snapshotAndWait(this.lifecycleIdentifier, {
|
|
3347
|
+
timeout: options?.timeout,
|
|
3348
|
+
pollInterval: options?.pollInterval,
|
|
3349
|
+
contentMode: options?.contentMode
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* List snapshots taken from this sandbox.
|
|
3354
|
+
*/
|
|
3355
|
+
async listSnapshots() {
|
|
3356
|
+
const client = this.requireLifecycleClient("listSnapshots");
|
|
3357
|
+
const all = await client.listSnapshots();
|
|
3358
|
+
const filtered = all.filter((s) => s.sandboxId === this.lifecycleIdentifier);
|
|
3359
|
+
return Object.assign(filtered, { traceId: all.traceId });
|
|
3360
|
+
}
|
|
3361
|
+
/** Close the HTTP client. The sandbox keeps running. */
|
|
3180
3362
|
close() {
|
|
3181
3363
|
this.http.close();
|
|
3182
3364
|
}
|
|
3365
|
+
/** Terminate the sandbox and release all resources. */
|
|
3183
3366
|
async terminate() {
|
|
3184
3367
|
const client = this.lifecycleClient;
|
|
3185
3368
|
this.ownsSandbox = false;
|
|
3186
3369
|
this.lifecycleClient = null;
|
|
3187
3370
|
this.close();
|
|
3188
3371
|
if (client) {
|
|
3189
|
-
await client.delete(this.
|
|
3372
|
+
await client.delete(this.lifecycleIdentifier);
|
|
3190
3373
|
}
|
|
3191
3374
|
}
|
|
3192
3375
|
// --- High-level convenience ---
|
|
@@ -3232,6 +3415,14 @@ var init_sandbox = __esm({
|
|
|
3232
3415
|
};
|
|
3233
3416
|
}
|
|
3234
3417
|
// --- Process management ---
|
|
3418
|
+
/**
|
|
3419
|
+
* Start a process in the sandbox without waiting for it to exit.
|
|
3420
|
+
*
|
|
3421
|
+
* Returns a `ProcessInfo` with the assigned `pid`. Use `getProcess()` to
|
|
3422
|
+
* poll status, or `followStdout()` / `followOutput()` to stream output
|
|
3423
|
+
* until the process exits. Use `run()` instead to block until completion
|
|
3424
|
+
* and get combined output in one call.
|
|
3425
|
+
*/
|
|
3235
3426
|
async startProcess(command, options) {
|
|
3236
3427
|
const payload = { command };
|
|
3237
3428
|
if (options?.args != null) payload.args = options.args;
|
|
@@ -3253,13 +3444,16 @@ var init_sandbox = __esm({
|
|
|
3253
3444
|
);
|
|
3254
3445
|
return fromSnakeKeys(raw);
|
|
3255
3446
|
}
|
|
3447
|
+
/** List all processes (running and exited) tracked by the sandbox daemon. */
|
|
3256
3448
|
async listProcesses() {
|
|
3257
3449
|
const raw = await this.http.requestJson(
|
|
3258
3450
|
"GET",
|
|
3259
3451
|
"/api/v1/processes"
|
|
3260
3452
|
);
|
|
3261
|
-
|
|
3453
|
+
const processes = (raw.processes ?? []).map((p) => fromSnakeKeys(p));
|
|
3454
|
+
return Object.assign(processes, { traceId: raw.traceId });
|
|
3262
3455
|
}
|
|
3456
|
+
/** Get current status and metadata for a process by PID. */
|
|
3263
3457
|
async getProcess(pid) {
|
|
3264
3458
|
const raw = await this.http.requestJson(
|
|
3265
3459
|
"GET",
|
|
@@ -3267,9 +3461,11 @@ var init_sandbox = __esm({
|
|
|
3267
3461
|
);
|
|
3268
3462
|
return fromSnakeKeys(raw);
|
|
3269
3463
|
}
|
|
3464
|
+
/** Send SIGKILL to a process. */
|
|
3270
3465
|
async killProcess(pid) {
|
|
3271
3466
|
await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
|
|
3272
3467
|
}
|
|
3468
|
+
/** Send an arbitrary signal to a process (e.g. `15` for SIGTERM, `9` for SIGKILL). */
|
|
3273
3469
|
async sendSignal(pid, signal) {
|
|
3274
3470
|
const raw = await this.http.requestJson(
|
|
3275
3471
|
"POST",
|
|
@@ -3279,15 +3475,18 @@ var init_sandbox = __esm({
|
|
|
3279
3475
|
return fromSnakeKeys(raw);
|
|
3280
3476
|
}
|
|
3281
3477
|
// --- Process I/O ---
|
|
3478
|
+
/** Write bytes to a process's stdin. The process must have been started with `stdinMode: StdinMode.PIPE`. */
|
|
3282
3479
|
async writeStdin(pid, data) {
|
|
3283
3480
|
await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
|
|
3284
3481
|
body: data,
|
|
3285
3482
|
contentType: "application/octet-stream"
|
|
3286
3483
|
});
|
|
3287
3484
|
}
|
|
3485
|
+
/** Close a process's stdin pipe, signalling EOF to the process. */
|
|
3288
3486
|
async closeStdin(pid) {
|
|
3289
3487
|
await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
|
|
3290
3488
|
}
|
|
3489
|
+
/** Return all captured stdout lines produced so far by a process. */
|
|
3291
3490
|
async getStdout(pid) {
|
|
3292
3491
|
const raw = await this.http.requestJson(
|
|
3293
3492
|
"GET",
|
|
@@ -3295,6 +3494,7 @@ var init_sandbox = __esm({
|
|
|
3295
3494
|
);
|
|
3296
3495
|
return fromSnakeKeys(raw);
|
|
3297
3496
|
}
|
|
3497
|
+
/** Return all captured stderr lines produced so far by a process. */
|
|
3298
3498
|
async getStderr(pid) {
|
|
3299
3499
|
const raw = await this.http.requestJson(
|
|
3300
3500
|
"GET",
|
|
@@ -3302,6 +3502,7 @@ var init_sandbox = __esm({
|
|
|
3302
3502
|
);
|
|
3303
3503
|
return fromSnakeKeys(raw);
|
|
3304
3504
|
}
|
|
3505
|
+
/** Return all captured stdout+stderr lines produced so far by a process. */
|
|
3305
3506
|
async getOutput(pid) {
|
|
3306
3507
|
const raw = await this.http.requestJson(
|
|
3307
3508
|
"GET",
|
|
@@ -3310,6 +3511,7 @@ var init_sandbox = __esm({
|
|
|
3310
3511
|
return fromSnakeKeys(raw);
|
|
3311
3512
|
}
|
|
3312
3513
|
// --- Streaming (SSE) ---
|
|
3514
|
+
/** Stream stdout events from a process until it exits. Yields one `OutputEvent` per line. */
|
|
3313
3515
|
async *followStdout(pid, options) {
|
|
3314
3516
|
const stream = await this.http.requestStream(
|
|
3315
3517
|
"GET",
|
|
@@ -3323,6 +3525,7 @@ var init_sandbox = __esm({
|
|
|
3323
3525
|
yield fromSnakeKeys(raw);
|
|
3324
3526
|
}
|
|
3325
3527
|
}
|
|
3528
|
+
/** Stream stderr events from a process until it exits. Yields one `OutputEvent` per line. */
|
|
3326
3529
|
async *followStderr(pid, options) {
|
|
3327
3530
|
const stream = await this.http.requestStream(
|
|
3328
3531
|
"GET",
|
|
@@ -3336,6 +3539,7 @@ var init_sandbox = __esm({
|
|
|
3336
3539
|
yield fromSnakeKeys(raw);
|
|
3337
3540
|
}
|
|
3338
3541
|
}
|
|
3542
|
+
/** Stream combined stdout+stderr events from a process until it exits. Yields one `OutputEvent` per line. */
|
|
3339
3543
|
async *followOutput(pid, options) {
|
|
3340
3544
|
const stream = await this.http.requestStream(
|
|
3341
3545
|
"GET",
|
|
@@ -3350,12 +3554,14 @@ var init_sandbox = __esm({
|
|
|
3350
3554
|
}
|
|
3351
3555
|
}
|
|
3352
3556
|
// --- File operations ---
|
|
3557
|
+
/** Read a file from the sandbox and return its raw bytes. */
|
|
3353
3558
|
async readFile(path2) {
|
|
3354
3559
|
return this.http.requestBytes(
|
|
3355
3560
|
"GET",
|
|
3356
3561
|
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
3357
3562
|
);
|
|
3358
3563
|
}
|
|
3564
|
+
/** Write raw bytes to a file in the sandbox, creating it if it does not exist. */
|
|
3359
3565
|
async writeFile(path2, content) {
|
|
3360
3566
|
await this.http.requestBytes(
|
|
3361
3567
|
"PUT",
|
|
@@ -3363,12 +3569,14 @@ var init_sandbox = __esm({
|
|
|
3363
3569
|
{ body: content, contentType: "application/octet-stream" }
|
|
3364
3570
|
);
|
|
3365
3571
|
}
|
|
3572
|
+
/** Delete a file from the sandbox. */
|
|
3366
3573
|
async deleteFile(path2) {
|
|
3367
3574
|
await this.http.requestJson(
|
|
3368
3575
|
"DELETE",
|
|
3369
3576
|
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
3370
3577
|
);
|
|
3371
3578
|
}
|
|
3579
|
+
/** List the contents of a directory in the sandbox. */
|
|
3372
3580
|
async listDirectory(path2) {
|
|
3373
3581
|
const raw = await this.http.requestJson(
|
|
3374
3582
|
"GET",
|
|
@@ -3377,6 +3585,7 @@ var init_sandbox = __esm({
|
|
|
3377
3585
|
return fromSnakeKeys(raw);
|
|
3378
3586
|
}
|
|
3379
3587
|
// --- PTY ---
|
|
3588
|
+
/** Create an interactive PTY session. Returns a `sessionId` and `token` for WebSocket connection via `connectPty()`. */
|
|
3380
3589
|
async createPtySession(options) {
|
|
3381
3590
|
const payload = {
|
|
3382
3591
|
command: options.command,
|
|
@@ -3393,6 +3602,7 @@ var init_sandbox = __esm({
|
|
|
3393
3602
|
);
|
|
3394
3603
|
return fromSnakeKeys(raw);
|
|
3395
3604
|
}
|
|
3605
|
+
/** Create a PTY session and connect to it immediately. Cleans up the session if the WebSocket connection fails. */
|
|
3396
3606
|
async createPty(options) {
|
|
3397
3607
|
const { onData, onExit, ...createOptions } = options;
|
|
3398
3608
|
const session = await this.createPtySession(createOptions);
|
|
@@ -3406,6 +3616,7 @@ var init_sandbox = __esm({
|
|
|
3406
3616
|
throw error;
|
|
3407
3617
|
}
|
|
3408
3618
|
}
|
|
3619
|
+
/** Attach to an existing PTY session by ID and token and return a connected `Pty` handle. */
|
|
3409
3620
|
async connectPty(sessionId, token, options) {
|
|
3410
3621
|
const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
|
|
3411
3622
|
const authToken = wsUrl.searchParams.get("token") ?? token;
|
|
@@ -3430,6 +3641,7 @@ var init_sandbox = __esm({
|
|
|
3430
3641
|
await pty.connect();
|
|
3431
3642
|
return pty;
|
|
3432
3643
|
}
|
|
3644
|
+
/** Open a TCP tunnel to a port inside the sandbox and return the local listener. */
|
|
3433
3645
|
async createTunnel(remotePort, options) {
|
|
3434
3646
|
return TcpTunnel.listen({
|
|
3435
3647
|
baseUrl: this.baseUrl,
|
|
@@ -3440,6 +3652,7 @@ var init_sandbox = __esm({
|
|
|
3440
3652
|
connectTimeout: options?.connectTimeout
|
|
3441
3653
|
});
|
|
3442
3654
|
}
|
|
3655
|
+
/** Connect to a sandbox VNC session for programmatic desktop control. */
|
|
3443
3656
|
async connectDesktop(options) {
|
|
3444
3657
|
return Desktop.connect({
|
|
3445
3658
|
baseUrl: this.baseUrl,
|
|
@@ -3462,6 +3675,7 @@ var init_sandbox = __esm({
|
|
|
3462
3675
|
return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
|
|
3463
3676
|
}
|
|
3464
3677
|
// --- Health ---
|
|
3678
|
+
/** Check the sandbox daemon health. */
|
|
3465
3679
|
async health() {
|
|
3466
3680
|
const raw = await this.http.requestJson(
|
|
3467
3681
|
"GET",
|
|
@@ -3469,6 +3683,7 @@ var init_sandbox = __esm({
|
|
|
3469
3683
|
);
|
|
3470
3684
|
return fromSnakeKeys(raw);
|
|
3471
3685
|
}
|
|
3686
|
+
/** Get sandbox daemon info (version, uptime, process counts). */
|
|
3472
3687
|
async info() {
|
|
3473
3688
|
const raw = await this.http.requestJson(
|
|
3474
3689
|
"GET",
|
|
@@ -3481,6 +3696,10 @@ var init_sandbox = __esm({
|
|
|
3481
3696
|
});
|
|
3482
3697
|
|
|
3483
3698
|
// src/client.ts
|
|
3699
|
+
var client_exports = {};
|
|
3700
|
+
__export(client_exports, {
|
|
3701
|
+
SandboxClient: () => SandboxClient
|
|
3702
|
+
});
|
|
3484
3703
|
function sleep2(ms) {
|
|
3485
3704
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3486
3705
|
}
|
|
@@ -3517,7 +3736,13 @@ var init_client = __esm({
|
|
|
3517
3736
|
projectId;
|
|
3518
3737
|
namespace;
|
|
3519
3738
|
local;
|
|
3520
|
-
|
|
3739
|
+
/** @internal Pass `true` to suppress the deprecation warning when used by `Sandbox.create()` / `Sandbox.connect()`. */
|
|
3740
|
+
constructor(options, _internal = false) {
|
|
3741
|
+
if (!_internal) {
|
|
3742
|
+
console.warn(
|
|
3743
|
+
"[tensorlake] SandboxClient is deprecated; use Sandbox.create() / Sandbox.connect() instead."
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3521
3746
|
this.apiUrl = options?.apiUrl ?? API_URL;
|
|
3522
3747
|
this.apiKey = options?.apiKey ?? API_KEY;
|
|
3523
3748
|
this.organizationId = options?.organizationId;
|
|
@@ -3557,12 +3782,13 @@ var init_client = __esm({
|
|
|
3557
3782
|
return lifecyclePath(subpath, this.local, this.namespace);
|
|
3558
3783
|
}
|
|
3559
3784
|
// --- Sandbox CRUD ---
|
|
3785
|
+
/** Create a new sandbox. Returns immediately; the sandbox may still be starting. Use `createAndConnect()` for a blocking, ready-to-use handle. */
|
|
3560
3786
|
async create(options) {
|
|
3561
3787
|
const body = {
|
|
3562
3788
|
resources: {
|
|
3563
3789
|
cpus: options?.cpus ?? 1,
|
|
3564
3790
|
memory_mb: options?.memoryMb ?? 1024,
|
|
3565
|
-
|
|
3791
|
+
...options?.diskMb != null ? { disk_mb: options.diskMb } : {}
|
|
3566
3792
|
}
|
|
3567
3793
|
};
|
|
3568
3794
|
if (options?.image != null) body.image = options.image;
|
|
@@ -3583,8 +3809,10 @@ var init_client = __esm({
|
|
|
3583
3809
|
this.path("sandboxes"),
|
|
3584
3810
|
{ body }
|
|
3585
3811
|
);
|
|
3586
|
-
|
|
3812
|
+
const result = fromSnakeKeys(raw, "sandboxId");
|
|
3813
|
+
return Object.assign(result, { traceId: raw.traceId });
|
|
3587
3814
|
}
|
|
3815
|
+
/** Get current state and metadata for a sandbox by ID. */
|
|
3588
3816
|
async get(sandboxId) {
|
|
3589
3817
|
const raw = await this.http.requestJson(
|
|
3590
3818
|
"GET",
|
|
@@ -3592,15 +3820,18 @@ var init_client = __esm({
|
|
|
3592
3820
|
);
|
|
3593
3821
|
return fromSnakeKeys(raw, "sandboxId");
|
|
3594
3822
|
}
|
|
3823
|
+
/** List all sandboxes in the namespace. */
|
|
3595
3824
|
async list() {
|
|
3596
3825
|
const raw = await this.http.requestJson(
|
|
3597
3826
|
"GET",
|
|
3598
3827
|
this.path("sandboxes")
|
|
3599
3828
|
);
|
|
3600
|
-
|
|
3829
|
+
const sandboxes = (raw.sandboxes ?? []).map(
|
|
3601
3830
|
(s) => fromSnakeKeys(s, "sandboxId")
|
|
3602
3831
|
);
|
|
3832
|
+
return Object.assign(sandboxes, { traceId: raw.traceId });
|
|
3603
3833
|
}
|
|
3834
|
+
/** Update sandbox properties such as name, exposed ports, and proxy auth settings. */
|
|
3604
3835
|
async update(sandboxId, options) {
|
|
3605
3836
|
const body = {};
|
|
3606
3837
|
if (options.name != null) body.name = options.name;
|
|
@@ -3620,6 +3851,7 @@ var init_client = __esm({
|
|
|
3620
3851
|
);
|
|
3621
3852
|
return fromSnakeKeys(raw, "sandboxId");
|
|
3622
3853
|
}
|
|
3854
|
+
/** Get the current proxy port settings for a sandbox. */
|
|
3623
3855
|
async getPortAccess(sandboxId) {
|
|
3624
3856
|
const info = await this.get(sandboxId);
|
|
3625
3857
|
return {
|
|
@@ -3628,6 +3860,7 @@ var init_client = __esm({
|
|
|
3628
3860
|
sandboxUrl: info.sandboxUrl
|
|
3629
3861
|
};
|
|
3630
3862
|
}
|
|
3863
|
+
/** Add one or more user ports to the sandbox proxy allowlist. */
|
|
3631
3864
|
async exposePorts(sandboxId, ports, options) {
|
|
3632
3865
|
const requestedPorts = normalizeUserPorts(ports);
|
|
3633
3866
|
const current = await this.getPortAccess(sandboxId);
|
|
@@ -3640,6 +3873,7 @@ var init_client = __esm({
|
|
|
3640
3873
|
exposedPorts: desiredPorts
|
|
3641
3874
|
});
|
|
3642
3875
|
}
|
|
3876
|
+
/** Remove one or more user ports from the sandbox proxy allowlist. */
|
|
3643
3877
|
async unexposePorts(sandboxId, ports) {
|
|
3644
3878
|
const requestedPorts = normalizeUserPorts(ports);
|
|
3645
3879
|
const current = await this.getPortAccess(sandboxId);
|
|
@@ -3650,32 +3884,98 @@ var init_client = __esm({
|
|
|
3650
3884
|
exposedPorts: desiredPorts
|
|
3651
3885
|
});
|
|
3652
3886
|
}
|
|
3887
|
+
/** Terminate and delete a sandbox. */
|
|
3653
3888
|
async delete(sandboxId) {
|
|
3654
3889
|
await this.http.requestJson(
|
|
3655
3890
|
"DELETE",
|
|
3656
3891
|
this.path(`sandboxes/${sandboxId}`)
|
|
3657
3892
|
);
|
|
3658
3893
|
}
|
|
3659
|
-
|
|
3894
|
+
/**
|
|
3895
|
+
* Suspend a named sandbox, preserving its state for later resume.
|
|
3896
|
+
*
|
|
3897
|
+
* Only sandboxes created with a `name` can be suspended; ephemeral sandboxes
|
|
3898
|
+
* cannot. By default blocks until the sandbox is fully `Suspended`. Pass
|
|
3899
|
+
* `{ wait: false }` to return immediately after the request is sent
|
|
3900
|
+
* (fire-and-return); the server processes the suspend asynchronously.
|
|
3901
|
+
*
|
|
3902
|
+
* @param sandboxId - ID or name of the sandbox.
|
|
3903
|
+
* @param options.wait - If `true` (default), poll until `Suspended`. Pass `false` to fire-and-return.
|
|
3904
|
+
* @param options.timeout - Max seconds to wait when `wait=true` (default 300).
|
|
3905
|
+
* @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
|
|
3906
|
+
* @throws {SandboxError} If `wait=true` and the sandbox does not reach `Suspended` within `timeout`.
|
|
3907
|
+
*/
|
|
3908
|
+
async suspend(sandboxId, options) {
|
|
3660
3909
|
await this.http.requestResponse(
|
|
3661
3910
|
"POST",
|
|
3662
3911
|
this.path(`sandboxes/${sandboxId}/suspend`)
|
|
3663
3912
|
);
|
|
3913
|
+
if (options?.wait === false) return;
|
|
3914
|
+
const timeout = options?.timeout ?? 300;
|
|
3915
|
+
const pollInterval = options?.pollInterval ?? 1;
|
|
3916
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
3917
|
+
while (Date.now() < deadline) {
|
|
3918
|
+
const info = await this.get(sandboxId);
|
|
3919
|
+
if (info.status === "suspended" /* SUSPENDED */) return;
|
|
3920
|
+
if (info.status === "terminated" /* TERMINATED */) {
|
|
3921
|
+
throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for suspend`);
|
|
3922
|
+
}
|
|
3923
|
+
await sleep2(pollInterval * 1e3);
|
|
3924
|
+
}
|
|
3925
|
+
throw new SandboxError(`Sandbox ${sandboxId} did not suspend within ${timeout}s`);
|
|
3664
3926
|
}
|
|
3665
|
-
|
|
3927
|
+
/**
|
|
3928
|
+
* Resume a suspended sandbox and bring it back to `Running`.
|
|
3929
|
+
*
|
|
3930
|
+
* By default blocks until the sandbox is `Running` and routable. Pass
|
|
3931
|
+
* `{ wait: false }` to return immediately after the request is sent
|
|
3932
|
+
* (fire-and-return); the server processes the resume asynchronously.
|
|
3933
|
+
*
|
|
3934
|
+
* @param sandboxId - ID or name of the sandbox.
|
|
3935
|
+
* @param options.wait - If `true` (default), poll until `Running`. Pass `false` to fire-and-return.
|
|
3936
|
+
* @param options.timeout - Max seconds to wait when `wait=true` (default 300).
|
|
3937
|
+
* @param options.pollInterval - Seconds between status polls when `wait=true` (default 1).
|
|
3938
|
+
* @throws {SandboxError} If `wait=true` and the sandbox does not reach `Running` within `timeout`.
|
|
3939
|
+
*/
|
|
3940
|
+
async resume(sandboxId, options) {
|
|
3666
3941
|
await this.http.requestResponse(
|
|
3667
3942
|
"POST",
|
|
3668
3943
|
this.path(`sandboxes/${sandboxId}/resume`)
|
|
3669
3944
|
);
|
|
3945
|
+
if (options?.wait === false) return;
|
|
3946
|
+
const timeout = options?.timeout ?? 300;
|
|
3947
|
+
const pollInterval = options?.pollInterval ?? 1;
|
|
3948
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
3949
|
+
while (Date.now() < deadline) {
|
|
3950
|
+
const info = await this.get(sandboxId);
|
|
3951
|
+
if (info.status === "running" /* RUNNING */) return;
|
|
3952
|
+
if (info.status === "terminated" /* TERMINATED */) {
|
|
3953
|
+
throw new SandboxError(`Sandbox ${sandboxId} terminated while waiting for resume`);
|
|
3954
|
+
}
|
|
3955
|
+
await sleep2(pollInterval * 1e3);
|
|
3956
|
+
}
|
|
3957
|
+
throw new SandboxError(`Sandbox ${sandboxId} did not resume within ${timeout}s`);
|
|
3670
3958
|
}
|
|
3959
|
+
/** Claim a warm sandbox from a pool, creating one if no warm containers are available. */
|
|
3671
3960
|
async claim(poolId) {
|
|
3672
3961
|
const raw = await this.http.requestJson(
|
|
3673
3962
|
"POST",
|
|
3674
3963
|
this.path(`sandbox-pools/${poolId}/sandboxes`)
|
|
3675
3964
|
);
|
|
3676
|
-
|
|
3965
|
+
const result = fromSnakeKeys(raw, "sandboxId");
|
|
3966
|
+
return Object.assign(result, { traceId: raw.traceId });
|
|
3677
3967
|
}
|
|
3678
3968
|
// --- Snapshots ---
|
|
3969
|
+
/**
|
|
3970
|
+
* Request a snapshot of a running sandbox's filesystem.
|
|
3971
|
+
*
|
|
3972
|
+
* This call **returns immediately** with a `snapshotId` and `in_progress`
|
|
3973
|
+
* status — the snapshot is created asynchronously. Poll `getSnapshot()` until
|
|
3974
|
+
* `completed` or `failed`, or use `snapshotAndWait()` to block automatically.
|
|
3975
|
+
*
|
|
3976
|
+
* @param options.contentMode - `"filesystem_only"` for cold-boot snapshots (e.g. image builds).
|
|
3977
|
+
* Omit to use the server default (full VM snapshot).
|
|
3978
|
+
*/
|
|
3679
3979
|
async snapshot(sandboxId, options) {
|
|
3680
3980
|
const requestOptions = options?.contentMode != null ? { body: { snapshot_content_mode: options.contentMode } } : void 0;
|
|
3681
3981
|
const raw = await this.http.requestJson(
|
|
@@ -3685,6 +3985,7 @@ var init_client = __esm({
|
|
|
3685
3985
|
);
|
|
3686
3986
|
return fromSnakeKeys(raw, "snapshotId");
|
|
3687
3987
|
}
|
|
3988
|
+
/** Get current status and metadata for a snapshot by ID. */
|
|
3688
3989
|
async getSnapshot(snapshotId) {
|
|
3689
3990
|
const raw = await this.http.requestJson(
|
|
3690
3991
|
"GET",
|
|
@@ -3692,21 +3993,37 @@ var init_client = __esm({
|
|
|
3692
3993
|
);
|
|
3693
3994
|
return fromSnakeKeys(raw, "snapshotId");
|
|
3694
3995
|
}
|
|
3996
|
+
/** List all snapshots in the namespace. */
|
|
3695
3997
|
async listSnapshots() {
|
|
3696
3998
|
const raw = await this.http.requestJson(
|
|
3697
3999
|
"GET",
|
|
3698
4000
|
this.path("snapshots")
|
|
3699
4001
|
);
|
|
3700
|
-
|
|
4002
|
+
const snapshots = (raw.snapshots ?? []).map(
|
|
3701
4003
|
(s) => fromSnakeKeys(s, "snapshotId")
|
|
3702
4004
|
);
|
|
4005
|
+
return Object.assign(snapshots, { traceId: raw.traceId });
|
|
3703
4006
|
}
|
|
4007
|
+
/** Delete a snapshot by ID. */
|
|
3704
4008
|
async deleteSnapshot(snapshotId) {
|
|
3705
4009
|
await this.http.requestJson(
|
|
3706
4010
|
"DELETE",
|
|
3707
4011
|
this.path(`snapshots/${snapshotId}`)
|
|
3708
4012
|
);
|
|
3709
4013
|
}
|
|
4014
|
+
/**
|
|
4015
|
+
* Create a snapshot and block until it is committed.
|
|
4016
|
+
*
|
|
4017
|
+
* Combines `snapshot()` with polling `getSnapshot()` until `completed`.
|
|
4018
|
+
* Prefer `sandbox.checkpoint()` on a `Sandbox` handle for the same behavior
|
|
4019
|
+
* without managing the client separately.
|
|
4020
|
+
*
|
|
4021
|
+
* @param sandboxId - ID of the running sandbox to snapshot.
|
|
4022
|
+
* @param options.timeout - Max seconds to wait (default 300).
|
|
4023
|
+
* @param options.pollInterval - Seconds between status polls (default 1).
|
|
4024
|
+
* @param options.contentMode - Content mode passed through to `snapshot()`.
|
|
4025
|
+
* @throws {SandboxError} If the snapshot fails or `timeout` elapses.
|
|
4026
|
+
*/
|
|
3710
4027
|
async snapshotAndWait(sandboxId, options) {
|
|
3711
4028
|
const timeout = options?.timeout ?? 300;
|
|
3712
4029
|
const pollInterval = options?.pollInterval ?? 1;
|
|
@@ -3729,6 +4046,7 @@ var init_client = __esm({
|
|
|
3729
4046
|
);
|
|
3730
4047
|
}
|
|
3731
4048
|
// --- Pools ---
|
|
4049
|
+
/** Create a new sandbox pool with warm pre-booted containers. */
|
|
3732
4050
|
async createPool(options) {
|
|
3733
4051
|
const body = {
|
|
3734
4052
|
image: options.image,
|
|
@@ -3750,6 +4068,7 @@ var init_client = __esm({
|
|
|
3750
4068
|
);
|
|
3751
4069
|
return fromSnakeKeys(raw, "poolId");
|
|
3752
4070
|
}
|
|
4071
|
+
/** Get current state and metadata for a sandbox pool by ID. */
|
|
3753
4072
|
async getPool(poolId) {
|
|
3754
4073
|
const raw = await this.http.requestJson(
|
|
3755
4074
|
"GET",
|
|
@@ -3757,15 +4076,18 @@ var init_client = __esm({
|
|
|
3757
4076
|
);
|
|
3758
4077
|
return fromSnakeKeys(raw, "poolId");
|
|
3759
4078
|
}
|
|
4079
|
+
/** List all sandbox pools in the namespace. */
|
|
3760
4080
|
async listPools() {
|
|
3761
4081
|
const raw = await this.http.requestJson(
|
|
3762
4082
|
"GET",
|
|
3763
4083
|
this.path("sandbox-pools")
|
|
3764
4084
|
);
|
|
3765
|
-
|
|
4085
|
+
const pools = (raw.pools ?? []).map(
|
|
3766
4086
|
(p) => fromSnakeKeys(p, "poolId")
|
|
3767
4087
|
);
|
|
4088
|
+
return Object.assign(pools, { traceId: raw.traceId });
|
|
3768
4089
|
}
|
|
4090
|
+
/** Replace the configuration of an existing sandbox pool. */
|
|
3769
4091
|
async updatePool(poolId, options) {
|
|
3770
4092
|
const body = {
|
|
3771
4093
|
image: options.image,
|
|
@@ -3787,6 +4109,7 @@ var init_client = __esm({
|
|
|
3787
4109
|
);
|
|
3788
4110
|
return fromSnakeKeys(raw, "poolId");
|
|
3789
4111
|
}
|
|
4112
|
+
/** Delete a sandbox pool. Fails if the pool has active containers. */
|
|
3790
4113
|
async deletePool(poolId) {
|
|
3791
4114
|
await this.http.requestJson(
|
|
3792
4115
|
"DELETE",
|
|
@@ -3794,6 +4117,7 @@ var init_client = __esm({
|
|
|
3794
4117
|
);
|
|
3795
4118
|
}
|
|
3796
4119
|
// --- Connect ---
|
|
4120
|
+
/** Return a `Sandbox` handle for an existing running sandbox without verifying it exists. */
|
|
3797
4121
|
connect(identifier, proxyUrl, routingHint) {
|
|
3798
4122
|
const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
|
|
3799
4123
|
return new Sandbox({
|
|
@@ -3805,26 +4129,35 @@ var init_client = __esm({
|
|
|
3805
4129
|
routingHint
|
|
3806
4130
|
});
|
|
3807
4131
|
}
|
|
4132
|
+
/**
|
|
4133
|
+
* Create a sandbox, wait for it to reach `Running`, and return a connected handle.
|
|
4134
|
+
*
|
|
4135
|
+
* Blocks until the sandbox is ready or `startupTimeout` elapses. The returned
|
|
4136
|
+
* `Sandbox` auto-terminates when `terminate()` is called.
|
|
4137
|
+
*
|
|
4138
|
+
* @param options.startupTimeout - Max seconds to wait for `Running` status (default 60).
|
|
4139
|
+
* @throws {SandboxError} If the sandbox terminates during startup or the timeout elapses.
|
|
4140
|
+
*/
|
|
3808
4141
|
async createAndConnect(options) {
|
|
3809
4142
|
const startupTimeout = options?.startupTimeout ?? 60;
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
result = await this.create(options);
|
|
3815
|
-
}
|
|
3816
|
-
if (result.status === "running" /* RUNNING */) {
|
|
3817
|
-
const sandbox = this.connect(result.sandboxId, options?.proxyUrl, result.routingHint);
|
|
4143
|
+
const result = options?.poolId != null ? await this.claim(options.poolId) : await this.create(options);
|
|
4144
|
+
const requestedName = options?.poolId != null ? null : options?.name ?? null;
|
|
4145
|
+
const finishConnect = (routingHint, name) => {
|
|
4146
|
+
const sandbox = this.connect(result.sandboxId, options?.proxyUrl, routingHint);
|
|
3818
4147
|
sandbox._setOwner(this);
|
|
4148
|
+
sandbox.traceId = result.traceId;
|
|
4149
|
+
sandbox._setLifecycleIdentifier(result.sandboxId);
|
|
4150
|
+
sandbox._setName(name ?? requestedName);
|
|
3819
4151
|
return sandbox;
|
|
4152
|
+
};
|
|
4153
|
+
if (result.status === "running" /* RUNNING */) {
|
|
4154
|
+
return finishConnect(result.routingHint, result.name);
|
|
3820
4155
|
}
|
|
3821
4156
|
const deadline = Date.now() + startupTimeout * 1e3;
|
|
3822
4157
|
while (Date.now() < deadline) {
|
|
3823
4158
|
const info = await this.get(result.sandboxId);
|
|
3824
4159
|
if (info.status === "running" /* RUNNING */) {
|
|
3825
|
-
|
|
3826
|
-
sandbox._setOwner(this);
|
|
3827
|
-
return sandbox;
|
|
4160
|
+
return finishConnect(info.routingHint, info.name);
|
|
3828
4161
|
}
|
|
3829
4162
|
if (info.status === "terminated" /* TERMINATED */) {
|
|
3830
4163
|
throw new SandboxError(
|
|
@@ -4666,7 +4999,8 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
4666
4999
|
sandbox = await client.createAndConnect({
|
|
4667
5000
|
...plan.baseImage == null ? {} : { image: plan.baseImage },
|
|
4668
5001
|
cpus: options.cpus ?? 2,
|
|
4669
|
-
memoryMb: options.memoryMb ?? 4096
|
|
5002
|
+
memoryMb: options.memoryMb ?? 4096,
|
|
5003
|
+
...options.diskMb != null ? { diskMb: options.diskMb } : {}
|
|
4670
5004
|
});
|
|
4671
5005
|
emit({
|
|
4672
5006
|
type: "status",
|
|
@@ -4679,8 +5013,7 @@ async function createSandboxImage(source, options = {}, deps = {}) {
|
|
|
4679
5013
|
});
|
|
4680
5014
|
emit({
|
|
4681
5015
|
type: "snapshot_created",
|
|
4682
|
-
snapshot_id: snapshot.snapshotId
|
|
4683
|
-
snapshot_uri: snapshot.snapshotUri ?? null
|
|
5016
|
+
snapshot_id: snapshot.snapshotId
|
|
4684
5017
|
});
|
|
4685
5018
|
if (!snapshot.snapshotUri) {
|
|
4686
5019
|
throw new Error(
|
|
@@ -4724,27 +5057,33 @@ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
|
|
|
4724
5057
|
name: { type: "string", short: "n" },
|
|
4725
5058
|
cpus: { type: "string" },
|
|
4726
5059
|
memory: { type: "string" },
|
|
5060
|
+
disk: { type: "string" },
|
|
4727
5061
|
public: { type: "boolean", default: false }
|
|
4728
5062
|
}
|
|
4729
5063
|
});
|
|
4730
5064
|
const dockerfilePath = parsed.positionals[0];
|
|
4731
5065
|
if (!dockerfilePath) {
|
|
4732
|
-
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
|
|
5066
|
+
throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--disk GB] [--public]");
|
|
4733
5067
|
}
|
|
4734
5068
|
const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
|
|
4735
5069
|
const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
|
|
5070
|
+
const diskGb = parsed.values.disk != null ? Number(parsed.values.disk) : void 0;
|
|
4736
5071
|
if (cpus != null && !Number.isFinite(cpus)) {
|
|
4737
5072
|
throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
|
|
4738
5073
|
}
|
|
4739
5074
|
if (memoryMb != null && !Number.isInteger(memoryMb)) {
|
|
4740
5075
|
throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
|
|
4741
5076
|
}
|
|
5077
|
+
if (diskGb != null && !Number.isInteger(diskGb)) {
|
|
5078
|
+
throw new Error(`Invalid --disk value: ${parsed.values.disk}`);
|
|
5079
|
+
}
|
|
4742
5080
|
await createSandboxImage(
|
|
4743
5081
|
dockerfilePath,
|
|
4744
5082
|
{
|
|
4745
5083
|
registeredName: parsed.values.name,
|
|
4746
5084
|
cpus,
|
|
4747
5085
|
memoryMb,
|
|
5086
|
+
diskMb: diskGb != null ? diskGb * 1024 : void 0,
|
|
4748
5087
|
isPublic: parsed.values.public
|
|
4749
5088
|
},
|
|
4750
5089
|
{ emit: ndjsonStdoutEmit }
|