tensorlake 0.4.40 → 0.4.42

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.
@@ -0,0 +1,2109 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/sandbox-image.ts
31
+ var sandbox_image_exports = {};
32
+ __export(sandbox_image_exports, {
33
+ createSandboxImage: () => createSandboxImage,
34
+ defaultRegisteredName: () => defaultRegisteredName,
35
+ loadDockerfilePlan: () => loadDockerfilePlan,
36
+ loadImagePlan: () => loadImagePlan,
37
+ logicalDockerfileLines: () => logicalDockerfileLines,
38
+ runCreateSandboxImageCli: () => runCreateSandboxImageCli
39
+ });
40
+ module.exports = __toCommonJS(sandbox_image_exports);
41
+ var import_promises = require("fs/promises");
42
+ var import_node_path = __toESM(require("path"), 1);
43
+ var import_node_util = require("util");
44
+
45
+ // src/models.ts
46
+ function snakeToCamel(str) {
47
+ return str.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
48
+ }
49
+ function parseTimestamp(v) {
50
+ if (v == null) return void 0;
51
+ if (v instanceof Date) return v;
52
+ if (typeof v === "string") {
53
+ const parsed = Date.parse(v);
54
+ return Number.isNaN(parsed) ? void 0 : new Date(parsed);
55
+ }
56
+ const ts = Number(v);
57
+ if (isNaN(ts)) return void 0;
58
+ if (ts > 1e15) return new Date(ts / 1e3);
59
+ if (ts > 1e12) return new Date(ts);
60
+ return new Date(ts * 1e3);
61
+ }
62
+ function fromSnakeKeys(obj, idField) {
63
+ if (Array.isArray(obj)) return obj.map((item) => fromSnakeKeys(item, idField));
64
+ if (obj !== null && typeof obj === "object" && !(obj instanceof Date)) {
65
+ const result = {};
66
+ for (const [k, v] of Object.entries(obj)) {
67
+ let key;
68
+ if (k === "id" && idField) {
69
+ key = idField;
70
+ } else {
71
+ key = snakeToCamel(k);
72
+ }
73
+ if (key.endsWith("At") || key === "timestamp" || key === "startedAt" || key === "endedAt") {
74
+ result[key] = parseTimestamp(v);
75
+ } else if (typeof v === "object" && v !== null && !Array.isArray(v)) {
76
+ result[key] = fromSnakeKeys(v);
77
+ } else if (Array.isArray(v)) {
78
+ result[key] = v.map((item) => fromSnakeKeys(item));
79
+ } else {
80
+ result[key] = v === null ? void 0 : v;
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+ return obj;
86
+ }
87
+
88
+ // src/defaults.ts
89
+ var API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
90
+ var API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
91
+ var NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
92
+ var SANDBOX_PROXY_URL = process.env.TENSORLAKE_SANDBOX_PROXY_URL ?? "https://sandbox.tensorlake.ai";
93
+ var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
94
+ var MAX_RETRIES = 3;
95
+ var RETRY_BACKOFF_MS = 500;
96
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
97
+
98
+ // src/errors.ts
99
+ var SandboxException = class extends Error {
100
+ constructor(message) {
101
+ super(message);
102
+ this.name = "SandboxException";
103
+ }
104
+ };
105
+ var SandboxError = class extends SandboxException {
106
+ constructor(message) {
107
+ super(message);
108
+ this.name = "SandboxError";
109
+ }
110
+ };
111
+ var SandboxConnectionError = class extends SandboxError {
112
+ constructor(message) {
113
+ super(`Connection error: ${message}`);
114
+ this.name = "SandboxConnectionError";
115
+ }
116
+ };
117
+ var SandboxNotFoundError = class extends SandboxError {
118
+ sandboxId;
119
+ constructor(sandboxId) {
120
+ super(`Sandbox not found: ${sandboxId}`);
121
+ this.name = "SandboxNotFoundError";
122
+ this.sandboxId = sandboxId;
123
+ }
124
+ };
125
+ var PoolNotFoundError = class extends SandboxError {
126
+ poolId;
127
+ constructor(poolId) {
128
+ super(`Sandbox pool not found: ${poolId}`);
129
+ this.name = "PoolNotFoundError";
130
+ this.poolId = poolId;
131
+ }
132
+ };
133
+ var PoolInUseError = class extends SandboxError {
134
+ poolId;
135
+ constructor(poolId, message) {
136
+ const base = `Cannot delete pool ${poolId}: pool is in use`;
137
+ super(message ? `${base} - ${message}` : base);
138
+ this.name = "PoolInUseError";
139
+ this.poolId = poolId;
140
+ }
141
+ };
142
+ var RemoteAPIError = class extends SandboxError {
143
+ statusCode;
144
+ responseMessage;
145
+ constructor(statusCode, message) {
146
+ super(`API error (status ${statusCode}): ${message}`);
147
+ this.name = "RemoteAPIError";
148
+ this.statusCode = statusCode;
149
+ this.responseMessage = message;
150
+ }
151
+ };
152
+
153
+ // src/http.ts
154
+ var HttpClient = class {
155
+ baseUrl;
156
+ headers;
157
+ maxRetries;
158
+ retryBackoffMs;
159
+ timeoutMs;
160
+ abortController = null;
161
+ constructor(options) {
162
+ const url = options.baseUrl;
163
+ this.baseUrl = url.endsWith("/") ? url.slice(0, -1) : url;
164
+ this.maxRetries = options.maxRetries ?? MAX_RETRIES;
165
+ this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
166
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
167
+ this.headers = {};
168
+ if (options.apiKey) {
169
+ this.headers["Authorization"] = `Bearer ${options.apiKey}`;
170
+ }
171
+ if (options.organizationId) {
172
+ this.headers["X-Forwarded-Organization-Id"] = options.organizationId;
173
+ }
174
+ if (options.projectId) {
175
+ this.headers["X-Forwarded-Project-Id"] = options.projectId;
176
+ }
177
+ if (options.hostHeader) {
178
+ this.headers["Host"] = options.hostHeader;
179
+ }
180
+ }
181
+ close() {
182
+ this.abortController?.abort();
183
+ this.abortController = null;
184
+ }
185
+ /** Make a JSON request, returning the parsed response body. */
186
+ async requestJson(method, path2, options) {
187
+ const response = await this.requestResponse(method, path2, {
188
+ json: options?.body,
189
+ headers: options?.headers,
190
+ signal: options?.signal
191
+ });
192
+ const text = await response.text();
193
+ if (!text) return void 0;
194
+ return JSON.parse(text);
195
+ }
196
+ /** Make a request returning raw bytes. */
197
+ async requestBytes(method, path2, options) {
198
+ const headers = { ...options?.headers ?? {} };
199
+ if (options?.contentType) {
200
+ headers["Content-Type"] = options.contentType;
201
+ }
202
+ const response = await this.requestResponse(
203
+ method,
204
+ path2,
205
+ {
206
+ body: options?.body,
207
+ headers,
208
+ signal: options?.signal
209
+ }
210
+ );
211
+ const buffer = await response.arrayBuffer();
212
+ return new Uint8Array(buffer);
213
+ }
214
+ /** Make a request and return the raw Response (for SSE streaming). */
215
+ async requestStream(method, path2, options) {
216
+ const response = await this.requestResponse(
217
+ method,
218
+ path2,
219
+ {
220
+ headers: { Accept: "text/event-stream" },
221
+ signal: options?.signal
222
+ }
223
+ );
224
+ if (!response.body) {
225
+ throw new RemoteAPIError(response.status, "No response body for SSE stream");
226
+ }
227
+ return response.body;
228
+ }
229
+ /** Make a request and return the raw Response. */
230
+ async requestResponse(method, path2, options) {
231
+ const headers = {
232
+ ...this.headers,
233
+ ...options?.headers ?? {}
234
+ };
235
+ const hasJsonBody = options?.json !== void 0;
236
+ if (hasJsonBody && !hasHeader(headers, "Content-Type")) {
237
+ headers["Content-Type"] = "application/json";
238
+ }
239
+ const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
240
+ return this.doFetch(
241
+ method,
242
+ path2,
243
+ body,
244
+ headers,
245
+ options?.signal,
246
+ options?.allowHttpErrors ?? false
247
+ );
248
+ }
249
+ async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
250
+ const url = `${this.baseUrl}${path2}`;
251
+ let lastError;
252
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
253
+ if (attempt > 0) {
254
+ const delay = this.retryBackoffMs * Math.pow(2, attempt - 1);
255
+ await sleep(delay);
256
+ }
257
+ this.abortController = new AbortController();
258
+ const timeoutId = setTimeout(
259
+ () => this.abortController?.abort(),
260
+ this.timeoutMs
261
+ );
262
+ const combinedSignal = signal ? anySignal([signal, this.abortController.signal]) : this.abortController.signal;
263
+ try {
264
+ const response = await fetch(url, {
265
+ method,
266
+ headers,
267
+ body,
268
+ signal: combinedSignal
269
+ });
270
+ clearTimeout(timeoutId);
271
+ if (response.ok) return response;
272
+ if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
273
+ lastError = new RemoteAPIError(
274
+ response.status,
275
+ await response.text().catch(() => "")
276
+ );
277
+ continue;
278
+ }
279
+ if (allowHttpErrors) {
280
+ return response;
281
+ }
282
+ const errorBody = await response.text().catch(() => "");
283
+ throwMappedError(response.status, errorBody, path2);
284
+ } catch (err) {
285
+ clearTimeout(timeoutId);
286
+ if (err instanceof RemoteAPIError || err instanceof SandboxNotFoundError || err instanceof PoolNotFoundError || err instanceof PoolInUseError) {
287
+ throw err;
288
+ }
289
+ if (signal?.aborted) {
290
+ throw new SandboxConnectionError("Request aborted");
291
+ }
292
+ lastError = err instanceof Error ? err : new Error(String(err));
293
+ if (attempt >= this.maxRetries) {
294
+ throw new SandboxConnectionError(lastError.message);
295
+ }
296
+ }
297
+ }
298
+ throw new SandboxConnectionError(lastError?.message ?? "Request failed");
299
+ }
300
+ };
301
+ function hasHeader(headers, name) {
302
+ const lowered = name.toLowerCase();
303
+ return Object.keys(headers).some((key) => key.toLowerCase() === lowered);
304
+ }
305
+ function normalizeRequestBody(body) {
306
+ if (body == null) {
307
+ return void 0;
308
+ }
309
+ if (body instanceof Uint8Array) {
310
+ return Uint8Array.from(body).buffer;
311
+ }
312
+ return body;
313
+ }
314
+ function throwMappedError(status, body, path2) {
315
+ let message = body;
316
+ try {
317
+ const parsed = JSON.parse(body);
318
+ if (parsed.message) message = parsed.message;
319
+ else if (parsed.error) message = parsed.error;
320
+ } catch {
321
+ }
322
+ if (status === 404) {
323
+ if (path2.includes("sandbox-pools") || path2.includes("pools")) {
324
+ const match = path2.match(/sandbox-pools\/([^/]+)/);
325
+ if (match) throw new PoolNotFoundError(match[1]);
326
+ }
327
+ if (path2.includes("sandboxes")) {
328
+ const match = path2.match(/sandboxes\/([^/]+)/);
329
+ if (match) throw new SandboxNotFoundError(match[1]);
330
+ }
331
+ throw new RemoteAPIError(404, message);
332
+ }
333
+ if (status === 409) {
334
+ if (path2.includes("sandbox-pools") || path2.includes("pools")) {
335
+ const match = path2.match(/sandbox-pools\/([^/]+)/);
336
+ if (match) throw new PoolInUseError(match[1], message);
337
+ }
338
+ }
339
+ throw new RemoteAPIError(status, message);
340
+ }
341
+ function anySignal(signals) {
342
+ const controller = new AbortController();
343
+ for (const signal of signals) {
344
+ if (signal.aborted) {
345
+ controller.abort(signal.reason);
346
+ return controller.signal;
347
+ }
348
+ signal.addEventListener("abort", () => controller.abort(signal.reason), {
349
+ once: true
350
+ });
351
+ }
352
+ return controller.signal;
353
+ }
354
+ function sleep(ms) {
355
+ return new Promise((resolve) => setTimeout(resolve, ms));
356
+ }
357
+
358
+ // src/sse.ts
359
+ async function* parseSSEMessages(stream, signal) {
360
+ const reader = stream.getReader();
361
+ const decoder = new TextDecoder();
362
+ let buffer = "";
363
+ try {
364
+ while (true) {
365
+ if (signal?.aborted) break;
366
+ const { done, value } = await reader.read();
367
+ if (done) break;
368
+ buffer += decoder.decode(value, { stream: true });
369
+ const parts = buffer.split(/\r?\n\r?\n/);
370
+ buffer = parts.pop() ?? "";
371
+ for (const part of parts) {
372
+ const lines = part.split(/\r?\n/);
373
+ const dataLines = [];
374
+ let event;
375
+ let id;
376
+ for (const line of lines) {
377
+ if (!line || line.startsWith(":")) continue;
378
+ const separator = line.indexOf(":");
379
+ const field = separator === -1 ? line : line.slice(0, separator);
380
+ let value2 = separator === -1 ? "" : line.slice(separator + 1);
381
+ if (value2.startsWith(" ")) {
382
+ value2 = value2.slice(1);
383
+ }
384
+ if (field === "data") {
385
+ dataLines.push(value2);
386
+ } else if (field === "event") {
387
+ event = value2;
388
+ } else if (field === "id") {
389
+ id = value2;
390
+ }
391
+ }
392
+ if (dataLines.length > 0 || event || id) {
393
+ yield { data: dataLines.join("\n"), event, id };
394
+ }
395
+ }
396
+ }
397
+ } finally {
398
+ reader.releaseLock();
399
+ }
400
+ }
401
+ async function* parseSSEStream(stream, signal) {
402
+ for await (const message of parseSSEMessages(stream, signal)) {
403
+ if (!message.data) continue;
404
+ try {
405
+ yield JSON.parse(message.data);
406
+ } catch {
407
+ }
408
+ }
409
+ }
410
+
411
+ // src/url.ts
412
+ function trimTrailingSlashes(url) {
413
+ return url.endsWith("/") ? url.slice(0, -1) : url;
414
+ }
415
+ function isLocalhost(apiUrl) {
416
+ try {
417
+ const parsed = new URL(apiUrl);
418
+ return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
423
+ function resolveProxyUrl(apiUrl) {
424
+ const explicit = process.env.TENSORLAKE_SANDBOX_PROXY_URL;
425
+ if (explicit) return explicit;
426
+ if (isLocalhost(apiUrl)) return "http://localhost:9443";
427
+ try {
428
+ const parsed = new URL(apiUrl);
429
+ const host = parsed.hostname;
430
+ if (host.startsWith("api.")) {
431
+ const proxyHost = "sandbox." + host.slice(4);
432
+ return `${parsed.protocol}//${proxyHost}`;
433
+ }
434
+ } catch {
435
+ }
436
+ return SANDBOX_PROXY_URL;
437
+ }
438
+ function resolveProxyTarget(proxyUrl, sandboxId) {
439
+ try {
440
+ const parsed = new URL(proxyUrl);
441
+ const host = parsed.hostname;
442
+ if (host === "localhost" || host === "127.0.0.1") {
443
+ return {
444
+ baseUrl: trimTrailingSlashes(proxyUrl),
445
+ hostHeader: `${sandboxId}.local`
446
+ };
447
+ }
448
+ const port = parsed.port ? `:${parsed.port}` : "";
449
+ return {
450
+ baseUrl: `${parsed.protocol}//${sandboxId}.${host}${port}`,
451
+ hostHeader: void 0
452
+ };
453
+ } catch {
454
+ return {
455
+ baseUrl: `${trimTrailingSlashes(proxyUrl)}/${sandboxId}`,
456
+ hostHeader: void 0
457
+ };
458
+ }
459
+ }
460
+ function lifecyclePath(path2, isLocal, namespace) {
461
+ if (isLocal) {
462
+ return `/v1/namespaces/${namespace}/${path2}`;
463
+ }
464
+ return `/${path2}`;
465
+ }
466
+
467
+ // src/sandbox.ts
468
+ var import_ws = __toESM(require("ws"), 1);
469
+ var PTY_OP_DATA = 0;
470
+ var PTY_OP_RESIZE = 1;
471
+ var PTY_OP_READY = 2;
472
+ var PTY_OP_EXIT = 3;
473
+ var Pty = class {
474
+ sessionId;
475
+ token;
476
+ wsUrl;
477
+ wsHeaders;
478
+ killSession;
479
+ socket = null;
480
+ connectPromise = null;
481
+ intentionalDisconnect = false;
482
+ exitCode = null;
483
+ waitSettled = false;
484
+ dataHandlers = /* @__PURE__ */ new Set();
485
+ exitHandlers = /* @__PURE__ */ new Set();
486
+ waitPromise;
487
+ resolveWait;
488
+ rejectWait;
489
+ constructor(options) {
490
+ this.sessionId = options.sessionId;
491
+ this.token = options.token;
492
+ this.wsUrl = options.wsUrl;
493
+ this.wsHeaders = options.wsHeaders;
494
+ this.killSession = options.killSession;
495
+ this.waitPromise = new Promise((resolve, reject) => {
496
+ this.resolveWait = resolve;
497
+ this.rejectWait = reject;
498
+ });
499
+ }
500
+ onData(handler) {
501
+ this.dataHandlers.add(handler);
502
+ return () => this.dataHandlers.delete(handler);
503
+ }
504
+ onExit(handler) {
505
+ this.exitHandlers.add(handler);
506
+ if (this.exitCode != null) {
507
+ queueMicrotask(() => handler(this.exitCode));
508
+ }
509
+ return () => this.exitHandlers.delete(handler);
510
+ }
511
+ async connect() {
512
+ if (this.socket?.readyState === import_ws.default.OPEN) {
513
+ return this;
514
+ }
515
+ if (this.connectPromise) {
516
+ return this.connectPromise;
517
+ }
518
+ this.intentionalDisconnect = false;
519
+ this.connectPromise = new Promise((resolve, reject) => {
520
+ let opened = false;
521
+ const socket = new import_ws.default(this.wsUrl, {
522
+ headers: this.wsHeaders
523
+ });
524
+ this.socket = socket;
525
+ socket.on("open", async () => {
526
+ try {
527
+ await sendPtyFrame(socket, Buffer.from([PTY_OP_READY]));
528
+ opened = true;
529
+ resolve(this);
530
+ } catch (error) {
531
+ reject(error);
532
+ }
533
+ });
534
+ socket.on("message", (message) => {
535
+ const bytes = normalizePtyMessage(message);
536
+ const opcode = bytes[0];
537
+ if (opcode === PTY_OP_DATA) {
538
+ const payload = bytes.subarray(1);
539
+ for (const handler of this.dataHandlers) {
540
+ handler(payload);
541
+ }
542
+ return;
543
+ }
544
+ if (opcode === PTY_OP_EXIT && bytes.length >= 5) {
545
+ this.finishWait(bytes.readInt32BE(1));
546
+ }
547
+ });
548
+ socket.on("close", (code, reason) => {
549
+ const closeReason = Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
550
+ if (this.socket === socket) {
551
+ this.socket = null;
552
+ }
553
+ this.connectPromise = null;
554
+ if (this.exitCode != null) {
555
+ this.finishWait(this.exitCode);
556
+ return;
557
+ }
558
+ if (closeReason.startsWith("exit:")) {
559
+ const parsed = Number.parseInt(closeReason.slice(5), 10);
560
+ this.finishWait(Number.isNaN(parsed) ? -1 : parsed);
561
+ return;
562
+ }
563
+ if (this.intentionalDisconnect) {
564
+ this.intentionalDisconnect = false;
565
+ return;
566
+ }
567
+ if (!opened) {
568
+ reject(new SandboxError(
569
+ `PTY websocket closed before READY completed: ${code} ${closeReason || "no reason"}`
570
+ ));
571
+ return;
572
+ }
573
+ if (closeReason === "session terminated") {
574
+ this.failWait(new SandboxError("PTY session terminated"));
575
+ return;
576
+ }
577
+ this.failWait(
578
+ new SandboxError(
579
+ `PTY websocket closed unexpectedly: ${code} ${closeReason || "no reason"}`
580
+ )
581
+ );
582
+ });
583
+ socket.on("error", (error) => {
584
+ if (!opened) {
585
+ reject(error);
586
+ }
587
+ });
588
+ });
589
+ return this.connectPromise;
590
+ }
591
+ async sendInput(input) {
592
+ const socket = this.requireOpenSocket();
593
+ await sendPtyFrame(socket, encodePtyInput(input));
594
+ }
595
+ async resize(cols, rows) {
596
+ const socket = this.requireOpenSocket();
597
+ await sendPtyFrame(socket, encodePtyResize(cols, rows));
598
+ }
599
+ disconnect(code = 1e3, reason = "client disconnect") {
600
+ if (!this.socket) return;
601
+ this.intentionalDisconnect = true;
602
+ this.socket.close(code, reason);
603
+ }
604
+ wait() {
605
+ return this.waitPromise;
606
+ }
607
+ async kill() {
608
+ await this.killSession();
609
+ }
610
+ requireOpenSocket() {
611
+ if (!this.socket || this.socket.readyState !== import_ws.default.OPEN) {
612
+ throw new SandboxError("PTY is not connected");
613
+ }
614
+ return this.socket;
615
+ }
616
+ finishWait(exitCode) {
617
+ if (this.waitSettled) return;
618
+ this.waitSettled = true;
619
+ this.exitCode = exitCode;
620
+ for (const handler of this.exitHandlers) {
621
+ handler(exitCode);
622
+ }
623
+ this.resolveWait(exitCode);
624
+ }
625
+ failWait(error) {
626
+ if (this.waitSettled) return;
627
+ this.waitSettled = true;
628
+ this.rejectWait(error);
629
+ }
630
+ };
631
+ function normalizePtyMessage(message) {
632
+ if (Buffer.isBuffer(message)) return message;
633
+ if (Array.isArray(message)) {
634
+ return Buffer.concat(message.map((part) => Buffer.from(part)));
635
+ }
636
+ return Buffer.from(message);
637
+ }
638
+ function encodePtyInput(input) {
639
+ const payload = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
640
+ return Buffer.concat([Buffer.from([PTY_OP_DATA]), payload]);
641
+ }
642
+ function encodePtyResize(cols, rows) {
643
+ const frame = Buffer.alloc(5);
644
+ frame[0] = PTY_OP_RESIZE;
645
+ frame.writeUInt16BE(cols, 1);
646
+ frame.writeUInt16BE(rows, 3);
647
+ return frame;
648
+ }
649
+ function sendPtyFrame(socket, frame) {
650
+ return new Promise((resolve, reject) => {
651
+ socket.send(frame, (error) => error ? reject(error) : resolve());
652
+ });
653
+ }
654
+ var Sandbox = class {
655
+ sandboxId;
656
+ http;
657
+ baseUrl;
658
+ wsHeaders;
659
+ ownsSandbox = false;
660
+ lifecycleClient = null;
661
+ constructor(options) {
662
+ this.sandboxId = options.sandboxId;
663
+ const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
664
+ const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
665
+ this.baseUrl = baseUrl;
666
+ this.wsHeaders = {};
667
+ if (options.apiKey) {
668
+ this.wsHeaders.Authorization = `Bearer ${options.apiKey}`;
669
+ }
670
+ if (options.organizationId) {
671
+ this.wsHeaders["X-Forwarded-Organization-Id"] = options.organizationId;
672
+ }
673
+ if (options.projectId) {
674
+ this.wsHeaders["X-Forwarded-Project-Id"] = options.projectId;
675
+ }
676
+ if (hostHeader) {
677
+ this.wsHeaders.Host = hostHeader;
678
+ }
679
+ this.http = new HttpClient({
680
+ baseUrl,
681
+ apiKey: options.apiKey,
682
+ organizationId: options.organizationId,
683
+ projectId: options.projectId,
684
+ hostHeader
685
+ });
686
+ }
687
+ /** @internal Used by SandboxClient.createAndConnect to set ownership. */
688
+ _setOwner(client) {
689
+ this.ownsSandbox = true;
690
+ this.lifecycleClient = client;
691
+ }
692
+ close() {
693
+ this.http.close();
694
+ }
695
+ async terminate() {
696
+ const client = this.lifecycleClient;
697
+ this.ownsSandbox = false;
698
+ this.lifecycleClient = null;
699
+ this.close();
700
+ if (client) {
701
+ await client.delete(this.sandboxId);
702
+ }
703
+ }
704
+ // --- High-level convenience ---
705
+ async run(command, options) {
706
+ const proc = await this.startProcess(command, {
707
+ args: options?.args,
708
+ env: options?.env,
709
+ workingDir: options?.workingDir
710
+ });
711
+ const deadline = options?.timeout ? Date.now() + options.timeout * 1e3 : null;
712
+ let info;
713
+ while (true) {
714
+ info = await this.getProcess(proc.pid);
715
+ if (info.status !== "running" /* RUNNING */) break;
716
+ if (deadline && Date.now() > deadline) {
717
+ await this.killProcess(proc.pid);
718
+ throw new SandboxError(`Command timed out after ${options.timeout}s`);
719
+ }
720
+ await sleep2(100);
721
+ }
722
+ const stdoutResp = await this.getStdout(proc.pid);
723
+ const stderrResp = await this.getStderr(proc.pid);
724
+ let exitCode;
725
+ if (info.exitCode != null) {
726
+ exitCode = info.exitCode;
727
+ } else if (info.signal != null) {
728
+ exitCode = -info.signal;
729
+ } else {
730
+ exitCode = -1;
731
+ }
732
+ return {
733
+ exitCode,
734
+ stdout: stdoutResp.lines.join("\n"),
735
+ stderr: stderrResp.lines.join("\n")
736
+ };
737
+ }
738
+ // --- Process management ---
739
+ async startProcess(command, options) {
740
+ const payload = { command };
741
+ if (options?.args != null) payload.args = options.args;
742
+ if (options?.env != null) payload.env = options.env;
743
+ if (options?.workingDir != null) payload.working_dir = options.workingDir;
744
+ if (options?.stdinMode != null && options.stdinMode !== "closed" /* CLOSED */) {
745
+ payload.stdin_mode = options.stdinMode;
746
+ }
747
+ if (options?.stdoutMode != null && options.stdoutMode !== "capture" /* CAPTURE */) {
748
+ payload.stdout_mode = options.stdoutMode;
749
+ }
750
+ if (options?.stderrMode != null && options.stderrMode !== "capture" /* CAPTURE */) {
751
+ payload.stderr_mode = options.stderrMode;
752
+ }
753
+ const raw = await this.http.requestJson(
754
+ "POST",
755
+ "/api/v1/processes",
756
+ { body: payload }
757
+ );
758
+ return fromSnakeKeys(raw);
759
+ }
760
+ async listProcesses() {
761
+ const raw = await this.http.requestJson(
762
+ "GET",
763
+ "/api/v1/processes"
764
+ );
765
+ return (raw.processes ?? []).map((p) => fromSnakeKeys(p));
766
+ }
767
+ async getProcess(pid) {
768
+ const raw = await this.http.requestJson(
769
+ "GET",
770
+ `/api/v1/processes/${pid}`
771
+ );
772
+ return fromSnakeKeys(raw);
773
+ }
774
+ async killProcess(pid) {
775
+ await this.http.requestJson("DELETE", `/api/v1/processes/${pid}`);
776
+ }
777
+ async sendSignal(pid, signal) {
778
+ const raw = await this.http.requestJson(
779
+ "POST",
780
+ `/api/v1/processes/${pid}/signal`,
781
+ { body: { signal } }
782
+ );
783
+ return fromSnakeKeys(raw);
784
+ }
785
+ // --- Process I/O ---
786
+ async writeStdin(pid, data) {
787
+ await this.http.requestBytes("POST", `/api/v1/processes/${pid}/stdin`, {
788
+ body: data,
789
+ contentType: "application/octet-stream"
790
+ });
791
+ }
792
+ async closeStdin(pid) {
793
+ await this.http.requestJson("POST", `/api/v1/processes/${pid}/stdin/close`);
794
+ }
795
+ async getStdout(pid) {
796
+ const raw = await this.http.requestJson(
797
+ "GET",
798
+ `/api/v1/processes/${pid}/stdout`
799
+ );
800
+ return fromSnakeKeys(raw);
801
+ }
802
+ async getStderr(pid) {
803
+ const raw = await this.http.requestJson(
804
+ "GET",
805
+ `/api/v1/processes/${pid}/stderr`
806
+ );
807
+ return fromSnakeKeys(raw);
808
+ }
809
+ async getOutput(pid) {
810
+ const raw = await this.http.requestJson(
811
+ "GET",
812
+ `/api/v1/processes/${pid}/output`
813
+ );
814
+ return fromSnakeKeys(raw);
815
+ }
816
+ // --- Streaming (SSE) ---
817
+ async *followStdout(pid, options) {
818
+ const stream = await this.http.requestStream(
819
+ "GET",
820
+ `/api/v1/processes/${pid}/stdout/follow`,
821
+ options
822
+ );
823
+ for await (const raw of parseSSEStream(
824
+ stream,
825
+ options?.signal
826
+ )) {
827
+ yield fromSnakeKeys(raw);
828
+ }
829
+ }
830
+ async *followStderr(pid, options) {
831
+ const stream = await this.http.requestStream(
832
+ "GET",
833
+ `/api/v1/processes/${pid}/stderr/follow`,
834
+ options
835
+ );
836
+ for await (const raw of parseSSEStream(
837
+ stream,
838
+ options?.signal
839
+ )) {
840
+ yield fromSnakeKeys(raw);
841
+ }
842
+ }
843
+ async *followOutput(pid, options) {
844
+ const stream = await this.http.requestStream(
845
+ "GET",
846
+ `/api/v1/processes/${pid}/output/follow`,
847
+ options
848
+ );
849
+ for await (const raw of parseSSEStream(
850
+ stream,
851
+ options?.signal
852
+ )) {
853
+ yield fromSnakeKeys(raw);
854
+ }
855
+ }
856
+ // --- File operations ---
857
+ async readFile(path2) {
858
+ return this.http.requestBytes(
859
+ "GET",
860
+ `/api/v1/files?path=${encodeURIComponent(path2)}`
861
+ );
862
+ }
863
+ async writeFile(path2, content) {
864
+ await this.http.requestBytes(
865
+ "PUT",
866
+ `/api/v1/files?path=${encodeURIComponent(path2)}`,
867
+ { body: content, contentType: "application/octet-stream" }
868
+ );
869
+ }
870
+ async deleteFile(path2) {
871
+ await this.http.requestJson(
872
+ "DELETE",
873
+ `/api/v1/files?path=${encodeURIComponent(path2)}`
874
+ );
875
+ }
876
+ async listDirectory(path2) {
877
+ const raw = await this.http.requestJson(
878
+ "GET",
879
+ `/api/v1/files/list?path=${encodeURIComponent(path2)}`
880
+ );
881
+ return fromSnakeKeys(raw);
882
+ }
883
+ // --- PTY ---
884
+ async createPtySession(options) {
885
+ const payload = {
886
+ command: options.command,
887
+ rows: options.rows ?? 24,
888
+ cols: options.cols ?? 80
889
+ };
890
+ if (options.args != null) payload.args = options.args;
891
+ if (options.env != null) payload.env = options.env;
892
+ if (options.workingDir != null) payload.working_dir = options.workingDir;
893
+ const raw = await this.http.requestJson(
894
+ "POST",
895
+ "/api/v1/pty",
896
+ { body: payload }
897
+ );
898
+ return fromSnakeKeys(raw);
899
+ }
900
+ async createPty(options) {
901
+ const { onData, onExit, ...createOptions } = options;
902
+ const session = await this.createPtySession(createOptions);
903
+ try {
904
+ return await this.connectPty(session.sessionId, session.token, { onData, onExit });
905
+ } catch (error) {
906
+ try {
907
+ await this.http.requestResponse("DELETE", `/api/v1/pty/${session.sessionId}`);
908
+ } catch {
909
+ }
910
+ throw error;
911
+ }
912
+ }
913
+ async connectPty(sessionId, token, options) {
914
+ const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
915
+ const authToken = wsUrl.searchParams.get("token") ?? token;
916
+ const pty = new Pty({
917
+ sessionId,
918
+ token: authToken,
919
+ wsUrl: wsUrl.toString(),
920
+ wsHeaders: {
921
+ ...this.wsHeaders,
922
+ "X-PTY-Token": authToken
923
+ },
924
+ killSession: async () => {
925
+ await this.http.requestResponse("DELETE", `/api/v1/pty/${sessionId}`);
926
+ }
927
+ });
928
+ if (options?.onData) {
929
+ pty.onData(options.onData);
930
+ }
931
+ if (options?.onExit) {
932
+ pty.onExit(options.onExit);
933
+ }
934
+ await pty.connect();
935
+ return pty;
936
+ }
937
+ ptyWsUrl(sessionId, token) {
938
+ let wsBase;
939
+ if (this.baseUrl.startsWith("https://")) {
940
+ wsBase = "wss://" + this.baseUrl.slice(8);
941
+ } else if (this.baseUrl.startsWith("http://")) {
942
+ wsBase = "ws://" + this.baseUrl.slice(7);
943
+ } else {
944
+ wsBase = this.baseUrl;
945
+ }
946
+ return `${wsBase}/api/v1/pty/${sessionId}/ws?token=${token}`;
947
+ }
948
+ // --- Health ---
949
+ async health() {
950
+ const raw = await this.http.requestJson(
951
+ "GET",
952
+ "/api/v1/health"
953
+ );
954
+ return fromSnakeKeys(raw);
955
+ }
956
+ async info() {
957
+ const raw = await this.http.requestJson(
958
+ "GET",
959
+ "/api/v1/info"
960
+ );
961
+ return fromSnakeKeys(raw);
962
+ }
963
+ };
964
+ function sleep2(ms) {
965
+ return new Promise((resolve) => setTimeout(resolve, ms));
966
+ }
967
+
968
+ // src/client.ts
969
+ var SandboxClient = class _SandboxClient {
970
+ http;
971
+ apiUrl;
972
+ apiKey;
973
+ organizationId;
974
+ projectId;
975
+ namespace;
976
+ local;
977
+ constructor(options) {
978
+ this.apiUrl = options?.apiUrl ?? API_URL;
979
+ this.apiKey = options?.apiKey ?? API_KEY;
980
+ this.organizationId = options?.organizationId;
981
+ this.projectId = options?.projectId;
982
+ this.namespace = options?.namespace ?? NAMESPACE;
983
+ this.local = isLocalhost(this.apiUrl);
984
+ this.http = new HttpClient({
985
+ baseUrl: this.apiUrl,
986
+ apiKey: this.apiKey,
987
+ organizationId: this.organizationId,
988
+ projectId: this.projectId,
989
+ maxRetries: options?.maxRetries ?? MAX_RETRIES,
990
+ retryBackoffMs: options?.retryBackoffMs ?? RETRY_BACKOFF_MS
991
+ });
992
+ }
993
+ /** Create a client for the TensorLake cloud platform. */
994
+ static forCloud(options) {
995
+ return new _SandboxClient({
996
+ apiUrl: options?.apiUrl ?? "https://api.tensorlake.ai",
997
+ apiKey: options?.apiKey,
998
+ organizationId: options?.organizationId,
999
+ projectId: options?.projectId
1000
+ });
1001
+ }
1002
+ /** Create a client for a local Indexify server. */
1003
+ static forLocalhost(options) {
1004
+ return new _SandboxClient({
1005
+ apiUrl: options?.apiUrl ?? "http://localhost:8900",
1006
+ namespace: options?.namespace ?? "default"
1007
+ });
1008
+ }
1009
+ close() {
1010
+ this.http.close();
1011
+ }
1012
+ // --- Path helper ---
1013
+ path(subpath) {
1014
+ return lifecyclePath(subpath, this.local, this.namespace);
1015
+ }
1016
+ // --- Sandbox CRUD ---
1017
+ async create(options) {
1018
+ const body = {
1019
+ resources: {
1020
+ cpus: options?.cpus ?? 1,
1021
+ memory_mb: options?.memoryMb ?? 1024,
1022
+ ephemeral_disk_mb: options?.ephemeralDiskMb ?? 1024
1023
+ }
1024
+ };
1025
+ if (options?.image != null) body.image = options.image;
1026
+ if (options?.secretNames != null) body.secret_names = options.secretNames;
1027
+ if (options?.timeoutSecs != null) body.timeout_secs = options.timeoutSecs;
1028
+ if (options?.entrypoint != null) body.entrypoint = options.entrypoint;
1029
+ if (options?.snapshotId != null) body.snapshot_id = options.snapshotId;
1030
+ if (options?.name != null) body.name = options.name;
1031
+ if (options?.allowInternetAccess === false || options?.allowOut != null || options?.denyOut != null) {
1032
+ body.network = {
1033
+ allow_internet_access: options?.allowInternetAccess ?? true,
1034
+ allow_out: options?.allowOut ?? [],
1035
+ deny_out: options?.denyOut ?? []
1036
+ };
1037
+ }
1038
+ const raw = await this.http.requestJson(
1039
+ "POST",
1040
+ this.path("sandboxes"),
1041
+ { body }
1042
+ );
1043
+ return fromSnakeKeys(raw, "sandboxId");
1044
+ }
1045
+ async get(sandboxId) {
1046
+ const raw = await this.http.requestJson(
1047
+ "GET",
1048
+ this.path(`sandboxes/${sandboxId}`)
1049
+ );
1050
+ return fromSnakeKeys(raw, "sandboxId");
1051
+ }
1052
+ async list() {
1053
+ const raw = await this.http.requestJson(
1054
+ "GET",
1055
+ this.path("sandboxes")
1056
+ );
1057
+ return (raw.sandboxes ?? []).map(
1058
+ (s) => fromSnakeKeys(s, "sandboxId")
1059
+ );
1060
+ }
1061
+ async update(sandboxId, options) {
1062
+ const body = {};
1063
+ if (options.name != null) body.name = options.name;
1064
+ if (options.allowUnauthenticatedAccess != null) {
1065
+ body.allow_unauthenticated_access = options.allowUnauthenticatedAccess;
1066
+ }
1067
+ if (options.exposedPorts != null) {
1068
+ body.exposed_ports = normalizeUserPorts(options.exposedPorts);
1069
+ }
1070
+ if (Object.keys(body).length === 0) {
1071
+ throw new SandboxError("At least one sandbox update field must be provided.");
1072
+ }
1073
+ const raw = await this.http.requestJson(
1074
+ "PATCH",
1075
+ this.path(`sandboxes/${sandboxId}`),
1076
+ { body }
1077
+ );
1078
+ return fromSnakeKeys(raw, "sandboxId");
1079
+ }
1080
+ async getPortAccess(sandboxId) {
1081
+ const info = await this.get(sandboxId);
1082
+ return {
1083
+ allowUnauthenticatedAccess: info.allowUnauthenticatedAccess ?? false,
1084
+ exposedPorts: dedupeAndSortPorts(info.exposedPorts ?? []),
1085
+ sandboxUrl: info.sandboxUrl
1086
+ };
1087
+ }
1088
+ async exposePorts(sandboxId, ports, options) {
1089
+ const requestedPorts = normalizeUserPorts(ports);
1090
+ const current = await this.getPortAccess(sandboxId);
1091
+ const desiredPorts = dedupeAndSortPorts([
1092
+ ...current.exposedPorts,
1093
+ ...requestedPorts
1094
+ ]);
1095
+ return this.update(sandboxId, {
1096
+ allowUnauthenticatedAccess: options?.allowUnauthenticatedAccess ?? current.allowUnauthenticatedAccess,
1097
+ exposedPorts: desiredPorts
1098
+ });
1099
+ }
1100
+ async unexposePorts(sandboxId, ports) {
1101
+ const requestedPorts = normalizeUserPorts(ports);
1102
+ const current = await this.getPortAccess(sandboxId);
1103
+ const toRemove = new Set(requestedPorts);
1104
+ const desiredPorts = current.exposedPorts.filter((port) => !toRemove.has(port));
1105
+ return this.update(sandboxId, {
1106
+ allowUnauthenticatedAccess: desiredPorts.length ? current.allowUnauthenticatedAccess : false,
1107
+ exposedPorts: desiredPorts
1108
+ });
1109
+ }
1110
+ async delete(sandboxId) {
1111
+ await this.http.requestJson(
1112
+ "DELETE",
1113
+ this.path(`sandboxes/${sandboxId}`)
1114
+ );
1115
+ }
1116
+ async suspend(sandboxId) {
1117
+ await this.http.requestResponse(
1118
+ "POST",
1119
+ this.path(`sandboxes/${sandboxId}/suspend`)
1120
+ );
1121
+ }
1122
+ async resume(sandboxId) {
1123
+ await this.http.requestResponse(
1124
+ "POST",
1125
+ this.path(`sandboxes/${sandboxId}/resume`)
1126
+ );
1127
+ }
1128
+ async claim(poolId) {
1129
+ const raw = await this.http.requestJson(
1130
+ "POST",
1131
+ this.path(`sandbox-pools/${poolId}/sandboxes`)
1132
+ );
1133
+ return fromSnakeKeys(raw, "sandboxId");
1134
+ }
1135
+ // --- Snapshots ---
1136
+ async snapshot(sandboxId) {
1137
+ const raw = await this.http.requestJson(
1138
+ "POST",
1139
+ this.path(`sandboxes/${sandboxId}/snapshot`)
1140
+ );
1141
+ return fromSnakeKeys(raw, "snapshotId");
1142
+ }
1143
+ async getSnapshot(snapshotId) {
1144
+ const raw = await this.http.requestJson(
1145
+ "GET",
1146
+ this.path(`snapshots/${snapshotId}`)
1147
+ );
1148
+ return fromSnakeKeys(raw, "snapshotId");
1149
+ }
1150
+ async listSnapshots() {
1151
+ const raw = await this.http.requestJson(
1152
+ "GET",
1153
+ this.path("snapshots")
1154
+ );
1155
+ return (raw.snapshots ?? []).map(
1156
+ (s) => fromSnakeKeys(s, "snapshotId")
1157
+ );
1158
+ }
1159
+ async deleteSnapshot(snapshotId) {
1160
+ await this.http.requestJson(
1161
+ "DELETE",
1162
+ this.path(`snapshots/${snapshotId}`)
1163
+ );
1164
+ }
1165
+ async snapshotAndWait(sandboxId, options) {
1166
+ const timeout = options?.timeout ?? 300;
1167
+ const pollInterval = options?.pollInterval ?? 1;
1168
+ const result = await this.snapshot(sandboxId);
1169
+ const deadline = Date.now() + timeout * 1e3;
1170
+ while (Date.now() < deadline) {
1171
+ const info = await this.getSnapshot(result.snapshotId);
1172
+ if (info.status === "completed" /* COMPLETED */) return info;
1173
+ if (info.status === "failed" /* FAILED */) {
1174
+ throw new SandboxError(
1175
+ `Snapshot ${result.snapshotId} failed: ${info.error}`
1176
+ );
1177
+ }
1178
+ await sleep3(pollInterval * 1e3);
1179
+ }
1180
+ throw new SandboxError(
1181
+ `Snapshot ${result.snapshotId} did not complete within ${timeout}s`
1182
+ );
1183
+ }
1184
+ // --- Pools ---
1185
+ async createPool(options) {
1186
+ const body = {
1187
+ image: options.image,
1188
+ resources: {
1189
+ cpus: options.cpus ?? 1,
1190
+ memory_mb: options.memoryMb ?? 1024,
1191
+ ephemeral_disk_mb: options.ephemeralDiskMb ?? 1024
1192
+ },
1193
+ timeout_secs: options.timeoutSecs ?? 0
1194
+ };
1195
+ if (options.secretNames != null) body.secret_names = options.secretNames;
1196
+ if (options.entrypoint != null) body.entrypoint = options.entrypoint;
1197
+ if (options.maxContainers != null) body.max_containers = options.maxContainers;
1198
+ if (options.warmContainers != null) body.warm_containers = options.warmContainers;
1199
+ const raw = await this.http.requestJson(
1200
+ "POST",
1201
+ this.path("sandbox-pools"),
1202
+ { body }
1203
+ );
1204
+ return fromSnakeKeys(raw, "poolId");
1205
+ }
1206
+ async getPool(poolId) {
1207
+ const raw = await this.http.requestJson(
1208
+ "GET",
1209
+ this.path(`sandbox-pools/${poolId}`)
1210
+ );
1211
+ return fromSnakeKeys(raw, "poolId");
1212
+ }
1213
+ async listPools() {
1214
+ const raw = await this.http.requestJson(
1215
+ "GET",
1216
+ this.path("sandbox-pools")
1217
+ );
1218
+ return (raw.pools ?? []).map(
1219
+ (p) => fromSnakeKeys(p, "poolId")
1220
+ );
1221
+ }
1222
+ async updatePool(poolId, options) {
1223
+ const body = {
1224
+ image: options.image,
1225
+ resources: {
1226
+ cpus: options.cpus ?? 1,
1227
+ memory_mb: options.memoryMb ?? 1024,
1228
+ ephemeral_disk_mb: options.ephemeralDiskMb ?? 1024
1229
+ },
1230
+ timeout_secs: options.timeoutSecs ?? 0
1231
+ };
1232
+ if (options.secretNames != null) body.secret_names = options.secretNames;
1233
+ if (options.entrypoint != null) body.entrypoint = options.entrypoint;
1234
+ if (options.maxContainers != null) body.max_containers = options.maxContainers;
1235
+ if (options.warmContainers != null) body.warm_containers = options.warmContainers;
1236
+ const raw = await this.http.requestJson(
1237
+ "PUT",
1238
+ this.path(`sandbox-pools/${poolId}`),
1239
+ { body }
1240
+ );
1241
+ return fromSnakeKeys(raw, "poolId");
1242
+ }
1243
+ async deletePool(poolId) {
1244
+ await this.http.requestJson(
1245
+ "DELETE",
1246
+ this.path(`sandbox-pools/${poolId}`)
1247
+ );
1248
+ }
1249
+ // --- Connect ---
1250
+ connect(identifier, proxyUrl) {
1251
+ const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
1252
+ return new Sandbox({
1253
+ sandboxId: identifier,
1254
+ proxyUrl: resolvedProxy,
1255
+ apiKey: this.apiKey,
1256
+ organizationId: this.organizationId,
1257
+ projectId: this.projectId
1258
+ });
1259
+ }
1260
+ async createAndConnect(options) {
1261
+ const startupTimeout = options?.startupTimeout ?? 60;
1262
+ let result;
1263
+ if (options?.poolId != null) {
1264
+ result = await this.claim(options.poolId);
1265
+ } else {
1266
+ result = await this.create(options);
1267
+ }
1268
+ const deadline = Date.now() + startupTimeout * 1e3;
1269
+ while (Date.now() < deadline) {
1270
+ const info = await this.get(result.sandboxId);
1271
+ if (info.status === "running" /* RUNNING */) {
1272
+ const sandbox = this.connect(result.sandboxId, options?.proxyUrl);
1273
+ sandbox._setOwner(this);
1274
+ return sandbox;
1275
+ }
1276
+ if (info.status === "terminated" /* TERMINATED */) {
1277
+ throw new SandboxError(
1278
+ `Sandbox ${result.sandboxId} terminated during startup`
1279
+ );
1280
+ }
1281
+ await sleep3(500);
1282
+ }
1283
+ try {
1284
+ await this.delete(result.sandboxId);
1285
+ } catch {
1286
+ }
1287
+ throw new SandboxError(
1288
+ `Sandbox ${result.sandboxId} did not start within ${startupTimeout}s`
1289
+ );
1290
+ }
1291
+ };
1292
+ function sleep3(ms) {
1293
+ return new Promise((resolve) => setTimeout(resolve, ms));
1294
+ }
1295
+ var RESERVED_SANDBOX_MANAGEMENT_PORT = 9501;
1296
+ function normalizeUserPorts(ports) {
1297
+ return dedupeAndSortPorts(ports.map(validateUserPort));
1298
+ }
1299
+ function validateUserPort(port) {
1300
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1301
+ throw new SandboxError(`invalid port '${port}'`);
1302
+ }
1303
+ if (port === RESERVED_SANDBOX_MANAGEMENT_PORT) {
1304
+ throw new SandboxError("port 9501 is reserved for sandbox management");
1305
+ }
1306
+ return port;
1307
+ }
1308
+ function dedupeAndSortPorts(ports) {
1309
+ return [...new Set(ports)].sort((a, b) => a - b);
1310
+ }
1311
+
1312
+ // src/image.ts
1313
+ var import_node_crypto = require("crypto");
1314
+ var ImageBuildOperationType = {
1315
+ ADD: "ADD",
1316
+ COPY: "COPY",
1317
+ ENV: "ENV",
1318
+ RUN: "RUN",
1319
+ WORKDIR: "WORKDIR"
1320
+ };
1321
+ function renderOptions(options) {
1322
+ const entries = Object.entries(options);
1323
+ if (entries.length === 0) {
1324
+ return "";
1325
+ }
1326
+ return ` ${entries.map(([key, value]) => `--${key}=${value}`).join(" ")}`;
1327
+ }
1328
+ function renderBuildOp(op) {
1329
+ const options = renderOptions(op.options);
1330
+ if (op.type === ImageBuildOperationType.ENV) {
1331
+ return `ENV${options} ${op.args[0]}=${JSON.stringify(op.args[1])}`;
1332
+ }
1333
+ return `${op.type}${options} ${op.args.join(" ")}`;
1334
+ }
1335
+ function dockerfileContent(image) {
1336
+ const lines = image.baseImage == null ? [] : [`FROM ${image.baseImage}`];
1337
+ lines.push(...image.buildOperations.map((op) => renderBuildOp(op)));
1338
+ return lines.join("\n");
1339
+ }
1340
+
1341
+ // src/sandbox-image.ts
1342
+ var BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
1343
+ var IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
1344
+ "CMD",
1345
+ "ENTRYPOINT",
1346
+ "EXPOSE",
1347
+ "HEALTHCHECK",
1348
+ "LABEL",
1349
+ "STOPSIGNAL",
1350
+ "VOLUME"
1351
+ ]);
1352
+ var UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
1353
+ "ARG",
1354
+ "ONBUILD",
1355
+ "SHELL",
1356
+ "USER"
1357
+ ]);
1358
+ function defaultRegisteredName(dockerfilePath) {
1359
+ const parsed = import_node_path.default.parse(dockerfilePath);
1360
+ if (parsed.name.toLowerCase() === "dockerfile") {
1361
+ const parentName = import_node_path.default.basename(import_node_path.default.dirname(dockerfilePath)).trim();
1362
+ return parentName || "sandbox-image";
1363
+ }
1364
+ return parsed.name || "sandbox-image";
1365
+ }
1366
+ function logicalDockerfileLines(dockerfileText) {
1367
+ const logicalLines = [];
1368
+ let parts = [];
1369
+ let startLine = null;
1370
+ for (const [index, rawLine] of dockerfileText.split(/\r?\n/).entries()) {
1371
+ const lineNumber = index + 1;
1372
+ const stripped = rawLine.trim();
1373
+ if (parts.length === 0 && (!stripped || stripped.startsWith("#"))) {
1374
+ continue;
1375
+ }
1376
+ if (startLine == null) {
1377
+ startLine = lineNumber;
1378
+ }
1379
+ let line = rawLine.replace(/\s+$/, "");
1380
+ const continued = line.endsWith("\\");
1381
+ if (continued) {
1382
+ line = line.slice(0, -1);
1383
+ }
1384
+ const normalized = line.trim();
1385
+ if (normalized && !normalized.startsWith("#")) {
1386
+ parts.push(normalized);
1387
+ }
1388
+ if (continued) {
1389
+ continue;
1390
+ }
1391
+ if (parts.length > 0) {
1392
+ logicalLines.push({
1393
+ lineNumber: startLine ?? lineNumber,
1394
+ line: parts.join(" ")
1395
+ });
1396
+ }
1397
+ parts = [];
1398
+ startLine = null;
1399
+ }
1400
+ if (parts.length > 0) {
1401
+ logicalLines.push({
1402
+ lineNumber: startLine ?? 1,
1403
+ line: parts.join(" ")
1404
+ });
1405
+ }
1406
+ return logicalLines;
1407
+ }
1408
+ function splitInstruction(line, lineNumber) {
1409
+ const trimmed = line.trim();
1410
+ if (!trimmed) {
1411
+ throw new Error(`line ${lineNumber}: empty Dockerfile instruction`);
1412
+ }
1413
+ const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
1414
+ if (!match) {
1415
+ throw new Error(`line ${lineNumber}: invalid Dockerfile instruction`);
1416
+ }
1417
+ return {
1418
+ keyword: match[1].toUpperCase(),
1419
+ value: (match[2] ?? "").trim()
1420
+ };
1421
+ }
1422
+ function shellSplit(input) {
1423
+ const tokens = [];
1424
+ let current = "";
1425
+ let quote = null;
1426
+ let escape = false;
1427
+ for (let i = 0; i < input.length; i++) {
1428
+ const char = input[i];
1429
+ if (escape) {
1430
+ current += char;
1431
+ escape = false;
1432
+ continue;
1433
+ }
1434
+ if (quote == null) {
1435
+ if (/\s/.test(char)) {
1436
+ if (current) {
1437
+ tokens.push(current);
1438
+ current = "";
1439
+ }
1440
+ continue;
1441
+ }
1442
+ if (char === "'" || char === '"') {
1443
+ quote = char;
1444
+ continue;
1445
+ }
1446
+ if (char === "\\") {
1447
+ escape = true;
1448
+ continue;
1449
+ }
1450
+ current += char;
1451
+ continue;
1452
+ }
1453
+ if (quote === "'") {
1454
+ if (char === "'") {
1455
+ quote = null;
1456
+ } else {
1457
+ current += char;
1458
+ }
1459
+ continue;
1460
+ }
1461
+ if (char === '"') {
1462
+ quote = null;
1463
+ continue;
1464
+ }
1465
+ if (char === "\\") {
1466
+ const next = input[++i];
1467
+ if (next == null) {
1468
+ throw new Error(`unterminated escape sequence in '${input}'`);
1469
+ }
1470
+ current += next;
1471
+ continue;
1472
+ }
1473
+ current += char;
1474
+ }
1475
+ if (escape) {
1476
+ throw new Error(`unterminated escape sequence in '${input}'`);
1477
+ }
1478
+ if (quote != null) {
1479
+ throw new Error(`unterminated quoted string in '${input}'`);
1480
+ }
1481
+ if (current) {
1482
+ tokens.push(current);
1483
+ }
1484
+ return tokens;
1485
+ }
1486
+ function shellQuote(value) {
1487
+ if (!value) {
1488
+ return "''";
1489
+ }
1490
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1491
+ }
1492
+ function stripLeadingFlags(value) {
1493
+ const flags = {};
1494
+ let remaining = value.trimStart();
1495
+ while (remaining.startsWith("--")) {
1496
+ const firstSpace = remaining.indexOf(" ");
1497
+ if (firstSpace === -1) {
1498
+ throw new Error(`invalid Dockerfile flag syntax: ${value}`);
1499
+ }
1500
+ const token = remaining.slice(0, firstSpace);
1501
+ const rest = remaining.slice(firstSpace + 1).trimStart();
1502
+ const flagBody = token.slice(2);
1503
+ if (flagBody.includes("=")) {
1504
+ const [key, flagValue2] = flagBody.split(/=(.*)/s, 2);
1505
+ flags[key] = flagValue2;
1506
+ remaining = rest;
1507
+ continue;
1508
+ }
1509
+ const [flagValue, ...restTokens] = shellSplit(rest);
1510
+ if (flagValue == null) {
1511
+ throw new Error(`missing value for Dockerfile flag '${token}'`);
1512
+ }
1513
+ flags[flagBody] = flagValue;
1514
+ remaining = restTokens.join(" ");
1515
+ }
1516
+ return { flags, remaining };
1517
+ }
1518
+ function parseFromValue(value, lineNumber) {
1519
+ const { remaining } = stripLeadingFlags(value);
1520
+ const tokens = shellSplit(remaining);
1521
+ if (tokens.length === 0) {
1522
+ throw new Error(`line ${lineNumber}: FROM must include a base image`);
1523
+ }
1524
+ if (tokens.length > 1 && tokens[1].toLowerCase() !== "as") {
1525
+ throw new Error(`line ${lineNumber}: unsupported FROM syntax '${value}'`);
1526
+ }
1527
+ return tokens[0];
1528
+ }
1529
+ function parseCopyLikeValues(value, lineNumber, keyword) {
1530
+ const { flags, remaining } = stripLeadingFlags(value);
1531
+ if ("from" in flags) {
1532
+ throw new Error(
1533
+ `line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
1534
+ );
1535
+ }
1536
+ const payload = remaining.trim();
1537
+ if (!payload) {
1538
+ throw new Error(
1539
+ `line ${lineNumber}: ${keyword} must include source and destination`
1540
+ );
1541
+ }
1542
+ let parts;
1543
+ if (payload.startsWith("[")) {
1544
+ let parsed;
1545
+ try {
1546
+ parsed = JSON.parse(payload);
1547
+ } catch (error) {
1548
+ throw new Error(
1549
+ `line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
1550
+ );
1551
+ }
1552
+ if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
1553
+ throw new Error(
1554
+ `line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
1555
+ );
1556
+ }
1557
+ parts = parsed;
1558
+ } else {
1559
+ parts = shellSplit(payload);
1560
+ if (parts.length < 2) {
1561
+ throw new Error(
1562
+ `line ${lineNumber}: ${keyword} must include at least one source and one destination`
1563
+ );
1564
+ }
1565
+ }
1566
+ return {
1567
+ flags,
1568
+ sources: parts.slice(0, -1),
1569
+ destination: parts[parts.length - 1]
1570
+ };
1571
+ }
1572
+ function parseEnvPairs(value, lineNumber) {
1573
+ const tokens = shellSplit(value);
1574
+ if (tokens.length === 0) {
1575
+ throw new Error(`line ${lineNumber}: ENV must include a key and value`);
1576
+ }
1577
+ if (tokens.every((token) => token.includes("="))) {
1578
+ return tokens.map((token) => {
1579
+ const [key, envValue] = token.split(/=(.*)/s, 2);
1580
+ if (!key) {
1581
+ throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
1582
+ }
1583
+ return [key, envValue];
1584
+ });
1585
+ }
1586
+ if (tokens.length < 2) {
1587
+ throw new Error(`line ${lineNumber}: ENV must include a key and value`);
1588
+ }
1589
+ return [[tokens[0], tokens.slice(1).join(" ")]];
1590
+ }
1591
+ function resolveContainerPath(containerPath, workingDir) {
1592
+ if (!containerPath) {
1593
+ return workingDir;
1594
+ }
1595
+ const normalized = containerPath.startsWith("/") ? import_node_path.default.posix.normalize(containerPath) : import_node_path.default.posix.normalize(import_node_path.default.posix.join(workingDir, containerPath));
1596
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
1597
+ }
1598
+ function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
1599
+ let baseImage;
1600
+ const instructions = [];
1601
+ for (const logicalLine of logicalDockerfileLines(dockerfileText)) {
1602
+ const { keyword, value } = splitInstruction(
1603
+ logicalLine.line,
1604
+ logicalLine.lineNumber
1605
+ );
1606
+ if (keyword === "FROM") {
1607
+ if (baseImage != null) {
1608
+ throw new Error(
1609
+ `line ${logicalLine.lineNumber}: multi-stage Dockerfiles are not supported for sandbox image creation`
1610
+ );
1611
+ }
1612
+ baseImage = parseFromValue(value, logicalLine.lineNumber);
1613
+ continue;
1614
+ }
1615
+ if (UNSUPPORTED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
1616
+ throw new Error(
1617
+ `line ${logicalLine.lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
1618
+ );
1619
+ }
1620
+ instructions.push({
1621
+ keyword,
1622
+ value,
1623
+ lineNumber: logicalLine.lineNumber
1624
+ });
1625
+ }
1626
+ if (!baseImage) {
1627
+ throw new Error("Dockerfile must contain a FROM instruction");
1628
+ }
1629
+ return {
1630
+ dockerfilePath,
1631
+ contextDir,
1632
+ registeredName: registeredName ?? defaultRegisteredName(dockerfilePath),
1633
+ dockerfileText,
1634
+ baseImage,
1635
+ instructions
1636
+ };
1637
+ }
1638
+ async function loadDockerfilePlan(dockerfilePath, registeredName) {
1639
+ const resolvedPath = import_node_path.default.resolve(dockerfilePath);
1640
+ const fileStats = await (0, import_promises.stat)(resolvedPath).catch(() => null);
1641
+ if (!fileStats?.isFile()) {
1642
+ throw new Error(`Dockerfile not found: ${dockerfilePath}`);
1643
+ }
1644
+ const dockerfileText = await (0, import_promises.readFile)(resolvedPath, "utf8");
1645
+ return buildPlanFromDockerfileText(
1646
+ dockerfileText,
1647
+ resolvedPath,
1648
+ import_node_path.default.dirname(resolvedPath),
1649
+ registeredName
1650
+ );
1651
+ }
1652
+ function loadImagePlan(image, options = {}) {
1653
+ const contextDir = import_node_path.default.resolve(options.contextDir ?? process.cwd());
1654
+ const dockerfileText = dockerfileContent(image);
1655
+ const logicalLines = logicalDockerfileLines(dockerfileText);
1656
+ const instructions = image.baseImage == null ? logicalLines : logicalLines.slice(1);
1657
+ return {
1658
+ dockerfilePath: import_node_path.default.join(contextDir, "Dockerfile"),
1659
+ contextDir,
1660
+ registeredName: options.registeredName ?? image.name,
1661
+ dockerfileText,
1662
+ baseImage: image.baseImage ?? void 0,
1663
+ instructions: instructions.map(({ line, lineNumber }) => {
1664
+ const parsed = splitInstruction(line, lineNumber);
1665
+ return {
1666
+ keyword: parsed.keyword,
1667
+ value: parsed.value,
1668
+ lineNumber
1669
+ };
1670
+ })
1671
+ };
1672
+ }
1673
+ function defaultEmit(event) {
1674
+ process.stdout.write(`${JSON.stringify(event)}
1675
+ `);
1676
+ }
1677
+ function debugEnabled() {
1678
+ return ["1", "true", "yes", "on"].includes(
1679
+ (process.env.TENSORLAKE_DEBUG ?? "").toLowerCase()
1680
+ );
1681
+ }
1682
+ function buildContextFromEnv() {
1683
+ return {
1684
+ apiUrl: process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai",
1685
+ apiKey: process.env.TENSORLAKE_API_KEY,
1686
+ personalAccessToken: process.env.TENSORLAKE_PAT,
1687
+ namespace: process.env.INDEXIFY_NAMESPACE ?? "default",
1688
+ organizationId: process.env.TENSORLAKE_ORGANIZATION_ID,
1689
+ projectId: process.env.TENSORLAKE_PROJECT_ID,
1690
+ debug: debugEnabled()
1691
+ };
1692
+ }
1693
+ function createDefaultClient(context) {
1694
+ return new SandboxClient({
1695
+ apiUrl: context.apiUrl,
1696
+ apiKey: context.apiKey ?? context.personalAccessToken,
1697
+ organizationId: context.organizationId,
1698
+ projectId: context.projectId,
1699
+ namespace: context.namespace
1700
+ });
1701
+ }
1702
+ async function runChecked(sandbox, command, args, env, workingDir) {
1703
+ const result = await sandbox.run(command, {
1704
+ args,
1705
+ env,
1706
+ workingDir
1707
+ });
1708
+ if (result.exitCode !== 0) {
1709
+ throw new Error(
1710
+ `Command '${command} ${args.join(" ")}' failed with exit code ${result.exitCode}`
1711
+ );
1712
+ }
1713
+ return result;
1714
+ }
1715
+ async function runStreaming(sandbox, emit, sleep4, command, args = [], env, workingDir) {
1716
+ const proc = await sandbox.startProcess(command, {
1717
+ args,
1718
+ env,
1719
+ workingDir
1720
+ });
1721
+ let stdoutSeen = 0;
1722
+ let stderrSeen = 0;
1723
+ let info;
1724
+ while (true) {
1725
+ const stdoutResp = await sandbox.getStdout(proc.pid);
1726
+ emitOutputLines(emit, "stdout", stdoutResp, stdoutSeen);
1727
+ stdoutSeen = stdoutResp.lines.length;
1728
+ const stderrResp = await sandbox.getStderr(proc.pid);
1729
+ emitOutputLines(emit, "stderr", stderrResp, stderrSeen);
1730
+ stderrSeen = stderrResp.lines.length;
1731
+ info = await sandbox.getProcess(proc.pid);
1732
+ if (info.status !== "running" /* RUNNING */) {
1733
+ const finalStdout = await sandbox.getStdout(proc.pid);
1734
+ emitOutputLines(emit, "stdout", finalStdout, stdoutSeen);
1735
+ stdoutSeen = finalStdout.lines.length;
1736
+ const finalStderr = await sandbox.getStderr(proc.pid);
1737
+ emitOutputLines(emit, "stderr", finalStderr, stderrSeen);
1738
+ break;
1739
+ }
1740
+ await sleep4(300);
1741
+ }
1742
+ for (let i = 0; i < 10; i++) {
1743
+ if (info.exitCode != null || info.signal != null) {
1744
+ break;
1745
+ }
1746
+ await sleep4(200);
1747
+ info = await sandbox.getProcess(proc.pid);
1748
+ }
1749
+ const exitCode = info.exitCode != null ? info.exitCode : info.signal != null ? -info.signal : 0;
1750
+ if (exitCode !== 0) {
1751
+ throw new Error(
1752
+ `Command '${command} ${args.join(" ")}' failed with exit code ${exitCode}`
1753
+ );
1754
+ }
1755
+ }
1756
+ function emitOutputLines(emit, stream, response, seen) {
1757
+ for (const line of response.lines.slice(seen)) {
1758
+ emit({ type: "build_log", stream, message: line });
1759
+ }
1760
+ }
1761
+ function isPathWithinContext(contextDir, localPath) {
1762
+ const relative = import_node_path.default.relative(contextDir, localPath);
1763
+ return relative === "" || !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
1764
+ }
1765
+ function resolveContextSourcePath(contextDir, source) {
1766
+ const resolvedContextDir = import_node_path.default.resolve(contextDir);
1767
+ const resolvedSource = import_node_path.default.resolve(resolvedContextDir, source);
1768
+ if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
1769
+ throw new Error(`Local path escapes the build context: ${source}`);
1770
+ }
1771
+ return resolvedSource;
1772
+ }
1773
+ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
1774
+ const fileStats = await (0, import_promises.stat)(localPath).catch(() => null);
1775
+ if (!fileStats) {
1776
+ throw new Error(`Local path not found: ${localPath}`);
1777
+ }
1778
+ if (fileStats.isFile()) {
1779
+ await runChecked(sandbox, "mkdir", ["-p", import_node_path.default.posix.dirname(remotePath)]);
1780
+ await sandbox.writeFile(remotePath, await (0, import_promises.readFile)(localPath));
1781
+ return;
1782
+ }
1783
+ if (!fileStats.isDirectory()) {
1784
+ throw new Error(`Local path not found: ${localPath}`);
1785
+ }
1786
+ const entries = await (0, import_promises.readdir)(localPath, { withFileTypes: true });
1787
+ for (const entry of entries) {
1788
+ const sourcePath = import_node_path.default.join(localPath, entry.name);
1789
+ const destinationPath = import_node_path.default.posix.join(remotePath, entry.name);
1790
+ if (entry.isDirectory()) {
1791
+ await runChecked(sandbox, "mkdir", ["-p", destinationPath]);
1792
+ await copyLocalPathToSandbox(sandbox, sourcePath, destinationPath);
1793
+ } else if (entry.isFile()) {
1794
+ await runChecked(
1795
+ sandbox,
1796
+ "mkdir",
1797
+ ["-p", import_node_path.default.posix.dirname(destinationPath)]
1798
+ );
1799
+ await sandbox.writeFile(destinationPath, await (0, import_promises.readFile)(sourcePath));
1800
+ }
1801
+ }
1802
+ }
1803
+ async function persistEnvVar(sandbox, processEnv, key, value) {
1804
+ const exportLine = `export ${key}=${shellQuote(value)}`;
1805
+ await runChecked(
1806
+ sandbox,
1807
+ "sh",
1808
+ ["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
1809
+ processEnv
1810
+ );
1811
+ }
1812
+ async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
1813
+ const destinationPath = resolveContainerPath(destination, workingDir);
1814
+ if (sources.length > 1 && !destinationPath.endsWith("/")) {
1815
+ throw new Error(
1816
+ `${keyword} with multiple sources requires a directory destination ending in '/'`
1817
+ );
1818
+ }
1819
+ for (const source of sources) {
1820
+ const localSource = resolveContextSourcePath(contextDir, source);
1821
+ const localStats = await (0, import_promises.stat)(localSource).catch(() => null);
1822
+ if (!localStats) {
1823
+ throw new Error(`Local path not found: ${localSource}`);
1824
+ }
1825
+ let remoteDestination = destinationPath;
1826
+ if (sources.length > 1) {
1827
+ remoteDestination = import_node_path.default.posix.join(
1828
+ destinationPath.replace(/\/$/, ""),
1829
+ import_node_path.default.posix.basename(source.replace(/\/$/, ""))
1830
+ );
1831
+ } else if (localStats.isFile() && destinationPath.endsWith("/")) {
1832
+ remoteDestination = import_node_path.default.posix.join(
1833
+ destinationPath.replace(/\/$/, ""),
1834
+ import_node_path.default.basename(source)
1835
+ );
1836
+ }
1837
+ emit({
1838
+ type: "status",
1839
+ message: `${keyword} ${source} -> ${remoteDestination}`
1840
+ });
1841
+ await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
1842
+ }
1843
+ }
1844
+ async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep4) {
1845
+ let destinationPath = resolveContainerPath(destination, workingDir);
1846
+ const parsedUrl = new URL(url);
1847
+ const fileName = import_node_path.default.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
1848
+ if (destinationPath.endsWith("/")) {
1849
+ destinationPath = import_node_path.default.posix.join(destinationPath.replace(/\/$/, ""), fileName);
1850
+ }
1851
+ const parentDir = import_node_path.default.posix.dirname(destinationPath) || "/";
1852
+ emit({
1853
+ type: "status",
1854
+ message: `ADD ${url} -> ${destinationPath}`
1855
+ });
1856
+ await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
1857
+ await runStreaming(
1858
+ sandbox,
1859
+ emit,
1860
+ sleep4,
1861
+ "sh",
1862
+ [
1863
+ "-c",
1864
+ `curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
1865
+ ],
1866
+ processEnv,
1867
+ workingDir
1868
+ );
1869
+ }
1870
+ async function executeDockerfilePlan(sandbox, plan, emit, sleep4) {
1871
+ const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
1872
+ let workingDir = "/";
1873
+ for (const instruction of plan.instructions) {
1874
+ const { keyword, value, lineNumber } = instruction;
1875
+ if (keyword === "RUN") {
1876
+ emit({ type: "status", message: `RUN ${value}` });
1877
+ await runStreaming(
1878
+ sandbox,
1879
+ emit,
1880
+ sleep4,
1881
+ "sh",
1882
+ ["-c", value],
1883
+ processEnv,
1884
+ workingDir
1885
+ );
1886
+ continue;
1887
+ }
1888
+ if (keyword === "WORKDIR") {
1889
+ const tokens = shellSplit(value);
1890
+ if (tokens.length !== 1) {
1891
+ throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
1892
+ }
1893
+ workingDir = resolveContainerPath(tokens[0], workingDir);
1894
+ emit({ type: "status", message: `WORKDIR ${workingDir}` });
1895
+ await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
1896
+ continue;
1897
+ }
1898
+ if (keyword === "ENV") {
1899
+ for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
1900
+ emit({ type: "status", message: `ENV ${key}=${envValue}` });
1901
+ processEnv[key] = envValue;
1902
+ await persistEnvVar(sandbox, processEnv, key, envValue);
1903
+ }
1904
+ continue;
1905
+ }
1906
+ if (keyword === "COPY") {
1907
+ const { sources, destination } = parseCopyLikeValues(
1908
+ value,
1909
+ lineNumber,
1910
+ keyword
1911
+ );
1912
+ await copyFromContext(
1913
+ sandbox,
1914
+ emit,
1915
+ plan.contextDir,
1916
+ sources,
1917
+ destination,
1918
+ workingDir,
1919
+ keyword
1920
+ );
1921
+ continue;
1922
+ }
1923
+ if (keyword === "ADD") {
1924
+ const { sources, destination } = parseCopyLikeValues(
1925
+ value,
1926
+ lineNumber,
1927
+ keyword
1928
+ );
1929
+ if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
1930
+ await addUrlToSandbox(
1931
+ sandbox,
1932
+ emit,
1933
+ sources[0],
1934
+ destination,
1935
+ workingDir,
1936
+ processEnv,
1937
+ sleep4
1938
+ );
1939
+ } else {
1940
+ await copyFromContext(
1941
+ sandbox,
1942
+ emit,
1943
+ plan.contextDir,
1944
+ sources,
1945
+ destination,
1946
+ workingDir,
1947
+ keyword
1948
+ );
1949
+ }
1950
+ continue;
1951
+ }
1952
+ if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
1953
+ emit({
1954
+ type: "warning",
1955
+ message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
1956
+ });
1957
+ continue;
1958
+ }
1959
+ throw new Error(
1960
+ `line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
1961
+ );
1962
+ }
1963
+ }
1964
+ async function registerImage(context, name, dockerfile, snapshotId, snapshotUri, isPublic) {
1965
+ if (!context.organizationId || !context.projectId) {
1966
+ throw new Error(
1967
+ "Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
1968
+ );
1969
+ }
1970
+ const bearerToken = context.apiKey ?? context.personalAccessToken;
1971
+ if (!bearerToken) {
1972
+ throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
1973
+ }
1974
+ const baseUrl = context.apiUrl.replace(/\/+$/, "");
1975
+ const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
1976
+ const headers = {
1977
+ Authorization: `Bearer ${bearerToken}`,
1978
+ "Content-Type": "application/json"
1979
+ };
1980
+ if (context.personalAccessToken && !context.apiKey) {
1981
+ headers["X-Forwarded-Organization-Id"] = context.organizationId;
1982
+ headers["X-Forwarded-Project-Id"] = context.projectId;
1983
+ }
1984
+ const response = await fetch(url, {
1985
+ method: "POST",
1986
+ headers,
1987
+ body: JSON.stringify({
1988
+ name,
1989
+ dockerfile,
1990
+ snapshotId,
1991
+ snapshotUri,
1992
+ isPublic
1993
+ })
1994
+ });
1995
+ if (!response.ok) {
1996
+ throw new Error(
1997
+ `${response.status} ${response.statusText}: ${await response.text()}`
1998
+ );
1999
+ }
2000
+ const text = await response.text();
2001
+ return text ? JSON.parse(text) : {};
2002
+ }
2003
+ async function createSandboxImage(source, options = {}, deps = {}) {
2004
+ const emit = deps.emit ?? defaultEmit;
2005
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2006
+ const context = buildContextFromEnv();
2007
+ const clientFactory = deps.createClient ?? createDefaultClient;
2008
+ const register = deps.registerImage ?? ((...args) => registerImage(...args));
2009
+ const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
2010
+ emit({ type: "status", message: `Loading ${sourceLabel}...` });
2011
+ const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
2012
+ emit({
2013
+ type: "status",
2014
+ message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
2015
+ });
2016
+ const client = clientFactory(context);
2017
+ let sandbox;
2018
+ try {
2019
+ sandbox = await client.createAndConnect({
2020
+ ...plan.baseImage == null ? {} : { image: plan.baseImage },
2021
+ cpus: options.cpus ?? 2,
2022
+ memoryMb: options.memoryMb ?? 4096
2023
+ });
2024
+ emit({
2025
+ type: "status",
2026
+ message: `Materializing image in sandbox ${sandbox.sandboxId}...`
2027
+ });
2028
+ await executeDockerfilePlan(sandbox, plan, emit, sleep4);
2029
+ emit({ type: "status", message: "Creating snapshot..." });
2030
+ const snapshot = await client.snapshotAndWait(sandbox.sandboxId);
2031
+ emit({
2032
+ type: "snapshot_created",
2033
+ snapshot_id: snapshot.snapshotId,
2034
+ snapshot_uri: snapshot.snapshotUri ?? null
2035
+ });
2036
+ if (!snapshot.snapshotUri) {
2037
+ throw new Error(
2038
+ `Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
2039
+ );
2040
+ }
2041
+ emit({
2042
+ type: "status",
2043
+ message: `Registering image '${plan.registeredName}'...`
2044
+ });
2045
+ const result = await register(
2046
+ context,
2047
+ plan.registeredName,
2048
+ plan.dockerfileText,
2049
+ snapshot.snapshotId,
2050
+ snapshot.snapshotUri,
2051
+ options.isPublic ?? false
2052
+ );
2053
+ emit({
2054
+ type: "image_registered",
2055
+ name: plan.registeredName,
2056
+ image_id: typeof result.id === "string" && result.id || typeof result.templateId === "string" && result.templateId || ""
2057
+ });
2058
+ emit({ type: "done" });
2059
+ return result;
2060
+ } finally {
2061
+ if (sandbox) {
2062
+ try {
2063
+ await sandbox.terminate();
2064
+ } catch {
2065
+ }
2066
+ }
2067
+ client.close();
2068
+ }
2069
+ }
2070
+ async function runCreateSandboxImageCli(argv = process.argv.slice(2)) {
2071
+ const parsed = (0, import_node_util.parseArgs)({
2072
+ args: argv,
2073
+ allowPositionals: true,
2074
+ options: {
2075
+ name: { type: "string", short: "n" },
2076
+ cpus: { type: "string" },
2077
+ memory: { type: "string" },
2078
+ public: { type: "boolean", default: false }
2079
+ }
2080
+ });
2081
+ const dockerfilePath = parsed.positionals[0];
2082
+ if (!dockerfilePath) {
2083
+ throw new Error("Usage: tensorlake-create-sandbox-image <dockerfile_path> [--name NAME] [--cpus N] [--memory MB] [--public]");
2084
+ }
2085
+ const cpus = parsed.values.cpus != null ? Number(parsed.values.cpus) : void 0;
2086
+ const memoryMb = parsed.values.memory != null ? Number(parsed.values.memory) : void 0;
2087
+ if (cpus != null && !Number.isFinite(cpus)) {
2088
+ throw new Error(`Invalid --cpus value: ${parsed.values.cpus}`);
2089
+ }
2090
+ if (memoryMb != null && !Number.isInteger(memoryMb)) {
2091
+ throw new Error(`Invalid --memory value: ${parsed.values.memory}`);
2092
+ }
2093
+ await createSandboxImage(dockerfilePath, {
2094
+ registeredName: parsed.values.name,
2095
+ cpus,
2096
+ memoryMb,
2097
+ isPublic: parsed.values.public
2098
+ });
2099
+ }
2100
+ // Annotate the CommonJS export names for ESM import in node:
2101
+ 0 && (module.exports = {
2102
+ createSandboxImage,
2103
+ defaultRegisteredName,
2104
+ loadDockerfilePlan,
2105
+ loadImagePlan,
2106
+ logicalDockerfileLines,
2107
+ runCreateSandboxImageCli
2108
+ });
2109
+ //# sourceMappingURL=sandbox-image.cjs.map