@cloudflare/sandbox 0.0.0-dc66e8e → 0.0.0-e1fa354
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 +137 -0
- package/Dockerfile +48 -24
- package/README.md +846 -0
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/control-process.ts +784 -0
- package/container_src/handler/exec.ts +99 -251
- package/container_src/handler/file.ts +204 -642
- package/container_src/handler/git.ts +28 -80
- package/container_src/handler/process.ts +443 -515
- package/container_src/handler/session.ts +92 -0
- package/container_src/index.ts +363 -123
- package/container_src/isolation.ts +1038 -0
- 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/shell-escape.ts +42 -0
- package/container_src/startup.sh +84 -0
- package/container_src/types.ts +42 -14
- package/package.json +5 -4
- package/src/client.ts +206 -235
- package/src/errors.ts +218 -0
- package/src/index.ts +59 -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 +502 -400
- package/src/types.ts +140 -24
- package/tsconfig.json +1 -1
package/container_src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { serve } from "bun";
|
|
3
2
|
import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
|
|
4
3
|
import {
|
|
5
4
|
handleDeleteFileRequest,
|
|
5
|
+
handleListFilesRequest,
|
|
6
6
|
handleMkdirRequest,
|
|
7
7
|
handleMoveFileRequest,
|
|
8
8
|
handleReadFileRequest,
|
|
@@ -25,38 +25,78 @@ import {
|
|
|
25
25
|
handleStartProcessRequest,
|
|
26
26
|
handleStreamProcessLogsRequest,
|
|
27
27
|
} from "./handler/process";
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
import { handleCreateSession, handleListSessions } from "./handler/session";
|
|
29
|
+
import { hasNamespaceSupport, SessionManager } from "./isolation";
|
|
30
|
+
import type { CreateContextRequest } from "./jupyter-server";
|
|
31
|
+
import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
|
|
32
32
|
|
|
33
33
|
// In-memory storage for exposed ports
|
|
34
34
|
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
// Check isolation capabilities on startup
|
|
37
|
+
const isolationAvailable = hasNamespaceSupport();
|
|
38
|
+
console.log(
|
|
39
|
+
`[Container] Process isolation: ${
|
|
40
|
+
isolationAvailable
|
|
41
|
+
? "ENABLED (production mode)"
|
|
42
|
+
: "DISABLED (development mode)"
|
|
43
|
+
}`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Session manager for secure execution with isolation
|
|
47
|
+
const sessionManager = new SessionManager();
|
|
48
|
+
|
|
49
|
+
// Graceful shutdown handler
|
|
50
|
+
const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
|
|
51
|
+
|
|
52
|
+
process.on("SIGTERM", async () => {
|
|
53
|
+
console.log("[Container] SIGTERM received, cleaning up sessions...");
|
|
54
|
+
await sessionManager.destroyAll();
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}, SHUTDOWN_GRACE_PERIOD_MS);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
process.on("SIGINT", async () => {
|
|
61
|
+
console.log("[Container] SIGINT received, cleaning up sessions...");
|
|
62
|
+
await sessionManager.destroyAll();
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}, SHUTDOWN_GRACE_PERIOD_MS);
|
|
66
|
+
});
|
|
54
67
|
|
|
55
|
-
//
|
|
56
|
-
|
|
68
|
+
// Cleanup on uncaught exceptions (log but still exit)
|
|
69
|
+
process.on("uncaughtException", async (error) => {
|
|
70
|
+
console.error("[Container] Uncaught exception:", error);
|
|
71
|
+
await sessionManager.destroyAll();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Initialize Jupyter service with graceful degradation
|
|
76
|
+
const jupyterService = new JupyterService();
|
|
77
|
+
|
|
78
|
+
// Start Jupyter initialization in background (non-blocking)
|
|
79
|
+
console.log("[Container] Starting Jupyter initialization in background...");
|
|
80
|
+
console.log(
|
|
81
|
+
"[Container] API endpoints are available immediately. Jupyter-dependent features will be available shortly."
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
jupyterService
|
|
85
|
+
.initialize()
|
|
86
|
+
.then(() => {
|
|
87
|
+
console.log(
|
|
88
|
+
"[Container] Jupyter fully initialized - all features available"
|
|
89
|
+
);
|
|
90
|
+
})
|
|
91
|
+
.catch((error) => {
|
|
92
|
+
console.error("[Container] Jupyter initialization failed:", error.message);
|
|
93
|
+
console.error(
|
|
94
|
+
"[Container] The API will continue in degraded mode without code execution capabilities"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
57
97
|
|
|
58
98
|
const server = serve({
|
|
59
|
-
fetch(req: Request) {
|
|
99
|
+
async fetch(req: Request) {
|
|
60
100
|
const url = new URL(req.url);
|
|
61
101
|
const pathname = url.pathname;
|
|
62
102
|
|
|
@@ -89,108 +129,41 @@ const server = serve({
|
|
|
89
129
|
|
|
90
130
|
case "/api/session/create":
|
|
91
131
|
if (req.method === "POST") {
|
|
92
|
-
|
|
93
|
-
const sessionData: SessionData = {
|
|
94
|
-
activeProcess: null,
|
|
95
|
-
createdAt: new Date(),
|
|
96
|
-
sessionId,
|
|
97
|
-
};
|
|
98
|
-
sessions.set(sessionId, sessionData);
|
|
99
|
-
|
|
100
|
-
console.log(`[Server] Created new session: ${sessionId}`);
|
|
101
|
-
|
|
102
|
-
return new Response(
|
|
103
|
-
JSON.stringify({
|
|
104
|
-
message: "Session created successfully",
|
|
105
|
-
sessionId,
|
|
106
|
-
timestamp: new Date().toISOString(),
|
|
107
|
-
}),
|
|
108
|
-
{
|
|
109
|
-
headers: {
|
|
110
|
-
"Content-Type": "application/json",
|
|
111
|
-
...corsHeaders,
|
|
112
|
-
},
|
|
113
|
-
}
|
|
114
|
-
);
|
|
132
|
+
return handleCreateSession(req, corsHeaders, sessionManager);
|
|
115
133
|
}
|
|
116
134
|
break;
|
|
117
135
|
|
|
118
136
|
case "/api/session/list":
|
|
119
137
|
if (req.method === "GET") {
|
|
120
|
-
|
|
121
|
-
(session) => ({
|
|
122
|
-
createdAt: session.createdAt.toISOString(),
|
|
123
|
-
hasActiveProcess: !!session.activeProcess,
|
|
124
|
-
sessionId: session.sessionId,
|
|
125
|
-
})
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
return new Response(
|
|
129
|
-
JSON.stringify({
|
|
130
|
-
count: sessionList.length,
|
|
131
|
-
sessions: sessionList,
|
|
132
|
-
timestamp: new Date().toISOString(),
|
|
133
|
-
}),
|
|
134
|
-
{
|
|
135
|
-
headers: {
|
|
136
|
-
"Content-Type": "application/json",
|
|
137
|
-
...corsHeaders,
|
|
138
|
-
},
|
|
139
|
-
}
|
|
140
|
-
);
|
|
138
|
+
return handleListSessions(corsHeaders, sessionManager);
|
|
141
139
|
}
|
|
142
140
|
break;
|
|
143
141
|
|
|
144
142
|
case "/api/execute":
|
|
145
143
|
if (req.method === "POST") {
|
|
146
|
-
return handleExecuteRequest(
|
|
144
|
+
return handleExecuteRequest(req, corsHeaders, sessionManager);
|
|
147
145
|
}
|
|
148
146
|
break;
|
|
149
|
-
|
|
147
|
+
|
|
150
148
|
case "/api/execute/stream":
|
|
151
149
|
if (req.method === "POST") {
|
|
152
|
-
return handleStreamingExecuteRequest(
|
|
150
|
+
return handleStreamingExecuteRequest(req, sessionManager, corsHeaders);
|
|
153
151
|
}
|
|
154
152
|
break;
|
|
155
153
|
|
|
156
154
|
case "/api/ping":
|
|
157
155
|
if (req.method === "GET") {
|
|
156
|
+
const health = await jupyterService.getHealthStatus();
|
|
158
157
|
return new Response(
|
|
159
158
|
JSON.stringify({
|
|
160
159
|
message: "pong",
|
|
161
160
|
timestamp: new Date().toISOString(),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
|
|
173
|
-
case "/api/commands":
|
|
174
|
-
if (req.method === "GET") {
|
|
175
|
-
return new Response(
|
|
176
|
-
JSON.stringify({
|
|
177
|
-
availableCommands: [
|
|
178
|
-
"ls",
|
|
179
|
-
"pwd",
|
|
180
|
-
"echo",
|
|
181
|
-
"cat",
|
|
182
|
-
"grep",
|
|
183
|
-
"find",
|
|
184
|
-
"whoami",
|
|
185
|
-
"date",
|
|
186
|
-
"uptime",
|
|
187
|
-
"ps",
|
|
188
|
-
"top",
|
|
189
|
-
"df",
|
|
190
|
-
"du",
|
|
191
|
-
"free",
|
|
192
|
-
],
|
|
193
|
-
timestamp: new Date().toISOString(),
|
|
161
|
+
jupyter: health.ready
|
|
162
|
+
? "ready"
|
|
163
|
+
: health.initializing
|
|
164
|
+
? "initializing"
|
|
165
|
+
: "not ready",
|
|
166
|
+
jupyterHealth: health,
|
|
194
167
|
}),
|
|
195
168
|
{
|
|
196
169
|
headers: {
|
|
@@ -204,43 +177,49 @@ const server = serve({
|
|
|
204
177
|
|
|
205
178
|
case "/api/git/checkout":
|
|
206
179
|
if (req.method === "POST") {
|
|
207
|
-
return handleGitCheckoutRequest(
|
|
180
|
+
return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
|
|
208
181
|
}
|
|
209
182
|
break;
|
|
210
183
|
|
|
211
184
|
case "/api/mkdir":
|
|
212
185
|
if (req.method === "POST") {
|
|
213
|
-
return handleMkdirRequest(
|
|
186
|
+
return handleMkdirRequest(req, corsHeaders, sessionManager);
|
|
214
187
|
}
|
|
215
188
|
break;
|
|
216
189
|
|
|
217
190
|
case "/api/write":
|
|
218
191
|
if (req.method === "POST") {
|
|
219
|
-
return handleWriteFileRequest(req, corsHeaders);
|
|
192
|
+
return handleWriteFileRequest(req, corsHeaders, sessionManager);
|
|
220
193
|
}
|
|
221
194
|
break;
|
|
222
195
|
|
|
223
196
|
case "/api/read":
|
|
224
197
|
if (req.method === "POST") {
|
|
225
|
-
return handleReadFileRequest(req, corsHeaders);
|
|
198
|
+
return handleReadFileRequest(req, corsHeaders, sessionManager);
|
|
226
199
|
}
|
|
227
200
|
break;
|
|
228
201
|
|
|
229
202
|
case "/api/delete":
|
|
230
203
|
if (req.method === "POST") {
|
|
231
|
-
return handleDeleteFileRequest(req, corsHeaders);
|
|
204
|
+
return handleDeleteFileRequest(req, corsHeaders, sessionManager);
|
|
232
205
|
}
|
|
233
206
|
break;
|
|
234
207
|
|
|
235
208
|
case "/api/rename":
|
|
236
209
|
if (req.method === "POST") {
|
|
237
|
-
return handleRenameFileRequest(req, corsHeaders);
|
|
210
|
+
return handleRenameFileRequest(req, corsHeaders, sessionManager);
|
|
238
211
|
}
|
|
239
212
|
break;
|
|
240
213
|
|
|
241
214
|
case "/api/move":
|
|
242
215
|
if (req.method === "POST") {
|
|
243
|
-
return handleMoveFileRequest(req, corsHeaders);
|
|
216
|
+
return handleMoveFileRequest(req, corsHeaders, sessionManager);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case "/api/list-files":
|
|
221
|
+
if (req.method === "POST") {
|
|
222
|
+
return handleListFilesRequest(req, corsHeaders, sessionManager);
|
|
244
223
|
}
|
|
245
224
|
break;
|
|
246
225
|
|
|
@@ -264,38 +243,291 @@ const server = serve({
|
|
|
264
243
|
|
|
265
244
|
case "/api/process/start":
|
|
266
245
|
if (req.method === "POST") {
|
|
267
|
-
return handleStartProcessRequest(
|
|
246
|
+
return handleStartProcessRequest(req, corsHeaders, sessionManager);
|
|
268
247
|
}
|
|
269
248
|
break;
|
|
270
249
|
|
|
271
250
|
case "/api/process/list":
|
|
272
251
|
if (req.method === "GET") {
|
|
273
|
-
return handleListProcessesRequest(
|
|
252
|
+
return handleListProcessesRequest(req, corsHeaders, sessionManager);
|
|
274
253
|
}
|
|
275
254
|
break;
|
|
276
255
|
|
|
277
256
|
case "/api/process/kill-all":
|
|
278
257
|
if (req.method === "DELETE") {
|
|
279
|
-
return handleKillAllProcessesRequest(
|
|
258
|
+
return handleKillAllProcessesRequest(
|
|
259
|
+
req,
|
|
260
|
+
corsHeaders,
|
|
261
|
+
sessionManager
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case "/api/contexts":
|
|
267
|
+
if (req.method === "POST") {
|
|
268
|
+
try {
|
|
269
|
+
const body = (await req.json()) as CreateContextRequest;
|
|
270
|
+
const context = await jupyterService.createContext(body);
|
|
271
|
+
return new Response(
|
|
272
|
+
JSON.stringify({
|
|
273
|
+
id: context.id,
|
|
274
|
+
language: context.language,
|
|
275
|
+
cwd: context.cwd,
|
|
276
|
+
createdAt: context.createdAt,
|
|
277
|
+
lastUsed: context.lastUsed,
|
|
278
|
+
}),
|
|
279
|
+
{
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "application/json",
|
|
282
|
+
...corsHeaders,
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof JupyterNotReadyError) {
|
|
288
|
+
console.log(
|
|
289
|
+
`[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
|
|
290
|
+
);
|
|
291
|
+
return new Response(
|
|
292
|
+
JSON.stringify({
|
|
293
|
+
error: error.message,
|
|
294
|
+
status: "initializing",
|
|
295
|
+
progress: error.progress,
|
|
296
|
+
}),
|
|
297
|
+
{
|
|
298
|
+
status: 503,
|
|
299
|
+
headers: {
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
"Retry-After": String(error.retryAfter),
|
|
302
|
+
...corsHeaders,
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if it's a circuit breaker error
|
|
309
|
+
if (
|
|
310
|
+
error instanceof Error &&
|
|
311
|
+
error.message.includes("Circuit breaker is open")
|
|
312
|
+
) {
|
|
313
|
+
console.log(
|
|
314
|
+
"[Container] Circuit breaker is open:",
|
|
315
|
+
error.message
|
|
316
|
+
);
|
|
317
|
+
return new Response(
|
|
318
|
+
JSON.stringify({
|
|
319
|
+
error:
|
|
320
|
+
"Service temporarily unavailable due to high error rate. Please try again later.",
|
|
321
|
+
status: "circuit_open",
|
|
322
|
+
details: error.message,
|
|
323
|
+
}),
|
|
324
|
+
{
|
|
325
|
+
status: 503,
|
|
326
|
+
headers: {
|
|
327
|
+
"Content-Type": "application/json",
|
|
328
|
+
"Retry-After": "60",
|
|
329
|
+
...corsHeaders,
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Only log actual errors with stack traces
|
|
336
|
+
console.error("[Container] Error creating context:", error);
|
|
337
|
+
return new Response(
|
|
338
|
+
JSON.stringify({
|
|
339
|
+
error:
|
|
340
|
+
error instanceof Error
|
|
341
|
+
? error.message
|
|
342
|
+
: "Failed to create context",
|
|
343
|
+
}),
|
|
344
|
+
{
|
|
345
|
+
status: 500,
|
|
346
|
+
headers: {
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
...corsHeaders,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
} else if (req.method === "GET") {
|
|
354
|
+
const contexts = await jupyterService.listContexts();
|
|
355
|
+
return new Response(JSON.stringify({ contexts }), {
|
|
356
|
+
headers: {
|
|
357
|
+
"Content-Type": "application/json",
|
|
358
|
+
...corsHeaders,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
|
|
364
|
+
case "/api/execute/code":
|
|
365
|
+
if (req.method === "POST") {
|
|
366
|
+
try {
|
|
367
|
+
const body = (await req.json()) as {
|
|
368
|
+
context_id: string;
|
|
369
|
+
code: string;
|
|
370
|
+
language?: string;
|
|
371
|
+
};
|
|
372
|
+
return await jupyterService.executeCode(
|
|
373
|
+
body.context_id,
|
|
374
|
+
body.code,
|
|
375
|
+
body.language
|
|
376
|
+
);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
// Check if it's a circuit breaker error
|
|
379
|
+
if (
|
|
380
|
+
error instanceof Error &&
|
|
381
|
+
error.message.includes("Circuit breaker is open")
|
|
382
|
+
) {
|
|
383
|
+
console.log(
|
|
384
|
+
"[Container] Circuit breaker is open for code execution:",
|
|
385
|
+
error.message
|
|
386
|
+
);
|
|
387
|
+
return new Response(
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
error:
|
|
390
|
+
"Service temporarily unavailable due to high error rate. Please try again later.",
|
|
391
|
+
status: "circuit_open",
|
|
392
|
+
details: error.message,
|
|
393
|
+
}),
|
|
394
|
+
{
|
|
395
|
+
status: 503,
|
|
396
|
+
headers: {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
"Retry-After": "30",
|
|
399
|
+
...corsHeaders,
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Don't log stack traces for expected initialization state
|
|
406
|
+
if (
|
|
407
|
+
error instanceof Error &&
|
|
408
|
+
error.message.includes("initializing")
|
|
409
|
+
) {
|
|
410
|
+
console.log(
|
|
411
|
+
"[Container] Code execution deferred - Jupyter still initializing"
|
|
412
|
+
);
|
|
413
|
+
} else {
|
|
414
|
+
console.error("[Container] Error executing code:", error);
|
|
415
|
+
}
|
|
416
|
+
// Error response is already handled by jupyterService.executeCode for not ready state
|
|
417
|
+
return new Response(
|
|
418
|
+
JSON.stringify({
|
|
419
|
+
error:
|
|
420
|
+
error instanceof Error
|
|
421
|
+
? error.message
|
|
422
|
+
: "Failed to execute code",
|
|
423
|
+
}),
|
|
424
|
+
{
|
|
425
|
+
status: 500,
|
|
426
|
+
headers: {
|
|
427
|
+
"Content-Type": "application/json",
|
|
428
|
+
...corsHeaders,
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
}
|
|
280
433
|
}
|
|
281
434
|
break;
|
|
282
435
|
|
|
283
436
|
default:
|
|
437
|
+
// Handle dynamic routes for contexts
|
|
438
|
+
if (
|
|
439
|
+
pathname.startsWith("/api/contexts/") &&
|
|
440
|
+
pathname.split("/").length === 4
|
|
441
|
+
) {
|
|
442
|
+
const contextId = pathname.split("/")[3];
|
|
443
|
+
if (req.method === "DELETE") {
|
|
444
|
+
try {
|
|
445
|
+
await jupyterService.deleteContext(contextId);
|
|
446
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
447
|
+
headers: {
|
|
448
|
+
"Content-Type": "application/json",
|
|
449
|
+
...corsHeaders,
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (error instanceof JupyterNotReadyError) {
|
|
454
|
+
console.log(
|
|
455
|
+
`[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
|
|
456
|
+
);
|
|
457
|
+
return new Response(
|
|
458
|
+
JSON.stringify({
|
|
459
|
+
error: error.message,
|
|
460
|
+
status: "initializing",
|
|
461
|
+
progress: error.progress,
|
|
462
|
+
}),
|
|
463
|
+
{
|
|
464
|
+
status: 503,
|
|
465
|
+
headers: {
|
|
466
|
+
"Content-Type": "application/json",
|
|
467
|
+
"Retry-After": "5",
|
|
468
|
+
...corsHeaders,
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return new Response(
|
|
474
|
+
JSON.stringify({
|
|
475
|
+
error:
|
|
476
|
+
error instanceof Error
|
|
477
|
+
? error.message
|
|
478
|
+
: "Failed to delete context",
|
|
479
|
+
}),
|
|
480
|
+
{
|
|
481
|
+
status:
|
|
482
|
+
error instanceof Error &&
|
|
483
|
+
error.message.includes("not found")
|
|
484
|
+
? 404
|
|
485
|
+
: 500,
|
|
486
|
+
headers: {
|
|
487
|
+
"Content-Type": "application/json",
|
|
488
|
+
...corsHeaders,
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
284
496
|
// Handle dynamic routes for individual processes
|
|
285
497
|
if (pathname.startsWith("/api/process/")) {
|
|
286
|
-
const segments = pathname.split(
|
|
498
|
+
const segments = pathname.split("/");
|
|
287
499
|
if (segments.length >= 4) {
|
|
288
500
|
const processId = segments[3];
|
|
289
501
|
const action = segments[4]; // Optional: logs, stream, etc.
|
|
290
502
|
|
|
291
503
|
if (!action && req.method === "GET") {
|
|
292
|
-
return handleGetProcessRequest(
|
|
504
|
+
return handleGetProcessRequest(
|
|
505
|
+
req,
|
|
506
|
+
corsHeaders,
|
|
507
|
+
processId,
|
|
508
|
+
sessionManager
|
|
509
|
+
);
|
|
293
510
|
} else if (!action && req.method === "DELETE") {
|
|
294
|
-
return handleKillProcessRequest(
|
|
511
|
+
return handleKillProcessRequest(
|
|
512
|
+
req,
|
|
513
|
+
corsHeaders,
|
|
514
|
+
processId,
|
|
515
|
+
sessionManager
|
|
516
|
+
);
|
|
295
517
|
} else if (action === "logs" && req.method === "GET") {
|
|
296
|
-
return handleGetProcessLogsRequest(
|
|
518
|
+
return handleGetProcessLogsRequest(
|
|
519
|
+
req,
|
|
520
|
+
corsHeaders,
|
|
521
|
+
processId,
|
|
522
|
+
sessionManager
|
|
523
|
+
);
|
|
297
524
|
} else if (action === "stream" && req.method === "GET") {
|
|
298
|
-
return handleStreamProcessLogsRequest(
|
|
525
|
+
return handleStreamProcessLogsRequest(
|
|
526
|
+
req,
|
|
527
|
+
corsHeaders,
|
|
528
|
+
processId,
|
|
529
|
+
sessionManager
|
|
530
|
+
);
|
|
299
531
|
}
|
|
300
532
|
}
|
|
301
533
|
}
|
|
@@ -311,7 +543,10 @@ const server = serve({
|
|
|
311
543
|
});
|
|
312
544
|
}
|
|
313
545
|
} catch (error) {
|
|
314
|
-
console.error(
|
|
546
|
+
console.error(
|
|
547
|
+
`[Container] Error handling ${req.method} ${pathname}:`,
|
|
548
|
+
error
|
|
549
|
+
);
|
|
315
550
|
return new Response(
|
|
316
551
|
JSON.stringify({
|
|
317
552
|
error: "Internal server error",
|
|
@@ -330,7 +565,7 @@ const server = serve({
|
|
|
330
565
|
hostname: "0.0.0.0",
|
|
331
566
|
port: 3000,
|
|
332
567
|
// We don't need this, but typescript complains
|
|
333
|
-
websocket: { async message() {
|
|
568
|
+
websocket: { async message() {} },
|
|
334
569
|
});
|
|
335
570
|
|
|
336
571
|
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
@@ -357,5 +592,10 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
|
|
|
357
592
|
console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
|
|
358
593
|
console.log(` DELETE /api/process/kill-all - Kill all processes`);
|
|
359
594
|
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
595
|
+
console.log(` POST /api/contexts - Create a code execution context`);
|
|
596
|
+
console.log(` GET /api/contexts - List all contexts`);
|
|
597
|
+
console.log(` DELETE /api/contexts/{id} - Delete a context`);
|
|
598
|
+
console.log(
|
|
599
|
+
` POST /api/execute/code - Execute code in a context (streaming)`
|
|
600
|
+
);
|
|
360
601
|
console.log(` GET /api/ping - Health check`);
|
|
361
|
-
console.log(` GET /api/commands - List available commands`);
|