@cloudflare/sandbox 0.0.0-d81d2a5 → 0.0.0-d86b60e

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.
@@ -1,8 +1,11 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { serve } from "bun";
3
- import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
2
+ import {
3
+ handleExecuteRequest,
4
+ handleStreamingExecuteRequest,
5
+ } from "./handler/exec";
4
6
  import {
5
7
  handleDeleteFileRequest,
8
+ handleListFilesRequest,
6
9
  handleMkdirRequest,
7
10
  handleMoveFileRequest,
8
11
  handleReadFileRequest,
@@ -25,56 +28,62 @@ import {
25
28
  handleStartProcessRequest,
26
29
  handleStreamProcessLogsRequest,
27
30
  } from "./handler/process";
28
- import { type CreateContextRequest, JupyterServer } from "./jupyter-server";
29
- import type { ProcessRecord, SessionData } from "./types";
30
-
31
- // In-memory session storage (in production, you'd want to use a proper database)
32
- const sessions = new Map<string, SessionData>();
31
+ import { handleCreateSession, handleListSessions } from "./handler/session";
32
+ import type { CreateContextRequest } from "./interpreter-service";
33
+ import {
34
+ InterpreterNotReadyError,
35
+ InterpreterService,
36
+ } from "./interpreter-service";
37
+ import { hasNamespaceSupport, SessionManager } from "./isolation";
33
38
 
34
39
  // In-memory storage for exposed ports
35
40
  const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
36
41
 
37
- // In-memory process storage - cleared on container restart
38
- const processes = new Map<string, ProcessRecord>();
39
-
40
- // Generate a unique session ID using cryptographically secure randomness
41
- function generateSessionId(): string {
42
- return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
43
- }
44
-
45
- // Clean up old sessions (older than 1 hour)
46
- function cleanupOldSessions() {
47
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
48
- for (const [sessionId, session] of sessions.entries()) {
49
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
50
- sessions.delete(sessionId);
51
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
52
- }
53
- }
54
- }
55
-
56
- // Run cleanup every 10 minutes
57
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
58
-
59
- // Initialize Jupyter server
60
- const jupyterServer = new JupyterServer();
61
- let jupyterInitialized = false;
62
-
63
- // Initialize Jupyter immediately since startup.sh ensures it's ready
64
- (async () => {
65
- try {
66
- await jupyterServer.initialize();
67
- jupyterInitialized = true;
68
- console.log("[Container] Jupyter integration initialized successfully");
69
- } catch (error) {
70
- console.error("[Container] Failed to initialize Jupyter:", error);
71
- // Log more details to help debug
72
- if (error instanceof Error) {
73
- console.error("[Container] Error details:", error.message);
74
- console.error("[Container] Stack trace:", error.stack);
75
- }
76
- }
77
- })();
42
+ // Check isolation capabilities on startup
43
+ const isolationAvailable = hasNamespaceSupport();
44
+ console.log(
45
+ `[Container] Process isolation: ${
46
+ isolationAvailable
47
+ ? "ENABLED (production mode)"
48
+ : "DISABLED (development mode)"
49
+ }`
50
+ );
51
+
52
+ // Session manager for secure execution with isolation
53
+ const sessionManager = new SessionManager();
54
+
55
+ // Graceful shutdown handler
56
+ const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
57
+
58
+ process.on("SIGTERM", async () => {
59
+ console.log("[Container] SIGTERM received, cleaning up sessions...");
60
+ await sessionManager.destroyAll();
61
+ setTimeout(() => {
62
+ process.exit(0);
63
+ }, SHUTDOWN_GRACE_PERIOD_MS);
64
+ });
65
+
66
+ process.on("SIGINT", async () => {
67
+ console.log("[Container] SIGINT received, cleaning up sessions...");
68
+ await sessionManager.destroyAll();
69
+ setTimeout(() => {
70
+ process.exit(0);
71
+ }, SHUTDOWN_GRACE_PERIOD_MS);
72
+ });
73
+
74
+ // Cleanup on uncaught exceptions (log but still exit)
75
+ process.on("uncaughtException", async (error) => {
76
+ console.error("[Container] Uncaught exception:", error);
77
+ await sessionManager.destroyAll();
78
+ process.exit(1);
79
+ });
80
+
81
+ // Initialize interpreter service
82
+ const interpreterService = new InterpreterService();
83
+
84
+ // No initialization needed - service is ready immediately!
85
+ console.log("[Container] Interpreter service ready - no cold start!");
86
+ console.log("[Container] All API endpoints available immediately");
78
87
 
79
88
  const server = serve({
80
89
  async fetch(req: Request) {
@@ -110,109 +119,42 @@ const server = serve({
110
119
 
111
120
  case "/api/session/create":
112
121
  if (req.method === "POST") {
113
- const sessionId = generateSessionId();
114
- const sessionData: SessionData = {
115
- activeProcess: null,
116
- createdAt: new Date(),
117
- sessionId,
118
- };
119
- sessions.set(sessionId, sessionData);
120
-
121
- console.log(`[Server] Created new session: ${sessionId}`);
122
-
123
- return new Response(
124
- JSON.stringify({
125
- message: "Session created successfully",
126
- sessionId,
127
- timestamp: new Date().toISOString(),
128
- }),
129
- {
130
- headers: {
131
- "Content-Type": "application/json",
132
- ...corsHeaders,
133
- },
134
- }
135
- );
122
+ return handleCreateSession(req, corsHeaders, sessionManager);
136
123
  }
137
124
  break;
138
125
 
139
126
  case "/api/session/list":
140
127
  if (req.method === "GET") {
141
- const sessionList = Array.from(sessions.values()).map(
142
- (session) => ({
143
- createdAt: session.createdAt.toISOString(),
144
- hasActiveProcess: !!session.activeProcess,
145
- sessionId: session.sessionId,
146
- })
147
- );
148
-
149
- return new Response(
150
- JSON.stringify({
151
- count: sessionList.length,
152
- sessions: sessionList,
153
- timestamp: new Date().toISOString(),
154
- }),
155
- {
156
- headers: {
157
- "Content-Type": "application/json",
158
- ...corsHeaders,
159
- },
160
- }
161
- );
128
+ return handleListSessions(corsHeaders, sessionManager);
162
129
  }
163
130
  break;
164
131
 
165
132
  case "/api/execute":
166
133
  if (req.method === "POST") {
167
- return handleExecuteRequest(sessions, req, corsHeaders);
134
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
168
135
  }
169
136
  break;
170
137
 
171
138
  case "/api/execute/stream":
172
139
  if (req.method === "POST") {
173
- return handleStreamingExecuteRequest(sessions, req, corsHeaders);
140
+ return handleStreamingExecuteRequest(
141
+ req,
142
+ sessionManager,
143
+ corsHeaders
144
+ );
174
145
  }
175
146
  break;
176
147
 
177
148
  case "/api/ping":
178
149
  if (req.method === "GET") {
150
+ const health = await interpreterService.getHealthStatus();
179
151
  return new Response(
180
152
  JSON.stringify({
181
153
  message: "pong",
182
154
  timestamp: new Date().toISOString(),
183
- jupyter: jupyterInitialized ? "ready" : "not ready",
184
- }),
185
- {
186
- headers: {
187
- "Content-Type": "application/json",
188
- ...corsHeaders,
189
- },
190
- }
191
- );
192
- }
193
- break;
194
-
195
- case "/api/commands":
196
- if (req.method === "GET") {
197
- return new Response(
198
- JSON.stringify({
199
- availableCommands: [
200
- "ls",
201
- "pwd",
202
- "echo",
203
- "cat",
204
- "grep",
205
- "find",
206
- "whoami",
207
- "date",
208
- "uptime",
209
- "ps",
210
- "top",
211
- "df",
212
- "du",
213
- "free",
214
- ],
215
- timestamp: new Date().toISOString(),
155
+ system: "interpreter (70x faster)",
156
+ status: health.ready ? "ready" : "initializing",
157
+ progress: health.progress,
216
158
  }),
217
159
  {
218
160
  headers: {
@@ -226,43 +168,49 @@ const server = serve({
226
168
 
227
169
  case "/api/git/checkout":
228
170
  if (req.method === "POST") {
229
- return handleGitCheckoutRequest(sessions, req, corsHeaders);
171
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
230
172
  }
231
173
  break;
232
174
 
233
175
  case "/api/mkdir":
234
176
  if (req.method === "POST") {
235
- return handleMkdirRequest(sessions, req, corsHeaders);
177
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
236
178
  }
237
179
  break;
238
180
 
239
181
  case "/api/write":
240
182
  if (req.method === "POST") {
241
- return handleWriteFileRequest(req, corsHeaders);
183
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
242
184
  }
243
185
  break;
244
186
 
245
187
  case "/api/read":
246
188
  if (req.method === "POST") {
247
- return handleReadFileRequest(req, corsHeaders);
189
+ return handleReadFileRequest(req, corsHeaders, sessionManager);
248
190
  }
249
191
  break;
250
192
 
251
193
  case "/api/delete":
252
194
  if (req.method === "POST") {
253
- return handleDeleteFileRequest(req, corsHeaders);
195
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
254
196
  }
255
197
  break;
256
198
 
257
199
  case "/api/rename":
258
200
  if (req.method === "POST") {
259
- return handleRenameFileRequest(req, corsHeaders);
201
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
260
202
  }
261
203
  break;
262
204
 
263
205
  case "/api/move":
264
206
  if (req.method === "POST") {
265
- return handleMoveFileRequest(req, corsHeaders);
207
+ return handleMoveFileRequest(req, corsHeaders, sessionManager);
208
+ }
209
+ break;
210
+
211
+ case "/api/list-files":
212
+ if (req.method === "POST") {
213
+ return handleListFilesRequest(req, corsHeaders, sessionManager);
266
214
  }
267
215
  break;
268
216
 
@@ -286,49 +234,38 @@ const server = serve({
286
234
 
287
235
  case "/api/process/start":
288
236
  if (req.method === "POST") {
289
- return handleStartProcessRequest(processes, req, corsHeaders);
237
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
290
238
  }
291
239
  break;
292
240
 
293
241
  case "/api/process/list":
294
242
  if (req.method === "GET") {
295
- return handleListProcessesRequest(processes, req, corsHeaders);
243
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
296
244
  }
297
245
  break;
298
246
 
299
247
  case "/api/process/kill-all":
300
248
  if (req.method === "DELETE") {
301
- return handleKillAllProcessesRequest(processes, req, corsHeaders);
249
+ return handleKillAllProcessesRequest(
250
+ req,
251
+ corsHeaders,
252
+ sessionManager
253
+ );
302
254
  }
303
255
  break;
304
256
 
305
- // Code interpreter endpoints
306
257
  case "/api/contexts":
307
258
  if (req.method === "POST") {
308
- if (!jupyterInitialized) {
309
- return new Response(
310
- JSON.stringify({
311
- error: "Jupyter server is not ready. Please try again in a moment."
312
- }),
313
- {
314
- status: 503,
315
- headers: {
316
- "Content-Type": "application/json",
317
- ...corsHeaders,
318
- },
319
- }
320
- );
321
- }
322
259
  try {
323
- const body = await req.json() as CreateContextRequest;
324
- const context = await jupyterServer.createContext(body);
260
+ const body = (await req.json()) as CreateContextRequest;
261
+ const context = await interpreterService.createContext(body);
325
262
  return new Response(
326
263
  JSON.stringify({
327
264
  id: context.id,
328
265
  language: context.language,
329
266
  cwd: context.cwd,
330
267
  createdAt: context.createdAt,
331
- lastUsed: context.lastUsed
268
+ lastUsed: context.lastUsed,
332
269
  }),
333
270
  {
334
271
  headers: {
@@ -338,10 +275,62 @@ const server = serve({
338
275
  }
339
276
  );
340
277
  } catch (error) {
278
+ if (error instanceof InterpreterNotReadyError) {
279
+ console.log(
280
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
281
+ );
282
+ return new Response(
283
+ JSON.stringify({
284
+ error: error.message,
285
+ status: "initializing",
286
+ progress: error.progress,
287
+ }),
288
+ {
289
+ status: 503,
290
+ headers: {
291
+ "Content-Type": "application/json",
292
+ "Retry-After": String(error.retryAfter),
293
+ ...corsHeaders,
294
+ },
295
+ }
296
+ );
297
+ }
298
+
299
+ // Check if it's a circuit breaker error
300
+ if (
301
+ error instanceof Error &&
302
+ error.message.includes("Circuit breaker is open")
303
+ ) {
304
+ console.log(
305
+ "[Container] Circuit breaker is open:",
306
+ error.message
307
+ );
308
+ return new Response(
309
+ JSON.stringify({
310
+ error:
311
+ "Service temporarily unavailable due to high error rate. Please try again later.",
312
+ status: "circuit_open",
313
+ details: error.message,
314
+ }),
315
+ {
316
+ status: 503,
317
+ headers: {
318
+ "Content-Type": "application/json",
319
+ "Retry-After": "60",
320
+ ...corsHeaders,
321
+ },
322
+ }
323
+ );
324
+ }
325
+
326
+ // Only log actual errors with stack traces
341
327
  console.error("[Container] Error creating context:", error);
342
328
  return new Response(
343
329
  JSON.stringify({
344
- error: error instanceof Error ? error.message : "Failed to create context"
330
+ error:
331
+ error instanceof Error
332
+ ? error.message
333
+ : "Failed to create context",
345
334
  }),
346
335
  {
347
336
  status: 500,
@@ -353,54 +342,75 @@ const server = serve({
353
342
  );
354
343
  }
355
344
  } else if (req.method === "GET") {
356
- if (!jupyterInitialized) {
357
- return new Response(
358
- JSON.stringify({ contexts: [] }),
359
- {
360
- headers: {
361
- "Content-Type": "application/json",
362
- ...corsHeaders,
363
- },
364
- }
365
- );
366
- }
367
- const contexts = await jupyterServer.listContexts();
368
- return new Response(
369
- JSON.stringify({ contexts }),
370
- {
371
- headers: {
372
- "Content-Type": "application/json",
373
- ...corsHeaders,
374
- },
375
- }
376
- );
345
+ const contexts = await interpreterService.listContexts();
346
+ return new Response(JSON.stringify({ contexts }), {
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ ...corsHeaders,
350
+ },
351
+ });
377
352
  }
378
353
  break;
379
354
 
380
355
  case "/api/execute/code":
381
356
  if (req.method === "POST") {
382
- if (!jupyterInitialized) {
383
- return new Response(
384
- JSON.stringify({
385
- error: "Jupyter server is not ready. Please try again in a moment."
386
- }),
387
- {
388
- status: 503,
389
- headers: {
390
- "Content-Type": "application/json",
391
- ...corsHeaders,
392
- },
393
- }
394
- );
395
- }
396
357
  try {
397
- const body = await req.json() as { context_id: string; code: string; language?: string };
398
- return await jupyterServer.executeCode(body.context_id, body.code, body.language);
358
+ const body = (await req.json()) as {
359
+ context_id: string;
360
+ code: string;
361
+ language?: string;
362
+ };
363
+ return await interpreterService.executeCode(
364
+ body.context_id,
365
+ body.code,
366
+ body.language
367
+ );
399
368
  } catch (error) {
400
- console.error("[Container] Error executing code:", error);
369
+ // Check if it's a circuit breaker error
370
+ if (
371
+ error instanceof Error &&
372
+ error.message.includes("Circuit breaker is open")
373
+ ) {
374
+ console.log(
375
+ "[Container] Circuit breaker is open for code execution:",
376
+ error.message
377
+ );
378
+ return new Response(
379
+ JSON.stringify({
380
+ error:
381
+ "Service temporarily unavailable due to high error rate. Please try again later.",
382
+ status: "circuit_open",
383
+ details: error.message,
384
+ }),
385
+ {
386
+ status: 503,
387
+ headers: {
388
+ "Content-Type": "application/json",
389
+ "Retry-After": "30",
390
+ ...corsHeaders,
391
+ },
392
+ }
393
+ );
394
+ }
395
+
396
+ // Don't log stack traces for expected initialization state
397
+ if (
398
+ error instanceof Error &&
399
+ error.message.includes("initializing")
400
+ ) {
401
+ console.log(
402
+ "[Container] Code execution deferred - service still initializing"
403
+ );
404
+ } else {
405
+ console.error("[Container] Error executing code:", error);
406
+ }
407
+ // Error response is already handled by service.executeCode for not ready state
401
408
  return new Response(
402
409
  JSON.stringify({
403
- error: error instanceof Error ? error.message : "Failed to execute code"
410
+ error:
411
+ error instanceof Error
412
+ ? error.message
413
+ : "Failed to execute code",
404
414
  }),
405
415
  {
406
416
  status: 500,
@@ -416,27 +426,54 @@ const server = serve({
416
426
 
417
427
  default:
418
428
  // Handle dynamic routes for contexts
419
- if (pathname.startsWith("/api/contexts/") && pathname.split('/').length === 4) {
420
- const contextId = pathname.split('/')[3];
429
+ if (
430
+ pathname.startsWith("/api/contexts/") &&
431
+ pathname.split("/").length === 4
432
+ ) {
433
+ const contextId = pathname.split("/")[3];
421
434
  if (req.method === "DELETE") {
422
435
  try {
423
- await jupyterServer.deleteContext(contextId);
424
- return new Response(
425
- JSON.stringify({ success: true }),
426
- {
427
- headers: {
428
- "Content-Type": "application/json",
429
- ...corsHeaders,
430
- },
431
- }
432
- );
436
+ await interpreterService.deleteContext(contextId);
437
+ return new Response(JSON.stringify({ success: true }), {
438
+ headers: {
439
+ "Content-Type": "application/json",
440
+ ...corsHeaders,
441
+ },
442
+ });
433
443
  } catch (error) {
444
+ if (error instanceof InterpreterNotReadyError) {
445
+ console.log(
446
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
447
+ );
448
+ return new Response(
449
+ JSON.stringify({
450
+ error: error.message,
451
+ status: "initializing",
452
+ progress: error.progress,
453
+ }),
454
+ {
455
+ status: 503,
456
+ headers: {
457
+ "Content-Type": "application/json",
458
+ "Retry-After": "5",
459
+ ...corsHeaders,
460
+ },
461
+ }
462
+ );
463
+ }
434
464
  return new Response(
435
465
  JSON.stringify({
436
- error: error instanceof Error ? error.message : "Failed to delete context"
466
+ error:
467
+ error instanceof Error
468
+ ? error.message
469
+ : "Failed to delete context",
437
470
  }),
438
471
  {
439
- status: error instanceof Error && error.message.includes("not found") ? 404 : 500,
472
+ status:
473
+ error instanceof Error &&
474
+ error.message.includes("not found")
475
+ ? 404
476
+ : 500,
440
477
  headers: {
441
478
  "Content-Type": "application/json",
442
479
  ...corsHeaders,
@@ -446,22 +483,42 @@ const server = serve({
446
483
  }
447
484
  }
448
485
  }
449
-
486
+
450
487
  // Handle dynamic routes for individual processes
451
488
  if (pathname.startsWith("/api/process/")) {
452
- const segments = pathname.split('/');
489
+ const segments = pathname.split("/");
453
490
  if (segments.length >= 4) {
454
491
  const processId = segments[3];
455
492
  const action = segments[4]; // Optional: logs, stream, etc.
456
493
 
457
494
  if (!action && req.method === "GET") {
458
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
495
+ return handleGetProcessRequest(
496
+ req,
497
+ corsHeaders,
498
+ processId,
499
+ sessionManager
500
+ );
459
501
  } else if (!action && req.method === "DELETE") {
460
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
502
+ return handleKillProcessRequest(
503
+ req,
504
+ corsHeaders,
505
+ processId,
506
+ sessionManager
507
+ );
461
508
  } else if (action === "logs" && req.method === "GET") {
462
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
509
+ return handleGetProcessLogsRequest(
510
+ req,
511
+ corsHeaders,
512
+ processId,
513
+ sessionManager
514
+ );
463
515
  } else if (action === "stream" && req.method === "GET") {
464
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
516
+ return handleStreamProcessLogsRequest(
517
+ req,
518
+ corsHeaders,
519
+ processId,
520
+ sessionManager
521
+ );
465
522
  }
466
523
  }
467
524
  }
@@ -477,7 +534,10 @@ const server = serve({
477
534
  });
478
535
  }
479
536
  } catch (error) {
480
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
537
+ console.error(
538
+ `[Container] Error handling ${req.method} ${pathname}:`,
539
+ error
540
+ );
481
541
  return new Response(
482
542
  JSON.stringify({
483
543
  error: "Internal server error",
@@ -496,7 +556,7 @@ const server = serve({
496
556
  hostname: "0.0.0.0",
497
557
  port: 3000,
498
558
  // We don't need this, but typescript complains
499
- websocket: { async message() { } },
559
+ websocket: { async message() {} },
500
560
  });
501
561
 
502
562
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -526,6 +586,7 @@ console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
526
586
  console.log(` POST /api/contexts - Create a code execution context`);
527
587
  console.log(` GET /api/contexts - List all contexts`);
528
588
  console.log(` DELETE /api/contexts/{id} - Delete a context`);
529
- console.log(` POST /api/execute/code - Execute code in a context (streaming)`);
589
+ console.log(
590
+ ` POST /api/execute/code - Execute code in a context (streaming)`
591
+ );
530
592
  console.log(` GET /api/ping - Health check`);
531
- console.log(` GET /api/commands - List available commands`);