@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.
- package/CHANGELOG.md +75 -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 +179 -837
- 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 +68 -130
- package/container_src/isolation.ts +1038 -0
- package/container_src/shell-escape.ts +42 -0
- package/container_src/types.ts +27 -13
- package/dist/{chunk-HHUDRGPY.js → chunk-BEQUGUY4.js} +2 -2
- package/dist/{chunk-CKIGERRS.js → chunk-LFLJGISB.js} +240 -264
- package/dist/chunk-LFLJGISB.js.map +1 -0
- package/dist/{chunk-3CQ6THKA.js → chunk-SMUEY5JR.js} +85 -103
- package/dist/chunk-SMUEY5JR.js.map +1 -0
- package/dist/{client-Ce40ujDF.d.ts → client-Dny_ro_v.d.ts} +41 -25
- 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 +1 -1
- package/dist/jupyter-client.d.ts +1 -1
- package/dist/jupyter-client.js +2 -2
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +3 -5
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +3 -5
- package/dist/types.d.ts +10 -21
- package/dist/types.js +35 -9
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +120 -135
- package/src/index.ts +8 -0
- package/src/sandbox.ts +290 -331
- package/src/types.ts +15 -24
- package/dist/chunk-3CQ6THKA.js.map +0 -1
- package/dist/chunk-6EWSYSO7.js +0 -46
- package/dist/chunk-6EWSYSO7.js.map +0 -1
- package/dist/chunk-CKIGERRS.js.map +0 -1
- /package/dist/{chunk-HHUDRGPY.js.map → chunk-BEQUGUY4.js.map} +0 -0
|
@@ -1,339 +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
|
-
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
stdout,
|
|
76
|
-
success: code === 0,
|
|
77
|
-
});
|
|
78
|
-
});
|
|
13
|
+
console.log(
|
|
14
|
+
`[Container] Session exec request for '${id}': ${command}`
|
|
15
|
+
);
|
|
79
16
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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("[
|
|
63
|
+
console.error("[Container] Session exec failed:", error);
|
|
139
64
|
return new Response(
|
|
140
65
|
JSON.stringify({
|
|
141
|
-
error: "
|
|
142
|
-
message:
|
|
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
|
-
)
|
|
85
|
+
) {
|
|
160
86
|
try {
|
|
161
|
-
const body = (await req.json()) as
|
|
162
|
-
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
|
+
);
|
|
163
93
|
|
|
164
|
-
if (!
|
|
94
|
+
if (!id || !command) {
|
|
165
95
|
return new Response(
|
|
166
96
|
JSON.stringify({
|
|
167
|
-
error: "
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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("[
|
|
169
|
+
console.error("[Container] Session stream exec failed:", error);
|
|
326
170
|
return new Response(
|
|
327
171
|
JSON.stringify({
|
|
328
|
-
error: "
|
|
329
|
-
message:
|
|
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
|
}
|