@cloudflare/sandbox 0.0.0-c87db11 → 0.0.0-cdb8197

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 +117 -0
  2. package/Dockerfile +32 -29
  3. package/README.md +127 -12
  4. package/container_src/bun.lock +31 -77
  5. package/container_src/control-process.ts +784 -0
  6. package/container_src/handler/exec.ts +99 -254
  7. package/container_src/handler/file.ts +253 -640
  8. package/container_src/handler/git.ts +28 -80
  9. package/container_src/handler/process.ts +443 -515
  10. package/container_src/handler/session.ts +92 -0
  11. package/container_src/index.ts +108 -163
  12. package/container_src/interpreter-service.ts +276 -0
  13. package/container_src/isolation.ts +1213 -0
  14. package/container_src/mime-processor.ts +1 -1
  15. package/container_src/package.json +4 -4
  16. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  17. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  18. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  19. package/container_src/runtime/process-pool.ts +464 -0
  20. package/container_src/shell-escape.ts +42 -0
  21. package/container_src/startup.sh +6 -79
  22. package/container_src/types.ts +35 -12
  23. package/package.json +2 -2
  24. package/src/client.ts +214 -187
  25. package/src/errors.ts +15 -14
  26. package/src/file-stream.ts +162 -0
  27. package/src/index.ts +43 -16
  28. package/src/{jupyter-client.ts → interpreter-client.ts} +6 -3
  29. package/src/interpreter-types.ts +102 -95
  30. package/src/interpreter.ts +8 -8
  31. package/src/sandbox.ts +314 -336
  32. package/src/types.ts +194 -24
  33. package/container_src/jupyter-server.ts +0 -579
  34. package/container_src/jupyter-service.ts +0 -458
  35. package/container_src/jupyter_config.py +0 -48
package/src/errors.ts CHANGED
@@ -23,17 +23,17 @@ export class SandboxError extends Error {
23
23
  }
24
24
 
25
25
  /**
26
- * Error thrown when Jupyter functionality is requested but the service is still initializing.
26
+ * Error thrown when interpreter functionality is requested but the service is still initializing.
27
27
  *
28
- * Note: With the current implementation, requests wait for Jupyter to be ready.
28
+ * Note: With the current implementation, requests wait for interpreter to be ready.
29
29
  * This error is only thrown when:
30
- * 1. The request times out waiting for Jupyter (default: 30 seconds)
31
- * 2. Jupyter initialization actually fails
30
+ * 1. The request times out waiting for interpreter (default: 30 seconds)
31
+ * 2. interpreter initialization actually fails
32
32
  *
33
33
  * Most requests will succeed after a delay, not throw this error.
34
34
  */
35
- export class JupyterNotReadyError extends SandboxError {
36
- public readonly code = "JUPYTER_NOT_READY";
35
+ export class InterpreterNotReadyError extends SandboxError {
36
+ public readonly code = "INTERPRETER_NOT_READY";
37
37
  public readonly retryAfter: number;
38
38
  public readonly progress?: number;
39
39
 
@@ -42,7 +42,8 @@ export class JupyterNotReadyError extends SandboxError {
42
42
  options?: { retryAfter?: number; progress?: number }
43
43
  ) {
44
44
  super(
45
- message || "Jupyter is still initializing. Please retry in a few seconds."
45
+ message ||
46
+ "Interpreter is still initializing. Please retry in a few seconds."
46
47
  );
47
48
  this.retryAfter = options?.retryAfter || 5;
48
49
  this.progress = options?.progress;
@@ -123,12 +124,12 @@ export class ServiceUnavailableError extends SandboxError {
123
124
  }
124
125
 
125
126
  /**
126
- * Type guard to check if an error is a JupyterNotReadyError
127
+ * Type guard to check if an error is a InterpreterNotReadyError
127
128
  */
128
- export function isJupyterNotReadyError(
129
+ export function isInterpreterNotReadyError(
129
130
  error: unknown
130
- ): error is JupyterNotReadyError {
131
- return error instanceof JupyterNotReadyError;
131
+ ): error is InterpreterNotReadyError {
132
+ return error instanceof InterpreterNotReadyError;
132
133
  }
133
134
 
134
135
  /**
@@ -143,7 +144,7 @@ export function isSandboxError(error: unknown): error is SandboxError {
143
144
  */
144
145
  export function isRetryableError(error: unknown): boolean {
145
146
  if (
146
- error instanceof JupyterNotReadyError ||
147
+ error instanceof InterpreterNotReadyError ||
147
148
  error instanceof ContainerNotReadyError ||
148
149
  error instanceof ServiceUnavailableError
149
150
  ) {
@@ -189,9 +190,9 @@ export async function parseErrorResponse(
189
190
  );
190
191
  }
191
192
 
192
- // Jupyter initialization error
193
+ // Interpreter initialization error
193
194
  if (data.status === "initializing") {
194
- return new JupyterNotReadyError(data.error, {
195
+ return new InterpreterNotReadyError(data.error, {
195
196
  retryAfter: parseInt(response.headers.get("Retry-After") || "5"),
196
197
  progress: data.progress,
197
198
  });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * File streaming utilities for reading binary and text files
3
+ * Provides simple AsyncIterable API over SSE stream with automatic base64 decoding
4
+ */
5
+
6
+ import { parseSSEStream } from './sse-parser';
7
+ import type { FileChunk, FileMetadata, FileStreamEvent } from './types';
8
+
9
+ /**
10
+ * Convert ReadableStream of SSE file events to AsyncIterable of file chunks
11
+ * Automatically decodes base64 for binary files and provides metadata
12
+ *
13
+ * @param stream - The SSE ReadableStream from readFileStream()
14
+ * @param signal - Optional AbortSignal for cancellation
15
+ * @returns AsyncIterable that yields file chunks (string for text, Uint8Array for binary)
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const stream = await sandbox.readFileStream('/path/to/file.png');
20
+ *
21
+ * for await (const chunk of streamFile(stream)) {
22
+ * if (chunk instanceof Uint8Array) {
23
+ * // Binary chunk - already decoded from base64
24
+ * console.log('Binary chunk:', chunk.byteLength, 'bytes');
25
+ * } else {
26
+ * // Text chunk
27
+ * console.log('Text chunk:', chunk);
28
+ * }
29
+ * }
30
+ *
31
+ * // Access metadata
32
+ * const iter = streamFile(stream);
33
+ * for await (const chunk of iter) {
34
+ * console.log('MIME type:', iter.metadata?.mimeType);
35
+ * // process chunk...
36
+ * }
37
+ * ```
38
+ */
39
+ export async function* streamFile(
40
+ stream: ReadableStream<Uint8Array>,
41
+ signal?: AbortSignal
42
+ ): AsyncGenerator<FileChunk, void, undefined> {
43
+ let metadata: FileMetadata | undefined;
44
+
45
+ try {
46
+ for await (const event of parseSSEStream<FileStreamEvent>(stream, signal)) {
47
+ switch (event.type) {
48
+ case 'metadata':
49
+ // Store metadata for access via iterator
50
+ metadata = {
51
+ mimeType: event.mimeType,
52
+ size: event.size,
53
+ isBinary: event.isBinary,
54
+ encoding: event.encoding,
55
+ };
56
+ // Store on generator function for external access
57
+ (streamFile as any).metadata = metadata;
58
+ break;
59
+
60
+ case 'chunk':
61
+ // Auto-decode base64 for binary files
62
+ if (metadata?.isBinary && metadata?.encoding === 'base64') {
63
+ // Decode base64 to Uint8Array
64
+ const binaryString = atob(event.data);
65
+ const bytes = new Uint8Array(binaryString.length);
66
+ for (let i = 0; i < binaryString.length; i++) {
67
+ bytes[i] = binaryString.charCodeAt(i);
68
+ }
69
+ yield bytes;
70
+ } else {
71
+ // Text file - yield as-is
72
+ yield event.data;
73
+ }
74
+ break;
75
+
76
+ case 'complete':
77
+ // Stream completed successfully
78
+ console.log(`[streamFile] File streaming complete: ${event.bytesRead} bytes read`);
79
+ return;
80
+
81
+ case 'error':
82
+ // Stream error
83
+ throw new Error(`File streaming error: ${event.error}`);
84
+ }
85
+ }
86
+ } catch (error) {
87
+ console.error('[streamFile] Error streaming file:', error);
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Helper to collect entire file from stream into memory
94
+ * Useful for smaller files where you want the complete content at once
95
+ *
96
+ * @param stream - The SSE ReadableStream from readFileStream()
97
+ * @param signal - Optional AbortSignal for cancellation
98
+ * @returns Object with content (string or Uint8Array) and metadata
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const stream = await sandbox.readFileStream('/path/to/image.png');
103
+ * const { content, metadata } = await collectFile(stream);
104
+ *
105
+ * if (content instanceof Uint8Array) {
106
+ * console.log('Binary file:', metadata.mimeType, content.byteLength, 'bytes');
107
+ * } else {
108
+ * console.log('Text file:', metadata.mimeType, content.length, 'chars');
109
+ * }
110
+ * ```
111
+ */
112
+ export async function collectFile(
113
+ stream: ReadableStream<Uint8Array>,
114
+ signal?: AbortSignal
115
+ ): Promise<{ content: string | Uint8Array; metadata: FileMetadata }> {
116
+ let metadata: FileMetadata | undefined;
117
+ const chunks: FileChunk[] = [];
118
+
119
+ for await (const chunk of streamFile(stream, signal)) {
120
+ chunks.push(chunk);
121
+ // Capture metadata from first iteration
122
+ if (!metadata && (streamFile as any).metadata) {
123
+ metadata = (streamFile as any).metadata;
124
+ }
125
+ }
126
+
127
+ if (!metadata) {
128
+ throw new Error('No metadata received from file stream');
129
+ }
130
+
131
+ // Combine chunks based on type
132
+ if (chunks.length === 0) {
133
+ // Empty file
134
+ return {
135
+ content: metadata.isBinary ? new Uint8Array(0) : '',
136
+ metadata,
137
+ };
138
+ }
139
+
140
+ // Check if binary or text based on first chunk
141
+ if (chunks[0] instanceof Uint8Array) {
142
+ // Binary file - concatenate Uint8Arrays
143
+ const totalLength = chunks.reduce((sum, chunk) => {
144
+ return sum + (chunk as Uint8Array).byteLength;
145
+ }, 0);
146
+
147
+ const result = new Uint8Array(totalLength);
148
+ let offset = 0;
149
+ for (const chunk of chunks) {
150
+ result.set(chunk as Uint8Array, offset);
151
+ offset += (chunk as Uint8Array).byteLength;
152
+ }
153
+
154
+ return { content: result, metadata };
155
+ } else {
156
+ // Text file - concatenate strings
157
+ return {
158
+ content: chunks.join(''),
159
+ metadata,
160
+ };
161
+ }
162
+ }
package/src/index.ts CHANGED
@@ -1,29 +1,31 @@
1
- // Export types from client
2
- export type {
3
- DeleteFileResponse,
4
- ExecuteResponse,
5
- GitCheckoutResponse,
6
- MkdirResponse,
7
- MoveFileResponse,
8
- ReadFileResponse,
9
- RenameFileResponse,
10
- WriteFileResponse,
11
- } from "./client";
12
- // Export errors
1
+ // biome-ignore-start assist/source/organizeImports: Need separate exports for deprecation warnings to work properly
2
+ /**
3
+ * @deprecated Use `InterpreterNotReadyError` instead. Will be removed in a future version.
4
+ */
5
+ export { InterpreterNotReadyError as JupyterNotReadyError } from "./errors";
6
+
7
+ /**
8
+ * @deprecated Use `isInterpreterNotReadyError` instead. Will be removed in a future version.
9
+ */
10
+ export { isInterpreterNotReadyError as isJupyterNotReadyError } from "./errors";
11
+ // biome-ignore-end assist/source/organizeImports: Need separate exports for deprecation warnings to work properly
12
+
13
+ // Export API response types
13
14
  export {
14
15
  CodeExecutionError,
15
16
  ContainerNotReadyError,
16
17
  ContextNotFoundError,
17
- isJupyterNotReadyError,
18
+ InterpreterNotReadyError,
19
+ isInterpreterNotReadyError,
18
20
  isRetryableError,
19
21
  isSandboxError,
20
- JupyterNotReadyError,
21
22
  parseErrorResponse,
22
23
  SandboxError,
23
24
  type SandboxErrorResponse,
24
25
  SandboxNetworkError,
25
26
  ServiceUnavailableError,
26
27
  } from "./errors";
28
+
27
29
  // Export code interpreter types
28
30
  export type {
29
31
  ChartData,
@@ -50,5 +52,30 @@ export {
50
52
  parseSSEStream,
51
53
  responseToAsyncIterable,
52
54
  } from "./sse-parser";
53
- // Export event types for streaming
54
- export type { ExecEvent, LogEvent } from "./types";
55
+ // Export file streaming utilities
56
+ export { streamFile, collectFile } from "./file-stream";
57
+ export type {
58
+ DeleteFileResponse,
59
+ ExecEvent,
60
+ ExecOptions,
61
+ ExecResult,
62
+ ExecuteResponse,
63
+ ExecutionSession,
64
+ FileChunk,
65
+ FileMetadata,
66
+ FileStream,
67
+ FileStreamEvent,
68
+ GitCheckoutResponse,
69
+ ISandbox,
70
+ ListFilesResponse,
71
+ LogEvent,
72
+ MkdirResponse,
73
+ MoveFileResponse,
74
+ Process,
75
+ ProcessOptions,
76
+ ProcessStatus,
77
+ ReadFileResponse,
78
+ RenameFileResponse,
79
+ StreamOptions,
80
+ WriteFileResponse,
81
+ } from "./types";
@@ -62,7 +62,7 @@ export interface ExecutionCallbacks {
62
62
  onError?: (error: ExecutionError) => void | Promise<void>;
63
63
  }
64
64
 
65
- export class JupyterClient extends HttpClient {
65
+ export class InterpreterClient extends HttpClient {
66
66
  private readonly maxRetries = 3;
67
67
  private readonly retryDelayMs = 1000;
68
68
 
@@ -239,7 +239,10 @@ export class JupyterClient extends HttpClient {
239
239
  break;
240
240
  }
241
241
  } catch (error) {
242
- console.error("[JupyterClient] Error parsing execution result:", error);
242
+ console.error(
243
+ "[InterpreterClient] Error parsing execution result:",
244
+ error
245
+ );
243
246
  }
244
247
  }
245
248
 
@@ -295,7 +298,7 @@ export class JupyterClient extends HttpClient {
295
298
  } catch (error) {
296
299
  lastError = error as Error;
297
300
 
298
- // Check if it's a retryable error (circuit breaker or Jupyter not ready)
301
+ // Check if it's a retryable error (circuit breaker or interpreter not ready)
299
302
  if (this.isRetryableError(error)) {
300
303
  // Don't retry on the last attempt
301
304
  if (attempt < this.maxRetries - 1) {