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