@cloudflare/sandbox 0.3.0 → 0.3.2

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 (61) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/Dockerfile +22 -24
  3. package/README.md +1 -1
  4. package/container_src/bun.lock +31 -77
  5. package/container_src/control-process.ts +1 -1
  6. package/container_src/handler/exec.ts +2 -2
  7. package/container_src/handler/session.ts +2 -2
  8. package/container_src/index.ts +34 -43
  9. package/container_src/interpreter-service.ts +276 -0
  10. package/container_src/isolation.ts +8 -9
  11. package/container_src/mime-processor.ts +1 -1
  12. package/container_src/package.json +4 -4
  13. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  14. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  15. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  16. package/container_src/runtime/process-pool.ts +464 -0
  17. package/container_src/startup.sh +6 -79
  18. package/dist/{chunk-LALY4SFU.js → chunk-FXYPFGOZ.js} +10 -10
  19. package/dist/chunk-FXYPFGOZ.js.map +1 -0
  20. package/dist/{chunk-GTGWAEED.js → chunk-H4PW2LGW.js} +9 -6
  21. package/dist/chunk-H4PW2LGW.js.map +1 -0
  22. package/dist/{chunk-FKBV7CZS.js → chunk-JTKON2SH.js} +9 -9
  23. package/dist/chunk-JTKON2SH.js.map +1 -0
  24. package/dist/{chunk-EGC5IYXA.js → chunk-W7TVRPBG.js} +2 -2
  25. package/dist/chunk-W7TVRPBG.js.map +1 -0
  26. package/dist/{chunk-BEQUGUY4.js → chunk-Z6OZPC6U.js} +9 -6
  27. package/dist/chunk-Z6OZPC6U.js.map +1 -0
  28. package/dist/{client-Dny_ro_v.d.ts → client-COGWU6bz.d.ts} +3 -3
  29. package/dist/client.d.ts +1 -1
  30. package/dist/errors.d.ts +9 -9
  31. package/dist/errors.js +5 -5
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +13 -11
  34. package/dist/interpreter-client.d.ts +4 -0
  35. package/dist/interpreter-client.js +9 -0
  36. package/dist/interpreter-types.d.ts +5 -5
  37. package/dist/interpreter-types.js +1 -1
  38. package/dist/interpreter.d.ts +2 -2
  39. package/dist/interpreter.js +2 -2
  40. package/dist/request-handler.d.ts +1 -1
  41. package/dist/request-handler.js +5 -5
  42. package/dist/sandbox.d.ts +1 -1
  43. package/dist/sandbox.js +5 -5
  44. package/package.json +2 -2
  45. package/src/errors.ts +15 -14
  46. package/src/index.ts +16 -5
  47. package/src/{jupyter-client.ts → interpreter-client.ts} +6 -3
  48. package/src/interpreter-types.ts +102 -95
  49. package/src/interpreter.ts +8 -8
  50. package/src/sandbox.ts +7 -3
  51. package/container_src/jupyter-server.ts +0 -579
  52. package/container_src/jupyter-service.ts +0 -461
  53. package/container_src/jupyter_config.py +0 -48
  54. package/dist/chunk-BEQUGUY4.js.map +0 -1
  55. package/dist/chunk-EGC5IYXA.js.map +0 -1
  56. package/dist/chunk-FKBV7CZS.js.map +0 -1
  57. package/dist/chunk-GTGWAEED.js.map +0 -1
  58. package/dist/chunk-LALY4SFU.js.map +0 -1
  59. package/dist/jupyter-client.d.ts +0 -4
  60. package/dist/jupyter-client.js +0 -9
  61. /package/dist/{jupyter-client.js.map → interpreter-client.js.map} +0 -0
@@ -0,0 +1,276 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { type InterpreterLanguage, processPool, type RichOutput } from "./runtime/process-pool";
3
+
4
+ export interface CreateContextRequest {
5
+ language?: string;
6
+ cwd?: string;
7
+ }
8
+
9
+ export interface Context {
10
+ id: string;
11
+ language: string;
12
+ cwd: string;
13
+ createdAt: string;
14
+ lastUsed: string;
15
+ }
16
+
17
+ export interface HealthStatus {
18
+ ready: boolean;
19
+ initializing: boolean;
20
+ progress: number;
21
+ }
22
+
23
+ export class InterpreterNotReadyError extends Error {
24
+ progress: number;
25
+ retryAfter: number;
26
+
27
+ constructor(message: string, progress: number = 100, retryAfter: number = 1) {
28
+ super(message);
29
+ this.progress = progress;
30
+ this.retryAfter = retryAfter;
31
+ this.name = "InterpreterNotReadyError";
32
+ }
33
+ }
34
+
35
+ export class InterpreterService {
36
+ private contexts: Map<string, Context> = new Map();
37
+
38
+ async getHealthStatus(): Promise<HealthStatus> {
39
+ return {
40
+ ready: true,
41
+ initializing: false,
42
+ progress: 100,
43
+ };
44
+ }
45
+
46
+ async createContext(request: CreateContextRequest): Promise<Context> {
47
+ const id = randomUUID();
48
+ const language = this.mapLanguage(request.language || "python");
49
+
50
+ const context: Context = {
51
+ id,
52
+ language,
53
+ cwd: request.cwd || "/workspace",
54
+ createdAt: new Date().toISOString(),
55
+ lastUsed: new Date().toISOString(),
56
+ };
57
+
58
+ this.contexts.set(id, context);
59
+ console.log(`[InterpreterService] Created context ${id} for ${language}`);
60
+
61
+ return context;
62
+ }
63
+
64
+ async listContexts(): Promise<Context[]> {
65
+ return Array.from(this.contexts.values());
66
+ }
67
+
68
+ async deleteContext(contextId: string): Promise<void> {
69
+ if (!this.contexts.has(contextId)) {
70
+ throw new Error(`Context ${contextId} not found`);
71
+ }
72
+
73
+ this.contexts.delete(contextId);
74
+ console.log(`[InterpreterService] Deleted context ${contextId}`);
75
+ }
76
+
77
+ async executeCode(
78
+ contextId: string,
79
+ code: string,
80
+ language?: string
81
+ ): Promise<Response> {
82
+ const context = this.contexts.get(contextId);
83
+ if (!context) {
84
+ return new Response(
85
+ JSON.stringify({
86
+ error: `Context ${contextId} not found`,
87
+ }),
88
+ {
89
+ status: 404,
90
+ headers: { "Content-Type": "application/json" },
91
+ }
92
+ );
93
+ }
94
+
95
+ context.lastUsed = new Date().toISOString();
96
+
97
+ const execLanguage = this.mapLanguage(language || context.language);
98
+
99
+ // Store reference to this for use in async function
100
+ const self = this;
101
+
102
+ const stream = new ReadableStream({
103
+ async start(controller) {
104
+ const encoder = new TextEncoder();
105
+ const startTime = Date.now();
106
+
107
+ try {
108
+ const result = await processPool.execute(
109
+ execLanguage,
110
+ code,
111
+ contextId,
112
+ 30000
113
+ );
114
+
115
+ const totalTime = Date.now() - startTime;
116
+ console.log(`[InterpreterService] Code execution completed in ${totalTime}ms`);
117
+
118
+ if (result.stdout) {
119
+ controller.enqueue(
120
+ encoder.encode(
121
+ `${JSON.stringify({
122
+ type: "stdout",
123
+ text: result.stdout,
124
+ })}\n`
125
+ )
126
+ );
127
+ }
128
+
129
+ if (result.stderr) {
130
+ controller.enqueue(
131
+ encoder.encode(
132
+ `${JSON.stringify({
133
+ type: "stderr",
134
+ text: result.stderr,
135
+ })}\n`
136
+ )
137
+ );
138
+ }
139
+
140
+ if (result.outputs && result.outputs.length > 0) {
141
+ for (const output of result.outputs) {
142
+ const outputData = self.formatOutputData(output);
143
+ controller.enqueue(
144
+ encoder.encode(
145
+ `${JSON.stringify({
146
+ type: "result",
147
+ ...outputData,
148
+ metadata: output.metadata || {},
149
+ })}\n`
150
+ )
151
+ );
152
+ }
153
+ }
154
+
155
+ if (result.success) {
156
+ controller.enqueue(
157
+ encoder.encode(
158
+ `${JSON.stringify({
159
+ type: "execution_complete",
160
+ execution_count: 1,
161
+ })}\n`
162
+ )
163
+ );
164
+ } else if (result.error) {
165
+ controller.enqueue(
166
+ encoder.encode(
167
+ `${JSON.stringify({
168
+ type: "error",
169
+ ename: result.error.type || "ExecutionError",
170
+ evalue: result.error.message || "Code execution failed",
171
+ traceback: result.error.traceback ? result.error.traceback.split('\n') : [],
172
+ })}\n`
173
+ )
174
+ );
175
+ } else {
176
+ controller.enqueue(
177
+ encoder.encode(
178
+ `${JSON.stringify({
179
+ type: "error",
180
+ ename: "ExecutionError",
181
+ evalue: result.stderr || "Code execution failed",
182
+ traceback: [],
183
+ })}\n`
184
+ )
185
+ );
186
+ }
187
+
188
+ controller.close();
189
+ } catch (error) {
190
+ console.error(`[InterpreterService] Code execution failed:`, error);
191
+
192
+ controller.enqueue(
193
+ encoder.encode(
194
+ `${JSON.stringify({
195
+ type: "error",
196
+ ename: "InternalError",
197
+ evalue: error instanceof Error ? error.message : String(error),
198
+ traceback: [],
199
+ })}\n`
200
+ )
201
+ );
202
+
203
+ controller.close();
204
+ }
205
+ },
206
+ });
207
+
208
+ return new Response(stream, {
209
+ headers: {
210
+ "Content-Type": "text/event-stream",
211
+ "Cache-Control": "no-cache",
212
+ Connection: "keep-alive",
213
+ },
214
+ });
215
+ }
216
+
217
+ private mapLanguage(language: string): InterpreterLanguage {
218
+ const normalized = language.toLowerCase();
219
+
220
+ switch (normalized) {
221
+ case "python":
222
+ case "python3":
223
+ return "python";
224
+ case "javascript":
225
+ case "js":
226
+ case "node":
227
+ return "javascript";
228
+ case "typescript":
229
+ case "ts":
230
+ return "typescript";
231
+ default:
232
+ console.warn(
233
+ `[InterpreterService] Unknown language ${language}, defaulting to python`
234
+ );
235
+ return "python";
236
+ }
237
+ }
238
+
239
+ private formatOutputData(output: RichOutput): Record<string, unknown> {
240
+ const result: Record<string, unknown> = {};
241
+
242
+ switch (output.type) {
243
+ case "image":
244
+ result.png = output.data;
245
+ break;
246
+ case "jpeg":
247
+ result.jpeg = output.data;
248
+ break;
249
+ case "svg":
250
+ result.svg = output.data;
251
+ break;
252
+ case "html":
253
+ result.html = output.data;
254
+ break;
255
+ case "json":
256
+ result.json = typeof output.data === 'string' ? JSON.parse(output.data) : output.data;
257
+ break;
258
+ case "latex":
259
+ result.latex = output.data;
260
+ break;
261
+ case "markdown":
262
+ result.markdown = output.data;
263
+ break;
264
+ case "javascript":
265
+ result.javascript = output.data;
266
+ break;
267
+ case "text":
268
+ result.text = output.data;
269
+ break;
270
+ default:
271
+ result.text = output.data || '';
272
+ }
273
+
274
+ return result;
275
+ }
276
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Implements PID namespace isolation to secure the sandbox environment.
5
5
  * Executed commands run in isolated namespaces, preventing them from:
6
- * - Seeing or killing control plane processes (Jupyter, Bun)
6
+ * - Seeing or killing control plane processes (Bun)
7
7
  * - Accessing platform secrets in /proc
8
8
  * - Hijacking control plane ports
9
9
  *
@@ -35,11 +35,11 @@ import type { ProcessRecord, ProcessStatus } from './types';
35
35
  // Configuration constants
36
36
  const CONFIG = {
37
37
  // Timeouts (in milliseconds)
38
- COMMAND_TIMEOUT_MS: 30000, // 30 seconds for command execution
39
38
  READY_TIMEOUT_MS: 5000, // 5 seconds for control process to initialize
40
- CLEANUP_INTERVAL_MS: 30000, // Run cleanup every 30 seconds
41
- TEMP_FILE_MAX_AGE_MS: 60000, // Delete temp files older than 60 seconds
42
39
  SHUTDOWN_GRACE_PERIOD_MS: 500, // Grace period for cleanup on shutdown
40
+ COMMAND_TIMEOUT_MS: parseInt(process.env.COMMAND_TIMEOUT_MS || '30000'), // 30 seconds for command execution
41
+ CLEANUP_INTERVAL_MS: parseInt(process.env.CLEANUP_INTERVAL_MS || '30000'), // Run cleanup every 30 seconds
42
+ TEMP_FILE_MAX_AGE_MS: parseInt(process.env.TEMP_FILE_MAX_AGE_MS || '60000'), // Delete temp files older than 60 seconds
43
43
 
44
44
  // Default paths
45
45
  DEFAULT_CWD: '/workspace',
@@ -423,12 +423,11 @@ export class Session {
423
423
 
424
424
  // File Operations - Execute as shell commands to inherit session context
425
425
  async writeFileOperation(path: string, content: string, encoding: string = 'utf-8'): Promise<{ success: boolean; exitCode: number; path: string }> {
426
- // Escape content for safe heredoc usage
427
- const safeContent = content.replace(/'/g, "'\\''");
428
-
429
426
  // Create parent directory if needed, then write file using heredoc
427
+ // Note: The quoted heredoc delimiter 'SANDBOX_EOF' prevents variable expansion
428
+ // and treats the content literally, so no escaping is required
430
429
  const command = `mkdir -p "$(dirname "${path}")" && cat > "${path}" << 'SANDBOX_EOF'
431
- ${safeContent}
430
+ ${content}
432
431
  SANDBOX_EOF`;
433
432
 
434
433
  const result = await this.exec(command);
@@ -1036,4 +1035,4 @@ export class SessionManager {
1036
1035
  }
1037
1036
  this.sessions.clear();
1038
1037
  }
1039
- }
1038
+ }
@@ -28,7 +28,7 @@ export interface ChartData {
28
28
  library?: 'matplotlib' | 'plotly' | 'altair' | 'seaborn' | 'unknown';
29
29
  }
30
30
 
31
- export function processJupyterMessage(msg: any): ExecutionResult | null {
31
+ export function processMessage(msg: any): ExecutionResult | null {
32
32
  const msgType = msg.header?.msg_type || msg.msg_type;
33
33
 
34
34
  switch (msgType) {
@@ -7,12 +7,12 @@
7
7
  "start": "bun run index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@jupyterlab/services": "^7.0.0",
11
- "ws": "^8.16.0",
10
+ "esbuild": "^0.21.5",
12
11
  "uuid": "^9.0.1"
13
12
  },
14
13
  "devDependencies": {
15
- "@types/ws": "^8.5.10",
16
- "@types/uuid": "^9.0.7"
14
+ "@types/node": "^20.0.0",
15
+ "@types/uuid": "^9.0.7",
16
+ "typescript": "^5.3.0"
17
17
  }
18
18
  }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as readline from 'node:readline';
4
+ import * as util from 'node:util';
5
+ import * as vm from 'node:vm';
6
+ import type { RichOutput } from '../../process-pool';
7
+
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ terminal: false
12
+ });
13
+
14
+ const sandbox = {
15
+ console: console,
16
+ process: process,
17
+ require: require,
18
+ Buffer: Buffer,
19
+ setTimeout: setTimeout,
20
+ setInterval: setInterval,
21
+ clearTimeout: clearTimeout,
22
+ clearInterval: clearInterval,
23
+ setImmediate: setImmediate,
24
+ clearImmediate: clearImmediate,
25
+ global: global,
26
+ __dirname: __dirname,
27
+ __filename: __filename
28
+ };
29
+
30
+ const context = vm.createContext(sandbox);
31
+
32
+ console.log(JSON.stringify({ status: "ready" }));
33
+
34
+ rl.on('line', async (line: string) => {
35
+ try {
36
+ const request = JSON.parse(line);
37
+ const { code, executionId } = request;
38
+
39
+ const originalStdoutWrite = process.stdout.write;
40
+ const originalStderrWrite = process.stderr.write;
41
+
42
+ let stdout = '';
43
+ let stderr = '';
44
+
45
+ (process.stdout.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => {
46
+ stdout += chunk.toString();
47
+ if (callback) callback();
48
+ return true;
49
+ };
50
+
51
+ (process.stderr.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => {
52
+ stderr += chunk.toString();
53
+ if (callback) callback();
54
+ return true;
55
+ };
56
+
57
+ let result: unknown;
58
+ let success = true;
59
+
60
+ try {
61
+ result = vm.runInContext(code, context, {
62
+ filename: `<execution-${executionId}>`,
63
+ timeout: 30000
64
+ });
65
+
66
+ } catch (error: unknown) {
67
+ const err = error as Error;
68
+ stderr += err.stack || err.toString();
69
+ success = false;
70
+ } finally {
71
+ process.stdout.write = originalStdoutWrite;
72
+ process.stderr.write = originalStderrWrite;
73
+ }
74
+
75
+ const outputs: RichOutput[] = [];
76
+
77
+ if (result !== undefined) {
78
+ if (typeof result === 'object' && result !== null) {
79
+ outputs.push({
80
+ type: 'json',
81
+ data: JSON.stringify(result, null, 2),
82
+ metadata: {}
83
+ });
84
+ } else {
85
+ outputs.push({
86
+ type: 'text',
87
+ data: util.inspect(result, { showHidden: false, depth: null, colors: false }),
88
+ metadata: {}
89
+ });
90
+ }
91
+ }
92
+
93
+ const response = {
94
+ stdout,
95
+ stderr,
96
+ success,
97
+ executionId,
98
+ outputs
99
+ };
100
+
101
+ console.log(JSON.stringify(response));
102
+
103
+ } catch (error: unknown) {
104
+ const err = error as Error;
105
+ console.log(JSON.stringify({
106
+ stdout: '',
107
+ stderr: `Error processing request: ${err.message}`,
108
+ success: false,
109
+ executionId: 'unknown',
110
+ outputs: []
111
+ }));
112
+ }
113
+ });
114
+
115
+ process.on('SIGTERM', () => {
116
+ rl.close();
117
+ process.exit(0);
118
+ });
119
+
120
+ process.on('SIGINT', () => {
121
+ rl.close();
122
+ process.exit(0);
123
+ });