@cloudflare/sandbox 0.0.0-603d05f → 0.0.0-66cc85b
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 +36 -0
- package/Dockerfile +23 -24
- package/README.md +59 -6
- package/container_src/bun.lock +31 -77
- package/container_src/control-process.ts +1 -1
- package/container_src/handler/exec.ts +2 -2
- package/container_src/handler/file.ts +51 -0
- package/container_src/handler/session.ts +2 -2
- package/container_src/index.ts +43 -43
- package/container_src/interpreter-service.ts +276 -0
- package/container_src/isolation.ts +207 -33
- package/container_src/mime-processor.ts +1 -1
- package/container_src/package.json +4 -4
- package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
- package/container_src/runtime/executors/python/ipython_executor.py +338 -0
- package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
- package/container_src/runtime/process-pool.ts +464 -0
- package/container_src/startup.sh +6 -79
- package/package.json +2 -2
- package/src/client.ts +39 -0
- package/src/errors.ts +15 -14
- package/src/file-stream.ts +162 -0
- package/src/index.ts +22 -5
- package/src/{jupyter-client.ts → interpreter-client.ts} +6 -3
- package/src/interpreter-types.ts +102 -95
- package/src/interpreter.ts +8 -8
- package/src/sandbox.ts +18 -5
- package/src/types.ts +69 -0
- package/container_src/jupyter-server.ts +0 -579
- package/container_src/jupyter-service.ts +0 -461
- package/container_src/jupyter_config.py +0 -48
package/container_src/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { serve } from "bun";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
handleExecuteRequest,
|
|
4
|
+
handleStreamingExecuteRequest,
|
|
5
|
+
} from "./handler/exec";
|
|
3
6
|
import {
|
|
4
7
|
handleDeleteFileRequest,
|
|
5
8
|
handleListFilesRequest,
|
|
6
9
|
handleMkdirRequest,
|
|
7
10
|
handleMoveFileRequest,
|
|
8
11
|
handleReadFileRequest,
|
|
12
|
+
handleReadFileStreamRequest,
|
|
9
13
|
handleRenameFileRequest,
|
|
10
14
|
handleWriteFileRequest,
|
|
11
15
|
} from "./handler/file";
|
|
@@ -26,9 +30,12 @@ import {
|
|
|
26
30
|
handleStreamProcessLogsRequest,
|
|
27
31
|
} from "./handler/process";
|
|
28
32
|
import { handleCreateSession, handleListSessions } from "./handler/session";
|
|
33
|
+
import type { CreateContextRequest } from "./interpreter-service";
|
|
34
|
+
import {
|
|
35
|
+
InterpreterNotReadyError,
|
|
36
|
+
InterpreterService,
|
|
37
|
+
} from "./interpreter-service";
|
|
29
38
|
import { hasNamespaceSupport, SessionManager } from "./isolation";
|
|
30
|
-
import type { CreateContextRequest } from "./jupyter-server";
|
|
31
|
-
import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
|
|
32
39
|
|
|
33
40
|
// In-memory storage for exposed ports
|
|
34
41
|
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
@@ -72,30 +79,15 @@ process.on("uncaughtException", async (error) => {
|
|
|
72
79
|
process.exit(1);
|
|
73
80
|
});
|
|
74
81
|
|
|
75
|
-
// Initialize
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
// Start Jupyter initialization in background (non-blocking)
|
|
79
|
-
console.log("[Container] Starting Jupyter initialization in background...");
|
|
80
|
-
console.log(
|
|
81
|
-
"[Container] API endpoints are available immediately. Jupyter-dependent features will be available shortly."
|
|
82
|
-
);
|
|
82
|
+
// Initialize interpreter service
|
|
83
|
+
const interpreterService = new InterpreterService();
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
console.log(
|
|
88
|
-
"[Container] Jupyter fully initialized - all features available"
|
|
89
|
-
);
|
|
90
|
-
})
|
|
91
|
-
.catch((error) => {
|
|
92
|
-
console.error("[Container] Jupyter initialization failed:", error.message);
|
|
93
|
-
console.error(
|
|
94
|
-
"[Container] The API will continue in degraded mode without code execution capabilities"
|
|
95
|
-
);
|
|
96
|
-
});
|
|
85
|
+
// No initialization needed - service is ready immediately!
|
|
86
|
+
console.log("[Container] Interpreter service ready - no cold start!");
|
|
87
|
+
console.log("[Container] All API endpoints available immediately");
|
|
97
88
|
|
|
98
89
|
const server = serve({
|
|
90
|
+
idleTimeout: 255,
|
|
99
91
|
async fetch(req: Request) {
|
|
100
92
|
const url = new URL(req.url);
|
|
101
93
|
const pathname = url.pathname;
|
|
@@ -144,26 +136,27 @@ const server = serve({
|
|
|
144
136
|
return handleExecuteRequest(req, corsHeaders, sessionManager);
|
|
145
137
|
}
|
|
146
138
|
break;
|
|
147
|
-
|
|
139
|
+
|
|
148
140
|
case "/api/execute/stream":
|
|
149
141
|
if (req.method === "POST") {
|
|
150
|
-
return handleStreamingExecuteRequest(
|
|
142
|
+
return handleStreamingExecuteRequest(
|
|
143
|
+
req,
|
|
144
|
+
sessionManager,
|
|
145
|
+
corsHeaders
|
|
146
|
+
);
|
|
151
147
|
}
|
|
152
148
|
break;
|
|
153
149
|
|
|
154
150
|
case "/api/ping":
|
|
155
151
|
if (req.method === "GET") {
|
|
156
|
-
const health = await
|
|
152
|
+
const health = await interpreterService.getHealthStatus();
|
|
157
153
|
return new Response(
|
|
158
154
|
JSON.stringify({
|
|
159
155
|
message: "pong",
|
|
160
156
|
timestamp: new Date().toISOString(),
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
? "initializing"
|
|
165
|
-
: "not ready",
|
|
166
|
-
jupyterHealth: health,
|
|
157
|
+
system: "interpreter (70x faster)",
|
|
158
|
+
status: health.ready ? "ready" : "initializing",
|
|
159
|
+
progress: health.progress,
|
|
167
160
|
}),
|
|
168
161
|
{
|
|
169
162
|
headers: {
|
|
@@ -199,6 +192,12 @@ const server = serve({
|
|
|
199
192
|
}
|
|
200
193
|
break;
|
|
201
194
|
|
|
195
|
+
case "/api/read/stream":
|
|
196
|
+
if (req.method === "POST") {
|
|
197
|
+
return handleReadFileStreamRequest(req, corsHeaders, sessionManager);
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
|
|
202
201
|
case "/api/delete":
|
|
203
202
|
if (req.method === "POST") {
|
|
204
203
|
return handleDeleteFileRequest(req, corsHeaders, sessionManager);
|
|
@@ -267,7 +266,7 @@ const server = serve({
|
|
|
267
266
|
if (req.method === "POST") {
|
|
268
267
|
try {
|
|
269
268
|
const body = (await req.json()) as CreateContextRequest;
|
|
270
|
-
const context = await
|
|
269
|
+
const context = await interpreterService.createContext(body);
|
|
271
270
|
return new Response(
|
|
272
271
|
JSON.stringify({
|
|
273
272
|
id: context.id,
|
|
@@ -284,9 +283,9 @@ const server = serve({
|
|
|
284
283
|
}
|
|
285
284
|
);
|
|
286
285
|
} catch (error) {
|
|
287
|
-
if (error instanceof
|
|
286
|
+
if (error instanceof InterpreterNotReadyError) {
|
|
288
287
|
console.log(
|
|
289
|
-
`[Container] Request timed out waiting for
|
|
288
|
+
`[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
|
|
290
289
|
);
|
|
291
290
|
return new Response(
|
|
292
291
|
JSON.stringify({
|
|
@@ -351,7 +350,7 @@ const server = serve({
|
|
|
351
350
|
);
|
|
352
351
|
}
|
|
353
352
|
} else if (req.method === "GET") {
|
|
354
|
-
const contexts = await
|
|
353
|
+
const contexts = await interpreterService.listContexts();
|
|
355
354
|
return new Response(JSON.stringify({ contexts }), {
|
|
356
355
|
headers: {
|
|
357
356
|
"Content-Type": "application/json",
|
|
@@ -369,7 +368,7 @@ const server = serve({
|
|
|
369
368
|
code: string;
|
|
370
369
|
language?: string;
|
|
371
370
|
};
|
|
372
|
-
return await
|
|
371
|
+
return await interpreterService.executeCode(
|
|
373
372
|
body.context_id,
|
|
374
373
|
body.code,
|
|
375
374
|
body.language
|
|
@@ -408,12 +407,12 @@ const server = serve({
|
|
|
408
407
|
error.message.includes("initializing")
|
|
409
408
|
) {
|
|
410
409
|
console.log(
|
|
411
|
-
"[Container] Code execution deferred -
|
|
410
|
+
"[Container] Code execution deferred - service still initializing"
|
|
412
411
|
);
|
|
413
412
|
} else {
|
|
414
413
|
console.error("[Container] Error executing code:", error);
|
|
415
414
|
}
|
|
416
|
-
// Error response is already handled by
|
|
415
|
+
// Error response is already handled by service.executeCode for not ready state
|
|
417
416
|
return new Response(
|
|
418
417
|
JSON.stringify({
|
|
419
418
|
error:
|
|
@@ -442,7 +441,7 @@ const server = serve({
|
|
|
442
441
|
const contextId = pathname.split("/")[3];
|
|
443
442
|
if (req.method === "DELETE") {
|
|
444
443
|
try {
|
|
445
|
-
await
|
|
444
|
+
await interpreterService.deleteContext(contextId);
|
|
446
445
|
return new Response(JSON.stringify({ success: true }), {
|
|
447
446
|
headers: {
|
|
448
447
|
"Content-Type": "application/json",
|
|
@@ -450,9 +449,9 @@ const server = serve({
|
|
|
450
449
|
},
|
|
451
450
|
});
|
|
452
451
|
} catch (error) {
|
|
453
|
-
if (error instanceof
|
|
452
|
+
if (error instanceof InterpreterNotReadyError) {
|
|
454
453
|
console.log(
|
|
455
|
-
`[Container] Request timed out waiting for
|
|
454
|
+
`[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
|
|
456
455
|
);
|
|
457
456
|
return new Response(
|
|
458
457
|
JSON.stringify({
|
|
@@ -578,6 +577,7 @@ console.log(` POST /api/git/checkout - Checkout a git repository`);
|
|
|
578
577
|
console.log(` POST /api/mkdir - Create a directory`);
|
|
579
578
|
console.log(` POST /api/write - Write a file`);
|
|
580
579
|
console.log(` POST /api/read - Read a file`);
|
|
580
|
+
console.log(` POST /api/read/stream - Stream a file (SSE)`);
|
|
581
581
|
console.log(` POST /api/delete - Delete a file`);
|
|
582
582
|
console.log(` POST /api/rename - Rename a file`);
|
|
583
583
|
console.log(` POST /api/move - Move a file`);
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { type InterpreterLanguage, processPool, type RichOutput } from "./runtime/process-pool";
|
|
3
|
+
|
|
4
|
+
export interface CreateContextRequest {
|
|
5
|
+
language?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Context {
|
|
10
|
+
id: string;
|
|
11
|
+
language: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
lastUsed: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HealthStatus {
|
|
18
|
+
ready: boolean;
|
|
19
|
+
initializing: boolean;
|
|
20
|
+
progress: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class InterpreterNotReadyError extends Error {
|
|
24
|
+
progress: number;
|
|
25
|
+
retryAfter: number;
|
|
26
|
+
|
|
27
|
+
constructor(message: string, progress: number = 100, retryAfter: number = 1) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.progress = progress;
|
|
30
|
+
this.retryAfter = retryAfter;
|
|
31
|
+
this.name = "InterpreterNotReadyError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class InterpreterService {
|
|
36
|
+
private contexts: Map<string, Context> = new Map();
|
|
37
|
+
|
|
38
|
+
async getHealthStatus(): Promise<HealthStatus> {
|
|
39
|
+
return {
|
|
40
|
+
ready: true,
|
|
41
|
+
initializing: false,
|
|
42
|
+
progress: 100,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async createContext(request: CreateContextRequest): Promise<Context> {
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
const language = this.mapLanguage(request.language || "python");
|
|
49
|
+
|
|
50
|
+
const context: Context = {
|
|
51
|
+
id,
|
|
52
|
+
language,
|
|
53
|
+
cwd: request.cwd || "/workspace",
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
lastUsed: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.contexts.set(id, context);
|
|
59
|
+
console.log(`[InterpreterService] Created context ${id} for ${language}`);
|
|
60
|
+
|
|
61
|
+
return context;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async listContexts(): Promise<Context[]> {
|
|
65
|
+
return Array.from(this.contexts.values());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async deleteContext(contextId: string): Promise<void> {
|
|
69
|
+
if (!this.contexts.has(contextId)) {
|
|
70
|
+
throw new Error(`Context ${contextId} not found`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.contexts.delete(contextId);
|
|
74
|
+
console.log(`[InterpreterService] Deleted context ${contextId}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async executeCode(
|
|
78
|
+
contextId: string,
|
|
79
|
+
code: string,
|
|
80
|
+
language?: string
|
|
81
|
+
): Promise<Response> {
|
|
82
|
+
const context = this.contexts.get(contextId);
|
|
83
|
+
if (!context) {
|
|
84
|
+
return new Response(
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
error: `Context ${contextId} not found`,
|
|
87
|
+
}),
|
|
88
|
+
{
|
|
89
|
+
status: 404,
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
context.lastUsed = new Date().toISOString();
|
|
96
|
+
|
|
97
|
+
const execLanguage = this.mapLanguage(language || context.language);
|
|
98
|
+
|
|
99
|
+
// Store reference to this for use in async function
|
|
100
|
+
const self = this;
|
|
101
|
+
|
|
102
|
+
const stream = new ReadableStream({
|
|
103
|
+
async start(controller) {
|
|
104
|
+
const encoder = new TextEncoder();
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await processPool.execute(
|
|
109
|
+
execLanguage,
|
|
110
|
+
code,
|
|
111
|
+
contextId,
|
|
112
|
+
30000
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const totalTime = Date.now() - startTime;
|
|
116
|
+
console.log(`[InterpreterService] Code execution completed in ${totalTime}ms`);
|
|
117
|
+
|
|
118
|
+
if (result.stdout) {
|
|
119
|
+
controller.enqueue(
|
|
120
|
+
encoder.encode(
|
|
121
|
+
`${JSON.stringify({
|
|
122
|
+
type: "stdout",
|
|
123
|
+
text: result.stdout,
|
|
124
|
+
})}\n`
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (result.stderr) {
|
|
130
|
+
controller.enqueue(
|
|
131
|
+
encoder.encode(
|
|
132
|
+
`${JSON.stringify({
|
|
133
|
+
type: "stderr",
|
|
134
|
+
text: result.stderr,
|
|
135
|
+
})}\n`
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result.outputs && result.outputs.length > 0) {
|
|
141
|
+
for (const output of result.outputs) {
|
|
142
|
+
const outputData = self.formatOutputData(output);
|
|
143
|
+
controller.enqueue(
|
|
144
|
+
encoder.encode(
|
|
145
|
+
`${JSON.stringify({
|
|
146
|
+
type: "result",
|
|
147
|
+
...outputData,
|
|
148
|
+
metadata: output.metadata || {},
|
|
149
|
+
})}\n`
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (result.success) {
|
|
156
|
+
controller.enqueue(
|
|
157
|
+
encoder.encode(
|
|
158
|
+
`${JSON.stringify({
|
|
159
|
+
type: "execution_complete",
|
|
160
|
+
execution_count: 1,
|
|
161
|
+
})}\n`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
} else if (result.error) {
|
|
165
|
+
controller.enqueue(
|
|
166
|
+
encoder.encode(
|
|
167
|
+
`${JSON.stringify({
|
|
168
|
+
type: "error",
|
|
169
|
+
ename: result.error.type || "ExecutionError",
|
|
170
|
+
evalue: result.error.message || "Code execution failed",
|
|
171
|
+
traceback: result.error.traceback ? result.error.traceback.split('\n') : [],
|
|
172
|
+
})}\n`
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
controller.enqueue(
|
|
177
|
+
encoder.encode(
|
|
178
|
+
`${JSON.stringify({
|
|
179
|
+
type: "error",
|
|
180
|
+
ename: "ExecutionError",
|
|
181
|
+
evalue: result.stderr || "Code execution failed",
|
|
182
|
+
traceback: [],
|
|
183
|
+
})}\n`
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
controller.close();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`[InterpreterService] Code execution failed:`, error);
|
|
191
|
+
|
|
192
|
+
controller.enqueue(
|
|
193
|
+
encoder.encode(
|
|
194
|
+
`${JSON.stringify({
|
|
195
|
+
type: "error",
|
|
196
|
+
ename: "InternalError",
|
|
197
|
+
evalue: error instanceof Error ? error.message : String(error),
|
|
198
|
+
traceback: [],
|
|
199
|
+
})}\n`
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
controller.close();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return new Response(stream, {
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "text/event-stream",
|
|
211
|
+
"Cache-Control": "no-cache",
|
|
212
|
+
Connection: "keep-alive",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private mapLanguage(language: string): InterpreterLanguage {
|
|
218
|
+
const normalized = language.toLowerCase();
|
|
219
|
+
|
|
220
|
+
switch (normalized) {
|
|
221
|
+
case "python":
|
|
222
|
+
case "python3":
|
|
223
|
+
return "python";
|
|
224
|
+
case "javascript":
|
|
225
|
+
case "js":
|
|
226
|
+
case "node":
|
|
227
|
+
return "javascript";
|
|
228
|
+
case "typescript":
|
|
229
|
+
case "ts":
|
|
230
|
+
return "typescript";
|
|
231
|
+
default:
|
|
232
|
+
console.warn(
|
|
233
|
+
`[InterpreterService] Unknown language ${language}, defaulting to python`
|
|
234
|
+
);
|
|
235
|
+
return "python";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private formatOutputData(output: RichOutput): Record<string, unknown> {
|
|
240
|
+
const result: Record<string, unknown> = {};
|
|
241
|
+
|
|
242
|
+
switch (output.type) {
|
|
243
|
+
case "image":
|
|
244
|
+
result.png = output.data;
|
|
245
|
+
break;
|
|
246
|
+
case "jpeg":
|
|
247
|
+
result.jpeg = output.data;
|
|
248
|
+
break;
|
|
249
|
+
case "svg":
|
|
250
|
+
result.svg = output.data;
|
|
251
|
+
break;
|
|
252
|
+
case "html":
|
|
253
|
+
result.html = output.data;
|
|
254
|
+
break;
|
|
255
|
+
case "json":
|
|
256
|
+
result.json = typeof output.data === 'string' ? JSON.parse(output.data) : output.data;
|
|
257
|
+
break;
|
|
258
|
+
case "latex":
|
|
259
|
+
result.latex = output.data;
|
|
260
|
+
break;
|
|
261
|
+
case "markdown":
|
|
262
|
+
result.markdown = output.data;
|
|
263
|
+
break;
|
|
264
|
+
case "javascript":
|
|
265
|
+
result.javascript = output.data;
|
|
266
|
+
break;
|
|
267
|
+
case "text":
|
|
268
|
+
result.text = output.data;
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
result.text = output.data || '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
}
|