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