@dotcms/ai 26.10.20-1 → 26.10.21-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 (72) hide show
  1. package/README.md +203 -29
  2. package/adapter.cjs.default.js +1 -0
  3. package/adapter.cjs.js +14 -0
  4. package/adapter.cjs.mjs +2 -0
  5. package/adapter.cjs2.js +0 -0
  6. package/adapter.d.ts +1 -0
  7. package/adapter.esm.js +2 -0
  8. package/adapter.esm2.js +0 -0
  9. package/errors.cjs.js +192 -0
  10. package/errors.esm.js +180 -0
  11. package/format-result.cjs.js +833 -0
  12. package/format-result.esm.js +825 -0
  13. package/package.json +25 -20
  14. package/runtime.cjs.js +0 -0
  15. package/runtime.esm.js +0 -0
  16. package/sandbox.cjs.default.js +1 -0
  17. package/sandbox.cjs.js +69 -0
  18. package/sandbox.cjs.mjs +2 -0
  19. package/sandbox.d.ts +1 -0
  20. package/sandbox.esm.js +50 -0
  21. package/spec.cjs.default.js +1 -0
  22. package/spec.cjs.js +3 -23912
  23. package/spec.cjs.mjs +2 -0
  24. package/spec.cjs2.js +23916 -0
  25. package/spec.d.ts +1 -0
  26. package/spec.esm.js +1 -23914
  27. package/spec.esm2.js +23914 -0
  28. package/src/adapter/context.d.ts +11 -0
  29. package/src/adapter/request-core.d.ts +30 -0
  30. package/src/adapter.d.ts +1 -0
  31. package/src/runtime.d.ts +5 -4
  32. package/src/sandbox/errors.d.ts +23 -1
  33. package/src/sandbox/index.d.ts +1 -1
  34. package/src/sandbox.d.ts +1 -0
  35. package/src/spec.d.ts +1 -0
  36. package/src/tools/definitions/download-assets.d.ts +27 -0
  37. package/src/tools/definitions/execute.d.ts +15 -0
  38. package/src/tools/definitions/page-create.d.ts +23 -0
  39. package/src/tools/definitions/page-place-content.d.ts +32 -0
  40. package/src/tools/definitions/page-verify.d.ts +20 -0
  41. package/src/tools/definitions/search.d.ts +13 -0
  42. package/src/tools/definitions/upload-assets.d.ts +23 -0
  43. package/src/tools/index.d.ts +40 -0
  44. package/src/tools/operations/download-assets.d.ts +55 -0
  45. package/src/tools/operations/page-create.d.ts +81 -0
  46. package/src/tools/operations/page-place-content.d.ts +102 -0
  47. package/src/tools/operations/page-verify.d.ts +164 -0
  48. package/src/tools/operations/shared/asset-common.d.ts +28 -0
  49. package/src/tools/operations/shared/glob.d.ts +22 -0
  50. package/src/tools/operations/shared/local-root.d.ts +11 -0
  51. package/src/tools/operations/shared/page-common.d.ts +45 -0
  52. package/src/tools/operations/shared/page-path.d.ts +49 -0
  53. package/src/tools/operations/shared/resolve.d.ts +43 -0
  54. package/src/tools/operations/upload-assets.d.ts +64 -0
  55. package/src/tools/toolkit/connection.d.ts +56 -0
  56. package/src/tools/toolkit/create-tool.d.ts +66 -0
  57. package/src/tools/toolkit/endpoints.d.ts +52 -0
  58. package/src/tools/toolkit/lenient-boolean.d.ts +14 -0
  59. package/src/tools/toolkit/results.d.ts +32 -0
  60. package/src/tools/toolkit/tool-runtime.d.ts +108 -0
  61. package/src/tools/toolkit/types.d.ts +120 -0
  62. package/src/tools.d.ts +1 -0
  63. package/tools.cjs.default.js +1 -0
  64. package/tools.cjs.js +3850 -0
  65. package/tools.cjs.mjs +2 -0
  66. package/tools.d.ts +1 -0
  67. package/tools.esm.js +3817 -0
  68. package/index.cjs.default.js +0 -1
  69. package/index.cjs.js +0 -7
  70. package/index.cjs.mjs +0 -2
  71. package/index.d.ts +0 -1
  72. package/index.esm.js +0 -1
@@ -0,0 +1,833 @@
1
+ 'use strict';
2
+
3
+ var errors = require('./errors.cjs.js');
4
+ var node_worker_threads = require('node:worker_threads');
5
+ var zod = require('zod');
6
+
7
+ /**
8
+ * The sandbox worker harness — the code that runs INSIDE the worker.
9
+ *
10
+ * It is identical for both backends (Node `worker_threads` and Bun Web Workers); the only
11
+ * difference is the transport, so each backend prepends a tiny bootstrap that defines two
12
+ * functions the harness calls — `__post(msg)` and `__onMessage(handler)` — and (for Node)
13
+ * neutralizes `require`. Keeping the harness in ONE place means a protocol change (e.g. the
14
+ * error round-tripping below) is written once, not copy-pasted into two ~150-line strings.
15
+ *
16
+ * Hoisted to a module constant (it is fully static) so it is not re-allocated per execution.
17
+ */
18
+ const HARNESS_BODY = `
19
+ // Block direct network access — all calls must go through adapters
20
+ const __networkError = () => { throw new Error('Network access is disabled in sandbox'); };
21
+ const __disableGlobalApi = (name) => {
22
+ try {
23
+ Object.defineProperty(globalThis, name, { value: __networkError, writable: false, configurable: false });
24
+ } catch { /* ignore if already frozen */ }
25
+ };
26
+ __disableGlobalApi('fetch');
27
+ __disableGlobalApi('XMLHttpRequest');
28
+ __disableGlobalApi('WebSocket');
29
+ __disableGlobalApi('EventSource');
30
+ if (globalThis.navigator && typeof globalThis.navigator === 'object') {
31
+ try {
32
+ Object.defineProperty(globalThis.navigator, 'sendBeacon', { value: __networkError, writable: false, configurable: false });
33
+ } catch { /* ignore */ }
34
+ }
35
+
36
+ // Clean process environment so sandboxed code cannot read host secrets.
37
+ if (typeof process !== 'undefined') { process.env = {}; }
38
+
39
+ const logs = [];
40
+ const pendingCalls = new Map();
41
+ let callId = 0;
42
+
43
+ const console = {
44
+ log: (...args) => logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
45
+ warn: (...args) => logs.push('[WARN] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
46
+ error: (...args) => logs.push('[ERROR] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
47
+ info: (...args) => logs.push('[INFO] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
48
+ };
49
+ globalThis.console = console;
50
+
51
+ globalThis.pick = (arr, fields) => {
52
+ if (!Array.isArray(arr)) return arr;
53
+ return arr.map(item => {
54
+ const result = {};
55
+ for (const field of fields) {
56
+ const parts = field.split('.');
57
+ let value = item;
58
+ let key = parts[parts.length - 1];
59
+ for (const part of parts) value = value?.[part];
60
+ result[key] = value;
61
+ }
62
+ return result;
63
+ });
64
+ };
65
+
66
+ globalThis.table = (arr, maxRows = 10) => {
67
+ if (!Array.isArray(arr) || arr.length === 0) return '(empty)';
68
+ const items = arr.slice(0, maxRows);
69
+ const keys = Object.keys(items[0]);
70
+ const header = '| ' + keys.join(' | ') + ' |';
71
+ const sep = '|' + keys.map(() => '---').join('|') + '|';
72
+ const rows = items.map(item => '| ' + keys.map(k => String(item[k] ?? '')).join(' | ') + ' |');
73
+ let result = [header, sep, ...rows].join('\\n');
74
+ if (arr.length > maxRows) result += '\\n... +' + (arr.length - maxRows) + ' more rows';
75
+ return result;
76
+ };
77
+
78
+ globalThis.count = (arr, field) => {
79
+ if (!Array.isArray(arr)) return {};
80
+ return arr.reduce((acc, item) => {
81
+ const key = String(item[field] ?? 'unknown');
82
+ acc[key] = (acc[key] || 0) + 1;
83
+ return acc;
84
+ }, {});
85
+ };
86
+
87
+ globalThis.sum = (arr, field) => {
88
+ if (!Array.isArray(arr)) return 0;
89
+ return arr.reduce((acc, item) => acc + (Number(item[field]) || 0), 0);
90
+ };
91
+
92
+ globalThis.first = (arr, n = 5) => {
93
+ if (!Array.isArray(arr)) return arr;
94
+ return arr.slice(0, n);
95
+ };
96
+
97
+ // Resolve OpenAPI $refs against spec.components.schemas, expanding nested refs up to
98
+ // 'depth' levels. Beyond depth, { $ref } objects are left verbatim so the model can
99
+ // resolve the next hop in a follow-up query (progressive disclosure). Only available in
100
+ // the search sandbox, where the 'spec' global is injected.
101
+ //
102
+ // THREE bounds, not one. 'depth' alone was never sufficient: it is chosen by the model,
103
+ // and asking for a bigger number is exactly what a model does when depth 2 looks
104
+ // truncated. Expansion copies the whole target subtree per $ref hop with no memo, so
105
+ // cost grows multiplicatively with fan-out — and the committed spec is NOT acyclic
106
+ // (self-refs on FolderView/MultiPart/Permissionable, the BodyPart -> MultiPart ->
107
+ // BodyPart cycle, and fan-out around 10 on PageView). resolveRef('PageView', 12) would
108
+ // exhaust the worker's heap and get it killed, which the caller sees as an opaque
109
+ // sandbox death with the accumulated logs lost. The formatSandboxResult cap does not
110
+ // help: that applies to the result string, long after the graph is built in memory.
111
+ globalThis.resolveRef = (schemaOrName, depth = 2) => {
112
+ const MAX_DEPTH = 5;
113
+ const MAX_NODES = 50000;
114
+ const spec = globalThis.spec;
115
+ const schemas = spec && spec.components && spec.components.schemas;
116
+ if (!schemas) {
117
+ throw new Error('resolveRef() needs the spec global (spec.components.schemas). It is only available in the search sandbox.');
118
+ }
119
+ // Clamped rather than rejected, and the clamp is self-evident in the output: an
120
+ // unexpanded { $ref } is the same signal the model already follows for a deeper hop.
121
+ const requested = Number(depth);
122
+ const maxDepth = Math.max(0, Math.min(Number.isFinite(requested) ? requested : 2, MAX_DEPTH));
123
+ let budget = MAX_NODES;
124
+ const nameOf = (ref) => String(ref).split('/').pop();
125
+ const expand = (node, d, path) => {
126
+ if (--budget < 0) {
127
+ throw new Error('resolveRef() expanded more than ' + MAX_NODES + ' nodes and was stopped before exhausting worker memory. Resolve a smaller schema, or use a lower depth and follow the remaining $refs in a second call.');
128
+ }
129
+ if (Array.isArray(node)) return node.map((item) => expand(item, d, path));
130
+ if (!node || typeof node !== 'object') return node;
131
+ if (typeof node.$ref === 'string') {
132
+ if (d <= 0) return node;
133
+ const name = nameOf(node.$ref);
134
+ // Already on this branch's ancestry: expanding again would recurse forever on a
135
+ // self-referential or mutually-referential schema. Leaving the $ref verbatim is
136
+ // the same progressive-disclosure contract as running out of depth.
137
+ if (path.indexOf(name) !== -1) return node;
138
+ const target = schemas[name];
139
+ if (!target) return node;
140
+ return expand(target, d - 1, path.concat(name));
141
+ }
142
+ const out = {};
143
+ for (const key of Object.keys(node)) out[key] = expand(node[key], d, path);
144
+ return out;
145
+ };
146
+ if (typeof schemaOrName === 'string') {
147
+ const name = nameOf(schemaOrName);
148
+ const target = schemas[name];
149
+ if (!target) {
150
+ throw new Error('Unknown schema "' + schemaOrName + '". List names with Object.keys(spec.components.schemas).');
151
+ }
152
+ return expand(target, maxDepth, [name]);
153
+ }
154
+ return expand(schemaOrName, maxDepth, []);
155
+ };
156
+
157
+ __onMessage(async (msg) => {
158
+ const { type, data } = msg;
159
+
160
+ if (type === 'init') {
161
+ const { variables, adapterMethods, globals } = data;
162
+
163
+ for (const [key, value] of Object.entries(variables || {})) {
164
+ globalThis[key] = value;
165
+ }
166
+
167
+ for (const [key, value] of Object.entries(globals || {})) {
168
+ globalThis[key] = value;
169
+ }
170
+
171
+ globalThis.adapters = {};
172
+ for (const [adapterName, methods] of Object.entries(adapterMethods)) {
173
+ const adapterObj = {};
174
+ for (const methodName of methods) {
175
+ adapterObj[methodName] = async (...args) => {
176
+ const id = ++callId;
177
+ return new Promise((resolve, reject) => {
178
+ pendingCalls.set(id, { resolve, reject });
179
+ __post({
180
+ type: 'adapter_call',
181
+ adapter: adapterName,
182
+ method: methodName,
183
+ args,
184
+ id
185
+ });
186
+ });
187
+ };
188
+ }
189
+ globalThis.adapters[adapterName] = adapterObj;
190
+ globalThis[adapterName] = adapterObj;
191
+ }
192
+
193
+ __post({ type: 'ready' });
194
+ }
195
+
196
+ else if (type === 'adapter_result') {
197
+ const { id, result, error } = data;
198
+ const pending = pendingCalls.get(id);
199
+ if (pending) {
200
+ pendingCalls.delete(id);
201
+ if (error) {
202
+ // 'error' is the serialized DotCMSError shape ({ name, code, message, detail }).
203
+ // Rebuild a plain Error (class identity can't cross the boundary) but keep the
204
+ // code/name/detail so model code can branch on them.
205
+ const e = new Error(error.message);
206
+ if (error.name) e.name = error.name;
207
+ if (error.code) e.code = error.code;
208
+ if (error.detail) e.detail = error.detail;
209
+ pending.reject(e);
210
+ }
211
+ else pending.resolve(result);
212
+ }
213
+ }
214
+
215
+ else if (type === 'execute') {
216
+ try {
217
+ // Block dynamic import() — \`require\` is already removed, but \`import('node:fs')\`
218
+ // / \`import('node:net')\` would re-open filesystem/network access and bypass the
219
+ // adapter boundary. This is a source-level guard (matches the "confinement for
220
+ // trusted code generators" threat model; not hardened against deliberate obfuscation).
221
+ if (/\\bimport\\s*\\(/.test(data.code)) {
222
+ throw new Error('Dynamic import() is disabled in the sandbox; reach the host only through adapters.');
223
+ }
224
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
225
+ const fn = new AsyncFunction(data.code);
226
+ const result = await fn();
227
+ __post({ type: 'result', success: true, value: result, logs });
228
+ } catch (err) {
229
+ __post({
230
+ type: 'result',
231
+ success: false,
232
+ error: { name: err.name, message: err.message, code: err.code, detail: err.detail, stack: err.stack },
233
+ logs
234
+ });
235
+ }
236
+ }
237
+ });
238
+ `;
239
+ /**
240
+ * Build the full worker source for a backend. `bootstrap` defines `__post` / `__onMessage`
241
+ * (and, on Node, neutralizes `require`) before the shared harness body runs.
242
+ */
243
+ function buildWorkerSource(bootstrap) {
244
+ return `${bootstrap}\n${HARNESS_BODY}`;
245
+ }
246
+ /** Node `worker_threads` bootstrap: transport via `parentPort`, plus `require` removal. */
247
+ const NODE_WORKER_CODE = buildWorkerSource(`
248
+ const { parentPort } = require('worker_threads');
249
+ const __post = (m) => parentPort.postMessage(m);
250
+ const __onMessage = (handler) => parentPort.on('message', handler);
251
+ try {
252
+ Object.defineProperty(globalThis, 'require', { value: undefined, writable: false, configurable: false });
253
+ } catch { /* ignore */ }
254
+ `);
255
+ /** Bun / Web Worker bootstrap: transport via `self`. */
256
+ const BUN_WORKER_CODE = buildWorkerSource(`
257
+ const __post = (m) => self.postMessage(m);
258
+ const __onMessage = (handler) => { self.onmessage = (event) => handler(event.data); };
259
+ try {
260
+ Object.defineProperty(globalThis, 'require', { value: undefined, writable: false, configurable: false });
261
+ } catch { /* ignore */ }
262
+ `);
263
+ /**
264
+ * Default per-execution resource caps, shared by both backends so a runaway/ballooning
265
+ * script can't exhaust the host. (Node applies these via `worker_threads.resourceLimits`;
266
+ * Bun applies what its Worker supports.)
267
+ */
268
+ const DEFAULT_RESOURCE_LIMITS = {
269
+ maxOldGenerationSizeMb: 256,
270
+ maxYoungGenerationSizeMb: 32,
271
+ stackSizeMb: 4
272
+ };
273
+
274
+ const DEFAULT_CONFIG$1 = {
275
+ timeout: 5000,
276
+ globals: {},
277
+ // Kept for config parity with the Node backend; Bun's Web Worker cannot enforce these
278
+ // (no `resourceLimits` option), so they are advisory here — documented, not silently dropped.
279
+ resourceLimits: DEFAULT_RESOURCE_LIMITS
280
+ };
281
+ class BunWorkerSandbox {
282
+ constructor(config) {
283
+ this.config = { ...DEFAULT_CONFIG$1, ...config };
284
+ }
285
+ getConfig() {
286
+ return { ...this.config };
287
+ }
288
+ configure(config) {
289
+ this.config = { ...this.config, ...config };
290
+ }
291
+ async execute(code, context) {
292
+ const startTime = performance.now();
293
+ return new Promise((resolve) => {
294
+ const adapterMethods = {};
295
+ for (const [name, methods] of Object.entries(context.adapters)) {
296
+ adapterMethods[name] = Object.keys(methods);
297
+ }
298
+ const workerCode = BUN_WORKER_CODE;
299
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
300
+ const url = URL.createObjectURL(blob);
301
+ const worker = new Worker(url);
302
+ let resolved = false;
303
+ const cleanup = () => {
304
+ if (!resolved) {
305
+ resolved = true;
306
+ worker.terminate();
307
+ URL.revokeObjectURL(url);
308
+ // Let the host abort any in-flight adapter work (threaded AbortSignal).
309
+ this.config.onTeardown?.();
310
+ }
311
+ };
312
+ const timeoutId = setTimeout(() => {
313
+ if (!resolved) {
314
+ cleanup();
315
+ resolve({
316
+ success: false,
317
+ error: {
318
+ name: 'TimeoutError',
319
+ code: 'TIMEOUT',
320
+ message: `Execution timed out after ${this.config.timeout}ms`
321
+ },
322
+ logs: [],
323
+ executionTime: performance.now() - startTime
324
+ });
325
+ }
326
+ }, this.config.timeout);
327
+ worker.onmessage = async (event) => {
328
+ const { type, ...data } = event.data;
329
+ if (type === 'ready') {
330
+ worker.postMessage({ type: 'execute', data: { code } });
331
+ }
332
+ else if (type === 'adapter_call') {
333
+ const { adapter, method, args, id } = data;
334
+ // Posting to a worker that was already terminated (e.g. by a
335
+ // timeout while this adapter call was in flight) throws and
336
+ // would escape as an unhandled rejection, crashing the host.
337
+ const postResult = (payload) => {
338
+ if (resolved) {
339
+ return;
340
+ }
341
+ try {
342
+ worker.postMessage({ type: 'adapter_result', data: payload });
343
+ }
344
+ catch {
345
+ /* worker gone — nothing to deliver the result to */
346
+ }
347
+ };
348
+ try {
349
+ const adapterObj = context.adapters[adapter];
350
+ if (!adapterObj || !adapterObj[method]) {
351
+ throw new Error(`Adapter method not found: ${adapter}.${method}`);
352
+ }
353
+ const result = await adapterObj[method](...args);
354
+ postResult({ id, result });
355
+ }
356
+ catch (err) {
357
+ // Send the serialized error (typed shape round-trips); drop `stack`
358
+ // so host stacks don't cross into the sandbox.
359
+ const error = errors.serializeError(err);
360
+ delete error.stack;
361
+ postResult({ id, error });
362
+ }
363
+ }
364
+ else if (type === 'result') {
365
+ clearTimeout(timeoutId);
366
+ cleanup();
367
+ const r = data;
368
+ resolve({
369
+ success: r.success,
370
+ value: r.value,
371
+ error: r.error,
372
+ logs: r.logs || [],
373
+ executionTime: performance.now() - startTime
374
+ });
375
+ }
376
+ };
377
+ worker.onerror = (error) => {
378
+ clearTimeout(timeoutId);
379
+ cleanup();
380
+ resolve({
381
+ success: false,
382
+ error: {
383
+ name: 'WorkerError',
384
+ code: 'RUNTIME',
385
+ message: error.message || 'Unknown worker error'
386
+ },
387
+ logs: [],
388
+ executionTime: performance.now() - startTime
389
+ });
390
+ };
391
+ worker.postMessage({
392
+ type: 'init',
393
+ data: {
394
+ variables: context.variables || {},
395
+ adapterMethods,
396
+ globals: this.config.globals
397
+ }
398
+ });
399
+ });
400
+ }
401
+ dispose() {
402
+ // Workers are created per execution, nothing to dispose
403
+ }
404
+ }
405
+
406
+ const DEFAULT_CONFIG = {
407
+ timeout: 5000,
408
+ globals: {},
409
+ resourceLimits: DEFAULT_RESOURCE_LIMITS
410
+ };
411
+ class NodeWorkerSandbox {
412
+ constructor(config) {
413
+ this.config = { ...DEFAULT_CONFIG, ...config };
414
+ }
415
+ getConfig() {
416
+ return { ...this.config };
417
+ }
418
+ configure(config) {
419
+ this.config = { ...this.config, ...config };
420
+ }
421
+ async execute(code, context) {
422
+ const startTime = performance.now();
423
+ return new Promise((resolve) => {
424
+ const adapterMethods = {};
425
+ for (const [name, methods] of Object.entries(context.adapters)) {
426
+ adapterMethods[name] = Object.keys(methods);
427
+ }
428
+ const workerCode = NODE_WORKER_CODE;
429
+ const limits = this.config.resourceLimits;
430
+ const worker = new node_worker_threads.Worker(workerCode, {
431
+ eval: true,
432
+ env: {},
433
+ resourceLimits: limits
434
+ ? {
435
+ maxOldGenerationSizeMb: limits.maxOldGenerationSizeMb,
436
+ maxYoungGenerationSizeMb: limits.maxYoungGenerationSizeMb,
437
+ stackSizeMb: limits.stackSizeMb
438
+ }
439
+ : undefined
440
+ });
441
+ let resolved = false;
442
+ const cleanup = () => {
443
+ if (!resolved) {
444
+ resolved = true;
445
+ worker.terminate();
446
+ // Let the host abort any in-flight adapter work (threaded AbortSignal).
447
+ this.config.onTeardown?.();
448
+ }
449
+ };
450
+ const timeoutId = setTimeout(() => {
451
+ if (!resolved) {
452
+ cleanup();
453
+ resolve({
454
+ success: false,
455
+ error: {
456
+ name: 'TimeoutError',
457
+ code: 'TIMEOUT',
458
+ message: `Execution timed out after ${this.config.timeout}ms`
459
+ },
460
+ logs: [],
461
+ executionTime: performance.now() - startTime
462
+ });
463
+ }
464
+ }, this.config.timeout);
465
+ worker.on('message', async (msg) => {
466
+ const { type, ...data } = msg;
467
+ if (type === 'ready') {
468
+ worker.postMessage({ type: 'execute', data: { code } });
469
+ }
470
+ else if (type === 'adapter_call') {
471
+ const { adapter, method, args, id } = data;
472
+ // Posting to a worker that was already terminated (e.g. by a
473
+ // timeout while this adapter call was in flight) throws and
474
+ // would escape as an unhandled rejection, crashing the host.
475
+ const postResult = (payload) => {
476
+ if (resolved) {
477
+ return;
478
+ }
479
+ try {
480
+ worker.postMessage({ type: 'adapter_result', data: payload });
481
+ }
482
+ catch {
483
+ /* worker gone — nothing to deliver the result to */
484
+ }
485
+ };
486
+ try {
487
+ const adapterObj = context.adapters[adapter];
488
+ if (!adapterObj || !adapterObj[method]) {
489
+ throw new Error(`Adapter method not found: ${adapter}.${method}`);
490
+ }
491
+ const result = await adapterObj[method](...args);
492
+ postResult({ id, result });
493
+ }
494
+ catch (err) {
495
+ // Send the serialized error (name + code + detail) so the typed shape
496
+ // round-trips the boundary. Drop `stack` — the worker rebuilds a fresh
497
+ // Error and never reads it, and host stacks shouldn't cross into the sandbox.
498
+ const error = errors.serializeError(err);
499
+ delete error.stack;
500
+ postResult({ id, error });
501
+ }
502
+ }
503
+ else if (type === 'result') {
504
+ clearTimeout(timeoutId);
505
+ cleanup();
506
+ const r = data;
507
+ resolve({
508
+ success: r.success,
509
+ value: r.value,
510
+ error: r.error,
511
+ logs: r.logs || [],
512
+ executionTime: performance.now() - startTime
513
+ });
514
+ }
515
+ });
516
+ worker.on('error', (error) => {
517
+ clearTimeout(timeoutId);
518
+ cleanup();
519
+ resolve({
520
+ success: false,
521
+ error: {
522
+ name: 'WorkerError',
523
+ code: 'RUNTIME',
524
+ message: error.message || 'Unknown worker error'
525
+ },
526
+ logs: [],
527
+ executionTime: performance.now() - startTime
528
+ });
529
+ });
530
+ worker.postMessage({
531
+ type: 'init',
532
+ data: {
533
+ variables: context.variables || {},
534
+ adapterMethods,
535
+ globals: this.config.globals
536
+ }
537
+ });
538
+ });
539
+ }
540
+ dispose() {
541
+ // Workers are created per execution, nothing to dispose
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Low-level worker-sandbox factory. Auto-detects the runtime: native Web Workers on Bun,
547
+ * `worker_threads` on Node. Returns the raw {@link ISandbox} whose `execute(code, context)`
548
+ * the {@link Executor} drives. Most callers want the higher-level `createSandbox` barrel
549
+ * export (which returns a `run(code)` surface) instead of this.
550
+ *
551
+ * Backends are statically imported (not `require`d) so this works in both the ESM and CJS
552
+ * builds — `require` is undefined in an ESM module. Importing both is cheap: each file only
553
+ * declares a class, and `node:worker_threads` (used by the Node backend) is available on Bun too.
554
+ */
555
+ function createWorkerSandbox(config) {
556
+ if (typeof globalThis.Bun !== 'undefined') {
557
+ return new BunWorkerSandbox(config);
558
+ }
559
+ return new NodeWorkerSandbox(config);
560
+ }
561
+
562
+ class Executor {
563
+ constructor(options = {}) {
564
+ this.adapters = new Map();
565
+ this.sandboxConfig = options.config?.sandbox ?? {
566
+ timeout: 5000,
567
+ globals: {}
568
+ };
569
+ this.sandboxFactory = options.sandboxFactory ?? createWorkerSandbox;
570
+ if (options.config?.adapters) {
571
+ for (const adapter of options.config.adapters) {
572
+ this.registerAdapter(adapter);
573
+ }
574
+ }
575
+ }
576
+ registerAdapter(adapter) {
577
+ this.adapters.set(adapter.name, adapter);
578
+ }
579
+ unregisterAdapter(name) {
580
+ return this.adapters.delete(name);
581
+ }
582
+ getAdapter(name) {
583
+ return this.adapters.get(name);
584
+ }
585
+ getAdapterNames() {
586
+ return Array.from(this.adapters.keys());
587
+ }
588
+ async execute(code, options = {}) {
589
+ const sandboxConfig = {
590
+ ...this.sandboxConfig,
591
+ ...options.sandbox
592
+ };
593
+ const sandbox = this.sandboxFactory(sandboxConfig);
594
+ try {
595
+ const context = this.buildExecutionContext(options.adapters, options.variables);
596
+ return await sandbox.execute(code, context);
597
+ }
598
+ finally {
599
+ sandbox.dispose();
600
+ }
601
+ }
602
+ configureSandbox(config) {
603
+ this.sandboxConfig = {
604
+ ...this.sandboxConfig,
605
+ ...config
606
+ };
607
+ }
608
+ buildExecutionContext(adapterNames, variables) {
609
+ const adaptersToInclude = adapterNames
610
+ ? adapterNames.filter((name) => this.adapters.has(name))
611
+ : Array.from(this.adapters.keys());
612
+ const adapterMethods = {};
613
+ for (const name of adaptersToInclude) {
614
+ const adapter = this.adapters.get(name);
615
+ if (!adapter)
616
+ continue;
617
+ adapterMethods[name] = {};
618
+ for (const [methodName, method] of adapter.methods) {
619
+ adapterMethods[name][methodName] = method.execute.bind(method);
620
+ }
621
+ }
622
+ return {
623
+ adapters: adapterMethods,
624
+ variables
625
+ };
626
+ }
627
+ }
628
+ function createExecutor(options) {
629
+ return new Executor(options);
630
+ }
631
+
632
+ function validateInput(methodName, schema, raw) {
633
+ const result = schema.safeParse(raw);
634
+ if (!result.success) {
635
+ throw new errors.ValidationError(`Invalid arguments for "${methodName}": ${result.error.message}`, result.error.issues);
636
+ }
637
+ return result.data;
638
+ }
639
+ function validateOutput(schema, value) {
640
+ if (!schema)
641
+ return value;
642
+ const result = schema.safeParse(value);
643
+ if (!result.success) {
644
+ // Output drift is a contract problem, not untrusted input — surface it as a
645
+ // validation error so a dotCMS response change shows up at the boundary rather
646
+ // than as a silent `undefined` downstream.
647
+ throw new errors.ValidationError(`Adapter result did not match its output schema: ${result.error.message}`, result.error.issues);
648
+ }
649
+ return result.data;
650
+ }
651
+ /**
652
+ * Build a typed, schema-validated adapter. Handlers get `(input, ctx)` — `input` already
653
+ * validated, `ctx.request` already auth-bound by the runtime.
654
+ *
655
+ * The returned value is a plain `Adapter` (the `methods` Map is populated with thunks that
656
+ * throw until `withContext` is called), so it can be wrapped (e.g. by an allow-list) before
657
+ * it reaches the runtime, exactly like the built-in `dotcmsAdapter`.
658
+ */
659
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
660
+ function defineAdapter(def) {
661
+ const schemas = {};
662
+ let modelExposable = true;
663
+ for (const [methodName, m] of Object.entries(def.methods)) {
664
+ schemas[methodName] = { description: m.description, input: m.input, output: m.output };
665
+ if (!m.output)
666
+ modelExposable = false;
667
+ }
668
+ const makeMethods = (ctx) => {
669
+ const methods = new Map();
670
+ for (const [methodName, m] of Object.entries(def.methods)) {
671
+ methods.set(methodName, {
672
+ name: methodName,
673
+ description: m.description,
674
+ parameters: [
675
+ {
676
+ name: 'input',
677
+ type: 'object',
678
+ description: m.description,
679
+ required: true
680
+ }
681
+ ],
682
+ async execute(...args) {
683
+ if (!ctx) {
684
+ throw new errors.ValidationError(`Adapter "${def.name}" was used without a bound context. ` +
685
+ `Call adapter.withContext(ctx) before running it.`);
686
+ }
687
+ const input = validateInput(methodName, m.input, args[0]);
688
+ const result = await m.handler(input, ctx);
689
+ return validateOutput(m.output, result);
690
+ }
691
+ });
692
+ }
693
+ return methods;
694
+ };
695
+ const base = {
696
+ name: def.name,
697
+ description: def.description,
698
+ version: def.version ?? '1.0.0',
699
+ methods: makeMethods(),
700
+ __defined: true,
701
+ __schemas: schemas,
702
+ modelExposable,
703
+ withContext(ctx) {
704
+ return {
705
+ name: def.name,
706
+ description: def.description,
707
+ version: def.version ?? '1.0.0',
708
+ methods: makeMethods(ctx)
709
+ };
710
+ }
711
+ };
712
+ return base;
713
+ }
714
+ /** Type guard: distinguishes a `defineAdapter` result from a hand-built `Adapter`. */
715
+ function isDefinedAdapter(adapter) {
716
+ return adapter.__defined === true;
717
+ }
718
+ /**
719
+ * Render a defined adapter's methods as JSON-Schema-ish tool definitions an LLM can consume.
720
+ * This is the bridge to MCP / tool-calling: the declared Zod schemas become the tool's
721
+ * input/output contract, replacing hand-written description prose.
722
+ *
723
+ * Only methods that declare an `output` schema are emitted — a method without one is treated
724
+ * as internal plumbing and withheld from the model.
725
+ */
726
+ function describeAdapterForLLM(adapter) {
727
+ const tools = [];
728
+ for (const [methodName, s] of Object.entries(adapter.__schemas)) {
729
+ if (!s.output)
730
+ continue; // internal-only method — not model-exposable
731
+ tools.push({
732
+ name: `${adapter.name}.${methodName}`,
733
+ description: s.description,
734
+ inputSchema: zod.z.toJSONSchema(s.input),
735
+ outputSchema: zod.z.toJSONSchema(s.output)
736
+ });
737
+ }
738
+ return tools;
739
+ }
740
+
741
+ /**
742
+ * Default hard cap on the string handed back to the model (~6k tokens). A depth-1/2
743
+ * `resolveRef` of even the largest schemas fits comfortably; a whole-`spec` dump does not.
744
+ */
745
+ const DEFAULT_MAX_CHARS = 25_000;
746
+ /**
747
+ * Render a {@link SandboxResult} into the single string a tool hands back to the model, and
748
+ * hard-cap its length so one query can't flood the context window.
749
+ *
750
+ * There is NO truncation anywhere else on the result path — whatever the model's code returns is
751
+ * stringified whole. This is the one place that bounds it. The cap is applied to the final
752
+ * combined string in BOTH the success and error branches (an `HttpError` body embedded in an
753
+ * error message can itself be huge). On truncation, a clear notice explains the cut and tells the
754
+ * model how to narrow the query rather than silently dropping data.
755
+ */
756
+ function formatSandboxResult(result, options) {
757
+ const maxChars = options?.maxChars ?? DEFAULT_MAX_CHARS;
758
+ let out;
759
+ if (!result.success) {
760
+ const errorMsg = result.error
761
+ ? `${result.error.name}: ${result.error.message}`
762
+ : 'Unknown error';
763
+ const logs = result.logs.length > 0 ? `\nLogs:\n${result.logs.join('\n')}` : '';
764
+ out = `Error: ${errorMsg}${logs}`;
765
+ }
766
+ else {
767
+ // Logs are built BEFORE the value is stringified. Serialization can fail (see
768
+ // `stringifyValue`), and when it did, the throw escaped this function with the logs
769
+ // still unattached — so every `console.log` the model had written to debug its code
770
+ // was discarded at the exact moment it became most useful.
771
+ const logs = result.logs.length > 0 ? `\n\n--- Logs ---\n${result.logs.join('\n')}` : '';
772
+ const value = typeof result.value === 'string' ? result.value : stringifyValue(result.value);
773
+ out = `${value}${logs}`;
774
+ }
775
+ if (out.length <= maxChars)
776
+ return out;
777
+ const hint = options?.truncationHint ? ` ${options.truncationHint}` : '';
778
+ return (out.slice(0, maxChars) +
779
+ `\n\n[output truncated at ${maxChars} of ${out.length} chars — refine the query: ` +
780
+ `select specific paths/fields, use pick()/first(), or resolve one schema at a time.${hint}]`);
781
+ }
782
+ /**
783
+ * `JSON.stringify` for a value that arrived over `postMessage`.
784
+ *
785
+ * The two serializers do NOT agree on what is representable. `postMessage` uses structured
786
+ * clone, which handles circular references and `BigInt`; JSON handles neither. So a worker
787
+ * could report `success: true`, the value could transfer intact, and stringifying it here
788
+ * would then throw `TypeError: Converting circular structure to JSON` — out of a function
789
+ * whose job is to REPORT the result. It presented as "the tool breaks only when my code
790
+ * succeeds", which is about the worst shape a failure can take.
791
+ *
792
+ * Circular references are not exotic here: building a tree from a flat folder or page list
793
+ * with parent back-pointers is the ordinary way to do it. They are replaced with a marker
794
+ * rather than dropped, so the model can see the shape it produced and why it could not be
795
+ * returned whole.
796
+ */
797
+ function stringifyValue(value) {
798
+ try {
799
+ return JSON.stringify(value, circularSafeReplacer(), 2) ?? String(value);
800
+ }
801
+ catch (error) {
802
+ const reason = error instanceof Error ? error.message : String(error);
803
+ return (`[value could not be serialized: ${reason}]\n` +
804
+ `The worker returned this value successfully, but it cannot be represented as JSON. ` +
805
+ `Return a plain-data projection instead — e.g. pick(items, ['id','title']) — rather ` +
806
+ `than the object graph itself.`);
807
+ }
808
+ }
809
+ /** A replacer that survives cycles and BigInt, the two structured-clone/JSON mismatches. */
810
+ function circularSafeReplacer() {
811
+ const seen = new WeakSet();
812
+ return function replacer(_key, value) {
813
+ if (typeof value === 'bigint') {
814
+ // BigInt has no JSON representation at all — stringify throws on it outright.
815
+ return `${value.toString()}n`;
816
+ }
817
+ if (typeof value === 'object' && value !== null) {
818
+ if (seen.has(value)) {
819
+ return '[Circular]';
820
+ }
821
+ seen.add(value);
822
+ }
823
+ return value;
824
+ };
825
+ }
826
+
827
+ exports.Executor = Executor;
828
+ exports.createExecutor = createExecutor;
829
+ exports.createWorkerSandbox = createWorkerSandbox;
830
+ exports.defineAdapter = defineAdapter;
831
+ exports.describeAdapterForLLM = describeAdapterForLLM;
832
+ exports.formatSandboxResult = formatSandboxResult;
833
+ exports.isDefinedAdapter = isDefinedAdapter;