@cloudflare/sandbox 0.0.0-fb3c9c2 → 0.0.0-feafd32

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.
@@ -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
  *
@@ -121,13 +121,20 @@ export class Session {
121
121
  timeout: NodeJS.Timeout;
122
122
  }>();
123
123
  private processes = new Map<string, ProcessRecord>(); // Session-specific processes
124
-
124
+
125
125
  constructor(private options: SessionOptions) {
126
126
  this.canIsolate = (options.isolation === true) && hasNamespaceSupport();
127
127
  if (options.isolation === true && !this.canIsolate) {
128
128
  console.log(`[Session] Isolation requested for '${options.id}' but not available`);
129
129
  }
130
130
  }
131
+
132
+ /**
133
+ * Check if the session is ready for command execution
134
+ */
135
+ isReady(): boolean {
136
+ return this.ready && this.control !== null && !this.control.killed;
137
+ }
131
138
 
132
139
  async initialize(): Promise<void> {
133
140
  // Use the proper TypeScript control process file
@@ -946,17 +953,25 @@ export class SessionManager {
946
953
  throw new Error(`cwd must be an absolute path starting with '/', got: ${options.cwd}`);
947
954
  }
948
955
  }
949
-
950
- // Clean up existing session with same name
956
+
957
+ // Check if session already exists
951
958
  const existing = this.sessions.get(options.id);
952
959
  if (existing) {
953
- existing.destroy();
960
+ // If the existing session is healthy and ready, reuse it
961
+ if (existing.isReady()) {
962
+ console.log(`[SessionManager] Reusing existing session '${options.id}'`);
963
+ return existing;
964
+ }
965
+
966
+ // If the session exists but is not ready, clean it up and create a new one
967
+ console.log(`[SessionManager] Destroying unhealthy session '${options.id}' before recreating`);
968
+ await existing.destroy();
954
969
  }
955
-
970
+
956
971
  // Create new session
957
972
  const session = new Session(options);
958
973
  await session.initialize();
959
-
974
+
960
975
  this.sessions.set(options.id, session);
961
976
  console.log(`[SessionManager] Created session '${options.id}'`);
962
977
  return session;
@@ -972,15 +987,11 @@ export class SessionManager {
972
987
 
973
988
  // Helper to get or create default session - reduces duplication
974
989
  async getOrCreateDefaultSession(): Promise<Session> {
975
- let defaultSession = this.sessions.get('default');
976
- if (!defaultSession) {
977
- defaultSession = await this.createSession({
978
- id: 'default',
979
- cwd: '/workspace', // Consistent default working directory
980
- isolation: true
981
- });
982
- }
983
- return defaultSession;
990
+ return await this.createSession({
991
+ id: 'default',
992
+ cwd: '/workspace', // Consistent default working directory
993
+ isolation: true
994
+ });
984
995
  }
985
996
 
986
997
  async exec(command: string, options?: { cwd?: string }): Promise<ExecResult> {
@@ -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
+ });