@execbox/quickjs 0.2.0 → 0.3.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 (53) hide show
  1. package/README.md +21 -5
  2. package/dist/index.cjs +610 -7
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +19 -4
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +19 -4
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +607 -4
  9. package/dist/index.js.map +1 -1
  10. package/dist/processEntry.cjs +17 -0
  11. package/dist/processEntry.cjs.map +1 -0
  12. package/dist/processEntry.d.cts +5 -0
  13. package/dist/processEntry.d.ts +5 -0
  14. package/dist/processEntry.js +18 -0
  15. package/dist/processEntry.js.map +1 -0
  16. package/dist/protocolEndpoint-A1JBvHG_.cjs +147 -0
  17. package/dist/protocolEndpoint-A1JBvHG_.cjs.map +1 -0
  18. package/dist/protocolEndpoint-CDGgtfFu.js +142 -0
  19. package/dist/protocolEndpoint-CDGgtfFu.js.map +1 -0
  20. package/dist/runner/index.cjs +1 -1
  21. package/dist/runner/index.d.cts +16 -78
  22. package/dist/runner/index.d.cts.map +1 -1
  23. package/dist/runner/index.d.ts +16 -78
  24. package/dist/runner/index.d.ts.map +1 -1
  25. package/dist/runner/index.js +1 -1
  26. package/dist/runner/protocolEndpoint.cjs +3 -137
  27. package/dist/runner/protocolEndpoint.d.cts +4 -0
  28. package/dist/runner/protocolEndpoint.d.cts.map +1 -1
  29. package/dist/runner/protocolEndpoint.d.ts +4 -0
  30. package/dist/runner/protocolEndpoint.d.ts.map +1 -1
  31. package/dist/runner/protocolEndpoint.js +3 -137
  32. package/dist/{runner-B4UG88h1.js → runner-CteKTaPD.js} +93 -10
  33. package/dist/runner-CteKTaPD.js.map +1 -0
  34. package/dist/{runner-DhOZH9xz.cjs → runner-DRt0kpEk.cjs} +140 -9
  35. package/dist/{runner-B4UG88h1.js.map → runner-DRt0kpEk.cjs.map} +1 -1
  36. package/dist/types-BeVqrcj8.d.ts +71 -0
  37. package/dist/types-BeVqrcj8.d.ts.map +1 -0
  38. package/dist/types-CnNmLawC.d.cts +71 -0
  39. package/dist/types-CnNmLawC.d.cts.map +1 -0
  40. package/dist/workerEntry.cjs +19 -0
  41. package/dist/workerEntry.cjs.map +1 -0
  42. package/dist/workerEntry.d.cts +5 -0
  43. package/dist/workerEntry.d.ts +5 -0
  44. package/dist/workerEntry.js +20 -0
  45. package/dist/workerEntry.js.map +1 -0
  46. package/package.json +16 -7
  47. package/dist/runner/protocolEndpoint.cjs.map +0 -1
  48. package/dist/runner/protocolEndpoint.js.map +0 -1
  49. package/dist/runner-DhOZH9xz.cjs.map +0 -1
  50. package/dist/types-BB8mb_-T.d.cts +0 -14
  51. package/dist/types-BB8mb_-T.d.cts.map +0 -1
  52. package/dist/types-D7uau0GM.d.ts +0 -14
  53. package/dist/types-D7uau0GM.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,23 +1,26 @@
1
1
  # @execbox/quickjs
2
2
 
3
- QuickJS executor backend for `@execbox/core`.
3
+ QuickJS executor backend for `@execbox/core`, supporting inline, worker-hosted, and process-hosted execution.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40execbox%2Fquickjs?style=flat-square)](https://www.npmjs.com/package/@execbox/quickjs)
6
6
  [![License](https://img.shields.io/github/license/aallam/execbox?style=flat-square)](https://github.com/aallam/execbox/blob/main/LICENSE)
7
7
 
8
+ Docs: https://execbox.aallam.com
9
+
8
10
  ## Choose QuickJS When
9
11
 
10
12
  - you want the easiest execbox backend to install
11
13
  - you do not want a native addon in CI or local development
12
14
  - you want fresh runtimes, captured `console.*` output, and JSON-only tool boundaries
15
+ - you want to move from inline execution to worker or child-process hosts without changing packages
13
16
 
14
17
  ## Security Notes
15
18
 
16
19
  - Each execution gets a fresh QuickJS runtime with no ambient Node globals injected by execbox.
17
20
  - Tool calls cross a JSON-only bridge, and executor timeouts propagate abort signals to in-flight provider work.
18
21
  - In the default deployment model, provider definitions are controlled by the host application, while hostile users control guest code and tool inputs.
19
- - This package is not presented as a hard security boundary for hostile code. It is best-effort in-process isolation.
20
- - If you need a stronger boundary, run execbox behind a separate process or container.
22
+ - This package is designed for host-controlled deployments and does not by itself create a hard isolation boundary for hostile code.
23
+ - If you need a stronger boundary, use `host: "process"`, move execution behind `@execbox/remote`, or place the runtime behind a container or VM.
21
24
 
22
25
  ## Architecture Docs
23
26
 
@@ -28,6 +31,7 @@ QuickJS executor backend for `@execbox/core`.
28
31
  ## Examples
29
32
 
30
33
  - [Basic provider execution](https://github.com/aallam/execbox/blob/main/examples/execbox-basic.ts)
34
+ - [Process-hosted QuickJS execution](https://github.com/aallam/execbox/blob/main/examples/execbox-process.ts)
31
35
  - [Worker-backed QuickJS execution](https://github.com/aallam/execbox/blob/main/examples/execbox-worker.ts)
32
36
  - [MCP provider wrapping](https://github.com/aallam/execbox/blob/main/examples/execbox-mcp-provider.ts)
33
37
  - [MCP server wrapper](https://github.com/aallam/execbox/blob/main/examples/execbox-mcp-server.ts)
@@ -40,7 +44,7 @@ npm install @execbox/core @execbox/quickjs
40
44
  ```
41
45
 
42
46
  Advanced consumers can also import the reusable QuickJS runner from `@execbox/quickjs/runner`.
43
- Transport-backed executors such as `@execbox/worker` and `@execbox/process` also reuse the shared QuickJS protocol endpoint from `@execbox/quickjs/runner/protocol-endpoint`.
47
+ Hosted worker/process execution and `@execbox/remote` also reuse the shared QuickJS protocol endpoint from `@execbox/quickjs/runner/protocol-endpoint`.
44
48
 
45
49
  ## Usage
46
50
 
@@ -64,4 +68,16 @@ const result = await executor.execute("await codemode.echo({ ok: true })", [
64
68
 
65
69
  Each execution runs in a fresh QuickJS runtime with timeout handling, captured logs, and JSON-only result and tool boundaries.
66
70
 
67
- `QuickJsExecutor` intentionally stays ephemeral. Every `execute()` call creates a fresh QuickJS runtime/context, and the package does not expose a pooling API.
71
+ `QuickJsExecutor` defaults to inline execution. Set `host: "worker"` when you want pooled worker-shell reuse, or `host: "process"` when you want pooled child-process reuse with a stronger lifecycle split.
72
+
73
+ ```ts
74
+ const executor = new QuickJsExecutor({
75
+ host: "worker",
76
+ pool: {
77
+ maxSize: 4,
78
+ prewarm: true,
79
+ },
80
+ });
81
+
82
+ await executor.prewarm();
83
+ ```
package/dist/index.cjs CHANGED
@@ -1,25 +1,628 @@
1
- const require_runner = require('./runner-DhOZH9xz.cjs');
2
- let __execbox_core = require("@execbox/core");
1
+ const require_runner = require('./runner-DRt0kpEk.cjs');
2
+ let node_child_process = require("node:child_process");
3
+ let node_url = require("node:url");
4
+ let __execbox_protocol = require("@execbox/protocol");
5
+ let node_crypto = require("node:crypto");
6
+ let node_os = require("node:os");
7
+ let node_worker_threads = require("node:worker_threads");
3
8
 
9
+ //#region ../core/src/runner.ts
10
+ function toTrustedExecuteError(error) {
11
+ if (require_runner.isExecuteFailure(error)) return {
12
+ code: error.code,
13
+ message: error.message
14
+ };
15
+ return {
16
+ code: "tool_error",
17
+ message: require_runner.normalizeThrownMessage(error)
18
+ };
19
+ }
20
+ /**
21
+ * Converts resolved providers into manifest metadata that reveals only namespace details.
22
+ */
23
+ function extractProviderManifests(providers) {
24
+ return providers.map((provider) => ({
25
+ name: provider.name,
26
+ tools: Object.fromEntries(Object.entries(provider.tools).map(([safeToolName, descriptor]) => [safeToolName, {
27
+ description: descriptor.description,
28
+ originalName: descriptor.originalName,
29
+ safeName: descriptor.safeName
30
+ }])),
31
+ types: provider.types
32
+ }));
33
+ }
34
+ /**
35
+ * Creates a host-side dispatcher for runner-emitted tool calls.
36
+ */
37
+ function createToolCallDispatcher(providers, signal) {
38
+ const providerMap = new Map(providers.map((provider) => [provider.name, provider]));
39
+ return async (call) => {
40
+ const provider = providerMap.get(call.providerName);
41
+ const descriptor = provider?.tools[call.safeToolName];
42
+ if (!provider || !descriptor) return {
43
+ error: {
44
+ code: "internal_error",
45
+ message: `Unknown tool ${call.providerName}.${call.safeToolName}`
46
+ },
47
+ ok: false
48
+ };
49
+ try {
50
+ if (signal.aborted) return {
51
+ error: {
52
+ code: "timeout",
53
+ message: require_runner.getExecutionTimeoutMessage()
54
+ },
55
+ ok: false
56
+ };
57
+ const result = await descriptor.execute(call.input, require_runner.createExecutionContext(signal, provider.name, descriptor.safeName, descriptor.originalName));
58
+ if (result !== void 0 && !require_runner.isJsonSerializable(result)) throw new require_runner.ExecuteFailure("serialization_error", "Host value is not JSON-serializable");
59
+ return {
60
+ ok: true,
61
+ result
62
+ };
63
+ } catch (error) {
64
+ return {
65
+ error: toTrustedExecuteError(error),
66
+ ok: false
67
+ };
68
+ }
69
+ };
70
+ }
71
+
72
+ //#endregion
73
+ //#region src/hosted/shared.ts
74
+ /**
75
+ * Default grace period before a hosted shell is forcefully terminated.
76
+ */
77
+ const DEFAULT_CANCEL_GRACE_MS = 25;
78
+ /**
79
+ * Default pooling limits shared by the hosted QuickJS executors.
80
+ */
81
+ const DEFAULT_POOL_OPTIONS = {
82
+ idleTimeoutMs: 3e4,
83
+ maxSize: 1,
84
+ minSize: 0,
85
+ prewarm: false
86
+ };
87
+ /**
88
+ * Minimal code used to warm a hosted shell without touching user providers.
89
+ */
90
+ const DEFAULT_PREWARM_CODE = "undefined";
91
+ /**
92
+ * Wraps a transport so warmup and pooled execution can borrow it without
93
+ * taking ownership of its lifecycle.
94
+ */
95
+ function createBorrowedTransport(transport) {
96
+ return {
97
+ dispose() {},
98
+ onClose: transport.onClose,
99
+ onError: transport.onError,
100
+ onMessage: transport.onMessage,
101
+ send: transport.send,
102
+ terminate: transport.terminate
103
+ };
104
+ }
105
+ /**
106
+ * Resolves how many pooled shells should be prewarmed from the pool settings.
107
+ */
108
+ function getPrewarmCount(pool) {
109
+ if (!pool?.prewarm) return 0;
110
+ if (typeof pool.prewarm === "number") return Math.max(0, Math.min(pool.prewarm, pool.maxSize));
111
+ return Math.max(1, Math.min(pool.minSize ?? 1, pool.maxSize));
112
+ }
113
+ /**
114
+ * Caps an explicit warmup request to the configured pool boundaries.
115
+ */
116
+ function getWarmupTarget(count, poolOptions) {
117
+ return Math.max(0, Math.min(count ?? poolOptions.minSize ?? 0, poolOptions.maxSize));
118
+ }
119
+ /**
120
+ * Returns whether a hosted execution result is safe to return to a pool.
121
+ */
122
+ function isReusableResult(result) {
123
+ return result.ok || !["internal_error", "timeout"].includes(result.error.code);
124
+ }
125
+ /**
126
+ * Normalizes a failed warmup result into an actionable host-side error.
127
+ */
128
+ function toWarmupError(label, result) {
129
+ if (result.ok) return /* @__PURE__ */ new Error(`Failed to prewarm pooled ${label}`);
130
+ return /* @__PURE__ */ new Error(`Failed to prewarm pooled ${label}: ${result.error.message}`);
131
+ }
132
+ /**
133
+ * Runs one transport-backed execution session with resolved runtime limits and
134
+ * a fresh execution identifier.
135
+ */
136
+ async function runHostedTransportSession(options) {
137
+ return await (0, __execbox_protocol.runHostTransportSession)({
138
+ cancelGraceMs: options.cancelGraceMs,
139
+ code: options.code,
140
+ executionId: (0, node_crypto.randomUUID)(),
141
+ onSettled: options.onSettled,
142
+ providers: options.providers,
143
+ runtimeOptions: require_runner.resolveExecutorRuntimeOptions(options.executorOptions, options.requestOptions),
144
+ signal: options.requestOptions?.signal,
145
+ transport: options.transport
146
+ });
147
+ }
148
+ /**
149
+ * Exercises a set of leased shells with a warmup run and releases each lease
150
+ * according to the warmup result.
151
+ */
152
+ async function warmHostedPool(options) {
153
+ const rejected = (await Promise.allSettled(options.shells.map(async (shell) => {
154
+ let reusable = false;
155
+ try {
156
+ const result = await options.runSession(createBorrowedTransport(options.getTransport(shell)), DEFAULT_PREWARM_CODE, []);
157
+ reusable = result.ok;
158
+ if (!result.ok) throw toWarmupError(options.label, result);
159
+ } finally {
160
+ await options.onRelease(shell, reusable);
161
+ }
162
+ }))).find((result) => result.status === "rejected");
163
+ if (rejected?.status === "rejected") throw rejected.reason;
164
+ }
165
+
166
+ //#endregion
167
+ //#region src/hosted/processHostedExecutor.ts
168
+ function resolveProcessEntryPath() {
169
+ const extension = require("url").pathToFileURL(__filename).href.endsWith(".ts") ? ".ts" : ".js";
170
+ return (0, node_url.fileURLToPath)(new URL(`../processEntry${extension}`, require("url").pathToFileURL(__filename).href));
171
+ }
172
+ function createUnexpectedExitMessage(code, signal) {
173
+ if (code !== null) return `Child process exited unexpectedly with code ${code}`;
174
+ if (signal) return `Child process exited unexpectedly with signal ${signal}`;
175
+ return "Child process exited unexpectedly";
176
+ }
177
+ function createChildProcess() {
178
+ return (0, node_child_process.fork)(resolveProcessEntryPath(), [], {
179
+ execArgv: (0, __execbox_protocol.getNodeTransportExecArgv)(require("url").pathToFileURL(__filename).href),
180
+ stdio: [
181
+ "ignore",
182
+ "ignore",
183
+ "ignore",
184
+ "ipc"
185
+ ]
186
+ });
187
+ }
188
+ function createProcessTransport(child) {
189
+ let terminated = false;
190
+ let closeReason;
191
+ const closeHandlers = /* @__PURE__ */ new Set();
192
+ const errorHandlers = /* @__PURE__ */ new Set();
193
+ const messageHandlers = /* @__PURE__ */ new Set();
194
+ const terminateChild = () => {
195
+ if (terminated) return;
196
+ terminated = true;
197
+ child.kill("SIGKILL");
198
+ };
199
+ const notifyClose = (reason) => {
200
+ if (closeReason) return;
201
+ closeReason = reason;
202
+ for (const handler of closeHandlers) handler(reason);
203
+ };
204
+ const onDisconnect = () => {
205
+ notifyClose({ message: "Child process disconnected unexpectedly" });
206
+ };
207
+ const onExit = (code, signal) => {
208
+ notifyClose({
209
+ code,
210
+ message: createUnexpectedExitMessage(code, signal),
211
+ signal
212
+ });
213
+ };
214
+ const onError = (error) => {
215
+ for (const handler of errorHandlers) handler(error);
216
+ };
217
+ const onMessage = (message) => {
218
+ for (const handler of messageHandlers) handler(message);
219
+ };
220
+ child.on("disconnect", onDisconnect);
221
+ child.on("exit", onExit);
222
+ child.on("error", onError);
223
+ child.on("message", onMessage);
224
+ return {
225
+ dispose: () => {
226
+ child.off("disconnect", onDisconnect);
227
+ child.off("exit", onExit);
228
+ child.off("error", onError);
229
+ child.off("message", onMessage);
230
+ terminateChild();
231
+ },
232
+ onClose: (handler) => {
233
+ closeHandlers.add(handler);
234
+ if (closeReason) queueMicrotask(() => {
235
+ if (closeHandlers.has(handler)) handler(closeReason);
236
+ });
237
+ return () => {
238
+ closeHandlers.delete(handler);
239
+ };
240
+ },
241
+ onError: (handler) => {
242
+ errorHandlers.add(handler);
243
+ return () => errorHandlers.delete(handler);
244
+ },
245
+ onMessage: (handler) => {
246
+ messageHandlers.add(handler);
247
+ return () => messageHandlers.delete(handler);
248
+ },
249
+ send: (message) => new Promise((resolve, reject) => {
250
+ if (!child.connected || typeof child.send !== "function") {
251
+ reject(/* @__PURE__ */ new Error("Child process disconnected unexpectedly"));
252
+ return;
253
+ }
254
+ child.send(message, (error) => {
255
+ if (error) {
256
+ reject(error);
257
+ return;
258
+ }
259
+ resolve();
260
+ });
261
+ }),
262
+ terminate: () => {
263
+ terminateChild();
264
+ }
265
+ };
266
+ }
267
+ function createProcessShell() {
268
+ const child = createChildProcess();
269
+ return {
270
+ child,
271
+ transport: createProcessTransport(child)
272
+ };
273
+ }
274
+ function resolvePoolOptions$1(options) {
275
+ if (options.mode === "ephemeral") return;
276
+ return {
277
+ ...DEFAULT_POOL_OPTIONS,
278
+ ...options.pool
279
+ };
280
+ }
281
+ /**
282
+ * Child-process executor that runs guest code inside a dedicated QuickJS runtime per call.
283
+ */
284
+ var ProcessHostedQuickJsExecutor = class {
285
+ cancelGraceMs;
286
+ options;
287
+ pool;
288
+ poolOptions;
289
+ warmup;
290
+ /**
291
+ * Creates a hosted QuickJS executor that launches child-process shells on demand.
292
+ */
293
+ constructor(options) {
294
+ this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
295
+ this.options = options;
296
+ const poolOptions = resolvePoolOptions$1(options);
297
+ this.poolOptions = poolOptions;
298
+ if (poolOptions) {
299
+ this.pool = (0, __execbox_protocol.createResourcePool)({
300
+ create: async () => createProcessShell(),
301
+ destroy: async (shell) => {
302
+ await shell.transport.dispose();
303
+ },
304
+ idleTimeoutMs: poolOptions.idleTimeoutMs,
305
+ maxSize: poolOptions.maxSize,
306
+ minSize: poolOptions.minSize
307
+ });
308
+ const prewarmCount = getPrewarmCount(poolOptions);
309
+ if (prewarmCount > 0) this.warmup = this.warmPool(prewarmCount);
310
+ }
311
+ }
312
+ /**
313
+ * Disposes any pooled child-process shells owned by this executor.
314
+ */
315
+ async dispose() {
316
+ await this.pool?.dispose();
317
+ }
318
+ /**
319
+ * Prewarms pooled child-process shells up to the requested count.
320
+ */
321
+ async prewarm(count) {
322
+ if (!this.pool || !this.poolOptions) return;
323
+ const target = getWarmupTarget(count, this.poolOptions);
324
+ if (target <= 0) return;
325
+ await this.warmPool(target);
326
+ }
327
+ async runTransportSession(transport, code, providers, options = {}, onSettled) {
328
+ return await runHostedTransportSession({
329
+ cancelGraceMs: this.cancelGraceMs,
330
+ code,
331
+ executorOptions: this.options,
332
+ onSettled,
333
+ providers,
334
+ requestOptions: options,
335
+ transport
336
+ });
337
+ }
338
+ async warmPool(count) {
339
+ if (!this.pool) return;
340
+ await this.pool.prewarm(count);
341
+ const leases = [];
342
+ try {
343
+ for (let index = 0; index < count; index += 1) leases.push(await this.pool.acquire());
344
+ } catch (error) {
345
+ await Promise.allSettled(leases.map(async (lease) => await lease.release(false)));
346
+ throw error;
347
+ }
348
+ await warmHostedPool({
349
+ count,
350
+ getTransport: (lease) => lease.value.transport,
351
+ label: "child process",
352
+ onRelease: async (lease, reusable) => await lease.release(reusable),
353
+ runSession: async (transport, code, providers) => await this.runTransportSession(transport, code, providers),
354
+ shells: leases
355
+ });
356
+ }
357
+ /**
358
+ * Executes guest code in a child-process-hosted QuickJS shell.
359
+ */
360
+ async execute(code, providers, options = {}) {
361
+ if (options.signal?.aborted) return require_runner.createTimeoutExecuteResult();
362
+ await this.warmup;
363
+ if (this.pool) {
364
+ const lease = await this.pool.acquire();
365
+ return await this.runTransportSession(createBorrowedTransport(lease.value.transport), code, providers, options, async (result) => {
366
+ await lease.release(isReusableResult(result));
367
+ });
368
+ }
369
+ let child;
370
+ try {
371
+ child = createChildProcess();
372
+ } catch (error) {
373
+ return {
374
+ durationMs: 0,
375
+ error: {
376
+ code: "internal_error",
377
+ message: error instanceof Error ? error.message : String(error)
378
+ },
379
+ logs: [],
380
+ ok: false
381
+ };
382
+ }
383
+ return await this.runTransportSession(createProcessTransport(child), code, providers, options);
384
+ }
385
+ };
386
+
387
+ //#endregion
388
+ //#region src/hosted/workerHostedExecutor.ts
389
+ const DEFAULT_POOLED_WORKER_MAX_SIZE = 4;
390
+ function resolveWorkerEntryUrl() {
391
+ const extension = require("url").pathToFileURL(__filename).href.endsWith(".ts") ? ".ts" : ".js";
392
+ return new URL(`../workerEntry${extension}`, require("url").pathToFileURL(__filename).href);
393
+ }
394
+ function createWorkerTransport(worker) {
395
+ let terminated = false;
396
+ let closeReason;
397
+ const closeHandlers = /* @__PURE__ */ new Set();
398
+ const errorHandlers = /* @__PURE__ */ new Set();
399
+ const messageHandlers = /* @__PURE__ */ new Set();
400
+ const terminateWorker = async () => {
401
+ if (terminated) return;
402
+ terminated = true;
403
+ await worker.terminate().catch(() => {});
404
+ };
405
+ const notifyClose = (reason) => {
406
+ if (closeReason) return;
407
+ closeReason = reason;
408
+ for (const handler of closeHandlers) handler(reason);
409
+ };
410
+ const onExit = (code) => {
411
+ notifyClose({
412
+ code,
413
+ message: `Worker exited unexpectedly with code ${code}`
414
+ });
415
+ };
416
+ const onError = (error) => {
417
+ for (const handler of errorHandlers) handler(error);
418
+ };
419
+ const onMessage = (message) => {
420
+ for (const handler of messageHandlers) handler(message);
421
+ };
422
+ worker.on("exit", onExit);
423
+ worker.on("error", onError);
424
+ worker.on("message", onMessage);
425
+ return {
426
+ dispose: async () => {
427
+ worker.off("exit", onExit);
428
+ worker.off("error", onError);
429
+ worker.off("message", onMessage);
430
+ await terminateWorker();
431
+ },
432
+ onClose: (handler) => {
433
+ closeHandlers.add(handler);
434
+ if (closeReason) queueMicrotask(() => {
435
+ if (closeHandlers.has(handler)) handler(closeReason);
436
+ });
437
+ return () => closeHandlers.delete(handler);
438
+ },
439
+ onError: (handler) => {
440
+ errorHandlers.add(handler);
441
+ return () => errorHandlers.delete(handler);
442
+ },
443
+ onMessage: (handler) => {
444
+ messageHandlers.add(handler);
445
+ return () => messageHandlers.delete(handler);
446
+ },
447
+ send: (message) => {
448
+ worker.postMessage(message);
449
+ },
450
+ terminate: async () => {
451
+ await terminateWorker();
452
+ }
453
+ };
454
+ }
455
+ function createWorkerShell(options) {
456
+ const worker = new node_worker_threads.Worker(resolveWorkerEntryUrl(), {
457
+ execArgv: (0, __execbox_protocol.getNodeTransportExecArgv)(require("url").pathToFileURL(__filename).href),
458
+ resourceLimits: options.workerResourceLimits
459
+ });
460
+ return {
461
+ transport: createWorkerTransport(worker),
462
+ worker
463
+ };
464
+ }
465
+ function getDefaultPoolMaxSize() {
466
+ try {
467
+ return Math.max(1, Math.min((0, node_os.availableParallelism)(), DEFAULT_POOLED_WORKER_MAX_SIZE));
468
+ } catch {
469
+ return 1;
470
+ }
471
+ }
472
+ function resolvePoolOptions(options) {
473
+ if (options.mode === "ephemeral") return;
474
+ return {
475
+ ...DEFAULT_POOL_OPTIONS,
476
+ maxSize: getDefaultPoolMaxSize(),
477
+ ...options.pool
478
+ };
479
+ }
480
+ /**
481
+ * Worker-thread executor that runs guest code inside a dedicated QuickJS runtime per call.
482
+ */
483
+ var WorkerHostedQuickJsExecutor = class {
484
+ cancelGraceMs;
485
+ options;
486
+ pool;
487
+ poolOptions;
488
+ warmup;
489
+ /**
490
+ * Creates a hosted QuickJS executor that launches worker-thread shells on demand.
491
+ */
492
+ constructor(options) {
493
+ this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
494
+ this.options = options;
495
+ const poolOptions = resolvePoolOptions(options);
496
+ this.poolOptions = poolOptions;
497
+ if (poolOptions) {
498
+ this.pool = (0, __execbox_protocol.createResourcePool)({
499
+ create: async () => createWorkerShell(options),
500
+ destroy: async (shell) => {
501
+ await shell.transport.dispose();
502
+ },
503
+ idleTimeoutMs: poolOptions.idleTimeoutMs,
504
+ maxSize: poolOptions.maxSize,
505
+ minSize: poolOptions.minSize
506
+ });
507
+ const prewarmCount = getPrewarmCount(poolOptions);
508
+ if (prewarmCount > 0) this.warmup = this.warmPool(prewarmCount);
509
+ }
510
+ }
511
+ /**
512
+ * Disposes any pooled worker-thread shells owned by this executor.
513
+ */
514
+ async dispose() {
515
+ await this.pool?.dispose();
516
+ }
517
+ /**
518
+ * Prewarms pooled worker-thread shells up to the requested count.
519
+ */
520
+ async prewarm(count) {
521
+ if (!this.pool || !this.poolOptions) return;
522
+ const target = getWarmupTarget(count, this.poolOptions);
523
+ if (target <= 0) return;
524
+ await this.warmPool(target);
525
+ }
526
+ async runTransportSession(transport, code, providers, options = {}, onSettled) {
527
+ return await runHostedTransportSession({
528
+ cancelGraceMs: this.cancelGraceMs,
529
+ code,
530
+ executorOptions: this.options,
531
+ onSettled,
532
+ providers,
533
+ requestOptions: options,
534
+ transport
535
+ });
536
+ }
537
+ async warmPool(count) {
538
+ if (!this.pool) return;
539
+ await this.pool.prewarm(count);
540
+ const leases = [];
541
+ try {
542
+ for (let index = 0; index < count; index += 1) leases.push(await this.pool.acquire());
543
+ } catch (error) {
544
+ await Promise.allSettled(leases.map(async (lease) => await lease.release(false)));
545
+ throw error;
546
+ }
547
+ await warmHostedPool({
548
+ count,
549
+ getTransport: (lease) => lease.value.transport,
550
+ label: "worker shell",
551
+ onRelease: async (lease, reusable) => await lease.release(reusable),
552
+ runSession: async (transport, code, providers) => await this.runTransportSession(transport, code, providers),
553
+ shells: leases
554
+ });
555
+ }
556
+ /**
557
+ * Executes guest code in a worker-thread-hosted QuickJS shell.
558
+ */
559
+ async execute(code, providers, options = {}) {
560
+ if (options.signal?.aborted) return require_runner.createTimeoutExecuteResult();
561
+ await this.warmup;
562
+ if (this.pool) {
563
+ const lease = await this.pool.acquire();
564
+ return await this.runTransportSession(createBorrowedTransport(lease.value.transport), code, providers, options, async (result) => {
565
+ await lease.release(isReusableResult(result));
566
+ });
567
+ }
568
+ const worker = new node_worker_threads.Worker(resolveWorkerEntryUrl(), {
569
+ execArgv: (0, __execbox_protocol.getNodeTransportExecArgv)(require("url").pathToFileURL(__filename).href),
570
+ resourceLimits: this.options.workerResourceLimits
571
+ });
572
+ return await this.runTransportSession(createWorkerTransport(worker), code, providers, options);
573
+ }
574
+ };
575
+
576
+ //#endregion
4
577
  //#region src/quickjsExecutor.ts
578
+ function isWorkerOptions(options) {
579
+ return options.host === "worker";
580
+ }
581
+ function isProcessOptions(options) {
582
+ return options.host === "process";
583
+ }
5
584
  /**
6
- * QuickJS-backed executor for ephemeral sandboxed JavaScript runs.
585
+ * QuickJS-backed executor for inline, worker-backed, or process-backed JavaScript runs.
7
586
  */
8
587
  var QuickJsExecutor = class {
588
+ hostedExecutor;
9
589
  options;
10
590
  /**
11
- * Creates a QuickJS executor with ephemeral runtime limits and host bridging configuration.
591
+ * Creates a QuickJS executor with inline QuickJS by default, or a hosted
592
+ * worker/process shell when `host` is explicitly set.
12
593
  */
13
594
  constructor(options = {}) {
595
+ if (isWorkerOptions(options)) {
596
+ this.hostedExecutor = new WorkerHostedQuickJsExecutor(options);
597
+ return;
598
+ }
599
+ if (isProcessOptions(options)) {
600
+ this.hostedExecutor = new ProcessHostedQuickJsExecutor(options);
601
+ return;
602
+ }
14
603
  this.options = options;
15
604
  }
16
605
  /**
606
+ * Disposes any pooled hosted shells owned by this executor.
607
+ */
608
+ async dispose() {
609
+ await this.hostedExecutor?.dispose?.();
610
+ }
611
+ /**
612
+ * Prewarms pooled hosted shells when the executor is running in worker or
613
+ * process mode. Inline mode treats this as a no-op.
614
+ */
615
+ async prewarm(count) {
616
+ await this.hostedExecutor?.prewarm?.(count);
617
+ }
618
+ /**
17
619
  * Executes JavaScript against the provided tool namespaces in a fresh QuickJS runtime.
18
620
  */
19
621
  async execute(code, providers, options = {}) {
20
- if (options.signal?.aborted) return (0, __execbox_core.createTimeoutExecuteResult)();
622
+ if (this.hostedExecutor) return await this.hostedExecutor.execute(code, providers, options);
623
+ if (options.signal?.aborted) return require_runner.createTimeoutExecuteResult();
21
624
  const abortController = new AbortController();
22
- const onToolCall = (0, __execbox_core.createToolCallDispatcher)(providers, abortController.signal);
625
+ const onToolCall = createToolCallDispatcher(providers, abortController.signal);
23
626
  const onAbort = () => {
24
627
  abortController.abort();
25
628
  };
@@ -29,7 +632,7 @@ var QuickJsExecutor = class {
29
632
  abortController,
30
633
  code,
31
634
  onToolCall,
32
- providers: (0, __execbox_core.extractProviderManifests)(providers)
635
+ providers: extractProviderManifests(providers)
33
636
  }, {
34
637
  ...this.options,
35
638
  ...options