@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.
- package/CHANGELOG.md +12 -0
- package/Dockerfile +31 -7
- package/README.md +226 -2
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/index.ts +305 -10
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +448 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +83 -0
- package/dist/{chunk-YVZ3K26G.js → chunk-CUHYLCMT.js} +9 -21
- package/dist/chunk-CUHYLCMT.js.map +1 -0
- package/dist/chunk-EGC5IYXA.js +108 -0
- package/dist/chunk-EGC5IYXA.js.map +1 -0
- package/dist/chunk-FKBV7CZS.js +113 -0
- package/dist/chunk-FKBV7CZS.js.map +1 -0
- package/dist/chunk-LALY4SFU.js +129 -0
- package/dist/chunk-LALY4SFU.js.map +1 -0
- package/dist/{chunk-6THNBO4S.js → chunk-S5FFBU4Y.js} +1 -1
- package/dist/{chunk-6THNBO4S.js.map → chunk-S5FFBU4Y.js.map} +1 -1
- package/dist/chunk-VTKZL632.js +237 -0
- package/dist/chunk-VTKZL632.js.map +1 -0
- package/dist/{chunk-ZJN2PQOS.js → chunk-ZMPO44U4.js} +171 -72
- package/dist/chunk-ZMPO44U4.js.map +1 -0
- package/dist/{client-BXYlxy-j.d.ts → client-bzEV222a.d.ts} +52 -4
- package/dist/client.d.ts +2 -1
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +95 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +33 -3
- package/dist/interpreter-types.d.ts +259 -0
- package/dist/interpreter-types.js +9 -0
- package/dist/interpreter-types.js.map +1 -0
- package/dist/interpreter.d.ts +33 -0
- package/dist/interpreter.js +8 -0
- package/dist/interpreter.js.map +1 -0
- package/dist/jupyter-client.d.ts +4 -0
- package/dist/jupyter-client.js +9 -0
- package/dist/jupyter-client.js.map +1 -0
- package/dist/request-handler.d.ts +2 -1
- package/dist/request-handler.js +8 -3
- package/dist/sandbox.d.ts +2 -1
- package/dist/sandbox.js +8 -3
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/client.ts +37 -54
- package/src/errors.ts +218 -0
- package/src/index.ts +44 -10
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -0
- package/src/sandbox.ts +281 -153
- package/src/types.ts +15 -0
- package/dist/chunk-YVZ3K26G.js.map +0 -1
- package/dist/chunk-ZJN2PQOS.js.map +0 -1
package/container_src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
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,
|
|
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(
|
|
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(
|
|
558
|
+
return handleGetProcessRequest(
|
|
559
|
+
processes,
|
|
560
|
+
req,
|
|
561
|
+
corsHeaders,
|
|
562
|
+
processId
|
|
563
|
+
);
|
|
293
564
|
} else if (!action && req.method === "DELETE") {
|
|
294
|
-
return handleKillProcessRequest(
|
|
565
|
+
return handleKillProcessRequest(
|
|
566
|
+
processes,
|
|
567
|
+
req,
|
|
568
|
+
corsHeaders,
|
|
569
|
+
processId
|
|
570
|
+
);
|
|
295
571
|
} else if (action === "logs" && req.method === "GET") {
|
|
296
|
-
return handleGetProcessLogsRequest(
|
|
572
|
+
return handleGetProcessLogsRequest(
|
|
573
|
+
processes,
|
|
574
|
+
req,
|
|
575
|
+
corsHeaders,
|
|
576
|
+
processId
|
|
577
|
+
);
|
|
297
578
|
} else if (action === "stream" && req.method === "GET") {
|
|
298
|
-
return handleStreamProcessLogsRequest(
|
|
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(
|
|
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`);
|