@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
@@ -1,339 +1,184 @@
1
- import { type SpawnOptions, spawn } from "node:child_process";
2
- import type { ExecuteOptions, ExecuteRequest, SessionData } from "../types";
1
+ import type { SessionManager } from "../isolation";
2
+ import type { SessionExecRequest } from "../types";
3
3
 
4
- function executeCommand(
5
- sessions: Map<string, SessionData>,
6
- command: string,
7
- options: ExecuteOptions,
8
- ): Promise<{
9
- success: boolean;
10
- stdout: string;
11
- stderr: string;
12
- exitCode: number;
13
- }> {
14
- return new Promise((resolve, reject) => {
15
- const spawnOptions: SpawnOptions = {
16
- shell: true,
17
- stdio: ["pipe", "pipe", "pipe"] as const,
18
- detached: options.background || false,
19
- cwd: options.cwd || "/workspace", // Default to clean /workspace directory
20
- env: options.env ? { ...process.env, ...options.env } : process.env
21
- };
22
-
23
- const child = spawn(command, spawnOptions);
24
-
25
- // Store the process reference for cleanup if sessionId is provided
26
- if (options.sessionId && sessions.has(options.sessionId)) {
27
- const session = sessions.get(options.sessionId)!;
28
- session.activeProcess = child;
29
- }
30
-
31
- let stdout = "";
32
- let stderr = "";
33
-
34
- child.stdout?.on("data", (data) => {
35
- stdout += data.toString();
36
- });
37
-
38
- child.stderr?.on("data", (data) => {
39
- stderr += data.toString();
40
- });
41
-
42
- if (options.background) {
43
- // For background processes, unref and return quickly
44
- child.unref();
45
-
46
- // Collect initial output for 100ms then return
47
- setTimeout(() => {
48
- resolve({
49
- exitCode: 0, // Process is still running
50
- stderr,
51
- stdout,
52
- success: true,
53
- });
54
- }, 100);
55
-
56
- // Still handle errors
57
- child.on("error", (error) => {
58
- console.error(`[Server] Background process error: ${command}`, error);
59
- // Don't reject since we might have already resolved
60
- });
61
- } else {
62
- // Normal synchronous execution
63
- child.on("close", (code) => {
64
- // Clear the active process reference
65
- if (options.sessionId && sessions.has(options.sessionId)) {
66
- const session = sessions.get(options.sessionId)!;
67
- session.activeProcess = null;
68
- }
69
-
70
- console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
4
+ export async function handleExecuteRequest(
5
+ req: Request,
6
+ corsHeaders: Record<string, string>,
7
+ sessionManager: SessionManager
8
+ ) {
9
+ try {
10
+ const body = (await req.json()) as SessionExecRequest;
11
+ const { id, command } = body;
71
12
 
72
- resolve({
73
- exitCode: code || 0,
74
- stderr,
75
- stdout,
76
- success: code === 0,
77
- });
78
- });
13
+ console.log(
14
+ `[Container] Session exec request for '${id}': ${command}`
15
+ );
79
16
 
80
- child.on("error", (error) => {
81
- // Clear the active process reference
82
- if (options.sessionId && sessions.has(options.sessionId)) {
83
- const session = sessions.get(options.sessionId)!;
84
- session.activeProcess = null;
17
+ if (!id || !command) {
18
+ return new Response(
19
+ JSON.stringify({
20
+ error: "Session ID and command are required",
21
+ }),
22
+ {
23
+ status: 400,
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ ...corsHeaders,
27
+ },
85
28
  }
86
-
87
- reject(error);
88
- });
29
+ );
89
30
  }
90
- });
91
- }
92
31
 
93
- export async function handleExecuteRequest(
94
- sessions: Map<string, SessionData>,
95
- req: Request,
96
- corsHeaders: Record<string, string>
97
- ): Promise<Response> {
98
- try {
99
- const body = (await req.json()) as ExecuteRequest;
100
- const { command, sessionId, background, cwd, env } = body;
32
+ const session = sessionManager.getSession(id);
33
+ if (!session) {
34
+ console.error(`[Container] Session '${id}' not found!`);
35
+ const availableSessions = sessionManager.listSessions();
36
+ console.log(
37
+ `[Container] Available sessions: ${
38
+ availableSessions.join(", ") || "none"
39
+ }`
40
+ );
101
41
 
102
- if (!command || typeof command !== "string") {
103
42
  return new Response(
104
43
  JSON.stringify({
105
- error: "Command is required and must be a string",
44
+ error: `Session '${id}' not found`,
45
+ availableSessions,
106
46
  }),
107
47
  {
48
+ status: 404,
108
49
  headers: {
109
50
  "Content-Type": "application/json",
110
51
  ...corsHeaders,
111
52
  },
112
- status: 400,
113
53
  }
114
54
  );
115
55
  }
116
56
 
117
- console.log(`[Server] Executing command: ${command}`);
118
-
119
- const result = await executeCommand(sessions, command, { sessionId, background, cwd, env });
57
+ const result = await session.exec(command);
120
58
 
121
- return new Response(
122
- JSON.stringify({
123
- command,
124
- exitCode: result.exitCode,
125
- stderr: result.stderr,
126
- stdout: result.stdout,
127
- success: result.success,
128
- timestamp: new Date().toISOString(),
129
- }),
130
- {
131
- headers: {
132
- "Content-Type": "application/json",
133
- ...corsHeaders,
134
- },
135
- }
136
- );
59
+ return new Response(JSON.stringify(result), {
60
+ headers: { "Content-Type": "application/json", ...corsHeaders },
61
+ });
137
62
  } catch (error) {
138
- console.error("[Server] Error in handleExecuteRequest:", error);
63
+ console.error("[Container] Session exec failed:", error);
139
64
  return new Response(
140
65
  JSON.stringify({
141
- error: "Failed to execute command",
142
- message: error instanceof Error ? error.message : "Unknown error",
66
+ error: "Command execution failed",
67
+ message:
68
+ error instanceof Error ? error.message : String(error),
143
69
  }),
144
70
  {
71
+ status: 500,
145
72
  headers: {
146
73
  "Content-Type": "application/json",
147
74
  ...corsHeaders,
148
75
  },
149
- status: 500,
150
76
  }
151
77
  );
152
78
  }
153
79
  }
154
80
 
155
81
  export async function handleStreamingExecuteRequest(
156
- sessions: Map<string, SessionData>,
157
82
  req: Request,
83
+ sessionManager: SessionManager,
158
84
  corsHeaders: Record<string, string>
159
- ): Promise<Response> {
85
+ ) {
160
86
  try {
161
- const body = (await req.json()) as ExecuteRequest;
162
- const { command, sessionId, background, cwd, env } = body;
87
+ const body = (await req.json()) as SessionExecRequest;
88
+ const { id, command } = body;
89
+
90
+ console.log(
91
+ `[Container] Session streaming exec request for '${id}': ${command}`
92
+ );
163
93
 
164
- if (!command || typeof command !== "string") {
94
+ if (!id || !command) {
165
95
  return new Response(
166
96
  JSON.stringify({
167
- error: "Command is required and must be a string",
97
+ error: "Session ID and command are required",
168
98
  }),
169
99
  {
100
+ status: 400,
170
101
  headers: {
171
102
  "Content-Type": "application/json",
172
103
  ...corsHeaders,
173
104
  },
174
- status: 400,
175
105
  }
176
106
  );
177
107
  }
178
108
 
179
- console.log(
180
- `[Server] Executing streaming command: ${command}`
181
- );
182
-
183
- const stream = new ReadableStream({
184
- start(controller) {
185
- const spawnOptions: SpawnOptions = {
186
- shell: true,
187
- stdio: ["pipe", "pipe", "pipe"] as const,
188
- detached: background || false,
189
- cwd: cwd || "/workspace", // Default to clean /workspace directory
190
- env: env ? { ...process.env, ...env } : process.env
191
- };
192
-
193
- const child = spawn(command, spawnOptions);
109
+ const session = sessionManager.getSession(id);
110
+ if (!session) {
111
+ console.error(`[Container] Session '${id}' not found!`);
112
+ const availableSessions = sessionManager.listSessions();
194
113
 
195
- // Store the process reference for cleanup if sessionId is provided
196
- if (sessionId && sessions.has(sessionId)) {
197
- const session = sessions.get(sessionId)!;
198
- session.activeProcess = child;
199
- }
200
-
201
- // For background processes, unref to prevent blocking
202
- if (background) {
203
- child.unref();
114
+ return new Response(
115
+ JSON.stringify({
116
+ error: `Session '${id}' not found`,
117
+ availableSessions,
118
+ }),
119
+ {
120
+ status: 404,
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ ...corsHeaders,
124
+ },
204
125
  }
126
+ );
127
+ }
205
128
 
206
- let stdout = "";
207
- let stderr = "";
208
-
209
- // Send command start event
210
- controller.enqueue(
211
- new TextEncoder().encode(
212
- `data: ${JSON.stringify({
213
- type: "start",
214
- timestamp: new Date().toISOString(),
215
- command,
216
- background: background || false,
217
- })}\n\n`
218
- )
219
- );
220
-
221
- child.stdout?.on("data", (data) => {
222
- const output = data.toString();
223
- stdout += output;
224
-
225
- // Send real-time output
226
- controller.enqueue(
227
- new TextEncoder().encode(
228
- `data: ${JSON.stringify({
229
- type: "stdout",
230
- timestamp: new Date().toISOString(),
231
- data: output,
232
- command,
233
- })}\n\n`
234
- )
235
- );
236
- });
237
-
238
- child.stderr?.on("data", (data) => {
239
- const output = data.toString();
240
- stderr += output;
241
-
242
- // Send real-time error output
243
- controller.enqueue(
244
- new TextEncoder().encode(
245
- `data: ${JSON.stringify({
246
- type: "stderr",
247
- timestamp: new Date().toISOString(),
248
- data: output,
249
- command,
250
- })}\n\n`
251
- )
252
- );
253
- });
254
-
255
- child.on("close", (code) => {
256
- // Clear the active process reference
257
- if (sessionId && sessions.has(sessionId)) {
258
- const session = sessions.get(sessionId)!;
259
- session.activeProcess = null;
260
- }
261
-
262
- console.log(
263
- `[Server] Command completed: ${command}, Exit code: ${code}`
264
- );
265
-
266
- // Send command completion event
267
- controller.enqueue(
268
- new TextEncoder().encode(
269
- `data: ${JSON.stringify({
270
- type: "complete",
271
- timestamp: new Date().toISOString(),
272
- command,
273
- exitCode: code,
274
- result: {
275
- success: code === 0,
276
- exitCode: code,
277
- stdout,
278
- stderr,
279
- command,
280
- timestamp: new Date().toISOString(),
281
- },
282
- })}\n\n`
283
- )
284
- );
285
-
286
- // For non-background processes, close the stream
287
- // For background processes with streaming, the stream stays open
288
- if (!background) {
289
- controller.close();
290
- }
291
- });
292
-
293
- child.on("error", (error) => {
294
- // Clear the active process reference
295
- if (sessionId && sessions.has(sessionId)) {
296
- const session = sessions.get(sessionId)!;
297
- session.activeProcess = null;
129
+ // Create a streaming response using the actual streaming method
130
+ const stream = new ReadableStream({
131
+ async start(controller) {
132
+ try {
133
+ // Use the streaming generator method
134
+ for await (const event of session.execStream(command)) {
135
+ // Forward each event as SSE
136
+ controller.enqueue(
137
+ new TextEncoder().encode(
138
+ `data: ${JSON.stringify(event)}\n\n`
139
+ )
140
+ );
298
141
  }
299
-
142
+ controller.close();
143
+ } catch (error) {
300
144
  controller.enqueue(
301
145
  new TextEncoder().encode(
302
146
  `data: ${JSON.stringify({
303
147
  type: "error",
304
- timestamp: new Date().toISOString(),
305
- error: error.message,
306
- command,
148
+ message:
149
+ error instanceof Error
150
+ ? error.message
151
+ : String(error),
307
152
  })}\n\n`
308
153
  )
309
154
  );
310
-
311
155
  controller.close();
312
- });
156
+ }
313
157
  },
314
158
  });
315
159
 
316
160
  return new Response(stream, {
317
161
  headers: {
162
+ "Content-Type": "text/event-stream",
318
163
  "Cache-Control": "no-cache",
319
164
  Connection: "keep-alive",
320
- "Content-Type": "text/event-stream",
321
165
  ...corsHeaders,
322
166
  },
323
167
  });
324
168
  } catch (error) {
325
- console.error("[Server] Error in handleStreamingExecuteRequest:", error);
169
+ console.error("[Container] Session stream exec failed:", error);
326
170
  return new Response(
327
171
  JSON.stringify({
328
- error: "Failed to execute streaming command",
329
- message: error instanceof Error ? error.message : "Unknown error",
172
+ error: "Stream execution failed",
173
+ message:
174
+ error instanceof Error ? error.message : String(error),
330
175
  }),
331
176
  {
177
+ status: 500,
332
178
  headers: {
333
179
  "Content-Type": "application/json",
334
180
  ...corsHeaders,
335
181
  },
336
- status: 500,
337
182
  }
338
183
  );
339
184
  }