tensorlake 0.4.39 → 0.4.41
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/bin/tensorlake-create-sandbox-image.cjs +6 -5
- 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 +1204 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -239
- package/dist/index.d.ts +56 -239
- package/dist/index.js +1188 -36
- package/dist/index.js.map +1 -1
- package/dist/sandbox-image-DKPhc4Lv.d.cts +375 -0
- package/dist/sandbox-image-DKPhc4Lv.d.ts +375 -0
- package/dist/sandbox-image.cjs +2105 -0
- package/dist/sandbox-image.cjs.map +1 -0
- package/dist/sandbox-image.d.cts +1 -0
- package/dist/sandbox-image.d.ts +1 -0
- package/dist/sandbox-image.js +2065 -0
- package/dist/sandbox-image.js.map +1 -0
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -23,10 +33,13 @@ __export(index_exports, {
|
|
|
23
33
|
APIClient: () => APIClient,
|
|
24
34
|
CloudClient: () => CloudClient,
|
|
25
35
|
ContainerState: () => ContainerState,
|
|
36
|
+
Image: () => Image,
|
|
37
|
+
ImageBuildOperationType: () => ImageBuildOperationType,
|
|
26
38
|
OutputMode: () => OutputMode,
|
|
27
39
|
PoolInUseError: () => PoolInUseError,
|
|
28
40
|
PoolNotFoundError: () => PoolNotFoundError,
|
|
29
41
|
ProcessStatus: () => ProcessStatus,
|
|
42
|
+
Pty: () => Pty,
|
|
30
43
|
RemoteAPIError: () => RemoteAPIError,
|
|
31
44
|
RequestExecutionError: () => RequestExecutionError,
|
|
32
45
|
RequestFailedError: () => RequestFailedError,
|
|
@@ -39,7 +52,9 @@ __export(index_exports, {
|
|
|
39
52
|
SandboxNotFoundError: () => SandboxNotFoundError,
|
|
40
53
|
SandboxStatus: () => SandboxStatus,
|
|
41
54
|
SnapshotStatus: () => SnapshotStatus,
|
|
42
|
-
StdinMode: () => StdinMode
|
|
55
|
+
StdinMode: () => StdinMode,
|
|
56
|
+
createSandboxImage: () => createSandboxImage,
|
|
57
|
+
dockerfileContent: () => dockerfileContent
|
|
43
58
|
});
|
|
44
59
|
module.exports = __toCommonJS(index_exports);
|
|
45
60
|
|
|
@@ -164,8 +179,8 @@ var HttpClient = class {
|
|
|
164
179
|
this.abortController = null;
|
|
165
180
|
}
|
|
166
181
|
/** Make a JSON request, returning the parsed response body. */
|
|
167
|
-
async requestJson(method,
|
|
168
|
-
const response = await this.requestResponse(method,
|
|
182
|
+
async requestJson(method, path2, options) {
|
|
183
|
+
const response = await this.requestResponse(method, path2, {
|
|
169
184
|
json: options?.body,
|
|
170
185
|
headers: options?.headers,
|
|
171
186
|
signal: options?.signal
|
|
@@ -175,14 +190,14 @@ var HttpClient = class {
|
|
|
175
190
|
return JSON.parse(text);
|
|
176
191
|
}
|
|
177
192
|
/** Make a request returning raw bytes. */
|
|
178
|
-
async requestBytes(method,
|
|
193
|
+
async requestBytes(method, path2, options) {
|
|
179
194
|
const headers = { ...options?.headers ?? {} };
|
|
180
195
|
if (options?.contentType) {
|
|
181
196
|
headers["Content-Type"] = options.contentType;
|
|
182
197
|
}
|
|
183
198
|
const response = await this.requestResponse(
|
|
184
199
|
method,
|
|
185
|
-
|
|
200
|
+
path2,
|
|
186
201
|
{
|
|
187
202
|
body: options?.body,
|
|
188
203
|
headers,
|
|
@@ -193,10 +208,10 @@ var HttpClient = class {
|
|
|
193
208
|
return new Uint8Array(buffer);
|
|
194
209
|
}
|
|
195
210
|
/** Make a request and return the raw Response (for SSE streaming). */
|
|
196
|
-
async requestStream(method,
|
|
211
|
+
async requestStream(method, path2, options) {
|
|
197
212
|
const response = await this.requestResponse(
|
|
198
213
|
method,
|
|
199
|
-
|
|
214
|
+
path2,
|
|
200
215
|
{
|
|
201
216
|
headers: { Accept: "text/event-stream" },
|
|
202
217
|
signal: options?.signal
|
|
@@ -208,7 +223,7 @@ var HttpClient = class {
|
|
|
208
223
|
return response.body;
|
|
209
224
|
}
|
|
210
225
|
/** Make a request and return the raw Response. */
|
|
211
|
-
async requestResponse(method,
|
|
226
|
+
async requestResponse(method, path2, options) {
|
|
212
227
|
const headers = {
|
|
213
228
|
...this.headers,
|
|
214
229
|
...options?.headers ?? {}
|
|
@@ -220,15 +235,15 @@ var HttpClient = class {
|
|
|
220
235
|
const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
|
|
221
236
|
return this.doFetch(
|
|
222
237
|
method,
|
|
223
|
-
|
|
238
|
+
path2,
|
|
224
239
|
body,
|
|
225
240
|
headers,
|
|
226
241
|
options?.signal,
|
|
227
242
|
options?.allowHttpErrors ?? false
|
|
228
243
|
);
|
|
229
244
|
}
|
|
230
|
-
async doFetch(method,
|
|
231
|
-
const url = `${this.baseUrl}${
|
|
245
|
+
async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
|
|
246
|
+
const url = `${this.baseUrl}${path2}`;
|
|
232
247
|
let lastError;
|
|
233
248
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
234
249
|
if (attempt > 0) {
|
|
@@ -261,7 +276,7 @@ var HttpClient = class {
|
|
|
261
276
|
return response;
|
|
262
277
|
}
|
|
263
278
|
const errorBody = await response.text().catch(() => "");
|
|
264
|
-
throwMappedError(response.status, errorBody,
|
|
279
|
+
throwMappedError(response.status, errorBody, path2);
|
|
265
280
|
} catch (err) {
|
|
266
281
|
clearTimeout(timeoutId);
|
|
267
282
|
if (err instanceof RemoteAPIError || err instanceof SandboxNotFoundError || err instanceof PoolNotFoundError || err instanceof PoolInUseError) {
|
|
@@ -292,7 +307,7 @@ function normalizeRequestBody(body) {
|
|
|
292
307
|
}
|
|
293
308
|
return body;
|
|
294
309
|
}
|
|
295
|
-
function throwMappedError(status, body,
|
|
310
|
+
function throwMappedError(status, body, path2) {
|
|
296
311
|
let message = body;
|
|
297
312
|
try {
|
|
298
313
|
const parsed = JSON.parse(body);
|
|
@@ -301,19 +316,19 @@ function throwMappedError(status, body, path) {
|
|
|
301
316
|
} catch {
|
|
302
317
|
}
|
|
303
318
|
if (status === 404) {
|
|
304
|
-
if (
|
|
305
|
-
const match =
|
|
319
|
+
if (path2.includes("sandbox-pools") || path2.includes("pools")) {
|
|
320
|
+
const match = path2.match(/sandbox-pools\/([^/]+)/);
|
|
306
321
|
if (match) throw new PoolNotFoundError(match[1]);
|
|
307
322
|
}
|
|
308
|
-
if (
|
|
309
|
-
const match =
|
|
323
|
+
if (path2.includes("sandboxes")) {
|
|
324
|
+
const match = path2.match(/sandboxes\/([^/]+)/);
|
|
310
325
|
if (match) throw new SandboxNotFoundError(match[1]);
|
|
311
326
|
}
|
|
312
327
|
throw new RemoteAPIError(404, message);
|
|
313
328
|
}
|
|
314
329
|
if (status === 409) {
|
|
315
|
-
if (
|
|
316
|
-
const match =
|
|
330
|
+
if (path2.includes("sandbox-pools") || path2.includes("pools")) {
|
|
331
|
+
const match = path2.match(/sandbox-pools\/([^/]+)/);
|
|
317
332
|
if (match) throw new PoolInUseError(match[1], message);
|
|
318
333
|
}
|
|
319
334
|
}
|
|
@@ -341,6 +356,7 @@ var SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => {
|
|
|
341
356
|
SandboxStatus2["PENDING"] = "pending";
|
|
342
357
|
SandboxStatus2["RUNNING"] = "running";
|
|
343
358
|
SandboxStatus2["SNAPSHOTTING"] = "snapshotting";
|
|
359
|
+
SandboxStatus2["SUSPENDING"] = "suspending";
|
|
344
360
|
SandboxStatus2["SUSPENDED"] = "suspended";
|
|
345
361
|
SandboxStatus2["TERMINATED"] = "terminated";
|
|
346
362
|
return SandboxStatus2;
|
|
@@ -513,18 +529,205 @@ function resolveProxyTarget(proxyUrl, sandboxId) {
|
|
|
513
529
|
};
|
|
514
530
|
}
|
|
515
531
|
}
|
|
516
|
-
function lifecyclePath(
|
|
532
|
+
function lifecyclePath(path2, isLocal, namespace) {
|
|
517
533
|
if (isLocal) {
|
|
518
|
-
return `/v1/namespaces/${namespace}/${
|
|
534
|
+
return `/v1/namespaces/${namespace}/${path2}`;
|
|
519
535
|
}
|
|
520
|
-
return `/${
|
|
536
|
+
return `/${path2}`;
|
|
521
537
|
}
|
|
522
538
|
|
|
523
539
|
// src/sandbox.ts
|
|
540
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
541
|
+
var PTY_OP_DATA = 0;
|
|
542
|
+
var PTY_OP_RESIZE = 1;
|
|
543
|
+
var PTY_OP_READY = 2;
|
|
544
|
+
var PTY_OP_EXIT = 3;
|
|
545
|
+
var Pty = class {
|
|
546
|
+
sessionId;
|
|
547
|
+
token;
|
|
548
|
+
wsUrl;
|
|
549
|
+
wsHeaders;
|
|
550
|
+
killSession;
|
|
551
|
+
socket = null;
|
|
552
|
+
connectPromise = null;
|
|
553
|
+
intentionalDisconnect = false;
|
|
554
|
+
exitCode = null;
|
|
555
|
+
waitSettled = false;
|
|
556
|
+
dataHandlers = /* @__PURE__ */ new Set();
|
|
557
|
+
exitHandlers = /* @__PURE__ */ new Set();
|
|
558
|
+
waitPromise;
|
|
559
|
+
resolveWait;
|
|
560
|
+
rejectWait;
|
|
561
|
+
constructor(options) {
|
|
562
|
+
this.sessionId = options.sessionId;
|
|
563
|
+
this.token = options.token;
|
|
564
|
+
this.wsUrl = options.wsUrl;
|
|
565
|
+
this.wsHeaders = options.wsHeaders;
|
|
566
|
+
this.killSession = options.killSession;
|
|
567
|
+
this.waitPromise = new Promise((resolve, reject) => {
|
|
568
|
+
this.resolveWait = resolve;
|
|
569
|
+
this.rejectWait = reject;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
onData(handler) {
|
|
573
|
+
this.dataHandlers.add(handler);
|
|
574
|
+
return () => this.dataHandlers.delete(handler);
|
|
575
|
+
}
|
|
576
|
+
onExit(handler) {
|
|
577
|
+
this.exitHandlers.add(handler);
|
|
578
|
+
if (this.exitCode != null) {
|
|
579
|
+
queueMicrotask(() => handler(this.exitCode));
|
|
580
|
+
}
|
|
581
|
+
return () => this.exitHandlers.delete(handler);
|
|
582
|
+
}
|
|
583
|
+
async connect() {
|
|
584
|
+
if (this.socket?.readyState === import_ws.default.OPEN) {
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
if (this.connectPromise) {
|
|
588
|
+
return this.connectPromise;
|
|
589
|
+
}
|
|
590
|
+
this.intentionalDisconnect = false;
|
|
591
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
592
|
+
let opened = false;
|
|
593
|
+
const socket = new import_ws.default(this.wsUrl, {
|
|
594
|
+
headers: this.wsHeaders
|
|
595
|
+
});
|
|
596
|
+
this.socket = socket;
|
|
597
|
+
socket.on("open", async () => {
|
|
598
|
+
try {
|
|
599
|
+
await sendPtyFrame(socket, Buffer.from([PTY_OP_READY]));
|
|
600
|
+
opened = true;
|
|
601
|
+
resolve(this);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
reject(error);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
socket.on("message", (message) => {
|
|
607
|
+
const bytes = normalizePtyMessage(message);
|
|
608
|
+
const opcode = bytes[0];
|
|
609
|
+
if (opcode === PTY_OP_DATA) {
|
|
610
|
+
const payload = bytes.subarray(1);
|
|
611
|
+
for (const handler of this.dataHandlers) {
|
|
612
|
+
handler(payload);
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (opcode === PTY_OP_EXIT && bytes.length >= 5) {
|
|
617
|
+
this.finishWait(bytes.readInt32BE(1));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
socket.on("close", (code, reason) => {
|
|
621
|
+
const closeReason = Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
|
|
622
|
+
if (this.socket === socket) {
|
|
623
|
+
this.socket = null;
|
|
624
|
+
}
|
|
625
|
+
this.connectPromise = null;
|
|
626
|
+
if (this.exitCode != null) {
|
|
627
|
+
this.finishWait(this.exitCode);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (closeReason.startsWith("exit:")) {
|
|
631
|
+
const parsed = Number.parseInt(closeReason.slice(5), 10);
|
|
632
|
+
this.finishWait(Number.isNaN(parsed) ? -1 : parsed);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (this.intentionalDisconnect) {
|
|
636
|
+
this.intentionalDisconnect = false;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (!opened) {
|
|
640
|
+
reject(new SandboxError(
|
|
641
|
+
`PTY websocket closed before READY completed: ${code} ${closeReason || "no reason"}`
|
|
642
|
+
));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (closeReason === "session terminated") {
|
|
646
|
+
this.failWait(new SandboxError("PTY session terminated"));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this.failWait(
|
|
650
|
+
new SandboxError(
|
|
651
|
+
`PTY websocket closed unexpectedly: ${code} ${closeReason || "no reason"}`
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
socket.on("error", (error) => {
|
|
656
|
+
if (!opened) {
|
|
657
|
+
reject(error);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
return this.connectPromise;
|
|
662
|
+
}
|
|
663
|
+
async sendInput(input) {
|
|
664
|
+
const socket = this.requireOpenSocket();
|
|
665
|
+
await sendPtyFrame(socket, encodePtyInput(input));
|
|
666
|
+
}
|
|
667
|
+
async resize(cols, rows) {
|
|
668
|
+
const socket = this.requireOpenSocket();
|
|
669
|
+
await sendPtyFrame(socket, encodePtyResize(cols, rows));
|
|
670
|
+
}
|
|
671
|
+
disconnect(code = 1e3, reason = "client disconnect") {
|
|
672
|
+
if (!this.socket) return;
|
|
673
|
+
this.intentionalDisconnect = true;
|
|
674
|
+
this.socket.close(code, reason);
|
|
675
|
+
}
|
|
676
|
+
wait() {
|
|
677
|
+
return this.waitPromise;
|
|
678
|
+
}
|
|
679
|
+
async kill() {
|
|
680
|
+
await this.killSession();
|
|
681
|
+
}
|
|
682
|
+
requireOpenSocket() {
|
|
683
|
+
if (!this.socket || this.socket.readyState !== import_ws.default.OPEN) {
|
|
684
|
+
throw new SandboxError("PTY is not connected");
|
|
685
|
+
}
|
|
686
|
+
return this.socket;
|
|
687
|
+
}
|
|
688
|
+
finishWait(exitCode) {
|
|
689
|
+
if (this.waitSettled) return;
|
|
690
|
+
this.waitSettled = true;
|
|
691
|
+
this.exitCode = exitCode;
|
|
692
|
+
for (const handler of this.exitHandlers) {
|
|
693
|
+
handler(exitCode);
|
|
694
|
+
}
|
|
695
|
+
this.resolveWait(exitCode);
|
|
696
|
+
}
|
|
697
|
+
failWait(error) {
|
|
698
|
+
if (this.waitSettled) return;
|
|
699
|
+
this.waitSettled = true;
|
|
700
|
+
this.rejectWait(error);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
function normalizePtyMessage(message) {
|
|
704
|
+
if (Buffer.isBuffer(message)) return message;
|
|
705
|
+
if (Array.isArray(message)) {
|
|
706
|
+
return Buffer.concat(message.map((part) => Buffer.from(part)));
|
|
707
|
+
}
|
|
708
|
+
return Buffer.from(message);
|
|
709
|
+
}
|
|
710
|
+
function encodePtyInput(input) {
|
|
711
|
+
const payload = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
|
|
712
|
+
return Buffer.concat([Buffer.from([PTY_OP_DATA]), payload]);
|
|
713
|
+
}
|
|
714
|
+
function encodePtyResize(cols, rows) {
|
|
715
|
+
const frame = Buffer.alloc(5);
|
|
716
|
+
frame[0] = PTY_OP_RESIZE;
|
|
717
|
+
frame.writeUInt16BE(cols, 1);
|
|
718
|
+
frame.writeUInt16BE(rows, 3);
|
|
719
|
+
return frame;
|
|
720
|
+
}
|
|
721
|
+
function sendPtyFrame(socket, frame) {
|
|
722
|
+
return new Promise((resolve, reject) => {
|
|
723
|
+
socket.send(frame, (error) => error ? reject(error) : resolve());
|
|
724
|
+
});
|
|
725
|
+
}
|
|
524
726
|
var Sandbox = class {
|
|
525
727
|
sandboxId;
|
|
526
728
|
http;
|
|
527
729
|
baseUrl;
|
|
730
|
+
wsHeaders;
|
|
528
731
|
ownsSandbox = false;
|
|
529
732
|
lifecycleClient = null;
|
|
530
733
|
constructor(options) {
|
|
@@ -532,6 +735,19 @@ var Sandbox = class {
|
|
|
532
735
|
const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
|
|
533
736
|
const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
|
|
534
737
|
this.baseUrl = baseUrl;
|
|
738
|
+
this.wsHeaders = {};
|
|
739
|
+
if (options.apiKey) {
|
|
740
|
+
this.wsHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
741
|
+
}
|
|
742
|
+
if (options.organizationId) {
|
|
743
|
+
this.wsHeaders["X-Forwarded-Organization-Id"] = options.organizationId;
|
|
744
|
+
}
|
|
745
|
+
if (options.projectId) {
|
|
746
|
+
this.wsHeaders["X-Forwarded-Project-Id"] = options.projectId;
|
|
747
|
+
}
|
|
748
|
+
if (hostHeader) {
|
|
749
|
+
this.wsHeaders.Host = hostHeader;
|
|
750
|
+
}
|
|
535
751
|
this.http = new HttpClient({
|
|
536
752
|
baseUrl,
|
|
537
753
|
apiKey: options.apiKey,
|
|
@@ -710,29 +926,29 @@ var Sandbox = class {
|
|
|
710
926
|
}
|
|
711
927
|
}
|
|
712
928
|
// --- File operations ---
|
|
713
|
-
async readFile(
|
|
929
|
+
async readFile(path2) {
|
|
714
930
|
return this.http.requestBytes(
|
|
715
931
|
"GET",
|
|
716
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
932
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
717
933
|
);
|
|
718
934
|
}
|
|
719
|
-
async writeFile(
|
|
935
|
+
async writeFile(path2, content) {
|
|
720
936
|
await this.http.requestBytes(
|
|
721
937
|
"PUT",
|
|
722
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
938
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`,
|
|
723
939
|
{ body: content, contentType: "application/octet-stream" }
|
|
724
940
|
);
|
|
725
941
|
}
|
|
726
|
-
async deleteFile(
|
|
942
|
+
async deleteFile(path2) {
|
|
727
943
|
await this.http.requestJson(
|
|
728
944
|
"DELETE",
|
|
729
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
945
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
730
946
|
);
|
|
731
947
|
}
|
|
732
|
-
async listDirectory(
|
|
948
|
+
async listDirectory(path2) {
|
|
733
949
|
const raw = await this.http.requestJson(
|
|
734
950
|
"GET",
|
|
735
|
-
`/api/v1/files/list?path=${encodeURIComponent(
|
|
951
|
+
`/api/v1/files/list?path=${encodeURIComponent(path2)}`
|
|
736
952
|
);
|
|
737
953
|
return fromSnakeKeys(raw);
|
|
738
954
|
}
|
|
@@ -753,6 +969,43 @@ var Sandbox = class {
|
|
|
753
969
|
);
|
|
754
970
|
return fromSnakeKeys(raw);
|
|
755
971
|
}
|
|
972
|
+
async createPty(options) {
|
|
973
|
+
const { onData, onExit, ...createOptions } = options;
|
|
974
|
+
const session = await this.createPtySession(createOptions);
|
|
975
|
+
try {
|
|
976
|
+
return await this.connectPty(session.sessionId, session.token, { onData, onExit });
|
|
977
|
+
} catch (error) {
|
|
978
|
+
try {
|
|
979
|
+
await this.http.requestResponse("DELETE", `/api/v1/pty/${session.sessionId}`);
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
async connectPty(sessionId, token, options) {
|
|
986
|
+
const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
|
|
987
|
+
const authToken = wsUrl.searchParams.get("token") ?? token;
|
|
988
|
+
const pty = new Pty({
|
|
989
|
+
sessionId,
|
|
990
|
+
token: authToken,
|
|
991
|
+
wsUrl: wsUrl.toString(),
|
|
992
|
+
wsHeaders: {
|
|
993
|
+
...this.wsHeaders,
|
|
994
|
+
"X-PTY-Token": authToken
|
|
995
|
+
},
|
|
996
|
+
killSession: async () => {
|
|
997
|
+
await this.http.requestResponse("DELETE", `/api/v1/pty/${sessionId}`);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
if (options?.onData) {
|
|
1001
|
+
pty.onData(options.onData);
|
|
1002
|
+
}
|
|
1003
|
+
if (options?.onExit) {
|
|
1004
|
+
pty.onExit(options.onExit);
|
|
1005
|
+
}
|
|
1006
|
+
await pty.connect();
|
|
1007
|
+
return pty;
|
|
1008
|
+
}
|
|
756
1009
|
ptyWsUrl(sessionId, token) {
|
|
757
1010
|
let wsBase;
|
|
758
1011
|
if (this.baseUrl.startsWith("https://")) {
|
|
@@ -880,6 +1133,15 @@ var SandboxClient = class _SandboxClient {
|
|
|
880
1133
|
async update(sandboxId, options) {
|
|
881
1134
|
const body = {};
|
|
882
1135
|
if (options.name != null) body.name = options.name;
|
|
1136
|
+
if (options.allowUnauthenticatedAccess != null) {
|
|
1137
|
+
body.allow_unauthenticated_access = options.allowUnauthenticatedAccess;
|
|
1138
|
+
}
|
|
1139
|
+
if (options.exposedPorts != null) {
|
|
1140
|
+
body.exposed_ports = normalizeUserPorts(options.exposedPorts);
|
|
1141
|
+
}
|
|
1142
|
+
if (Object.keys(body).length === 0) {
|
|
1143
|
+
throw new SandboxError("At least one sandbox update field must be provided.");
|
|
1144
|
+
}
|
|
883
1145
|
const raw = await this.http.requestJson(
|
|
884
1146
|
"PATCH",
|
|
885
1147
|
this.path(`sandboxes/${sandboxId}`),
|
|
@@ -887,12 +1149,54 @@ var SandboxClient = class _SandboxClient {
|
|
|
887
1149
|
);
|
|
888
1150
|
return fromSnakeKeys(raw, "sandboxId");
|
|
889
1151
|
}
|
|
1152
|
+
async getPortAccess(sandboxId) {
|
|
1153
|
+
const info = await this.get(sandboxId);
|
|
1154
|
+
return {
|
|
1155
|
+
allowUnauthenticatedAccess: info.allowUnauthenticatedAccess ?? false,
|
|
1156
|
+
exposedPorts: dedupeAndSortPorts(info.exposedPorts ?? []),
|
|
1157
|
+
sandboxUrl: info.sandboxUrl
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async exposePorts(sandboxId, ports, options) {
|
|
1161
|
+
const requestedPorts = normalizeUserPorts(ports);
|
|
1162
|
+
const current = await this.getPortAccess(sandboxId);
|
|
1163
|
+
const desiredPorts = dedupeAndSortPorts([
|
|
1164
|
+
...current.exposedPorts,
|
|
1165
|
+
...requestedPorts
|
|
1166
|
+
]);
|
|
1167
|
+
return this.update(sandboxId, {
|
|
1168
|
+
allowUnauthenticatedAccess: options?.allowUnauthenticatedAccess ?? current.allowUnauthenticatedAccess,
|
|
1169
|
+
exposedPorts: desiredPorts
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
async unexposePorts(sandboxId, ports) {
|
|
1173
|
+
const requestedPorts = normalizeUserPorts(ports);
|
|
1174
|
+
const current = await this.getPortAccess(sandboxId);
|
|
1175
|
+
const toRemove = new Set(requestedPorts);
|
|
1176
|
+
const desiredPorts = current.exposedPorts.filter((port) => !toRemove.has(port));
|
|
1177
|
+
return this.update(sandboxId, {
|
|
1178
|
+
allowUnauthenticatedAccess: desiredPorts.length ? current.allowUnauthenticatedAccess : false,
|
|
1179
|
+
exposedPorts: desiredPorts
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
890
1182
|
async delete(sandboxId) {
|
|
891
1183
|
await this.http.requestJson(
|
|
892
1184
|
"DELETE",
|
|
893
1185
|
this.path(`sandboxes/${sandboxId}`)
|
|
894
1186
|
);
|
|
895
1187
|
}
|
|
1188
|
+
async suspend(sandboxId) {
|
|
1189
|
+
await this.http.requestResponse(
|
|
1190
|
+
"POST",
|
|
1191
|
+
this.path(`sandboxes/${sandboxId}/suspend`)
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
async resume(sandboxId) {
|
|
1195
|
+
await this.http.requestResponse(
|
|
1196
|
+
"POST",
|
|
1197
|
+
this.path(`sandboxes/${sandboxId}/resume`)
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
896
1200
|
async claim(poolId) {
|
|
897
1201
|
const raw = await this.http.requestJson(
|
|
898
1202
|
"POST",
|
|
@@ -1060,6 +1364,22 @@ var SandboxClient = class _SandboxClient {
|
|
|
1060
1364
|
function sleep3(ms) {
|
|
1061
1365
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1062
1366
|
}
|
|
1367
|
+
var RESERVED_SANDBOX_MANAGEMENT_PORT = 9501;
|
|
1368
|
+
function normalizeUserPorts(ports) {
|
|
1369
|
+
return dedupeAndSortPorts(ports.map(validateUserPort));
|
|
1370
|
+
}
|
|
1371
|
+
function validateUserPort(port) {
|
|
1372
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1373
|
+
throw new SandboxError(`invalid port '${port}'`);
|
|
1374
|
+
}
|
|
1375
|
+
if (port === RESERVED_SANDBOX_MANAGEMENT_PORT) {
|
|
1376
|
+
throw new SandboxError("port 9501 is reserved for sandbox management");
|
|
1377
|
+
}
|
|
1378
|
+
return port;
|
|
1379
|
+
}
|
|
1380
|
+
function dedupeAndSortPorts(ports) {
|
|
1381
|
+
return [...new Set(ports)].sort((a, b) => a - b);
|
|
1382
|
+
}
|
|
1063
1383
|
|
|
1064
1384
|
// src/cloud-client.ts
|
|
1065
1385
|
var CloudClient = class _CloudClient {
|
|
@@ -1126,19 +1446,19 @@ var CloudClient = class _CloudClient {
|
|
|
1126
1446
|
return fromSnakeKeys(raw);
|
|
1127
1447
|
}
|
|
1128
1448
|
async runRequest(applicationName, inputs = []) {
|
|
1129
|
-
const
|
|
1449
|
+
const path2 = this.namespacePath(
|
|
1130
1450
|
`applications/${encodeURIComponent(applicationName)}`
|
|
1131
1451
|
);
|
|
1132
|
-
const response = inputs.length === 0 ? await this.http.requestResponse("POST",
|
|
1452
|
+
const response = inputs.length === 0 ? await this.http.requestResponse("POST", path2, {
|
|
1133
1453
|
body: new Uint8Array(),
|
|
1134
1454
|
headers: { Accept: "application/json" }
|
|
1135
|
-
}) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST",
|
|
1455
|
+
}) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST", path2, {
|
|
1136
1456
|
body: toRequestBody(inputs[0].data),
|
|
1137
1457
|
headers: {
|
|
1138
1458
|
Accept: "application/json",
|
|
1139
1459
|
"Content-Type": inputs[0].contentType
|
|
1140
1460
|
}
|
|
1141
|
-
}) : await this.runMultipartRequest(
|
|
1461
|
+
}) : await this.runMultipartRequest(path2, inputs);
|
|
1142
1462
|
const body = await parseJsonResponse(response);
|
|
1143
1463
|
const requestId = body?.request_id;
|
|
1144
1464
|
if (!requestId) {
|
|
@@ -1292,7 +1612,7 @@ var CloudClient = class _CloudClient {
|
|
|
1292
1612
|
yield fromSnakeKeys(event);
|
|
1293
1613
|
}
|
|
1294
1614
|
}
|
|
1295
|
-
async runMultipartRequest(
|
|
1615
|
+
async runMultipartRequest(path2, inputs) {
|
|
1296
1616
|
const form = new FormData();
|
|
1297
1617
|
for (const input of inputs) {
|
|
1298
1618
|
form.append(
|
|
@@ -1301,7 +1621,7 @@ var CloudClient = class _CloudClient {
|
|
|
1301
1621
|
input.name
|
|
1302
1622
|
);
|
|
1303
1623
|
}
|
|
1304
|
-
return this.http.requestResponse("POST",
|
|
1624
|
+
return this.http.requestResponse("POST", path2, {
|
|
1305
1625
|
body: form,
|
|
1306
1626
|
headers: { Accept: "application/json" }
|
|
1307
1627
|
});
|
|
@@ -1436,15 +1756,860 @@ var APIClient = class {
|
|
|
1436
1756
|
return this.cloudClient.requestOutput(applicationName, requestId);
|
|
1437
1757
|
}
|
|
1438
1758
|
};
|
|
1759
|
+
|
|
1760
|
+
// src/sandbox-image.ts
|
|
1761
|
+
var import_promises = require("fs/promises");
|
|
1762
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1763
|
+
var import_node_util = require("util");
|
|
1764
|
+
|
|
1765
|
+
// src/image.ts
|
|
1766
|
+
var import_node_crypto = require("crypto");
|
|
1767
|
+
var ImageBuildOperationType = {
|
|
1768
|
+
ADD: "ADD",
|
|
1769
|
+
COPY: "COPY",
|
|
1770
|
+
ENV: "ENV",
|
|
1771
|
+
RUN: "RUN",
|
|
1772
|
+
WORKDIR: "WORKDIR"
|
|
1773
|
+
};
|
|
1774
|
+
function cloneOperation(op) {
|
|
1775
|
+
return {
|
|
1776
|
+
type: op.type,
|
|
1777
|
+
args: [...op.args],
|
|
1778
|
+
options: { ...op.options }
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
var Image = class {
|
|
1782
|
+
_id;
|
|
1783
|
+
_name;
|
|
1784
|
+
_tag;
|
|
1785
|
+
_baseImage;
|
|
1786
|
+
_buildOperations;
|
|
1787
|
+
constructor(nameOrOptions = {}, tag = "latest", baseImage = null) {
|
|
1788
|
+
this._id = (0, import_node_crypto.randomUUID)();
|
|
1789
|
+
this._buildOperations = [];
|
|
1790
|
+
if (typeof nameOrOptions === "string") {
|
|
1791
|
+
this._name = nameOrOptions;
|
|
1792
|
+
this._tag = tag;
|
|
1793
|
+
this._baseImage = baseImage;
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
this._name = nameOrOptions.name ?? "default";
|
|
1797
|
+
this._tag = nameOrOptions.tag ?? "latest";
|
|
1798
|
+
this._baseImage = nameOrOptions.baseImage ?? null;
|
|
1799
|
+
}
|
|
1800
|
+
get name() {
|
|
1801
|
+
return this._name;
|
|
1802
|
+
}
|
|
1803
|
+
get tag() {
|
|
1804
|
+
return this._tag;
|
|
1805
|
+
}
|
|
1806
|
+
get baseImage() {
|
|
1807
|
+
return this._baseImage;
|
|
1808
|
+
}
|
|
1809
|
+
get buildOperations() {
|
|
1810
|
+
return this._buildOperations.map(cloneOperation);
|
|
1811
|
+
}
|
|
1812
|
+
add(src, dest, options = void 0) {
|
|
1813
|
+
return this._addOperation({
|
|
1814
|
+
type: ImageBuildOperationType.ADD,
|
|
1815
|
+
args: [src, dest],
|
|
1816
|
+
options: options == null ? {} : { ...options }
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
copy(src, dest, options = void 0) {
|
|
1820
|
+
return this._addOperation({
|
|
1821
|
+
type: ImageBuildOperationType.COPY,
|
|
1822
|
+
args: [src, dest],
|
|
1823
|
+
options: options == null ? {} : { ...options }
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
env(key, value) {
|
|
1827
|
+
return this._addOperation({
|
|
1828
|
+
type: ImageBuildOperationType.ENV,
|
|
1829
|
+
args: [key, value],
|
|
1830
|
+
options: {}
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
run(commands, options = void 0) {
|
|
1834
|
+
return this._addOperation({
|
|
1835
|
+
type: ImageBuildOperationType.RUN,
|
|
1836
|
+
args: Array.isArray(commands) ? [...commands] : [commands],
|
|
1837
|
+
options: options == null ? {} : { ...options }
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
workdir(directory) {
|
|
1841
|
+
return this._addOperation({
|
|
1842
|
+
type: ImageBuildOperationType.WORKDIR,
|
|
1843
|
+
args: [directory],
|
|
1844
|
+
options: {}
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
_addOperation(op) {
|
|
1848
|
+
this._buildOperations.push(op);
|
|
1849
|
+
return this;
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
function renderOptions(options) {
|
|
1853
|
+
const entries = Object.entries(options);
|
|
1854
|
+
if (entries.length === 0) {
|
|
1855
|
+
return "";
|
|
1856
|
+
}
|
|
1857
|
+
return ` ${entries.map(([key, value]) => `--${key}=${value}`).join(" ")}`;
|
|
1858
|
+
}
|
|
1859
|
+
function renderBuildOp(op) {
|
|
1860
|
+
const options = renderOptions(op.options);
|
|
1861
|
+
if (op.type === ImageBuildOperationType.ENV) {
|
|
1862
|
+
return `ENV${options} ${op.args[0]}=${JSON.stringify(op.args[1])}`;
|
|
1863
|
+
}
|
|
1864
|
+
return `${op.type}${options} ${op.args.join(" ")}`;
|
|
1865
|
+
}
|
|
1866
|
+
function dockerfileContent(image) {
|
|
1867
|
+
const lines = image.baseImage == null ? [] : [`FROM ${image.baseImage}`];
|
|
1868
|
+
lines.push(...image.buildOperations.map((op) => renderBuildOp(op)));
|
|
1869
|
+
return lines.join("\n");
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/sandbox-image.ts
|
|
1873
|
+
var BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
|
|
1874
|
+
var IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
1875
|
+
"CMD",
|
|
1876
|
+
"ENTRYPOINT",
|
|
1877
|
+
"EXPOSE",
|
|
1878
|
+
"HEALTHCHECK",
|
|
1879
|
+
"LABEL",
|
|
1880
|
+
"STOPSIGNAL",
|
|
1881
|
+
"VOLUME"
|
|
1882
|
+
]);
|
|
1883
|
+
var UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
1884
|
+
"ARG",
|
|
1885
|
+
"ONBUILD",
|
|
1886
|
+
"SHELL",
|
|
1887
|
+
"USER"
|
|
1888
|
+
]);
|
|
1889
|
+
function defaultRegisteredName(dockerfilePath) {
|
|
1890
|
+
const parsed = import_node_path.default.parse(dockerfilePath);
|
|
1891
|
+
if (parsed.name.toLowerCase() === "dockerfile") {
|
|
1892
|
+
const parentName = import_node_path.default.basename(import_node_path.default.dirname(dockerfilePath)).trim();
|
|
1893
|
+
return parentName || "sandbox-image";
|
|
1894
|
+
}
|
|
1895
|
+
return parsed.name || "sandbox-image";
|
|
1896
|
+
}
|
|
1897
|
+
function logicalDockerfileLines(dockerfileText) {
|
|
1898
|
+
const logicalLines = [];
|
|
1899
|
+
let parts = [];
|
|
1900
|
+
let startLine = null;
|
|
1901
|
+
for (const [index, rawLine] of dockerfileText.split(/\r?\n/).entries()) {
|
|
1902
|
+
const lineNumber = index + 1;
|
|
1903
|
+
const stripped = rawLine.trim();
|
|
1904
|
+
if (parts.length === 0 && (!stripped || stripped.startsWith("#"))) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if (startLine == null) {
|
|
1908
|
+
startLine = lineNumber;
|
|
1909
|
+
}
|
|
1910
|
+
let line = rawLine.replace(/\s+$/, "");
|
|
1911
|
+
const continued = line.endsWith("\\");
|
|
1912
|
+
if (continued) {
|
|
1913
|
+
line = line.slice(0, -1);
|
|
1914
|
+
}
|
|
1915
|
+
const normalized = line.trim();
|
|
1916
|
+
if (normalized && !normalized.startsWith("#")) {
|
|
1917
|
+
parts.push(normalized);
|
|
1918
|
+
}
|
|
1919
|
+
if (continued) {
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (parts.length > 0) {
|
|
1923
|
+
logicalLines.push({
|
|
1924
|
+
lineNumber: startLine ?? lineNumber,
|
|
1925
|
+
line: parts.join(" ")
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
parts = [];
|
|
1929
|
+
startLine = null;
|
|
1930
|
+
}
|
|
1931
|
+
if (parts.length > 0) {
|
|
1932
|
+
logicalLines.push({
|
|
1933
|
+
lineNumber: startLine ?? 1,
|
|
1934
|
+
line: parts.join(" ")
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
return logicalLines;
|
|
1938
|
+
}
|
|
1939
|
+
function splitInstruction(line, lineNumber) {
|
|
1940
|
+
const trimmed = line.trim();
|
|
1941
|
+
if (!trimmed) {
|
|
1942
|
+
throw new Error(`line ${lineNumber}: empty Dockerfile instruction`);
|
|
1943
|
+
}
|
|
1944
|
+
const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
|
|
1945
|
+
if (!match) {
|
|
1946
|
+
throw new Error(`line ${lineNumber}: invalid Dockerfile instruction`);
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
keyword: match[1].toUpperCase(),
|
|
1950
|
+
value: (match[2] ?? "").trim()
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function shellSplit(input) {
|
|
1954
|
+
const tokens = [];
|
|
1955
|
+
let current = "";
|
|
1956
|
+
let quote = null;
|
|
1957
|
+
let escape = false;
|
|
1958
|
+
for (let i = 0; i < input.length; i++) {
|
|
1959
|
+
const char = input[i];
|
|
1960
|
+
if (escape) {
|
|
1961
|
+
current += char;
|
|
1962
|
+
escape = false;
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
if (quote == null) {
|
|
1966
|
+
if (/\s/.test(char)) {
|
|
1967
|
+
if (current) {
|
|
1968
|
+
tokens.push(current);
|
|
1969
|
+
current = "";
|
|
1970
|
+
}
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
if (char === "'" || char === '"') {
|
|
1974
|
+
quote = char;
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (char === "\\") {
|
|
1978
|
+
escape = true;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
current += char;
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (quote === "'") {
|
|
1985
|
+
if (char === "'") {
|
|
1986
|
+
quote = null;
|
|
1987
|
+
} else {
|
|
1988
|
+
current += char;
|
|
1989
|
+
}
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
if (char === '"') {
|
|
1993
|
+
quote = null;
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
if (char === "\\") {
|
|
1997
|
+
const next = input[++i];
|
|
1998
|
+
if (next == null) {
|
|
1999
|
+
throw new Error(`unterminated escape sequence in '${input}'`);
|
|
2000
|
+
}
|
|
2001
|
+
current += next;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
current += char;
|
|
2005
|
+
}
|
|
2006
|
+
if (escape) {
|
|
2007
|
+
throw new Error(`unterminated escape sequence in '${input}'`);
|
|
2008
|
+
}
|
|
2009
|
+
if (quote != null) {
|
|
2010
|
+
throw new Error(`unterminated quoted string in '${input}'`);
|
|
2011
|
+
}
|
|
2012
|
+
if (current) {
|
|
2013
|
+
tokens.push(current);
|
|
2014
|
+
}
|
|
2015
|
+
return tokens;
|
|
2016
|
+
}
|
|
2017
|
+
function shellQuote(value) {
|
|
2018
|
+
if (!value) {
|
|
2019
|
+
return "''";
|
|
2020
|
+
}
|
|
2021
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2022
|
+
}
|
|
2023
|
+
function stripLeadingFlags(value) {
|
|
2024
|
+
const flags = {};
|
|
2025
|
+
let remaining = value.trimStart();
|
|
2026
|
+
while (remaining.startsWith("--")) {
|
|
2027
|
+
const firstSpace = remaining.indexOf(" ");
|
|
2028
|
+
if (firstSpace === -1) {
|
|
2029
|
+
throw new Error(`invalid Dockerfile flag syntax: ${value}`);
|
|
2030
|
+
}
|
|
2031
|
+
const token = remaining.slice(0, firstSpace);
|
|
2032
|
+
const rest = remaining.slice(firstSpace + 1).trimStart();
|
|
2033
|
+
const flagBody = token.slice(2);
|
|
2034
|
+
if (flagBody.includes("=")) {
|
|
2035
|
+
const [key, flagValue2] = flagBody.split(/=(.*)/s, 2);
|
|
2036
|
+
flags[key] = flagValue2;
|
|
2037
|
+
remaining = rest;
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const [flagValue, ...restTokens] = shellSplit(rest);
|
|
2041
|
+
if (flagValue == null) {
|
|
2042
|
+
throw new Error(`missing value for Dockerfile flag '${token}'`);
|
|
2043
|
+
}
|
|
2044
|
+
flags[flagBody] = flagValue;
|
|
2045
|
+
remaining = restTokens.join(" ");
|
|
2046
|
+
}
|
|
2047
|
+
return { flags, remaining };
|
|
2048
|
+
}
|
|
2049
|
+
function parseFromValue(value, lineNumber) {
|
|
2050
|
+
const { remaining } = stripLeadingFlags(value);
|
|
2051
|
+
const tokens = shellSplit(remaining);
|
|
2052
|
+
if (tokens.length === 0) {
|
|
2053
|
+
throw new Error(`line ${lineNumber}: FROM must include a base image`);
|
|
2054
|
+
}
|
|
2055
|
+
if (tokens.length > 1 && tokens[1].toLowerCase() !== "as") {
|
|
2056
|
+
throw new Error(`line ${lineNumber}: unsupported FROM syntax '${value}'`);
|
|
2057
|
+
}
|
|
2058
|
+
return tokens[0];
|
|
2059
|
+
}
|
|
2060
|
+
function parseCopyLikeValues(value, lineNumber, keyword) {
|
|
2061
|
+
const { flags, remaining } = stripLeadingFlags(value);
|
|
2062
|
+
if ("from" in flags) {
|
|
2063
|
+
throw new Error(
|
|
2064
|
+
`line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
const payload = remaining.trim();
|
|
2068
|
+
if (!payload) {
|
|
2069
|
+
throw new Error(
|
|
2070
|
+
`line ${lineNumber}: ${keyword} must include source and destination`
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
let parts;
|
|
2074
|
+
if (payload.startsWith("[")) {
|
|
2075
|
+
let parsed;
|
|
2076
|
+
try {
|
|
2077
|
+
parsed = JSON.parse(payload);
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
throw new Error(
|
|
2080
|
+
`line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
|
|
2084
|
+
throw new Error(
|
|
2085
|
+
`line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
parts = parsed;
|
|
2089
|
+
} else {
|
|
2090
|
+
parts = shellSplit(payload);
|
|
2091
|
+
if (parts.length < 2) {
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
`line ${lineNumber}: ${keyword} must include at least one source and one destination`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return {
|
|
2098
|
+
flags,
|
|
2099
|
+
sources: parts.slice(0, -1),
|
|
2100
|
+
destination: parts[parts.length - 1]
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
function parseEnvPairs(value, lineNumber) {
|
|
2104
|
+
const tokens = shellSplit(value);
|
|
2105
|
+
if (tokens.length === 0) {
|
|
2106
|
+
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
2107
|
+
}
|
|
2108
|
+
if (tokens.every((token) => token.includes("="))) {
|
|
2109
|
+
return tokens.map((token) => {
|
|
2110
|
+
const [key, envValue] = token.split(/=(.*)/s, 2);
|
|
2111
|
+
if (!key) {
|
|
2112
|
+
throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
|
|
2113
|
+
}
|
|
2114
|
+
return [key, envValue];
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
if (tokens.length < 2) {
|
|
2118
|
+
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
2119
|
+
}
|
|
2120
|
+
return [[tokens[0], tokens.slice(1).join(" ")]];
|
|
2121
|
+
}
|
|
2122
|
+
function resolveContainerPath(containerPath, workingDir) {
|
|
2123
|
+
if (!containerPath) {
|
|
2124
|
+
return workingDir;
|
|
2125
|
+
}
|
|
2126
|
+
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));
|
|
2127
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2128
|
+
}
|
|
2129
|
+
function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
|
|
2130
|
+
let baseImage;
|
|
2131
|
+
const instructions = [];
|
|
2132
|
+
for (const logicalLine of logicalDockerfileLines(dockerfileText)) {
|
|
2133
|
+
const { keyword, value } = splitInstruction(
|
|
2134
|
+
logicalLine.line,
|
|
2135
|
+
logicalLine.lineNumber
|
|
2136
|
+
);
|
|
2137
|
+
if (keyword === "FROM") {
|
|
2138
|
+
if (baseImage != null) {
|
|
2139
|
+
throw new Error(
|
|
2140
|
+
`line ${logicalLine.lineNumber}: multi-stage Dockerfiles are not supported for sandbox image creation`
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
baseImage = parseFromValue(value, logicalLine.lineNumber);
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
if (UNSUPPORTED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
`line ${logicalLine.lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
instructions.push({
|
|
2152
|
+
keyword,
|
|
2153
|
+
value,
|
|
2154
|
+
lineNumber: logicalLine.lineNumber
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
if (!baseImage) {
|
|
2158
|
+
throw new Error("Dockerfile must contain a FROM instruction");
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
dockerfilePath,
|
|
2162
|
+
contextDir,
|
|
2163
|
+
registeredName: registeredName ?? defaultRegisteredName(dockerfilePath),
|
|
2164
|
+
dockerfileText,
|
|
2165
|
+
baseImage,
|
|
2166
|
+
instructions
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
async function loadDockerfilePlan(dockerfilePath, registeredName) {
|
|
2170
|
+
const resolvedPath = import_node_path.default.resolve(dockerfilePath);
|
|
2171
|
+
const fileStats = await (0, import_promises.stat)(resolvedPath).catch(() => null);
|
|
2172
|
+
if (!fileStats?.isFile()) {
|
|
2173
|
+
throw new Error(`Dockerfile not found: ${dockerfilePath}`);
|
|
2174
|
+
}
|
|
2175
|
+
const dockerfileText = await (0, import_promises.readFile)(resolvedPath, "utf8");
|
|
2176
|
+
return buildPlanFromDockerfileText(
|
|
2177
|
+
dockerfileText,
|
|
2178
|
+
resolvedPath,
|
|
2179
|
+
import_node_path.default.dirname(resolvedPath),
|
|
2180
|
+
registeredName
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
function loadImagePlan(image, options = {}) {
|
|
2184
|
+
const contextDir = import_node_path.default.resolve(options.contextDir ?? process.cwd());
|
|
2185
|
+
const dockerfileText = dockerfileContent(image);
|
|
2186
|
+
const logicalLines = logicalDockerfileLines(dockerfileText);
|
|
2187
|
+
const instructions = image.baseImage == null ? logicalLines : logicalLines.slice(1);
|
|
2188
|
+
return {
|
|
2189
|
+
dockerfilePath: import_node_path.default.join(contextDir, "Dockerfile"),
|
|
2190
|
+
contextDir,
|
|
2191
|
+
registeredName: options.registeredName ?? image.name,
|
|
2192
|
+
dockerfileText,
|
|
2193
|
+
baseImage: image.baseImage ?? void 0,
|
|
2194
|
+
instructions: instructions.map(({ line, lineNumber }) => {
|
|
2195
|
+
const parsed = splitInstruction(line, lineNumber);
|
|
2196
|
+
return {
|
|
2197
|
+
keyword: parsed.keyword,
|
|
2198
|
+
value: parsed.value,
|
|
2199
|
+
lineNumber
|
|
2200
|
+
};
|
|
2201
|
+
})
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
function defaultEmit(event) {
|
|
2205
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
2206
|
+
`);
|
|
2207
|
+
}
|
|
2208
|
+
function debugEnabled() {
|
|
2209
|
+
return ["1", "true", "yes", "on"].includes(
|
|
2210
|
+
(process.env.TENSORLAKE_DEBUG ?? "").toLowerCase()
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
function buildContextFromEnv() {
|
|
2214
|
+
return {
|
|
2215
|
+
apiUrl: process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai",
|
|
2216
|
+
apiKey: process.env.TENSORLAKE_API_KEY,
|
|
2217
|
+
personalAccessToken: process.env.TENSORLAKE_PAT,
|
|
2218
|
+
namespace: process.env.INDEXIFY_NAMESPACE ?? "default",
|
|
2219
|
+
organizationId: process.env.TENSORLAKE_ORGANIZATION_ID,
|
|
2220
|
+
projectId: process.env.TENSORLAKE_PROJECT_ID,
|
|
2221
|
+
debug: debugEnabled()
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
function createDefaultClient(context) {
|
|
2225
|
+
return new SandboxClient({
|
|
2226
|
+
apiUrl: context.apiUrl,
|
|
2227
|
+
apiKey: context.apiKey ?? context.personalAccessToken,
|
|
2228
|
+
organizationId: context.organizationId,
|
|
2229
|
+
projectId: context.projectId,
|
|
2230
|
+
namespace: context.namespace
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
async function runChecked(sandbox, command, args, env, workingDir) {
|
|
2234
|
+
const result = await sandbox.run(command, {
|
|
2235
|
+
args,
|
|
2236
|
+
env,
|
|
2237
|
+
workingDir
|
|
2238
|
+
});
|
|
2239
|
+
if (result.exitCode !== 0) {
|
|
2240
|
+
throw new Error(
|
|
2241
|
+
`Command '${command} ${args.join(" ")}' failed with exit code ${result.exitCode}`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
}
|
|
2246
|
+
async function runStreaming(sandbox, emit, sleep4, command, args = [], env, workingDir) {
|
|
2247
|
+
const proc = await sandbox.startProcess(command, {
|
|
2248
|
+
args,
|
|
2249
|
+
env,
|
|
2250
|
+
workingDir
|
|
2251
|
+
});
|
|
2252
|
+
let stdoutSeen = 0;
|
|
2253
|
+
let stderrSeen = 0;
|
|
2254
|
+
let info;
|
|
2255
|
+
while (true) {
|
|
2256
|
+
const stdoutResp = await sandbox.getStdout(proc.pid);
|
|
2257
|
+
emitOutputLines(emit, "stdout", stdoutResp, stdoutSeen);
|
|
2258
|
+
stdoutSeen = stdoutResp.lines.length;
|
|
2259
|
+
const stderrResp = await sandbox.getStderr(proc.pid);
|
|
2260
|
+
emitOutputLines(emit, "stderr", stderrResp, stderrSeen);
|
|
2261
|
+
stderrSeen = stderrResp.lines.length;
|
|
2262
|
+
info = await sandbox.getProcess(proc.pid);
|
|
2263
|
+
if (info.status !== "running" /* RUNNING */) {
|
|
2264
|
+
const finalStdout = await sandbox.getStdout(proc.pid);
|
|
2265
|
+
emitOutputLines(emit, "stdout", finalStdout, stdoutSeen);
|
|
2266
|
+
stdoutSeen = finalStdout.lines.length;
|
|
2267
|
+
const finalStderr = await sandbox.getStderr(proc.pid);
|
|
2268
|
+
emitOutputLines(emit, "stderr", finalStderr, stderrSeen);
|
|
2269
|
+
break;
|
|
2270
|
+
}
|
|
2271
|
+
await sleep4(300);
|
|
2272
|
+
}
|
|
2273
|
+
for (let i = 0; i < 10; i++) {
|
|
2274
|
+
if (info.exitCode != null || info.signal != null) {
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
await sleep4(200);
|
|
2278
|
+
info = await sandbox.getProcess(proc.pid);
|
|
2279
|
+
}
|
|
2280
|
+
const exitCode = info.exitCode != null ? info.exitCode : info.signal != null ? -info.signal : 0;
|
|
2281
|
+
if (exitCode !== 0) {
|
|
2282
|
+
throw new Error(
|
|
2283
|
+
`Command '${command} ${args.join(" ")}' failed with exit code ${exitCode}`
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
function emitOutputLines(emit, stream, response, seen) {
|
|
2288
|
+
for (const line of response.lines.slice(seen)) {
|
|
2289
|
+
emit({ type: "build_log", stream, message: line });
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function isPathWithinContext(contextDir, localPath) {
|
|
2293
|
+
const relative = import_node_path.default.relative(contextDir, localPath);
|
|
2294
|
+
return relative === "" || !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
|
|
2295
|
+
}
|
|
2296
|
+
function resolveContextSourcePath(contextDir, source) {
|
|
2297
|
+
const resolvedContextDir = import_node_path.default.resolve(contextDir);
|
|
2298
|
+
const resolvedSource = import_node_path.default.resolve(resolvedContextDir, source);
|
|
2299
|
+
if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
|
|
2300
|
+
throw new Error(`Local path escapes the build context: ${source}`);
|
|
2301
|
+
}
|
|
2302
|
+
return resolvedSource;
|
|
2303
|
+
}
|
|
2304
|
+
async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
2305
|
+
const fileStats = await (0, import_promises.stat)(localPath).catch(() => null);
|
|
2306
|
+
if (!fileStats) {
|
|
2307
|
+
throw new Error(`Local path not found: ${localPath}`);
|
|
2308
|
+
}
|
|
2309
|
+
if (fileStats.isFile()) {
|
|
2310
|
+
await runChecked(sandbox, "mkdir", ["-p", import_node_path.default.posix.dirname(remotePath)]);
|
|
2311
|
+
await sandbox.writeFile(remotePath, await (0, import_promises.readFile)(localPath));
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
if (!fileStats.isDirectory()) {
|
|
2315
|
+
throw new Error(`Local path not found: ${localPath}`);
|
|
2316
|
+
}
|
|
2317
|
+
const entries = await (0, import_promises.readdir)(localPath, { withFileTypes: true });
|
|
2318
|
+
for (const entry of entries) {
|
|
2319
|
+
const sourcePath = import_node_path.default.join(localPath, entry.name);
|
|
2320
|
+
const destinationPath = import_node_path.default.posix.join(remotePath, entry.name);
|
|
2321
|
+
if (entry.isDirectory()) {
|
|
2322
|
+
await runChecked(sandbox, "mkdir", ["-p", destinationPath]);
|
|
2323
|
+
await copyLocalPathToSandbox(sandbox, sourcePath, destinationPath);
|
|
2324
|
+
} else if (entry.isFile()) {
|
|
2325
|
+
await runChecked(
|
|
2326
|
+
sandbox,
|
|
2327
|
+
"mkdir",
|
|
2328
|
+
["-p", import_node_path.default.posix.dirname(destinationPath)]
|
|
2329
|
+
);
|
|
2330
|
+
await sandbox.writeFile(destinationPath, await (0, import_promises.readFile)(sourcePath));
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
async function persistEnvVar(sandbox, processEnv, key, value) {
|
|
2335
|
+
const exportLine = `export ${key}=${shellQuote(value)}`;
|
|
2336
|
+
await runChecked(
|
|
2337
|
+
sandbox,
|
|
2338
|
+
"sh",
|
|
2339
|
+
["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
|
|
2340
|
+
processEnv
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
|
|
2344
|
+
const destinationPath = resolveContainerPath(destination, workingDir);
|
|
2345
|
+
if (sources.length > 1 && !destinationPath.endsWith("/")) {
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
`${keyword} with multiple sources requires a directory destination ending in '/'`
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
for (const source of sources) {
|
|
2351
|
+
const localSource = resolveContextSourcePath(contextDir, source);
|
|
2352
|
+
const localStats = await (0, import_promises.stat)(localSource).catch(() => null);
|
|
2353
|
+
if (!localStats) {
|
|
2354
|
+
throw new Error(`Local path not found: ${localSource}`);
|
|
2355
|
+
}
|
|
2356
|
+
let remoteDestination = destinationPath;
|
|
2357
|
+
if (sources.length > 1) {
|
|
2358
|
+
remoteDestination = import_node_path.default.posix.join(
|
|
2359
|
+
destinationPath.replace(/\/$/, ""),
|
|
2360
|
+
import_node_path.default.posix.basename(source.replace(/\/$/, ""))
|
|
2361
|
+
);
|
|
2362
|
+
} else if (localStats.isFile() && destinationPath.endsWith("/")) {
|
|
2363
|
+
remoteDestination = import_node_path.default.posix.join(
|
|
2364
|
+
destinationPath.replace(/\/$/, ""),
|
|
2365
|
+
import_node_path.default.basename(source)
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
emit({
|
|
2369
|
+
type: "status",
|
|
2370
|
+
message: `${keyword} ${source} -> ${remoteDestination}`
|
|
2371
|
+
});
|
|
2372
|
+
await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep4) {
|
|
2376
|
+
let destinationPath = resolveContainerPath(destination, workingDir);
|
|
2377
|
+
const parsedUrl = new URL(url);
|
|
2378
|
+
const fileName = import_node_path.default.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
|
|
2379
|
+
if (destinationPath.endsWith("/")) {
|
|
2380
|
+
destinationPath = import_node_path.default.posix.join(destinationPath.replace(/\/$/, ""), fileName);
|
|
2381
|
+
}
|
|
2382
|
+
const parentDir = import_node_path.default.posix.dirname(destinationPath) || "/";
|
|
2383
|
+
emit({
|
|
2384
|
+
type: "status",
|
|
2385
|
+
message: `ADD ${url} -> ${destinationPath}`
|
|
2386
|
+
});
|
|
2387
|
+
await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
|
|
2388
|
+
await runStreaming(
|
|
2389
|
+
sandbox,
|
|
2390
|
+
emit,
|
|
2391
|
+
sleep4,
|
|
2392
|
+
"sh",
|
|
2393
|
+
[
|
|
2394
|
+
"-c",
|
|
2395
|
+
`curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
|
|
2396
|
+
],
|
|
2397
|
+
processEnv,
|
|
2398
|
+
workingDir
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
async function executeDockerfilePlan(sandbox, plan, emit, sleep4) {
|
|
2402
|
+
const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
|
|
2403
|
+
let workingDir = "/";
|
|
2404
|
+
for (const instruction of plan.instructions) {
|
|
2405
|
+
const { keyword, value, lineNumber } = instruction;
|
|
2406
|
+
if (keyword === "RUN") {
|
|
2407
|
+
emit({ type: "status", message: `RUN ${value}` });
|
|
2408
|
+
await runStreaming(
|
|
2409
|
+
sandbox,
|
|
2410
|
+
emit,
|
|
2411
|
+
sleep4,
|
|
2412
|
+
"sh",
|
|
2413
|
+
["-c", value],
|
|
2414
|
+
processEnv,
|
|
2415
|
+
workingDir
|
|
2416
|
+
);
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (keyword === "WORKDIR") {
|
|
2420
|
+
const tokens = shellSplit(value);
|
|
2421
|
+
if (tokens.length !== 1) {
|
|
2422
|
+
throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
|
|
2423
|
+
}
|
|
2424
|
+
workingDir = resolveContainerPath(tokens[0], workingDir);
|
|
2425
|
+
emit({ type: "status", message: `WORKDIR ${workingDir}` });
|
|
2426
|
+
await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
if (keyword === "ENV") {
|
|
2430
|
+
for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
|
|
2431
|
+
emit({ type: "status", message: `ENV ${key}=${envValue}` });
|
|
2432
|
+
processEnv[key] = envValue;
|
|
2433
|
+
await persistEnvVar(sandbox, processEnv, key, envValue);
|
|
2434
|
+
}
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
if (keyword === "COPY") {
|
|
2438
|
+
const { sources, destination } = parseCopyLikeValues(
|
|
2439
|
+
value,
|
|
2440
|
+
lineNumber,
|
|
2441
|
+
keyword
|
|
2442
|
+
);
|
|
2443
|
+
await copyFromContext(
|
|
2444
|
+
sandbox,
|
|
2445
|
+
emit,
|
|
2446
|
+
plan.contextDir,
|
|
2447
|
+
sources,
|
|
2448
|
+
destination,
|
|
2449
|
+
workingDir,
|
|
2450
|
+
keyword
|
|
2451
|
+
);
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
if (keyword === "ADD") {
|
|
2455
|
+
const { sources, destination } = parseCopyLikeValues(
|
|
2456
|
+
value,
|
|
2457
|
+
lineNumber,
|
|
2458
|
+
keyword
|
|
2459
|
+
);
|
|
2460
|
+
if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
|
|
2461
|
+
await addUrlToSandbox(
|
|
2462
|
+
sandbox,
|
|
2463
|
+
emit,
|
|
2464
|
+
sources[0],
|
|
2465
|
+
destination,
|
|
2466
|
+
workingDir,
|
|
2467
|
+
processEnv,
|
|
2468
|
+
sleep4
|
|
2469
|
+
);
|
|
2470
|
+
} else {
|
|
2471
|
+
await copyFromContext(
|
|
2472
|
+
sandbox,
|
|
2473
|
+
emit,
|
|
2474
|
+
plan.contextDir,
|
|
2475
|
+
sources,
|
|
2476
|
+
destination,
|
|
2477
|
+
workingDir,
|
|
2478
|
+
keyword
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
2484
|
+
emit({
|
|
2485
|
+
type: "warning",
|
|
2486
|
+
message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
|
|
2487
|
+
});
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
`line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async function registerImage(context, name, dockerfile, snapshotId, snapshotUri, isPublic) {
|
|
2496
|
+
if (!context.organizationId || !context.projectId) {
|
|
2497
|
+
throw new Error(
|
|
2498
|
+
"Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
2502
|
+
if (!bearerToken) {
|
|
2503
|
+
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
2504
|
+
}
|
|
2505
|
+
const baseUrl = context.apiUrl.replace(/\/+$/, "");
|
|
2506
|
+
const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
2507
|
+
const headers = {
|
|
2508
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
2509
|
+
"Content-Type": "application/json"
|
|
2510
|
+
};
|
|
2511
|
+
if (context.personalAccessToken && !context.apiKey) {
|
|
2512
|
+
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
2513
|
+
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
2514
|
+
}
|
|
2515
|
+
const response = await fetch(url, {
|
|
2516
|
+
method: "POST",
|
|
2517
|
+
headers,
|
|
2518
|
+
body: JSON.stringify({
|
|
2519
|
+
name,
|
|
2520
|
+
dockerfile,
|
|
2521
|
+
snapshotId,
|
|
2522
|
+
snapshotUri,
|
|
2523
|
+
isPublic
|
|
2524
|
+
})
|
|
2525
|
+
});
|
|
2526
|
+
if (!response.ok) {
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
`${response.status} ${response.statusText}: ${await response.text()}`
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
const text = await response.text();
|
|
2532
|
+
return text ? JSON.parse(text) : {};
|
|
2533
|
+
}
|
|
2534
|
+
async function createSandboxImage(source, options = {}, deps = {}) {
|
|
2535
|
+
const emit = deps.emit ?? defaultEmit;
|
|
2536
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2537
|
+
const context = buildContextFromEnv();
|
|
2538
|
+
const clientFactory = deps.createClient ?? createDefaultClient;
|
|
2539
|
+
const register = deps.registerImage ?? ((...args) => registerImage(...args));
|
|
2540
|
+
const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
|
|
2541
|
+
emit({ type: "status", message: `Loading ${sourceLabel}...` });
|
|
2542
|
+
const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
|
|
2543
|
+
emit({
|
|
2544
|
+
type: "status",
|
|
2545
|
+
message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
|
|
2546
|
+
});
|
|
2547
|
+
const client = clientFactory(context);
|
|
2548
|
+
let sandbox;
|
|
2549
|
+
try {
|
|
2550
|
+
sandbox = await client.createAndConnect({
|
|
2551
|
+
...plan.baseImage == null ? {} : { image: plan.baseImage },
|
|
2552
|
+
cpus: options.cpus ?? 2,
|
|
2553
|
+
memoryMb: options.memoryMb ?? 4096
|
|
2554
|
+
});
|
|
2555
|
+
emit({
|
|
2556
|
+
type: "status",
|
|
2557
|
+
message: `Materializing image in sandbox ${sandbox.sandboxId}...`
|
|
2558
|
+
});
|
|
2559
|
+
await executeDockerfilePlan(sandbox, plan, emit, sleep4);
|
|
2560
|
+
emit({ type: "status", message: "Creating snapshot..." });
|
|
2561
|
+
const snapshot = await client.snapshotAndWait(sandbox.sandboxId);
|
|
2562
|
+
emit({
|
|
2563
|
+
type: "snapshot_created",
|
|
2564
|
+
snapshot_id: snapshot.snapshotId,
|
|
2565
|
+
snapshot_uri: snapshot.snapshotUri ?? null
|
|
2566
|
+
});
|
|
2567
|
+
if (!snapshot.snapshotUri) {
|
|
2568
|
+
throw new Error(
|
|
2569
|
+
`Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
emit({
|
|
2573
|
+
type: "status",
|
|
2574
|
+
message: `Registering image '${plan.registeredName}'...`
|
|
2575
|
+
});
|
|
2576
|
+
const result = await register(
|
|
2577
|
+
context,
|
|
2578
|
+
plan.registeredName,
|
|
2579
|
+
plan.dockerfileText,
|
|
2580
|
+
snapshot.snapshotId,
|
|
2581
|
+
snapshot.snapshotUri,
|
|
2582
|
+
options.isPublic ?? false
|
|
2583
|
+
);
|
|
2584
|
+
emit({
|
|
2585
|
+
type: "image_registered",
|
|
2586
|
+
name: plan.registeredName,
|
|
2587
|
+
image_id: typeof result.id === "string" && result.id || typeof result.templateId === "string" && result.templateId || ""
|
|
2588
|
+
});
|
|
2589
|
+
emit({ type: "done" });
|
|
2590
|
+
return result;
|
|
2591
|
+
} finally {
|
|
2592
|
+
if (sandbox) {
|
|
2593
|
+
try {
|
|
2594
|
+
await sandbox.terminate();
|
|
2595
|
+
} catch {
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
client.close();
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
1439
2601
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1440
2602
|
0 && (module.exports = {
|
|
1441
2603
|
APIClient,
|
|
1442
2604
|
CloudClient,
|
|
1443
2605
|
ContainerState,
|
|
2606
|
+
Image,
|
|
2607
|
+
ImageBuildOperationType,
|
|
1444
2608
|
OutputMode,
|
|
1445
2609
|
PoolInUseError,
|
|
1446
2610
|
PoolNotFoundError,
|
|
1447
2611
|
ProcessStatus,
|
|
2612
|
+
Pty,
|
|
1448
2613
|
RemoteAPIError,
|
|
1449
2614
|
RequestExecutionError,
|
|
1450
2615
|
RequestFailedError,
|
|
@@ -1457,6 +2622,8 @@ var APIClient = class {
|
|
|
1457
2622
|
SandboxNotFoundError,
|
|
1458
2623
|
SandboxStatus,
|
|
1459
2624
|
SnapshotStatus,
|
|
1460
|
-
StdinMode
|
|
2625
|
+
StdinMode,
|
|
2626
|
+
createSandboxImage,
|
|
2627
|
+
dockerfileContent
|
|
1461
2628
|
});
|
|
1462
2629
|
//# sourceMappingURL=index.cjs.map
|