@cloudflare/sandbox 0.5.6 → 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 (56) hide show
  1. package/Dockerfile +54 -56
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -1
  5. package/dist/index.js.map +1 -1
  6. package/package.json +11 -6
  7. package/.turbo/turbo-build.log +0 -23
  8. package/CHANGELOG.md +0 -463
  9. package/src/clients/base-client.ts +0 -356
  10. package/src/clients/command-client.ts +0 -133
  11. package/src/clients/file-client.ts +0 -300
  12. package/src/clients/git-client.ts +0 -98
  13. package/src/clients/index.ts +0 -64
  14. package/src/clients/interpreter-client.ts +0 -339
  15. package/src/clients/port-client.ts +0 -105
  16. package/src/clients/process-client.ts +0 -198
  17. package/src/clients/sandbox-client.ts +0 -39
  18. package/src/clients/types.ts +0 -88
  19. package/src/clients/utility-client.ts +0 -156
  20. package/src/errors/adapter.ts +0 -238
  21. package/src/errors/classes.ts +0 -594
  22. package/src/errors/index.ts +0 -109
  23. package/src/file-stream.ts +0 -175
  24. package/src/index.ts +0 -121
  25. package/src/interpreter.ts +0 -168
  26. package/src/openai/index.ts +0 -465
  27. package/src/request-handler.ts +0 -184
  28. package/src/sandbox.ts +0 -1937
  29. package/src/security.ts +0 -119
  30. package/src/sse-parser.ts +0 -147
  31. package/src/storage-mount/credential-detection.ts +0 -41
  32. package/src/storage-mount/errors.ts +0 -51
  33. package/src/storage-mount/index.ts +0 -17
  34. package/src/storage-mount/provider-detection.ts +0 -93
  35. package/src/storage-mount/types.ts +0 -17
  36. package/src/version.ts +0 -6
  37. package/tests/base-client.test.ts +0 -582
  38. package/tests/command-client.test.ts +0 -444
  39. package/tests/file-client.test.ts +0 -831
  40. package/tests/file-stream.test.ts +0 -310
  41. package/tests/get-sandbox.test.ts +0 -172
  42. package/tests/git-client.test.ts +0 -455
  43. package/tests/openai-shell-editor.test.ts +0 -434
  44. package/tests/port-client.test.ts +0 -283
  45. package/tests/process-client.test.ts +0 -649
  46. package/tests/request-handler.test.ts +0 -292
  47. package/tests/sandbox.test.ts +0 -890
  48. package/tests/sse-parser.test.ts +0 -291
  49. package/tests/storage-mount/credential-detection.test.ts +0 -119
  50. package/tests/storage-mount/provider-detection.test.ts +0 -77
  51. package/tests/utility-client.test.ts +0 -339
  52. package/tests/version.test.ts +0 -16
  53. package/tests/wrangler.jsonc +0 -35
  54. package/tsconfig.json +0 -11
  55. package/tsdown.config.ts +0 -13
  56. package/vitest.config.ts +0 -31
@@ -1,339 +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
- // Cancel the stream first to properly terminate HTTP connections when breaking early
271
- try {
272
- await reader.cancel();
273
- } catch {
274
- // Ignore cancel errors (stream may already be closed)
275
- }
276
- reader.releaseLock();
277
- }
278
- }
279
-
280
- private async parseExecutionResult(
281
- line: string,
282
- callbacks: ExecutionCallbacks
283
- ) {
284
- if (!line.trim()) return;
285
-
286
- // Skip lines that don't start with "data: " (SSE format)
287
- if (!line.startsWith('data: ')) return;
288
-
289
- try {
290
- // Strip "data: " prefix and parse JSON
291
- const jsonData = line.substring(6); // "data: " is 6 characters
292
- const data = JSON.parse(jsonData) as StreamingExecutionData;
293
-
294
- switch (data.type) {
295
- case 'stdout':
296
- if (callbacks.onStdout && data.text) {
297
- await callbacks.onStdout({
298
- text: data.text,
299
- timestamp: data.timestamp || Date.now()
300
- });
301
- }
302
- break;
303
-
304
- case 'stderr':
305
- if (callbacks.onStderr && data.text) {
306
- await callbacks.onStderr({
307
- text: data.text,
308
- timestamp: data.timestamp || Date.now()
309
- });
310
- }
311
- break;
312
-
313
- case 'result':
314
- if (callbacks.onResult) {
315
- // Create a ResultImpl instance from the raw data
316
- const result = new ResultImpl(data);
317
- await callbacks.onResult(result);
318
- }
319
- break;
320
-
321
- case 'error':
322
- if (callbacks.onError) {
323
- await callbacks.onError({
324
- name: data.ename || 'Error',
325
- message: data.evalue || 'Unknown error',
326
- traceback: data.traceback || []
327
- });
328
- }
329
- break;
330
-
331
- case 'execution_complete':
332
- // Signal completion - callbacks can handle cleanup if needed
333
- break;
334
- }
335
- } catch (error) {
336
- this.logError('parseExecutionResult', error);
337
- }
338
- }
339
- }
@@ -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
- }