@cloudflare/sandbox 0.5.4 → 0.6.0

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 (57) hide show
  1. package/Dockerfile +54 -59
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +12 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +13 -8
  8. package/.turbo/turbo-build.log +0 -23
  9. package/CHANGELOG.md +0 -441
  10. package/src/clients/base-client.ts +0 -356
  11. package/src/clients/command-client.ts +0 -133
  12. package/src/clients/file-client.ts +0 -300
  13. package/src/clients/git-client.ts +0 -98
  14. package/src/clients/index.ts +0 -64
  15. package/src/clients/interpreter-client.ts +0 -333
  16. package/src/clients/port-client.ts +0 -105
  17. package/src/clients/process-client.ts +0 -198
  18. package/src/clients/sandbox-client.ts +0 -39
  19. package/src/clients/types.ts +0 -88
  20. package/src/clients/utility-client.ts +0 -156
  21. package/src/errors/adapter.ts +0 -238
  22. package/src/errors/classes.ts +0 -594
  23. package/src/errors/index.ts +0 -109
  24. package/src/file-stream.ts +0 -169
  25. package/src/index.ts +0 -121
  26. package/src/interpreter.ts +0 -168
  27. package/src/openai/index.ts +0 -465
  28. package/src/request-handler.ts +0 -184
  29. package/src/sandbox.ts +0 -1937
  30. package/src/security.ts +0 -119
  31. package/src/sse-parser.ts +0 -144
  32. package/src/storage-mount/credential-detection.ts +0 -41
  33. package/src/storage-mount/errors.ts +0 -51
  34. package/src/storage-mount/index.ts +0 -17
  35. package/src/storage-mount/provider-detection.ts +0 -93
  36. package/src/storage-mount/types.ts +0 -17
  37. package/src/version.ts +0 -6
  38. package/tests/base-client.test.ts +0 -582
  39. package/tests/command-client.test.ts +0 -444
  40. package/tests/file-client.test.ts +0 -831
  41. package/tests/file-stream.test.ts +0 -310
  42. package/tests/get-sandbox.test.ts +0 -172
  43. package/tests/git-client.test.ts +0 -455
  44. package/tests/openai-shell-editor.test.ts +0 -434
  45. package/tests/port-client.test.ts +0 -283
  46. package/tests/process-client.test.ts +0 -649
  47. package/tests/request-handler.test.ts +0 -292
  48. package/tests/sandbox.test.ts +0 -890
  49. package/tests/sse-parser.test.ts +0 -291
  50. package/tests/storage-mount/credential-detection.test.ts +0 -119
  51. package/tests/storage-mount/provider-detection.test.ts +0 -77
  52. package/tests/utility-client.test.ts +0 -339
  53. package/tests/version.test.ts +0 -16
  54. package/tests/wrangler.jsonc +0 -35
  55. package/tsconfig.json +0 -11
  56. package/tsdown.config.ts +0 -13
  57. package/vitest.config.ts +0 -31
@@ -1,333 +0,0 @@
1
- import {
2
- type CodeContext,
3
- type ContextCreateResult,
4
- type ContextListResult,
5
- type CreateContextOptions,
6
- type ExecutionError,
7
- type OutputMessage,
8
- type Result,
9
- ResultImpl
10
- } from '@repo/shared';
11
- import type { ErrorResponse } from '../errors';
12
- import {
13
- createErrorFromResponse,
14
- ErrorCode,
15
- InterpreterNotReadyError
16
- } from '../errors';
17
- import { BaseHttpClient } from './base-client.js';
18
- import type { HttpClientOptions } from './types.js';
19
-
20
- // Streaming execution data from the server
21
- interface StreamingExecutionData {
22
- type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete';
23
- text?: string;
24
- html?: string;
25
- png?: string; // base64
26
- jpeg?: string; // base64
27
- svg?: string;
28
- latex?: string;
29
- markdown?: string;
30
- javascript?: string;
31
- json?: unknown;
32
- chart?: {
33
- type:
34
- | 'line'
35
- | 'bar'
36
- | 'scatter'
37
- | 'pie'
38
- | 'histogram'
39
- | 'heatmap'
40
- | 'unknown';
41
- data: unknown;
42
- options?: unknown;
43
- };
44
- data?: unknown;
45
- metadata?: Record<string, unknown>;
46
- execution_count?: number;
47
- ename?: string;
48
- evalue?: string;
49
- traceback?: string[];
50
- lineNumber?: number;
51
- timestamp?: number;
52
- }
53
-
54
- export interface ExecutionCallbacks {
55
- onStdout?: (output: OutputMessage) => void | Promise<void>;
56
- onStderr?: (output: OutputMessage) => void | Promise<void>;
57
- onResult?: (result: Result) => void | Promise<void>;
58
- onError?: (error: ExecutionError) => void | Promise<void>;
59
- }
60
-
61
- export class InterpreterClient extends BaseHttpClient {
62
- private readonly maxRetries = 3;
63
- private readonly retryDelayMs = 1000;
64
-
65
- async createCodeContext(
66
- options: CreateContextOptions = {}
67
- ): Promise<CodeContext> {
68
- return this.executeWithRetry(async () => {
69
- const response = await this.doFetch('/api/contexts', {
70
- method: 'POST',
71
- headers: { 'Content-Type': 'application/json' },
72
- body: JSON.stringify({
73
- language: options.language || 'python',
74
- cwd: options.cwd || '/workspace',
75
- env_vars: options.envVars
76
- })
77
- });
78
-
79
- if (!response.ok) {
80
- const error = await this.parseErrorResponse(response);
81
- throw error;
82
- }
83
-
84
- const data = (await response.json()) as ContextCreateResult;
85
- if (!data.success) {
86
- throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
87
- }
88
-
89
- return {
90
- id: data.contextId,
91
- language: data.language,
92
- cwd: data.cwd || '/workspace',
93
- createdAt: new Date(data.timestamp),
94
- lastUsed: new Date(data.timestamp)
95
- };
96
- });
97
- }
98
-
99
- async runCodeStream(
100
- contextId: string | undefined,
101
- code: string,
102
- language: string | undefined,
103
- callbacks: ExecutionCallbacks,
104
- timeoutMs?: number
105
- ): Promise<void> {
106
- return this.executeWithRetry(async () => {
107
- const response = await this.doFetch('/api/execute/code', {
108
- method: 'POST',
109
- headers: {
110
- 'Content-Type': 'application/json',
111
- Accept: 'text/event-stream'
112
- },
113
- body: JSON.stringify({
114
- context_id: contextId,
115
- code,
116
- language,
117
- ...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
118
- })
119
- });
120
-
121
- if (!response.ok) {
122
- const error = await this.parseErrorResponse(response);
123
- throw error;
124
- }
125
-
126
- if (!response.body) {
127
- throw new Error('No response body for streaming execution');
128
- }
129
-
130
- // Process streaming response
131
- for await (const chunk of this.readLines(response.body)) {
132
- await this.parseExecutionResult(chunk, callbacks);
133
- }
134
- });
135
- }
136
-
137
- async listCodeContexts(): Promise<CodeContext[]> {
138
- return this.executeWithRetry(async () => {
139
- const response = await this.doFetch('/api/contexts', {
140
- method: 'GET',
141
- headers: { 'Content-Type': 'application/json' }
142
- });
143
-
144
- if (!response.ok) {
145
- const error = await this.parseErrorResponse(response);
146
- throw error;
147
- }
148
-
149
- const data = (await response.json()) as ContextListResult;
150
- if (!data.success) {
151
- throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
152
- }
153
-
154
- return data.contexts.map((ctx) => ({
155
- id: ctx.id,
156
- language: ctx.language,
157
- cwd: ctx.cwd || '/workspace',
158
- createdAt: new Date(data.timestamp),
159
- lastUsed: new Date(data.timestamp)
160
- }));
161
- });
162
- }
163
-
164
- async deleteCodeContext(contextId: string): Promise<void> {
165
- return this.executeWithRetry(async () => {
166
- const response = await this.doFetch(`/api/contexts/${contextId}`, {
167
- method: 'DELETE',
168
- headers: { 'Content-Type': 'application/json' }
169
- });
170
-
171
- if (!response.ok) {
172
- const error = await this.parseErrorResponse(response);
173
- throw error;
174
- }
175
- });
176
- }
177
-
178
- /**
179
- * Execute an operation with automatic retry for transient errors
180
- */
181
- private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
182
- let lastError: Error | undefined;
183
-
184
- for (let attempt = 0; attempt < this.maxRetries; attempt++) {
185
- try {
186
- return await operation();
187
- } catch (error) {
188
- this.logError('executeWithRetry', error);
189
- lastError = error as Error;
190
-
191
- // Check if it's a retryable error (interpreter not ready)
192
- if (this.isRetryableError(error)) {
193
- // Don't retry on the last attempt
194
- if (attempt < this.maxRetries - 1) {
195
- // Exponential backoff with jitter
196
- const delay =
197
- this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
198
- await new Promise((resolve) => setTimeout(resolve, delay));
199
- continue;
200
- }
201
- }
202
-
203
- // Not retryable or last attempt - throw the error
204
- throw error;
205
- }
206
- }
207
-
208
- throw lastError || new Error('Execution failed after retries');
209
- }
210
-
211
- private isRetryableError(error: unknown): boolean {
212
- if (error instanceof InterpreterNotReadyError) {
213
- return true;
214
- }
215
-
216
- if (error instanceof Error) {
217
- return (
218
- error.message.includes('not ready') ||
219
- error.message.includes('initializing')
220
- );
221
- }
222
-
223
- return false;
224
- }
225
-
226
- private async parseErrorResponse(response: Response): Promise<Error> {
227
- try {
228
- const errorData = (await response.json()) as ErrorResponse;
229
- return createErrorFromResponse(errorData);
230
- } catch {
231
- // Fallback if response isn't JSON
232
- const errorResponse: ErrorResponse = {
233
- code: ErrorCode.INTERNAL_ERROR,
234
- message: `HTTP ${response.status}: ${response.statusText}`,
235
- context: {},
236
- httpStatus: response.status,
237
- timestamp: new Date().toISOString()
238
- };
239
- return createErrorFromResponse(errorResponse);
240
- }
241
- }
242
-
243
- private async *readLines(
244
- stream: ReadableStream<Uint8Array>
245
- ): AsyncGenerator<string> {
246
- const reader = stream.getReader();
247
- let buffer = '';
248
-
249
- try {
250
- while (true) {
251
- const { done, value } = await reader.read();
252
- if (value) {
253
- buffer += new TextDecoder().decode(value);
254
- }
255
- if (done) break;
256
-
257
- let newlineIdx = buffer.indexOf('\n');
258
- while (newlineIdx !== -1) {
259
- yield buffer.slice(0, newlineIdx);
260
- buffer = buffer.slice(newlineIdx + 1);
261
- newlineIdx = buffer.indexOf('\n');
262
- }
263
- }
264
-
265
- // Yield any remaining data
266
- if (buffer.length > 0) {
267
- yield buffer;
268
- }
269
- } finally {
270
- reader.releaseLock();
271
- }
272
- }
273
-
274
- private async parseExecutionResult(
275
- line: string,
276
- callbacks: ExecutionCallbacks
277
- ) {
278
- if (!line.trim()) return;
279
-
280
- // Skip lines that don't start with "data: " (SSE format)
281
- if (!line.startsWith('data: ')) return;
282
-
283
- try {
284
- // Strip "data: " prefix and parse JSON
285
- const jsonData = line.substring(6); // "data: " is 6 characters
286
- const data = JSON.parse(jsonData) as StreamingExecutionData;
287
-
288
- switch (data.type) {
289
- case 'stdout':
290
- if (callbacks.onStdout && data.text) {
291
- await callbacks.onStdout({
292
- text: data.text,
293
- timestamp: data.timestamp || Date.now()
294
- });
295
- }
296
- break;
297
-
298
- case 'stderr':
299
- if (callbacks.onStderr && data.text) {
300
- await callbacks.onStderr({
301
- text: data.text,
302
- timestamp: data.timestamp || Date.now()
303
- });
304
- }
305
- break;
306
-
307
- case 'result':
308
- if (callbacks.onResult) {
309
- // Create a ResultImpl instance from the raw data
310
- const result = new ResultImpl(data);
311
- await callbacks.onResult(result);
312
- }
313
- break;
314
-
315
- case 'error':
316
- if (callbacks.onError) {
317
- await callbacks.onError({
318
- name: data.ename || 'Error',
319
- message: data.evalue || 'Unknown error',
320
- traceback: data.traceback || []
321
- });
322
- }
323
- break;
324
-
325
- case 'execution_complete':
326
- // Signal completion - callbacks can handle cleanup if needed
327
- break;
328
- }
329
- } catch (error) {
330
- this.logError('parseExecutionResult', error);
331
- }
332
- }
333
- }
@@ -1,105 +0,0 @@
1
- import type {
2
- PortCloseResult,
3
- PortExposeResult,
4
- PortListResult
5
- } from '@repo/shared';
6
- import { BaseHttpClient } from './base-client';
7
- import type { HttpClientOptions } from './types';
8
-
9
- // Re-export for convenience
10
- export type { PortExposeResult, PortCloseResult, PortListResult };
11
-
12
- /**
13
- * Request interface for exposing ports
14
- */
15
- export interface ExposePortRequest {
16
- port: number;
17
- name?: string;
18
- }
19
-
20
- /**
21
- * Request interface for unexposing ports
22
- */
23
- export interface UnexposePortRequest {
24
- port: number;
25
- }
26
-
27
- /**
28
- * Client for port management and preview URL operations
29
- */
30
- export class PortClient extends BaseHttpClient {
31
- /**
32
- * Expose a port and get a preview URL
33
- * @param port - Port number to expose
34
- * @param sessionId - The session ID for this operation
35
- * @param name - Optional name for the port
36
- */
37
- async exposePort(
38
- port: number,
39
- sessionId: string,
40
- name?: string
41
- ): Promise<PortExposeResult> {
42
- try {
43
- const data = { port, sessionId, name };
44
-
45
- const response = await this.post<PortExposeResult>(
46
- '/api/expose-port',
47
- data
48
- );
49
-
50
- this.logSuccess(
51
- 'Port exposed',
52
- `${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
53
- );
54
-
55
- return response;
56
- } catch (error) {
57
- this.logError('exposePort', error);
58
- throw error;
59
- }
60
- }
61
-
62
- /**
63
- * Unexpose a port and remove its preview URL
64
- * @param port - Port number to unexpose
65
- * @param sessionId - The session ID for this operation
66
- */
67
- async unexposePort(
68
- port: number,
69
- sessionId: string
70
- ): Promise<PortCloseResult> {
71
- try {
72
- const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(
73
- sessionId
74
- )}`;
75
- const response = await this.delete<PortCloseResult>(url);
76
-
77
- this.logSuccess('Port unexposed', `${port}`);
78
- return response;
79
- } catch (error) {
80
- this.logError('unexposePort', error);
81
- throw error;
82
- }
83
- }
84
-
85
- /**
86
- * Get all currently exposed ports
87
- * @param sessionId - The session ID for this operation
88
- */
89
- async getExposedPorts(sessionId: string): Promise<PortListResult> {
90
- try {
91
- const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
92
- const response = await this.get<PortListResult>(url);
93
-
94
- this.logSuccess(
95
- 'Exposed ports retrieved',
96
- `${response.ports.length} ports exposed`
97
- );
98
-
99
- return response;
100
- } catch (error) {
101
- this.logError('getExposedPorts', error);
102
- throw error;
103
- }
104
- }
105
- }
@@ -1,198 +0,0 @@
1
- import type {
2
- ProcessCleanupResult,
3
- ProcessInfoResult,
4
- ProcessKillResult,
5
- ProcessListResult,
6
- ProcessLogsResult,
7
- ProcessStartResult,
8
- StartProcessRequest
9
- } from '@repo/shared';
10
- import { BaseHttpClient } from './base-client';
11
- import type { HttpClientOptions } from './types';
12
-
13
- // Re-export for convenience
14
- export type {
15
- StartProcessRequest,
16
- ProcessStartResult,
17
- ProcessListResult,
18
- ProcessInfoResult,
19
- ProcessKillResult,
20
- ProcessLogsResult,
21
- ProcessCleanupResult
22
- };
23
-
24
- /**
25
- * Client for background process management
26
- */
27
- export class ProcessClient extends BaseHttpClient {
28
- /**
29
- * Start a background process
30
- * @param command - Command to execute as a background process
31
- * @param sessionId - The session ID for this operation
32
- * @param options - Optional settings (processId)
33
- */
34
- async startProcess(
35
- command: string,
36
- sessionId: string,
37
- options?: {
38
- processId?: string;
39
- timeoutMs?: number;
40
- env?: Record<string, string>;
41
- cwd?: string;
42
- encoding?: string;
43
- autoCleanup?: boolean;
44
- }
45
- ): Promise<ProcessStartResult> {
46
- try {
47
- const data: StartProcessRequest = {
48
- command,
49
- sessionId,
50
- ...(options?.processId !== undefined && {
51
- processId: options.processId
52
- }),
53
- ...(options?.timeoutMs !== undefined && {
54
- timeoutMs: options.timeoutMs
55
- }),
56
- ...(options?.env !== undefined && { env: options.env }),
57
- ...(options?.cwd !== undefined && { cwd: options.cwd }),
58
- ...(options?.encoding !== undefined && { encoding: options.encoding }),
59
- ...(options?.autoCleanup !== undefined && {
60
- autoCleanup: options.autoCleanup
61
- })
62
- };
63
-
64
- const response = await this.post<ProcessStartResult>(
65
- '/api/process/start',
66
- data
67
- );
68
-
69
- this.logSuccess(
70
- 'Process started',
71
- `${command} (ID: ${response.processId})`
72
- );
73
-
74
- return response;
75
- } catch (error) {
76
- this.logError('startProcess', error);
77
- throw error;
78
- }
79
- }
80
-
81
- /**
82
- * List all processes (sandbox-scoped, not session-scoped)
83
- */
84
- async listProcesses(): Promise<ProcessListResult> {
85
- try {
86
- const url = `/api/process/list`;
87
- const response = await this.get<ProcessListResult>(url);
88
-
89
- this.logSuccess(
90
- 'Processes listed',
91
- `${response.processes.length} processes`
92
- );
93
- return response;
94
- } catch (error) {
95
- this.logError('listProcesses', error);
96
- throw error;
97
- }
98
- }
99
-
100
- /**
101
- * Get information about a specific process (sandbox-scoped, not session-scoped)
102
- * @param processId - ID of the process to retrieve
103
- */
104
- async getProcess(processId: string): Promise<ProcessInfoResult> {
105
- try {
106
- const url = `/api/process/${processId}`;
107
- const response = await this.get<ProcessInfoResult>(url);
108
-
109
- this.logSuccess('Process retrieved', `ID: ${processId}`);
110
- return response;
111
- } catch (error) {
112
- this.logError('getProcess', error);
113
- throw error;
114
- }
115
- }
116
-
117
- /**
118
- * Kill a specific process (sandbox-scoped, not session-scoped)
119
- * @param processId - ID of the process to kill
120
- */
121
- async killProcess(processId: string): Promise<ProcessKillResult> {
122
- try {
123
- const url = `/api/process/${processId}`;
124
- const response = await this.delete<ProcessKillResult>(url);
125
-
126
- this.logSuccess('Process killed', `ID: ${processId}`);
127
- return response;
128
- } catch (error) {
129
- this.logError('killProcess', error);
130
- throw error;
131
- }
132
- }
133
-
134
- /**
135
- * Kill all running processes (sandbox-scoped, not session-scoped)
136
- */
137
- async killAllProcesses(): Promise<ProcessCleanupResult> {
138
- try {
139
- const url = `/api/process/kill-all`;
140
- const response = await this.delete<ProcessCleanupResult>(url);
141
-
142
- this.logSuccess(
143
- 'All processes killed',
144
- `${response.cleanedCount} processes terminated`
145
- );
146
-
147
- return response;
148
- } catch (error) {
149
- this.logError('killAllProcesses', error);
150
- throw error;
151
- }
152
- }
153
-
154
- /**
155
- * Get logs from a specific process (sandbox-scoped, not session-scoped)
156
- * @param processId - ID of the process to get logs from
157
- */
158
- async getProcessLogs(processId: string): Promise<ProcessLogsResult> {
159
- try {
160
- const url = `/api/process/${processId}/logs`;
161
- const response = await this.get<ProcessLogsResult>(url);
162
-
163
- this.logSuccess(
164
- 'Process logs retrieved',
165
- `ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`
166
- );
167
-
168
- return response;
169
- } catch (error) {
170
- this.logError('getProcessLogs', error);
171
- throw error;
172
- }
173
- }
174
-
175
- /**
176
- * Stream logs from a specific process (sandbox-scoped, not session-scoped)
177
- * @param processId - ID of the process to stream logs from
178
- */
179
- async streamProcessLogs(
180
- processId: string
181
- ): Promise<ReadableStream<Uint8Array>> {
182
- try {
183
- const url = `/api/process/${processId}/stream`;
184
- const response = await this.doFetch(url, {
185
- method: 'GET'
186
- });
187
-
188
- const stream = await this.handleStreamResponse(response);
189
-
190
- this.logSuccess('Process log stream started', `ID: ${processId}`);
191
-
192
- return stream;
193
- } catch (error) {
194
- this.logError('streamProcessLogs', error);
195
- throw error;
196
- }
197
- }
198
- }
@@ -1,39 +0,0 @@
1
- import { CommandClient } from './command-client';
2
- import { FileClient } from './file-client';
3
- import { GitClient } from './git-client';
4
- import { InterpreterClient } from './interpreter-client';
5
- import { PortClient } from './port-client';
6
- import { ProcessClient } from './process-client';
7
- import type { HttpClientOptions } from './types';
8
- import { UtilityClient } from './utility-client';
9
-
10
- /**
11
- * Main sandbox client that composes all domain-specific clients
12
- * Provides organized access to all sandbox functionality
13
- */
14
- export class SandboxClient {
15
- public readonly commands: CommandClient;
16
- public readonly files: FileClient;
17
- public readonly processes: ProcessClient;
18
- public readonly ports: PortClient;
19
- public readonly git: GitClient;
20
- public readonly interpreter: InterpreterClient;
21
- public readonly utils: UtilityClient;
22
-
23
- constructor(options: HttpClientOptions) {
24
- // Ensure baseUrl is provided for all clients
25
- const clientOptions: HttpClientOptions = {
26
- baseUrl: 'http://localhost:3000',
27
- ...options
28
- };
29
-
30
- // Initialize all domain clients with shared options
31
- this.commands = new CommandClient(clientOptions);
32
- this.files = new FileClient(clientOptions);
33
- this.processes = new ProcessClient(clientOptions);
34
- this.ports = new PortClient(clientOptions);
35
- this.git = new GitClient(clientOptions);
36
- this.interpreter = new InterpreterClient(clientOptions);
37
- this.utils = new UtilityClient(clientOptions);
38
- }
39
- }