@cloudflare/sandbox 0.2.4 → 0.3.1
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 +75 -0
- package/Dockerfile +9 -11
- package/README.md +69 -7
- package/container_src/control-process.ts +784 -0
- package/container_src/handler/exec.ts +99 -254
- package/container_src/handler/file.ts +179 -837
- 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 +68 -130
- package/container_src/isolation.ts +1038 -0
- package/container_src/shell-escape.ts +42 -0
- package/container_src/types.ts +27 -13
- package/dist/{chunk-HHUDRGPY.js → chunk-BEQUGUY4.js} +2 -2
- package/dist/{chunk-CKIGERRS.js → chunk-LFLJGISB.js} +240 -264
- package/dist/chunk-LFLJGISB.js.map +1 -0
- package/dist/{chunk-3CQ6THKA.js → chunk-SMUEY5JR.js} +85 -103
- package/dist/chunk-SMUEY5JR.js.map +1 -0
- package/dist/{client-Ce40ujDF.d.ts → client-Dny_ro_v.d.ts} +41 -25
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -9
- package/dist/interpreter.d.ts +1 -1
- package/dist/jupyter-client.d.ts +1 -1
- package/dist/jupyter-client.js +2 -2
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +3 -5
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +3 -5
- package/dist/types.d.ts +10 -21
- package/dist/types.js +35 -9
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +120 -135
- package/src/index.ts +8 -0
- package/src/sandbox.ts +290 -331
- package/src/types.ts +15 -24
- package/dist/chunk-3CQ6THKA.js.map +0 -1
- package/dist/chunk-6EWSYSO7.js +0 -46
- package/dist/chunk-6EWSYSO7.js.map +0 -1
- package/dist/chunk-CKIGERRS.js.map +0 -1
- /package/dist/{chunk-HHUDRGPY.js.map → chunk-BEQUGUY4.js.map} +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { SessionManager } from "../isolation";
|
|
2
|
+
import type { CreateSessionRequest } from "../types";
|
|
3
|
+
|
|
4
|
+
export async function handleCreateSession(
|
|
5
|
+
req: Request,
|
|
6
|
+
corsHeaders: Record<string, string>,
|
|
7
|
+
sessionManager: SessionManager
|
|
8
|
+
) {
|
|
9
|
+
try {
|
|
10
|
+
const body = (await req.json()) as CreateSessionRequest;
|
|
11
|
+
const { id, env, cwd, isolation } = body;
|
|
12
|
+
|
|
13
|
+
if (!id) {
|
|
14
|
+
return new Response(
|
|
15
|
+
JSON.stringify({ error: "Session ID is required" }),
|
|
16
|
+
{
|
|
17
|
+
status: 400,
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
...corsHeaders,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await sessionManager.createSession({
|
|
27
|
+
id,
|
|
28
|
+
env: env || {},
|
|
29
|
+
cwd: cwd || "/workspace",
|
|
30
|
+
isolation: isolation !== false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log(`[Container] Session '${id}' created successfully`);
|
|
34
|
+
console.log(
|
|
35
|
+
`[Container] Available sessions now: ${sessionManager
|
|
36
|
+
.listSessions()
|
|
37
|
+
.join(", ")}`
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return new Response(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
success: true,
|
|
43
|
+
id,
|
|
44
|
+
message: `Session '${id}' created with${
|
|
45
|
+
isolation !== false ? "" : "out"
|
|
46
|
+
} isolation`,
|
|
47
|
+
}),
|
|
48
|
+
{
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
...corsHeaders,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("[Container] Failed to create session:", error);
|
|
57
|
+
return new Response(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
error: "Failed to create session",
|
|
60
|
+
message:
|
|
61
|
+
error instanceof Error ? error.message : String(error),
|
|
62
|
+
}),
|
|
63
|
+
{
|
|
64
|
+
status: 500,
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
...corsHeaders,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function handleListSessions(
|
|
75
|
+
corsHeaders: Record<string, string>,
|
|
76
|
+
sessionManager: SessionManager
|
|
77
|
+
) {
|
|
78
|
+
const sessionList = sessionManager.listSessions();
|
|
79
|
+
return new Response(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
count: sessionList.length,
|
|
82
|
+
sessions: sessionList,
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
}),
|
|
85
|
+
{
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
...corsHeaders,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
package/container_src/index.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { serve } from "bun";
|
|
3
|
-
import {
|
|
4
|
-
handleExecuteRequest,
|
|
5
|
-
handleStreamingExecuteRequest,
|
|
6
|
-
} from "./handler/exec";
|
|
2
|
+
import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
|
|
7
3
|
import {
|
|
8
4
|
handleDeleteFileRequest,
|
|
9
5
|
handleListFilesRequest,
|
|
@@ -29,37 +25,52 @@ import {
|
|
|
29
25
|
handleStartProcessRequest,
|
|
30
26
|
handleStreamProcessLogsRequest,
|
|
31
27
|
} from "./handler/process";
|
|
28
|
+
import { handleCreateSession, handleListSessions } from "./handler/session";
|
|
29
|
+
import { hasNamespaceSupport, SessionManager } from "./isolation";
|
|
32
30
|
import type { CreateContextRequest } from "./jupyter-server";
|
|
33
31
|
import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
|
|
34
|
-
import type { ProcessRecord, SessionData } from "./types";
|
|
35
|
-
|
|
36
|
-
// In-memory session storage (in production, you'd want to use a proper database)
|
|
37
|
-
const sessions = new Map<string, SessionData>();
|
|
38
32
|
|
|
39
33
|
// In-memory storage for exposed ports
|
|
40
34
|
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
41
35
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
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)
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
});
|
|
67
|
+
|
|
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
|
+
});
|
|
63
74
|
|
|
64
75
|
// Initialize Jupyter service with graceful degradation
|
|
65
76
|
const jupyterService = new JupyterService();
|
|
@@ -118,67 +129,25 @@ const server = serve({
|
|
|
118
129
|
|
|
119
130
|
case "/api/session/create":
|
|
120
131
|
if (req.method === "POST") {
|
|
121
|
-
|
|
122
|
-
const sessionData: SessionData = {
|
|
123
|
-
activeProcess: null,
|
|
124
|
-
createdAt: new Date(),
|
|
125
|
-
sessionId,
|
|
126
|
-
};
|
|
127
|
-
sessions.set(sessionId, sessionData);
|
|
128
|
-
|
|
129
|
-
console.log(`[Server] Created new session: ${sessionId}`);
|
|
130
|
-
|
|
131
|
-
return new Response(
|
|
132
|
-
JSON.stringify({
|
|
133
|
-
message: "Session created successfully",
|
|
134
|
-
sessionId,
|
|
135
|
-
timestamp: new Date().toISOString(),
|
|
136
|
-
}),
|
|
137
|
-
{
|
|
138
|
-
headers: {
|
|
139
|
-
"Content-Type": "application/json",
|
|
140
|
-
...corsHeaders,
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
);
|
|
132
|
+
return handleCreateSession(req, corsHeaders, sessionManager);
|
|
144
133
|
}
|
|
145
134
|
break;
|
|
146
135
|
|
|
147
136
|
case "/api/session/list":
|
|
148
137
|
if (req.method === "GET") {
|
|
149
|
-
|
|
150
|
-
(session) => ({
|
|
151
|
-
createdAt: session.createdAt.toISOString(),
|
|
152
|
-
hasActiveProcess: !!session.activeProcess,
|
|
153
|
-
sessionId: session.sessionId,
|
|
154
|
-
})
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
return new Response(
|
|
158
|
-
JSON.stringify({
|
|
159
|
-
count: sessionList.length,
|
|
160
|
-
sessions: sessionList,
|
|
161
|
-
timestamp: new Date().toISOString(),
|
|
162
|
-
}),
|
|
163
|
-
{
|
|
164
|
-
headers: {
|
|
165
|
-
"Content-Type": "application/json",
|
|
166
|
-
...corsHeaders,
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
);
|
|
138
|
+
return handleListSessions(corsHeaders, sessionManager);
|
|
170
139
|
}
|
|
171
140
|
break;
|
|
172
141
|
|
|
173
142
|
case "/api/execute":
|
|
174
143
|
if (req.method === "POST") {
|
|
175
|
-
return handleExecuteRequest(
|
|
144
|
+
return handleExecuteRequest(req, corsHeaders, sessionManager);
|
|
176
145
|
}
|
|
177
146
|
break;
|
|
178
|
-
|
|
147
|
+
|
|
179
148
|
case "/api/execute/stream":
|
|
180
149
|
if (req.method === "POST") {
|
|
181
|
-
return handleStreamingExecuteRequest(
|
|
150
|
+
return handleStreamingExecuteRequest(req, sessionManager, corsHeaders);
|
|
182
151
|
}
|
|
183
152
|
break;
|
|
184
153
|
|
|
@@ -206,83 +175,51 @@ const server = serve({
|
|
|
206
175
|
}
|
|
207
176
|
break;
|
|
208
177
|
|
|
209
|
-
case "/api/commands":
|
|
210
|
-
if (req.method === "GET") {
|
|
211
|
-
return new Response(
|
|
212
|
-
JSON.stringify({
|
|
213
|
-
availableCommands: [
|
|
214
|
-
"ls",
|
|
215
|
-
"pwd",
|
|
216
|
-
"echo",
|
|
217
|
-
"cat",
|
|
218
|
-
"grep",
|
|
219
|
-
"find",
|
|
220
|
-
"whoami",
|
|
221
|
-
"date",
|
|
222
|
-
"uptime",
|
|
223
|
-
"ps",
|
|
224
|
-
"top",
|
|
225
|
-
"df",
|
|
226
|
-
"du",
|
|
227
|
-
"free",
|
|
228
|
-
],
|
|
229
|
-
timestamp: new Date().toISOString(),
|
|
230
|
-
}),
|
|
231
|
-
{
|
|
232
|
-
headers: {
|
|
233
|
-
"Content-Type": "application/json",
|
|
234
|
-
...corsHeaders,
|
|
235
|
-
},
|
|
236
|
-
}
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
break;
|
|
240
|
-
|
|
241
178
|
case "/api/git/checkout":
|
|
242
179
|
if (req.method === "POST") {
|
|
243
|
-
return handleGitCheckoutRequest(
|
|
180
|
+
return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
|
|
244
181
|
}
|
|
245
182
|
break;
|
|
246
183
|
|
|
247
184
|
case "/api/mkdir":
|
|
248
185
|
if (req.method === "POST") {
|
|
249
|
-
return handleMkdirRequest(
|
|
186
|
+
return handleMkdirRequest(req, corsHeaders, sessionManager);
|
|
250
187
|
}
|
|
251
188
|
break;
|
|
252
189
|
|
|
253
190
|
case "/api/write":
|
|
254
191
|
if (req.method === "POST") {
|
|
255
|
-
return handleWriteFileRequest(req, corsHeaders);
|
|
192
|
+
return handleWriteFileRequest(req, corsHeaders, sessionManager);
|
|
256
193
|
}
|
|
257
194
|
break;
|
|
258
195
|
|
|
259
196
|
case "/api/read":
|
|
260
197
|
if (req.method === "POST") {
|
|
261
|
-
return handleReadFileRequest(req, corsHeaders);
|
|
198
|
+
return handleReadFileRequest(req, corsHeaders, sessionManager);
|
|
262
199
|
}
|
|
263
200
|
break;
|
|
264
201
|
|
|
265
202
|
case "/api/delete":
|
|
266
203
|
if (req.method === "POST") {
|
|
267
|
-
return handleDeleteFileRequest(req, corsHeaders);
|
|
204
|
+
return handleDeleteFileRequest(req, corsHeaders, sessionManager);
|
|
268
205
|
}
|
|
269
206
|
break;
|
|
270
207
|
|
|
271
208
|
case "/api/rename":
|
|
272
209
|
if (req.method === "POST") {
|
|
273
|
-
return handleRenameFileRequest(req, corsHeaders);
|
|
210
|
+
return handleRenameFileRequest(req, corsHeaders, sessionManager);
|
|
274
211
|
}
|
|
275
212
|
break;
|
|
276
213
|
|
|
277
214
|
case "/api/move":
|
|
278
215
|
if (req.method === "POST") {
|
|
279
|
-
return handleMoveFileRequest(req, corsHeaders);
|
|
216
|
+
return handleMoveFileRequest(req, corsHeaders, sessionManager);
|
|
280
217
|
}
|
|
281
218
|
break;
|
|
282
219
|
|
|
283
220
|
case "/api/list-files":
|
|
284
221
|
if (req.method === "POST") {
|
|
285
|
-
return handleListFilesRequest(req, corsHeaders);
|
|
222
|
+
return handleListFilesRequest(req, corsHeaders, sessionManager);
|
|
286
223
|
}
|
|
287
224
|
break;
|
|
288
225
|
|
|
@@ -306,23 +243,26 @@ const server = serve({
|
|
|
306
243
|
|
|
307
244
|
case "/api/process/start":
|
|
308
245
|
if (req.method === "POST") {
|
|
309
|
-
return handleStartProcessRequest(
|
|
246
|
+
return handleStartProcessRequest(req, corsHeaders, sessionManager);
|
|
310
247
|
}
|
|
311
248
|
break;
|
|
312
249
|
|
|
313
250
|
case "/api/process/list":
|
|
314
251
|
if (req.method === "GET") {
|
|
315
|
-
return handleListProcessesRequest(
|
|
252
|
+
return handleListProcessesRequest(req, corsHeaders, sessionManager);
|
|
316
253
|
}
|
|
317
254
|
break;
|
|
318
255
|
|
|
319
256
|
case "/api/process/kill-all":
|
|
320
257
|
if (req.method === "DELETE") {
|
|
321
|
-
return handleKillAllProcessesRequest(
|
|
258
|
+
return handleKillAllProcessesRequest(
|
|
259
|
+
req,
|
|
260
|
+
corsHeaders,
|
|
261
|
+
sessionManager
|
|
262
|
+
);
|
|
322
263
|
}
|
|
323
264
|
break;
|
|
324
265
|
|
|
325
|
-
// Code interpreter endpoints
|
|
326
266
|
case "/api/contexts":
|
|
327
267
|
if (req.method === "POST") {
|
|
328
268
|
try {
|
|
@@ -345,7 +285,6 @@ const server = serve({
|
|
|
345
285
|
);
|
|
346
286
|
} catch (error) {
|
|
347
287
|
if (error instanceof JupyterNotReadyError) {
|
|
348
|
-
// This happens when request times out waiting for Jupyter
|
|
349
288
|
console.log(
|
|
350
289
|
`[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
|
|
351
290
|
);
|
|
@@ -563,31 +502,31 @@ const server = serve({
|
|
|
563
502
|
|
|
564
503
|
if (!action && req.method === "GET") {
|
|
565
504
|
return handleGetProcessRequest(
|
|
566
|
-
processes,
|
|
567
505
|
req,
|
|
568
506
|
corsHeaders,
|
|
569
|
-
processId
|
|
507
|
+
processId,
|
|
508
|
+
sessionManager
|
|
570
509
|
);
|
|
571
510
|
} else if (!action && req.method === "DELETE") {
|
|
572
511
|
return handleKillProcessRequest(
|
|
573
|
-
processes,
|
|
574
512
|
req,
|
|
575
513
|
corsHeaders,
|
|
576
|
-
processId
|
|
514
|
+
processId,
|
|
515
|
+
sessionManager
|
|
577
516
|
);
|
|
578
517
|
} else if (action === "logs" && req.method === "GET") {
|
|
579
518
|
return handleGetProcessLogsRequest(
|
|
580
|
-
processes,
|
|
581
519
|
req,
|
|
582
520
|
corsHeaders,
|
|
583
|
-
processId
|
|
521
|
+
processId,
|
|
522
|
+
sessionManager
|
|
584
523
|
);
|
|
585
524
|
} else if (action === "stream" && req.method === "GET") {
|
|
586
525
|
return handleStreamProcessLogsRequest(
|
|
587
|
-
processes,
|
|
588
526
|
req,
|
|
589
527
|
corsHeaders,
|
|
590
|
-
processId
|
|
528
|
+
processId,
|
|
529
|
+
sessionManager
|
|
591
530
|
);
|
|
592
531
|
}
|
|
593
532
|
}
|
|
@@ -660,4 +599,3 @@ console.log(
|
|
|
660
599
|
` POST /api/execute/code - Execute code in a context (streaming)`
|
|
661
600
|
);
|
|
662
601
|
console.log(` GET /api/ping - Health check`);
|
|
663
|
-
console.log(` GET /api/commands - List available commands`);
|