@cloudflare/sandbox 0.0.0-0608f1e → 0.0.0-12bbd12
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/CHANGELOG.md +58 -0
- package/Dockerfile +44 -18
- package/README.md +784 -0
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/handler/exec.ts +17 -14
- package/container_src/handler/file.ts +222 -2
- package/container_src/handler/process.ts +1 -1
- package/container_src/index.ts +312 -10
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +461 -0
- package/container_src/jupyter_config.py +48 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +84 -0
- package/container_src/types.ts +17 -3
- package/package.json +5 -4
- package/src/client.ts +108 -122
- package/src/errors.ts +218 -0
- package/src/index.ts +51 -15
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -0
- package/src/sandbox.ts +292 -149
- package/src/types.ts +125 -0
- package/tsconfig.json +1 -1
package/src/client.ts
CHANGED
|
@@ -1,41 +1,23 @@
|
|
|
1
|
+
import type { ExecuteRequest } from "../container_src/types";
|
|
1
2
|
import type { Sandbox } from "./index";
|
|
2
3
|
import type {
|
|
4
|
+
BaseExecOptions,
|
|
5
|
+
DeleteFileResponse,
|
|
6
|
+
ExecuteResponse,
|
|
3
7
|
GetProcessLogsResponse,
|
|
4
8
|
GetProcessResponse,
|
|
9
|
+
GitCheckoutResponse,
|
|
10
|
+
ListFilesResponse,
|
|
5
11
|
ListProcessesResponse,
|
|
12
|
+
MkdirResponse,
|
|
13
|
+
MoveFileResponse,
|
|
14
|
+
ReadFileResponse,
|
|
15
|
+
RenameFileResponse,
|
|
6
16
|
StartProcessRequest,
|
|
7
|
-
StartProcessResponse
|
|
17
|
+
StartProcessResponse,
|
|
18
|
+
WriteFileResponse,
|
|
8
19
|
} from "./types";
|
|
9
20
|
|
|
10
|
-
interface ExecuteRequest {
|
|
11
|
-
command: string;
|
|
12
|
-
sessionId?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ExecuteResponse {
|
|
16
|
-
success: boolean;
|
|
17
|
-
stdout: string;
|
|
18
|
-
stderr: string;
|
|
19
|
-
exitCode: number;
|
|
20
|
-
command: string;
|
|
21
|
-
timestamp: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface SessionResponse {
|
|
25
|
-
sessionId: string;
|
|
26
|
-
message: string;
|
|
27
|
-
timestamp: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface SessionListResponse {
|
|
31
|
-
sessions: Array<{
|
|
32
|
-
sessionId: string;
|
|
33
|
-
hasActiveProcess: boolean;
|
|
34
|
-
createdAt: string;
|
|
35
|
-
}>;
|
|
36
|
-
count: number;
|
|
37
|
-
timestamp: string;
|
|
38
|
-
}
|
|
39
21
|
|
|
40
22
|
interface CommandsResponse {
|
|
41
23
|
availableCommands: string[];
|
|
@@ -49,16 +31,6 @@ interface GitCheckoutRequest {
|
|
|
49
31
|
sessionId?: string;
|
|
50
32
|
}
|
|
51
33
|
|
|
52
|
-
export interface GitCheckoutResponse {
|
|
53
|
-
success: boolean;
|
|
54
|
-
stdout: string;
|
|
55
|
-
stderr: string;
|
|
56
|
-
exitCode: number;
|
|
57
|
-
repoUrl: string;
|
|
58
|
-
branch: string;
|
|
59
|
-
targetDir: string;
|
|
60
|
-
timestamp: string;
|
|
61
|
-
}
|
|
62
34
|
|
|
63
35
|
interface MkdirRequest {
|
|
64
36
|
path: string;
|
|
@@ -66,15 +38,6 @@ interface MkdirRequest {
|
|
|
66
38
|
sessionId?: string;
|
|
67
39
|
}
|
|
68
40
|
|
|
69
|
-
export interface MkdirResponse {
|
|
70
|
-
success: boolean;
|
|
71
|
-
stdout: string;
|
|
72
|
-
stderr: string;
|
|
73
|
-
exitCode: number;
|
|
74
|
-
path: string;
|
|
75
|
-
recursive: boolean;
|
|
76
|
-
timestamp: string;
|
|
77
|
-
}
|
|
78
41
|
|
|
79
42
|
interface WriteFileRequest {
|
|
80
43
|
path: string;
|
|
@@ -83,12 +46,6 @@ interface WriteFileRequest {
|
|
|
83
46
|
sessionId?: string;
|
|
84
47
|
}
|
|
85
48
|
|
|
86
|
-
export interface WriteFileResponse {
|
|
87
|
-
success: boolean;
|
|
88
|
-
exitCode: number;
|
|
89
|
-
path: string;
|
|
90
|
-
timestamp: string;
|
|
91
|
-
}
|
|
92
49
|
|
|
93
50
|
interface ReadFileRequest {
|
|
94
51
|
path: string;
|
|
@@ -96,25 +53,12 @@ interface ReadFileRequest {
|
|
|
96
53
|
sessionId?: string;
|
|
97
54
|
}
|
|
98
55
|
|
|
99
|
-
export interface ReadFileResponse {
|
|
100
|
-
success: boolean;
|
|
101
|
-
exitCode: number;
|
|
102
|
-
path: string;
|
|
103
|
-
content: string;
|
|
104
|
-
timestamp: string;
|
|
105
|
-
}
|
|
106
56
|
|
|
107
57
|
interface DeleteFileRequest {
|
|
108
58
|
path: string;
|
|
109
59
|
sessionId?: string;
|
|
110
60
|
}
|
|
111
61
|
|
|
112
|
-
export interface DeleteFileResponse {
|
|
113
|
-
success: boolean;
|
|
114
|
-
exitCode: number;
|
|
115
|
-
path: string;
|
|
116
|
-
timestamp: string;
|
|
117
|
-
}
|
|
118
62
|
|
|
119
63
|
interface RenameFileRequest {
|
|
120
64
|
oldPath: string;
|
|
@@ -122,13 +66,6 @@ interface RenameFileRequest {
|
|
|
122
66
|
sessionId?: string;
|
|
123
67
|
}
|
|
124
68
|
|
|
125
|
-
export interface RenameFileResponse {
|
|
126
|
-
success: boolean;
|
|
127
|
-
exitCode: number;
|
|
128
|
-
oldPath: string;
|
|
129
|
-
newPath: string;
|
|
130
|
-
timestamp: string;
|
|
131
|
-
}
|
|
132
69
|
|
|
133
70
|
interface MoveFileRequest {
|
|
134
71
|
sourcePath: string;
|
|
@@ -136,14 +73,17 @@ interface MoveFileRequest {
|
|
|
136
73
|
sessionId?: string;
|
|
137
74
|
}
|
|
138
75
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
76
|
+
|
|
77
|
+
interface ListFilesRequest {
|
|
78
|
+
path: string;
|
|
79
|
+
options?: {
|
|
80
|
+
recursive?: boolean;
|
|
81
|
+
includeHidden?: boolean;
|
|
82
|
+
};
|
|
83
|
+
sessionId?: string;
|
|
145
84
|
}
|
|
146
85
|
|
|
86
|
+
|
|
147
87
|
interface PreviewInfo {
|
|
148
88
|
url: string;
|
|
149
89
|
port: number;
|
|
@@ -212,7 +152,7 @@ export class HttpClient {
|
|
|
212
152
|
this.baseUrl = this.options.baseUrl!;
|
|
213
153
|
}
|
|
214
154
|
|
|
215
|
-
|
|
155
|
+
protected async doFetch(
|
|
216
156
|
path: string,
|
|
217
157
|
options?: RequestInit
|
|
218
158
|
): Promise<Response> {
|
|
@@ -255,16 +195,19 @@ export class HttpClient {
|
|
|
255
195
|
|
|
256
196
|
async execute(
|
|
257
197
|
command: string,
|
|
258
|
-
sessionId
|
|
198
|
+
options: Pick<BaseExecOptions, "sessionId" | "cwd" | "env">
|
|
259
199
|
): Promise<ExecuteResponse> {
|
|
260
200
|
try {
|
|
261
|
-
const targetSessionId = sessionId || this.sessionId;
|
|
201
|
+
const targetSessionId = options.sessionId || this.sessionId;
|
|
202
|
+
const executeRequest = {
|
|
203
|
+
command,
|
|
204
|
+
sessionId: targetSessionId,
|
|
205
|
+
cwd: options.cwd,
|
|
206
|
+
env: options.env,
|
|
207
|
+
} satisfies ExecuteRequest;
|
|
262
208
|
|
|
263
209
|
const response = await this.doFetch(`/api/execute`, {
|
|
264
|
-
body: JSON.stringify(
|
|
265
|
-
command,
|
|
266
|
-
sessionId: targetSessionId,
|
|
267
|
-
} as ExecuteRequest),
|
|
210
|
+
body: JSON.stringify(executeRequest),
|
|
268
211
|
headers: {
|
|
269
212
|
"Content-Type": "application/json",
|
|
270
213
|
},
|
|
@@ -305,7 +248,6 @@ export class HttpClient {
|
|
|
305
248
|
}
|
|
306
249
|
}
|
|
307
250
|
|
|
308
|
-
|
|
309
251
|
async executeCommandStream(
|
|
310
252
|
command: string,
|
|
311
253
|
sessionId?: string
|
|
@@ -320,7 +262,7 @@ export class HttpClient {
|
|
|
320
262
|
}),
|
|
321
263
|
headers: {
|
|
322
264
|
"Content-Type": "application/json",
|
|
323
|
-
|
|
265
|
+
Accept: "text/event-stream",
|
|
324
266
|
},
|
|
325
267
|
method: "POST",
|
|
326
268
|
});
|
|
@@ -338,9 +280,7 @@ export class HttpClient {
|
|
|
338
280
|
throw new Error("No response body for streaming request");
|
|
339
281
|
}
|
|
340
282
|
|
|
341
|
-
console.log(
|
|
342
|
-
`[HTTP Client] Started command stream: ${command}`
|
|
343
|
-
);
|
|
283
|
+
console.log(`[HTTP Client] Started command stream: ${command}`);
|
|
344
284
|
|
|
345
285
|
return response.body;
|
|
346
286
|
} catch (error) {
|
|
@@ -392,7 +332,6 @@ export class HttpClient {
|
|
|
392
332
|
}
|
|
393
333
|
}
|
|
394
334
|
|
|
395
|
-
|
|
396
335
|
async mkdir(
|
|
397
336
|
path: string,
|
|
398
337
|
recursive: boolean = false,
|
|
@@ -434,7 +373,6 @@ export class HttpClient {
|
|
|
434
373
|
}
|
|
435
374
|
}
|
|
436
375
|
|
|
437
|
-
|
|
438
376
|
async writeFile(
|
|
439
377
|
path: string,
|
|
440
378
|
content: string,
|
|
@@ -478,7 +416,6 @@ export class HttpClient {
|
|
|
478
416
|
}
|
|
479
417
|
}
|
|
480
418
|
|
|
481
|
-
|
|
482
419
|
async readFile(
|
|
483
420
|
path: string,
|
|
484
421
|
encoding: string = "utf-8",
|
|
@@ -520,7 +457,6 @@ export class HttpClient {
|
|
|
520
457
|
}
|
|
521
458
|
}
|
|
522
459
|
|
|
523
|
-
|
|
524
460
|
async deleteFile(
|
|
525
461
|
path: string,
|
|
526
462
|
sessionId?: string
|
|
@@ -560,7 +496,6 @@ export class HttpClient {
|
|
|
560
496
|
}
|
|
561
497
|
}
|
|
562
498
|
|
|
563
|
-
|
|
564
499
|
async renameFile(
|
|
565
500
|
oldPath: string,
|
|
566
501
|
newPath: string,
|
|
@@ -602,7 +537,6 @@ export class HttpClient {
|
|
|
602
537
|
}
|
|
603
538
|
}
|
|
604
539
|
|
|
605
|
-
|
|
606
540
|
async moveFile(
|
|
607
541
|
sourcePath: string,
|
|
608
542
|
destinationPath: string,
|
|
@@ -644,6 +578,49 @@ export class HttpClient {
|
|
|
644
578
|
}
|
|
645
579
|
}
|
|
646
580
|
|
|
581
|
+
async listFiles(
|
|
582
|
+
path: string,
|
|
583
|
+
options?: {
|
|
584
|
+
recursive?: boolean;
|
|
585
|
+
includeHidden?: boolean;
|
|
586
|
+
},
|
|
587
|
+
sessionId?: string
|
|
588
|
+
): Promise<ListFilesResponse> {
|
|
589
|
+
try {
|
|
590
|
+
const targetSessionId = sessionId || this.sessionId;
|
|
591
|
+
|
|
592
|
+
const response = await this.doFetch(`/api/list-files`, {
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
path,
|
|
595
|
+
options,
|
|
596
|
+
sessionId: targetSessionId,
|
|
597
|
+
} as ListFilesRequest),
|
|
598
|
+
headers: {
|
|
599
|
+
"Content-Type": "application/json",
|
|
600
|
+
},
|
|
601
|
+
method: "POST",
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
const errorData = (await response.json().catch(() => ({}))) as {
|
|
606
|
+
error?: string;
|
|
607
|
+
};
|
|
608
|
+
throw new Error(
|
|
609
|
+
errorData.error || `HTTP error! status: ${response.status}`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const data: ListFilesResponse = await response.json();
|
|
614
|
+
console.log(
|
|
615
|
+
`[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}`
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
return data;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.error("[HTTP Client] Error listing files:", error);
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
647
624
|
|
|
648
625
|
async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
|
|
649
626
|
try {
|
|
@@ -670,7 +647,9 @@ export class HttpClient {
|
|
|
670
647
|
|
|
671
648
|
const data: ExposePortResponse = await response.json();
|
|
672
649
|
console.log(
|
|
673
|
-
`[HTTP Client] Port exposed: ${port}${
|
|
650
|
+
`[HTTP Client] Port exposed: ${port}${
|
|
651
|
+
name ? ` (${name})` : ""
|
|
652
|
+
}, Success: ${data.success}`
|
|
674
653
|
);
|
|
675
654
|
|
|
676
655
|
return data;
|
|
@@ -732,9 +711,7 @@ export class HttpClient {
|
|
|
732
711
|
}
|
|
733
712
|
|
|
734
713
|
const data: GetExposedPortsResponse = await response.json();
|
|
735
|
-
console.log(
|
|
736
|
-
`[HTTP Client] Got ${data.count} exposed ports`
|
|
737
|
-
);
|
|
714
|
+
console.log(`[HTTP Client] Got ${data.count} exposed ports`);
|
|
738
715
|
|
|
739
716
|
return data;
|
|
740
717
|
} catch (error) {
|
|
@@ -871,9 +848,7 @@ export class HttpClient {
|
|
|
871
848
|
}
|
|
872
849
|
|
|
873
850
|
const data: ListProcessesResponse = await response.json();
|
|
874
|
-
console.log(
|
|
875
|
-
`[HTTP Client] Listed ${data.processes.length} processes`
|
|
876
|
-
);
|
|
851
|
+
console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
|
|
877
852
|
|
|
878
853
|
return data;
|
|
879
854
|
} catch (error) {
|
|
@@ -902,7 +877,9 @@ export class HttpClient {
|
|
|
902
877
|
|
|
903
878
|
const data: GetProcessResponse = await response.json();
|
|
904
879
|
console.log(
|
|
905
|
-
`[HTTP Client] Got process ${processId}: ${
|
|
880
|
+
`[HTTP Client] Got process ${processId}: ${
|
|
881
|
+
data.process?.status || "not found"
|
|
882
|
+
}`
|
|
906
883
|
);
|
|
907
884
|
|
|
908
885
|
return data;
|
|
@@ -912,7 +889,9 @@ export class HttpClient {
|
|
|
912
889
|
}
|
|
913
890
|
}
|
|
914
891
|
|
|
915
|
-
async killProcess(
|
|
892
|
+
async killProcess(
|
|
893
|
+
processId: string
|
|
894
|
+
): Promise<{ success: boolean; message: string }> {
|
|
916
895
|
try {
|
|
917
896
|
const response = await this.doFetch(`/api/process/${processId}`, {
|
|
918
897
|
headers: {
|
|
@@ -930,10 +909,11 @@ export class HttpClient {
|
|
|
930
909
|
);
|
|
931
910
|
}
|
|
932
911
|
|
|
933
|
-
const data = await response.json() as {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
912
|
+
const data = (await response.json()) as {
|
|
913
|
+
success: boolean;
|
|
914
|
+
message: string;
|
|
915
|
+
};
|
|
916
|
+
console.log(`[HTTP Client] Killed process ${processId}`);
|
|
937
917
|
|
|
938
918
|
return data;
|
|
939
919
|
} catch (error) {
|
|
@@ -942,7 +922,11 @@ export class HttpClient {
|
|
|
942
922
|
}
|
|
943
923
|
}
|
|
944
924
|
|
|
945
|
-
async killAllProcesses(): Promise<{
|
|
925
|
+
async killAllProcesses(): Promise<{
|
|
926
|
+
success: boolean;
|
|
927
|
+
killedCount: number;
|
|
928
|
+
message: string;
|
|
929
|
+
}> {
|
|
946
930
|
try {
|
|
947
931
|
const response = await this.doFetch("/api/process/kill-all", {
|
|
948
932
|
headers: {
|
|
@@ -960,10 +944,12 @@ export class HttpClient {
|
|
|
960
944
|
);
|
|
961
945
|
}
|
|
962
946
|
|
|
963
|
-
const data = await response.json() as {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
947
|
+
const data = (await response.json()) as {
|
|
948
|
+
success: boolean;
|
|
949
|
+
killedCount: number;
|
|
950
|
+
message: string;
|
|
951
|
+
};
|
|
952
|
+
console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
|
|
967
953
|
|
|
968
954
|
return data;
|
|
969
955
|
} catch (error) {
|
|
@@ -991,9 +977,7 @@ export class HttpClient {
|
|
|
991
977
|
}
|
|
992
978
|
|
|
993
979
|
const data: GetProcessLogsResponse = await response.json();
|
|
994
|
-
console.log(
|
|
995
|
-
`[HTTP Client] Got logs for process ${processId}`
|
|
996
|
-
);
|
|
980
|
+
console.log(`[HTTP Client] Got logs for process ${processId}`);
|
|
997
981
|
|
|
998
982
|
return data;
|
|
999
983
|
} catch (error) {
|
|
@@ -1002,11 +986,13 @@ export class HttpClient {
|
|
|
1002
986
|
}
|
|
1003
987
|
}
|
|
1004
988
|
|
|
1005
|
-
async streamProcessLogs(
|
|
989
|
+
async streamProcessLogs(
|
|
990
|
+
processId: string
|
|
991
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
1006
992
|
try {
|
|
1007
993
|
const response = await this.doFetch(`/api/process/${processId}/stream`, {
|
|
1008
994
|
headers: {
|
|
1009
|
-
|
|
995
|
+
Accept: "text/event-stream",
|
|
1010
996
|
"Cache-Control": "no-cache",
|
|
1011
997
|
},
|
|
1012
998
|
method: "GET",
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard error response from the sandbox API
|
|
3
|
+
*/
|
|
4
|
+
export interface SandboxErrorResponse {
|
|
5
|
+
error?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
progress?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base error class for all Sandbox-related errors
|
|
12
|
+
*/
|
|
13
|
+
export class SandboxError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = this.constructor.name;
|
|
17
|
+
|
|
18
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
19
|
+
if (Error.captureStackTrace) {
|
|
20
|
+
Error.captureStackTrace(this, this.constructor);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error thrown when Jupyter functionality is requested but the service is still initializing.
|
|
27
|
+
*
|
|
28
|
+
* Note: With the current implementation, requests wait for Jupyter to be ready.
|
|
29
|
+
* This error is only thrown when:
|
|
30
|
+
* 1. The request times out waiting for Jupyter (default: 30 seconds)
|
|
31
|
+
* 2. Jupyter initialization actually fails
|
|
32
|
+
*
|
|
33
|
+
* Most requests will succeed after a delay, not throw this error.
|
|
34
|
+
*/
|
|
35
|
+
export class JupyterNotReadyError extends SandboxError {
|
|
36
|
+
public readonly code = "JUPYTER_NOT_READY";
|
|
37
|
+
public readonly retryAfter: number;
|
|
38
|
+
public readonly progress?: number;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
message?: string,
|
|
42
|
+
options?: { retryAfter?: number; progress?: number }
|
|
43
|
+
) {
|
|
44
|
+
super(
|
|
45
|
+
message || "Jupyter is still initializing. Please retry in a few seconds."
|
|
46
|
+
);
|
|
47
|
+
this.retryAfter = options?.retryAfter || 5;
|
|
48
|
+
this.progress = options?.progress;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Error thrown when a context is not found
|
|
54
|
+
*/
|
|
55
|
+
export class ContextNotFoundError extends SandboxError {
|
|
56
|
+
public readonly code = "CONTEXT_NOT_FOUND";
|
|
57
|
+
public readonly contextId: string;
|
|
58
|
+
|
|
59
|
+
constructor(contextId: string) {
|
|
60
|
+
super(`Context ${contextId} not found`);
|
|
61
|
+
this.contextId = contextId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Error thrown when code execution fails
|
|
67
|
+
*/
|
|
68
|
+
export class CodeExecutionError extends SandboxError {
|
|
69
|
+
public readonly code = "CODE_EXECUTION_ERROR";
|
|
70
|
+
public readonly executionError?: {
|
|
71
|
+
ename?: string;
|
|
72
|
+
evalue?: string;
|
|
73
|
+
traceback?: string[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
constructor(message: string, executionError?: any) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.executionError = executionError;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Error thrown when the sandbox container is not ready
|
|
84
|
+
*/
|
|
85
|
+
export class ContainerNotReadyError extends SandboxError {
|
|
86
|
+
public readonly code = "CONTAINER_NOT_READY";
|
|
87
|
+
|
|
88
|
+
constructor(message?: string) {
|
|
89
|
+
super(
|
|
90
|
+
message ||
|
|
91
|
+
"Container is not ready. Please wait for initialization to complete."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Error thrown when a network request to the sandbox fails
|
|
98
|
+
*/
|
|
99
|
+
export class SandboxNetworkError extends SandboxError {
|
|
100
|
+
public readonly code = "NETWORK_ERROR";
|
|
101
|
+
public readonly statusCode?: number;
|
|
102
|
+
public readonly statusText?: string;
|
|
103
|
+
|
|
104
|
+
constructor(message: string, statusCode?: number, statusText?: string) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.statusCode = statusCode;
|
|
107
|
+
this.statusText = statusText;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Error thrown when service is temporarily unavailable (e.g., circuit breaker open)
|
|
113
|
+
*/
|
|
114
|
+
export class ServiceUnavailableError extends SandboxError {
|
|
115
|
+
public readonly code = "SERVICE_UNAVAILABLE";
|
|
116
|
+
public readonly retryAfter?: number;
|
|
117
|
+
|
|
118
|
+
constructor(message?: string, retryAfter?: number) {
|
|
119
|
+
// Simple, user-friendly message without implementation details
|
|
120
|
+
super(message || "Service temporarily unavailable");
|
|
121
|
+
this.retryAfter = retryAfter;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Type guard to check if an error is a JupyterNotReadyError
|
|
127
|
+
*/
|
|
128
|
+
export function isJupyterNotReadyError(
|
|
129
|
+
error: unknown
|
|
130
|
+
): error is JupyterNotReadyError {
|
|
131
|
+
return error instanceof JupyterNotReadyError;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Type guard to check if an error is any SandboxError
|
|
136
|
+
*/
|
|
137
|
+
export function isSandboxError(error: unknown): error is SandboxError {
|
|
138
|
+
return error instanceof SandboxError;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Helper to determine if an error is retryable
|
|
143
|
+
*/
|
|
144
|
+
export function isRetryableError(error: unknown): boolean {
|
|
145
|
+
if (
|
|
146
|
+
error instanceof JupyterNotReadyError ||
|
|
147
|
+
error instanceof ContainerNotReadyError ||
|
|
148
|
+
error instanceof ServiceUnavailableError
|
|
149
|
+
) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (error instanceof SandboxNetworkError) {
|
|
154
|
+
// Retry on 502, 503, 504 (gateway/service unavailable errors)
|
|
155
|
+
return error.statusCode
|
|
156
|
+
? [502, 503, 504].includes(error.statusCode)
|
|
157
|
+
: false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse error response from the sandbox API and return appropriate error instance
|
|
165
|
+
*/
|
|
166
|
+
export async function parseErrorResponse(
|
|
167
|
+
response: Response
|
|
168
|
+
): Promise<SandboxError> {
|
|
169
|
+
let data: SandboxErrorResponse;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
data = (await response.json()) as SandboxErrorResponse;
|
|
173
|
+
} catch {
|
|
174
|
+
// If JSON parsing fails, return a generic network error
|
|
175
|
+
return new SandboxNetworkError(
|
|
176
|
+
`Request failed with status ${response.status}`,
|
|
177
|
+
response.status,
|
|
178
|
+
response.statusText
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for specific error types based on response
|
|
183
|
+
if (response.status === 503) {
|
|
184
|
+
// Circuit breaker error
|
|
185
|
+
if (data.status === "circuit_open") {
|
|
186
|
+
return new ServiceUnavailableError(
|
|
187
|
+
"Service temporarily unavailable",
|
|
188
|
+
parseInt(response.headers.get("Retry-After") || "30")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Jupyter initialization error
|
|
193
|
+
if (data.status === "initializing") {
|
|
194
|
+
return new JupyterNotReadyError(data.error, {
|
|
195
|
+
retryAfter: parseInt(response.headers.get("Retry-After") || "5"),
|
|
196
|
+
progress: data.progress,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for context not found
|
|
202
|
+
if (
|
|
203
|
+
response.status === 404 &&
|
|
204
|
+
data.error?.includes("Context") &&
|
|
205
|
+
data.error?.includes("not found")
|
|
206
|
+
) {
|
|
207
|
+
const contextId =
|
|
208
|
+
data.error.match(/Context (\S+) not found/)?.[1] || "unknown";
|
|
209
|
+
return new ContextNotFoundError(contextId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Default network error
|
|
213
|
+
return new SandboxNetworkError(
|
|
214
|
+
data.error || `Request failed with status ${response.status}`,
|
|
215
|
+
response.status,
|
|
216
|
+
response.statusText
|
|
217
|
+
);
|
|
218
|
+
}
|