@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +75 -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 +179 -837
  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 +68 -130
  11. package/container_src/isolation.ts +1038 -0
  12. package/container_src/shell-escape.ts +42 -0
  13. package/container_src/types.ts +27 -13
  14. package/dist/{chunk-HHUDRGPY.js → chunk-BEQUGUY4.js} +2 -2
  15. package/dist/{chunk-CKIGERRS.js → chunk-LFLJGISB.js} +240 -264
  16. package/dist/chunk-LFLJGISB.js.map +1 -0
  17. package/dist/{chunk-3CQ6THKA.js → chunk-SMUEY5JR.js} +85 -103
  18. package/dist/chunk-SMUEY5JR.js.map +1 -0
  19. package/dist/{client-Ce40ujDF.d.ts → client-Dny_ro_v.d.ts} +41 -25
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +8 -9
  24. package/dist/interpreter.d.ts +1 -1
  25. package/dist/jupyter-client.d.ts +1 -1
  26. package/dist/jupyter-client.js +2 -2
  27. package/dist/request-handler.d.ts +1 -1
  28. package/dist/request-handler.js +3 -5
  29. package/dist/sandbox.d.ts +1 -1
  30. package/dist/sandbox.js +3 -5
  31. package/dist/types.d.ts +10 -21
  32. package/dist/types.js +35 -9
  33. package/dist/types.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/client.ts +120 -135
  36. package/src/index.ts +8 -0
  37. package/src/sandbox.ts +290 -331
  38. package/src/types.ts +15 -24
  39. package/dist/chunk-3CQ6THKA.js.map +0 -1
  40. package/dist/chunk-6EWSYSO7.js +0 -46
  41. package/dist/chunk-6EWSYSO7.js.map +0 -1
  42. package/dist/chunk-CKIGERRS.js.map +0 -1
  43. /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
+ }
@@ -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
- // In-memory process storage - cleared on container restart
43
- const processes = new Map<string, ProcessRecord>();
44
-
45
- // Generate a unique session ID using cryptographically secure randomness
46
- function generateSessionId(): string {
47
- return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
48
- }
49
-
50
- // Clean up old sessions (older than 1 hour)
51
- function cleanupOldSessions() {
52
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
53
- for (const [sessionId, session] of sessions.entries()) {
54
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
55
- sessions.delete(sessionId);
56
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
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
- // Run cleanup every 10 minutes
62
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
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
- const sessionId = generateSessionId();
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
- const sessionList = Array.from(sessions.values()).map(
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(sessions, req, corsHeaders);
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(sessions, req, corsHeaders);
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(sessions, req, corsHeaders);
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(sessions, req, corsHeaders);
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(processes, req, corsHeaders);
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(processes, req, corsHeaders);
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(processes, req, corsHeaders);
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`);