@cloudflare/sandbox 0.0.0-d81d2a5 → 0.0.0-d951819

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 (35) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/Dockerfile +34 -27
  3. package/README.md +127 -12
  4. package/container_src/bun.lock +31 -77
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/control-process.ts +784 -0
  7. package/container_src/handler/exec.ts +99 -254
  8. package/container_src/handler/file.ts +253 -640
  9. package/container_src/handler/git.ts +28 -80
  10. package/container_src/handler/process.ts +443 -515
  11. package/container_src/handler/session.ts +92 -0
  12. package/container_src/index.ts +289 -219
  13. package/container_src/interpreter-service.ts +276 -0
  14. package/container_src/isolation.ts +1213 -0
  15. package/container_src/mime-processor.ts +1 -1
  16. package/container_src/package.json +4 -4
  17. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  18. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  19. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  20. package/container_src/runtime/process-pool.ts +464 -0
  21. package/container_src/shell-escape.ts +42 -0
  22. package/container_src/startup.sh +6 -47
  23. package/container_src/types.ts +35 -12
  24. package/package.json +2 -2
  25. package/src/client.ts +214 -187
  26. package/src/errors.ts +219 -0
  27. package/src/file-stream.ts +162 -0
  28. package/src/index.ts +66 -14
  29. package/src/interpreter-client.ts +352 -0
  30. package/src/interpreter-types.ts +102 -95
  31. package/src/interpreter.ts +8 -8
  32. package/src/sandbox.ts +315 -337
  33. package/src/types.ts +194 -24
  34. package/container_src/jupyter-server.ts +0 -336
  35. package/src/jupyter-client.ts +0 -266
package/src/types.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  // Core Types
2
2
 
3
3
  export interface BaseExecOptions {
4
- /**
5
- * Session ID for grouping related commands
6
- */
7
- sessionId?: string;
8
-
9
4
  /**
10
5
  * Maximum execution time in milliseconds
11
6
  */
@@ -90,11 +85,6 @@ export interface ExecResult {
90
85
  * ISO timestamp when command started
91
86
  */
92
87
  timestamp: string;
93
-
94
- /**
95
- * Session ID if provided
96
- */
97
- sessionId?: string;
98
88
  }
99
89
 
100
90
  // Background Process Types
@@ -177,11 +167,6 @@ export interface Process {
177
167
  */
178
168
  readonly exitCode?: number;
179
169
 
180
- /**
181
- * Session ID if provided
182
- */
183
- readonly sessionId?: string;
184
-
185
170
  /**
186
171
  * Kill the process
187
172
  */
@@ -208,7 +193,6 @@ export interface ExecEvent {
208
193
  exitCode?: number;
209
194
  result?: ExecResult;
210
195
  error?: string; // Changed to string for serialization
211
- sessionId?: string;
212
196
  }
213
197
 
214
198
  export interface LogEvent {
@@ -216,7 +200,6 @@ export interface LogEvent {
216
200
  timestamp: string;
217
201
  data: string;
218
202
  processId: string;
219
- sessionId?: string;
220
203
  exitCode?: number; // For 'exit' events
221
204
  }
222
205
 
@@ -232,6 +215,54 @@ export interface StreamOptions extends BaseExecOptions {
232
215
  signal?: AbortSignal;
233
216
  }
234
217
 
218
+ // File Streaming Types
219
+
220
+ /**
221
+ * SSE events for file streaming
222
+ */
223
+ export type FileStreamEvent =
224
+ | {
225
+ type: 'metadata';
226
+ mimeType: string;
227
+ size: number;
228
+ isBinary: boolean;
229
+ encoding: 'utf-8' | 'base64';
230
+ }
231
+ | {
232
+ type: 'chunk';
233
+ data: string; // base64 for binary, UTF-8 for text
234
+ }
235
+ | {
236
+ type: 'complete';
237
+ bytesRead: number;
238
+ }
239
+ | {
240
+ type: 'error';
241
+ error: string;
242
+ };
243
+
244
+ /**
245
+ * File metadata from streaming
246
+ */
247
+ export interface FileMetadata {
248
+ mimeType: string;
249
+ size: number;
250
+ isBinary: boolean;
251
+ encoding: 'utf-8' | 'base64';
252
+ }
253
+
254
+ /**
255
+ * File stream chunk - either string (text) or Uint8Array (binary, auto-decoded)
256
+ */
257
+ export type FileChunk = string | Uint8Array;
258
+
259
+ /**
260
+ * AsyncIterable of file chunks with metadata
261
+ */
262
+ export interface FileStream extends AsyncIterable<FileChunk> {
263
+ metadata?: FileMetadata;
264
+ }
265
+
235
266
  // Error Types
236
267
 
237
268
  export class SandboxError extends Error {
@@ -272,10 +303,8 @@ export interface ProcessRecord {
272
303
  startTime: Date;
273
304
  endTime?: Date;
274
305
  exitCode?: number;
275
- sessionId?: string;
276
306
 
277
307
  // Internal fields
278
- childProcess?: any; // Node.js ChildProcess
279
308
  stdout: string; // Accumulated output (ephemeral)
280
309
  stderr: string; // Accumulated output (ephemeral)
281
310
 
@@ -290,7 +319,6 @@ export interface StartProcessRequest {
290
319
  command: string;
291
320
  options?: {
292
321
  processId?: string;
293
- sessionId?: string;
294
322
  timeout?: number;
295
323
  env?: Record<string, string>;
296
324
  cwd?: string;
@@ -304,9 +332,11 @@ export interface StartProcessResponse {
304
332
  id: string;
305
333
  pid?: number;
306
334
  command: string;
307
- status: ProcessStatus;
335
+ status: ProcessStatus;
308
336
  startTime: string;
309
- sessionId?: string;
337
+ endTime?: string | null;
338
+ exitCode?: number | null;
339
+ sessionId: string;
310
340
  };
311
341
  }
312
342
 
@@ -319,7 +349,6 @@ export interface ListProcessesResponse {
319
349
  startTime: string;
320
350
  endTime?: string;
321
351
  exitCode?: number;
322
- sessionId?: string;
323
352
  }>;
324
353
  }
325
354
 
@@ -332,7 +361,6 @@ export interface GetProcessResponse {
332
361
  startTime: string;
333
362
  endTime?: string;
334
363
  exitCode?: number;
335
- sessionId?: string;
336
364
  } | null;
337
365
  }
338
366
 
@@ -371,6 +399,26 @@ export interface ISandbox {
371
399
  cleanupCompletedProcesses(): Promise<number>;
372
400
  getProcessLogs(id: string): Promise<{ stdout: string; stderr: string }>;
373
401
 
402
+ // File operations
403
+ gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string }): Promise<GitCheckoutResponse>;
404
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<MkdirResponse>;
405
+ writeFile(path: string, content: string, options?: { encoding?: string }): Promise<WriteFileResponse>;
406
+ deleteFile(path: string): Promise<DeleteFileResponse>;
407
+ renameFile(oldPath: string, newPath: string): Promise<RenameFileResponse>;
408
+ moveFile(sourcePath: string, destinationPath: string): Promise<MoveFileResponse>;
409
+ readFile(path: string, options?: { encoding?: string }): Promise<ReadFileResponse>;
410
+ readFileStream(path: string): Promise<ReadableStream<Uint8Array>>;
411
+ listFiles(path: string, options?: { recursive?: boolean; includeHidden?: boolean }): Promise<ListFilesResponse>;
412
+
413
+ // Port management
414
+ exposePort(port: number, options: { name?: string; hostname: string }): Promise<{ url: string; port: number; name?: string }>;
415
+ unexposePort(port: number): Promise<void>;
416
+ getExposedPorts(hostname: string): Promise<Array<{ url: string; port: number; name?: string; exposedAt: string }>>;
417
+
418
+ // Environment management
419
+ setEnvVars(envVars: Record<string, string>): Promise<void>;
420
+ setSandboxName(name: string): Promise<void>;
421
+
374
422
  // Code Interpreter API
375
423
  createCodeContext(options?: CreateContextOptions): Promise<CodeContext>;
376
424
  runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult>;
@@ -379,6 +427,128 @@ export interface ISandbox {
379
427
  deleteCodeContext(contextId: string): Promise<void>;
380
428
  }
381
429
 
430
+ // Execution session returned by createSession()
431
+ // Sessions are full-featured sandbox objects with scoped execution context
432
+ // Inherits all ISandbox methods except createSession (sessions can't create sub-sessions),
433
+ // and setSandboxName (sessions inherit sandbox name).
434
+ export interface ExecutionSession extends Omit<ISandbox, 'createSession' | 'setSandboxName'> {
435
+ /**
436
+ * Session ID
437
+ */
438
+ id: string;
439
+ }
440
+
441
+ // API Response Types
442
+
443
+ export interface ExecuteResponse {
444
+ success: boolean;
445
+ stdout: string;
446
+ stderr: string;
447
+ exitCode: number;
448
+ command: string;
449
+ timestamp: string;
450
+ }
451
+
452
+ export interface GitCheckoutResponse {
453
+ success: boolean;
454
+ stdout: string;
455
+ stderr: string;
456
+ exitCode: number;
457
+ repoUrl: string;
458
+ branch: string;
459
+ targetDir: string;
460
+ timestamp: string;
461
+ }
462
+
463
+ export interface MkdirResponse {
464
+ success: boolean;
465
+ stdout: string;
466
+ stderr: string;
467
+ exitCode: number;
468
+ path: string;
469
+ recursive: boolean;
470
+ timestamp: string;
471
+ }
472
+
473
+ export interface WriteFileResponse {
474
+ success: boolean;
475
+ exitCode: number;
476
+ path: string;
477
+ timestamp: string;
478
+ }
479
+
480
+ export interface ReadFileResponse {
481
+ success: boolean;
482
+ exitCode: number;
483
+ path: string;
484
+ content: string;
485
+ timestamp: string;
486
+
487
+ /**
488
+ * Encoding used for content (utf-8 for text, base64 for binary)
489
+ */
490
+ encoding?: 'utf-8' | 'base64';
491
+
492
+ /**
493
+ * Whether the file is detected as binary
494
+ */
495
+ isBinary?: boolean;
496
+
497
+ /**
498
+ * MIME type of the file (e.g., 'image/png', 'text/plain')
499
+ */
500
+ mimeType?: string;
501
+
502
+ /**
503
+ * File size in bytes
504
+ */
505
+ size?: number;
506
+ }
507
+
508
+ export interface DeleteFileResponse {
509
+ success: boolean;
510
+ exitCode: number;
511
+ path: string;
512
+ timestamp: string;
513
+ }
514
+
515
+ export interface RenameFileResponse {
516
+ success: boolean;
517
+ exitCode: number;
518
+ oldPath: string;
519
+ newPath: string;
520
+ timestamp: string;
521
+ }
522
+
523
+ export interface MoveFileResponse {
524
+ success: boolean;
525
+ exitCode: number;
526
+ sourcePath: string;
527
+ destinationPath: string;
528
+ timestamp: string;
529
+ }
530
+
531
+ export interface ListFilesResponse {
532
+ success: boolean;
533
+ exitCode: number;
534
+ path: string;
535
+ files: Array<{
536
+ name: string;
537
+ absolutePath: string;
538
+ relativePath: string;
539
+ type: 'file' | 'directory' | 'symlink' | 'other';
540
+ size: number;
541
+ modifiedAt: string;
542
+ mode: string;
543
+ permissions: {
544
+ readable: boolean;
545
+ writable: boolean;
546
+ executable: boolean;
547
+ };
548
+ }>;
549
+ timestamp: string;
550
+ }
551
+
382
552
  // Type Guards
383
553
 
384
554
  export function isExecResult(value: any): value is ExecResult {
@@ -1,336 +0,0 @@
1
- import { type Kernel, KernelManager, ServerConnection } from "@jupyterlab/services";
2
- import type {
3
- IDisplayDataMsg,
4
- IErrorMsg,
5
- IExecuteResultMsg,
6
- IIOPubMessage,
7
- IStreamMsg
8
- } from "@jupyterlab/services/lib/kernel/messages";
9
- import {
10
- isDisplayDataMsg,
11
- isErrorMsg,
12
- isExecuteResultMsg,
13
- isStreamMsg
14
- } from "@jupyterlab/services/lib/kernel/messages";
15
- import { v4 as uuidv4 } from "uuid";
16
- import type { ExecutionResult } from "./mime-processor";
17
- import { processJupyterMessage } from "./mime-processor";
18
-
19
- export interface JupyterContext {
20
- id: string;
21
- language: string;
22
- connection: Kernel.IKernelConnection;
23
- cwd: string;
24
- createdAt: Date;
25
- lastUsed: Date;
26
- }
27
-
28
- export interface CreateContextRequest {
29
- language?: string;
30
- cwd?: string;
31
- envVars?: Record<string, string>;
32
- }
33
-
34
- export interface ExecuteCodeRequest {
35
- context_id?: string;
36
- code: string;
37
- language?: string;
38
- env_vars?: Record<string, string>;
39
- }
40
-
41
- export class JupyterServer {
42
- private kernelManager: KernelManager;
43
- private contexts = new Map<string, JupyterContext>();
44
- private defaultContexts = new Map<string, string>(); // language -> context_id
45
-
46
- constructor() {
47
- // Configure connection to local Jupyter server
48
- const serverSettings = ServerConnection.makeSettings({
49
- baseUrl: "http://localhost:8888",
50
- token: "",
51
- appUrl: "",
52
- wsUrl: "ws://localhost:8888",
53
- appendToken: false,
54
- init: {
55
- headers: {
56
- 'Content-Type': 'application/json'
57
- }
58
- }
59
- });
60
-
61
- this.kernelManager = new KernelManager({ serverSettings });
62
- }
63
-
64
- async initialize() {
65
- await this.kernelManager.ready;
66
- console.log("[JupyterServer] Kernel manager initialized");
67
-
68
- // Create default Python context
69
- const pythonContext = await this.createContext({ language: "python" });
70
- this.defaultContexts.set("python", pythonContext.id);
71
- console.log(
72
- "[JupyterServer] Default Python context created:",
73
- pythonContext.id
74
- );
75
- }
76
-
77
- async createContext(req: CreateContextRequest): Promise<JupyterContext> {
78
- const language = req.language || "python";
79
- const cwd = req.cwd || "/workspace";
80
-
81
- const kernelModel = await this.kernelManager.startNew({
82
- name: this.getKernelName(language),
83
- });
84
-
85
- const connection = this.kernelManager.connectTo({ model: kernelModel });
86
-
87
- const context: JupyterContext = {
88
- id: uuidv4(),
89
- language,
90
- connection,
91
- cwd,
92
- createdAt: new Date(),
93
- lastUsed: new Date(),
94
- };
95
-
96
- this.contexts.set(context.id, context);
97
-
98
- // Set working directory
99
- if (cwd !== "/workspace") {
100
- await this.changeWorkingDirectory(context, cwd);
101
- }
102
-
103
- // Set environment variables if provided
104
- if (req.envVars) {
105
- await this.setEnvironmentVariables(context, req.envVars);
106
- }
107
-
108
- return context;
109
- }
110
-
111
- async executeCode(
112
- contextId: string | undefined,
113
- code: string,
114
- language?: string
115
- ): Promise<Response> {
116
- let context: JupyterContext | undefined;
117
-
118
- if (contextId) {
119
- context = this.contexts.get(contextId);
120
- if (!context) {
121
- return new Response(
122
- JSON.stringify({ error: `Context ${contextId} not found` }),
123
- {
124
- status: 404,
125
- headers: { "Content-Type": "application/json" },
126
- }
127
- );
128
- }
129
- } else if (language) {
130
- // Use default context for the language
131
- const defaultContextId = this.defaultContexts.get(language);
132
- if (defaultContextId) {
133
- context = this.contexts.get(defaultContextId);
134
- }
135
-
136
- // Create new default context if needed
137
- if (!context) {
138
- context = await this.createContext({ language });
139
- this.defaultContexts.set(language, context.id);
140
- }
141
- } else {
142
- // Use default Python context
143
- const pythonContextId = this.defaultContexts.get("python");
144
- context = pythonContextId
145
- ? this.contexts.get(pythonContextId)
146
- : undefined;
147
- }
148
-
149
- if (!context) {
150
- return new Response(JSON.stringify({ error: "No context available" }), {
151
- status: 400,
152
- headers: { "Content-Type": "application/json" },
153
- });
154
- }
155
-
156
- // Update last used
157
- context.lastUsed = new Date();
158
-
159
- // Execute with streaming
160
- return this.streamExecution(context.connection, code);
161
- }
162
-
163
- private async streamExecution(
164
- connection: Kernel.IKernelConnection,
165
- code: string
166
- ): Promise<Response> {
167
- const stream = new ReadableStream({
168
- async start(controller) {
169
- const future = connection.requestExecute({
170
- code,
171
- stop_on_error: false,
172
- store_history: true,
173
- silent: false,
174
- allow_stdin: false,
175
- });
176
-
177
- // Handle different message types
178
- future.onIOPub = (msg: IIOPubMessage) => {
179
- const result = processJupyterMessage(msg);
180
- if (result) {
181
- controller.enqueue(
182
- new TextEncoder().encode(`${JSON.stringify(result)}\n`)
183
- );
184
- }
185
- };
186
-
187
- future.onReply = (msg: any) => {
188
- if (msg.content.status === "ok") {
189
- controller.enqueue(
190
- new TextEncoder().encode(
191
- `${JSON.stringify({
192
- type: "execution_complete",
193
- execution_count: msg.content.execution_count,
194
- })}\n`
195
- )
196
- );
197
- } else if (msg.content.status === "error") {
198
- controller.enqueue(
199
- new TextEncoder().encode(
200
- `${JSON.stringify({
201
- type: "error",
202
- ename: msg.content.ename,
203
- evalue: msg.content.evalue,
204
- traceback: msg.content.traceback,
205
- })}\n`
206
- )
207
- );
208
- }
209
- controller.close();
210
- };
211
-
212
- future.onStdin = (msg: any) => {
213
- // We don't support stdin for now
214
- console.warn("[JupyterServer] Stdin requested but not supported");
215
- };
216
- },
217
- });
218
-
219
- return new Response(stream, {
220
- headers: {
221
- "Content-Type": "text/event-stream",
222
- "Cache-Control": "no-cache",
223
- Connection: "keep-alive",
224
- },
225
- });
226
- }
227
-
228
- private getKernelName(language: string): string {
229
- const kernelMap: Record<string, string> = {
230
- python: "python3",
231
- javascript: "javascript",
232
- typescript: "javascript",
233
- js: "javascript",
234
- ts: "javascript",
235
- };
236
- return kernelMap[language.toLowerCase()] || "python3";
237
- }
238
-
239
- private async changeWorkingDirectory(context: JupyterContext, cwd: string) {
240
- const code =
241
- context.language === "python"
242
- ? `import os; os.chdir('${cwd}')`
243
- : `process.chdir('${cwd}')`;
244
-
245
- const future = context.connection.requestExecute({
246
- code,
247
- silent: true,
248
- store_history: false,
249
- });
250
-
251
- return future.done;
252
- }
253
-
254
- private async setEnvironmentVariables(
255
- context: JupyterContext,
256
- envVars: Record<string, string>
257
- ) {
258
- const commands: string[] = [];
259
-
260
- for (const [key, value] of Object.entries(envVars)) {
261
- if (context.language === "python") {
262
- commands.push(`import os; os.environ['${key}'] = '${value}'`);
263
- } else if (
264
- context.language === "javascript" ||
265
- context.language === "typescript"
266
- ) {
267
- commands.push(`process.env['${key}'] = '${value}'`);
268
- }
269
- }
270
-
271
- if (commands.length > 0) {
272
- const code = commands.join("\n");
273
- const future = context.connection.requestExecute({
274
- code,
275
- silent: true,
276
- store_history: false,
277
- });
278
-
279
- return future.done;
280
- }
281
- }
282
-
283
- async listContexts(): Promise<
284
- Array<{
285
- id: string;
286
- language: string;
287
- cwd: string;
288
- createdAt: Date;
289
- lastUsed: Date;
290
- }>
291
- > {
292
- return Array.from(this.contexts.values()).map((ctx) => ({
293
- id: ctx.id,
294
- language: ctx.language,
295
- cwd: ctx.cwd,
296
- createdAt: ctx.createdAt,
297
- lastUsed: ctx.lastUsed,
298
- }));
299
- }
300
-
301
- async deleteContext(contextId: string): Promise<void> {
302
- const context = this.contexts.get(contextId);
303
- if (!context) {
304
- throw new Error(`Context ${contextId} not found`);
305
- }
306
-
307
- // Shutdown the kernel
308
- await context.connection.shutdown();
309
-
310
- // Remove from maps
311
- this.contexts.delete(contextId);
312
-
313
- // Remove from default contexts if it was a default
314
- for (const [lang, id] of this.defaultContexts.entries()) {
315
- if (id === contextId) {
316
- this.defaultContexts.delete(lang);
317
- break;
318
- }
319
- }
320
- }
321
-
322
- async shutdown() {
323
- // Shutdown all kernels
324
- for (const context of this.contexts.values()) {
325
- try {
326
- await context.connection.shutdown();
327
- } catch (error) {
328
- console.error("[JupyterServer] Error shutting down kernel:", error);
329
- }
330
- }
331
-
332
- this.contexts.clear();
333
- this.defaultContexts.clear();
334
- }
335
- }
336
-