@cloudflare/sandbox 0.0.0-fd5ec7f → 0.0.0-feafd32

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,38 +28,65 @@ import {
25
28
  handleStartProcessRequest,
26
29
  handleStreamProcessLogsRequest,
27
30
  } from "./handler/process";
28
- import type { ProcessRecord, SessionData } from "./types";
29
-
30
- // In-memory session storage (in production, you'd want to use a proper database)
31
- 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";
32
38
 
33
39
  // In-memory storage for exposed ports
34
40
  const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
35
41
 
36
- // In-memory process storage - cleared on container restart
37
- const processes = new Map<string, ProcessRecord>();
38
-
39
- // Generate a unique session ID using cryptographically secure randomness
40
- function generateSessionId(): string {
41
- return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
42
- }
43
-
44
- // Clean up old sessions (older than 1 hour)
45
- function cleanupOldSessions() {
46
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
47
- for (const [sessionId, session] of sessions.entries()) {
48
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
49
- sessions.delete(sessionId);
50
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
51
- }
52
- }
53
- }
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();
54
83
 
55
- // Run cleanup every 10 minutes
56
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
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");
57
87
 
58
88
  const server = serve({
59
- fetch(req: Request) {
89
+ async fetch(req: Request) {
60
90
  const url = new URL(req.url);
61
91
  const pathname = url.pathname;
62
92
 
@@ -89,108 +119,42 @@ const server = serve({
89
119
 
90
120
  case "/api/session/create":
91
121
  if (req.method === "POST") {
92
- const sessionId = generateSessionId();
93
- const sessionData: SessionData = {
94
- activeProcess: null,
95
- createdAt: new Date(),
96
- sessionId,
97
- };
98
- sessions.set(sessionId, sessionData);
99
-
100
- console.log(`[Server] Created new session: ${sessionId}`);
101
-
102
- return new Response(
103
- JSON.stringify({
104
- message: "Session created successfully",
105
- sessionId,
106
- timestamp: new Date().toISOString(),
107
- }),
108
- {
109
- headers: {
110
- "Content-Type": "application/json",
111
- ...corsHeaders,
112
- },
113
- }
114
- );
122
+ return handleCreateSession(req, corsHeaders, sessionManager);
115
123
  }
116
124
  break;
117
125
 
118
126
  case "/api/session/list":
119
127
  if (req.method === "GET") {
120
- const sessionList = Array.from(sessions.values()).map(
121
- (session) => ({
122
- createdAt: session.createdAt.toISOString(),
123
- hasActiveProcess: !!session.activeProcess,
124
- sessionId: session.sessionId,
125
- })
126
- );
127
-
128
- return new Response(
129
- JSON.stringify({
130
- count: sessionList.length,
131
- sessions: sessionList,
132
- timestamp: new Date().toISOString(),
133
- }),
134
- {
135
- headers: {
136
- "Content-Type": "application/json",
137
- ...corsHeaders,
138
- },
139
- }
140
- );
128
+ return handleListSessions(corsHeaders, sessionManager);
141
129
  }
142
130
  break;
143
131
 
144
132
  case "/api/execute":
145
133
  if (req.method === "POST") {
146
- return handleExecuteRequest(sessions, req, corsHeaders);
134
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
147
135
  }
148
136
  break;
149
137
 
150
138
  case "/api/execute/stream":
151
139
  if (req.method === "POST") {
152
- return handleStreamingExecuteRequest(sessions, req, corsHeaders);
140
+ return handleStreamingExecuteRequest(
141
+ req,
142
+ sessionManager,
143
+ corsHeaders
144
+ );
153
145
  }
154
146
  break;
155
147
 
156
148
  case "/api/ping":
157
149
  if (req.method === "GET") {
150
+ const health = await interpreterService.getHealthStatus();
158
151
  return new Response(
159
152
  JSON.stringify({
160
153
  message: "pong",
161
154
  timestamp: new Date().toISOString(),
162
- }),
163
- {
164
- headers: {
165
- "Content-Type": "application/json",
166
- ...corsHeaders,
167
- },
168
- }
169
- );
170
- }
171
- break;
172
-
173
- case "/api/commands":
174
- if (req.method === "GET") {
175
- return new Response(
176
- JSON.stringify({
177
- availableCommands: [
178
- "ls",
179
- "pwd",
180
- "echo",
181
- "cat",
182
- "grep",
183
- "find",
184
- "whoami",
185
- "date",
186
- "uptime",
187
- "ps",
188
- "top",
189
- "df",
190
- "du",
191
- "free",
192
- ],
193
- timestamp: new Date().toISOString(),
155
+ system: "interpreter (70x faster)",
156
+ status: health.ready ? "ready" : "initializing",
157
+ progress: health.progress,
194
158
  }),
195
159
  {
196
160
  headers: {
@@ -204,43 +168,49 @@ const server = serve({
204
168
 
205
169
  case "/api/git/checkout":
206
170
  if (req.method === "POST") {
207
- return handleGitCheckoutRequest(sessions, req, corsHeaders);
171
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
208
172
  }
209
173
  break;
210
174
 
211
175
  case "/api/mkdir":
212
176
  if (req.method === "POST") {
213
- return handleMkdirRequest(sessions, req, corsHeaders);
177
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
214
178
  }
215
179
  break;
216
180
 
217
181
  case "/api/write":
218
182
  if (req.method === "POST") {
219
- return handleWriteFileRequest(req, corsHeaders);
183
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
220
184
  }
221
185
  break;
222
186
 
223
187
  case "/api/read":
224
188
  if (req.method === "POST") {
225
- return handleReadFileRequest(req, corsHeaders);
189
+ return handleReadFileRequest(req, corsHeaders, sessionManager);
226
190
  }
227
191
  break;
228
192
 
229
193
  case "/api/delete":
230
194
  if (req.method === "POST") {
231
- return handleDeleteFileRequest(req, corsHeaders);
195
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
232
196
  }
233
197
  break;
234
198
 
235
199
  case "/api/rename":
236
200
  if (req.method === "POST") {
237
- return handleRenameFileRequest(req, corsHeaders);
201
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
238
202
  }
239
203
  break;
240
204
 
241
205
  case "/api/move":
242
206
  if (req.method === "POST") {
243
- 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);
244
214
  }
245
215
  break;
246
216
 
@@ -264,38 +234,291 @@ const server = serve({
264
234
 
265
235
  case "/api/process/start":
266
236
  if (req.method === "POST") {
267
- return handleStartProcessRequest(processes, req, corsHeaders);
237
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
268
238
  }
269
239
  break;
270
240
 
271
241
  case "/api/process/list":
272
242
  if (req.method === "GET") {
273
- return handleListProcessesRequest(processes, req, corsHeaders);
243
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
274
244
  }
275
245
  break;
276
246
 
277
247
  case "/api/process/kill-all":
278
248
  if (req.method === "DELETE") {
279
- return handleKillAllProcessesRequest(processes, req, corsHeaders);
249
+ return handleKillAllProcessesRequest(
250
+ req,
251
+ corsHeaders,
252
+ sessionManager
253
+ );
254
+ }
255
+ break;
256
+
257
+ case "/api/contexts":
258
+ if (req.method === "POST") {
259
+ try {
260
+ const body = (await req.json()) as CreateContextRequest;
261
+ const context = await interpreterService.createContext(body);
262
+ return new Response(
263
+ JSON.stringify({
264
+ id: context.id,
265
+ language: context.language,
266
+ cwd: context.cwd,
267
+ createdAt: context.createdAt,
268
+ lastUsed: context.lastUsed,
269
+ }),
270
+ {
271
+ headers: {
272
+ "Content-Type": "application/json",
273
+ ...corsHeaders,
274
+ },
275
+ }
276
+ );
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
327
+ console.error("[Container] Error creating context:", error);
328
+ return new Response(
329
+ JSON.stringify({
330
+ error:
331
+ error instanceof Error
332
+ ? error.message
333
+ : "Failed to create context",
334
+ }),
335
+ {
336
+ status: 500,
337
+ headers: {
338
+ "Content-Type": "application/json",
339
+ ...corsHeaders,
340
+ },
341
+ }
342
+ );
343
+ }
344
+ } else if (req.method === "GET") {
345
+ const contexts = await interpreterService.listContexts();
346
+ return new Response(JSON.stringify({ contexts }), {
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ ...corsHeaders,
350
+ },
351
+ });
352
+ }
353
+ break;
354
+
355
+ case "/api/execute/code":
356
+ if (req.method === "POST") {
357
+ try {
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
+ );
368
+ } catch (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
408
+ return new Response(
409
+ JSON.stringify({
410
+ error:
411
+ error instanceof Error
412
+ ? error.message
413
+ : "Failed to execute code",
414
+ }),
415
+ {
416
+ status: 500,
417
+ headers: {
418
+ "Content-Type": "application/json",
419
+ ...corsHeaders,
420
+ },
421
+ }
422
+ );
423
+ }
280
424
  }
281
425
  break;
282
426
 
283
427
  default:
428
+ // Handle dynamic routes for contexts
429
+ if (
430
+ pathname.startsWith("/api/contexts/") &&
431
+ pathname.split("/").length === 4
432
+ ) {
433
+ const contextId = pathname.split("/")[3];
434
+ if (req.method === "DELETE") {
435
+ try {
436
+ await interpreterService.deleteContext(contextId);
437
+ return new Response(JSON.stringify({ success: true }), {
438
+ headers: {
439
+ "Content-Type": "application/json",
440
+ ...corsHeaders,
441
+ },
442
+ });
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
+ }
464
+ return new Response(
465
+ JSON.stringify({
466
+ error:
467
+ error instanceof Error
468
+ ? error.message
469
+ : "Failed to delete context",
470
+ }),
471
+ {
472
+ status:
473
+ error instanceof Error &&
474
+ error.message.includes("not found")
475
+ ? 404
476
+ : 500,
477
+ headers: {
478
+ "Content-Type": "application/json",
479
+ ...corsHeaders,
480
+ },
481
+ }
482
+ );
483
+ }
484
+ }
485
+ }
486
+
284
487
  // Handle dynamic routes for individual processes
285
488
  if (pathname.startsWith("/api/process/")) {
286
- const segments = pathname.split('/');
489
+ const segments = pathname.split("/");
287
490
  if (segments.length >= 4) {
288
491
  const processId = segments[3];
289
492
  const action = segments[4]; // Optional: logs, stream, etc.
290
493
 
291
494
  if (!action && req.method === "GET") {
292
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
495
+ return handleGetProcessRequest(
496
+ req,
497
+ corsHeaders,
498
+ processId,
499
+ sessionManager
500
+ );
293
501
  } else if (!action && req.method === "DELETE") {
294
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
502
+ return handleKillProcessRequest(
503
+ req,
504
+ corsHeaders,
505
+ processId,
506
+ sessionManager
507
+ );
295
508
  } else if (action === "logs" && req.method === "GET") {
296
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
509
+ return handleGetProcessLogsRequest(
510
+ req,
511
+ corsHeaders,
512
+ processId,
513
+ sessionManager
514
+ );
297
515
  } else if (action === "stream" && req.method === "GET") {
298
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
516
+ return handleStreamProcessLogsRequest(
517
+ req,
518
+ corsHeaders,
519
+ processId,
520
+ sessionManager
521
+ );
299
522
  }
300
523
  }
301
524
  }
@@ -311,7 +534,10 @@ const server = serve({
311
534
  });
312
535
  }
313
536
  } catch (error) {
314
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
537
+ console.error(
538
+ `[Container] Error handling ${req.method} ${pathname}:`,
539
+ error
540
+ );
315
541
  return new Response(
316
542
  JSON.stringify({
317
543
  error: "Internal server error",
@@ -330,7 +556,7 @@ const server = serve({
330
556
  hostname: "0.0.0.0",
331
557
  port: 3000,
332
558
  // We don't need this, but typescript complains
333
- websocket: { async message() { } },
559
+ websocket: { async message() {} },
334
560
  });
335
561
 
336
562
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -357,5 +583,10 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
357
583
  console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
358
584
  console.log(` DELETE /api/process/kill-all - Kill all processes`);
359
585
  console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
586
+ console.log(` POST /api/contexts - Create a code execution context`);
587
+ console.log(` GET /api/contexts - List all contexts`);
588
+ console.log(` DELETE /api/contexts/{id} - Delete a context`);
589
+ console.log(
590
+ ` POST /api/execute/code - Execute code in a context (streaming)`
591
+ );
360
592
  console.log(` GET /api/ping - Health check`);
361
- console.log(` GET /api/commands - List available commands`);