@cloudflare/sandbox 0.0.0-0608f1e → 0.0.0-12bbd12
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.
- package/CHANGELOG.md +58 -0
- package/Dockerfile +44 -18
- package/README.md +784 -0
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/handler/exec.ts +17 -14
- package/container_src/handler/file.ts +222 -2
- package/container_src/handler/process.ts +1 -1
- package/container_src/index.ts +312 -10
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +461 -0
- package/container_src/jupyter_config.py +48 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +84 -0
- package/container_src/types.ts +17 -3
- package/package.json +5 -4
- package/src/client.ts +108 -122
- package/src/errors.ts +218 -0
- package/src/index.ts +51 -15
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -0
- package/src/sandbox.ts +292 -149
- package/src/types.ts +125 -0
- package/tsconfig.json +1 -1
package/container_src/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { serve } from "bun";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
handleExecuteRequest,
|
|
5
|
+
handleStreamingExecuteRequest,
|
|
6
|
+
} from "./handler/exec";
|
|
4
7
|
import {
|
|
5
8
|
handleDeleteFileRequest,
|
|
9
|
+
handleListFilesRequest,
|
|
6
10
|
handleMkdirRequest,
|
|
7
11
|
handleMoveFileRequest,
|
|
8
12
|
handleReadFileRequest,
|
|
@@ -25,6 +29,8 @@ import {
|
|
|
25
29
|
handleStartProcessRequest,
|
|
26
30
|
handleStreamProcessLogsRequest,
|
|
27
31
|
} from "./handler/process";
|
|
32
|
+
import type { CreateContextRequest } from "./jupyter-server";
|
|
33
|
+
import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
|
|
28
34
|
import type { ProcessRecord, SessionData } from "./types";
|
|
29
35
|
|
|
30
36
|
// In-memory session storage (in production, you'd want to use a proper database)
|
|
@@ -38,7 +44,7 @@ const processes = new Map<string, ProcessRecord>();
|
|
|
38
44
|
|
|
39
45
|
// Generate a unique session ID using cryptographically secure randomness
|
|
40
46
|
function generateSessionId(): string {
|
|
41
|
-
return `session_${Date.now()}_${randomBytes(6).toString(
|
|
47
|
+
return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
// Clean up old sessions (older than 1 hour)
|
|
@@ -55,8 +61,31 @@ function cleanupOldSessions() {
|
|
|
55
61
|
// Run cleanup every 10 minutes
|
|
56
62
|
setInterval(cleanupOldSessions, 10 * 60 * 1000);
|
|
57
63
|
|
|
64
|
+
// Initialize Jupyter service with graceful degradation
|
|
65
|
+
const jupyterService = new JupyterService();
|
|
66
|
+
|
|
67
|
+
// Start Jupyter initialization in background (non-blocking)
|
|
68
|
+
console.log("[Container] Starting Jupyter initialization in background...");
|
|
69
|
+
console.log(
|
|
70
|
+
"[Container] API endpoints are available immediately. Jupyter-dependent features will be available shortly."
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
jupyterService
|
|
74
|
+
.initialize()
|
|
75
|
+
.then(() => {
|
|
76
|
+
console.log(
|
|
77
|
+
"[Container] Jupyter fully initialized - all features available"
|
|
78
|
+
);
|
|
79
|
+
})
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
console.error("[Container] Jupyter initialization failed:", error.message);
|
|
82
|
+
console.error(
|
|
83
|
+
"[Container] The API will continue in degraded mode without code execution capabilities"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
58
87
|
const server = serve({
|
|
59
|
-
fetch(req: Request) {
|
|
88
|
+
async fetch(req: Request) {
|
|
60
89
|
const url = new URL(req.url);
|
|
61
90
|
const pathname = url.pathname;
|
|
62
91
|
|
|
@@ -155,10 +184,17 @@ const server = serve({
|
|
|
155
184
|
|
|
156
185
|
case "/api/ping":
|
|
157
186
|
if (req.method === "GET") {
|
|
187
|
+
const health = await jupyterService.getHealthStatus();
|
|
158
188
|
return new Response(
|
|
159
189
|
JSON.stringify({
|
|
160
190
|
message: "pong",
|
|
161
191
|
timestamp: new Date().toISOString(),
|
|
192
|
+
jupyter: health.ready
|
|
193
|
+
? "ready"
|
|
194
|
+
: health.initializing
|
|
195
|
+
? "initializing"
|
|
196
|
+
: "not ready",
|
|
197
|
+
jupyterHealth: health,
|
|
162
198
|
}),
|
|
163
199
|
{
|
|
164
200
|
headers: {
|
|
@@ -244,6 +280,12 @@ const server = serve({
|
|
|
244
280
|
}
|
|
245
281
|
break;
|
|
246
282
|
|
|
283
|
+
case "/api/list-files":
|
|
284
|
+
if (req.method === "POST") {
|
|
285
|
+
return handleListFilesRequest(req, corsHeaders);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
|
|
247
289
|
case "/api/expose-port":
|
|
248
290
|
if (req.method === "POST") {
|
|
249
291
|
return handleExposePortRequest(exposedPorts, req, corsHeaders);
|
|
@@ -280,22 +322,273 @@ const server = serve({
|
|
|
280
322
|
}
|
|
281
323
|
break;
|
|
282
324
|
|
|
325
|
+
// Code interpreter endpoints
|
|
326
|
+
case "/api/contexts":
|
|
327
|
+
if (req.method === "POST") {
|
|
328
|
+
try {
|
|
329
|
+
const body = (await req.json()) as CreateContextRequest;
|
|
330
|
+
const context = await jupyterService.createContext(body);
|
|
331
|
+
return new Response(
|
|
332
|
+
JSON.stringify({
|
|
333
|
+
id: context.id,
|
|
334
|
+
language: context.language,
|
|
335
|
+
cwd: context.cwd,
|
|
336
|
+
createdAt: context.createdAt,
|
|
337
|
+
lastUsed: context.lastUsed,
|
|
338
|
+
}),
|
|
339
|
+
{
|
|
340
|
+
headers: {
|
|
341
|
+
"Content-Type": "application/json",
|
|
342
|
+
...corsHeaders,
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (error instanceof JupyterNotReadyError) {
|
|
348
|
+
// This happens when request times out waiting for Jupyter
|
|
349
|
+
console.log(
|
|
350
|
+
`[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
|
|
351
|
+
);
|
|
352
|
+
return new Response(
|
|
353
|
+
JSON.stringify({
|
|
354
|
+
error: error.message,
|
|
355
|
+
status: "initializing",
|
|
356
|
+
progress: error.progress,
|
|
357
|
+
}),
|
|
358
|
+
{
|
|
359
|
+
status: 503,
|
|
360
|
+
headers: {
|
|
361
|
+
"Content-Type": "application/json",
|
|
362
|
+
"Retry-After": String(error.retryAfter),
|
|
363
|
+
...corsHeaders,
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
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:",
|
|
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": "60",
|
|
390
|
+
...corsHeaders,
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Only log actual errors with stack traces
|
|
397
|
+
console.error("[Container] Error creating context:", error);
|
|
398
|
+
return new Response(
|
|
399
|
+
JSON.stringify({
|
|
400
|
+
error:
|
|
401
|
+
error instanceof Error
|
|
402
|
+
? error.message
|
|
403
|
+
: "Failed to create context",
|
|
404
|
+
}),
|
|
405
|
+
{
|
|
406
|
+
status: 500,
|
|
407
|
+
headers: {
|
|
408
|
+
"Content-Type": "application/json",
|
|
409
|
+
...corsHeaders,
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
} else if (req.method === "GET") {
|
|
415
|
+
const contexts = await jupyterService.listContexts();
|
|
416
|
+
return new Response(JSON.stringify({ contexts }), {
|
|
417
|
+
headers: {
|
|
418
|
+
"Content-Type": "application/json",
|
|
419
|
+
...corsHeaders,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case "/api/execute/code":
|
|
426
|
+
if (req.method === "POST") {
|
|
427
|
+
try {
|
|
428
|
+
const body = (await req.json()) as {
|
|
429
|
+
context_id: string;
|
|
430
|
+
code: string;
|
|
431
|
+
language?: string;
|
|
432
|
+
};
|
|
433
|
+
return await jupyterService.executeCode(
|
|
434
|
+
body.context_id,
|
|
435
|
+
body.code,
|
|
436
|
+
body.language
|
|
437
|
+
);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
// Check if it's a circuit breaker error
|
|
440
|
+
if (
|
|
441
|
+
error instanceof Error &&
|
|
442
|
+
error.message.includes("Circuit breaker is open")
|
|
443
|
+
) {
|
|
444
|
+
console.log(
|
|
445
|
+
"[Container] Circuit breaker is open for code execution:",
|
|
446
|
+
error.message
|
|
447
|
+
);
|
|
448
|
+
return new Response(
|
|
449
|
+
JSON.stringify({
|
|
450
|
+
error:
|
|
451
|
+
"Service temporarily unavailable due to high error rate. Please try again later.",
|
|
452
|
+
status: "circuit_open",
|
|
453
|
+
details: error.message,
|
|
454
|
+
}),
|
|
455
|
+
{
|
|
456
|
+
status: 503,
|
|
457
|
+
headers: {
|
|
458
|
+
"Content-Type": "application/json",
|
|
459
|
+
"Retry-After": "30",
|
|
460
|
+
...corsHeaders,
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Don't log stack traces for expected initialization state
|
|
467
|
+
if (
|
|
468
|
+
error instanceof Error &&
|
|
469
|
+
error.message.includes("initializing")
|
|
470
|
+
) {
|
|
471
|
+
console.log(
|
|
472
|
+
"[Container] Code execution deferred - Jupyter still initializing"
|
|
473
|
+
);
|
|
474
|
+
} else {
|
|
475
|
+
console.error("[Container] Error executing code:", error);
|
|
476
|
+
}
|
|
477
|
+
// Error response is already handled by jupyterService.executeCode for not ready state
|
|
478
|
+
return new Response(
|
|
479
|
+
JSON.stringify({
|
|
480
|
+
error:
|
|
481
|
+
error instanceof Error
|
|
482
|
+
? error.message
|
|
483
|
+
: "Failed to execute code",
|
|
484
|
+
}),
|
|
485
|
+
{
|
|
486
|
+
status: 500,
|
|
487
|
+
headers: {
|
|
488
|
+
"Content-Type": "application/json",
|
|
489
|
+
...corsHeaders,
|
|
490
|
+
},
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
|
|
283
497
|
default:
|
|
498
|
+
// Handle dynamic routes for contexts
|
|
499
|
+
if (
|
|
500
|
+
pathname.startsWith("/api/contexts/") &&
|
|
501
|
+
pathname.split("/").length === 4
|
|
502
|
+
) {
|
|
503
|
+
const contextId = pathname.split("/")[3];
|
|
504
|
+
if (req.method === "DELETE") {
|
|
505
|
+
try {
|
|
506
|
+
await jupyterService.deleteContext(contextId);
|
|
507
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
508
|
+
headers: {
|
|
509
|
+
"Content-Type": "application/json",
|
|
510
|
+
...corsHeaders,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error instanceof JupyterNotReadyError) {
|
|
515
|
+
console.log(
|
|
516
|
+
`[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
|
|
517
|
+
);
|
|
518
|
+
return new Response(
|
|
519
|
+
JSON.stringify({
|
|
520
|
+
error: error.message,
|
|
521
|
+
status: "initializing",
|
|
522
|
+
progress: error.progress,
|
|
523
|
+
}),
|
|
524
|
+
{
|
|
525
|
+
status: 503,
|
|
526
|
+
headers: {
|
|
527
|
+
"Content-Type": "application/json",
|
|
528
|
+
"Retry-After": "5",
|
|
529
|
+
...corsHeaders,
|
|
530
|
+
},
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return new Response(
|
|
535
|
+
JSON.stringify({
|
|
536
|
+
error:
|
|
537
|
+
error instanceof Error
|
|
538
|
+
? error.message
|
|
539
|
+
: "Failed to delete context",
|
|
540
|
+
}),
|
|
541
|
+
{
|
|
542
|
+
status:
|
|
543
|
+
error instanceof Error &&
|
|
544
|
+
error.message.includes("not found")
|
|
545
|
+
? 404
|
|
546
|
+
: 500,
|
|
547
|
+
headers: {
|
|
548
|
+
"Content-Type": "application/json",
|
|
549
|
+
...corsHeaders,
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
284
557
|
// Handle dynamic routes for individual processes
|
|
285
558
|
if (pathname.startsWith("/api/process/")) {
|
|
286
|
-
const segments = pathname.split(
|
|
559
|
+
const segments = pathname.split("/");
|
|
287
560
|
if (segments.length >= 4) {
|
|
288
561
|
const processId = segments[3];
|
|
289
562
|
const action = segments[4]; // Optional: logs, stream, etc.
|
|
290
563
|
|
|
291
564
|
if (!action && req.method === "GET") {
|
|
292
|
-
return handleGetProcessRequest(
|
|
565
|
+
return handleGetProcessRequest(
|
|
566
|
+
processes,
|
|
567
|
+
req,
|
|
568
|
+
corsHeaders,
|
|
569
|
+
processId
|
|
570
|
+
);
|
|
293
571
|
} else if (!action && req.method === "DELETE") {
|
|
294
|
-
return handleKillProcessRequest(
|
|
572
|
+
return handleKillProcessRequest(
|
|
573
|
+
processes,
|
|
574
|
+
req,
|
|
575
|
+
corsHeaders,
|
|
576
|
+
processId
|
|
577
|
+
);
|
|
295
578
|
} else if (action === "logs" && req.method === "GET") {
|
|
296
|
-
return handleGetProcessLogsRequest(
|
|
579
|
+
return handleGetProcessLogsRequest(
|
|
580
|
+
processes,
|
|
581
|
+
req,
|
|
582
|
+
corsHeaders,
|
|
583
|
+
processId
|
|
584
|
+
);
|
|
297
585
|
} else if (action === "stream" && req.method === "GET") {
|
|
298
|
-
return handleStreamProcessLogsRequest(
|
|
586
|
+
return handleStreamProcessLogsRequest(
|
|
587
|
+
processes,
|
|
588
|
+
req,
|
|
589
|
+
corsHeaders,
|
|
590
|
+
processId
|
|
591
|
+
);
|
|
299
592
|
}
|
|
300
593
|
}
|
|
301
594
|
}
|
|
@@ -311,7 +604,10 @@ const server = serve({
|
|
|
311
604
|
});
|
|
312
605
|
}
|
|
313
606
|
} catch (error) {
|
|
314
|
-
console.error(
|
|
607
|
+
console.error(
|
|
608
|
+
`[Container] Error handling ${req.method} ${pathname}:`,
|
|
609
|
+
error
|
|
610
|
+
);
|
|
315
611
|
return new Response(
|
|
316
612
|
JSON.stringify({
|
|
317
613
|
error: "Internal server error",
|
|
@@ -330,7 +626,7 @@ const server = serve({
|
|
|
330
626
|
hostname: "0.0.0.0",
|
|
331
627
|
port: 3000,
|
|
332
628
|
// We don't need this, but typescript complains
|
|
333
|
-
websocket: { async message() {
|
|
629
|
+
websocket: { async message() {} },
|
|
334
630
|
});
|
|
335
631
|
|
|
336
632
|
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
@@ -357,5 +653,11 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
|
|
|
357
653
|
console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
|
|
358
654
|
console.log(` DELETE /api/process/kill-all - Kill all processes`);
|
|
359
655
|
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
656
|
+
console.log(` POST /api/contexts - Create a code execution context`);
|
|
657
|
+
console.log(` GET /api/contexts - List all contexts`);
|
|
658
|
+
console.log(` DELETE /api/contexts/{id} - Delete a context`);
|
|
659
|
+
console.log(
|
|
660
|
+
` POST /api/execute/code - Execute code in a context (streaming)`
|
|
661
|
+
);
|
|
360
662
|
console.log(` GET /api/ping - Health check`);
|
|
361
663
|
console.log(` GET /api/commands - List available commands`);
|