@liquidmetal-ai/precip 1.0.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 (78) hide show
  1. package/.prettierrc +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/eslint.config.mjs +28 -0
  4. package/package.json +53 -0
  5. package/src/engine/agent.ts +478 -0
  6. package/src/engine/llm-provider.test.ts +275 -0
  7. package/src/engine/llm-provider.ts +330 -0
  8. package/src/engine/stream-parser.ts +170 -0
  9. package/src/index.ts +142 -0
  10. package/src/mounts/mount-manager.test.ts +516 -0
  11. package/src/mounts/mount-manager.ts +327 -0
  12. package/src/mounts/mount-registry.ts +196 -0
  13. package/src/mounts/zod-to-string.test.ts +154 -0
  14. package/src/mounts/zod-to-string.ts +213 -0
  15. package/src/presets/agent-tools.ts +57 -0
  16. package/src/presets/index.ts +5 -0
  17. package/src/sandbox/README.md +1321 -0
  18. package/src/sandbox/bridges/README.md +571 -0
  19. package/src/sandbox/bridges/actor.test.ts +229 -0
  20. package/src/sandbox/bridges/actor.ts +195 -0
  21. package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
  22. package/src/sandbox/bridges/bucket.test.ts +300 -0
  23. package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
  24. package/src/sandbox/bridges/console-multiple.test.ts +187 -0
  25. package/src/sandbox/bridges/console.test.ts +157 -0
  26. package/src/sandbox/bridges/console.ts +122 -0
  27. package/src/sandbox/bridges/fetch.ts +93 -0
  28. package/src/sandbox/bridges/index.ts +78 -0
  29. package/src/sandbox/bridges/readable-stream.ts +323 -0
  30. package/src/sandbox/bridges/response.test.ts +154 -0
  31. package/src/sandbox/bridges/response.ts +123 -0
  32. package/src/sandbox/bridges/review-fixes.test.ts +331 -0
  33. package/src/sandbox/bridges/search.test.ts +475 -0
  34. package/src/sandbox/bridges/search.ts +264 -0
  35. package/src/sandbox/bridges/shared/body-methods.ts +93 -0
  36. package/src/sandbox/bridges/shared/cleanup.ts +112 -0
  37. package/src/sandbox/bridges/shared/convert.ts +76 -0
  38. package/src/sandbox/bridges/shared/headers.ts +181 -0
  39. package/src/sandbox/bridges/shared/index.ts +36 -0
  40. package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
  41. package/src/sandbox/bridges/shared/path-parser.ts +109 -0
  42. package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
  43. package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
  44. package/src/sandbox/bridges/shared/response-object.ts +280 -0
  45. package/src/sandbox/bridges/shared/result-builder.ts +130 -0
  46. package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
  47. package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
  48. package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
  49. package/src/sandbox/bridges/storage.ts +421 -0
  50. package/src/sandbox/bridges/text-decoder.ts +190 -0
  51. package/src/sandbox/bridges/text-encoder.ts +102 -0
  52. package/src/sandbox/bridges/types.ts +39 -0
  53. package/src/sandbox/bridges/utils.ts +123 -0
  54. package/src/sandbox/index.ts +6 -0
  55. package/src/sandbox/quickjs-wasm.d.ts +9 -0
  56. package/src/sandbox/sandbox.test.ts +191 -0
  57. package/src/sandbox/sandbox.ts +831 -0
  58. package/src/sandbox/test-helper.ts +43 -0
  59. package/src/sandbox/test-mocks.ts +154 -0
  60. package/src/sandbox/user-stream.test.ts +77 -0
  61. package/src/skills/frontmatter.test.ts +305 -0
  62. package/src/skills/frontmatter.ts +200 -0
  63. package/src/skills/index.ts +9 -0
  64. package/src/skills/skills-loader.test.ts +237 -0
  65. package/src/skills/skills-loader.ts +200 -0
  66. package/src/tools/actor-storage-tools.ts +250 -0
  67. package/src/tools/code-tools.test.ts +199 -0
  68. package/src/tools/code-tools.ts +444 -0
  69. package/src/tools/file-tools.ts +206 -0
  70. package/src/tools/registry.ts +125 -0
  71. package/src/tools/script-tools.ts +145 -0
  72. package/src/tools/smartbucket-tools.ts +203 -0
  73. package/src/tools/sql-tools.ts +213 -0
  74. package/src/tools/tool-factory.ts +119 -0
  75. package/src/types.ts +512 -0
  76. package/tsconfig.eslint.json +5 -0
  77. package/tsconfig.json +15 -0
  78. package/vitest.config.ts +33 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * ReadableStream bridge - Bridges runtime's ReadableStream for response.body
3
+ */
4
+
5
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
6
+ import type { BridgeContext } from './types.js';
7
+ import { unwrapResult } from './shared/index.js';
8
+
9
+ // Internal symbol name for cached Uint8Array wrapper on context global
10
+ const UINT8_WRAPPER_CACHE_KEY = '__cachedUint8ArrayWrapper';
11
+
12
+ // Minimum chunk size before passing to QuickJS (16KB)
13
+ // This reduces boundary crossing overhead for streams with many small chunks
14
+ const MIN_CHUNK_SIZE = 16 * 1024;
15
+
16
+ /**
17
+ * Get or create a cached Uint8Array wrapper function for the context.
18
+ * This avoids the expensive evalCode() call on every read().
19
+ *
20
+ * The cached wrapper is stored on the context's global object, so it's
21
+ * automatically cleaned up when the context is disposed.
22
+ */
23
+ function getUint8ArrayWrapper(ctx: BridgeContext): QuickJSHandle {
24
+ const { context } = ctx;
25
+
26
+ // Check if we already cached the wrapper on the global
27
+ const cachedHandle = context.getProp(context.global, UINT8_WRAPPER_CACHE_KEY);
28
+ const cachedType = context.typeof(cachedHandle);
29
+
30
+ if (cachedType === 'function') {
31
+ // Already cached - return it (caller must dispose)
32
+ return cachedHandle;
33
+ }
34
+
35
+ // Not cached yet - dispose the undefined handle we got
36
+ cachedHandle.dispose();
37
+
38
+ // Create the wrapper function once - this evalCode is amortized over all reads
39
+ const wrapper = unwrapResult(
40
+ context.evalCode('(function(ab) { return new Uint8Array(ab); })'),
41
+ context
42
+ );
43
+
44
+ // Store on global for future calls — non-enumerable to hide from sandbox code
45
+ context.setProp(context.global, UINT8_WRAPPER_CACHE_KEY, wrapper);
46
+ {
47
+ const hideResult = context.evalCode(`
48
+ try {
49
+ Object.defineProperty(globalThis, '${UINT8_WRAPPER_CACHE_KEY}', {
50
+ enumerable: false, writable: false, configurable: false
51
+ });
52
+ } catch(e) {}
53
+ undefined;
54
+ `);
55
+ if (hideResult.error) {
56
+ hideResult.error.dispose();
57
+ } else {
58
+ (hideResult as { value: QuickJSHandle }).value.dispose();
59
+ }
60
+ }
61
+
62
+ // Return the handle - caller must dispose their reference
63
+ return wrapper;
64
+ }
65
+
66
+ /**
67
+ * Coalescing reader that buffers small chunks before passing to QuickJS.
68
+ * Reduces boundary crossing overhead for streams with many small chunks.
69
+ */
70
+ class CoalescingReader {
71
+ private buffer: Uint8Array[] = [];
72
+ private bufferSize = 0;
73
+ private done = false;
74
+
75
+ constructor(
76
+ private hostReader: ReadableStreamDefaultReader<Uint8Array>,
77
+ private minChunkSize: number = MIN_CHUNK_SIZE
78
+ ) {}
79
+
80
+ /**
81
+ * Read and coalesce chunks until we have at least minChunkSize bytes or stream ends
82
+ */
83
+ async read(): Promise<ReadableStreamReadResult<Uint8Array>> {
84
+ // If we already know the stream is done, return done
85
+ if (this.done && this.bufferSize === 0) {
86
+ return { done: true, value: undefined };
87
+ }
88
+
89
+ try {
90
+ // Keep reading until we have enough data or stream ends
91
+ while (!this.done && this.bufferSize < this.minChunkSize) {
92
+ const result = await this.hostReader.read();
93
+
94
+ if (result.done) {
95
+ this.done = true;
96
+ break;
97
+ }
98
+
99
+ if (result.value && result.value.length > 0) {
100
+ this.buffer.push(result.value);
101
+ this.bufferSize += result.value.length;
102
+ }
103
+ }
104
+
105
+ // If we have no data and stream is done, return done
106
+ if (this.bufferSize === 0) {
107
+ return { done: true, value: undefined };
108
+ }
109
+
110
+ // Coalesce buffered chunks into a single Uint8Array
111
+ let value: Uint8Array;
112
+ if (this.buffer.length === 1) {
113
+ // Optimization: no copy needed for single chunk
114
+ value = this.buffer[0]!;
115
+ } else {
116
+ // Merge multiple chunks
117
+ value = new Uint8Array(this.bufferSize);
118
+ let offset = 0;
119
+ for (const chunk of this.buffer) {
120
+ value.set(chunk, offset);
121
+ offset += chunk.length;
122
+ }
123
+ }
124
+
125
+ // Clear buffer
126
+ this.buffer = [];
127
+ this.bufferSize = 0;
128
+
129
+ return { done: false, value };
130
+ } catch (e) {
131
+ // Reset state on error to avoid returning stale data on retry
132
+ this.buffer = [];
133
+ this.bufferSize = 0;
134
+ this.done = true;
135
+ throw e;
136
+ }
137
+ }
138
+
139
+ releaseLock(): void {
140
+ this.hostReader.releaseLock();
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Create a bridged ReadableStreamReader object
146
+ */
147
+ export function createReaderObject(
148
+ ctx: BridgeContext,
149
+ hostReader: ReadableStreamDefaultReader<Uint8Array>
150
+ ): QuickJSHandle {
151
+ const { context, runtime, tracker } = ctx;
152
+ const readerHandle = context.newObject();
153
+
154
+ // Wrap in coalescing reader to reduce boundary crossings
155
+ const coalescingReader = new CoalescingReader(hostReader);
156
+
157
+ // Create read() method
158
+ {
159
+ using readMethod = context.newFunction('read', () => {
160
+ const deferred = context.newPromise();
161
+ tracker.deferredPromises.add(deferred);
162
+
163
+ const hostPromise = coalescingReader
164
+ .read()
165
+ .then((result: ReadableStreamReadResult<Uint8Array>) => {
166
+ try {
167
+ // Create result object: { done: boolean, value?: Uint8Array }
168
+ const resultHandle = context.newObject();
169
+
170
+ context.setProp(resultHandle, 'done', result.done ? context.true : context.false);
171
+
172
+ if (result.value) {
173
+ // Create a Uint8Array in QuickJS from the ArrayBuffer
174
+ // Use slice to handle Uint8Array views into larger ArrayBuffers (e.g. Node.js Buffer)
175
+ const buf =
176
+ result.value.byteOffset === 0 &&
177
+ result.value.byteLength === result.value.buffer.byteLength
178
+ ? result.value.buffer
179
+ : result.value.buffer.slice(
180
+ result.value.byteOffset,
181
+ result.value.byteOffset + result.value.byteLength
182
+ );
183
+ using arrayBufferHandle = context.newArrayBuffer(buf);
184
+
185
+ // Use cached wrapper function instead of evalCode on every read
186
+ using wrapperFn = getUint8ArrayWrapper(ctx);
187
+
188
+ using uint8ArrayHandle = unwrapResult(
189
+ context.callFunction(wrapperFn, context.undefined, arrayBufferHandle),
190
+ context
191
+ );
192
+ context.setProp(resultHandle, 'value', uint8ArrayHandle);
193
+ }
194
+
195
+ deferred.resolve(resultHandle);
196
+ resultHandle.dispose();
197
+ runtime.executePendingJobs();
198
+ } catch (e) {
199
+ using errHandle = context.newString(e instanceof Error ? e.message : String(e));
200
+ deferred.reject(errHandle);
201
+ runtime.executePendingJobs();
202
+ }
203
+ })
204
+ .catch((err: Error) => {
205
+ try {
206
+ using errHandle = context.newString(err.message || String(err));
207
+ deferred.reject(errHandle);
208
+ runtime.executePendingJobs();
209
+ } catch {
210
+ // Context may have been disposed
211
+ }
212
+ })
213
+ .finally(() => {
214
+ tracker.pendingPromises.delete(hostPromise);
215
+ tracker.deferredPromises.delete(deferred);
216
+ });
217
+
218
+ // Track for cleanup and race optimization
219
+ tracker.pendingPromises.add(hostPromise);
220
+ tracker.notifyNewPromise();
221
+
222
+ return deferred.handle;
223
+ });
224
+ context.setProp(readerHandle, 'read', readMethod);
225
+ }
226
+
227
+ // Create releaseLock() method
228
+ {
229
+ using releaseLockMethod = context.newFunction('releaseLock', () => {
230
+ try {
231
+ coalescingReader.releaseLock();
232
+ return context.undefined;
233
+ } catch (e) {
234
+ return context.newError(e instanceof Error ? e.message : String(e));
235
+ }
236
+ });
237
+ context.setProp(readerHandle, 'releaseLock', releaseLockMethod);
238
+ }
239
+
240
+ return readerHandle;
241
+ }
242
+
243
+ /**
244
+ * Create a bridged ReadableStream object
245
+ */
246
+ export function createStreamObject(
247
+ ctx: BridgeContext,
248
+ hostStream: ReadableStream,
249
+ streamId: number,
250
+ hostReaders: Map<number, ReadableStreamDefaultReader>,
251
+ readerIdCounter: { value: number }
252
+ ): QuickJSHandle {
253
+ const { context } = ctx;
254
+ const streamHandle = context.newObject();
255
+
256
+ // Create getReader() method
257
+ {
258
+ using getReaderMethod = context.newFunction('getReader', () => {
259
+ try {
260
+ const hostReader = hostStream.getReader();
261
+ const readerId = ++readerIdCounter.value;
262
+ hostReaders.set(readerId, hostReader);
263
+
264
+ return createReaderObject(ctx, hostReader);
265
+ } catch (e) {
266
+ return context.newError(e instanceof Error ? e.message : String(e));
267
+ }
268
+ });
269
+ context.setProp(streamHandle, 'getReader', getReaderMethod);
270
+ }
271
+
272
+ // Create a host function to get the locked state dynamically
273
+ {
274
+ using getLockedFn = context.newFunction('__getLockedFn', () => {
275
+ return hostStream.locked ? context.true : context.false;
276
+ });
277
+
278
+ // Store the function on the stream object itself (scoped to this stream)
279
+ context.setProp(streamHandle, '__getLockedFn', getLockedFn);
280
+ }
281
+
282
+ // Define the 'locked' property as a getter using Object.defineProperty
283
+ const defineCode = `
284
+ (function(stream) {
285
+ var fn = stream.__getLockedFn;
286
+ Object.defineProperty(stream, 'locked', {
287
+ get: function() { return fn(); },
288
+ enumerable: true,
289
+ configurable: true
290
+ });
291
+ delete stream.__getLockedFn; // Clean up the visible property
292
+ return stream;
293
+ })
294
+ `;
295
+
296
+ const defineResult = context.evalCode(defineCode);
297
+
298
+ if (defineResult.error) {
299
+ // If we can't define the getter, fall back to static property
300
+ const error = context.dump(defineResult.error);
301
+ defineResult.error.dispose();
302
+ ctx.logger?.warn?.(`[ReadableStream] Could not define locked getter: ${JSON.stringify(error)}`);
303
+ context.setProp(streamHandle, 'locked', hostStream.locked ? context.true : context.false);
304
+ } else {
305
+ // Apply the function to our stream handle
306
+ using defineFunc = defineResult.value;
307
+ const applyResult = context.callFunction(defineFunc, context.undefined, streamHandle);
308
+
309
+ if (applyResult.error) {
310
+ const error = context.dump(applyResult.error);
311
+ applyResult.error.dispose();
312
+ ctx.logger?.warn?.(
313
+ `[ReadableStream] Could not apply locked getter: ${JSON.stringify(error)}`
314
+ );
315
+ // Fallback to static property
316
+ context.setProp(streamHandle, 'locked', hostStream.locked ? context.true : context.false);
317
+ } else {
318
+ (applyResult as { value: QuickJSHandle }).value.dispose();
319
+ }
320
+ }
321
+
322
+ return streamHandle;
323
+ }
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { executeWithAsyncHost } from '../test-helper.js';
3
+
4
+ describe('Response Bridge', () => {
5
+ it('should provide Response constructor', async () => {
6
+ const result = await executeWithAsyncHost(`return typeof Response`, {});
7
+
8
+ expect(result.success).toBe(true);
9
+ expect(result.result).toBe('function');
10
+ });
11
+
12
+ it('should create Response from string', async () => {
13
+ const code = `
14
+ const res = new Response('Hello World');
15
+ return {
16
+ status: res.status,
17
+ ok: res.ok,
18
+ statusText: res.statusText
19
+ }
20
+ `;
21
+
22
+ const result = await executeWithAsyncHost(code, {});
23
+
24
+ if (!result.success) {
25
+ console.error('Error:', result.error);
26
+ }
27
+
28
+ expect(result.success).toBe(true);
29
+ expect(result.result.status).toBe(200);
30
+ expect(result.result.ok).toBe(true);
31
+ expect(result.result.statusText).toBe('');
32
+ });
33
+
34
+ it('should create Response with custom status', async () => {
35
+ const code = `
36
+ const res = new Response('Not found', { status: 404 });
37
+ return res.status
38
+ `;
39
+
40
+ const result = await executeWithAsyncHost(code, {});
41
+
42
+ expect(result.success).toBe(true);
43
+ expect(result.result).toBe(404);
44
+ });
45
+
46
+ it('should consume response body as text', async () => {
47
+ const code = `
48
+ const res = new Response('Hello World');
49
+ const text = await res.text();
50
+ return text
51
+ `;
52
+
53
+ const result = await executeWithAsyncHost(code, {});
54
+
55
+ expect(result.success).toBe(true);
56
+ expect(result.result).toBe('Hello World');
57
+ });
58
+
59
+ it('should consume response body as JSON', async () => {
60
+ const code = `
61
+ const res = new Response('{"message":"Hello"}');
62
+ const json = await res.json();
63
+ return json
64
+ `;
65
+
66
+ const result = await executeWithAsyncHost(code, {});
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.result).toEqual({ message: 'Hello' });
70
+ });
71
+
72
+ it('should create Response with custom headers', async () => {
73
+ const code = `
74
+ const res = new Response('data', {
75
+ headers: { 'Content-Type': 'text/plain', 'X-Custom': 'value' }
76
+ });
77
+ return {
78
+ contentType: res.headers.get('Content-Type'),
79
+ custom: res.headers.get('X-Custom')
80
+ }
81
+ `;
82
+
83
+ const result = await executeWithAsyncHost(code, {});
84
+
85
+ expect(result.success).toBe(true);
86
+ expect(result.result.contentType).toBe('text/plain');
87
+ expect(result.result.custom).toBe('value');
88
+ });
89
+
90
+ it('should support has() method on headers', async () => {
91
+ const code = `
92
+ const res = new Response('data', {
93
+ headers: { 'Content-Type': 'text/plain' }
94
+ });
95
+ return res.headers.has('Content-Type')
96
+ `;
97
+
98
+ const result = await executeWithAsyncHost(code, {});
99
+
100
+ expect(result.success).toBe(true);
101
+ expect(result.result).toBe(true);
102
+ });
103
+
104
+ it('should support entries() method on headers', async () => {
105
+ const code = `
106
+ const res = new Response('data', {
107
+ headers: { 'content-type': 'text/plain', 'x-custom': 'value' }
108
+ });
109
+ const entries = res.headers.entries();
110
+ return entries
111
+ `;
112
+
113
+ const result = await executeWithAsyncHost(code, {});
114
+
115
+ expect(result.success).toBe(true);
116
+ expect(result.result.length).toBeGreaterThanOrEqual(2);
117
+ // Check that our headers are present (Headers normalizes to lowercase)
118
+ const entries = result.result;
119
+ const hasContentType = entries.some(
120
+ (e: string[]) => e[0].toLowerCase() === 'content-type' && e[1] === 'text/plain'
121
+ );
122
+ const hasCustom = entries.some(
123
+ (e: string[]) => e[0].toLowerCase() === 'x-custom' && e[1] === 'value'
124
+ );
125
+ expect(hasContentType).toBe(true);
126
+ expect(hasCustom).toBe(true);
127
+ });
128
+
129
+ it('should support ok property based on status', async () => {
130
+ const code = `
131
+ const okRes = new Response('good', { status: 200 });
132
+ const notOkRes = new Response('bad', { status: 404 });
133
+ return { ok: okRes.ok, notOk: notOkRes.ok }
134
+ `;
135
+
136
+ const result = await executeWithAsyncHost(code, {});
137
+
138
+ expect(result.success).toBe(true);
139
+ expect(result.result.ok).toBe(true);
140
+ expect(result.result.notOk).toBe(false);
141
+ });
142
+
143
+ it('should support custom statusText', async () => {
144
+ const code = `
145
+ const res = new Response('Error', { status: 500, statusText: 'Internal Server Error' });
146
+ return res.statusText
147
+ `;
148
+
149
+ const result = await executeWithAsyncHost(code, {});
150
+
151
+ expect(result.success).toBe(true);
152
+ expect(result.result).toBe('Internal Server Error');
153
+ });
154
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Response API bridge - Provides Response constructor for sandbox
3
+ *
4
+ * Allows creating Response objects manually, useful for:
5
+ * - Consuming ReadableStreams with .text(), .json(), .arrayBuffer() methods
6
+ * - Creating mock responses for testing
7
+ * - Handlers that need to return Response objects
8
+ */
9
+
10
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
11
+ import type { BridgeContext } from './types.js';
12
+ import {
13
+ createResponseObject,
14
+ setupStreamRegistry,
15
+ unwrapResult
16
+ } from './shared/index.js';
17
+
18
+ /**
19
+ * Install Response constructor in the sandbox
20
+ */
21
+ export function installResponse(ctx: BridgeContext): void {
22
+ const { context, logger } = ctx;
23
+
24
+ // Create response object context for the factory (includes cleanup handler)
25
+ const objCtx = setupStreamRegistry(ctx, 'Response');
26
+
27
+ // Create host function to create Response
28
+ {
29
+ using hostCreateResponse = context.newFunction(
30
+ '__hostCreateResponse',
31
+ (bodyHandle?: QuickJSHandle, initHandle?: QuickJSHandle) => {
32
+ // Parse body argument
33
+ let body: BodyInit | null = null;
34
+ if (bodyHandle) {
35
+ const bodyValue = context.dump(bodyHandle);
36
+ // Check if it's a ReadableStream (has a _streamId property)
37
+ if (bodyValue && typeof bodyValue === 'object' && typeof bodyValue._streamId === 'number') {
38
+ const stream = objCtx.registry.hostStreams.get(bodyValue._streamId);
39
+ if (!stream) {
40
+ throw new Error(`Invalid stream ID: ${bodyValue._streamId}. Stream may have been consumed or cleaned up.`);
41
+ }
42
+ body = stream;
43
+ } else if (
44
+ bodyValue &&
45
+ typeof bodyValue === 'object' &&
46
+ 'byteLength' in bodyValue &&
47
+ 'buffer' in bodyValue &&
48
+ typeof (bodyValue as any).byteLength === 'number' &&
49
+ (bodyValue as any).byteLength >= 0
50
+ ) {
51
+ // It's a Uint8Array or ArrayBufferView - cast to BodyInit
52
+ body = bodyValue as BodyInit;
53
+ } else {
54
+ body = bodyValue as BodyInit;
55
+ }
56
+ }
57
+
58
+ // Parse init argument
59
+ let init: ResponseInit = {};
60
+ if (initHandle) {
61
+ try {
62
+ init = context.dump(initHandle) || {};
63
+ } catch {
64
+ // Invalid init object, use defaults
65
+ }
66
+ }
67
+
68
+ // Create host Response
69
+ const hostResponse = new Response(body, init);
70
+
71
+ // Create QuickJS Response object using the unified factory
72
+ return createResponseObject(ctx, { response: hostResponse }, objCtx);
73
+ }
74
+ );
75
+
76
+ context.setProp(context.global, '__hostCreateResponse', hostCreateResponse);
77
+ }
78
+
79
+ // Define Response class in QuickJS
80
+ const classCode = `
81
+ class Response {
82
+ constructor(body, init) {
83
+ const response = __hostCreateResponse(body, init);
84
+ // Copy all properties from the response to this
85
+ this.status = response.status;
86
+ this.statusText = response.statusText;
87
+ this.ok = response.ok;
88
+ this.url = response.url;
89
+ this.type = response.type;
90
+ this.redirected = response.redirected;
91
+ this.body = response.body;
92
+ this.headers = response.headers;
93
+ this.json = response.json.bind(response);
94
+ this.text = response.text.bind(response);
95
+ this.arrayBuffer = response.arrayBuffer.bind(response);
96
+ }
97
+ }
98
+ `;
99
+
100
+ {
101
+ using classHandle = unwrapResult(context.evalCode(classCode), context);
102
+ context.setProp(context.global, 'Response', classHandle);
103
+
104
+ // Hide internal helper from sandbox code
105
+ // NOTE: The sandbox is not a hard security boundary. Internal helpers are hidden via
106
+ // enumerable: false to prevent accidental access, but can still be called by name if
107
+ // the sandbox code knows them. This is acceptable because:
108
+ // 1. The bridges are the primary security boundary, not QuickJS containment
109
+ // 2. Internal helpers don't expose dangerous capabilities beyond what bridges already provide
110
+ // 3. For the use case of safely executing LLM-generated code, this is sufficient
111
+ // See Sandbox.ts for the full security model documentation.
112
+ const hideResult = context.evalCode(`(function() {
113
+ try { Object.defineProperty(globalThis, '__hostCreateResponse', { enumerable: false, writable: false, configurable: false }); } catch(e) {}
114
+ })()`);
115
+ if (hideResult.error) {
116
+ hideResult.error.dispose();
117
+ } else {
118
+ (hideResult as { value: QuickJSHandle }).value.dispose();
119
+ }
120
+ }
121
+
122
+ logger?.info?.('[Bridge] Response constructor installed');
123
+ }