@cloudflare/sandbox 0.6.6 → 0.6.7
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/README.md +2 -1
- package/dist/{dist-2SF6oOaz.js → dist-Dgwt0coR.js} +35 -8
- package/dist/dist-Dgwt0coR.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +704 -153
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/openai/index.js +1 -1
- package/dist/opencode/index.d.ts +1 -1
- package/dist/opencode/index.d.ts.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-09Ce7yli.d.ts → sandbox-uifih-hT.d.ts} +137 -35
- package/dist/sandbox-uifih-hT.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/dist-2SF6oOaz.js.map +0 -1
- package/dist/sandbox-09Ce7yli.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as isExecResult, c as shellEscape, d as TraceContext, f as Execution, h as getEnvString, i as isWSStreamChunk, l as createLogger, m as GitLogger, n as isWSError, o as isProcess, p as ResultImpl, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createNoOpLogger } from "./dist-Dgwt0coR.js";
|
|
2
2
|
import { t as ErrorCode } from "./errors-BCXUmJUn.js";
|
|
3
3
|
import { Container, getContainer, switchPort } from "@cloudflare/containers";
|
|
4
4
|
|
|
@@ -569,31 +569,37 @@ function createErrorFromResponse(errorResponse) {
|
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
//#endregion
|
|
572
|
-
//#region src/clients/base-
|
|
572
|
+
//#region src/clients/transport/base-transport.ts
|
|
573
|
+
/**
|
|
574
|
+
* Container startup retry configuration
|
|
575
|
+
*/
|
|
573
576
|
const TIMEOUT_MS = 12e4;
|
|
574
577
|
const MIN_TIME_FOR_RETRY_MS = 15e3;
|
|
575
578
|
/**
|
|
576
|
-
* Abstract base
|
|
579
|
+
* Abstract base transport with shared retry logic
|
|
580
|
+
*
|
|
581
|
+
* Handles 503 retry for container startup - shared by all transports.
|
|
582
|
+
* Subclasses implement the transport-specific fetch and stream logic.
|
|
577
583
|
*/
|
|
578
|
-
var
|
|
579
|
-
|
|
580
|
-
options;
|
|
584
|
+
var BaseTransport = class {
|
|
585
|
+
config;
|
|
581
586
|
logger;
|
|
582
|
-
constructor(
|
|
583
|
-
this.
|
|
584
|
-
this.logger =
|
|
585
|
-
this.baseUrl = this.options.baseUrl;
|
|
587
|
+
constructor(config) {
|
|
588
|
+
this.config = config;
|
|
589
|
+
this.logger = config.logger ?? createNoOpLogger();
|
|
586
590
|
}
|
|
587
591
|
/**
|
|
588
|
-
*
|
|
589
|
-
*
|
|
592
|
+
* Fetch with automatic retry for 503 (container starting)
|
|
593
|
+
*
|
|
594
|
+
* This is the primary entry point for making requests. It wraps the
|
|
595
|
+
* transport-specific doFetch() with retry logic for container startup.
|
|
590
596
|
*/
|
|
591
|
-
async
|
|
597
|
+
async fetch(path, options) {
|
|
592
598
|
const startTime = Date.now();
|
|
593
599
|
let attempt = 0;
|
|
594
600
|
while (true) {
|
|
595
|
-
const response = await this.
|
|
596
|
-
if (
|
|
601
|
+
const response = await this.doFetch(path, options);
|
|
602
|
+
if (response.status === 503) {
|
|
597
603
|
const elapsed = Date.now() - startTime;
|
|
598
604
|
const remaining = TIMEOUT_MS - elapsed;
|
|
599
605
|
if (remaining > MIN_TIME_FOR_RETRY_MS) {
|
|
@@ -602,19 +608,537 @@ var BaseHttpClient = class {
|
|
|
602
608
|
status: response.status,
|
|
603
609
|
attempt: attempt + 1,
|
|
604
610
|
delayMs: delay,
|
|
605
|
-
remainingSec: Math.floor(remaining / 1e3)
|
|
611
|
+
remainingSec: Math.floor(remaining / 1e3),
|
|
612
|
+
mode: this.getMode()
|
|
606
613
|
});
|
|
607
|
-
await
|
|
614
|
+
await this.sleep(delay);
|
|
608
615
|
attempt++;
|
|
609
616
|
continue;
|
|
610
617
|
}
|
|
611
618
|
this.logger.error("Container failed to become ready", /* @__PURE__ */ new Error(`Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1e3)}s`));
|
|
612
|
-
return response;
|
|
613
619
|
}
|
|
614
620
|
return response;
|
|
615
621
|
}
|
|
616
622
|
}
|
|
617
623
|
/**
|
|
624
|
+
* Sleep utility for retry delays
|
|
625
|
+
*/
|
|
626
|
+
sleep(ms) {
|
|
627
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/clients/transport/http-transport.ts
|
|
633
|
+
/**
|
|
634
|
+
* HTTP transport implementation
|
|
635
|
+
*
|
|
636
|
+
* Uses standard fetch API for communication with the container.
|
|
637
|
+
* HTTP is stateless, so connect/disconnect are no-ops.
|
|
638
|
+
*/
|
|
639
|
+
var HttpTransport = class extends BaseTransport {
|
|
640
|
+
baseUrl;
|
|
641
|
+
constructor(config) {
|
|
642
|
+
super(config);
|
|
643
|
+
this.baseUrl = config.baseUrl ?? "http://localhost:3000";
|
|
644
|
+
}
|
|
645
|
+
getMode() {
|
|
646
|
+
return "http";
|
|
647
|
+
}
|
|
648
|
+
async connect() {}
|
|
649
|
+
disconnect() {}
|
|
650
|
+
isConnected() {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
async doFetch(path, options) {
|
|
654
|
+
const url = this.buildUrl(path);
|
|
655
|
+
if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
|
|
656
|
+
return globalThis.fetch(url, options);
|
|
657
|
+
}
|
|
658
|
+
async fetchStream(path, body, method = "POST") {
|
|
659
|
+
const url = this.buildUrl(path);
|
|
660
|
+
const options = this.buildStreamOptions(body, method);
|
|
661
|
+
let response;
|
|
662
|
+
if (this.config.stub) response = await this.config.stub.containerFetch(url, options, this.config.port);
|
|
663
|
+
else response = await globalThis.fetch(url, options);
|
|
664
|
+
if (!response.ok) {
|
|
665
|
+
const errorBody = await response.text();
|
|
666
|
+
throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
|
|
667
|
+
}
|
|
668
|
+
if (!response.body) throw new Error("No response body for streaming");
|
|
669
|
+
return response.body;
|
|
670
|
+
}
|
|
671
|
+
buildUrl(path) {
|
|
672
|
+
if (this.config.stub) return `http://localhost:${this.config.port}${path}`;
|
|
673
|
+
return `${this.baseUrl}${path}`;
|
|
674
|
+
}
|
|
675
|
+
buildStreamOptions(body, method) {
|
|
676
|
+
return {
|
|
677
|
+
method,
|
|
678
|
+
headers: body && method === "POST" ? { "Content-Type": "application/json" } : void 0,
|
|
679
|
+
body: body && method === "POST" ? JSON.stringify(body) : void 0
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/clients/transport/ws-transport.ts
|
|
686
|
+
/**
|
|
687
|
+
* WebSocket transport implementation
|
|
688
|
+
*
|
|
689
|
+
* Multiplexes HTTP-like requests over a single WebSocket connection.
|
|
690
|
+
* Useful when running inside Workers/DO where sub-request limits apply.
|
|
691
|
+
*/
|
|
692
|
+
var WebSocketTransport = class extends BaseTransport {
|
|
693
|
+
ws = null;
|
|
694
|
+
state = "disconnected";
|
|
695
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
696
|
+
connectPromise = null;
|
|
697
|
+
boundHandleMessage;
|
|
698
|
+
boundHandleClose;
|
|
699
|
+
constructor(config) {
|
|
700
|
+
super(config);
|
|
701
|
+
if (!config.wsUrl) throw new Error("wsUrl is required for WebSocket transport");
|
|
702
|
+
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
703
|
+
this.boundHandleClose = this.handleClose.bind(this);
|
|
704
|
+
}
|
|
705
|
+
getMode() {
|
|
706
|
+
return "websocket";
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Check if WebSocket is connected
|
|
710
|
+
*/
|
|
711
|
+
isConnected() {
|
|
712
|
+
return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Connect to the WebSocket server
|
|
716
|
+
*
|
|
717
|
+
* The connection promise is assigned synchronously so concurrent
|
|
718
|
+
* callers share the same connection attempt.
|
|
719
|
+
*/
|
|
720
|
+
async connect() {
|
|
721
|
+
if (this.isConnected()) return;
|
|
722
|
+
if (this.connectPromise) return this.connectPromise;
|
|
723
|
+
this.connectPromise = this.doConnect();
|
|
724
|
+
try {
|
|
725
|
+
await this.connectPromise;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
this.connectPromise = null;
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Disconnect from the WebSocket server
|
|
733
|
+
*/
|
|
734
|
+
disconnect() {
|
|
735
|
+
this.cleanup();
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Transport-specific fetch implementation
|
|
739
|
+
* Converts WebSocket response to standard Response object.
|
|
740
|
+
*/
|
|
741
|
+
async doFetch(path, options) {
|
|
742
|
+
await this.connect();
|
|
743
|
+
const method = options?.method || "GET";
|
|
744
|
+
const body = this.parseBody(options?.body);
|
|
745
|
+
const result = await this.request(method, path, body);
|
|
746
|
+
return new Response(JSON.stringify(result.body), {
|
|
747
|
+
status: result.status,
|
|
748
|
+
headers: { "Content-Type": "application/json" }
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Streaming fetch implementation
|
|
753
|
+
*/
|
|
754
|
+
async fetchStream(path, body, method = "POST") {
|
|
755
|
+
return this.requestStream(method, path, body);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Parse request body from RequestInit
|
|
759
|
+
*/
|
|
760
|
+
parseBody(body) {
|
|
761
|
+
if (!body) return;
|
|
762
|
+
if (typeof body === "string") try {
|
|
763
|
+
return JSON.parse(body);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw new Error(`Request body must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
766
|
+
}
|
|
767
|
+
throw new Error(`WebSocket transport only supports string bodies. Got: ${typeof body}`);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Internal connection logic
|
|
771
|
+
*/
|
|
772
|
+
async doConnect() {
|
|
773
|
+
this.state = "connecting";
|
|
774
|
+
if (this.config.stub) await this.connectViaFetch();
|
|
775
|
+
else await this.connectViaWebSocket();
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Connect using fetch-based WebSocket (Cloudflare Workers style)
|
|
779
|
+
* This is required when running inside a Durable Object.
|
|
780
|
+
*
|
|
781
|
+
* Uses stub.fetch() which routes WebSocket upgrade requests through the
|
|
782
|
+
* parent Container class that supports the WebSocket protocol.
|
|
783
|
+
*/
|
|
784
|
+
async connectViaFetch() {
|
|
785
|
+
const timeoutMs = this.config.connectTimeoutMs ?? 3e4;
|
|
786
|
+
const controller = new AbortController();
|
|
787
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
788
|
+
try {
|
|
789
|
+
const wsPath = new URL(this.config.wsUrl).pathname;
|
|
790
|
+
const httpUrl = `http://localhost:${this.config.port || 3e3}${wsPath}`;
|
|
791
|
+
const request = new Request(httpUrl, {
|
|
792
|
+
headers: {
|
|
793
|
+
Upgrade: "websocket",
|
|
794
|
+
Connection: "Upgrade"
|
|
795
|
+
},
|
|
796
|
+
signal: controller.signal
|
|
797
|
+
});
|
|
798
|
+
const response = await this.config.stub.fetch(request);
|
|
799
|
+
clearTimeout(timeout);
|
|
800
|
+
if (response.status !== 101) throw new Error(`WebSocket upgrade failed: ${response.status} ${response.statusText}`);
|
|
801
|
+
const ws = response.webSocket;
|
|
802
|
+
if (!ws) throw new Error("No WebSocket in upgrade response");
|
|
803
|
+
ws.accept();
|
|
804
|
+
this.ws = ws;
|
|
805
|
+
this.state = "connected";
|
|
806
|
+
this.ws.addEventListener("close", this.boundHandleClose);
|
|
807
|
+
this.ws.addEventListener("message", this.boundHandleMessage);
|
|
808
|
+
this.logger.debug("WebSocket connected via fetch", { url: this.config.wsUrl });
|
|
809
|
+
} catch (error) {
|
|
810
|
+
clearTimeout(timeout);
|
|
811
|
+
this.state = "error";
|
|
812
|
+
this.logger.error("WebSocket fetch connection failed", error instanceof Error ? error : new Error(String(error)));
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Connect using standard WebSocket API (browser/Node style)
|
|
818
|
+
*/
|
|
819
|
+
connectViaWebSocket() {
|
|
820
|
+
return new Promise((resolve, reject) => {
|
|
821
|
+
const timeoutMs = this.config.connectTimeoutMs ?? 3e4;
|
|
822
|
+
const timeout = setTimeout(() => {
|
|
823
|
+
this.cleanup();
|
|
824
|
+
reject(/* @__PURE__ */ new Error(`WebSocket connection timeout after ${timeoutMs}ms`));
|
|
825
|
+
}, timeoutMs);
|
|
826
|
+
try {
|
|
827
|
+
this.ws = new WebSocket(this.config.wsUrl);
|
|
828
|
+
const onOpen = () => {
|
|
829
|
+
clearTimeout(timeout);
|
|
830
|
+
this.ws?.removeEventListener("open", onOpen);
|
|
831
|
+
this.ws?.removeEventListener("error", onConnectError);
|
|
832
|
+
this.state = "connected";
|
|
833
|
+
this.logger.debug("WebSocket connected", { url: this.config.wsUrl });
|
|
834
|
+
resolve();
|
|
835
|
+
};
|
|
836
|
+
const onConnectError = () => {
|
|
837
|
+
clearTimeout(timeout);
|
|
838
|
+
this.ws?.removeEventListener("open", onOpen);
|
|
839
|
+
this.ws?.removeEventListener("error", onConnectError);
|
|
840
|
+
this.state = "error";
|
|
841
|
+
this.logger.error("WebSocket error", /* @__PURE__ */ new Error("WebSocket connection failed"));
|
|
842
|
+
reject(/* @__PURE__ */ new Error("WebSocket connection failed"));
|
|
843
|
+
};
|
|
844
|
+
this.ws.addEventListener("open", onOpen);
|
|
845
|
+
this.ws.addEventListener("error", onConnectError);
|
|
846
|
+
this.ws.addEventListener("close", this.boundHandleClose);
|
|
847
|
+
this.ws.addEventListener("message", this.boundHandleMessage);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
clearTimeout(timeout);
|
|
850
|
+
this.state = "error";
|
|
851
|
+
reject(error);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Send a request and wait for response
|
|
857
|
+
*/
|
|
858
|
+
async request(method, path, body) {
|
|
859
|
+
await this.connect();
|
|
860
|
+
const id = generateRequestId();
|
|
861
|
+
const request = {
|
|
862
|
+
type: "request",
|
|
863
|
+
id,
|
|
864
|
+
method,
|
|
865
|
+
path,
|
|
866
|
+
body
|
|
867
|
+
};
|
|
868
|
+
return new Promise((resolve, reject) => {
|
|
869
|
+
const timeoutMs = this.config.requestTimeoutMs ?? 12e4;
|
|
870
|
+
const timeoutId = setTimeout(() => {
|
|
871
|
+
this.pendingRequests.delete(id);
|
|
872
|
+
reject(/* @__PURE__ */ new Error(`Request timeout after ${timeoutMs}ms: ${method} ${path}`));
|
|
873
|
+
}, timeoutMs);
|
|
874
|
+
this.pendingRequests.set(id, {
|
|
875
|
+
resolve: (response) => {
|
|
876
|
+
clearTimeout(timeoutId);
|
|
877
|
+
this.pendingRequests.delete(id);
|
|
878
|
+
resolve({
|
|
879
|
+
status: response.status,
|
|
880
|
+
body: response.body
|
|
881
|
+
});
|
|
882
|
+
},
|
|
883
|
+
reject: (error) => {
|
|
884
|
+
clearTimeout(timeoutId);
|
|
885
|
+
this.pendingRequests.delete(id);
|
|
886
|
+
reject(error);
|
|
887
|
+
},
|
|
888
|
+
isStreaming: false,
|
|
889
|
+
timeoutId
|
|
890
|
+
});
|
|
891
|
+
try {
|
|
892
|
+
this.send(request);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
clearTimeout(timeoutId);
|
|
895
|
+
this.pendingRequests.delete(id);
|
|
896
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Send a streaming request and return a ReadableStream
|
|
902
|
+
*
|
|
903
|
+
* The stream will receive data chunks as they arrive over the WebSocket.
|
|
904
|
+
* Format matches SSE for compatibility with existing streaming code.
|
|
905
|
+
*/
|
|
906
|
+
async requestStream(method, path, body) {
|
|
907
|
+
await this.connect();
|
|
908
|
+
const id = generateRequestId();
|
|
909
|
+
const request = {
|
|
910
|
+
type: "request",
|
|
911
|
+
id,
|
|
912
|
+
method,
|
|
913
|
+
path,
|
|
914
|
+
body
|
|
915
|
+
};
|
|
916
|
+
return new ReadableStream({
|
|
917
|
+
start: (controller) => {
|
|
918
|
+
const timeoutMs = this.config.requestTimeoutMs ?? 12e4;
|
|
919
|
+
const timeoutId = setTimeout(() => {
|
|
920
|
+
this.pendingRequests.delete(id);
|
|
921
|
+
controller.error(/* @__PURE__ */ new Error(`Stream timeout after ${timeoutMs}ms: ${method} ${path}`));
|
|
922
|
+
}, timeoutMs);
|
|
923
|
+
this.pendingRequests.set(id, {
|
|
924
|
+
resolve: (response) => {
|
|
925
|
+
clearTimeout(timeoutId);
|
|
926
|
+
this.pendingRequests.delete(id);
|
|
927
|
+
if (response.status >= 400) controller.error(/* @__PURE__ */ new Error(`Stream error: ${response.status} - ${JSON.stringify(response.body)}`));
|
|
928
|
+
else controller.close();
|
|
929
|
+
},
|
|
930
|
+
reject: (error) => {
|
|
931
|
+
clearTimeout(timeoutId);
|
|
932
|
+
this.pendingRequests.delete(id);
|
|
933
|
+
controller.error(error);
|
|
934
|
+
},
|
|
935
|
+
streamController: controller,
|
|
936
|
+
isStreaming: true,
|
|
937
|
+
timeoutId
|
|
938
|
+
});
|
|
939
|
+
try {
|
|
940
|
+
this.send(request);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
clearTimeout(timeoutId);
|
|
943
|
+
this.pendingRequests.delete(id);
|
|
944
|
+
controller.error(error instanceof Error ? error : new Error(String(error)));
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
cancel: () => {
|
|
948
|
+
const pending = this.pendingRequests.get(id);
|
|
949
|
+
if (pending?.timeoutId) clearTimeout(pending.timeoutId);
|
|
950
|
+
this.pendingRequests.delete(id);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Send a message over the WebSocket
|
|
956
|
+
*/
|
|
957
|
+
send(message) {
|
|
958
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error("WebSocket not connected");
|
|
959
|
+
this.ws.send(JSON.stringify(message));
|
|
960
|
+
this.logger.debug("WebSocket sent", {
|
|
961
|
+
id: message.id,
|
|
962
|
+
method: message.method,
|
|
963
|
+
path: message.path
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Handle incoming WebSocket messages
|
|
968
|
+
*/
|
|
969
|
+
handleMessage(event) {
|
|
970
|
+
try {
|
|
971
|
+
const message = JSON.parse(event.data);
|
|
972
|
+
if (isWSResponse(message)) this.handleResponse(message);
|
|
973
|
+
else if (isWSStreamChunk(message)) this.handleStreamChunk(message);
|
|
974
|
+
else if (isWSError(message)) this.handleError(message);
|
|
975
|
+
else this.logger.warn("Unknown WebSocket message type", { message });
|
|
976
|
+
} catch (error) {
|
|
977
|
+
this.logger.error("Failed to parse WebSocket message", error instanceof Error ? error : new Error(String(error)));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Handle a response message
|
|
982
|
+
*/
|
|
983
|
+
handleResponse(response) {
|
|
984
|
+
const pending = this.pendingRequests.get(response.id);
|
|
985
|
+
if (!pending) {
|
|
986
|
+
this.logger.warn("Received response for unknown request", { id: response.id });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
this.logger.debug("WebSocket response", {
|
|
990
|
+
id: response.id,
|
|
991
|
+
status: response.status,
|
|
992
|
+
done: response.done
|
|
993
|
+
});
|
|
994
|
+
if (response.done) pending.resolve(response);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Handle a stream chunk message
|
|
998
|
+
*/
|
|
999
|
+
handleStreamChunk(chunk) {
|
|
1000
|
+
const pending = this.pendingRequests.get(chunk.id);
|
|
1001
|
+
if (!pending || !pending.streamController) {
|
|
1002
|
+
this.logger.warn("Received stream chunk for unknown request", { id: chunk.id });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const encoder = new TextEncoder();
|
|
1006
|
+
let sseData;
|
|
1007
|
+
if (chunk.event) sseData = `event: ${chunk.event}\ndata: ${chunk.data}\n\n`;
|
|
1008
|
+
else sseData = `data: ${chunk.data}\n\n`;
|
|
1009
|
+
try {
|
|
1010
|
+
pending.streamController.enqueue(encoder.encode(sseData));
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
this.logger.debug("Failed to enqueue stream chunk, cleaning up", {
|
|
1013
|
+
id: chunk.id,
|
|
1014
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1015
|
+
});
|
|
1016
|
+
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
|
1017
|
+
this.pendingRequests.delete(chunk.id);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Handle an error message
|
|
1022
|
+
*/
|
|
1023
|
+
handleError(error) {
|
|
1024
|
+
if (error.id) {
|
|
1025
|
+
const pending = this.pendingRequests.get(error.id);
|
|
1026
|
+
if (pending) {
|
|
1027
|
+
pending.reject(/* @__PURE__ */ new Error(`${error.code}: ${error.message}`));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
this.logger.error("WebSocket error message", new Error(error.message), {
|
|
1032
|
+
code: error.code,
|
|
1033
|
+
status: error.status
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Handle WebSocket close
|
|
1038
|
+
*/
|
|
1039
|
+
handleClose(event) {
|
|
1040
|
+
this.state = "disconnected";
|
|
1041
|
+
this.ws = null;
|
|
1042
|
+
const closeError = /* @__PURE__ */ new Error(`WebSocket closed: ${event.code} ${event.reason || "No reason"}`);
|
|
1043
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1044
|
+
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
|
1045
|
+
if (pending.streamController) try {
|
|
1046
|
+
pending.streamController.error(closeError);
|
|
1047
|
+
} catch {}
|
|
1048
|
+
pending.reject(closeError);
|
|
1049
|
+
}
|
|
1050
|
+
this.pendingRequests.clear();
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Cleanup resources
|
|
1054
|
+
*/
|
|
1055
|
+
cleanup() {
|
|
1056
|
+
if (this.ws) {
|
|
1057
|
+
this.ws.removeEventListener("close", this.boundHandleClose);
|
|
1058
|
+
this.ws.removeEventListener("message", this.boundHandleMessage);
|
|
1059
|
+
this.ws.close();
|
|
1060
|
+
this.ws = null;
|
|
1061
|
+
}
|
|
1062
|
+
this.state = "disconnected";
|
|
1063
|
+
this.connectPromise = null;
|
|
1064
|
+
for (const pending of this.pendingRequests.values()) if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
|
1065
|
+
this.pendingRequests.clear();
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/clients/transport/factory.ts
|
|
1071
|
+
/**
|
|
1072
|
+
* Create a transport instance based on mode
|
|
1073
|
+
*
|
|
1074
|
+
* This is the primary API for creating transports. It handles
|
|
1075
|
+
* the selection of HTTP or WebSocket transport based on the mode.
|
|
1076
|
+
*
|
|
1077
|
+
* @example
|
|
1078
|
+
* ```typescript
|
|
1079
|
+
* // HTTP transport (default)
|
|
1080
|
+
* const http = createTransport({
|
|
1081
|
+
* mode: 'http',
|
|
1082
|
+
* baseUrl: 'http://localhost:3000'
|
|
1083
|
+
* });
|
|
1084
|
+
*
|
|
1085
|
+
* // WebSocket transport
|
|
1086
|
+
* const ws = createTransport({
|
|
1087
|
+
* mode: 'websocket',
|
|
1088
|
+
* wsUrl: 'ws://localhost:3000/ws'
|
|
1089
|
+
* });
|
|
1090
|
+
* ```
|
|
1091
|
+
*/
|
|
1092
|
+
function createTransport(options) {
|
|
1093
|
+
switch (options.mode) {
|
|
1094
|
+
case "websocket": return new WebSocketTransport(options);
|
|
1095
|
+
default: return new HttpTransport(options);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/clients/base-client.ts
|
|
1101
|
+
/**
|
|
1102
|
+
* Abstract base class providing common HTTP/WebSocket functionality for all domain clients
|
|
1103
|
+
*
|
|
1104
|
+
* All requests go through the Transport abstraction layer, which handles:
|
|
1105
|
+
* - HTTP and WebSocket modes transparently
|
|
1106
|
+
* - Automatic retry for 503 errors (container starting)
|
|
1107
|
+
* - Streaming responses
|
|
1108
|
+
*
|
|
1109
|
+
* WebSocket mode is useful when running inside Workers/Durable Objects
|
|
1110
|
+
* where sub-request limits apply.
|
|
1111
|
+
*/
|
|
1112
|
+
var BaseHttpClient = class {
|
|
1113
|
+
options;
|
|
1114
|
+
logger;
|
|
1115
|
+
transport;
|
|
1116
|
+
constructor(options = {}) {
|
|
1117
|
+
this.options = options;
|
|
1118
|
+
this.logger = options.logger ?? createNoOpLogger();
|
|
1119
|
+
if (options.transport) this.transport = options.transport;
|
|
1120
|
+
else this.transport = createTransport({
|
|
1121
|
+
mode: options.transportMode ?? "http",
|
|
1122
|
+
baseUrl: options.baseUrl ?? "http://localhost:3000",
|
|
1123
|
+
wsUrl: options.wsUrl,
|
|
1124
|
+
logger: this.logger,
|
|
1125
|
+
stub: options.stub,
|
|
1126
|
+
port: options.port
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Check if using WebSocket transport
|
|
1131
|
+
*/
|
|
1132
|
+
isWebSocketMode() {
|
|
1133
|
+
return this.transport.getMode() === "websocket";
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Core fetch method - delegates to Transport which handles retry logic
|
|
1137
|
+
*/
|
|
1138
|
+
async doFetch(path, options) {
|
|
1139
|
+
return this.transport.fetch(path, options);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
618
1142
|
* Make a POST request with JSON body
|
|
619
1143
|
*/
|
|
620
1144
|
async post(endpoint, data, responseHandler) {
|
|
@@ -686,6 +1210,30 @@ var BaseHttpClient = class {
|
|
|
686
1210
|
return response.body;
|
|
687
1211
|
}
|
|
688
1212
|
/**
|
|
1213
|
+
* Stream request handler
|
|
1214
|
+
*
|
|
1215
|
+
* For HTTP mode, uses doFetch + handleStreamResponse to get proper error typing.
|
|
1216
|
+
* For WebSocket mode, uses Transport's streaming support.
|
|
1217
|
+
*
|
|
1218
|
+
* @param path - The API path to call
|
|
1219
|
+
* @param body - Optional request body (for POST requests)
|
|
1220
|
+
* @param method - HTTP method (default: POST, use GET for process logs)
|
|
1221
|
+
*/
|
|
1222
|
+
async doStreamFetch(path, body, method = "POST") {
|
|
1223
|
+
if (this.transport.getMode() === "websocket") try {
|
|
1224
|
+
return await this.transport.fetchStream(path, body, method);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
this.logError(`stream ${method} ${path}`, error);
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
const response = await this.doFetch(path, {
|
|
1230
|
+
method,
|
|
1231
|
+
headers: { "Content-Type": "application/json" },
|
|
1232
|
+
body: body && method === "POST" ? JSON.stringify(body) : void 0
|
|
1233
|
+
});
|
|
1234
|
+
return this.handleStreamResponse(response);
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
689
1237
|
* Utility method to log successful operations
|
|
690
1238
|
*/
|
|
691
1239
|
logSuccess(operation, details) {
|
|
@@ -704,35 +1252,6 @@ var BaseHttpClient = class {
|
|
|
704
1252
|
if (httpStatus >= 500) this.logger.error(`Unexpected error in ${operation}`, error instanceof Error ? error : new Error(String(error)), { httpStatus });
|
|
705
1253
|
} else this.logger.error(`Error in ${operation}`, error instanceof Error ? error : new Error(String(error)));
|
|
706
1254
|
}
|
|
707
|
-
/**
|
|
708
|
-
* Check if response indicates a retryable container error
|
|
709
|
-
*
|
|
710
|
-
* The Sandbox DO returns proper HTTP status codes:
|
|
711
|
-
* - 503 Service Unavailable: Transient errors (container starting, port not ready)
|
|
712
|
-
* - 500 Internal Server Error: Permanent errors (bad config, missing image)
|
|
713
|
-
*
|
|
714
|
-
* We only retry on 503, which indicates the container is starting up.
|
|
715
|
-
* The Retry-After header suggests how long to wait.
|
|
716
|
-
*
|
|
717
|
-
* @param response - HTTP response to check
|
|
718
|
-
* @returns true if error is retryable (503), false otherwise
|
|
719
|
-
*/
|
|
720
|
-
isRetryableContainerError(response) {
|
|
721
|
-
return response.status === 503;
|
|
722
|
-
}
|
|
723
|
-
async executeFetch(path, options) {
|
|
724
|
-
const url = this.options.stub ? `http://localhost:${this.options.port}${path}` : `${this.baseUrl}${path}`;
|
|
725
|
-
try {
|
|
726
|
-
if (this.options.stub) return await this.options.stub.containerFetch(url, options || {}, this.options.port);
|
|
727
|
-
else return await fetch(url, options);
|
|
728
|
-
} catch (error) {
|
|
729
|
-
this.logger.error("HTTP request error", error instanceof Error ? error : new Error(String(error)), {
|
|
730
|
-
method: options?.method || "GET",
|
|
731
|
-
url
|
|
732
|
-
});
|
|
733
|
-
throw error;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
1255
|
};
|
|
737
1256
|
|
|
738
1257
|
//#endregion
|
|
@@ -783,12 +1302,7 @@ var CommandClient = class extends BaseHttpClient {
|
|
|
783
1302
|
...options?.env !== void 0 && { env: options.env },
|
|
784
1303
|
...options?.cwd !== void 0 && { cwd: options.cwd }
|
|
785
1304
|
};
|
|
786
|
-
const
|
|
787
|
-
method: "POST",
|
|
788
|
-
headers: { "Content-Type": "application/json" },
|
|
789
|
-
body: JSON.stringify(data)
|
|
790
|
-
});
|
|
791
|
-
const stream = await this.handleStreamResponse(response);
|
|
1305
|
+
const stream = await this.doStreamFetch("/api/execute/stream", data);
|
|
792
1306
|
this.logSuccess("Command stream started", command);
|
|
793
1307
|
return stream;
|
|
794
1308
|
} catch (error) {
|
|
@@ -882,12 +1396,7 @@ var FileClient = class extends BaseHttpClient {
|
|
|
882
1396
|
path,
|
|
883
1397
|
sessionId
|
|
884
1398
|
};
|
|
885
|
-
const
|
|
886
|
-
method: "POST",
|
|
887
|
-
headers: { "Content-Type": "application/json" },
|
|
888
|
-
body: JSON.stringify(data)
|
|
889
|
-
});
|
|
890
|
-
const stream = await this.handleStreamResponse(response);
|
|
1399
|
+
const stream = await this.doStreamFetch("/api/read/stream", data);
|
|
891
1400
|
this.logSuccess("File stream started", path);
|
|
892
1401
|
return stream;
|
|
893
1402
|
} catch (error) {
|
|
@@ -1076,22 +1585,13 @@ var InterpreterClient = class extends BaseHttpClient {
|
|
|
1076
1585
|
}
|
|
1077
1586
|
async runCodeStream(contextId, code, language, callbacks, timeoutMs) {
|
|
1078
1587
|
return this.executeWithRetry(async () => {
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
},
|
|
1085
|
-
body: JSON.stringify({
|
|
1086
|
-
context_id: contextId,
|
|
1087
|
-
code,
|
|
1088
|
-
language,
|
|
1089
|
-
...timeoutMs !== void 0 && { timeout_ms: timeoutMs }
|
|
1090
|
-
})
|
|
1588
|
+
const stream = await this.doStreamFetch("/api/execute/code", {
|
|
1589
|
+
context_id: contextId,
|
|
1590
|
+
code,
|
|
1591
|
+
language,
|
|
1592
|
+
...timeoutMs !== void 0 && { timeout_ms: timeoutMs }
|
|
1091
1593
|
});
|
|
1092
|
-
|
|
1093
|
-
if (!response.body) throw new Error("No response body for streaming execution");
|
|
1094
|
-
for await (const chunk of this.readLines(response.body)) await this.parseExecutionResult(chunk, callbacks);
|
|
1594
|
+
for await (const chunk of this.readLines(stream)) await this.parseExecutionResult(chunk, callbacks);
|
|
1095
1595
|
});
|
|
1096
1596
|
}
|
|
1097
1597
|
async listCodeContexts() {
|
|
@@ -1122,6 +1622,17 @@ var InterpreterClient = class extends BaseHttpClient {
|
|
|
1122
1622
|
});
|
|
1123
1623
|
}
|
|
1124
1624
|
/**
|
|
1625
|
+
* Get a raw stream for code execution.
|
|
1626
|
+
* Used by CodeInterpreter.runCodeStreaming() for direct stream access.
|
|
1627
|
+
*/
|
|
1628
|
+
async streamCode(contextId, code, language) {
|
|
1629
|
+
return this.doStreamFetch("/api/execute/code", {
|
|
1630
|
+
context_id: contextId,
|
|
1631
|
+
code,
|
|
1632
|
+
language
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1125
1636
|
* Execute an operation with automatic retry for transient errors
|
|
1126
1637
|
*/
|
|
1127
1638
|
async executeWithRetry(operation) {
|
|
@@ -1282,17 +1793,18 @@ var PortClient = class extends BaseHttpClient {
|
|
|
1282
1793
|
}
|
|
1283
1794
|
}
|
|
1284
1795
|
/**
|
|
1285
|
-
*
|
|
1286
|
-
* @param request - Port
|
|
1796
|
+
* Watch a port for readiness via SSE stream
|
|
1797
|
+
* @param request - Port watch configuration
|
|
1798
|
+
* @returns SSE stream that emits PortWatchEvent objects
|
|
1287
1799
|
*/
|
|
1288
|
-
async
|
|
1800
|
+
async watchPort(request) {
|
|
1289
1801
|
try {
|
|
1290
|
-
|
|
1802
|
+
const stream = await this.doStreamFetch("/api/port-watch", request);
|
|
1803
|
+
this.logSuccess("Port watch started", `port ${request.port}`);
|
|
1804
|
+
return stream;
|
|
1291
1805
|
} catch (error) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
error: error instanceof Error ? error.message : "Port check failed"
|
|
1295
|
-
};
|
|
1806
|
+
this.logError("watchPort", error);
|
|
1807
|
+
throw error;
|
|
1296
1808
|
}
|
|
1297
1809
|
}
|
|
1298
1810
|
};
|
|
@@ -1407,8 +1919,7 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
1407
1919
|
async streamProcessLogs(processId) {
|
|
1408
1920
|
try {
|
|
1409
1921
|
const url = `/api/process/${processId}/stream`;
|
|
1410
|
-
const
|
|
1411
|
-
const stream = await this.handleStreamResponse(response);
|
|
1922
|
+
const stream = await this.doStreamFetch(url, void 0, "GET");
|
|
1412
1923
|
this.logSuccess("Process log stream started", `ID: ${processId}`);
|
|
1413
1924
|
return stream;
|
|
1414
1925
|
} catch (error) {
|
|
@@ -1499,6 +2010,12 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
1499
2010
|
/**
|
|
1500
2011
|
* Main sandbox client that composes all domain-specific clients
|
|
1501
2012
|
* Provides organized access to all sandbox functionality
|
|
2013
|
+
*
|
|
2014
|
+
* Supports two transport modes:
|
|
2015
|
+
* - HTTP (default): Each request is a separate HTTP call
|
|
2016
|
+
* - WebSocket: All requests multiplexed over a single connection
|
|
2017
|
+
*
|
|
2018
|
+
* WebSocket mode reduces sub-request count when running inside Workers/Durable Objects.
|
|
1502
2019
|
*/
|
|
1503
2020
|
var SandboxClient = class {
|
|
1504
2021
|
commands;
|
|
@@ -1508,10 +2025,20 @@ var SandboxClient = class {
|
|
|
1508
2025
|
git;
|
|
1509
2026
|
interpreter;
|
|
1510
2027
|
utils;
|
|
2028
|
+
transport = null;
|
|
1511
2029
|
constructor(options) {
|
|
2030
|
+
if (options.transportMode === "websocket" && options.wsUrl) this.transport = createTransport({
|
|
2031
|
+
mode: "websocket",
|
|
2032
|
+
wsUrl: options.wsUrl,
|
|
2033
|
+
baseUrl: options.baseUrl,
|
|
2034
|
+
logger: options.logger,
|
|
2035
|
+
stub: options.stub,
|
|
2036
|
+
port: options.port
|
|
2037
|
+
});
|
|
1512
2038
|
const clientOptions = {
|
|
1513
2039
|
baseUrl: "http://localhost:3000",
|
|
1514
|
-
...options
|
|
2040
|
+
...options,
|
|
2041
|
+
transport: this.transport ?? options.transport
|
|
1515
2042
|
};
|
|
1516
2043
|
this.commands = new CommandClient(clientOptions);
|
|
1517
2044
|
this.files = new FileClient(clientOptions);
|
|
@@ -1521,6 +2048,33 @@ var SandboxClient = class {
|
|
|
1521
2048
|
this.interpreter = new InterpreterClient(clientOptions);
|
|
1522
2049
|
this.utils = new UtilityClient(clientOptions);
|
|
1523
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Get the current transport mode
|
|
2053
|
+
*/
|
|
2054
|
+
getTransportMode() {
|
|
2055
|
+
return this.transport?.getMode() ?? "http";
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Check if WebSocket is connected (only relevant in WebSocket mode)
|
|
2059
|
+
*/
|
|
2060
|
+
isWebSocketConnected() {
|
|
2061
|
+
return this.transport?.isConnected() ?? false;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Connect WebSocket transport (no-op in HTTP mode)
|
|
2065
|
+
* Called automatically on first request, but can be called explicitly
|
|
2066
|
+
* to establish connection upfront.
|
|
2067
|
+
*/
|
|
2068
|
+
async connect() {
|
|
2069
|
+
if (this.transport) await this.transport.connect();
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Disconnect WebSocket transport (no-op in HTTP mode)
|
|
2073
|
+
* Should be called when the sandbox is destroyed.
|
|
2074
|
+
*/
|
|
2075
|
+
disconnect() {
|
|
2076
|
+
if (this.transport) this.transport.disconnect();
|
|
2077
|
+
}
|
|
1524
2078
|
};
|
|
1525
2079
|
|
|
1526
2080
|
//#endregion
|
|
@@ -1647,24 +2201,7 @@ var CodeInterpreter = class {
|
|
|
1647
2201
|
const language = options.language || "python";
|
|
1648
2202
|
context = await this.getOrCreateDefaultContext(language);
|
|
1649
2203
|
}
|
|
1650
|
-
|
|
1651
|
-
method: "POST",
|
|
1652
|
-
headers: {
|
|
1653
|
-
"Content-Type": "application/json",
|
|
1654
|
-
Accept: "text/event-stream"
|
|
1655
|
-
},
|
|
1656
|
-
body: JSON.stringify({
|
|
1657
|
-
context_id: context.id,
|
|
1658
|
-
code,
|
|
1659
|
-
language: options.language
|
|
1660
|
-
})
|
|
1661
|
-
});
|
|
1662
|
-
if (!response.ok) {
|
|
1663
|
-
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
1664
|
-
throw new Error(errorData.error || `Failed to execute code: ${response.status}`);
|
|
1665
|
-
}
|
|
1666
|
-
if (!response.body) throw new Error("No response body for streaming execution");
|
|
1667
|
-
return response.body;
|
|
2204
|
+
return this.interpreterClient.streamCode(context.id, code, options.language);
|
|
1668
2205
|
}
|
|
1669
2206
|
/**
|
|
1670
2207
|
* List all code contexts
|
|
@@ -1987,7 +2524,7 @@ function resolveS3fsOptions(provider, userOptions) {
|
|
|
1987
2524
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
1988
2525
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
1989
2526
|
*/
|
|
1990
|
-
const SDK_VERSION = "0.6.
|
|
2527
|
+
const SDK_VERSION = "0.6.7";
|
|
1991
2528
|
|
|
1992
2529
|
//#endregion
|
|
1993
2530
|
//#region src/sandbox.ts
|
|
@@ -2019,12 +2556,12 @@ var Sandbox = class extends Container {
|
|
|
2019
2556
|
sandboxName = null;
|
|
2020
2557
|
normalizeId = false;
|
|
2021
2558
|
baseUrl = null;
|
|
2022
|
-
portTokens = /* @__PURE__ */ new Map();
|
|
2023
2559
|
defaultSession = null;
|
|
2024
2560
|
envVars = {};
|
|
2025
2561
|
logger;
|
|
2026
2562
|
keepAliveEnabled = false;
|
|
2027
2563
|
activeMounts = /* @__PURE__ */ new Map();
|
|
2564
|
+
transport = "http";
|
|
2028
2565
|
/**
|
|
2029
2566
|
* Default container startup timeouts (conservative for production)
|
|
2030
2567
|
* Based on Cloudflare docs: "Containers take several minutes to provision"
|
|
@@ -2039,6 +2576,20 @@ var Sandbox = class extends Container {
|
|
|
2039
2576
|
* Can be set via options, env vars, or defaults
|
|
2040
2577
|
*/
|
|
2041
2578
|
containerTimeouts = { ...this.DEFAULT_CONTAINER_TIMEOUTS };
|
|
2579
|
+
/**
|
|
2580
|
+
* Create a SandboxClient with current transport settings
|
|
2581
|
+
*/
|
|
2582
|
+
createSandboxClient() {
|
|
2583
|
+
return new SandboxClient({
|
|
2584
|
+
logger: this.logger,
|
|
2585
|
+
port: 3e3,
|
|
2586
|
+
stub: this,
|
|
2587
|
+
...this.transport === "websocket" && {
|
|
2588
|
+
transportMode: "websocket",
|
|
2589
|
+
wsUrl: "ws://localhost:3000/ws"
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2042
2593
|
constructor(ctx, env) {
|
|
2043
2594
|
super(ctx, env);
|
|
2044
2595
|
const envObj = env;
|
|
@@ -2050,19 +2601,15 @@ var Sandbox = class extends Container {
|
|
|
2050
2601
|
component: "sandbox-do",
|
|
2051
2602
|
sandboxId: this.ctx.id.toString()
|
|
2052
2603
|
});
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
});
|
|
2604
|
+
const transportEnv = envObj?.SANDBOX_TRANSPORT;
|
|
2605
|
+
if (transportEnv === "websocket") this.transport = "websocket";
|
|
2606
|
+
else if (transportEnv != null && transportEnv !== "http") this.logger.warn(`Invalid SANDBOX_TRANSPORT value: "${transportEnv}". Must be "http" or "websocket". Defaulting to "http".`);
|
|
2607
|
+
this.client = this.createSandboxClient();
|
|
2058
2608
|
this.codeInterpreter = new CodeInterpreter(this);
|
|
2059
2609
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
2060
2610
|
this.sandboxName = await this.ctx.storage.get("sandboxName") || null;
|
|
2061
2611
|
this.normalizeId = await this.ctx.storage.get("normalizeId") || false;
|
|
2062
2612
|
this.defaultSession = await this.ctx.storage.get("defaultSession") || null;
|
|
2063
|
-
const storedTokens = await this.ctx.storage.get("portTokens") || {};
|
|
2064
|
-
this.portTokens = /* @__PURE__ */ new Map();
|
|
2065
|
-
for (const [portStr, token] of Object.entries(storedTokens)) this.portTokens.set(parseInt(portStr, 10), token);
|
|
2066
2613
|
const storedTimeouts = await this.ctx.storage.get("containerTimeouts");
|
|
2067
2614
|
if (storedTimeouts) this.containerTimeouts = {
|
|
2068
2615
|
...this.containerTimeouts,
|
|
@@ -2270,6 +2817,7 @@ var Sandbox = class extends Container {
|
|
|
2270
2817
|
*/
|
|
2271
2818
|
async destroy() {
|
|
2272
2819
|
this.logger.info("Destroying sandbox container");
|
|
2820
|
+
this.client.disconnect();
|
|
2273
2821
|
for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
|
|
2274
2822
|
if (mountInfo.mounted) try {
|
|
2275
2823
|
this.logger.info(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
|
|
@@ -2314,7 +2862,6 @@ var Sandbox = class extends Container {
|
|
|
2314
2862
|
}
|
|
2315
2863
|
async onStop() {
|
|
2316
2864
|
this.logger.debug("Sandbox stopped");
|
|
2317
|
-
this.portTokens.clear();
|
|
2318
2865
|
this.defaultSession = null;
|
|
2319
2866
|
this.activeMounts.clear();
|
|
2320
2867
|
await Promise.all([this.ctx.storage.delete("portTokens"), this.ctx.storage.delete("defaultSession")]);
|
|
@@ -2458,7 +3005,7 @@ var Sandbox = class extends Container {
|
|
|
2458
3005
|
return await this.containerFetch(request, port);
|
|
2459
3006
|
}
|
|
2460
3007
|
wsConnect(request, port) {
|
|
2461
|
-
throw new Error("
|
|
3008
|
+
throw new Error("wsConnect must be called on the stub returned by getSandbox()");
|
|
2462
3009
|
}
|
|
2463
3010
|
determinePort(url) {
|
|
2464
3011
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -2469,9 +3016,9 @@ var Sandbox = class extends Container {
|
|
|
2469
3016
|
* Ensure default session exists - lazy initialization
|
|
2470
3017
|
* This is called automatically by all public methods that need a session
|
|
2471
3018
|
*
|
|
2472
|
-
* The session is persisted to
|
|
2473
|
-
*
|
|
2474
|
-
*
|
|
3019
|
+
* The session ID is persisted to DO storage. On container restart, if the
|
|
3020
|
+
* container already has this session (from a previous instance), we sync
|
|
3021
|
+
* our state rather than failing on duplicate creation.
|
|
2475
3022
|
*/
|
|
2476
3023
|
async ensureDefaultSession() {
|
|
2477
3024
|
const sessionId = `sandbox-${this.sandboxName || "default"}`;
|
|
@@ -2709,35 +3256,41 @@ var Sandbox = class extends Container {
|
|
|
2709
3256
|
min: 200,
|
|
2710
3257
|
max: 399
|
|
2711
3258
|
}, timeout, interval = 500 } = options ?? {};
|
|
2712
|
-
const startTime = Date.now();
|
|
2713
3259
|
const conditionStr = mode === "http" ? `port ${port} (HTTP ${path})` : `port ${port} (TCP)`;
|
|
2714
|
-
const
|
|
2715
|
-
|
|
2716
|
-
const
|
|
3260
|
+
const statusMin = typeof status === "number" ? status : status.min;
|
|
3261
|
+
const statusMax = typeof status === "number" ? status : status.max;
|
|
3262
|
+
const stream = await this.client.ports.watchPort({
|
|
2717
3263
|
port,
|
|
2718
3264
|
mode,
|
|
2719
3265
|
path,
|
|
2720
|
-
statusMin
|
|
2721
|
-
statusMax
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
3266
|
+
statusMin,
|
|
3267
|
+
statusMax,
|
|
3268
|
+
processId,
|
|
3269
|
+
interval
|
|
3270
|
+
});
|
|
3271
|
+
let timeoutId;
|
|
3272
|
+
let timeoutPromise;
|
|
3273
|
+
if (timeout !== void 0) timeoutPromise = new Promise((_, reject) => {
|
|
3274
|
+
timeoutId = setTimeout(() => {
|
|
3275
|
+
reject(this.createReadyTimeoutError(processId, command, conditionStr, timeout));
|
|
3276
|
+
}, timeout);
|
|
3277
|
+
});
|
|
3278
|
+
try {
|
|
3279
|
+
const streamProcessor = async () => {
|
|
3280
|
+
for await (const event of parseSSEStream(stream)) switch (event.type) {
|
|
3281
|
+
case "ready": return;
|
|
3282
|
+
case "process_exited": throw this.createExitedBeforeReadyError(processId, command, conditionStr, event.exitCode ?? 1);
|
|
3283
|
+
case "error": throw new Error(event.error || "Port watch failed");
|
|
3284
|
+
}
|
|
3285
|
+
throw new Error("Port watch stream ended unexpectedly");
|
|
3286
|
+
};
|
|
3287
|
+
if (timeoutPromise) await Promise.race([streamProcessor(), timeoutPromise]);
|
|
3288
|
+
else await streamProcessor();
|
|
3289
|
+
} finally {
|
|
3290
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
2732
3291
|
try {
|
|
2733
|
-
|
|
3292
|
+
await stream.cancel();
|
|
2734
3293
|
} catch {}
|
|
2735
|
-
checkCount++;
|
|
2736
|
-
const iterationDuration = Date.now() - iterationStart;
|
|
2737
|
-
const sleepTime = Math.max(0, targetInterval - iterationDuration);
|
|
2738
|
-
if (sleepTime > 0) {
|
|
2739
|
-
if (timeout === void 0 || Date.now() - startTime + sleepTime < timeout) await new Promise((resolve) => setTimeout(resolve, sleepTime));
|
|
2740
|
-
}
|
|
2741
3294
|
}
|
|
2742
3295
|
}
|
|
2743
3296
|
/**
|
|
@@ -3022,8 +3575,9 @@ var Sandbox = class extends Container {
|
|
|
3022
3575
|
await this.client.ports.exposePort(port, sessionId, options?.name);
|
|
3023
3576
|
if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
|
|
3024
3577
|
const token = this.generatePortToken();
|
|
3025
|
-
this.
|
|
3026
|
-
|
|
3578
|
+
const tokens = await this.ctx.storage.get("portTokens") || {};
|
|
3579
|
+
tokens[port.toString()] = token;
|
|
3580
|
+
await this.ctx.storage.put("portTokens", tokens);
|
|
3027
3581
|
return {
|
|
3028
3582
|
url: this.constructPreviewUrl(port, this.sandboxName, options.hostname, token),
|
|
3029
3583
|
port,
|
|
@@ -3034,17 +3588,19 @@ var Sandbox = class extends Container {
|
|
|
3034
3588
|
if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
|
|
3035
3589
|
const sessionId = await this.ensureDefaultSession();
|
|
3036
3590
|
await this.client.ports.unexposePort(port, sessionId);
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3591
|
+
const tokens = await this.ctx.storage.get("portTokens") || {};
|
|
3592
|
+
if (tokens[port.toString()]) {
|
|
3593
|
+
delete tokens[port.toString()];
|
|
3594
|
+
await this.ctx.storage.put("portTokens", tokens);
|
|
3040
3595
|
}
|
|
3041
3596
|
}
|
|
3042
3597
|
async getExposedPorts(hostname) {
|
|
3043
3598
|
const sessionId = await this.ensureDefaultSession();
|
|
3044
3599
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
3045
3600
|
if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
|
|
3601
|
+
const tokens = await this.ctx.storage.get("portTokens") || {};
|
|
3046
3602
|
return response.ports.map((port) => {
|
|
3047
|
-
const token =
|
|
3603
|
+
const token = tokens[port.port.toString()];
|
|
3048
3604
|
if (!token) throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`);
|
|
3049
3605
|
return {
|
|
3050
3606
|
url: this.constructPreviewUrl(port.port, this.sandboxName, hostname, token),
|
|
@@ -3064,7 +3620,7 @@ var Sandbox = class extends Container {
|
|
|
3064
3620
|
}
|
|
3065
3621
|
async validatePortToken(port, token) {
|
|
3066
3622
|
if (!await this.isPortExposed(port)) return false;
|
|
3067
|
-
const storedToken = this.
|
|
3623
|
+
const storedToken = (await this.ctx.storage.get("portTokens") || {})[port.toString()];
|
|
3068
3624
|
if (!storedToken) {
|
|
3069
3625
|
this.logger.error("Port is exposed but has no token - bug detected", void 0, { port });
|
|
3070
3626
|
return false;
|
|
@@ -3076,11 +3632,6 @@ var Sandbox = class extends Container {
|
|
|
3076
3632
|
crypto.getRandomValues(array);
|
|
3077
3633
|
return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "").toLowerCase();
|
|
3078
3634
|
}
|
|
3079
|
-
async persistPortTokens() {
|
|
3080
|
-
const tokensObj = {};
|
|
3081
|
-
for (const [port, token] of this.portTokens.entries()) tokensObj[port.toString()] = token;
|
|
3082
|
-
await this.ctx.storage.put("portTokens", tokensObj);
|
|
3083
|
-
}
|
|
3084
3635
|
constructPreviewUrl(port, sandboxId, hostname, token) {
|
|
3085
3636
|
if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
|
|
3086
3637
|
const effectiveId = this.sandboxName || sandboxId;
|