@cloudflare/sandbox 0.0.0-dc66e8e → 0.0.0-dcf36ef

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,11 +1,15 @@
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,
12
+ handleReadFileStreamRequest,
9
13
  handleRenameFileRequest,
10
14
  handleWriteFileRequest,
11
15
  } from "./handler/file";
@@ -25,38 +29,66 @@ import {
25
29
  handleStartProcessRequest,
26
30
  handleStreamProcessLogsRequest,
27
31
  } 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>();
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";
32
39
 
33
40
  // In-memory storage for exposed ports
34
41
  const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
35
42
 
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
- }
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
+ );
52
+
53
+ // Session manager for secure execution with isolation
54
+ const sessionManager = new SessionManager();
55
+
56
+ // Graceful shutdown handler
57
+ const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
58
+
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
+ });
54
66
 
55
- // Run cleanup every 10 minutes
56
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
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");
57
88
 
58
89
  const server = serve({
59
- fetch(req: Request) {
90
+ idleTimeout: 255,
91
+ async fetch(req: Request) {
60
92
  const url = new URL(req.url);
61
93
  const pathname = url.pathname;
62
94
 
@@ -89,108 +121,42 @@ const server = serve({
89
121
 
90
122
  case "/api/session/create":
91
123
  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
- );
124
+ return handleCreateSession(req, corsHeaders, sessionManager);
115
125
  }
116
126
  break;
117
127
 
118
128
  case "/api/session/list":
119
129
  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
- );
130
+ return handleListSessions(corsHeaders, sessionManager);
141
131
  }
142
132
  break;
143
133
 
144
134
  case "/api/execute":
145
135
  if (req.method === "POST") {
146
- return handleExecuteRequest(sessions, req, corsHeaders);
136
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
147
137
  }
148
138
  break;
149
139
 
150
140
  case "/api/execute/stream":
151
141
  if (req.method === "POST") {
152
- return handleStreamingExecuteRequest(sessions, req, corsHeaders);
142
+ return handleStreamingExecuteRequest(
143
+ req,
144
+ sessionManager,
145
+ corsHeaders
146
+ );
153
147
  }
154
148
  break;
155
149
 
156
150
  case "/api/ping":
157
151
  if (req.method === "GET") {
152
+ const health = await interpreterService.getHealthStatus();
158
153
  return new Response(
159
154
  JSON.stringify({
160
155
  message: "pong",
161
156
  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(),
157
+ system: "interpreter (70x faster)",
158
+ status: health.ready ? "ready" : "initializing",
159
+ progress: health.progress,
194
160
  }),
195
161
  {
196
162
  headers: {
@@ -204,43 +170,55 @@ const server = serve({
204
170
 
205
171
  case "/api/git/checkout":
206
172
  if (req.method === "POST") {
207
- return handleGitCheckoutRequest(sessions, req, corsHeaders);
173
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
208
174
  }
209
175
  break;
210
176
 
211
177
  case "/api/mkdir":
212
178
  if (req.method === "POST") {
213
- return handleMkdirRequest(sessions, req, corsHeaders);
179
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
214
180
  }
215
181
  break;
216
182
 
217
183
  case "/api/write":
218
184
  if (req.method === "POST") {
219
- return handleWriteFileRequest(req, corsHeaders);
185
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
220
186
  }
221
187
  break;
222
188
 
223
189
  case "/api/read":
224
190
  if (req.method === "POST") {
225
- 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);
226
198
  }
227
199
  break;
228
200
 
229
201
  case "/api/delete":
230
202
  if (req.method === "POST") {
231
- return handleDeleteFileRequest(req, corsHeaders);
203
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
232
204
  }
233
205
  break;
234
206
 
235
207
  case "/api/rename":
236
208
  if (req.method === "POST") {
237
- return handleRenameFileRequest(req, corsHeaders);
209
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
238
210
  }
239
211
  break;
240
212
 
241
213
  case "/api/move":
242
214
  if (req.method === "POST") {
243
- 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);
244
222
  }
245
223
  break;
246
224
 
@@ -264,38 +242,291 @@ const server = serve({
264
242
 
265
243
  case "/api/process/start":
266
244
  if (req.method === "POST") {
267
- return handleStartProcessRequest(processes, req, corsHeaders);
245
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
268
246
  }
269
247
  break;
270
248
 
271
249
  case "/api/process/list":
272
250
  if (req.method === "GET") {
273
- return handleListProcessesRequest(processes, req, corsHeaders);
251
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
274
252
  }
275
253
  break;
276
254
 
277
255
  case "/api/process/kill-all":
278
256
  if (req.method === "DELETE") {
279
- return handleKillAllProcessesRequest(processes, req, corsHeaders);
257
+ return handleKillAllProcessesRequest(
258
+ req,
259
+ corsHeaders,
260
+ sessionManager
261
+ );
262
+ }
263
+ break;
264
+
265
+ case "/api/contexts":
266
+ if (req.method === "POST") {
267
+ try {
268
+ const body = (await req.json()) as CreateContextRequest;
269
+ const context = await interpreterService.createContext(body);
270
+ return new Response(
271
+ JSON.stringify({
272
+ id: context.id,
273
+ language: context.language,
274
+ cwd: context.cwd,
275
+ createdAt: context.createdAt,
276
+ lastUsed: context.lastUsed,
277
+ }),
278
+ {
279
+ headers: {
280
+ "Content-Type": "application/json",
281
+ ...corsHeaders,
282
+ },
283
+ }
284
+ );
285
+ } catch (error) {
286
+ if (error instanceof InterpreterNotReadyError) {
287
+ console.log(
288
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
289
+ );
290
+ return new Response(
291
+ JSON.stringify({
292
+ error: error.message,
293
+ status: "initializing",
294
+ progress: error.progress,
295
+ }),
296
+ {
297
+ status: 503,
298
+ headers: {
299
+ "Content-Type": "application/json",
300
+ "Retry-After": String(error.retryAfter),
301
+ ...corsHeaders,
302
+ },
303
+ }
304
+ );
305
+ }
306
+
307
+ // Check if it's a circuit breaker error
308
+ if (
309
+ error instanceof Error &&
310
+ error.message.includes("Circuit breaker is open")
311
+ ) {
312
+ console.log(
313
+ "[Container] Circuit breaker is open:",
314
+ error.message
315
+ );
316
+ return new Response(
317
+ JSON.stringify({
318
+ error:
319
+ "Service temporarily unavailable due to high error rate. Please try again later.",
320
+ status: "circuit_open",
321
+ details: error.message,
322
+ }),
323
+ {
324
+ status: 503,
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ "Retry-After": "60",
328
+ ...corsHeaders,
329
+ },
330
+ }
331
+ );
332
+ }
333
+
334
+ // Only log actual errors with stack traces
335
+ console.error("[Container] Error creating context:", error);
336
+ return new Response(
337
+ JSON.stringify({
338
+ error:
339
+ error instanceof Error
340
+ ? error.message
341
+ : "Failed to create context",
342
+ }),
343
+ {
344
+ status: 500,
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ ...corsHeaders,
348
+ },
349
+ }
350
+ );
351
+ }
352
+ } else if (req.method === "GET") {
353
+ const contexts = await interpreterService.listContexts();
354
+ return new Response(JSON.stringify({ contexts }), {
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ ...corsHeaders,
358
+ },
359
+ });
360
+ }
361
+ break;
362
+
363
+ case "/api/execute/code":
364
+ if (req.method === "POST") {
365
+ try {
366
+ const body = (await req.json()) as {
367
+ context_id: string;
368
+ code: string;
369
+ language?: string;
370
+ };
371
+ return await interpreterService.executeCode(
372
+ body.context_id,
373
+ body.code,
374
+ body.language
375
+ );
376
+ } catch (error) {
377
+ // Check if it's a circuit breaker error
378
+ if (
379
+ error instanceof Error &&
380
+ error.message.includes("Circuit breaker is open")
381
+ ) {
382
+ console.log(
383
+ "[Container] Circuit breaker is open for code execution:",
384
+ error.message
385
+ );
386
+ return new Response(
387
+ JSON.stringify({
388
+ error:
389
+ "Service temporarily unavailable due to high error rate. Please try again later.",
390
+ status: "circuit_open",
391
+ details: error.message,
392
+ }),
393
+ {
394
+ status: 503,
395
+ headers: {
396
+ "Content-Type": "application/json",
397
+ "Retry-After": "30",
398
+ ...corsHeaders,
399
+ },
400
+ }
401
+ );
402
+ }
403
+
404
+ // Don't log stack traces for expected initialization state
405
+ if (
406
+ error instanceof Error &&
407
+ error.message.includes("initializing")
408
+ ) {
409
+ console.log(
410
+ "[Container] Code execution deferred - service still initializing"
411
+ );
412
+ } else {
413
+ console.error("[Container] Error executing code:", error);
414
+ }
415
+ // Error response is already handled by service.executeCode for not ready state
416
+ return new Response(
417
+ JSON.stringify({
418
+ error:
419
+ error instanceof Error
420
+ ? error.message
421
+ : "Failed to execute code",
422
+ }),
423
+ {
424
+ status: 500,
425
+ headers: {
426
+ "Content-Type": "application/json",
427
+ ...corsHeaders,
428
+ },
429
+ }
430
+ );
431
+ }
280
432
  }
281
433
  break;
282
434
 
283
435
  default:
436
+ // Handle dynamic routes for contexts
437
+ if (
438
+ pathname.startsWith("/api/contexts/") &&
439
+ pathname.split("/").length === 4
440
+ ) {
441
+ const contextId = pathname.split("/")[3];
442
+ if (req.method === "DELETE") {
443
+ try {
444
+ await interpreterService.deleteContext(contextId);
445
+ return new Response(JSON.stringify({ success: true }), {
446
+ headers: {
447
+ "Content-Type": "application/json",
448
+ ...corsHeaders,
449
+ },
450
+ });
451
+ } catch (error) {
452
+ if (error instanceof InterpreterNotReadyError) {
453
+ console.log(
454
+ `[Container] Request timed out waiting for interpreter (${error.progress}% complete)`
455
+ );
456
+ return new Response(
457
+ JSON.stringify({
458
+ error: error.message,
459
+ status: "initializing",
460
+ progress: error.progress,
461
+ }),
462
+ {
463
+ status: 503,
464
+ headers: {
465
+ "Content-Type": "application/json",
466
+ "Retry-After": "5",
467
+ ...corsHeaders,
468
+ },
469
+ }
470
+ );
471
+ }
472
+ return new Response(
473
+ JSON.stringify({
474
+ error:
475
+ error instanceof Error
476
+ ? error.message
477
+ : "Failed to delete context",
478
+ }),
479
+ {
480
+ status:
481
+ error instanceof Error &&
482
+ error.message.includes("not found")
483
+ ? 404
484
+ : 500,
485
+ headers: {
486
+ "Content-Type": "application/json",
487
+ ...corsHeaders,
488
+ },
489
+ }
490
+ );
491
+ }
492
+ }
493
+ }
494
+
284
495
  // Handle dynamic routes for individual processes
285
496
  if (pathname.startsWith("/api/process/")) {
286
- const segments = pathname.split('/');
497
+ const segments = pathname.split("/");
287
498
  if (segments.length >= 4) {
288
499
  const processId = segments[3];
289
500
  const action = segments[4]; // Optional: logs, stream, etc.
290
501
 
291
502
  if (!action && req.method === "GET") {
292
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
503
+ return handleGetProcessRequest(
504
+ req,
505
+ corsHeaders,
506
+ processId,
507
+ sessionManager
508
+ );
293
509
  } else if (!action && req.method === "DELETE") {
294
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
510
+ return handleKillProcessRequest(
511
+ req,
512
+ corsHeaders,
513
+ processId,
514
+ sessionManager
515
+ );
295
516
  } else if (action === "logs" && req.method === "GET") {
296
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
517
+ return handleGetProcessLogsRequest(
518
+ req,
519
+ corsHeaders,
520
+ processId,
521
+ sessionManager
522
+ );
297
523
  } else if (action === "stream" && req.method === "GET") {
298
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
524
+ return handleStreamProcessLogsRequest(
525
+ req,
526
+ corsHeaders,
527
+ processId,
528
+ sessionManager
529
+ );
299
530
  }
300
531
  }
301
532
  }
@@ -311,7 +542,10 @@ const server = serve({
311
542
  });
312
543
  }
313
544
  } catch (error) {
314
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
545
+ console.error(
546
+ `[Container] Error handling ${req.method} ${pathname}:`,
547
+ error
548
+ );
315
549
  return new Response(
316
550
  JSON.stringify({
317
551
  error: "Internal server error",
@@ -330,7 +564,7 @@ const server = serve({
330
564
  hostname: "0.0.0.0",
331
565
  port: 3000,
332
566
  // We don't need this, but typescript complains
333
- websocket: { async message() { } },
567
+ websocket: { async message() {} },
334
568
  });
335
569
 
336
570
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -343,6 +577,7 @@ console.log(` POST /api/git/checkout - Checkout a git repository`);
343
577
  console.log(` POST /api/mkdir - Create a directory`);
344
578
  console.log(` POST /api/write - Write a file`);
345
579
  console.log(` POST /api/read - Read a file`);
580
+ console.log(` POST /api/read/stream - Stream a file (SSE)`);
346
581
  console.log(` POST /api/delete - Delete a file`);
347
582
  console.log(` POST /api/rename - Rename a file`);
348
583
  console.log(` POST /api/move - Move a file`);
@@ -357,5 +592,10 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
357
592
  console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
358
593
  console.log(` DELETE /api/process/kill-all - Kill all processes`);
359
594
  console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
595
+ console.log(` POST /api/contexts - Create a code execution context`);
596
+ console.log(` GET /api/contexts - List all contexts`);
597
+ console.log(` DELETE /api/contexts/{id} - Delete a context`);
598
+ console.log(
599
+ ` POST /api/execute/code - Execute code in a context (streaming)`
600
+ );
360
601
  console.log(` GET /api/ping - Health check`);
361
- console.log(` GET /api/commands - List available commands`);