@cloudflare/sandbox 0.2.1 → 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 +6 -0
- package/README.md +1 -1
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/index.ts +228 -103
- package/container_src/jupyter-server.ts +289 -46
- package/container_src/jupyter-service.ts +448 -0
- package/container_src/startup.sh +59 -28
- package/dist/chunk-LALY4SFU.js +129 -0
- package/dist/chunk-LALY4SFU.js.map +1 -0
- package/dist/{chunk-SYMWNYWA.js → chunk-VTKZL632.js} +116 -64
- package/dist/chunk-VTKZL632.js.map +1 -0
- package/dist/{chunk-IATLC32Y.js → chunk-ZMPO44U4.js} +8 -8
- package/dist/{client-C7rKCYBD.d.ts → client-bzEV222a.d.ts} +10 -0
- package/dist/client.d.ts +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 +2 -1
- package/dist/index.js +27 -3
- package/dist/interpreter.d.ts +1 -1
- package/dist/jupyter-client.d.ts +1 -1
- package/dist/jupyter-client.js +2 -1
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +4 -3
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +4 -3
- package/package.json +1 -1
- package/src/errors.ts +218 -0
- package/src/index.ts +33 -8
- package/src/jupyter-client.ts +225 -142
- package/dist/chunk-SYMWNYWA.js.map +0 -1
- /package/dist/{chunk-IATLC32Y.js.map → chunk-ZMPO44U4.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @cloudflare/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#51](https://github.com/cloudflare/sandbox-sdk/pull/51) [`4aceb32`](https://github.com/cloudflare/sandbox-sdk/commit/4aceb3215c836f59afcb88b2b325016b3f623f46) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Handle intermittent interpreter failures and decouple jupyter startup
|
|
8
|
+
|
|
3
9
|
## 0.2.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ npm install @cloudflare/sandbox
|
|
|
72
72
|
1. **Create a Dockerfile** (temporary requirement, will be removed in future releases):
|
|
73
73
|
|
|
74
74
|
```dockerfile
|
|
75
|
-
FROM docker.io/cloudflare/sandbox:0.2.
|
|
75
|
+
FROM docker.io/cloudflare/sandbox:0.2.2
|
|
76
76
|
|
|
77
77
|
# Expose the ports you want to expose
|
|
78
78
|
EXPOSE 3000
|
|
@@ -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
|
+
}
|
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,7 +28,8 @@ import {
|
|
|
25
28
|
handleStartProcessRequest,
|
|
26
29
|
handleStreamProcessLogsRequest,
|
|
27
30
|
} from "./handler/process";
|
|
28
|
-
import {
|
|
31
|
+
import type { CreateContextRequest } from "./jupyter-server";
|
|
32
|
+
import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
|
|
29
33
|
import type { ProcessRecord, SessionData } from "./types";
|
|
30
34
|
|
|
31
35
|
// In-memory session storage (in production, you'd want to use a proper database)
|
|
@@ -39,7 +43,7 @@ const processes = new Map<string, ProcessRecord>();
|
|
|
39
43
|
|
|
40
44
|
// Generate a unique session ID using cryptographically secure randomness
|
|
41
45
|
function generateSessionId(): string {
|
|
42
|
-
return `session_${Date.now()}_${randomBytes(6).toString(
|
|
46
|
+
return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
// Clean up old sessions (older than 1 hour)
|
|
@@ -56,25 +60,28 @@ function cleanupOldSessions() {
|
|
|
56
60
|
// Run cleanup every 10 minutes
|
|
57
61
|
setInterval(cleanupOldSessions, 10 * 60 * 1000);
|
|
58
62
|
|
|
59
|
-
// Initialize Jupyter
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
});
|
|
78
85
|
|
|
79
86
|
const server = serve({
|
|
80
87
|
async fetch(req: Request) {
|
|
@@ -176,11 +183,17 @@ const server = serve({
|
|
|
176
183
|
|
|
177
184
|
case "/api/ping":
|
|
178
185
|
if (req.method === "GET") {
|
|
186
|
+
const health = await jupyterService.getHealthStatus();
|
|
179
187
|
return new Response(
|
|
180
188
|
JSON.stringify({
|
|
181
189
|
message: "pong",
|
|
182
190
|
timestamp: new Date().toISOString(),
|
|
183
|
-
jupyter:
|
|
191
|
+
jupyter: health.ready
|
|
192
|
+
? "ready"
|
|
193
|
+
: health.initializing
|
|
194
|
+
? "initializing"
|
|
195
|
+
: "not ready",
|
|
196
|
+
jupyterHealth: health,
|
|
184
197
|
}),
|
|
185
198
|
{
|
|
186
199
|
headers: {
|
|
@@ -305,30 +318,16 @@ const server = serve({
|
|
|
305
318
|
// Code interpreter endpoints
|
|
306
319
|
case "/api/contexts":
|
|
307
320
|
if (req.method === "POST") {
|
|
308
|
-
if (!jupyterInitialized) {
|
|
309
|
-
return new Response(
|
|
310
|
-
JSON.stringify({
|
|
311
|
-
error: "Jupyter server is not ready. Please try again in a moment."
|
|
312
|
-
}),
|
|
313
|
-
{
|
|
314
|
-
status: 503,
|
|
315
|
-
headers: {
|
|
316
|
-
"Content-Type": "application/json",
|
|
317
|
-
...corsHeaders,
|
|
318
|
-
},
|
|
319
|
-
}
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
321
|
try {
|
|
323
|
-
const body = await req.json() as CreateContextRequest;
|
|
324
|
-
const context = await
|
|
322
|
+
const body = (await req.json()) as CreateContextRequest;
|
|
323
|
+
const context = await jupyterService.createContext(body);
|
|
325
324
|
return new Response(
|
|
326
325
|
JSON.stringify({
|
|
327
326
|
id: context.id,
|
|
328
327
|
language: context.language,
|
|
329
328
|
cwd: context.cwd,
|
|
330
329
|
createdAt: context.createdAt,
|
|
331
|
-
lastUsed: context.lastUsed
|
|
330
|
+
lastUsed: context.lastUsed,
|
|
332
331
|
}),
|
|
333
332
|
{
|
|
334
333
|
headers: {
|
|
@@ -338,10 +337,63 @@ const server = serve({
|
|
|
338
337
|
}
|
|
339
338
|
);
|
|
340
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
|
|
341
390
|
console.error("[Container] Error creating context:", error);
|
|
342
391
|
return new Response(
|
|
343
392
|
JSON.stringify({
|
|
344
|
-
error:
|
|
393
|
+
error:
|
|
394
|
+
error instanceof Error
|
|
395
|
+
? error.message
|
|
396
|
+
: "Failed to create context",
|
|
345
397
|
}),
|
|
346
398
|
{
|
|
347
399
|
status: 500,
|
|
@@ -353,54 +405,75 @@ const server = serve({
|
|
|
353
405
|
);
|
|
354
406
|
}
|
|
355
407
|
} else if (req.method === "GET") {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
},
|
|
364
|
-
}
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
const contexts = await jupyterServer.listContexts();
|
|
368
|
-
return new Response(
|
|
369
|
-
JSON.stringify({ contexts }),
|
|
370
|
-
{
|
|
371
|
-
headers: {
|
|
372
|
-
"Content-Type": "application/json",
|
|
373
|
-
...corsHeaders,
|
|
374
|
-
},
|
|
375
|
-
}
|
|
376
|
-
);
|
|
408
|
+
const contexts = await jupyterService.listContexts();
|
|
409
|
+
return new Response(JSON.stringify({ contexts }), {
|
|
410
|
+
headers: {
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
...corsHeaders,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
377
415
|
}
|
|
378
416
|
break;
|
|
379
417
|
|
|
380
418
|
case "/api/execute/code":
|
|
381
419
|
if (req.method === "POST") {
|
|
382
|
-
if (!jupyterInitialized) {
|
|
383
|
-
return new Response(
|
|
384
|
-
JSON.stringify({
|
|
385
|
-
error: "Jupyter server is not ready. Please try again in a moment."
|
|
386
|
-
}),
|
|
387
|
-
{
|
|
388
|
-
status: 503,
|
|
389
|
-
headers: {
|
|
390
|
-
"Content-Type": "application/json",
|
|
391
|
-
...corsHeaders,
|
|
392
|
-
},
|
|
393
|
-
}
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
420
|
try {
|
|
397
|
-
const body = await req.json() as {
|
|
398
|
-
|
|
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
|
+
);
|
|
399
431
|
} catch (error) {
|
|
400
|
-
|
|
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
|
|
401
471
|
return new Response(
|
|
402
472
|
JSON.stringify({
|
|
403
|
-
error:
|
|
473
|
+
error:
|
|
474
|
+
error instanceof Error
|
|
475
|
+
? error.message
|
|
476
|
+
: "Failed to execute code",
|
|
404
477
|
}),
|
|
405
478
|
{
|
|
406
479
|
status: 500,
|
|
@@ -416,27 +489,54 @@ const server = serve({
|
|
|
416
489
|
|
|
417
490
|
default:
|
|
418
491
|
// Handle dynamic routes for contexts
|
|
419
|
-
if (
|
|
420
|
-
|
|
492
|
+
if (
|
|
493
|
+
pathname.startsWith("/api/contexts/") &&
|
|
494
|
+
pathname.split("/").length === 4
|
|
495
|
+
) {
|
|
496
|
+
const contextId = pathname.split("/")[3];
|
|
421
497
|
if (req.method === "DELETE") {
|
|
422
498
|
try {
|
|
423
|
-
await
|
|
424
|
-
return new Response(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
},
|
|
431
|
-
}
|
|
432
|
-
);
|
|
499
|
+
await jupyterService.deleteContext(contextId);
|
|
500
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
501
|
+
headers: {
|
|
502
|
+
"Content-Type": "application/json",
|
|
503
|
+
...corsHeaders,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
433
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
|
+
}
|
|
434
527
|
return new Response(
|
|
435
528
|
JSON.stringify({
|
|
436
|
-
error:
|
|
529
|
+
error:
|
|
530
|
+
error instanceof Error
|
|
531
|
+
? error.message
|
|
532
|
+
: "Failed to delete context",
|
|
437
533
|
}),
|
|
438
534
|
{
|
|
439
|
-
status:
|
|
535
|
+
status:
|
|
536
|
+
error instanceof Error &&
|
|
537
|
+
error.message.includes("not found")
|
|
538
|
+
? 404
|
|
539
|
+
: 500,
|
|
440
540
|
headers: {
|
|
441
541
|
"Content-Type": "application/json",
|
|
442
542
|
...corsHeaders,
|
|
@@ -446,22 +546,42 @@ const server = serve({
|
|
|
446
546
|
}
|
|
447
547
|
}
|
|
448
548
|
}
|
|
449
|
-
|
|
549
|
+
|
|
450
550
|
// Handle dynamic routes for individual processes
|
|
451
551
|
if (pathname.startsWith("/api/process/")) {
|
|
452
|
-
const segments = pathname.split(
|
|
552
|
+
const segments = pathname.split("/");
|
|
453
553
|
if (segments.length >= 4) {
|
|
454
554
|
const processId = segments[3];
|
|
455
555
|
const action = segments[4]; // Optional: logs, stream, etc.
|
|
456
556
|
|
|
457
557
|
if (!action && req.method === "GET") {
|
|
458
|
-
return handleGetProcessRequest(
|
|
558
|
+
return handleGetProcessRequest(
|
|
559
|
+
processes,
|
|
560
|
+
req,
|
|
561
|
+
corsHeaders,
|
|
562
|
+
processId
|
|
563
|
+
);
|
|
459
564
|
} else if (!action && req.method === "DELETE") {
|
|
460
|
-
return handleKillProcessRequest(
|
|
565
|
+
return handleKillProcessRequest(
|
|
566
|
+
processes,
|
|
567
|
+
req,
|
|
568
|
+
corsHeaders,
|
|
569
|
+
processId
|
|
570
|
+
);
|
|
461
571
|
} else if (action === "logs" && req.method === "GET") {
|
|
462
|
-
return handleGetProcessLogsRequest(
|
|
572
|
+
return handleGetProcessLogsRequest(
|
|
573
|
+
processes,
|
|
574
|
+
req,
|
|
575
|
+
corsHeaders,
|
|
576
|
+
processId
|
|
577
|
+
);
|
|
463
578
|
} else if (action === "stream" && req.method === "GET") {
|
|
464
|
-
return handleStreamProcessLogsRequest(
|
|
579
|
+
return handleStreamProcessLogsRequest(
|
|
580
|
+
processes,
|
|
581
|
+
req,
|
|
582
|
+
corsHeaders,
|
|
583
|
+
processId
|
|
584
|
+
);
|
|
465
585
|
}
|
|
466
586
|
}
|
|
467
587
|
}
|
|
@@ -477,7 +597,10 @@ const server = serve({
|
|
|
477
597
|
});
|
|
478
598
|
}
|
|
479
599
|
} catch (error) {
|
|
480
|
-
console.error(
|
|
600
|
+
console.error(
|
|
601
|
+
`[Container] Error handling ${req.method} ${pathname}:`,
|
|
602
|
+
error
|
|
603
|
+
);
|
|
481
604
|
return new Response(
|
|
482
605
|
JSON.stringify({
|
|
483
606
|
error: "Internal server error",
|
|
@@ -496,7 +619,7 @@ const server = serve({
|
|
|
496
619
|
hostname: "0.0.0.0",
|
|
497
620
|
port: 3000,
|
|
498
621
|
// We don't need this, but typescript complains
|
|
499
|
-
websocket: { async message() {
|
|
622
|
+
websocket: { async message() {} },
|
|
500
623
|
});
|
|
501
624
|
|
|
502
625
|
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
@@ -526,6 +649,8 @@ console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
|
526
649
|
console.log(` POST /api/contexts - Create a code execution context`);
|
|
527
650
|
console.log(` GET /api/contexts - List all contexts`);
|
|
528
651
|
console.log(` DELETE /api/contexts/{id} - Delete a context`);
|
|
529
|
-
console.log(
|
|
652
|
+
console.log(
|
|
653
|
+
` POST /api/execute/code - Execute code in a context (streaming)`
|
|
654
|
+
);
|
|
530
655
|
console.log(` GET /api/ping - Health check`);
|
|
531
656
|
console.log(` GET /api/commands - List available commands`);
|