@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,356 +0,0 @@
1
- import type { Logger } from '@repo/shared';
2
- import { createNoOpLogger } from '@repo/shared';
3
- import { getHttpStatus } from '@repo/shared/errors';
4
- import type { ErrorResponse as NewErrorResponse } from '../errors';
5
- import { createErrorFromResponse, ErrorCode } from '../errors';
6
- import type { SandboxError } from '../errors/classes';
7
- import type { HttpClientOptions, ResponseHandler } from './types';
8
-
9
- // Container startup retry configuration
10
- const TIMEOUT_MS = 120_000; // 2 minutes total retry budget
11
- const MIN_TIME_FOR_RETRY_MS = 15_000; // Need at least 15s remaining to retry (allows for longer container startups)
12
-
13
- /**
14
- * Abstract base class providing common HTTP functionality for all domain clients
15
- */
16
- export abstract class BaseHttpClient {
17
- protected baseUrl: string;
18
- protected options: HttpClientOptions;
19
- protected logger: Logger;
20
-
21
- constructor(options: HttpClientOptions = {}) {
22
- this.options = options;
23
- this.logger = options.logger ?? createNoOpLogger();
24
- this.baseUrl = this.options.baseUrl!;
25
- }
26
-
27
- /**
28
- * Core HTTP request method with automatic retry for container startup delays
29
- * Retries both 503 (provisioning) and 500 (startup failure) errors when they're container-related
30
- */
31
- protected async doFetch(
32
- path: string,
33
- options?: RequestInit
34
- ): Promise<Response> {
35
- const startTime = Date.now();
36
- let attempt = 0;
37
-
38
- while (true) {
39
- const response = await this.executeFetch(path, options);
40
-
41
- // Check if this is a retryable container error (both 500 and 503)
42
- const shouldRetry = await this.isRetryableContainerError(response);
43
-
44
- if (shouldRetry) {
45
- const elapsed = Date.now() - startTime;
46
- const remaining = TIMEOUT_MS - elapsed;
47
-
48
- // Check if we have enough time for another attempt
49
- if (remaining > MIN_TIME_FOR_RETRY_MS) {
50
- // Exponential backoff with longer delays for container ops: 3s, 6s, 12s, 24s, 30s
51
- const delay = Math.min(3000 * 2 ** attempt, 30000);
52
-
53
- this.logger.info('Container not ready, retrying', {
54
- status: response.status,
55
- attempt: attempt + 1,
56
- delayMs: delay,
57
- remainingSec: Math.floor(remaining / 1000)
58
- });
59
-
60
- await new Promise((resolve) => setTimeout(resolve, delay));
61
- attempt++;
62
- continue;
63
- }
64
-
65
- // Timeout exhausted
66
- this.logger.error(
67
- 'Container failed to become ready',
68
- new Error(
69
- `Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1000)}s`
70
- )
71
- );
72
- return response;
73
- }
74
-
75
- // Not a retryable error or request succeeded
76
- return response;
77
- }
78
- }
79
-
80
- /**
81
- * Make a POST request with JSON body
82
- */
83
- protected async post<T>(
84
- endpoint: string,
85
- data: unknown,
86
- responseHandler?: ResponseHandler<T>
87
- ): Promise<T> {
88
- const response = await this.doFetch(endpoint, {
89
- method: 'POST',
90
- headers: {
91
- 'Content-Type': 'application/json'
92
- },
93
- body: JSON.stringify(data)
94
- });
95
-
96
- return this.handleResponse(response, responseHandler);
97
- }
98
-
99
- /**
100
- * Make a GET request
101
- */
102
- protected async get<T>(
103
- endpoint: string,
104
- responseHandler?: ResponseHandler<T>
105
- ): Promise<T> {
106
- const response = await this.doFetch(endpoint, {
107
- method: 'GET'
108
- });
109
-
110
- return this.handleResponse(response, responseHandler);
111
- }
112
-
113
- /**
114
- * Make a DELETE request
115
- */
116
- protected async delete<T>(
117
- endpoint: string,
118
- responseHandler?: ResponseHandler<T>
119
- ): Promise<T> {
120
- const response = await this.doFetch(endpoint, {
121
- method: 'DELETE'
122
- });
123
-
124
- return this.handleResponse(response, responseHandler);
125
- }
126
-
127
- /**
128
- * Handle HTTP response with error checking and parsing
129
- */
130
- protected async handleResponse<T>(
131
- response: Response,
132
- customHandler?: ResponseHandler<T>
133
- ): Promise<T> {
134
- if (!response.ok) {
135
- await this.handleErrorResponse(response);
136
- }
137
-
138
- if (customHandler) {
139
- return customHandler(response);
140
- }
141
-
142
- try {
143
- return await response.json();
144
- } catch (error) {
145
- // Handle malformed JSON responses gracefully
146
- const errorResponse: NewErrorResponse = {
147
- code: ErrorCode.INVALID_JSON_RESPONSE,
148
- message: `Invalid JSON response: ${
149
- error instanceof Error ? error.message : 'Unknown parsing error'
150
- }`,
151
- context: {},
152
- httpStatus: response.status,
153
- timestamp: new Date().toISOString()
154
- };
155
- throw createErrorFromResponse(errorResponse);
156
- }
157
- }
158
-
159
- /**
160
- * Handle error responses with consistent error throwing
161
- */
162
- protected async handleErrorResponse(response: Response): Promise<never> {
163
- let errorData: NewErrorResponse;
164
-
165
- try {
166
- errorData = await response.json();
167
- } catch {
168
- // Fallback if response isn't JSON or parsing fails
169
- errorData = {
170
- code: ErrorCode.INTERNAL_ERROR,
171
- message: `HTTP error! status: ${response.status}`,
172
- context: { statusText: response.statusText },
173
- httpStatus: response.status,
174
- timestamp: new Date().toISOString()
175
- };
176
- }
177
-
178
- // Convert ErrorResponse to appropriate Error class
179
- const error = createErrorFromResponse(errorData);
180
-
181
- // Call error callback if provided
182
- this.options.onError?.(errorData.message, undefined);
183
-
184
- throw error;
185
- }
186
-
187
- /**
188
- * Create a streaming response handler for Server-Sent Events
189
- */
190
- protected async handleStreamResponse(
191
- response: Response
192
- ): Promise<ReadableStream<Uint8Array>> {
193
- if (!response.ok) {
194
- await this.handleErrorResponse(response);
195
- }
196
-
197
- if (!response.body) {
198
- throw new Error('No response body for streaming');
199
- }
200
-
201
- return response.body;
202
- }
203
-
204
- /**
205
- * Utility method to log successful operations
206
- */
207
- protected logSuccess(operation: string, details?: string): void {
208
- this.logger.info(
209
- `${operation} completed successfully`,
210
- details ? { details } : undefined
211
- );
212
- }
213
-
214
- /**
215
- * Utility method to log errors intelligently
216
- * Only logs unexpected errors (5xx), not expected errors (4xx)
217
- *
218
- * - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
219
- * - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
220
- */
221
- protected logError(operation: string, error: unknown): void {
222
- // Check if it's a SandboxError with HTTP status
223
- if (error && typeof error === 'object' && 'httpStatus' in error) {
224
- const httpStatus = (error as SandboxError).httpStatus;
225
-
226
- // Only log server errors (5xx), not client errors (4xx)
227
- if (httpStatus >= 500) {
228
- this.logger.error(
229
- `Unexpected error in ${operation}`,
230
- error instanceof Error ? error : new Error(String(error)),
231
- { httpStatus }
232
- );
233
- }
234
- // 4xx errors are expected (validation, not found, etc.) - don't log
235
- } else {
236
- // Non-SandboxError (unexpected) - log it
237
- this.logger.error(
238
- `Error in ${operation}`,
239
- error instanceof Error ? error : new Error(String(error))
240
- );
241
- }
242
- }
243
-
244
- /**
245
- * Check if response indicates a retryable container error
246
- * Uses fail-safe strategy: only retry known transient errors
247
- *
248
- * TODO: This relies on string matching error messages, which is brittle.
249
- * Ideally, the container API should return structured errors with a
250
- * `retryable: boolean` field to avoid coupling to error message format.
251
- *
252
- * @param response - HTTP response to check
253
- * @returns true if error is retryable container error, false otherwise
254
- */
255
- private async isRetryableContainerError(
256
- response: Response
257
- ): Promise<boolean> {
258
- // Only consider 500 and 503 status codes
259
- if (response.status !== 500 && response.status !== 503) {
260
- return false;
261
- }
262
-
263
- try {
264
- const cloned = response.clone();
265
- const text = await cloned.text();
266
- const textLower = text.toLowerCase();
267
-
268
- // Step 1: Check for permanent errors (fail fast)
269
- const permanentErrors = [
270
- 'no such image', // Missing Docker image
271
- 'container already exists', // Name collision
272
- 'malformed containerinspect' // Docker API issue
273
- ];
274
-
275
- if (permanentErrors.some((err) => textLower.includes(err))) {
276
- this.logger.debug('Detected permanent error, not retrying', { text });
277
- return false; // Don't retry
278
- }
279
-
280
- // Step 2: Check for known transient errors (do retry)
281
- const transientErrors = [
282
- // Platform provisioning (503)
283
- 'no container instance available',
284
- 'currently provisioning',
285
-
286
- // Port mapping race conditions (500)
287
- 'container port not found',
288
- 'connection refused: container port',
289
-
290
- // Application startup delays (500)
291
- 'the container is not listening',
292
- 'failed to verify port',
293
- 'container did not start',
294
-
295
- // Network transients (500)
296
- 'network connection lost',
297
- 'container suddenly disconnected',
298
-
299
- // Monitor race conditions (500)
300
- 'monitor failed to find container',
301
-
302
- // General timeouts (500)
303
- 'timed out',
304
- 'timeout'
305
- ];
306
-
307
- const shouldRetry = transientErrors.some((err) =>
308
- textLower.includes(err)
309
- );
310
-
311
- if (!shouldRetry) {
312
- this.logger.debug('Unknown error pattern, not retrying', {
313
- status: response.status,
314
- text: text.substring(0, 200) // Log first 200 chars
315
- });
316
- }
317
-
318
- return shouldRetry;
319
- } catch (error) {
320
- this.logger.error(
321
- 'Error checking if response is retryable',
322
- error instanceof Error ? error : new Error(String(error))
323
- );
324
- // If we can't read response, don't retry (fail fast)
325
- return false;
326
- }
327
- }
328
-
329
- private async executeFetch(
330
- path: string,
331
- options?: RequestInit
332
- ): Promise<Response> {
333
- const url = this.options.stub
334
- ? `http://localhost:${this.options.port}${path}`
335
- : `${this.baseUrl}${path}`;
336
-
337
- try {
338
- if (this.options.stub) {
339
- return await this.options.stub.containerFetch(
340
- url,
341
- options || {},
342
- this.options.port
343
- );
344
- } else {
345
- return await fetch(url, options);
346
- }
347
- } catch (error) {
348
- this.logger.error(
349
- 'HTTP request error',
350
- error instanceof Error ? error : new Error(String(error)),
351
- { method: options?.method || 'GET', url }
352
- );
353
- throw error;
354
- }
355
- }
356
- }
@@ -1,133 +0,0 @@
1
- import type { ExecuteRequest } from '@repo/shared';
2
- import { BaseHttpClient } from './base-client';
3
- import type { BaseApiResponse } from './types';
4
-
5
- /**
6
- * Request interface for command execution
7
- */
8
- export type { ExecuteRequest };
9
-
10
- /**
11
- * Response interface for command execution
12
- */
13
- export interface ExecuteResponse extends BaseApiResponse {
14
- stdout: string;
15
- stderr: string;
16
- exitCode: number;
17
- command: string;
18
- }
19
-
20
- /**
21
- * Client for command execution operations
22
- */
23
- export class CommandClient extends BaseHttpClient {
24
- /**
25
- * Execute a command and return the complete result
26
- * @param command - The command to execute
27
- * @param sessionId - The session ID for this command execution
28
- * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
29
- * @param env - Optional environment variables for this command
30
- * @param cwd - Optional working directory for this command
31
- */
32
- async execute(
33
- command: string,
34
- sessionId: string,
35
- options?: {
36
- timeoutMs?: number;
37
- env?: Record<string, string>;
38
- cwd?: string;
39
- }
40
- ): Promise<ExecuteResponse> {
41
- try {
42
- const data: ExecuteRequest = {
43
- command,
44
- sessionId,
45
- ...(options?.timeoutMs !== undefined && {
46
- timeoutMs: options.timeoutMs
47
- }),
48
- ...(options?.env !== undefined && { env: options.env }),
49
- ...(options?.cwd !== undefined && { cwd: options.cwd })
50
- };
51
-
52
- const response = await this.post<ExecuteResponse>('/api/execute', data);
53
-
54
- this.logSuccess(
55
- 'Command executed',
56
- `${command}, Success: ${response.success}`
57
- );
58
-
59
- // Call the callback if provided
60
- this.options.onCommandComplete?.(
61
- response.success,
62
- response.exitCode,
63
- response.stdout,
64
- response.stderr,
65
- response.command
66
- );
67
-
68
- return response;
69
- } catch (error) {
70
- this.logError('execute', error);
71
-
72
- // Call error callback if provided
73
- this.options.onError?.(
74
- error instanceof Error ? error.message : String(error),
75
- command
76
- );
77
-
78
- throw error;
79
- }
80
- }
81
-
82
- /**
83
- * Execute a command and return a stream of events
84
- * @param command - The command to execute
85
- * @param sessionId - The session ID for this command execution
86
- * @param options - Optional per-command execution settings
87
- */
88
- async executeStream(
89
- command: string,
90
- sessionId: string,
91
- options?: {
92
- timeoutMs?: number;
93
- env?: Record<string, string>;
94
- cwd?: string;
95
- }
96
- ): Promise<ReadableStream<Uint8Array>> {
97
- try {
98
- const data = {
99
- command,
100
- sessionId,
101
- ...(options?.timeoutMs !== undefined && {
102
- timeoutMs: options.timeoutMs
103
- }),
104
- ...(options?.env !== undefined && { env: options.env }),
105
- ...(options?.cwd !== undefined && { cwd: options.cwd })
106
- };
107
-
108
- const response = await this.doFetch('/api/execute/stream', {
109
- method: 'POST',
110
- headers: {
111
- 'Content-Type': 'application/json'
112
- },
113
- body: JSON.stringify(data)
114
- });
115
-
116
- const stream = await this.handleStreamResponse(response);
117
-
118
- this.logSuccess('Command stream started', command);
119
-
120
- return stream;
121
- } catch (error) {
122
- this.logError('executeStream', error);
123
-
124
- // Call error callback if provided
125
- this.options.onError?.(
126
- error instanceof Error ? error.message : String(error),
127
- command
128
- );
129
-
130
- throw error;
131
- }
132
- }
133
- }