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.js
CHANGED
|
@@ -119,8 +119,8 @@ var HttpClient = class {
|
|
|
119
119
|
this.abortController = null;
|
|
120
120
|
}
|
|
121
121
|
/** Make a JSON request, returning the parsed response body. */
|
|
122
|
-
async requestJson(method,
|
|
123
|
-
const response = await this.requestResponse(method,
|
|
122
|
+
async requestJson(method, path2, options) {
|
|
123
|
+
const response = await this.requestResponse(method, path2, {
|
|
124
124
|
json: options?.body,
|
|
125
125
|
headers: options?.headers,
|
|
126
126
|
signal: options?.signal
|
|
@@ -130,14 +130,14 @@ var HttpClient = class {
|
|
|
130
130
|
return JSON.parse(text);
|
|
131
131
|
}
|
|
132
132
|
/** Make a request returning raw bytes. */
|
|
133
|
-
async requestBytes(method,
|
|
133
|
+
async requestBytes(method, path2, options) {
|
|
134
134
|
const headers = { ...options?.headers ?? {} };
|
|
135
135
|
if (options?.contentType) {
|
|
136
136
|
headers["Content-Type"] = options.contentType;
|
|
137
137
|
}
|
|
138
138
|
const response = await this.requestResponse(
|
|
139
139
|
method,
|
|
140
|
-
|
|
140
|
+
path2,
|
|
141
141
|
{
|
|
142
142
|
body: options?.body,
|
|
143
143
|
headers,
|
|
@@ -148,10 +148,10 @@ var HttpClient = class {
|
|
|
148
148
|
return new Uint8Array(buffer);
|
|
149
149
|
}
|
|
150
150
|
/** Make a request and return the raw Response (for SSE streaming). */
|
|
151
|
-
async requestStream(method,
|
|
151
|
+
async requestStream(method, path2, options) {
|
|
152
152
|
const response = await this.requestResponse(
|
|
153
153
|
method,
|
|
154
|
-
|
|
154
|
+
path2,
|
|
155
155
|
{
|
|
156
156
|
headers: { Accept: "text/event-stream" },
|
|
157
157
|
signal: options?.signal
|
|
@@ -163,7 +163,7 @@ var HttpClient = class {
|
|
|
163
163
|
return response.body;
|
|
164
164
|
}
|
|
165
165
|
/** Make a request and return the raw Response. */
|
|
166
|
-
async requestResponse(method,
|
|
166
|
+
async requestResponse(method, path2, options) {
|
|
167
167
|
const headers = {
|
|
168
168
|
...this.headers,
|
|
169
169
|
...options?.headers ?? {}
|
|
@@ -175,15 +175,15 @@ var HttpClient = class {
|
|
|
175
175
|
const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
|
|
176
176
|
return this.doFetch(
|
|
177
177
|
method,
|
|
178
|
-
|
|
178
|
+
path2,
|
|
179
179
|
body,
|
|
180
180
|
headers,
|
|
181
181
|
options?.signal,
|
|
182
182
|
options?.allowHttpErrors ?? false
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
|
-
async doFetch(method,
|
|
186
|
-
const url = `${this.baseUrl}${
|
|
185
|
+
async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
|
|
186
|
+
const url = `${this.baseUrl}${path2}`;
|
|
187
187
|
let lastError;
|
|
188
188
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
189
189
|
if (attempt > 0) {
|
|
@@ -216,7 +216,7 @@ var HttpClient = class {
|
|
|
216
216
|
return response;
|
|
217
217
|
}
|
|
218
218
|
const errorBody = await response.text().catch(() => "");
|
|
219
|
-
throwMappedError(response.status, errorBody,
|
|
219
|
+
throwMappedError(response.status, errorBody, path2);
|
|
220
220
|
} catch (err) {
|
|
221
221
|
clearTimeout(timeoutId);
|
|
222
222
|
if (err instanceof RemoteAPIError || err instanceof SandboxNotFoundError || err instanceof PoolNotFoundError || err instanceof PoolInUseError) {
|
|
@@ -247,7 +247,7 @@ function normalizeRequestBody(body) {
|
|
|
247
247
|
}
|
|
248
248
|
return body;
|
|
249
249
|
}
|
|
250
|
-
function throwMappedError(status, body,
|
|
250
|
+
function throwMappedError(status, body, path2) {
|
|
251
251
|
let message = body;
|
|
252
252
|
try {
|
|
253
253
|
const parsed = JSON.parse(body);
|
|
@@ -256,19 +256,19 @@ function throwMappedError(status, body, path) {
|
|
|
256
256
|
} catch {
|
|
257
257
|
}
|
|
258
258
|
if (status === 404) {
|
|
259
|
-
if (
|
|
260
|
-
const match =
|
|
259
|
+
if (path2.includes("sandbox-pools") || path2.includes("pools")) {
|
|
260
|
+
const match = path2.match(/sandbox-pools\/([^/]+)/);
|
|
261
261
|
if (match) throw new PoolNotFoundError(match[1]);
|
|
262
262
|
}
|
|
263
|
-
if (
|
|
264
|
-
const match =
|
|
263
|
+
if (path2.includes("sandboxes")) {
|
|
264
|
+
const match = path2.match(/sandboxes\/([^/]+)/);
|
|
265
265
|
if (match) throw new SandboxNotFoundError(match[1]);
|
|
266
266
|
}
|
|
267
267
|
throw new RemoteAPIError(404, message);
|
|
268
268
|
}
|
|
269
269
|
if (status === 409) {
|
|
270
|
-
if (
|
|
271
|
-
const match =
|
|
270
|
+
if (path2.includes("sandbox-pools") || path2.includes("pools")) {
|
|
271
|
+
const match = path2.match(/sandbox-pools\/([^/]+)/);
|
|
272
272
|
if (match) throw new PoolInUseError(match[1], message);
|
|
273
273
|
}
|
|
274
274
|
}
|
|
@@ -296,6 +296,7 @@ var SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => {
|
|
|
296
296
|
SandboxStatus2["PENDING"] = "pending";
|
|
297
297
|
SandboxStatus2["RUNNING"] = "running";
|
|
298
298
|
SandboxStatus2["SNAPSHOTTING"] = "snapshotting";
|
|
299
|
+
SandboxStatus2["SUSPENDING"] = "suspending";
|
|
299
300
|
SandboxStatus2["SUSPENDED"] = "suspended";
|
|
300
301
|
SandboxStatus2["TERMINATED"] = "terminated";
|
|
301
302
|
return SandboxStatus2;
|
|
@@ -468,18 +469,205 @@ function resolveProxyTarget(proxyUrl, sandboxId) {
|
|
|
468
469
|
};
|
|
469
470
|
}
|
|
470
471
|
}
|
|
471
|
-
function lifecyclePath(
|
|
472
|
+
function lifecyclePath(path2, isLocal, namespace) {
|
|
472
473
|
if (isLocal) {
|
|
473
|
-
return `/v1/namespaces/${namespace}/${
|
|
474
|
+
return `/v1/namespaces/${namespace}/${path2}`;
|
|
474
475
|
}
|
|
475
|
-
return `/${
|
|
476
|
+
return `/${path2}`;
|
|
476
477
|
}
|
|
477
478
|
|
|
478
479
|
// src/sandbox.ts
|
|
480
|
+
import WebSocket from "ws";
|
|
481
|
+
var PTY_OP_DATA = 0;
|
|
482
|
+
var PTY_OP_RESIZE = 1;
|
|
483
|
+
var PTY_OP_READY = 2;
|
|
484
|
+
var PTY_OP_EXIT = 3;
|
|
485
|
+
var Pty = class {
|
|
486
|
+
sessionId;
|
|
487
|
+
token;
|
|
488
|
+
wsUrl;
|
|
489
|
+
wsHeaders;
|
|
490
|
+
killSession;
|
|
491
|
+
socket = null;
|
|
492
|
+
connectPromise = null;
|
|
493
|
+
intentionalDisconnect = false;
|
|
494
|
+
exitCode = null;
|
|
495
|
+
waitSettled = false;
|
|
496
|
+
dataHandlers = /* @__PURE__ */ new Set();
|
|
497
|
+
exitHandlers = /* @__PURE__ */ new Set();
|
|
498
|
+
waitPromise;
|
|
499
|
+
resolveWait;
|
|
500
|
+
rejectWait;
|
|
501
|
+
constructor(options) {
|
|
502
|
+
this.sessionId = options.sessionId;
|
|
503
|
+
this.token = options.token;
|
|
504
|
+
this.wsUrl = options.wsUrl;
|
|
505
|
+
this.wsHeaders = options.wsHeaders;
|
|
506
|
+
this.killSession = options.killSession;
|
|
507
|
+
this.waitPromise = new Promise((resolve, reject) => {
|
|
508
|
+
this.resolveWait = resolve;
|
|
509
|
+
this.rejectWait = reject;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
onData(handler) {
|
|
513
|
+
this.dataHandlers.add(handler);
|
|
514
|
+
return () => this.dataHandlers.delete(handler);
|
|
515
|
+
}
|
|
516
|
+
onExit(handler) {
|
|
517
|
+
this.exitHandlers.add(handler);
|
|
518
|
+
if (this.exitCode != null) {
|
|
519
|
+
queueMicrotask(() => handler(this.exitCode));
|
|
520
|
+
}
|
|
521
|
+
return () => this.exitHandlers.delete(handler);
|
|
522
|
+
}
|
|
523
|
+
async connect() {
|
|
524
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
525
|
+
return this;
|
|
526
|
+
}
|
|
527
|
+
if (this.connectPromise) {
|
|
528
|
+
return this.connectPromise;
|
|
529
|
+
}
|
|
530
|
+
this.intentionalDisconnect = false;
|
|
531
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
532
|
+
let opened = false;
|
|
533
|
+
const socket = new WebSocket(this.wsUrl, {
|
|
534
|
+
headers: this.wsHeaders
|
|
535
|
+
});
|
|
536
|
+
this.socket = socket;
|
|
537
|
+
socket.on("open", async () => {
|
|
538
|
+
try {
|
|
539
|
+
await sendPtyFrame(socket, Buffer.from([PTY_OP_READY]));
|
|
540
|
+
opened = true;
|
|
541
|
+
resolve(this);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
reject(error);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
socket.on("message", (message) => {
|
|
547
|
+
const bytes = normalizePtyMessage(message);
|
|
548
|
+
const opcode = bytes[0];
|
|
549
|
+
if (opcode === PTY_OP_DATA) {
|
|
550
|
+
const payload = bytes.subarray(1);
|
|
551
|
+
for (const handler of this.dataHandlers) {
|
|
552
|
+
handler(payload);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (opcode === PTY_OP_EXIT && bytes.length >= 5) {
|
|
557
|
+
this.finishWait(bytes.readInt32BE(1));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
socket.on("close", (code, reason) => {
|
|
561
|
+
const closeReason = Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
|
|
562
|
+
if (this.socket === socket) {
|
|
563
|
+
this.socket = null;
|
|
564
|
+
}
|
|
565
|
+
this.connectPromise = null;
|
|
566
|
+
if (this.exitCode != null) {
|
|
567
|
+
this.finishWait(this.exitCode);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (closeReason.startsWith("exit:")) {
|
|
571
|
+
const parsed = Number.parseInt(closeReason.slice(5), 10);
|
|
572
|
+
this.finishWait(Number.isNaN(parsed) ? -1 : parsed);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (this.intentionalDisconnect) {
|
|
576
|
+
this.intentionalDisconnect = false;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (!opened) {
|
|
580
|
+
reject(new SandboxError(
|
|
581
|
+
`PTY websocket closed before READY completed: ${code} ${closeReason || "no reason"}`
|
|
582
|
+
));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (closeReason === "session terminated") {
|
|
586
|
+
this.failWait(new SandboxError("PTY session terminated"));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.failWait(
|
|
590
|
+
new SandboxError(
|
|
591
|
+
`PTY websocket closed unexpectedly: ${code} ${closeReason || "no reason"}`
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
socket.on("error", (error) => {
|
|
596
|
+
if (!opened) {
|
|
597
|
+
reject(error);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
return this.connectPromise;
|
|
602
|
+
}
|
|
603
|
+
async sendInput(input) {
|
|
604
|
+
const socket = this.requireOpenSocket();
|
|
605
|
+
await sendPtyFrame(socket, encodePtyInput(input));
|
|
606
|
+
}
|
|
607
|
+
async resize(cols, rows) {
|
|
608
|
+
const socket = this.requireOpenSocket();
|
|
609
|
+
await sendPtyFrame(socket, encodePtyResize(cols, rows));
|
|
610
|
+
}
|
|
611
|
+
disconnect(code = 1e3, reason = "client disconnect") {
|
|
612
|
+
if (!this.socket) return;
|
|
613
|
+
this.intentionalDisconnect = true;
|
|
614
|
+
this.socket.close(code, reason);
|
|
615
|
+
}
|
|
616
|
+
wait() {
|
|
617
|
+
return this.waitPromise;
|
|
618
|
+
}
|
|
619
|
+
async kill() {
|
|
620
|
+
await this.killSession();
|
|
621
|
+
}
|
|
622
|
+
requireOpenSocket() {
|
|
623
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
624
|
+
throw new SandboxError("PTY is not connected");
|
|
625
|
+
}
|
|
626
|
+
return this.socket;
|
|
627
|
+
}
|
|
628
|
+
finishWait(exitCode) {
|
|
629
|
+
if (this.waitSettled) return;
|
|
630
|
+
this.waitSettled = true;
|
|
631
|
+
this.exitCode = exitCode;
|
|
632
|
+
for (const handler of this.exitHandlers) {
|
|
633
|
+
handler(exitCode);
|
|
634
|
+
}
|
|
635
|
+
this.resolveWait(exitCode);
|
|
636
|
+
}
|
|
637
|
+
failWait(error) {
|
|
638
|
+
if (this.waitSettled) return;
|
|
639
|
+
this.waitSettled = true;
|
|
640
|
+
this.rejectWait(error);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
function normalizePtyMessage(message) {
|
|
644
|
+
if (Buffer.isBuffer(message)) return message;
|
|
645
|
+
if (Array.isArray(message)) {
|
|
646
|
+
return Buffer.concat(message.map((part) => Buffer.from(part)));
|
|
647
|
+
}
|
|
648
|
+
return Buffer.from(message);
|
|
649
|
+
}
|
|
650
|
+
function encodePtyInput(input) {
|
|
651
|
+
const payload = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
|
|
652
|
+
return Buffer.concat([Buffer.from([PTY_OP_DATA]), payload]);
|
|
653
|
+
}
|
|
654
|
+
function encodePtyResize(cols, rows) {
|
|
655
|
+
const frame = Buffer.alloc(5);
|
|
656
|
+
frame[0] = PTY_OP_RESIZE;
|
|
657
|
+
frame.writeUInt16BE(cols, 1);
|
|
658
|
+
frame.writeUInt16BE(rows, 3);
|
|
659
|
+
return frame;
|
|
660
|
+
}
|
|
661
|
+
function sendPtyFrame(socket, frame) {
|
|
662
|
+
return new Promise((resolve, reject) => {
|
|
663
|
+
socket.send(frame, (error) => error ? reject(error) : resolve());
|
|
664
|
+
});
|
|
665
|
+
}
|
|
479
666
|
var Sandbox = class {
|
|
480
667
|
sandboxId;
|
|
481
668
|
http;
|
|
482
669
|
baseUrl;
|
|
670
|
+
wsHeaders;
|
|
483
671
|
ownsSandbox = false;
|
|
484
672
|
lifecycleClient = null;
|
|
485
673
|
constructor(options) {
|
|
@@ -487,6 +675,19 @@ var Sandbox = class {
|
|
|
487
675
|
const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
|
|
488
676
|
const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
|
|
489
677
|
this.baseUrl = baseUrl;
|
|
678
|
+
this.wsHeaders = {};
|
|
679
|
+
if (options.apiKey) {
|
|
680
|
+
this.wsHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
681
|
+
}
|
|
682
|
+
if (options.organizationId) {
|
|
683
|
+
this.wsHeaders["X-Forwarded-Organization-Id"] = options.organizationId;
|
|
684
|
+
}
|
|
685
|
+
if (options.projectId) {
|
|
686
|
+
this.wsHeaders["X-Forwarded-Project-Id"] = options.projectId;
|
|
687
|
+
}
|
|
688
|
+
if (hostHeader) {
|
|
689
|
+
this.wsHeaders.Host = hostHeader;
|
|
690
|
+
}
|
|
490
691
|
this.http = new HttpClient({
|
|
491
692
|
baseUrl,
|
|
492
693
|
apiKey: options.apiKey,
|
|
@@ -665,29 +866,29 @@ var Sandbox = class {
|
|
|
665
866
|
}
|
|
666
867
|
}
|
|
667
868
|
// --- File operations ---
|
|
668
|
-
async readFile(
|
|
869
|
+
async readFile(path2) {
|
|
669
870
|
return this.http.requestBytes(
|
|
670
871
|
"GET",
|
|
671
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
872
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
672
873
|
);
|
|
673
874
|
}
|
|
674
|
-
async writeFile(
|
|
875
|
+
async writeFile(path2, content) {
|
|
675
876
|
await this.http.requestBytes(
|
|
676
877
|
"PUT",
|
|
677
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
878
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`,
|
|
678
879
|
{ body: content, contentType: "application/octet-stream" }
|
|
679
880
|
);
|
|
680
881
|
}
|
|
681
|
-
async deleteFile(
|
|
882
|
+
async deleteFile(path2) {
|
|
682
883
|
await this.http.requestJson(
|
|
683
884
|
"DELETE",
|
|
684
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
885
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
685
886
|
);
|
|
686
887
|
}
|
|
687
|
-
async listDirectory(
|
|
888
|
+
async listDirectory(path2) {
|
|
688
889
|
const raw = await this.http.requestJson(
|
|
689
890
|
"GET",
|
|
690
|
-
`/api/v1/files/list?path=${encodeURIComponent(
|
|
891
|
+
`/api/v1/files/list?path=${encodeURIComponent(path2)}`
|
|
691
892
|
);
|
|
692
893
|
return fromSnakeKeys(raw);
|
|
693
894
|
}
|
|
@@ -708,6 +909,43 @@ var Sandbox = class {
|
|
|
708
909
|
);
|
|
709
910
|
return fromSnakeKeys(raw);
|
|
710
911
|
}
|
|
912
|
+
async createPty(options) {
|
|
913
|
+
const { onData, onExit, ...createOptions } = options;
|
|
914
|
+
const session = await this.createPtySession(createOptions);
|
|
915
|
+
try {
|
|
916
|
+
return await this.connectPty(session.sessionId, session.token, { onData, onExit });
|
|
917
|
+
} catch (error) {
|
|
918
|
+
try {
|
|
919
|
+
await this.http.requestResponse("DELETE", `/api/v1/pty/${session.sessionId}`);
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
throw error;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async connectPty(sessionId, token, options) {
|
|
926
|
+
const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
|
|
927
|
+
const authToken = wsUrl.searchParams.get("token") ?? token;
|
|
928
|
+
const pty = new Pty({
|
|
929
|
+
sessionId,
|
|
930
|
+
token: authToken,
|
|
931
|
+
wsUrl: wsUrl.toString(),
|
|
932
|
+
wsHeaders: {
|
|
933
|
+
...this.wsHeaders,
|
|
934
|
+
"X-PTY-Token": authToken
|
|
935
|
+
},
|
|
936
|
+
killSession: async () => {
|
|
937
|
+
await this.http.requestResponse("DELETE", `/api/v1/pty/${sessionId}`);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
if (options?.onData) {
|
|
941
|
+
pty.onData(options.onData);
|
|
942
|
+
}
|
|
943
|
+
if (options?.onExit) {
|
|
944
|
+
pty.onExit(options.onExit);
|
|
945
|
+
}
|
|
946
|
+
await pty.connect();
|
|
947
|
+
return pty;
|
|
948
|
+
}
|
|
711
949
|
ptyWsUrl(sessionId, token) {
|
|
712
950
|
let wsBase;
|
|
713
951
|
if (this.baseUrl.startsWith("https://")) {
|
|
@@ -835,6 +1073,15 @@ var SandboxClient = class _SandboxClient {
|
|
|
835
1073
|
async update(sandboxId, options) {
|
|
836
1074
|
const body = {};
|
|
837
1075
|
if (options.name != null) body.name = options.name;
|
|
1076
|
+
if (options.allowUnauthenticatedAccess != null) {
|
|
1077
|
+
body.allow_unauthenticated_access = options.allowUnauthenticatedAccess;
|
|
1078
|
+
}
|
|
1079
|
+
if (options.exposedPorts != null) {
|
|
1080
|
+
body.exposed_ports = normalizeUserPorts(options.exposedPorts);
|
|
1081
|
+
}
|
|
1082
|
+
if (Object.keys(body).length === 0) {
|
|
1083
|
+
throw new SandboxError("At least one sandbox update field must be provided.");
|
|
1084
|
+
}
|
|
838
1085
|
const raw = await this.http.requestJson(
|
|
839
1086
|
"PATCH",
|
|
840
1087
|
this.path(`sandboxes/${sandboxId}`),
|
|
@@ -842,12 +1089,54 @@ var SandboxClient = class _SandboxClient {
|
|
|
842
1089
|
);
|
|
843
1090
|
return fromSnakeKeys(raw, "sandboxId");
|
|
844
1091
|
}
|
|
1092
|
+
async getPortAccess(sandboxId) {
|
|
1093
|
+
const info = await this.get(sandboxId);
|
|
1094
|
+
return {
|
|
1095
|
+
allowUnauthenticatedAccess: info.allowUnauthenticatedAccess ?? false,
|
|
1096
|
+
exposedPorts: dedupeAndSortPorts(info.exposedPorts ?? []),
|
|
1097
|
+
sandboxUrl: info.sandboxUrl
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
async exposePorts(sandboxId, ports, options) {
|
|
1101
|
+
const requestedPorts = normalizeUserPorts(ports);
|
|
1102
|
+
const current = await this.getPortAccess(sandboxId);
|
|
1103
|
+
const desiredPorts = dedupeAndSortPorts([
|
|
1104
|
+
...current.exposedPorts,
|
|
1105
|
+
...requestedPorts
|
|
1106
|
+
]);
|
|
1107
|
+
return this.update(sandboxId, {
|
|
1108
|
+
allowUnauthenticatedAccess: options?.allowUnauthenticatedAccess ?? current.allowUnauthenticatedAccess,
|
|
1109
|
+
exposedPorts: desiredPorts
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
async unexposePorts(sandboxId, ports) {
|
|
1113
|
+
const requestedPorts = normalizeUserPorts(ports);
|
|
1114
|
+
const current = await this.getPortAccess(sandboxId);
|
|
1115
|
+
const toRemove = new Set(requestedPorts);
|
|
1116
|
+
const desiredPorts = current.exposedPorts.filter((port) => !toRemove.has(port));
|
|
1117
|
+
return this.update(sandboxId, {
|
|
1118
|
+
allowUnauthenticatedAccess: desiredPorts.length ? current.allowUnauthenticatedAccess : false,
|
|
1119
|
+
exposedPorts: desiredPorts
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
845
1122
|
async delete(sandboxId) {
|
|
846
1123
|
await this.http.requestJson(
|
|
847
1124
|
"DELETE",
|
|
848
1125
|
this.path(`sandboxes/${sandboxId}`)
|
|
849
1126
|
);
|
|
850
1127
|
}
|
|
1128
|
+
async suspend(sandboxId) {
|
|
1129
|
+
await this.http.requestResponse(
|
|
1130
|
+
"POST",
|
|
1131
|
+
this.path(`sandboxes/${sandboxId}/suspend`)
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
async resume(sandboxId) {
|
|
1135
|
+
await this.http.requestResponse(
|
|
1136
|
+
"POST",
|
|
1137
|
+
this.path(`sandboxes/${sandboxId}/resume`)
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
851
1140
|
async claim(poolId) {
|
|
852
1141
|
const raw = await this.http.requestJson(
|
|
853
1142
|
"POST",
|
|
@@ -1015,6 +1304,22 @@ var SandboxClient = class _SandboxClient {
|
|
|
1015
1304
|
function sleep3(ms) {
|
|
1016
1305
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1017
1306
|
}
|
|
1307
|
+
var RESERVED_SANDBOX_MANAGEMENT_PORT = 9501;
|
|
1308
|
+
function normalizeUserPorts(ports) {
|
|
1309
|
+
return dedupeAndSortPorts(ports.map(validateUserPort));
|
|
1310
|
+
}
|
|
1311
|
+
function validateUserPort(port) {
|
|
1312
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1313
|
+
throw new SandboxError(`invalid port '${port}'`);
|
|
1314
|
+
}
|
|
1315
|
+
if (port === RESERVED_SANDBOX_MANAGEMENT_PORT) {
|
|
1316
|
+
throw new SandboxError("port 9501 is reserved for sandbox management");
|
|
1317
|
+
}
|
|
1318
|
+
return port;
|
|
1319
|
+
}
|
|
1320
|
+
function dedupeAndSortPorts(ports) {
|
|
1321
|
+
return [...new Set(ports)].sort((a, b) => a - b);
|
|
1322
|
+
}
|
|
1018
1323
|
|
|
1019
1324
|
// src/cloud-client.ts
|
|
1020
1325
|
var CloudClient = class _CloudClient {
|
|
@@ -1081,19 +1386,19 @@ var CloudClient = class _CloudClient {
|
|
|
1081
1386
|
return fromSnakeKeys(raw);
|
|
1082
1387
|
}
|
|
1083
1388
|
async runRequest(applicationName, inputs = []) {
|
|
1084
|
-
const
|
|
1389
|
+
const path2 = this.namespacePath(
|
|
1085
1390
|
`applications/${encodeURIComponent(applicationName)}`
|
|
1086
1391
|
);
|
|
1087
|
-
const response = inputs.length === 0 ? await this.http.requestResponse("POST",
|
|
1392
|
+
const response = inputs.length === 0 ? await this.http.requestResponse("POST", path2, {
|
|
1088
1393
|
body: new Uint8Array(),
|
|
1089
1394
|
headers: { Accept: "application/json" }
|
|
1090
|
-
}) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST",
|
|
1395
|
+
}) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST", path2, {
|
|
1091
1396
|
body: toRequestBody(inputs[0].data),
|
|
1092
1397
|
headers: {
|
|
1093
1398
|
Accept: "application/json",
|
|
1094
1399
|
"Content-Type": inputs[0].contentType
|
|
1095
1400
|
}
|
|
1096
|
-
}) : await this.runMultipartRequest(
|
|
1401
|
+
}) : await this.runMultipartRequest(path2, inputs);
|
|
1097
1402
|
const body = await parseJsonResponse(response);
|
|
1098
1403
|
const requestId = body?.request_id;
|
|
1099
1404
|
if (!requestId) {
|
|
@@ -1247,7 +1552,7 @@ var CloudClient = class _CloudClient {
|
|
|
1247
1552
|
yield fromSnakeKeys(event);
|
|
1248
1553
|
}
|
|
1249
1554
|
}
|
|
1250
|
-
async runMultipartRequest(
|
|
1555
|
+
async runMultipartRequest(path2, inputs) {
|
|
1251
1556
|
const form = new FormData();
|
|
1252
1557
|
for (const input of inputs) {
|
|
1253
1558
|
form.append(
|
|
@@ -1256,7 +1561,7 @@ var CloudClient = class _CloudClient {
|
|
|
1256
1561
|
input.name
|
|
1257
1562
|
);
|
|
1258
1563
|
}
|
|
1259
|
-
return this.http.requestResponse("POST",
|
|
1564
|
+
return this.http.requestResponse("POST", path2, {
|
|
1260
1565
|
body: form,
|
|
1261
1566
|
headers: { Accept: "application/json" }
|
|
1262
1567
|
});
|
|
@@ -1391,14 +1696,859 @@ var APIClient = class {
|
|
|
1391
1696
|
return this.cloudClient.requestOutput(applicationName, requestId);
|
|
1392
1697
|
}
|
|
1393
1698
|
};
|
|
1699
|
+
|
|
1700
|
+
// src/sandbox-image.ts
|
|
1701
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
1702
|
+
import path from "path";
|
|
1703
|
+
import { parseArgs } from "util";
|
|
1704
|
+
|
|
1705
|
+
// src/image.ts
|
|
1706
|
+
import { randomUUID } from "crypto";
|
|
1707
|
+
var ImageBuildOperationType = {
|
|
1708
|
+
ADD: "ADD",
|
|
1709
|
+
COPY: "COPY",
|
|
1710
|
+
ENV: "ENV",
|
|
1711
|
+
RUN: "RUN",
|
|
1712
|
+
WORKDIR: "WORKDIR"
|
|
1713
|
+
};
|
|
1714
|
+
function cloneOperation(op) {
|
|
1715
|
+
return {
|
|
1716
|
+
type: op.type,
|
|
1717
|
+
args: [...op.args],
|
|
1718
|
+
options: { ...op.options }
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
var Image = class {
|
|
1722
|
+
_id;
|
|
1723
|
+
_name;
|
|
1724
|
+
_tag;
|
|
1725
|
+
_baseImage;
|
|
1726
|
+
_buildOperations;
|
|
1727
|
+
constructor(nameOrOptions = {}, tag = "latest", baseImage = null) {
|
|
1728
|
+
this._id = randomUUID();
|
|
1729
|
+
this._buildOperations = [];
|
|
1730
|
+
if (typeof nameOrOptions === "string") {
|
|
1731
|
+
this._name = nameOrOptions;
|
|
1732
|
+
this._tag = tag;
|
|
1733
|
+
this._baseImage = baseImage;
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
this._name = nameOrOptions.name ?? "default";
|
|
1737
|
+
this._tag = nameOrOptions.tag ?? "latest";
|
|
1738
|
+
this._baseImage = nameOrOptions.baseImage ?? null;
|
|
1739
|
+
}
|
|
1740
|
+
get name() {
|
|
1741
|
+
return this._name;
|
|
1742
|
+
}
|
|
1743
|
+
get tag() {
|
|
1744
|
+
return this._tag;
|
|
1745
|
+
}
|
|
1746
|
+
get baseImage() {
|
|
1747
|
+
return this._baseImage;
|
|
1748
|
+
}
|
|
1749
|
+
get buildOperations() {
|
|
1750
|
+
return this._buildOperations.map(cloneOperation);
|
|
1751
|
+
}
|
|
1752
|
+
add(src, dest, options = void 0) {
|
|
1753
|
+
return this._addOperation({
|
|
1754
|
+
type: ImageBuildOperationType.ADD,
|
|
1755
|
+
args: [src, dest],
|
|
1756
|
+
options: options == null ? {} : { ...options }
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
copy(src, dest, options = void 0) {
|
|
1760
|
+
return this._addOperation({
|
|
1761
|
+
type: ImageBuildOperationType.COPY,
|
|
1762
|
+
args: [src, dest],
|
|
1763
|
+
options: options == null ? {} : { ...options }
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
env(key, value) {
|
|
1767
|
+
return this._addOperation({
|
|
1768
|
+
type: ImageBuildOperationType.ENV,
|
|
1769
|
+
args: [key, value],
|
|
1770
|
+
options: {}
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
run(commands, options = void 0) {
|
|
1774
|
+
return this._addOperation({
|
|
1775
|
+
type: ImageBuildOperationType.RUN,
|
|
1776
|
+
args: Array.isArray(commands) ? [...commands] : [commands],
|
|
1777
|
+
options: options == null ? {} : { ...options }
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
workdir(directory) {
|
|
1781
|
+
return this._addOperation({
|
|
1782
|
+
type: ImageBuildOperationType.WORKDIR,
|
|
1783
|
+
args: [directory],
|
|
1784
|
+
options: {}
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
_addOperation(op) {
|
|
1788
|
+
this._buildOperations.push(op);
|
|
1789
|
+
return this;
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
function renderOptions(options) {
|
|
1793
|
+
const entries = Object.entries(options);
|
|
1794
|
+
if (entries.length === 0) {
|
|
1795
|
+
return "";
|
|
1796
|
+
}
|
|
1797
|
+
return ` ${entries.map(([key, value]) => `--${key}=${value}`).join(" ")}`;
|
|
1798
|
+
}
|
|
1799
|
+
function renderBuildOp(op) {
|
|
1800
|
+
const options = renderOptions(op.options);
|
|
1801
|
+
if (op.type === ImageBuildOperationType.ENV) {
|
|
1802
|
+
return `ENV${options} ${op.args[0]}=${JSON.stringify(op.args[1])}`;
|
|
1803
|
+
}
|
|
1804
|
+
return `${op.type}${options} ${op.args.join(" ")}`;
|
|
1805
|
+
}
|
|
1806
|
+
function dockerfileContent(image) {
|
|
1807
|
+
const lines = image.baseImage == null ? [] : [`FROM ${image.baseImage}`];
|
|
1808
|
+
lines.push(...image.buildOperations.map((op) => renderBuildOp(op)));
|
|
1809
|
+
return lines.join("\n");
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/sandbox-image.ts
|
|
1813
|
+
var BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
|
|
1814
|
+
var IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
1815
|
+
"CMD",
|
|
1816
|
+
"ENTRYPOINT",
|
|
1817
|
+
"EXPOSE",
|
|
1818
|
+
"HEALTHCHECK",
|
|
1819
|
+
"LABEL",
|
|
1820
|
+
"STOPSIGNAL",
|
|
1821
|
+
"VOLUME"
|
|
1822
|
+
]);
|
|
1823
|
+
var UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
1824
|
+
"ARG",
|
|
1825
|
+
"ONBUILD",
|
|
1826
|
+
"SHELL",
|
|
1827
|
+
"USER"
|
|
1828
|
+
]);
|
|
1829
|
+
function defaultRegisteredName(dockerfilePath) {
|
|
1830
|
+
const parsed = path.parse(dockerfilePath);
|
|
1831
|
+
if (parsed.name.toLowerCase() === "dockerfile") {
|
|
1832
|
+
const parentName = path.basename(path.dirname(dockerfilePath)).trim();
|
|
1833
|
+
return parentName || "sandbox-image";
|
|
1834
|
+
}
|
|
1835
|
+
return parsed.name || "sandbox-image";
|
|
1836
|
+
}
|
|
1837
|
+
function logicalDockerfileLines(dockerfileText) {
|
|
1838
|
+
const logicalLines = [];
|
|
1839
|
+
let parts = [];
|
|
1840
|
+
let startLine = null;
|
|
1841
|
+
for (const [index, rawLine] of dockerfileText.split(/\r?\n/).entries()) {
|
|
1842
|
+
const lineNumber = index + 1;
|
|
1843
|
+
const stripped = rawLine.trim();
|
|
1844
|
+
if (parts.length === 0 && (!stripped || stripped.startsWith("#"))) {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
if (startLine == null) {
|
|
1848
|
+
startLine = lineNumber;
|
|
1849
|
+
}
|
|
1850
|
+
let line = rawLine.replace(/\s+$/, "");
|
|
1851
|
+
const continued = line.endsWith("\\");
|
|
1852
|
+
if (continued) {
|
|
1853
|
+
line = line.slice(0, -1);
|
|
1854
|
+
}
|
|
1855
|
+
const normalized = line.trim();
|
|
1856
|
+
if (normalized && !normalized.startsWith("#")) {
|
|
1857
|
+
parts.push(normalized);
|
|
1858
|
+
}
|
|
1859
|
+
if (continued) {
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (parts.length > 0) {
|
|
1863
|
+
logicalLines.push({
|
|
1864
|
+
lineNumber: startLine ?? lineNumber,
|
|
1865
|
+
line: parts.join(" ")
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
parts = [];
|
|
1869
|
+
startLine = null;
|
|
1870
|
+
}
|
|
1871
|
+
if (parts.length > 0) {
|
|
1872
|
+
logicalLines.push({
|
|
1873
|
+
lineNumber: startLine ?? 1,
|
|
1874
|
+
line: parts.join(" ")
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
return logicalLines;
|
|
1878
|
+
}
|
|
1879
|
+
function splitInstruction(line, lineNumber) {
|
|
1880
|
+
const trimmed = line.trim();
|
|
1881
|
+
if (!trimmed) {
|
|
1882
|
+
throw new Error(`line ${lineNumber}: empty Dockerfile instruction`);
|
|
1883
|
+
}
|
|
1884
|
+
const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
|
|
1885
|
+
if (!match) {
|
|
1886
|
+
throw new Error(`line ${lineNumber}: invalid Dockerfile instruction`);
|
|
1887
|
+
}
|
|
1888
|
+
return {
|
|
1889
|
+
keyword: match[1].toUpperCase(),
|
|
1890
|
+
value: (match[2] ?? "").trim()
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function shellSplit(input) {
|
|
1894
|
+
const tokens = [];
|
|
1895
|
+
let current = "";
|
|
1896
|
+
let quote = null;
|
|
1897
|
+
let escape = false;
|
|
1898
|
+
for (let i = 0; i < input.length; i++) {
|
|
1899
|
+
const char = input[i];
|
|
1900
|
+
if (escape) {
|
|
1901
|
+
current += char;
|
|
1902
|
+
escape = false;
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
if (quote == null) {
|
|
1906
|
+
if (/\s/.test(char)) {
|
|
1907
|
+
if (current) {
|
|
1908
|
+
tokens.push(current);
|
|
1909
|
+
current = "";
|
|
1910
|
+
}
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (char === "'" || char === '"') {
|
|
1914
|
+
quote = char;
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
if (char === "\\") {
|
|
1918
|
+
escape = true;
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
current += char;
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
if (quote === "'") {
|
|
1925
|
+
if (char === "'") {
|
|
1926
|
+
quote = null;
|
|
1927
|
+
} else {
|
|
1928
|
+
current += char;
|
|
1929
|
+
}
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
if (char === '"') {
|
|
1933
|
+
quote = null;
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
if (char === "\\") {
|
|
1937
|
+
const next = input[++i];
|
|
1938
|
+
if (next == null) {
|
|
1939
|
+
throw new Error(`unterminated escape sequence in '${input}'`);
|
|
1940
|
+
}
|
|
1941
|
+
current += next;
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
current += char;
|
|
1945
|
+
}
|
|
1946
|
+
if (escape) {
|
|
1947
|
+
throw new Error(`unterminated escape sequence in '${input}'`);
|
|
1948
|
+
}
|
|
1949
|
+
if (quote != null) {
|
|
1950
|
+
throw new Error(`unterminated quoted string in '${input}'`);
|
|
1951
|
+
}
|
|
1952
|
+
if (current) {
|
|
1953
|
+
tokens.push(current);
|
|
1954
|
+
}
|
|
1955
|
+
return tokens;
|
|
1956
|
+
}
|
|
1957
|
+
function shellQuote(value) {
|
|
1958
|
+
if (!value) {
|
|
1959
|
+
return "''";
|
|
1960
|
+
}
|
|
1961
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1962
|
+
}
|
|
1963
|
+
function stripLeadingFlags(value) {
|
|
1964
|
+
const flags = {};
|
|
1965
|
+
let remaining = value.trimStart();
|
|
1966
|
+
while (remaining.startsWith("--")) {
|
|
1967
|
+
const firstSpace = remaining.indexOf(" ");
|
|
1968
|
+
if (firstSpace === -1) {
|
|
1969
|
+
throw new Error(`invalid Dockerfile flag syntax: ${value}`);
|
|
1970
|
+
}
|
|
1971
|
+
const token = remaining.slice(0, firstSpace);
|
|
1972
|
+
const rest = remaining.slice(firstSpace + 1).trimStart();
|
|
1973
|
+
const flagBody = token.slice(2);
|
|
1974
|
+
if (flagBody.includes("=")) {
|
|
1975
|
+
const [key, flagValue2] = flagBody.split(/=(.*)/s, 2);
|
|
1976
|
+
flags[key] = flagValue2;
|
|
1977
|
+
remaining = rest;
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
const [flagValue, ...restTokens] = shellSplit(rest);
|
|
1981
|
+
if (flagValue == null) {
|
|
1982
|
+
throw new Error(`missing value for Dockerfile flag '${token}'`);
|
|
1983
|
+
}
|
|
1984
|
+
flags[flagBody] = flagValue;
|
|
1985
|
+
remaining = restTokens.join(" ");
|
|
1986
|
+
}
|
|
1987
|
+
return { flags, remaining };
|
|
1988
|
+
}
|
|
1989
|
+
function parseFromValue(value, lineNumber) {
|
|
1990
|
+
const { remaining } = stripLeadingFlags(value);
|
|
1991
|
+
const tokens = shellSplit(remaining);
|
|
1992
|
+
if (tokens.length === 0) {
|
|
1993
|
+
throw new Error(`line ${lineNumber}: FROM must include a base image`);
|
|
1994
|
+
}
|
|
1995
|
+
if (tokens.length > 1 && tokens[1].toLowerCase() !== "as") {
|
|
1996
|
+
throw new Error(`line ${lineNumber}: unsupported FROM syntax '${value}'`);
|
|
1997
|
+
}
|
|
1998
|
+
return tokens[0];
|
|
1999
|
+
}
|
|
2000
|
+
function parseCopyLikeValues(value, lineNumber, keyword) {
|
|
2001
|
+
const { flags, remaining } = stripLeadingFlags(value);
|
|
2002
|
+
if ("from" in flags) {
|
|
2003
|
+
throw new Error(
|
|
2004
|
+
`line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
const payload = remaining.trim();
|
|
2008
|
+
if (!payload) {
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
`line ${lineNumber}: ${keyword} must include source and destination`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
let parts;
|
|
2014
|
+
if (payload.startsWith("[")) {
|
|
2015
|
+
let parsed;
|
|
2016
|
+
try {
|
|
2017
|
+
parsed = JSON.parse(payload);
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
throw new Error(
|
|
2020
|
+
`line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
|
|
2024
|
+
throw new Error(
|
|
2025
|
+
`line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
parts = parsed;
|
|
2029
|
+
} else {
|
|
2030
|
+
parts = shellSplit(payload);
|
|
2031
|
+
if (parts.length < 2) {
|
|
2032
|
+
throw new Error(
|
|
2033
|
+
`line ${lineNumber}: ${keyword} must include at least one source and one destination`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
flags,
|
|
2039
|
+
sources: parts.slice(0, -1),
|
|
2040
|
+
destination: parts[parts.length - 1]
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function parseEnvPairs(value, lineNumber) {
|
|
2044
|
+
const tokens = shellSplit(value);
|
|
2045
|
+
if (tokens.length === 0) {
|
|
2046
|
+
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
2047
|
+
}
|
|
2048
|
+
if (tokens.every((token) => token.includes("="))) {
|
|
2049
|
+
return tokens.map((token) => {
|
|
2050
|
+
const [key, envValue] = token.split(/=(.*)/s, 2);
|
|
2051
|
+
if (!key) {
|
|
2052
|
+
throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
|
|
2053
|
+
}
|
|
2054
|
+
return [key, envValue];
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
if (tokens.length < 2) {
|
|
2058
|
+
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
2059
|
+
}
|
|
2060
|
+
return [[tokens[0], tokens.slice(1).join(" ")]];
|
|
2061
|
+
}
|
|
2062
|
+
function resolveContainerPath(containerPath, workingDir) {
|
|
2063
|
+
if (!containerPath) {
|
|
2064
|
+
return workingDir;
|
|
2065
|
+
}
|
|
2066
|
+
const normalized = containerPath.startsWith("/") ? path.posix.normalize(containerPath) : path.posix.normalize(path.posix.join(workingDir, containerPath));
|
|
2067
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2068
|
+
}
|
|
2069
|
+
function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
|
|
2070
|
+
let baseImage;
|
|
2071
|
+
const instructions = [];
|
|
2072
|
+
for (const logicalLine of logicalDockerfileLines(dockerfileText)) {
|
|
2073
|
+
const { keyword, value } = splitInstruction(
|
|
2074
|
+
logicalLine.line,
|
|
2075
|
+
logicalLine.lineNumber
|
|
2076
|
+
);
|
|
2077
|
+
if (keyword === "FROM") {
|
|
2078
|
+
if (baseImage != null) {
|
|
2079
|
+
throw new Error(
|
|
2080
|
+
`line ${logicalLine.lineNumber}: multi-stage Dockerfiles are not supported for sandbox image creation`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
baseImage = parseFromValue(value, logicalLine.lineNumber);
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
if (UNSUPPORTED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
2087
|
+
throw new Error(
|
|
2088
|
+
`line ${logicalLine.lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
instructions.push({
|
|
2092
|
+
keyword,
|
|
2093
|
+
value,
|
|
2094
|
+
lineNumber: logicalLine.lineNumber
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
if (!baseImage) {
|
|
2098
|
+
throw new Error("Dockerfile must contain a FROM instruction");
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
dockerfilePath,
|
|
2102
|
+
contextDir,
|
|
2103
|
+
registeredName: registeredName ?? defaultRegisteredName(dockerfilePath),
|
|
2104
|
+
dockerfileText,
|
|
2105
|
+
baseImage,
|
|
2106
|
+
instructions
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
async function loadDockerfilePlan(dockerfilePath, registeredName) {
|
|
2110
|
+
const resolvedPath = path.resolve(dockerfilePath);
|
|
2111
|
+
const fileStats = await stat(resolvedPath).catch(() => null);
|
|
2112
|
+
if (!fileStats?.isFile()) {
|
|
2113
|
+
throw new Error(`Dockerfile not found: ${dockerfilePath}`);
|
|
2114
|
+
}
|
|
2115
|
+
const dockerfileText = await readFile(resolvedPath, "utf8");
|
|
2116
|
+
return buildPlanFromDockerfileText(
|
|
2117
|
+
dockerfileText,
|
|
2118
|
+
resolvedPath,
|
|
2119
|
+
path.dirname(resolvedPath),
|
|
2120
|
+
registeredName
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
function loadImagePlan(image, options = {}) {
|
|
2124
|
+
const contextDir = path.resolve(options.contextDir ?? process.cwd());
|
|
2125
|
+
const dockerfileText = dockerfileContent(image);
|
|
2126
|
+
const logicalLines = logicalDockerfileLines(dockerfileText);
|
|
2127
|
+
const instructions = image.baseImage == null ? logicalLines : logicalLines.slice(1);
|
|
2128
|
+
return {
|
|
2129
|
+
dockerfilePath: path.join(contextDir, "Dockerfile"),
|
|
2130
|
+
contextDir,
|
|
2131
|
+
registeredName: options.registeredName ?? image.name,
|
|
2132
|
+
dockerfileText,
|
|
2133
|
+
baseImage: image.baseImage ?? void 0,
|
|
2134
|
+
instructions: instructions.map(({ line, lineNumber }) => {
|
|
2135
|
+
const parsed = splitInstruction(line, lineNumber);
|
|
2136
|
+
return {
|
|
2137
|
+
keyword: parsed.keyword,
|
|
2138
|
+
value: parsed.value,
|
|
2139
|
+
lineNumber
|
|
2140
|
+
};
|
|
2141
|
+
})
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function defaultEmit(event) {
|
|
2145
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
2146
|
+
`);
|
|
2147
|
+
}
|
|
2148
|
+
function debugEnabled() {
|
|
2149
|
+
return ["1", "true", "yes", "on"].includes(
|
|
2150
|
+
(process.env.TENSORLAKE_DEBUG ?? "").toLowerCase()
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
function buildContextFromEnv() {
|
|
2154
|
+
return {
|
|
2155
|
+
apiUrl: process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai",
|
|
2156
|
+
apiKey: process.env.TENSORLAKE_API_KEY,
|
|
2157
|
+
personalAccessToken: process.env.TENSORLAKE_PAT,
|
|
2158
|
+
namespace: process.env.INDEXIFY_NAMESPACE ?? "default",
|
|
2159
|
+
organizationId: process.env.TENSORLAKE_ORGANIZATION_ID,
|
|
2160
|
+
projectId: process.env.TENSORLAKE_PROJECT_ID,
|
|
2161
|
+
debug: debugEnabled()
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function createDefaultClient(context) {
|
|
2165
|
+
return new SandboxClient({
|
|
2166
|
+
apiUrl: context.apiUrl,
|
|
2167
|
+
apiKey: context.apiKey ?? context.personalAccessToken,
|
|
2168
|
+
organizationId: context.organizationId,
|
|
2169
|
+
projectId: context.projectId,
|
|
2170
|
+
namespace: context.namespace
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
async function runChecked(sandbox, command, args, env, workingDir) {
|
|
2174
|
+
const result = await sandbox.run(command, {
|
|
2175
|
+
args,
|
|
2176
|
+
env,
|
|
2177
|
+
workingDir
|
|
2178
|
+
});
|
|
2179
|
+
if (result.exitCode !== 0) {
|
|
2180
|
+
throw new Error(
|
|
2181
|
+
`Command '${command} ${args.join(" ")}' failed with exit code ${result.exitCode}`
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
return result;
|
|
2185
|
+
}
|
|
2186
|
+
async function runStreaming(sandbox, emit, sleep4, command, args = [], env, workingDir) {
|
|
2187
|
+
const proc = await sandbox.startProcess(command, {
|
|
2188
|
+
args,
|
|
2189
|
+
env,
|
|
2190
|
+
workingDir
|
|
2191
|
+
});
|
|
2192
|
+
let stdoutSeen = 0;
|
|
2193
|
+
let stderrSeen = 0;
|
|
2194
|
+
let info;
|
|
2195
|
+
while (true) {
|
|
2196
|
+
const stdoutResp = await sandbox.getStdout(proc.pid);
|
|
2197
|
+
emitOutputLines(emit, "stdout", stdoutResp, stdoutSeen);
|
|
2198
|
+
stdoutSeen = stdoutResp.lines.length;
|
|
2199
|
+
const stderrResp = await sandbox.getStderr(proc.pid);
|
|
2200
|
+
emitOutputLines(emit, "stderr", stderrResp, stderrSeen);
|
|
2201
|
+
stderrSeen = stderrResp.lines.length;
|
|
2202
|
+
info = await sandbox.getProcess(proc.pid);
|
|
2203
|
+
if (info.status !== "running" /* RUNNING */) {
|
|
2204
|
+
const finalStdout = await sandbox.getStdout(proc.pid);
|
|
2205
|
+
emitOutputLines(emit, "stdout", finalStdout, stdoutSeen);
|
|
2206
|
+
stdoutSeen = finalStdout.lines.length;
|
|
2207
|
+
const finalStderr = await sandbox.getStderr(proc.pid);
|
|
2208
|
+
emitOutputLines(emit, "stderr", finalStderr, stderrSeen);
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
await sleep4(300);
|
|
2212
|
+
}
|
|
2213
|
+
for (let i = 0; i < 10; i++) {
|
|
2214
|
+
if (info.exitCode != null || info.signal != null) {
|
|
2215
|
+
break;
|
|
2216
|
+
}
|
|
2217
|
+
await sleep4(200);
|
|
2218
|
+
info = await sandbox.getProcess(proc.pid);
|
|
2219
|
+
}
|
|
2220
|
+
const exitCode = info.exitCode != null ? info.exitCode : info.signal != null ? -info.signal : 0;
|
|
2221
|
+
if (exitCode !== 0) {
|
|
2222
|
+
throw new Error(
|
|
2223
|
+
`Command '${command} ${args.join(" ")}' failed with exit code ${exitCode}`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
function emitOutputLines(emit, stream, response, seen) {
|
|
2228
|
+
for (const line of response.lines.slice(seen)) {
|
|
2229
|
+
emit({ type: "build_log", stream, message: line });
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
function isPathWithinContext(contextDir, localPath) {
|
|
2233
|
+
const relative = path.relative(contextDir, localPath);
|
|
2234
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
2235
|
+
}
|
|
2236
|
+
function resolveContextSourcePath(contextDir, source) {
|
|
2237
|
+
const resolvedContextDir = path.resolve(contextDir);
|
|
2238
|
+
const resolvedSource = path.resolve(resolvedContextDir, source);
|
|
2239
|
+
if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
|
|
2240
|
+
throw new Error(`Local path escapes the build context: ${source}`);
|
|
2241
|
+
}
|
|
2242
|
+
return resolvedSource;
|
|
2243
|
+
}
|
|
2244
|
+
async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
2245
|
+
const fileStats = await stat(localPath).catch(() => null);
|
|
2246
|
+
if (!fileStats) {
|
|
2247
|
+
throw new Error(`Local path not found: ${localPath}`);
|
|
2248
|
+
}
|
|
2249
|
+
if (fileStats.isFile()) {
|
|
2250
|
+
await runChecked(sandbox, "mkdir", ["-p", path.posix.dirname(remotePath)]);
|
|
2251
|
+
await sandbox.writeFile(remotePath, await readFile(localPath));
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
if (!fileStats.isDirectory()) {
|
|
2255
|
+
throw new Error(`Local path not found: ${localPath}`);
|
|
2256
|
+
}
|
|
2257
|
+
const entries = await readdir(localPath, { withFileTypes: true });
|
|
2258
|
+
for (const entry of entries) {
|
|
2259
|
+
const sourcePath = path.join(localPath, entry.name);
|
|
2260
|
+
const destinationPath = path.posix.join(remotePath, entry.name);
|
|
2261
|
+
if (entry.isDirectory()) {
|
|
2262
|
+
await runChecked(sandbox, "mkdir", ["-p", destinationPath]);
|
|
2263
|
+
await copyLocalPathToSandbox(sandbox, sourcePath, destinationPath);
|
|
2264
|
+
} else if (entry.isFile()) {
|
|
2265
|
+
await runChecked(
|
|
2266
|
+
sandbox,
|
|
2267
|
+
"mkdir",
|
|
2268
|
+
["-p", path.posix.dirname(destinationPath)]
|
|
2269
|
+
);
|
|
2270
|
+
await sandbox.writeFile(destinationPath, await readFile(sourcePath));
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
async function persistEnvVar(sandbox, processEnv, key, value) {
|
|
2275
|
+
const exportLine = `export ${key}=${shellQuote(value)}`;
|
|
2276
|
+
await runChecked(
|
|
2277
|
+
sandbox,
|
|
2278
|
+
"sh",
|
|
2279
|
+
["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
|
|
2280
|
+
processEnv
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
|
|
2284
|
+
const destinationPath = resolveContainerPath(destination, workingDir);
|
|
2285
|
+
if (sources.length > 1 && !destinationPath.endsWith("/")) {
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
`${keyword} with multiple sources requires a directory destination ending in '/'`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
for (const source of sources) {
|
|
2291
|
+
const localSource = resolveContextSourcePath(contextDir, source);
|
|
2292
|
+
const localStats = await stat(localSource).catch(() => null);
|
|
2293
|
+
if (!localStats) {
|
|
2294
|
+
throw new Error(`Local path not found: ${localSource}`);
|
|
2295
|
+
}
|
|
2296
|
+
let remoteDestination = destinationPath;
|
|
2297
|
+
if (sources.length > 1) {
|
|
2298
|
+
remoteDestination = path.posix.join(
|
|
2299
|
+
destinationPath.replace(/\/$/, ""),
|
|
2300
|
+
path.posix.basename(source.replace(/\/$/, ""))
|
|
2301
|
+
);
|
|
2302
|
+
} else if (localStats.isFile() && destinationPath.endsWith("/")) {
|
|
2303
|
+
remoteDestination = path.posix.join(
|
|
2304
|
+
destinationPath.replace(/\/$/, ""),
|
|
2305
|
+
path.basename(source)
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
emit({
|
|
2309
|
+
type: "status",
|
|
2310
|
+
message: `${keyword} ${source} -> ${remoteDestination}`
|
|
2311
|
+
});
|
|
2312
|
+
await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep4) {
|
|
2316
|
+
let destinationPath = resolveContainerPath(destination, workingDir);
|
|
2317
|
+
const parsedUrl = new URL(url);
|
|
2318
|
+
const fileName = path.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
|
|
2319
|
+
if (destinationPath.endsWith("/")) {
|
|
2320
|
+
destinationPath = path.posix.join(destinationPath.replace(/\/$/, ""), fileName);
|
|
2321
|
+
}
|
|
2322
|
+
const parentDir = path.posix.dirname(destinationPath) || "/";
|
|
2323
|
+
emit({
|
|
2324
|
+
type: "status",
|
|
2325
|
+
message: `ADD ${url} -> ${destinationPath}`
|
|
2326
|
+
});
|
|
2327
|
+
await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
|
|
2328
|
+
await runStreaming(
|
|
2329
|
+
sandbox,
|
|
2330
|
+
emit,
|
|
2331
|
+
sleep4,
|
|
2332
|
+
"sh",
|
|
2333
|
+
[
|
|
2334
|
+
"-c",
|
|
2335
|
+
`curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
|
|
2336
|
+
],
|
|
2337
|
+
processEnv,
|
|
2338
|
+
workingDir
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
async function executeDockerfilePlan(sandbox, plan, emit, sleep4) {
|
|
2342
|
+
const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
|
|
2343
|
+
let workingDir = "/";
|
|
2344
|
+
for (const instruction of plan.instructions) {
|
|
2345
|
+
const { keyword, value, lineNumber } = instruction;
|
|
2346
|
+
if (keyword === "RUN") {
|
|
2347
|
+
emit({ type: "status", message: `RUN ${value}` });
|
|
2348
|
+
await runStreaming(
|
|
2349
|
+
sandbox,
|
|
2350
|
+
emit,
|
|
2351
|
+
sleep4,
|
|
2352
|
+
"sh",
|
|
2353
|
+
["-c", value],
|
|
2354
|
+
processEnv,
|
|
2355
|
+
workingDir
|
|
2356
|
+
);
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
if (keyword === "WORKDIR") {
|
|
2360
|
+
const tokens = shellSplit(value);
|
|
2361
|
+
if (tokens.length !== 1) {
|
|
2362
|
+
throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
|
|
2363
|
+
}
|
|
2364
|
+
workingDir = resolveContainerPath(tokens[0], workingDir);
|
|
2365
|
+
emit({ type: "status", message: `WORKDIR ${workingDir}` });
|
|
2366
|
+
await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
if (keyword === "ENV") {
|
|
2370
|
+
for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
|
|
2371
|
+
emit({ type: "status", message: `ENV ${key}=${envValue}` });
|
|
2372
|
+
processEnv[key] = envValue;
|
|
2373
|
+
await persistEnvVar(sandbox, processEnv, key, envValue);
|
|
2374
|
+
}
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
if (keyword === "COPY") {
|
|
2378
|
+
const { sources, destination } = parseCopyLikeValues(
|
|
2379
|
+
value,
|
|
2380
|
+
lineNumber,
|
|
2381
|
+
keyword
|
|
2382
|
+
);
|
|
2383
|
+
await copyFromContext(
|
|
2384
|
+
sandbox,
|
|
2385
|
+
emit,
|
|
2386
|
+
plan.contextDir,
|
|
2387
|
+
sources,
|
|
2388
|
+
destination,
|
|
2389
|
+
workingDir,
|
|
2390
|
+
keyword
|
|
2391
|
+
);
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
if (keyword === "ADD") {
|
|
2395
|
+
const { sources, destination } = parseCopyLikeValues(
|
|
2396
|
+
value,
|
|
2397
|
+
lineNumber,
|
|
2398
|
+
keyword
|
|
2399
|
+
);
|
|
2400
|
+
if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
|
|
2401
|
+
await addUrlToSandbox(
|
|
2402
|
+
sandbox,
|
|
2403
|
+
emit,
|
|
2404
|
+
sources[0],
|
|
2405
|
+
destination,
|
|
2406
|
+
workingDir,
|
|
2407
|
+
processEnv,
|
|
2408
|
+
sleep4
|
|
2409
|
+
);
|
|
2410
|
+
} else {
|
|
2411
|
+
await copyFromContext(
|
|
2412
|
+
sandbox,
|
|
2413
|
+
emit,
|
|
2414
|
+
plan.contextDir,
|
|
2415
|
+
sources,
|
|
2416
|
+
destination,
|
|
2417
|
+
workingDir,
|
|
2418
|
+
keyword
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
2424
|
+
emit({
|
|
2425
|
+
type: "warning",
|
|
2426
|
+
message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
|
|
2427
|
+
});
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
throw new Error(
|
|
2431
|
+
`line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
async function registerImage(context, name, dockerfile, snapshotId, snapshotUri, isPublic) {
|
|
2436
|
+
if (!context.organizationId || !context.projectId) {
|
|
2437
|
+
throw new Error(
|
|
2438
|
+
"Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
2442
|
+
if (!bearerToken) {
|
|
2443
|
+
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
2444
|
+
}
|
|
2445
|
+
const baseUrl = context.apiUrl.replace(/\/+$/, "");
|
|
2446
|
+
const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
2447
|
+
const headers = {
|
|
2448
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
2449
|
+
"Content-Type": "application/json"
|
|
2450
|
+
};
|
|
2451
|
+
if (context.personalAccessToken && !context.apiKey) {
|
|
2452
|
+
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
2453
|
+
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
2454
|
+
}
|
|
2455
|
+
const response = await fetch(url, {
|
|
2456
|
+
method: "POST",
|
|
2457
|
+
headers,
|
|
2458
|
+
body: JSON.stringify({
|
|
2459
|
+
name,
|
|
2460
|
+
dockerfile,
|
|
2461
|
+
snapshotId,
|
|
2462
|
+
snapshotUri,
|
|
2463
|
+
isPublic
|
|
2464
|
+
})
|
|
2465
|
+
});
|
|
2466
|
+
if (!response.ok) {
|
|
2467
|
+
throw new Error(
|
|
2468
|
+
`${response.status} ${response.statusText}: ${await response.text()}`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
const text = await response.text();
|
|
2472
|
+
return text ? JSON.parse(text) : {};
|
|
2473
|
+
}
|
|
2474
|
+
async function createSandboxImage(source, options = {}, deps = {}) {
|
|
2475
|
+
const emit = deps.emit ?? defaultEmit;
|
|
2476
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2477
|
+
const context = buildContextFromEnv();
|
|
2478
|
+
const clientFactory = deps.createClient ?? createDefaultClient;
|
|
2479
|
+
const register = deps.registerImage ?? ((...args) => registerImage(...args));
|
|
2480
|
+
const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
|
|
2481
|
+
emit({ type: "status", message: `Loading ${sourceLabel}...` });
|
|
2482
|
+
const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
|
|
2483
|
+
emit({
|
|
2484
|
+
type: "status",
|
|
2485
|
+
message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
|
|
2486
|
+
});
|
|
2487
|
+
const client = clientFactory(context);
|
|
2488
|
+
let sandbox;
|
|
2489
|
+
try {
|
|
2490
|
+
sandbox = await client.createAndConnect({
|
|
2491
|
+
...plan.baseImage == null ? {} : { image: plan.baseImage },
|
|
2492
|
+
cpus: options.cpus ?? 2,
|
|
2493
|
+
memoryMb: options.memoryMb ?? 4096
|
|
2494
|
+
});
|
|
2495
|
+
emit({
|
|
2496
|
+
type: "status",
|
|
2497
|
+
message: `Materializing image in sandbox ${sandbox.sandboxId}...`
|
|
2498
|
+
});
|
|
2499
|
+
await executeDockerfilePlan(sandbox, plan, emit, sleep4);
|
|
2500
|
+
emit({ type: "status", message: "Creating snapshot..." });
|
|
2501
|
+
const snapshot = await client.snapshotAndWait(sandbox.sandboxId);
|
|
2502
|
+
emit({
|
|
2503
|
+
type: "snapshot_created",
|
|
2504
|
+
snapshot_id: snapshot.snapshotId,
|
|
2505
|
+
snapshot_uri: snapshot.snapshotUri ?? null
|
|
2506
|
+
});
|
|
2507
|
+
if (!snapshot.snapshotUri) {
|
|
2508
|
+
throw new Error(
|
|
2509
|
+
`Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
emit({
|
|
2513
|
+
type: "status",
|
|
2514
|
+
message: `Registering image '${plan.registeredName}'...`
|
|
2515
|
+
});
|
|
2516
|
+
const result = await register(
|
|
2517
|
+
context,
|
|
2518
|
+
plan.registeredName,
|
|
2519
|
+
plan.dockerfileText,
|
|
2520
|
+
snapshot.snapshotId,
|
|
2521
|
+
snapshot.snapshotUri,
|
|
2522
|
+
options.isPublic ?? false
|
|
2523
|
+
);
|
|
2524
|
+
emit({
|
|
2525
|
+
type: "image_registered",
|
|
2526
|
+
name: plan.registeredName,
|
|
2527
|
+
image_id: typeof result.id === "string" && result.id || typeof result.templateId === "string" && result.templateId || ""
|
|
2528
|
+
});
|
|
2529
|
+
emit({ type: "done" });
|
|
2530
|
+
return result;
|
|
2531
|
+
} finally {
|
|
2532
|
+
if (sandbox) {
|
|
2533
|
+
try {
|
|
2534
|
+
await sandbox.terminate();
|
|
2535
|
+
} catch {
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
client.close();
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
1394
2541
|
export {
|
|
1395
2542
|
APIClient,
|
|
1396
2543
|
CloudClient,
|
|
1397
2544
|
ContainerState,
|
|
2545
|
+
Image,
|
|
2546
|
+
ImageBuildOperationType,
|
|
1398
2547
|
OutputMode,
|
|
1399
2548
|
PoolInUseError,
|
|
1400
2549
|
PoolNotFoundError,
|
|
1401
2550
|
ProcessStatus,
|
|
2551
|
+
Pty,
|
|
1402
2552
|
RemoteAPIError,
|
|
1403
2553
|
RequestExecutionError,
|
|
1404
2554
|
RequestFailedError,
|
|
@@ -1411,6 +2561,8 @@ export {
|
|
|
1411
2561
|
SandboxNotFoundError,
|
|
1412
2562
|
SandboxStatus,
|
|
1413
2563
|
SnapshotStatus,
|
|
1414
|
-
StdinMode
|
|
2564
|
+
StdinMode,
|
|
2565
|
+
createSandboxImage,
|
|
2566
|
+
dockerfileContent
|
|
1415
2567
|
};
|
|
1416
2568
|
//# sourceMappingURL=index.js.map
|