@cloudflare/sandbox 0.0.0-c87db11 → 0.0.0-cdb8197

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/Dockerfile +32 -29
  3. package/README.md +127 -12
  4. package/container_src/bun.lock +31 -77
  5. package/container_src/control-process.ts +784 -0
  6. package/container_src/handler/exec.ts +99 -254
  7. package/container_src/handler/file.ts +253 -640
  8. package/container_src/handler/git.ts +28 -80
  9. package/container_src/handler/process.ts +443 -515
  10. package/container_src/handler/session.ts +92 -0
  11. package/container_src/index.ts +108 -163
  12. package/container_src/interpreter-service.ts +276 -0
  13. package/container_src/isolation.ts +1213 -0
  14. package/container_src/mime-processor.ts +1 -1
  15. package/container_src/package.json +4 -4
  16. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  17. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  18. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  19. package/container_src/runtime/process-pool.ts +464 -0
  20. package/container_src/shell-escape.ts +42 -0
  21. package/container_src/startup.sh +6 -79
  22. package/container_src/types.ts +35 -12
  23. package/package.json +2 -2
  24. package/src/client.ts +214 -187
  25. package/src/errors.ts +15 -14
  26. package/src/file-stream.ts +162 -0
  27. package/src/index.ts +43 -16
  28. package/src/{jupyter-client.ts → interpreter-client.ts} +6 -3
  29. package/src/interpreter-types.ts +102 -95
  30. package/src/interpreter.ts +8 -8
  31. package/src/sandbox.ts +314 -336
  32. package/src/types.ts +194 -24
  33. package/container_src/jupyter-server.ts +0 -579
  34. package/container_src/jupyter-service.ts +0 -458
  35. package/container_src/jupyter_config.py +0 -48
@@ -0,0 +1,92 @@
1
+ import type { SessionManager } from "../isolation";
2
+ import type { CreateSessionRequest } from "../types";
3
+
4
+ export async function handleCreateSession(
5
+ req: Request,
6
+ corsHeaders: Record<string, string>,
7
+ sessionManager: SessionManager
8
+ ) {
9
+ try {
10
+ const body = (await req.json()) as CreateSessionRequest;
11
+ const { id, env, cwd, isolation } = body;
12
+
13
+ if (!id) {
14
+ return new Response(
15
+ JSON.stringify({ error: "Session ID is required" }),
16
+ {
17
+ status: 400,
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ ...corsHeaders,
21
+ },
22
+ }
23
+ );
24
+ }
25
+
26
+ await sessionManager.createSession({
27
+ id,
28
+ env: env || {},
29
+ cwd: cwd || "/workspace",
30
+ isolation: isolation !== false,
31
+ });
32
+
33
+ console.log(`[Container] Session '${id}' created successfully`);
34
+ console.log(
35
+ `[Container] Available sessions now: ${sessionManager
36
+ .listSessions()
37
+ .join(", ")}`
38
+ );
39
+
40
+ return new Response(
41
+ JSON.stringify({
42
+ success: true,
43
+ id,
44
+ message: `Session '${id}' created with${
45
+ isolation !== false ? "" : "out"
46
+ } isolation`,
47
+ }),
48
+ {
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ ...corsHeaders,
52
+ },
53
+ }
54
+ );
55
+ } catch (error) {
56
+ console.error("[Container] Failed to create session:", error);
57
+ return new Response(
58
+ JSON.stringify({
59
+ error: "Failed to create session",
60
+ message:
61
+ error instanceof Error ? error.message : String(error),
62
+ }),
63
+ {
64
+ status: 500,
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ ...corsHeaders,
68
+ },
69
+ }
70
+ );
71
+ }
72
+ }
73
+
74
+ export function handleListSessions(
75
+ corsHeaders: Record<string, string>,
76
+ sessionManager: SessionManager
77
+ ) {
78
+ const sessionList = sessionManager.listSessions();
79
+ return new Response(
80
+ JSON.stringify({
81
+ count: sessionList.length,
82
+ sessions: sessionList,
83
+ timestamp: new Date().toISOString(),
84
+ }),
85
+ {
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ ...corsHeaders,
89
+ },
90
+ }
91
+ );
92
+ }
@@ -1,4 +1,3 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { serve } from "bun";
3
2
  import {
4
3
  handleExecuteRequest,
@@ -6,9 +5,11 @@ import {
6
5
  } from "./handler/exec";
7
6
  import {
8
7
  handleDeleteFileRequest,
8
+ handleListFilesRequest,
9
9
  handleMkdirRequest,
10
10
  handleMoveFileRequest,
11
11
  handleReadFileRequest,
12
+ handleReadFileStreamRequest,
12
13
  handleRenameFileRequest,
13
14
  handleWriteFileRequest,
14
15
  } from "./handler/file";
@@ -28,62 +29,65 @@ import {
28
29
  handleStartProcessRequest,
29
30
  handleStreamProcessLogsRequest,
30
31
  } from "./handler/process";
31
- import type { CreateContextRequest } from "./jupyter-server";
32
- import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
33
- import type { ProcessRecord, SessionData } from "./types";
34
-
35
- // In-memory session storage (in production, you'd want to use a proper database)
36
- const sessions = new Map<string, SessionData>();
32
+ import { handleCreateSession, handleListSessions } from "./handler/session";
33
+ import type { CreateContextRequest } from "./interpreter-service";
34
+ import {
35
+ InterpreterNotReadyError,
36
+ InterpreterService,
37
+ } from "./interpreter-service";
38
+ import { hasNamespaceSupport, SessionManager } from "./isolation";
37
39
 
38
40
  // In-memory storage for exposed ports
39
41
  const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
40
42
 
41
- // In-memory process storage - cleared on container restart
42
- const processes = new Map<string, ProcessRecord>();
43
-
44
- // Generate a unique session ID using cryptographically secure randomness
45
- function generateSessionId(): string {
46
- return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
47
- }
48
-
49
- // Clean up old sessions (older than 1 hour)
50
- function cleanupOldSessions() {
51
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
52
- for (const [sessionId, session] of sessions.entries()) {
53
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
54
- sessions.delete(sessionId);
55
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
56
- }
57
- }
58
- }
43
+ // Check isolation capabilities on startup
44
+ const isolationAvailable = hasNamespaceSupport();
45
+ console.log(
46
+ `[Container] Process isolation: ${
47
+ isolationAvailable
48
+ ? "ENABLED (production mode)"
49
+ : "DISABLED (development mode)"
50
+ }`
51
+ );
59
52
 
60
- // Run cleanup every 10 minutes
61
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
53
+ // Session manager for secure execution with isolation
54
+ const sessionManager = new SessionManager();
62
55
 
63
- // Initialize Jupyter service with graceful degradation
64
- const jupyterService = new JupyterService();
56
+ // Graceful shutdown handler
57
+ const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
65
58
 
66
- // Start Jupyter initialization in background (non-blocking)
67
- console.log("[Container] Starting Jupyter initialization in background...");
68
- console.log(
69
- "[Container] API endpoints are available immediately. Jupyter-dependent features will be available shortly."
70
- );
59
+ process.on("SIGTERM", async () => {
60
+ console.log("[Container] SIGTERM received, cleaning up sessions...");
61
+ await sessionManager.destroyAll();
62
+ setTimeout(() => {
63
+ process.exit(0);
64
+ }, SHUTDOWN_GRACE_PERIOD_MS);
65
+ });
71
66
 
72
- jupyterService
73
- .initialize()
74
- .then(() => {
75
- console.log(
76
- "[Container] Jupyter fully initialized - all features available"
77
- );
78
- })
79
- .catch((error) => {
80
- console.error("[Container] Jupyter initialization failed:", error.message);
81
- console.error(
82
- "[Container] The API will continue in degraded mode without code execution capabilities"
83
- );
84
- });
67
+ process.on("SIGINT", async () => {
68
+ console.log("[Container] SIGINT received, cleaning up sessions...");
69
+ await sessionManager.destroyAll();
70
+ setTimeout(() => {
71
+ process.exit(0);
72
+ }, SHUTDOWN_GRACE_PERIOD_MS);
73
+ });
74
+
75
+ // Cleanup on uncaught exceptions (log but still exit)
76
+ process.on("uncaughtException", async (error) => {
77
+ console.error("[Container] Uncaught exception:", error);
78
+ await sessionManager.destroyAll();
79
+ process.exit(1);
80
+ });
81
+
82
+ // Initialize interpreter service
83
+ const interpreterService = new InterpreterService();
84
+
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");
85
88
 
86
89
  const server = serve({
90
+ idleTimeout: 255,
87
91
  async fetch(req: Request) {
88
92
  const url = new URL(req.url);
89
93
  const pathname = url.pathname;
@@ -117,115 +121,42 @@ const server = serve({
117
121
 
118
122
  case "/api/session/create":
119
123
  if (req.method === "POST") {
120
- const sessionId = generateSessionId();
121
- const sessionData: SessionData = {
122
- activeProcess: null,
123
- createdAt: new Date(),
124
- sessionId,
125
- };
126
- sessions.set(sessionId, sessionData);
127
-
128
- console.log(`[Server] Created new session: ${sessionId}`);
129
-
130
- return new Response(
131
- JSON.stringify({
132
- message: "Session created successfully",
133
- sessionId,
134
- timestamp: new Date().toISOString(),
135
- }),
136
- {
137
- headers: {
138
- "Content-Type": "application/json",
139
- ...corsHeaders,
140
- },
141
- }
142
- );
124
+ return handleCreateSession(req, corsHeaders, sessionManager);
143
125
  }
144
126
  break;
145
127
 
146
128
  case "/api/session/list":
147
129
  if (req.method === "GET") {
148
- const sessionList = Array.from(sessions.values()).map(
149
- (session) => ({
150
- createdAt: session.createdAt.toISOString(),
151
- hasActiveProcess: !!session.activeProcess,
152
- sessionId: session.sessionId,
153
- })
154
- );
155
-
156
- return new Response(
157
- JSON.stringify({
158
- count: sessionList.length,
159
- sessions: sessionList,
160
- timestamp: new Date().toISOString(),
161
- }),
162
- {
163
- headers: {
164
- "Content-Type": "application/json",
165
- ...corsHeaders,
166
- },
167
- }
168
- );
130
+ return handleListSessions(corsHeaders, sessionManager);
169
131
  }
170
132
  break;
171
133
 
172
134
  case "/api/execute":
173
135
  if (req.method === "POST") {
174
- return handleExecuteRequest(sessions, req, corsHeaders);
136
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
175
137
  }
176
138
  break;
177
139
 
178
140
  case "/api/execute/stream":
179
141
  if (req.method === "POST") {
180
- return handleStreamingExecuteRequest(sessions, req, corsHeaders);
142
+ return handleStreamingExecuteRequest(
143
+ req,
144
+ sessionManager,
145
+ corsHeaders
146
+ );
181
147
  }
182
148
  break;
183
149
 
184
150
  case "/api/ping":
185
151
  if (req.method === "GET") {
186
- const health = await jupyterService.getHealthStatus();
152
+ const health = await interpreterService.getHealthStatus();
187
153
  return new Response(
188
154
  JSON.stringify({
189
155
  message: "pong",
190
156
  timestamp: new Date().toISOString(),
191
- jupyter: health.ready
192
- ? "ready"
193
- : health.initializing
194
- ? "initializing"
195
- : "not ready",
196
- jupyterHealth: health,
197
- }),
198
- {
199
- headers: {
200
- "Content-Type": "application/json",
201
- ...corsHeaders,
202
- },
203
- }
204
- );
205
- }
206
- break;
207
-
208
- case "/api/commands":
209
- if (req.method === "GET") {
210
- return new Response(
211
- JSON.stringify({
212
- availableCommands: [
213
- "ls",
214
- "pwd",
215
- "echo",
216
- "cat",
217
- "grep",
218
- "find",
219
- "whoami",
220
- "date",
221
- "uptime",
222
- "ps",
223
- "top",
224
- "df",
225
- "du",
226
- "free",
227
- ],
228
- timestamp: new Date().toISOString(),
157
+ system: "interpreter (70x faster)",
158
+ status: health.ready ? "ready" : "initializing",
159
+ progress: health.progress,
229
160
  }),
230
161
  {
231
162
  headers: {
@@ -239,43 +170,55 @@ const server = serve({
239
170
 
240
171
  case "/api/git/checkout":
241
172
  if (req.method === "POST") {
242
- return handleGitCheckoutRequest(sessions, req, corsHeaders);
173
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
243
174
  }
244
175
  break;
245
176
 
246
177
  case "/api/mkdir":
247
178
  if (req.method === "POST") {
248
- return handleMkdirRequest(sessions, req, corsHeaders);
179
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
249
180
  }
250
181
  break;
251
182
 
252
183
  case "/api/write":
253
184
  if (req.method === "POST") {
254
- return handleWriteFileRequest(req, corsHeaders);
185
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
255
186
  }
256
187
  break;
257
188
 
258
189
  case "/api/read":
259
190
  if (req.method === "POST") {
260
- return handleReadFileRequest(req, corsHeaders);
191
+ return handleReadFileRequest(req, corsHeaders, sessionManager);
192
+ }
193
+ break;
194
+
195
+ case "/api/read/stream":
196
+ if (req.method === "POST") {
197
+ return handleReadFileStreamRequest(req, corsHeaders, sessionManager);
261
198
  }
262
199
  break;
263
200
 
264
201
  case "/api/delete":
265
202
  if (req.method === "POST") {
266
- return handleDeleteFileRequest(req, corsHeaders);
203
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
267
204
  }
268
205
  break;
269
206
 
270
207
  case "/api/rename":
271
208
  if (req.method === "POST") {
272
- return handleRenameFileRequest(req, corsHeaders);
209
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
273
210
  }
274
211
  break;
275
212
 
276
213
  case "/api/move":
277
214
  if (req.method === "POST") {
278
- return handleMoveFileRequest(req, corsHeaders);
215
+ return handleMoveFileRequest(req, corsHeaders, sessionManager);
216
+ }
217
+ break;
218
+
219
+ case "/api/list-files":
220
+ if (req.method === "POST") {
221
+ return handleListFilesRequest(req, corsHeaders, sessionManager);
279
222
  }
280
223
  break;
281
224
 
@@ -299,28 +242,31 @@ const server = serve({
299
242
 
300
243
  case "/api/process/start":
301
244
  if (req.method === "POST") {
302
- return handleStartProcessRequest(processes, req, corsHeaders);
245
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
303
246
  }
304
247
  break;
305
248
 
306
249
  case "/api/process/list":
307
250
  if (req.method === "GET") {
308
- return handleListProcessesRequest(processes, req, corsHeaders);
251
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
309
252
  }
310
253
  break;
311
254
 
312
255
  case "/api/process/kill-all":
313
256
  if (req.method === "DELETE") {
314
- return handleKillAllProcessesRequest(processes, req, corsHeaders);
257
+ return handleKillAllProcessesRequest(
258
+ req,
259
+ corsHeaders,
260
+ sessionManager
261
+ );
315
262
  }
316
263
  break;
317
264
 
318
- // Code interpreter endpoints
319
265
  case "/api/contexts":
320
266
  if (req.method === "POST") {
321
267
  try {
322
268
  const body = (await req.json()) as CreateContextRequest;
323
- const context = await jupyterService.createContext(body);
269
+ const context = await interpreterService.createContext(body);
324
270
  return new Response(
325
271
  JSON.stringify({
326
272
  id: context.id,
@@ -337,10 +283,9 @@ const server = serve({
337
283
  }
338
284
  );
339
285
  } catch (error) {
340
- if (error instanceof JupyterNotReadyError) {
341
- // This happens when request times out waiting for Jupyter
286
+ if (error instanceof InterpreterNotReadyError) {
342
287
  console.log(
343
- `[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
288
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
344
289
  );
345
290
  return new Response(
346
291
  JSON.stringify({
@@ -405,7 +350,7 @@ const server = serve({
405
350
  );
406
351
  }
407
352
  } else if (req.method === "GET") {
408
- const contexts = await jupyterService.listContexts();
353
+ const contexts = await interpreterService.listContexts();
409
354
  return new Response(JSON.stringify({ contexts }), {
410
355
  headers: {
411
356
  "Content-Type": "application/json",
@@ -423,7 +368,7 @@ const server = serve({
423
368
  code: string;
424
369
  language?: string;
425
370
  };
426
- return await jupyterService.executeCode(
371
+ return await interpreterService.executeCode(
427
372
  body.context_id,
428
373
  body.code,
429
374
  body.language
@@ -462,12 +407,12 @@ const server = serve({
462
407
  error.message.includes("initializing")
463
408
  ) {
464
409
  console.log(
465
- "[Container] Code execution deferred - Jupyter still initializing"
410
+ "[Container] Code execution deferred - service still initializing"
466
411
  );
467
412
  } else {
468
413
  console.error("[Container] Error executing code:", error);
469
414
  }
470
- // Error response is already handled by jupyterService.executeCode for not ready state
415
+ // Error response is already handled by service.executeCode for not ready state
471
416
  return new Response(
472
417
  JSON.stringify({
473
418
  error:
@@ -496,7 +441,7 @@ const server = serve({
496
441
  const contextId = pathname.split("/")[3];
497
442
  if (req.method === "DELETE") {
498
443
  try {
499
- await jupyterService.deleteContext(contextId);
444
+ await interpreterService.deleteContext(contextId);
500
445
  return new Response(JSON.stringify({ success: true }), {
501
446
  headers: {
502
447
  "Content-Type": "application/json",
@@ -504,9 +449,9 @@ const server = serve({
504
449
  },
505
450
  });
506
451
  } catch (error) {
507
- if (error instanceof JupyterNotReadyError) {
452
+ if (error instanceof InterpreterNotReadyError) {
508
453
  console.log(
509
- `[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
454
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
510
455
  );
511
456
  return new Response(
512
457
  JSON.stringify({
@@ -556,31 +501,31 @@ const server = serve({
556
501
 
557
502
  if (!action && req.method === "GET") {
558
503
  return handleGetProcessRequest(
559
- processes,
560
504
  req,
561
505
  corsHeaders,
562
- processId
506
+ processId,
507
+ sessionManager
563
508
  );
564
509
  } else if (!action && req.method === "DELETE") {
565
510
  return handleKillProcessRequest(
566
- processes,
567
511
  req,
568
512
  corsHeaders,
569
- processId
513
+ processId,
514
+ sessionManager
570
515
  );
571
516
  } else if (action === "logs" && req.method === "GET") {
572
517
  return handleGetProcessLogsRequest(
573
- processes,
574
518
  req,
575
519
  corsHeaders,
576
- processId
520
+ processId,
521
+ sessionManager
577
522
  );
578
523
  } else if (action === "stream" && req.method === "GET") {
579
524
  return handleStreamProcessLogsRequest(
580
- processes,
581
525
  req,
582
526
  corsHeaders,
583
- processId
527
+ processId,
528
+ sessionManager
584
529
  );
585
530
  }
586
531
  }
@@ -632,6 +577,7 @@ console.log(` POST /api/git/checkout - Checkout a git repository`);
632
577
  console.log(` POST /api/mkdir - Create a directory`);
633
578
  console.log(` POST /api/write - Write a file`);
634
579
  console.log(` POST /api/read - Read a file`);
580
+ console.log(` POST /api/read/stream - Stream a file (SSE)`);
635
581
  console.log(` POST /api/delete - Delete a file`);
636
582
  console.log(` POST /api/rename - Rename a file`);
637
583
  console.log(` POST /api/move - Move a file`);
@@ -653,4 +599,3 @@ console.log(
653
599
  ` POST /api/execute/code - Execute code in a context (streaming)`
654
600
  );
655
601
  console.log(` GET /api/ping - Health check`);
656
- console.log(` GET /api/commands - List available commands`);