@cloudflare/sandbox 0.0.0-7de28be → 0.0.0-871f813

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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Circuit Breaker implementation to prevent cascading failures
3
+ */
4
+ export class CircuitBreaker {
5
+ private failures = 0;
6
+ private lastFailure: number = 0;
7
+ private successCount = 0;
8
+ private state: "closed" | "open" | "half-open" = "closed";
9
+
10
+ // Configuration
11
+ private readonly threshold: number;
12
+ private readonly timeout: number;
13
+ private readonly halfOpenSuccessThreshold: number;
14
+ private readonly name: string;
15
+
16
+ constructor(options: {
17
+ name: string;
18
+ threshold?: number;
19
+ timeout?: number;
20
+ halfOpenSuccessThreshold?: number;
21
+ }) {
22
+ this.name = options.name;
23
+ this.threshold = options.threshold || 5;
24
+ this.timeout = options.timeout || 30000; // 30 seconds
25
+ this.halfOpenSuccessThreshold = options.halfOpenSuccessThreshold || 3;
26
+ }
27
+
28
+ /**
29
+ * Execute an operation with circuit breaker protection
30
+ */
31
+ async execute<T>(operation: () => Promise<T>): Promise<T> {
32
+ // Check circuit state
33
+ if (this.state === "open") {
34
+ if (Date.now() - this.lastFailure > this.timeout) {
35
+ console.log(
36
+ `[CircuitBreaker ${this.name}] Transitioning from open to half-open`
37
+ );
38
+ this.state = "half-open";
39
+ this.successCount = 0;
40
+ } else {
41
+ throw new Error(
42
+ `Circuit breaker is open for ${this.name}. Retry after ${
43
+ this.timeout - (Date.now() - this.lastFailure)
44
+ }ms`
45
+ );
46
+ }
47
+ }
48
+
49
+ try {
50
+ const result = await operation();
51
+
52
+ // Record success
53
+ if (this.state === "half-open") {
54
+ this.successCount++;
55
+ if (this.successCount >= this.halfOpenSuccessThreshold) {
56
+ console.log(
57
+ `[CircuitBreaker ${this.name}] Transitioning from half-open to closed`
58
+ );
59
+ this.state = "closed";
60
+ this.failures = 0;
61
+ }
62
+ } else if (this.state === "closed") {
63
+ // Reset failure count on success
64
+ this.failures = 0;
65
+ }
66
+
67
+ return result;
68
+ } catch (error) {
69
+ this.recordFailure();
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Record a failure and update circuit state
76
+ */
77
+ private recordFailure() {
78
+ this.failures++;
79
+ this.lastFailure = Date.now();
80
+
81
+ if (this.state === "half-open") {
82
+ console.log(
83
+ `[CircuitBreaker ${this.name}] Failure in half-open state, transitioning to open`
84
+ );
85
+ this.state = "open";
86
+ } else if (this.failures >= this.threshold) {
87
+ console.log(
88
+ `[CircuitBreaker ${this.name}] Threshold reached (${this.failures}/${this.threshold}), transitioning to open`
89
+ );
90
+ this.state = "open";
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get current circuit breaker state
96
+ */
97
+ getState(): {
98
+ state: string;
99
+ failures: number;
100
+ lastFailure: number;
101
+ isOpen: boolean;
102
+ } {
103
+ return {
104
+ state: this.state,
105
+ failures: this.failures,
106
+ lastFailure: this.lastFailure,
107
+ isOpen: this.state === "open",
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Reset the circuit breaker
113
+ */
114
+ reset() {
115
+ this.state = "closed";
116
+ this.failures = 0;
117
+ this.successCount = 0;
118
+ this.lastFailure = 0;
119
+ console.log(`[CircuitBreaker ${this.name}] Reset to closed state`);
120
+ }
121
+ }
@@ -16,7 +16,7 @@ function executeCommand(
16
16
  shell: true,
17
17
  stdio: ["pipe", "pipe", "pipe"] as const,
18
18
  detached: options.background || false,
19
- cwd: options.cwd,
19
+ cwd: options.cwd || "/workspace", // Default to clean /workspace directory
20
20
  env: options.env ? { ...process.env, ...options.env } : process.env
21
21
  };
22
22
 
@@ -159,7 +159,7 @@ export async function handleStreamingExecuteRequest(
159
159
  ): Promise<Response> {
160
160
  try {
161
161
  const body = (await req.json()) as ExecuteRequest;
162
- const { command, sessionId, background } = body;
162
+ const { command, sessionId, background, cwd, env } = body;
163
163
 
164
164
  if (!command || typeof command !== "string") {
165
165
  return new Response(
@@ -186,6 +186,8 @@ export async function handleStreamingExecuteRequest(
186
186
  shell: true,
187
187
  stdio: ["pipe", "pipe", "pipe"] as const,
188
188
  detached: background || false,
189
+ cwd: cwd || "/workspace", // Default to clean /workspace directory
190
+ env: env ? { ...process.env, ...env } : process.env
189
191
  };
190
192
 
191
193
  const child = spawn(command, spawnOptions);
@@ -72,7 +72,7 @@ export async function handleStartProcessRequest(
72
72
  // Start the actual process
73
73
  try {
74
74
  const spawnOptions: SpawnOptions = {
75
- cwd: options.cwd || process.cwd(),
75
+ cwd: options.cwd || "/workspace", // Default to /workspace for consistency with exec commands
76
76
  env: { ...process.env, ...options.env },
77
77
  detached: false,
78
78
  shell: true,
@@ -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`);