@cloudflare/sandbox 0.2.3 → 0.3.0
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 +69 -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 +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 +74 -129
- package/container_src/isolation.ts +1039 -0
- package/container_src/jupyter-service.ts +8 -5
- package/container_src/shell-escape.ts +42 -0
- package/container_src/types.ts +35 -12
- package/dist/{chunk-VTKZL632.js → chunk-BEQUGUY4.js} +2 -2
- package/dist/{chunk-4KELYYKS.js → chunk-GTGWAEED.js} +239 -265
- package/dist/chunk-GTGWAEED.js.map +1 -0
- package/dist/{chunk-CUHYLCMT.js → chunk-SMUEY5JR.js} +111 -99
- package/dist/chunk-SMUEY5JR.js.map +1 -0
- package/dist/{client-bzEV222a.d.ts → client-Dny_ro_v.d.ts} +48 -84
- 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 +2 -2
- package/dist/jupyter-client.d.ts +2 -2
- package/dist/jupyter-client.js +2 -2
- package/dist/request-handler.d.ts +3 -3
- package/dist/request-handler.js +3 -5
- package/dist/sandbox.d.ts +2 -2
- package/dist/sandbox.js +3 -5
- package/dist/types.d.ts +127 -21
- package/dist/types.js +35 -9
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +175 -187
- package/src/index.ts +23 -13
- package/src/sandbox.ts +297 -332
- package/src/types.ts +125 -24
- package/dist/chunk-4KELYYKS.js.map +0 -1
- package/dist/chunk-CUHYLCMT.js.map +0 -1
- package/dist/chunk-S5FFBU4Y.js +0 -46
- package/dist/chunk-S5FFBU4Y.js.map +0 -1
- /package/dist/{chunk-VTKZL632.js.map → chunk-BEQUGUY4.js.map} +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { SessionManager } from "../isolation";
|
|
2
|
+
import { 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,11 +1,8 @@
|
|
|
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,
|
|
5
|
+
handleListFilesRequest,
|
|
9
6
|
handleMkdirRequest,
|
|
10
7
|
handleMoveFileRequest,
|
|
11
8
|
handleReadFileRequest,
|
|
@@ -28,37 +25,52 @@ import {
|
|
|
28
25
|
handleStartProcessRequest,
|
|
29
26
|
handleStreamProcessLogsRequest,
|
|
30
27
|
} from "./handler/process";
|
|
28
|
+
import { handleCreateSession, handleListSessions } from "./handler/session";
|
|
29
|
+
import { hasNamespaceSupport, SessionManager } from "./isolation";
|
|
31
30
|
import type { CreateContextRequest } from "./jupyter-server";
|
|
32
31
|
import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
|
|
33
|
-
import type { ProcessRecord, SessionData } from "./types";
|
|
34
|
-
|
|
35
|
-
// In-memory session storage (in production, you'd want to use a proper database)
|
|
36
|
-
const sessions = new Map<string, SessionData>();
|
|
37
32
|
|
|
38
33
|
// In-memory storage for exposed ports
|
|
39
34
|
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
40
35
|
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (session.createdAt < oneHourAgo && !session.activeProcess) {
|
|
54
|
-
sessions.delete(sessionId);
|
|
55
|
-
console.log(`[Server] Cleaned up old session: ${sessionId}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
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();
|
|
59
48
|
|
|
60
|
-
//
|
|
61
|
-
|
|
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
|
+
});
|
|
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
|
+
});
|
|
62
74
|
|
|
63
75
|
// Initialize Jupyter service with graceful degradation
|
|
64
76
|
const jupyterService = new JupyterService();
|
|
@@ -117,67 +129,25 @@ const server = serve({
|
|
|
117
129
|
|
|
118
130
|
case "/api/session/create":
|
|
119
131
|
if (req.method === "POST") {
|
|
120
|
-
|
|
121
|
-
const sessionData: SessionData = {
|
|
122
|
-
activeProcess: null,
|
|
123
|
-
createdAt: new Date(),
|
|
124
|
-
sessionId,
|
|
125
|
-
};
|
|
126
|
-
sessions.set(sessionId, sessionData);
|
|
127
|
-
|
|
128
|
-
console.log(`[Server] Created new session: ${sessionId}`);
|
|
129
|
-
|
|
130
|
-
return new Response(
|
|
131
|
-
JSON.stringify({
|
|
132
|
-
message: "Session created successfully",
|
|
133
|
-
sessionId,
|
|
134
|
-
timestamp: new Date().toISOString(),
|
|
135
|
-
}),
|
|
136
|
-
{
|
|
137
|
-
headers: {
|
|
138
|
-
"Content-Type": "application/json",
|
|
139
|
-
...corsHeaders,
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
);
|
|
132
|
+
return handleCreateSession(req, corsHeaders, sessionManager);
|
|
143
133
|
}
|
|
144
134
|
break;
|
|
145
135
|
|
|
146
136
|
case "/api/session/list":
|
|
147
137
|
if (req.method === "GET") {
|
|
148
|
-
|
|
149
|
-
(session) => ({
|
|
150
|
-
createdAt: session.createdAt.toISOString(),
|
|
151
|
-
hasActiveProcess: !!session.activeProcess,
|
|
152
|
-
sessionId: session.sessionId,
|
|
153
|
-
})
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
return new Response(
|
|
157
|
-
JSON.stringify({
|
|
158
|
-
count: sessionList.length,
|
|
159
|
-
sessions: sessionList,
|
|
160
|
-
timestamp: new Date().toISOString(),
|
|
161
|
-
}),
|
|
162
|
-
{
|
|
163
|
-
headers: {
|
|
164
|
-
"Content-Type": "application/json",
|
|
165
|
-
...corsHeaders,
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
);
|
|
138
|
+
return handleListSessions(corsHeaders, sessionManager);
|
|
169
139
|
}
|
|
170
140
|
break;
|
|
171
141
|
|
|
172
142
|
case "/api/execute":
|
|
173
143
|
if (req.method === "POST") {
|
|
174
|
-
return handleExecuteRequest(
|
|
144
|
+
return handleExecuteRequest(req, corsHeaders, sessionManager);
|
|
175
145
|
}
|
|
176
146
|
break;
|
|
177
|
-
|
|
147
|
+
|
|
178
148
|
case "/api/execute/stream":
|
|
179
149
|
if (req.method === "POST") {
|
|
180
|
-
return handleStreamingExecuteRequest(
|
|
150
|
+
return handleStreamingExecuteRequest(req, sessionManager, corsHeaders);
|
|
181
151
|
}
|
|
182
152
|
break;
|
|
183
153
|
|
|
@@ -205,77 +175,51 @@ const server = serve({
|
|
|
205
175
|
}
|
|
206
176
|
break;
|
|
207
177
|
|
|
208
|
-
case "/api/commands":
|
|
209
|
-
if (req.method === "GET") {
|
|
210
|
-
return new Response(
|
|
211
|
-
JSON.stringify({
|
|
212
|
-
availableCommands: [
|
|
213
|
-
"ls",
|
|
214
|
-
"pwd",
|
|
215
|
-
"echo",
|
|
216
|
-
"cat",
|
|
217
|
-
"grep",
|
|
218
|
-
"find",
|
|
219
|
-
"whoami",
|
|
220
|
-
"date",
|
|
221
|
-
"uptime",
|
|
222
|
-
"ps",
|
|
223
|
-
"top",
|
|
224
|
-
"df",
|
|
225
|
-
"du",
|
|
226
|
-
"free",
|
|
227
|
-
],
|
|
228
|
-
timestamp: new Date().toISOString(),
|
|
229
|
-
}),
|
|
230
|
-
{
|
|
231
|
-
headers: {
|
|
232
|
-
"Content-Type": "application/json",
|
|
233
|
-
...corsHeaders,
|
|
234
|
-
},
|
|
235
|
-
}
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
break;
|
|
239
|
-
|
|
240
178
|
case "/api/git/checkout":
|
|
241
179
|
if (req.method === "POST") {
|
|
242
|
-
return handleGitCheckoutRequest(
|
|
180
|
+
return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
|
|
243
181
|
}
|
|
244
182
|
break;
|
|
245
183
|
|
|
246
184
|
case "/api/mkdir":
|
|
247
185
|
if (req.method === "POST") {
|
|
248
|
-
return handleMkdirRequest(
|
|
186
|
+
return handleMkdirRequest(req, corsHeaders, sessionManager);
|
|
249
187
|
}
|
|
250
188
|
break;
|
|
251
189
|
|
|
252
190
|
case "/api/write":
|
|
253
191
|
if (req.method === "POST") {
|
|
254
|
-
return handleWriteFileRequest(req, corsHeaders);
|
|
192
|
+
return handleWriteFileRequest(req, corsHeaders, sessionManager);
|
|
255
193
|
}
|
|
256
194
|
break;
|
|
257
195
|
|
|
258
196
|
case "/api/read":
|
|
259
197
|
if (req.method === "POST") {
|
|
260
|
-
return handleReadFileRequest(req, corsHeaders);
|
|
198
|
+
return handleReadFileRequest(req, corsHeaders, sessionManager);
|
|
261
199
|
}
|
|
262
200
|
break;
|
|
263
201
|
|
|
264
202
|
case "/api/delete":
|
|
265
203
|
if (req.method === "POST") {
|
|
266
|
-
return handleDeleteFileRequest(req, corsHeaders);
|
|
204
|
+
return handleDeleteFileRequest(req, corsHeaders, sessionManager);
|
|
267
205
|
}
|
|
268
206
|
break;
|
|
269
207
|
|
|
270
208
|
case "/api/rename":
|
|
271
209
|
if (req.method === "POST") {
|
|
272
|
-
return handleRenameFileRequest(req, corsHeaders);
|
|
210
|
+
return handleRenameFileRequest(req, corsHeaders, sessionManager);
|
|
273
211
|
}
|
|
274
212
|
break;
|
|
275
213
|
|
|
276
214
|
case "/api/move":
|
|
277
215
|
if (req.method === "POST") {
|
|
278
|
-
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);
|
|
279
223
|
}
|
|
280
224
|
break;
|
|
281
225
|
|
|
@@ -299,23 +243,26 @@ const server = serve({
|
|
|
299
243
|
|
|
300
244
|
case "/api/process/start":
|
|
301
245
|
if (req.method === "POST") {
|
|
302
|
-
return handleStartProcessRequest(
|
|
246
|
+
return handleStartProcessRequest(req, corsHeaders, sessionManager);
|
|
303
247
|
}
|
|
304
248
|
break;
|
|
305
249
|
|
|
306
250
|
case "/api/process/list":
|
|
307
251
|
if (req.method === "GET") {
|
|
308
|
-
return handleListProcessesRequest(
|
|
252
|
+
return handleListProcessesRequest(req, corsHeaders, sessionManager);
|
|
309
253
|
}
|
|
310
254
|
break;
|
|
311
255
|
|
|
312
256
|
case "/api/process/kill-all":
|
|
313
257
|
if (req.method === "DELETE") {
|
|
314
|
-
return handleKillAllProcessesRequest(
|
|
258
|
+
return handleKillAllProcessesRequest(
|
|
259
|
+
req,
|
|
260
|
+
corsHeaders,
|
|
261
|
+
sessionManager
|
|
262
|
+
);
|
|
315
263
|
}
|
|
316
264
|
break;
|
|
317
265
|
|
|
318
|
-
// Code interpreter endpoints
|
|
319
266
|
case "/api/contexts":
|
|
320
267
|
if (req.method === "POST") {
|
|
321
268
|
try {
|
|
@@ -338,7 +285,6 @@ const server = serve({
|
|
|
338
285
|
);
|
|
339
286
|
} catch (error) {
|
|
340
287
|
if (error instanceof JupyterNotReadyError) {
|
|
341
|
-
// This happens when request times out waiting for Jupyter
|
|
342
288
|
console.log(
|
|
343
289
|
`[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
|
|
344
290
|
);
|
|
@@ -556,31 +502,31 @@ const server = serve({
|
|
|
556
502
|
|
|
557
503
|
if (!action && req.method === "GET") {
|
|
558
504
|
return handleGetProcessRequest(
|
|
559
|
-
processes,
|
|
560
505
|
req,
|
|
561
506
|
corsHeaders,
|
|
562
|
-
processId
|
|
507
|
+
processId,
|
|
508
|
+
sessionManager
|
|
563
509
|
);
|
|
564
510
|
} else if (!action && req.method === "DELETE") {
|
|
565
511
|
return handleKillProcessRequest(
|
|
566
|
-
processes,
|
|
567
512
|
req,
|
|
568
513
|
corsHeaders,
|
|
569
|
-
processId
|
|
514
|
+
processId,
|
|
515
|
+
sessionManager
|
|
570
516
|
);
|
|
571
517
|
} else if (action === "logs" && req.method === "GET") {
|
|
572
518
|
return handleGetProcessLogsRequest(
|
|
573
|
-
processes,
|
|
574
519
|
req,
|
|
575
520
|
corsHeaders,
|
|
576
|
-
processId
|
|
521
|
+
processId,
|
|
522
|
+
sessionManager
|
|
577
523
|
);
|
|
578
524
|
} else if (action === "stream" && req.method === "GET") {
|
|
579
525
|
return handleStreamProcessLogsRequest(
|
|
580
|
-
processes,
|
|
581
526
|
req,
|
|
582
527
|
corsHeaders,
|
|
583
|
-
processId
|
|
528
|
+
processId,
|
|
529
|
+
sessionManager
|
|
584
530
|
);
|
|
585
531
|
}
|
|
586
532
|
}
|
|
@@ -653,4 +599,3 @@ console.log(
|
|
|
653
599
|
` POST /api/execute/code - Execute code in a context (streaming)`
|
|
654
600
|
);
|
|
655
601
|
console.log(` GET /api/ping - Health check`);
|
|
656
|
-
console.log(` GET /api/commands - List available commands`);
|