@cloudflare/sandbox 0.3.6 → 0.4.1

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 (120) hide show
  1. package/.turbo/turbo-build.log +44 -0
  2. package/CHANGELOG.md +6 -8
  3. package/Dockerfile +88 -18
  4. package/README.md +89 -824
  5. package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
  6. package/dist/chunk-BCJ7SF3Q.js.map +1 -0
  7. package/dist/chunk-BFVUNTP4.js +104 -0
  8. package/dist/chunk-BFVUNTP4.js.map +1 -0
  9. package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
  10. package/dist/chunk-EKSWCBCA.js.map +1 -0
  11. package/dist/chunk-HGF554LH.js +2236 -0
  12. package/dist/chunk-HGF554LH.js.map +1 -0
  13. package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
  14. package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
  15. package/dist/file-stream.d.ts +16 -38
  16. package/dist/file-stream.js +1 -2
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.js +35 -39
  19. package/dist/index.js.map +1 -1
  20. package/dist/interpreter.d.ts +3 -3
  21. package/dist/interpreter.js +2 -2
  22. package/dist/request-handler.d.ts +4 -3
  23. package/dist/request-handler.js +4 -7
  24. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  25. package/dist/sandbox.d.ts +3 -3
  26. package/dist/sandbox.js +4 -7
  27. package/dist/security.d.ts +4 -3
  28. package/dist/security.js +3 -3
  29. package/dist/sse-parser.js +1 -1
  30. package/package.json +11 -5
  31. package/src/clients/base-client.ts +280 -0
  32. package/src/clients/command-client.ts +115 -0
  33. package/src/clients/file-client.ts +269 -0
  34. package/src/clients/git-client.ts +92 -0
  35. package/src/clients/index.ts +63 -0
  36. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  37. package/src/clients/port-client.ts +105 -0
  38. package/src/clients/process-client.ts +177 -0
  39. package/src/clients/sandbox-client.ts +41 -0
  40. package/src/clients/types.ts +84 -0
  41. package/src/clients/utility-client.ts +94 -0
  42. package/src/errors/adapter.ts +180 -0
  43. package/src/errors/classes.ts +469 -0
  44. package/src/errors/index.ts +105 -0
  45. package/src/file-stream.ts +119 -117
  46. package/src/index.ts +81 -69
  47. package/src/interpreter.ts +17 -8
  48. package/src/request-handler.ts +69 -43
  49. package/src/sandbox.ts +694 -533
  50. package/src/security.ts +14 -23
  51. package/src/sse-parser.ts +4 -8
  52. package/startup.sh +3 -0
  53. package/tests/base-client.test.ts +328 -0
  54. package/tests/command-client.test.ts +407 -0
  55. package/tests/file-client.test.ts +643 -0
  56. package/tests/file-stream.test.ts +306 -0
  57. package/tests/git-client.test.ts +328 -0
  58. package/tests/port-client.test.ts +301 -0
  59. package/tests/process-client.test.ts +658 -0
  60. package/tests/sandbox.test.ts +465 -0
  61. package/tests/sse-parser.test.ts +290 -0
  62. package/tests/utility-client.test.ts +266 -0
  63. package/tests/wrangler.jsonc +35 -0
  64. package/tsconfig.json +9 -1
  65. package/vitest.config.ts +31 -0
  66. package/container_src/bun.lock +0 -76
  67. package/container_src/circuit-breaker.ts +0 -121
  68. package/container_src/control-process.ts +0 -784
  69. package/container_src/handler/exec.ts +0 -185
  70. package/container_src/handler/file.ts +0 -457
  71. package/container_src/handler/git.ts +0 -130
  72. package/container_src/handler/ports.ts +0 -314
  73. package/container_src/handler/process.ts +0 -568
  74. package/container_src/handler/session.ts +0 -92
  75. package/container_src/index.ts +0 -601
  76. package/container_src/interpreter-service.ts +0 -276
  77. package/container_src/isolation.ts +0 -1213
  78. package/container_src/mime-processor.ts +0 -255
  79. package/container_src/package.json +0 -18
  80. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  81. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  82. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  83. package/container_src/runtime/process-pool.ts +0 -464
  84. package/container_src/shell-escape.ts +0 -42
  85. package/container_src/startup.sh +0 -11
  86. package/container_src/types.ts +0 -131
  87. package/dist/chunk-32UDXUPC.js +0 -671
  88. package/dist/chunk-32UDXUPC.js.map +0 -1
  89. package/dist/chunk-5DILEXGY.js +0 -85
  90. package/dist/chunk-5DILEXGY.js.map +0 -1
  91. package/dist/chunk-D3U63BZP.js +0 -240
  92. package/dist/chunk-D3U63BZP.js.map +0 -1
  93. package/dist/chunk-FXYPFGOZ.js +0 -129
  94. package/dist/chunk-FXYPFGOZ.js.map +0 -1
  95. package/dist/chunk-JTKON2SH.js.map +0 -1
  96. package/dist/chunk-NNGBXDMY.js.map +0 -1
  97. package/dist/chunk-SQLJNZ3K.js +0 -674
  98. package/dist/chunk-SQLJNZ3K.js.map +0 -1
  99. package/dist/chunk-W7TVRPBG.js +0 -108
  100. package/dist/chunk-W7TVRPBG.js.map +0 -1
  101. package/dist/client-B3RUab0s.d.ts +0 -225
  102. package/dist/client.d.ts +0 -4
  103. package/dist/client.js +0 -7
  104. package/dist/client.js.map +0 -1
  105. package/dist/errors.d.ts +0 -95
  106. package/dist/errors.js +0 -27
  107. package/dist/errors.js.map +0 -1
  108. package/dist/interpreter-client.d.ts +0 -4
  109. package/dist/interpreter-client.js +0 -9
  110. package/dist/interpreter-client.js.map +0 -1
  111. package/dist/interpreter-types.d.ts +0 -259
  112. package/dist/interpreter-types.js +0 -9
  113. package/dist/interpreter-types.js.map +0 -1
  114. package/dist/types.d.ts +0 -453
  115. package/dist/types.js +0 -45
  116. package/dist/types.js.map +0 -1
  117. package/src/client.ts +0 -1048
  118. package/src/errors.ts +0 -219
  119. package/src/interpreter-types.ts +0 -390
  120. package/src/types.ts +0 -571
@@ -1,338 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- IPython-based executor for clean Python code execution.
4
- Uses IPython's built-in display formatter for rich outputs.
5
- """
6
-
7
- import sys
8
- import json
9
- import traceback
10
- import base64
11
- from io import BytesIO
12
-
13
- try:
14
- from IPython.core.interactiveshell import InteractiveShell
15
- from IPython.utils.capture import capture_output
16
- except ImportError as e:
17
- print(json.dumps({
18
- "error": f"IPython import failed: {str(e)}",
19
- "python_path": sys.path
20
- }), flush=True)
21
- sys.exit(1)
22
-
23
- # Create IPython shell instance
24
- shell = InteractiveShell.instance()
25
-
26
- # Configure for non-interactive use
27
- shell.colors = 'NoColor'
28
- shell.quiet = True
29
- shell.ast_node_interactivity = 'last_expr' # Only show the last expression
30
-
31
- # Configure matplotlib backend for non-interactive use
32
- import os
33
- os.environ['MPLBACKEND'] = 'Agg'
34
-
35
- # Send ready signal after IPython is initialized
36
- print(json.dumps({"status": "ready", "version": "2.0.0"}), flush=True)
37
-
38
- def capture_matplotlib_figures():
39
- """Capture any matplotlib figures that exist"""
40
- figures = []
41
- try:
42
- # Only import if matplotlib is already loaded
43
- if 'matplotlib.pyplot' in sys.modules:
44
- import matplotlib.pyplot as plt
45
- # Get all figure numbers
46
- fig_nums = plt.get_fignums()
47
- for fig_num in fig_nums:
48
- fig = plt.figure(fig_num)
49
- # Save to BytesIO buffer
50
- buf = BytesIO()
51
- fig.savefig(buf, format='png', bbox_inches='tight')
52
- buf.seek(0)
53
- # Encode to base64
54
- img_data = base64.b64encode(buf.read()).decode('utf-8')
55
- figures.append(img_data)
56
- buf.close()
57
- # Close all figures to prevent memory leaks
58
- plt.close('all')
59
- except Exception as e:
60
- pass # Silently fail if matplotlib not available
61
- return figures
62
-
63
- def execute_code(code):
64
- """Execute code using IPython's built-in display system."""
65
- result = {
66
- 'stdout': '',
67
- 'stderr': '',
68
- 'error': None,
69
- 'outputs': []
70
- }
71
-
72
- try:
73
- # Use capture_output to capture display() calls AND stdout/stderr
74
- with capture_output() as captured:
75
- # Execute code using IPython
76
- exec_result = shell.run_cell(code, store_history=False, silent=False)
77
-
78
- # Get captured stdout/stderr from capture_output
79
- result['stdout'] = captured.stdout
80
- result['stderr'] = captured.stderr
81
-
82
- # Handle execution errors
83
- if exec_result.error_in_exec:
84
- error = exec_result.error_in_exec
85
- result['error'] = {
86
- 'type': error.__class__.__name__,
87
- 'message': str(error),
88
- 'traceback': '\n'.join(traceback.format_tb(error.__traceback__))
89
- }
90
-
91
- # Process display() outputs from capture_output
92
- for output in captured.outputs:
93
- # Check the structure of the output object
94
- if hasattr(output, 'data'):
95
- data = output.data
96
- metadata = getattr(output, 'metadata', {})
97
-
98
- # Process different MIME types
99
- if 'image/png' in data:
100
- result['outputs'].append({
101
- 'type': 'image',
102
- 'data': data['image/png'],
103
- 'metadata': metadata.get('image/png', {})
104
- })
105
-
106
- if 'image/jpeg' in data:
107
- result['outputs'].append({
108
- 'type': 'jpeg',
109
- 'data': data['image/jpeg'],
110
- 'metadata': metadata.get('image/jpeg', {})
111
- })
112
-
113
- if 'image/svg+xml' in data:
114
- result['outputs'].append({
115
- 'type': 'svg',
116
- 'data': data['image/svg+xml'],
117
- 'metadata': metadata.get('image/svg+xml', {})
118
- })
119
-
120
- if 'text/html' in data:
121
- result['outputs'].append({
122
- 'type': 'html',
123
- 'data': data['text/html'],
124
- 'metadata': metadata.get('text/html', {})
125
- })
126
-
127
- if 'application/json' in data:
128
- result['outputs'].append({
129
- 'type': 'json',
130
- 'data': json.dumps(data['application/json']),
131
- 'metadata': metadata.get('application/json', {})
132
- })
133
-
134
- if 'text/latex' in data:
135
- result['outputs'].append({
136
- 'type': 'latex',
137
- 'data': data['text/latex'],
138
- 'metadata': metadata.get('text/latex', {})
139
- })
140
-
141
- if 'text/markdown' in data:
142
- result['outputs'].append({
143
- 'type': 'markdown',
144
- 'data': data['text/markdown'],
145
- 'metadata': metadata.get('text/markdown', {})
146
- })
147
-
148
- if 'application/javascript' in data:
149
- result['outputs'].append({
150
- 'type': 'javascript',
151
- 'data': data['application/javascript'],
152
- 'metadata': metadata.get('application/javascript', {})
153
- })
154
-
155
- # Include plain text if nothing else was captured from this output
156
- if 'text/plain' in data and len(data) == 1:
157
- result['outputs'].append({
158
- 'type': 'text',
159
- 'data': data['text/plain'],
160
- 'metadata': metadata.get('text/plain', {})
161
- })
162
-
163
- # Capture any matplotlib figures that were created
164
- # This handles plt.show() calls
165
- matplotlib_figures = capture_matplotlib_figures()
166
- for fig_data in matplotlib_figures:
167
- result['outputs'].append({
168
- 'type': 'image',
169
- 'data': fig_data,
170
- 'metadata': {}
171
- })
172
-
173
- # Also check if the last expression produced a result
174
- # Always process the last expression result (even if there were display outputs)
175
- if exec_result.result is not None:
176
- # Check if result is a dict or list that should be rendered as JSON
177
- if isinstance(exec_result.result, (dict, list)):
178
- result['outputs'].append({
179
- 'type': 'json',
180
- 'data': json.dumps(exec_result.result),
181
- 'metadata': {}
182
- })
183
- else:
184
- # Use IPython's display formatter to get all available representations
185
- formatted_dict, metadata = shell.display_formatter.format(exec_result.result)
186
-
187
- # Process all available MIME types in order of preference
188
- output_added = False
189
-
190
- # Images
191
- if 'image/png' in formatted_dict:
192
- result['outputs'].append({
193
- 'type': 'image',
194
- 'data': formatted_dict['image/png'],
195
- 'metadata': metadata.get('image/png', {})
196
- })
197
- output_added = True
198
-
199
- if 'image/jpeg' in formatted_dict:
200
- result['outputs'].append({
201
- 'type': 'jpeg',
202
- 'data': formatted_dict['image/jpeg'],
203
- 'metadata': metadata.get('image/jpeg', {})
204
- })
205
- output_added = True
206
-
207
- if 'image/svg+xml' in formatted_dict:
208
- result['outputs'].append({
209
- 'type': 'svg',
210
- 'data': formatted_dict['image/svg+xml'],
211
- 'metadata': metadata.get('image/svg+xml', {})
212
- })
213
- output_added = True
214
-
215
- # HTML (pandas DataFrames often use this)
216
- if 'text/html' in formatted_dict:
217
- result['outputs'].append({
218
- 'type': 'html',
219
- 'data': formatted_dict['text/html'],
220
- 'metadata': metadata.get('text/html', {})
221
- })
222
- # Don't set output_added for HTML - we want text too for DataFrames
223
-
224
- # JSON data
225
- if 'application/json' in formatted_dict:
226
- result['outputs'].append({
227
- 'type': 'json',
228
- 'data': json.dumps(formatted_dict['application/json']),
229
- 'metadata': metadata.get('application/json', {})
230
- })
231
- output_added = True
232
-
233
- # LaTeX
234
- if 'text/latex' in formatted_dict:
235
- result['outputs'].append({
236
- 'type': 'latex',
237
- 'data': formatted_dict['text/latex'],
238
- 'metadata': metadata.get('text/latex', {})
239
- })
240
- output_added = True
241
-
242
- # Markdown
243
- if 'text/markdown' in formatted_dict:
244
- result['outputs'].append({
245
- 'type': 'markdown',
246
- 'data': formatted_dict['text/markdown'],
247
- 'metadata': metadata.get('text/markdown', {})
248
- })
249
- output_added = True
250
-
251
- # JavaScript
252
- if 'application/javascript' in formatted_dict:
253
- result['outputs'].append({
254
- 'type': 'javascript',
255
- 'data': formatted_dict['application/javascript'],
256
- 'metadata': metadata.get('application/javascript', {})
257
- })
258
- output_added = True
259
-
260
- # Plain text - always include if no other output or if HTML present (for DataFrames)
261
- if 'text/plain' in formatted_dict:
262
- # Include plain text if: no other output was added, OR if we have HTML (DataFrames)
263
- has_html = any(o['type'] == 'html' for o in result.get('outputs', []))
264
- if not output_added or has_html:
265
- result['outputs'].append({
266
- 'type': 'text',
267
- 'data': formatted_dict['text/plain'],
268
- 'metadata': metadata.get('text/plain', {})
269
- })
270
-
271
- except Exception as e:
272
- # Handle protocol-level errors
273
- result['error'] = {
274
- 'type': type(e).__name__,
275
- 'message': str(e),
276
- 'traceback': traceback.format_exc()
277
- }
278
-
279
- return result
280
-
281
- def main():
282
- """Main loop - read JSON requests, execute, return JSON responses."""
283
-
284
- while True:
285
- try:
286
- line = sys.stdin.readline()
287
- if not line:
288
- break
289
-
290
- # Parse request
291
- request = json.loads(line.strip())
292
-
293
- # Execute code
294
- result = execute_code(request.get('code', ''))
295
-
296
- # Add execution ID to response
297
- result['executionId'] = request.get('executionId')
298
- result['success'] = result['error'] is None
299
-
300
- # Send response
301
- print(json.dumps(result), flush=True)
302
-
303
- except json.JSONDecodeError as e:
304
- # Invalid JSON
305
- error_response = {
306
- 'error': {
307
- 'type': 'ProtocolError',
308
- 'message': f'Invalid JSON: {str(e)}'
309
- },
310
- 'status': 'error'
311
- }
312
- print(json.dumps(error_response), flush=True)
313
-
314
- except KeyboardInterrupt:
315
- break
316
-
317
- except Exception as e:
318
- # Unexpected error
319
- error_response = {
320
- 'error': {
321
- 'type': 'InternalError',
322
- 'message': f'Unexpected error: {str(e)}',
323
- 'traceback': traceback.format_exc()
324
- },
325
- 'status': 'error'
326
- }
327
- print(json.dumps(error_response), flush=True)
328
-
329
- if __name__ == '__main__':
330
- try:
331
- main()
332
- except Exception as e:
333
- # Send error as JSON to stdout so process pool can see it
334
- print(json.dumps({
335
- "error": f"Failed to start: {str(e)}",
336
- "traceback": traceback.format_exc()
337
- }), flush=True)
338
- sys.exit(1)
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import * as readline from 'node:readline';
4
- import * as util from 'node:util';
5
- import * as vm from 'node:vm';
6
- import { transformSync } from 'esbuild';
7
- import type { RichOutput } from '../../process-pool';
8
-
9
- const rl = readline.createInterface({
10
- input: process.stdin,
11
- output: process.stdout,
12
- terminal: false
13
- });
14
-
15
- const sandbox = {
16
- console: console,
17
- process: process,
18
- require: require,
19
- Buffer: Buffer,
20
- setTimeout: setTimeout,
21
- setInterval: setInterval,
22
- clearTimeout: clearTimeout,
23
- clearInterval: clearInterval,
24
- setImmediate: setImmediate,
25
- clearImmediate: clearImmediate,
26
- global: global,
27
- __dirname: __dirname,
28
- __filename: __filename
29
- };
30
-
31
- const context = vm.createContext(sandbox);
32
-
33
- console.log(JSON.stringify({ status: "ready" }));
34
-
35
- rl.on('line', async (line: string) => {
36
- try {
37
- const request = JSON.parse(line);
38
- const { code, executionId } = request;
39
-
40
- const originalStdoutWrite = process.stdout.write;
41
- const originalStderrWrite = process.stderr.write;
42
-
43
- let stdout = '';
44
- let stderr = '';
45
-
46
- (process.stdout.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => {
47
- stdout += chunk.toString();
48
- if (callback) callback();
49
- return true;
50
- };
51
-
52
- (process.stderr.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => {
53
- stderr += chunk.toString();
54
- if (callback) callback();
55
- return true;
56
- };
57
-
58
- let result: unknown;
59
- let success = true;
60
-
61
- try {
62
- const transpileResult = transformSync(code, {
63
- loader: 'ts',
64
- target: 'es2020',
65
- format: 'cjs',
66
- sourcemap: false,
67
- treeShaking: false,
68
- minify: false,
69
- });
70
-
71
- const jsCode = transpileResult.code;
72
- result = vm.runInContext(jsCode, context, {
73
- filename: `<execution-${executionId}>`,
74
- timeout: 30000
75
- });
76
-
77
- } catch (error: unknown) {
78
- const err = error as Error;
79
- if (err.message?.includes('Transform failed')) {
80
- stderr += `TypeScript compilation error: ${err.message}\n`;
81
- } else {
82
- stderr += err.stack || err.toString();
83
- }
84
- success = false;
85
- } finally {
86
- process.stdout.write = originalStdoutWrite;
87
- process.stderr.write = originalStderrWrite;
88
- }
89
-
90
- const outputs: RichOutput[] = [];
91
-
92
- if (result !== undefined) {
93
- if (typeof result === 'object' && result !== null) {
94
- outputs.push({
95
- type: 'json',
96
- data: JSON.stringify(result, null, 2),
97
- metadata: {}
98
- });
99
- } else {
100
- outputs.push({
101
- type: 'text',
102
- data: util.inspect(result, { showHidden: false, depth: null, colors: false }),
103
- metadata: {}
104
- });
105
- }
106
- }
107
-
108
- const response = {
109
- stdout,
110
- stderr,
111
- success,
112
- executionId,
113
- outputs
114
- };
115
-
116
- console.log(JSON.stringify(response));
117
-
118
- } catch (error: unknown) {
119
- const err = error as Error;
120
- console.log(JSON.stringify({
121
- stdout: '',
122
- stderr: `Error processing request: ${err.message}`,
123
- success: false,
124
- executionId: 'unknown',
125
- outputs: []
126
- }));
127
- }
128
- });
129
-
130
- process.on('SIGTERM', () => {
131
- rl.close();
132
- process.exit(0);
133
- });
134
-
135
- process.on('SIGINT', () => {
136
- rl.close();
137
- process.exit(0);
138
- });