@cloudflare/sandbox 0.2.0 → 0.2.2

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 (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/Dockerfile +31 -7
  3. package/README.md +226 -2
  4. package/container_src/bun.lock +122 -0
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/index.ts +305 -10
  7. package/container_src/jupyter-server.ts +579 -0
  8. package/container_src/jupyter-service.ts +448 -0
  9. package/container_src/mime-processor.ts +255 -0
  10. package/container_src/package.json +9 -0
  11. package/container_src/startup.sh +83 -0
  12. package/dist/{chunk-YVZ3K26G.js → chunk-CUHYLCMT.js} +9 -21
  13. package/dist/chunk-CUHYLCMT.js.map +1 -0
  14. package/dist/chunk-EGC5IYXA.js +108 -0
  15. package/dist/chunk-EGC5IYXA.js.map +1 -0
  16. package/dist/chunk-FKBV7CZS.js +113 -0
  17. package/dist/chunk-FKBV7CZS.js.map +1 -0
  18. package/dist/chunk-LALY4SFU.js +129 -0
  19. package/dist/chunk-LALY4SFU.js.map +1 -0
  20. package/dist/{chunk-6THNBO4S.js → chunk-S5FFBU4Y.js} +1 -1
  21. package/dist/{chunk-6THNBO4S.js.map → chunk-S5FFBU4Y.js.map} +1 -1
  22. package/dist/chunk-VTKZL632.js +237 -0
  23. package/dist/chunk-VTKZL632.js.map +1 -0
  24. package/dist/{chunk-ZJN2PQOS.js → chunk-ZMPO44U4.js} +171 -72
  25. package/dist/chunk-ZMPO44U4.js.map +1 -0
  26. package/dist/{client-BXYlxy-j.d.ts → client-bzEV222a.d.ts} +52 -4
  27. package/dist/client.d.ts +2 -1
  28. package/dist/client.js +1 -1
  29. package/dist/errors.d.ts +95 -0
  30. package/dist/errors.js +27 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/index.d.ts +3 -1
  33. package/dist/index.js +33 -3
  34. package/dist/interpreter-types.d.ts +259 -0
  35. package/dist/interpreter-types.js +9 -0
  36. package/dist/interpreter-types.js.map +1 -0
  37. package/dist/interpreter.d.ts +33 -0
  38. package/dist/interpreter.js +8 -0
  39. package/dist/interpreter.js.map +1 -0
  40. package/dist/jupyter-client.d.ts +4 -0
  41. package/dist/jupyter-client.js +9 -0
  42. package/dist/jupyter-client.js.map +1 -0
  43. package/dist/request-handler.d.ts +2 -1
  44. package/dist/request-handler.js +8 -3
  45. package/dist/sandbox.d.ts +2 -1
  46. package/dist/sandbox.js +8 -3
  47. package/dist/types.d.ts +8 -0
  48. package/dist/types.js +1 -1
  49. package/package.json +1 -1
  50. package/src/client.ts +37 -54
  51. package/src/errors.ts +218 -0
  52. package/src/index.ts +44 -10
  53. package/src/interpreter-types.ts +383 -0
  54. package/src/interpreter.ts +150 -0
  55. package/src/jupyter-client.ts +349 -0
  56. package/src/sandbox.ts +281 -153
  57. package/src/types.ts +15 -0
  58. package/dist/chunk-YVZ3K26G.js.map +0 -1
  59. package/dist/chunk-ZJN2PQOS.js.map +0 -1
@@ -1,6 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { serve } from "bun";
3
- import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
3
+ import {
4
+ handleExecuteRequest,
5
+ handleStreamingExecuteRequest,
6
+ } from "./handler/exec";
4
7
  import {
5
8
  handleDeleteFileRequest,
6
9
  handleMkdirRequest,
@@ -25,6 +28,8 @@ import {
25
28
  handleStartProcessRequest,
26
29
  handleStreamProcessLogsRequest,
27
30
  } from "./handler/process";
31
+ import type { CreateContextRequest } from "./jupyter-server";
32
+ import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
28
33
  import type { ProcessRecord, SessionData } from "./types";
29
34
 
30
35
  // In-memory session storage (in production, you'd want to use a proper database)
@@ -38,7 +43,7 @@ const processes = new Map<string, ProcessRecord>();
38
43
 
39
44
  // Generate a unique session ID using cryptographically secure randomness
40
45
  function generateSessionId(): string {
41
- return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
46
+ return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
42
47
  }
43
48
 
44
49
  // Clean up old sessions (older than 1 hour)
@@ -55,8 +60,31 @@ function cleanupOldSessions() {
55
60
  // Run cleanup every 10 minutes
56
61
  setInterval(cleanupOldSessions, 10 * 60 * 1000);
57
62
 
63
+ // Initialize Jupyter service with graceful degradation
64
+ const jupyterService = new JupyterService();
65
+
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
+ );
71
+
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
+ });
85
+
58
86
  const server = serve({
59
- fetch(req: Request) {
87
+ async fetch(req: Request) {
60
88
  const url = new URL(req.url);
61
89
  const pathname = url.pathname;
62
90
 
@@ -155,10 +183,17 @@ const server = serve({
155
183
 
156
184
  case "/api/ping":
157
185
  if (req.method === "GET") {
186
+ const health = await jupyterService.getHealthStatus();
158
187
  return new Response(
159
188
  JSON.stringify({
160
189
  message: "pong",
161
190
  timestamp: new Date().toISOString(),
191
+ jupyter: health.ready
192
+ ? "ready"
193
+ : health.initializing
194
+ ? "initializing"
195
+ : "not ready",
196
+ jupyterHealth: health,
162
197
  }),
163
198
  {
164
199
  headers: {
@@ -280,22 +315,273 @@ const server = serve({
280
315
  }
281
316
  break;
282
317
 
318
+ // Code interpreter endpoints
319
+ case "/api/contexts":
320
+ if (req.method === "POST") {
321
+ try {
322
+ const body = (await req.json()) as CreateContextRequest;
323
+ const context = await jupyterService.createContext(body);
324
+ return new Response(
325
+ JSON.stringify({
326
+ id: context.id,
327
+ language: context.language,
328
+ cwd: context.cwd,
329
+ createdAt: context.createdAt,
330
+ lastUsed: context.lastUsed,
331
+ }),
332
+ {
333
+ headers: {
334
+ "Content-Type": "application/json",
335
+ ...corsHeaders,
336
+ },
337
+ }
338
+ );
339
+ } catch (error) {
340
+ if (error instanceof JupyterNotReadyError) {
341
+ // This happens when request times out waiting for Jupyter
342
+ console.log(
343
+ `[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
344
+ );
345
+ return new Response(
346
+ JSON.stringify({
347
+ error: error.message,
348
+ status: "initializing",
349
+ progress: error.progress,
350
+ }),
351
+ {
352
+ status: 503,
353
+ headers: {
354
+ "Content-Type": "application/json",
355
+ "Retry-After": String(error.retryAfter),
356
+ ...corsHeaders,
357
+ },
358
+ }
359
+ );
360
+ }
361
+
362
+ // Check if it's a circuit breaker error
363
+ if (
364
+ error instanceof Error &&
365
+ error.message.includes("Circuit breaker is open")
366
+ ) {
367
+ console.log(
368
+ "[Container] Circuit breaker is open:",
369
+ error.message
370
+ );
371
+ return new Response(
372
+ JSON.stringify({
373
+ error:
374
+ "Service temporarily unavailable due to high error rate. Please try again later.",
375
+ status: "circuit_open",
376
+ details: error.message,
377
+ }),
378
+ {
379
+ status: 503,
380
+ headers: {
381
+ "Content-Type": "application/json",
382
+ "Retry-After": "60",
383
+ ...corsHeaders,
384
+ },
385
+ }
386
+ );
387
+ }
388
+
389
+ // Only log actual errors with stack traces
390
+ console.error("[Container] Error creating context:", error);
391
+ return new Response(
392
+ JSON.stringify({
393
+ error:
394
+ error instanceof Error
395
+ ? error.message
396
+ : "Failed to create context",
397
+ }),
398
+ {
399
+ status: 500,
400
+ headers: {
401
+ "Content-Type": "application/json",
402
+ ...corsHeaders,
403
+ },
404
+ }
405
+ );
406
+ }
407
+ } else if (req.method === "GET") {
408
+ const contexts = await jupyterService.listContexts();
409
+ return new Response(JSON.stringify({ contexts }), {
410
+ headers: {
411
+ "Content-Type": "application/json",
412
+ ...corsHeaders,
413
+ },
414
+ });
415
+ }
416
+ break;
417
+
418
+ case "/api/execute/code":
419
+ if (req.method === "POST") {
420
+ try {
421
+ const body = (await req.json()) as {
422
+ context_id: string;
423
+ code: string;
424
+ language?: string;
425
+ };
426
+ return await jupyterService.executeCode(
427
+ body.context_id,
428
+ body.code,
429
+ body.language
430
+ );
431
+ } catch (error) {
432
+ // Check if it's a circuit breaker error
433
+ if (
434
+ error instanceof Error &&
435
+ error.message.includes("Circuit breaker is open")
436
+ ) {
437
+ console.log(
438
+ "[Container] Circuit breaker is open for code execution:",
439
+ error.message
440
+ );
441
+ return new Response(
442
+ JSON.stringify({
443
+ error:
444
+ "Service temporarily unavailable due to high error rate. Please try again later.",
445
+ status: "circuit_open",
446
+ details: error.message,
447
+ }),
448
+ {
449
+ status: 503,
450
+ headers: {
451
+ "Content-Type": "application/json",
452
+ "Retry-After": "30",
453
+ ...corsHeaders,
454
+ },
455
+ }
456
+ );
457
+ }
458
+
459
+ // Don't log stack traces for expected initialization state
460
+ if (
461
+ error instanceof Error &&
462
+ error.message.includes("initializing")
463
+ ) {
464
+ console.log(
465
+ "[Container] Code execution deferred - Jupyter still initializing"
466
+ );
467
+ } else {
468
+ console.error("[Container] Error executing code:", error);
469
+ }
470
+ // Error response is already handled by jupyterService.executeCode for not ready state
471
+ return new Response(
472
+ JSON.stringify({
473
+ error:
474
+ error instanceof Error
475
+ ? error.message
476
+ : "Failed to execute code",
477
+ }),
478
+ {
479
+ status: 500,
480
+ headers: {
481
+ "Content-Type": "application/json",
482
+ ...corsHeaders,
483
+ },
484
+ }
485
+ );
486
+ }
487
+ }
488
+ break;
489
+
283
490
  default:
491
+ // Handle dynamic routes for contexts
492
+ if (
493
+ pathname.startsWith("/api/contexts/") &&
494
+ pathname.split("/").length === 4
495
+ ) {
496
+ const contextId = pathname.split("/")[3];
497
+ if (req.method === "DELETE") {
498
+ try {
499
+ await jupyterService.deleteContext(contextId);
500
+ return new Response(JSON.stringify({ success: true }), {
501
+ headers: {
502
+ "Content-Type": "application/json",
503
+ ...corsHeaders,
504
+ },
505
+ });
506
+ } catch (error) {
507
+ if (error instanceof JupyterNotReadyError) {
508
+ console.log(
509
+ `[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
510
+ );
511
+ return new Response(
512
+ JSON.stringify({
513
+ error: error.message,
514
+ status: "initializing",
515
+ progress: error.progress,
516
+ }),
517
+ {
518
+ status: 503,
519
+ headers: {
520
+ "Content-Type": "application/json",
521
+ "Retry-After": "5",
522
+ ...corsHeaders,
523
+ },
524
+ }
525
+ );
526
+ }
527
+ return new Response(
528
+ JSON.stringify({
529
+ error:
530
+ error instanceof Error
531
+ ? error.message
532
+ : "Failed to delete context",
533
+ }),
534
+ {
535
+ status:
536
+ error instanceof Error &&
537
+ error.message.includes("not found")
538
+ ? 404
539
+ : 500,
540
+ headers: {
541
+ "Content-Type": "application/json",
542
+ ...corsHeaders,
543
+ },
544
+ }
545
+ );
546
+ }
547
+ }
548
+ }
549
+
284
550
  // Handle dynamic routes for individual processes
285
551
  if (pathname.startsWith("/api/process/")) {
286
- const segments = pathname.split('/');
552
+ const segments = pathname.split("/");
287
553
  if (segments.length >= 4) {
288
554
  const processId = segments[3];
289
555
  const action = segments[4]; // Optional: logs, stream, etc.
290
556
 
291
557
  if (!action && req.method === "GET") {
292
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
558
+ return handleGetProcessRequest(
559
+ processes,
560
+ req,
561
+ corsHeaders,
562
+ processId
563
+ );
293
564
  } else if (!action && req.method === "DELETE") {
294
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
565
+ return handleKillProcessRequest(
566
+ processes,
567
+ req,
568
+ corsHeaders,
569
+ processId
570
+ );
295
571
  } else if (action === "logs" && req.method === "GET") {
296
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
572
+ return handleGetProcessLogsRequest(
573
+ processes,
574
+ req,
575
+ corsHeaders,
576
+ processId
577
+ );
297
578
  } else if (action === "stream" && req.method === "GET") {
298
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
579
+ return handleStreamProcessLogsRequest(
580
+ processes,
581
+ req,
582
+ corsHeaders,
583
+ processId
584
+ );
299
585
  }
300
586
  }
301
587
  }
@@ -311,7 +597,10 @@ const server = serve({
311
597
  });
312
598
  }
313
599
  } catch (error) {
314
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
600
+ console.error(
601
+ `[Container] Error handling ${req.method} ${pathname}:`,
602
+ error
603
+ );
315
604
  return new Response(
316
605
  JSON.stringify({
317
606
  error: "Internal server error",
@@ -330,7 +619,7 @@ const server = serve({
330
619
  hostname: "0.0.0.0",
331
620
  port: 3000,
332
621
  // We don't need this, but typescript complains
333
- websocket: { async message() { } },
622
+ websocket: { async message() {} },
334
623
  });
335
624
 
336
625
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -357,5 +646,11 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
357
646
  console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
358
647
  console.log(` DELETE /api/process/kill-all - Kill all processes`);
359
648
  console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
649
+ console.log(` POST /api/contexts - Create a code execution context`);
650
+ console.log(` GET /api/contexts - List all contexts`);
651
+ console.log(` DELETE /api/contexts/{id} - Delete a context`);
652
+ console.log(
653
+ ` POST /api/execute/code - Execute code in a context (streaming)`
654
+ );
360
655
  console.log(` GET /api/ping - Health check`);
361
656
  console.log(` GET /api/commands - List available commands`);