@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/Dockerfile +9 -11
  3. package/README.md +69 -7
  4. package/container_src/control-process.ts +784 -0
  5. package/container_src/handler/exec.ts +99 -254
  6. package/container_src/handler/file.ts +204 -642
  7. package/container_src/handler/git.ts +28 -80
  8. package/container_src/handler/process.ts +443 -515
  9. package/container_src/handler/session.ts +92 -0
  10. package/container_src/index.ts +74 -129
  11. package/container_src/isolation.ts +1039 -0
  12. package/container_src/jupyter-service.ts +8 -5
  13. package/container_src/shell-escape.ts +42 -0
  14. package/container_src/types.ts +35 -12
  15. package/dist/{chunk-VTKZL632.js → chunk-BEQUGUY4.js} +2 -2
  16. package/dist/{chunk-4KELYYKS.js → chunk-GTGWAEED.js} +239 -265
  17. package/dist/chunk-GTGWAEED.js.map +1 -0
  18. package/dist/{chunk-CUHYLCMT.js → chunk-SMUEY5JR.js} +111 -99
  19. package/dist/chunk-SMUEY5JR.js.map +1 -0
  20. package/dist/{client-bzEV222a.d.ts → client-Dny_ro_v.d.ts} +48 -84
  21. package/dist/client.d.ts +1 -1
  22. package/dist/client.js +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +8 -9
  25. package/dist/interpreter.d.ts +2 -2
  26. package/dist/jupyter-client.d.ts +2 -2
  27. package/dist/jupyter-client.js +2 -2
  28. package/dist/request-handler.d.ts +3 -3
  29. package/dist/request-handler.js +3 -5
  30. package/dist/sandbox.d.ts +2 -2
  31. package/dist/sandbox.js +3 -5
  32. package/dist/types.d.ts +127 -21
  33. package/dist/types.js +35 -9
  34. package/dist/types.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/client.ts +175 -187
  37. package/src/index.ts +23 -13
  38. package/src/sandbox.ts +297 -332
  39. package/src/types.ts +125 -24
  40. package/dist/chunk-4KELYYKS.js.map +0 -1
  41. package/dist/chunk-CUHYLCMT.js.map +0 -1
  42. package/dist/chunk-S5FFBU4Y.js +0 -46
  43. package/dist/chunk-S5FFBU4Y.js.map +0 -1
  44. /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
+ }
@@ -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
- // In-memory process storage - cleared on container restart
42
- const processes = new Map<string, ProcessRecord>();
43
-
44
- // Generate a unique session ID using cryptographically secure randomness
45
- function generateSessionId(): string {
46
- return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
47
- }
48
-
49
- // Clean up old sessions (older than 1 hour)
50
- function cleanupOldSessions() {
51
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
52
- for (const [sessionId, session] of sessions.entries()) {
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
- // Run cleanup every 10 minutes
61
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
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
- const sessionId = generateSessionId();
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
- const sessionList = Array.from(sessions.values()).map(
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(sessions, req, corsHeaders);
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(sessions, req, corsHeaders);
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(sessions, req, corsHeaders);
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(sessions, req, corsHeaders);
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(processes, req, corsHeaders);
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(processes, req, corsHeaders);
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(processes, req, corsHeaders);
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`);