@hydraharness/harness-code-runtime-worker-thread 0.1.1-rc.6

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.
package/lib/index.js ADDED
@@ -0,0 +1,941 @@
1
+ import { stripTypeScriptTypes } from "node:module";
2
+ import { Worker } from "node:worker_threads";
3
+ import { fileURLToPath } from "node:url";
4
+ import z from "@hydraharness/schemastery";
5
+ import { MAX_TIMER_DELAY_MS } from "@hydraharness/harness-timeout";
6
+ import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from "@hydraharness/harness-code-runtime";
7
+ import { snapshotJsonValue } from "@hydraharness/harness-session";
8
+ //#region lib/types/output-json.js
9
+ /** JSON string-prefix accounting for the outer-output ledger. @module @hydraharness/harness-code-runtime-worker-thread/output-json */
10
+ const intrinsicReflectApply$1 = Reflect.apply;
11
+ const intrinsicArrayIsArray$1 = Array.isArray;
12
+ const IntrinsicBuffer = Buffer;
13
+ const intrinsicBufferByteLength = Reflect.get(Buffer, "byteLength");
14
+ const intrinsicObjectCreate$1 = Object.create;
15
+ const intrinsicObjectDefineProperty$1 = Object.defineProperty;
16
+ const intrinsicObjectKeys$1 = Object.keys;
17
+ const intrinsicString = String;
18
+ const intrinsicStringCharCodeAt = Reflect.get(String.prototype, "charCodeAt");
19
+ const intrinsicStringCodePointAt = Reflect.get(String.prototype, "codePointAt");
20
+ const intrinsicStringSlice = Reflect.get(String.prototype, "slice");
21
+ /** Build a data descriptor that cannot inherit model-defined accessor fields. */
22
+ function dataDescriptor$1(value) {
23
+ const descriptor = intrinsicObjectCreate$1(null);
24
+ descriptor.value = value;
25
+ return descriptor;
26
+ }
27
+ /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
28
+ function defineEnumerableDataProperty$1(target, key, value) {
29
+ const descriptor = dataDescriptor$1(value);
30
+ descriptor.enumerable = true;
31
+ descriptor.configurable = true;
32
+ descriptor.writable = true;
33
+ intrinsicObjectDefineProperty$1(target, key, descriptor);
34
+ }
35
+ /** UTF-8 byte length through the module-captured Node intrinsic. */
36
+ function byteLength(text) {
37
+ return intrinsicReflectApply$1(intrinsicBufferByteLength, IntrinsicBuffer, [text, "utf8"]);
38
+ }
39
+ /** Append without consulting a model-mutated `Array.prototype`. */
40
+ function append$1(target, value) {
41
+ defineEnumerableDataProperty$1(target, target.length, value);
42
+ }
43
+ /** Pop without consulting a model-mutated `Array.prototype`. */
44
+ function takeLast$1(target) {
45
+ if (target.length === 0) return void 0;
46
+ const index = target.length - 1;
47
+ const value = target[index];
48
+ intrinsicObjectDefineProperty$1(target, "length", dataDescriptor$1(index));
49
+ return value;
50
+ }
51
+ /** One code-point-aligned character from a string. */
52
+ function characterAt(text, index) {
53
+ return intrinsicReflectApply$1(intrinsicStringSlice, text, [index, index + (intrinsicReflectApply$1(intrinsicStringCodePointAt, text, [index]) > 65535 ? 2 : 1)]);
54
+ }
55
+ /** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
56
+ function serializedCharacterBytes(character) {
57
+ if (character.length === 2) return 4;
58
+ if (character === "\"" || character === "\\") return 2;
59
+ const code = intrinsicReflectApply$1(intrinsicStringCharCodeAt, character, [0]);
60
+ if (code >= 55296 && code <= 57343) return 6;
61
+ if (code < 32) return code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
62
+ return byteLength(character);
63
+ }
64
+ /**
65
+ * Measure one JSON string without materializing its complete escaped form.
66
+ * @param text - the candidate string.
67
+ * @param maxBytes - largest serialized size the caller can admit.
68
+ * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
69
+ */
70
+ function jsonStringBytesUpTo(text, maxBytes) {
71
+ if (maxBytes < 2) return void 0;
72
+ let bytes = 2;
73
+ for (let index = 0; index < text.length;) {
74
+ const character = characterAt(text, index);
75
+ bytes += serializedCharacterBytes(character);
76
+ if (bytes > maxBytes) return void 0;
77
+ index += character.length;
78
+ }
79
+ return bytes;
80
+ }
81
+ /**
82
+ * Measure one lossless JSON value without allocating its serialized form.
83
+ * @param value - already validated lossless JSON.
84
+ * @param maxBytes - largest serialized size the caller can admit.
85
+ * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
86
+ */
87
+ function jsonValueBytesUpTo(value, maxBytes) {
88
+ let bytes = 0;
89
+ const add = (cost) => {
90
+ bytes += cost;
91
+ return bytes <= maxBytes;
92
+ };
93
+ const tasks = [{
94
+ kind: "value",
95
+ value
96
+ }];
97
+ for (let task = takeLast$1(tasks); task !== void 0; task = takeLast$1(tasks)) {
98
+ if (task.kind === "value") {
99
+ const current = task.value;
100
+ if (current === null) {
101
+ if (!add(4)) return void 0;
102
+ } else if (typeof current === "string") {
103
+ const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes);
104
+ if (stringBytes === void 0) return void 0;
105
+ bytes += stringBytes;
106
+ } else if (typeof current === "number") {
107
+ if (!add(byteLength(intrinsicString(current)))) return void 0;
108
+ } else if (typeof current === "boolean") {
109
+ if (!add(current ? 4 : 5)) return void 0;
110
+ } else if (intrinsicArrayIsArray$1(current)) {
111
+ if (!add(2)) return void 0;
112
+ if (current.length > 0) append$1(tasks, {
113
+ kind: "array",
114
+ value: current,
115
+ index: 0
116
+ });
117
+ } else {
118
+ if (!add(2)) return void 0;
119
+ const keys = intrinsicObjectKeys$1(current);
120
+ if (keys.length > 0) append$1(tasks, {
121
+ kind: "object",
122
+ value: current,
123
+ keys,
124
+ index: 0
125
+ });
126
+ }
127
+ continue;
128
+ }
129
+ if (task.index > 0 && !add(1)) return void 0;
130
+ if (task.kind === "array") {
131
+ const item = task.value[task.index];
132
+ if (item === void 0) return void 0;
133
+ if (task.index + 1 < task.value.length) append$1(tasks, {
134
+ ...task,
135
+ index: task.index + 1
136
+ });
137
+ append$1(tasks, {
138
+ kind: "value",
139
+ value: item
140
+ });
141
+ continue;
142
+ }
143
+ const key = task.keys[task.index];
144
+ /* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
145
+ if (key === void 0) return void 0;
146
+ const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes);
147
+ if (keyBytes === void 0) return void 0;
148
+ if (!add(keyBytes + 1)) return void 0;
149
+ const item = task.value[key];
150
+ if (item === void 0) return void 0;
151
+ if (task.index + 1 < task.keys.length) append$1(tasks, {
152
+ ...task,
153
+ index: task.index + 1
154
+ });
155
+ append$1(tasks, {
156
+ kind: "value",
157
+ value: item
158
+ });
159
+ }
160
+ return bytes;
161
+ }
162
+ /**
163
+ * Return the longest code-point-aligned prefix whose JSON string encoding,
164
+ * including its surrounding quotes, fits `maxBytes`.
165
+ *
166
+ * @param text - the candidate string.
167
+ * @param maxBytes - serialized JSON-string bytes available.
168
+ * @returns the fitting prefix, or an empty string when even useful content cannot fit.
169
+ */
170
+ function truncateJsonStringBytes(text, maxBytes) {
171
+ if (maxBytes < 2) return "";
172
+ let bytes = 2;
173
+ let end = 0;
174
+ for (let index = 0; index < text.length;) {
175
+ const character = characterAt(text, index);
176
+ const cost = serializedCharacterBytes(character);
177
+ if (bytes + cost > maxBytes) break;
178
+ bytes += cost;
179
+ end += character.length;
180
+ index += character.length;
181
+ }
182
+ return end === text.length ? text : intrinsicReflectApply$1(intrinsicStringSlice, text, [0, end]);
183
+ }
184
+ //#endregion
185
+ //#region lib/types/worker-json.js
186
+ /**
187
+ * Lossless-JSON snapshots for the dependency-free source worker closure.
188
+ * @module @hydraharness/harness-code-runtime-worker-thread/worker-json
189
+ */
190
+ const intrinsicFunctionToString = Reflect.get(Function.prototype, "toString");
191
+ const intrinsicReflectApply = Reflect.get(Reflect, "apply");
192
+ const IntrinsicError = Error;
193
+ const IntrinsicSet = Set;
194
+ const intrinsicArrayIsArray = Array.isArray;
195
+ const intrinsicArrayPrototype = Array.prototype;
196
+ const intrinsicNumberIsFinite = Number.isFinite;
197
+ const intrinsicNumberIsSafeInteger = Number.isSafeInteger;
198
+ const intrinsicObjectCreate = Object.create;
199
+ const intrinsicObjectDefineProperty = Object.defineProperty;
200
+ const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
201
+ const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf;
202
+ const intrinsicObjectHasOwn = Object.hasOwn;
203
+ const intrinsicObjectIs = Object.is;
204
+ const intrinsicObjectKeys = Object.keys;
205
+ const intrinsicObjectPrototype = Object.prototype;
206
+ const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, "propertyIsEnumerable");
207
+ const intrinsicReflectOwnKeys = Reflect.ownKeys;
208
+ const intrinsicSetAdd = Reflect.get(Set.prototype, "add");
209
+ Reflect.get(Set.prototype, "delete");
210
+ const intrinsicSetHas = Reflect.get(Set.prototype, "has");
211
+ /** Build a data descriptor that cannot inherit model-defined accessor fields. */
212
+ function dataDescriptor(value) {
213
+ const descriptor = intrinsicObjectCreate(null);
214
+ descriptor.value = value;
215
+ return descriptor;
216
+ }
217
+ /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
218
+ function defineEnumerableDataProperty(target, key, value) {
219
+ const descriptor = dataDescriptor(value);
220
+ descriptor.enumerable = true;
221
+ descriptor.configurable = true;
222
+ descriptor.writable = true;
223
+ intrinsicObjectDefineProperty(target, key, descriptor);
224
+ }
225
+ /** Append without consulting a model-mutated `Array.prototype`. */
226
+ function append(target, value) {
227
+ defineEnumerableDataProperty(target, target.length, value);
228
+ }
229
+ /** Pop without consulting a model-mutated `Array.prototype`. */
230
+ function takeLast(target) {
231
+ if (target.length === 0) return void 0;
232
+ const index = target.length - 1;
233
+ const value = target[index];
234
+ intrinsicObjectDefineProperty(target, "length", dataDescriptor(index));
235
+ return value;
236
+ }
237
+ /** Whether one captured-intrinsic Set contains a value. */
238
+ function setHas(target, value) {
239
+ return intrinsicReflectApply(intrinsicSetHas, target, [value]);
240
+ }
241
+ /** Add to one captured-intrinsic Set. */
242
+ function setAdd(target, value) {
243
+ intrinsicReflectApply(intrinsicSetAdd, target, [value]);
244
+ }
245
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
246
+ function hasIntrinsicConstructor(prototype, name) {
247
+ const constructor = intrinsicObjectGetOwnPropertyDescriptor(prototype, "constructor")?.value;
248
+ if (typeof constructor !== "function") return false;
249
+ try {
250
+ return constructor.name === name && constructor.prototype === prototype && intrinsicReflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+ /** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
256
+ function isForeignIntrinsicObjectPrototype(value) {
257
+ return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
258
+ }
259
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
260
+ function hasPlainArrayPrototype(value) {
261
+ const prototype = intrinsicObjectGetPrototypeOf(value);
262
+ if (prototype === intrinsicArrayPrototype) return true;
263
+ if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
264
+ const objectPrototype = intrinsicObjectGetPrototypeOf(prototype);
265
+ return typeof objectPrototype === "object" && objectPrototype !== null && isForeignIntrinsicObjectPrototype(objectPrototype);
266
+ }
267
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
268
+ function hasPlainObjectPrototype(value) {
269
+ const prototype = intrinsicObjectGetPrototypeOf(value);
270
+ return prototype === null || prototype === intrinsicObjectPrototype || typeof prototype === "object" && isForeignIntrinsicObjectPrototype(prototype);
271
+ }
272
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
273
+ function enumerableStringKeys(value) {
274
+ const keys = intrinsicReflectOwnKeys(value);
275
+ for (let index = 0; index < keys.length; index++) {
276
+ const key = keys[index];
277
+ if (typeof key !== "string" || !intrinsicReflectApply(intrinsicObjectPropertyIsEnumerable, value, [key])) return void 0;
278
+ }
279
+ return keys;
280
+ }
281
+ /**
282
+ * Flatten one validated JSON value for the worker-thread message port.
283
+ * @param value - the lossless JSON value to transport.
284
+ * @returns a pre-order token stream whose own nesting is bounded.
285
+ */
286
+ function encodeWorkerJson(value) {
287
+ const wire = [];
288
+ const pending = [value];
289
+ for (let current = takeLast(pending); current !== void 0; current = takeLast(pending)) {
290
+ if (current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string") {
291
+ append(wire, current);
292
+ continue;
293
+ }
294
+ if (intrinsicArrayIsArray(current)) {
295
+ append(wire, {
296
+ kind: "array",
297
+ length: current.length
298
+ });
299
+ for (let index = current.length - 1; index >= 0; index--) {
300
+ const item = current[index];
301
+ if (item === void 0) throw new IntrinsicError("cannot encode a sparse JSON array");
302
+ append(pending, item);
303
+ }
304
+ continue;
305
+ }
306
+ const keys = intrinsicObjectKeys(current);
307
+ append(wire, {
308
+ kind: "object",
309
+ keys
310
+ });
311
+ for (let index = keys.length - 1; index >= 0; index--) {
312
+ const key = keys[index];
313
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
314
+ if (key === void 0) throw new IntrinsicError("cannot encode a missing JSON object key");
315
+ const item = current[key];
316
+ if (item === void 0) throw new IntrinsicError("cannot encode an undefined JSON object property");
317
+ append(pending, item);
318
+ }
319
+ }
320
+ return wire;
321
+ }
322
+ /** Whether an array contains exactly its dense indexed slots and `length`. */
323
+ function isDenseArray(value) {
324
+ if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false;
325
+ for (let index = 0; index < value.length; index++) if (!intrinsicObjectHasOwn(value, index)) return false;
326
+ return true;
327
+ }
328
+ /** Whether one exact string-key list contains a key, without consulting its prototype. */
329
+ function keysContain(keys, expected) {
330
+ for (let index = 0; index < keys.length; index++) if (keys[index] === expected) return true;
331
+ return false;
332
+ }
333
+ /** Return one exact container marker, or reject any extra/missing fields. */
334
+ function containerToken(value) {
335
+ if (intrinsicArrayIsArray(value) || !hasPlainObjectPrototype(value)) return void 0;
336
+ const keys = enumerableStringKeys(value);
337
+ if (keys === void 0) return void 0;
338
+ const token = value;
339
+ if (token.kind === "array") {
340
+ if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "length")) return void 0;
341
+ const length = token.length;
342
+ return typeof length === "number" && intrinsicNumberIsSafeInteger(length) && length >= 0 ? {
343
+ kind: "array",
344
+ length
345
+ } : void 0;
346
+ }
347
+ if (token.kind === "object") {
348
+ if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "keys")) return void 0;
349
+ const objectKeys = token.keys;
350
+ if (!intrinsicArrayIsArray(objectKeys) || !isDenseArray(objectKeys)) return void 0;
351
+ const unique = new IntrinsicSet();
352
+ const normalizedKeys = [];
353
+ const objectKeyValues = objectKeys;
354
+ for (let index = 0; index < objectKeyValues.length; index++) {
355
+ const key = objectKeyValues[index];
356
+ if (typeof key !== "string" || setHas(unique, key)) return void 0;
357
+ setAdd(unique, key);
358
+ append(normalizedKeys, key);
359
+ }
360
+ return {
361
+ kind: "object",
362
+ keys: normalizedKeys
363
+ };
364
+ }
365
+ }
366
+ /**
367
+ * Rebuild one lossless JSON value from the flat worker-thread wire format.
368
+ * Malformed or incomplete traffic returns `undefined`; traversal is iterative
369
+ * and therefore independent of the transported value's application depth.
370
+ * @param input - untrusted message-port payload.
371
+ * @returns the detached JSON value, or `undefined` when the wire is invalid.
372
+ */
373
+ function decodeWorkerJson(input) {
374
+ try {
375
+ if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return void 0;
376
+ const wire = input;
377
+ const frames = [];
378
+ let root;
379
+ let rootAssigned = false;
380
+ const attach = (value) => {
381
+ const parent = frames[frames.length - 1];
382
+ if (!parent) {
383
+ if (rootAssigned) return false;
384
+ root = value;
385
+ rootAssigned = true;
386
+ return true;
387
+ }
388
+ /* v8 ignore next -- completed frames are popped before another token can attach. */
389
+ if (parent.index >= (parent.kind === "array" ? parent.length : parent.keys.length)) return false;
390
+ if (parent.kind === "array") append(parent.target, value);
391
+ else {
392
+ const key = parent.keys[parent.index];
393
+ /* v8 ignore next -- object frames are built from validated keys and their exact length. */
394
+ if (key === void 0) return false;
395
+ defineEnumerableDataProperty(parent.target, key, value);
396
+ }
397
+ parent.index += 1;
398
+ return true;
399
+ };
400
+ for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
401
+ const token = wire[tokenIndex];
402
+ let value;
403
+ let frame;
404
+ if (token === null || typeof token === "boolean" || typeof token === "string") value = token;
405
+ else if (typeof token === "number") {
406
+ if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return void 0;
407
+ value = token;
408
+ } else {
409
+ if (typeof token !== "object") return void 0;
410
+ const marker = containerToken(token);
411
+ if (!marker) return void 0;
412
+ const remainingTokens = wire.length - tokenIndex - 1;
413
+ if (marker.kind === "array") {
414
+ if (marker.length > remainingTokens) return void 0;
415
+ const target = [];
416
+ value = target;
417
+ if (marker.length > 0) frame = {
418
+ kind: "array",
419
+ target,
420
+ length: marker.length,
421
+ index: 0
422
+ };
423
+ } else {
424
+ if (marker.keys.length > remainingTokens) return void 0;
425
+ const target = {};
426
+ value = target;
427
+ if (marker.keys.length > 0) frame = {
428
+ kind: "object",
429
+ target,
430
+ keys: marker.keys,
431
+ index: 0
432
+ };
433
+ }
434
+ }
435
+ if (!attach(value)) return void 0;
436
+ if (frame) append(frames, frame);
437
+ while (frames.length > 0) {
438
+ const current = frames[frames.length - 1];
439
+ /* v8 ignore next -- the loop condition guarantees a final frame. */
440
+ if (current === void 0) break;
441
+ if (current.index < (current.kind === "array" ? current.length : current.keys.length)) break;
442
+ takeLast(frames);
443
+ }
444
+ }
445
+ return frames.length === 0 ? root : void 0;
446
+ } catch {
447
+ return;
448
+ }
449
+ }
450
+ //#endregion
451
+ //#region lib/types/index.js
452
+ /**
453
+ * Worker-thread code runtime: a fresh worker runs each host-type-stripped TypeScript program
454
+ * and bridges bindings over its message port. This is containment, not a security boundary:
455
+ * model code has bash-equivalent trust despite an empty environment, a heap cap, measured
456
+ * event-loop busy-time and wall-time budgets, and termination that also stops synchronous loops.
457
+ * @module @hydraharness/harness-code-runtime-worker-thread
458
+ */
459
+ /**
460
+ * How often the host samples the worker's event-loop utilization for the
461
+ * `computeMs` budget. An internal cadence, not config: the only effect of
462
+ * the interval is budget-expiry granularity (a run can overshoot by up to
463
+ * one interval), and nothing a deployment could tune here improves that
464
+ * without burning host CPU.
465
+ */
466
+ const ELU_POLL_INTERVAL_MS = 25;
467
+ /** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */
468
+ const MIN_OUTPUT_BYTES = 4;
469
+ /**
470
+ * The seam's language-portable identifier subset (see
471
+ * `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same
472
+ * namespace list must be usable against every backend regardless of language.
473
+ */
474
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
475
+ /**
476
+ * The shell a program is wrapped in for the type-strip, matching the
477
+ * grammatical context it will execute in (an async function body, where
478
+ * top-level `return` and `await` are legal — a bare module parse would
479
+ * reject the `return`). Strip mode is position-preserving (removed syntax
480
+ * becomes whitespace, nothing shifts), so the wrapper survives the strip
481
+ * byte-identical and the body slices back out with the model's own
482
+ * line/column positions intact.
483
+ */
484
+ const STRIP_WRAP = {
485
+ prefix: "async function __hydra_program__() {\n",
486
+ suffix: "\n}"
487
+ };
488
+ /**
489
+ * The worker entry path. Source runs unbuilt (`src/worker.ts`, loadable
490
+ * directly on this repo's Node range via native type stripping — the file
491
+ * is erasable-only with type-only relative imports); the built package
492
+ * ships it as a sibling CommonJS bundle (`lib/worker.cjs`, its own tsdown
493
+ * entry) because pkg's VFS Worker hook compiles string-path entries as
494
+ * CommonJS.
495
+ * The URL *pathname*'s extension says which world this module is in —
496
+ * pathname, because dev-time module runners (vitest) may suffix
497
+ * `import.meta.url` with a query string; relative resolution drops it. Worker
498
+ * receives a filesystem string so pkg's VFS Worker hook can resolve it.
499
+ */
500
+ /* v8 ignore next -- the './worker.cjs' arm is the built-lib world, unreachable unbuilt by construction; the built-lib e2e pins it. */
501
+ const WORKER_PATH = fileURLToPath(new URL(new URL(import.meta.url).pathname.endsWith(".ts") ? "./worker.ts" : "./worker.cjs", import.meta.url));
502
+ /** Render an unknown thrown value as a message, `Error` or not. */
503
+ function messageOf(error) {
504
+ return error instanceof Error ? error.message : String(error);
505
+ }
506
+ /** Resolve after a worker pipe emits all queued data, or closes/errors during termination. */
507
+ function waitForPipeDrain(stream) {
508
+ if (stream.readableEnded || stream.destroyed) return Promise.resolve();
509
+ return new Promise((resolve) => {
510
+ const done = () => {
511
+ stream.off("end", done);
512
+ stream.off("close", done);
513
+ stream.off("error", done);
514
+ resolve();
515
+ };
516
+ stream.once("end", done);
517
+ stream.once("close", done);
518
+ stream.once("error", done);
519
+ /* v8 ignore next -- this race cannot be scheduled deterministically between the adjacent state check and listener registration. */
520
+ if (stream.readableEnded || stream.destroyed) done();
521
+ });
522
+ }
523
+ /**
524
+ * Runtime shape gate for inbound port traffic. The peer runs MODEL CODE and
525
+ * can post anything — `null`, primitives, objects with poisoned fields — so
526
+ * the compile-time `WorkerToHost` type means nothing here: everything is
527
+ * re-validated and REBUILT field by field (a forged extra field never rides
528
+ * along; a non-number call id can never be echoed into a reply). Junk returns
529
+ * `undefined` and is dropped — a throw in the host's `message` listener would
530
+ * crash the host process.
531
+ */
532
+ function parseWorkerMessage(raw) {
533
+ if (typeof raw !== "object" || raw === null) return void 0;
534
+ const m = raw;
535
+ switch (m.type) {
536
+ case "call":
537
+ if (typeof m.id !== "number" || typeof m.global !== "string" || typeof m.name !== "string") return void 0;
538
+ return {
539
+ type: "call",
540
+ id: m.id,
541
+ global: m.global,
542
+ name: m.name,
543
+ args: m.args
544
+ };
545
+ case "log":
546
+ if (typeof m.text !== "string") return void 0;
547
+ return {
548
+ type: "log",
549
+ text: m.text
550
+ };
551
+ case "output-limit": return { type: "output-limit" };
552
+ case "done": {
553
+ if (m.error === void 0) return {
554
+ type: "done",
555
+ ...m.value !== void 0 ? { value: m.value } : {}
556
+ };
557
+ const error = m.error;
558
+ if (typeof error !== "object" || error === null) return void 0;
559
+ const { kind, message } = error;
560
+ if (kind !== "exception" && kind !== "invalid-output" && kind !== "output-limit" || typeof message !== "string") return void 0;
561
+ return {
562
+ type: "done",
563
+ error: {
564
+ kind,
565
+ message
566
+ }
567
+ };
568
+ }
569
+ default: return;
570
+ }
571
+ }
572
+ /** One run's combined outer-output ledger; binding values never enter it. */
573
+ var OutputLedger = class {
574
+ maxBytes;
575
+ bytes = 2;
576
+ entries = 0;
577
+ constructor(maxBytes) {
578
+ this.maxBytes = maxBytes;
579
+ }
580
+ /** Admit one exact log entry, or report that the hard cap was crossed. */
581
+ admit(text, sink) {
582
+ const separatorBytes = this.entries > 0 ? 1 : 0;
583
+ const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes);
584
+ if (stringBytes === void 0) return false;
585
+ this.bytes += stringBytes + separatorBytes;
586
+ this.entries += 1;
587
+ sink.push(text);
588
+ return true;
589
+ }
590
+ /** Finalize a successful absent-or-JSON completion against the combined cap. */
591
+ success(logs, value) {
592
+ if (value !== void 0 && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === void 0) return this.limit(logs);
593
+ return {
594
+ logs,
595
+ ...value !== void 0 ? { value } : {}
596
+ };
597
+ }
598
+ /** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
599
+ failure(logs, error) {
600
+ if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === void 0) return this.limit(logs);
601
+ return {
602
+ logs,
603
+ error
604
+ };
605
+ }
606
+ /** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
607
+ limit(logs) {
608
+ const fullMessage = `outer output exceeded ${this.maxBytes} bytes`;
609
+ const messageBytes = fullMessage.length + 2;
610
+ const retained = [];
611
+ let retainedBytes = 2;
612
+ const logBudget = this.maxBytes - messageBytes;
613
+ for (const text of logs) {
614
+ const separatorBytes = retained.length > 0 ? 1 : 0;
615
+ const availableBytes = logBudget - retainedBytes - separatorBytes;
616
+ const stringBytes = jsonStringBytesUpTo(text, availableBytes);
617
+ if (stringBytes !== void 0) {
618
+ retained.push(text);
619
+ retainedBytes += stringBytes + separatorBytes;
620
+ continue;
621
+ }
622
+ const prefix = truncateJsonStringBytes(text, availableBytes);
623
+ if (prefix.length > 0) {
624
+ const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes);
625
+ /* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
626
+ if (prefixBytes === void 0) throw new Error("output ledger produced an oversized log prefix");
627
+ retained.push(prefix);
628
+ retainedBytes += prefixBytes + separatorBytes;
629
+ }
630
+ break;
631
+ }
632
+ return {
633
+ logs: retained,
634
+ error: {
635
+ kind: "output-limit",
636
+ message: truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
637
+ }
638
+ };
639
+ }
640
+ };
641
+ /**
642
+ * The shipped {@link CodeRuntime} backend (`ctx.codeRuntime`). Registers as
643
+ * the `codeRuntime` service; every cap comes from validated config. See the
644
+ * module doc for the containment model and the Service Definition's class JSDoc for
645
+ * the contract this implements (error-as-field, hostile-peer port,
646
+ * no cross-run state, dispose to quiescence).
647
+ */
648
+ var WorkerThreadCodeRuntime = class extends CodeRuntime {
649
+ static Config = z.object({
650
+ computeMs: z.number().default(6e4),
651
+ maxWallMs: z.number().default(6e5),
652
+ maxOutputBytes: z.number().default(67108864),
653
+ maxOldGenerationSizeMb: z.number().default(512)
654
+ });
655
+ language = "typescript";
656
+ isolation = "worker-thread";
657
+ config;
658
+ live = /* @__PURE__ */ new Set();
659
+ disposed = false;
660
+ constructor(ctx, config) {
661
+ super(ctx);
662
+ this.config = config;
663
+ for (const [key, value] of Object.entries(this.config)) if (!(Number.isFinite(value) && value > 0)) throw new Error(`hydra-code-runtime-worker-thread: config.${key} must be a positive number, got ${String(value)}`);
664
+ if (!Number.isSafeInteger(this.config.maxOutputBytes) || this.config.maxOutputBytes < MIN_OUTPUT_BYTES) throw new Error(`hydra-code-runtime-worker-thread: config.maxOutputBytes must be a safe integer of at least ${MIN_OUTPUT_BYTES}, got ${String(this.config.maxOutputBytes)}`);
665
+ if (this.config.maxWallMs > MAX_TIMER_DELAY_MS) throw new Error(`hydra-code-runtime-worker-thread: config.maxWallMs must be at most ${MAX_TIMER_DELAY_MS} (Node clamps a longer setTimeout delay to 1ms), got ${String(this.config.maxWallMs)}`);
666
+ ctx.effect(() => () => this.teardown(), "worker code-runtime teardown");
667
+ }
668
+ /**
669
+ * Dispose to quiescence: mark the service unusable, fail every in-flight
670
+ * run as aborted, and AWAIT each worker's exit so no worker outlives the
671
+ * fiber.
672
+ */
673
+ async teardown() {
674
+ this.disposed = true;
675
+ const runs = [...this.live];
676
+ for (const run of runs) run.settle({
677
+ kind: "abort",
678
+ message: "runtime disposed"
679
+ });
680
+ await Promise.all(runs.map((run) => run.finished));
681
+ }
682
+ /**
683
+ * Execute one program in a fresh worker. Program outcomes — including a
684
+ * type-strip syntax error, which never spawns a worker — resolve with
685
+ * `result.error`; the method rejects only for Service Definition contract misuse (a disposed
686
+ * runtime, an invalid binding namespace).
687
+ * @param request - the program, its bindings, and the abort signal.
688
+ * @returns the run's outcome per the seam contract.
689
+ */
690
+ async run(request) {
691
+ if (this.disposed) throw new Error("hydra-code-runtime-worker-thread: run() after disposal");
692
+ const bindings = this.validateBindings(request);
693
+ if (request.signal?.aborted) return this.failureBeforeWorker({
694
+ kind: "abort",
695
+ message: String(request.signal.reason)
696
+ });
697
+ let code;
698
+ try {
699
+ const stripped = stripTypeScriptTypes(STRIP_WRAP.prefix + request.program + STRIP_WRAP.suffix);
700
+ code = stripped.slice(STRIP_WRAP.prefix.length, stripped.length - STRIP_WRAP.suffix.length);
701
+ } catch (error) {
702
+ return this.failureBeforeWorker({
703
+ kind: "exception",
704
+ message: messageOf(error)
705
+ });
706
+ }
707
+ return await this.execute(request, code, bindings);
708
+ }
709
+ /** Apply the outer-output ledger to failures that occur before a worker owns one. */
710
+ failureBeforeWorker(error) {
711
+ return new OutputLedger(this.config.maxOutputBytes).failure([], error);
712
+ }
713
+ /** Reject malformed binding globals or typed-error declarations as Service Definition contract misuse. */
714
+ validateBindings(request) {
715
+ const bindings = /* @__PURE__ */ new Map();
716
+ for (const namespace of request.bindings) {
717
+ if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) throw new Error(`hydra-code-runtime-worker-thread: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`);
718
+ if (RESERVED_BINDING_GLOBALS.has(namespace.global)) throw new Error(`hydra-code-runtime-worker-thread: reserved binding global ${JSON.stringify(namespace.global)}`);
719
+ if (bindings.has(namespace.global)) throw new Error(`hydra-code-runtime-worker-thread: duplicate binding global ${JSON.stringify(namespace.global)}`);
720
+ bindings.set(namespace.global, namespace);
721
+ }
722
+ const errorClassNames = /* @__PURE__ */ new Set();
723
+ for (const namespace of request.bindings) {
724
+ const descriptor = namespace.errorClass;
725
+ if (!descriptor) continue;
726
+ if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) throw new Error(`hydra-code-runtime-worker-thread: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`);
727
+ if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) throw new Error(`hydra-code-runtime-worker-thread: reserved binding global ${JSON.stringify(descriptor.name)}`);
728
+ if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) throw new Error(`hydra-code-runtime-worker-thread: duplicate injected global ${JSON.stringify(descriptor.name)}`);
729
+ const member = descriptor.memberNameProperty;
730
+ if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) throw new Error(`hydra-code-runtime-worker-thread: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`);
731
+ errorClassNames.add(descriptor.name);
732
+ }
733
+ return bindings;
734
+ }
735
+ /** Spawn the worker for one validated, type-stripped run and drive it to settlement. */
736
+ execute(request, code, bindings) {
737
+ const worker = new Worker(WORKER_PATH, {
738
+ workerData: {
739
+ code,
740
+ namespaces: [...bindings].map(([global, namespace]) => ({
741
+ global,
742
+ names: Object.keys(namespace.functions),
743
+ ...namespace.errorClass ? { errorClass: namespace.errorClass } : {}
744
+ })),
745
+ maxOutputBytes: this.config.maxOutputBytes
746
+ },
747
+ env: {},
748
+ execArgv: [],
749
+ resourceLimits: { maxOldGenerationSizeMb: this.config.maxOldGenerationSizeMb },
750
+ stdout: true,
751
+ stderr: true
752
+ });
753
+ return new Promise((resolve) => {
754
+ let settled = false;
755
+ const answered = /* @__PURE__ */ new Set();
756
+ const logs = [];
757
+ const strayLogs = [];
758
+ const output = new OutputLedger(this.config.maxOutputBytes);
759
+ let terminalOverride;
760
+ const captureStray = (chunk) => {
761
+ /* v8 ignore next -- a second post-overflow chunk races immediate worker termination; the first overflow path is covered. */
762
+ if (terminalOverride !== void 0) return;
763
+ const text = chunk.toString("utf8");
764
+ if (!output.admit(text, strayLogs)) {
765
+ const limited = output.limit([
766
+ ...logs,
767
+ ...strayLogs,
768
+ text
769
+ ]);
770
+ terminalOverride = limited;
771
+ finish(limited);
772
+ }
773
+ };
774
+ worker.stdout.on("data", captureStray);
775
+ worker.stderr.on("data", captureStray);
776
+ let finishResolve;
777
+ const finished = new Promise((done) => {
778
+ finishResolve = done;
779
+ });
780
+ const finish = (finalize) => {
781
+ if (settled) return;
782
+ settled = true;
783
+ clearInterval(eluTimer);
784
+ clearTimeout(wallTimer);
785
+ request.signal?.removeEventListener("abort", onAbort);
786
+ this.live.delete(live);
787
+ new Promise((resume) => {
788
+ setImmediate(resume);
789
+ }).then(async () => {
790
+ const stdoutDrained = waitForPipeDrain(worker.stdout);
791
+ const stderrDrained = waitForPipeDrain(worker.stderr);
792
+ await Promise.all([
793
+ worker.terminate(),
794
+ stdoutDrained,
795
+ stderrDrained
796
+ ]);
797
+ const result = terminalOverride ?? (typeof finalize === "function" ? finalize() : finalize);
798
+ finishResolve();
799
+ resolve(result);
800
+ });
801
+ };
802
+ const onDone = (message) => {
803
+ if (message.type !== "done") return;
804
+ if (message.error) {
805
+ const error = message.error;
806
+ finish(() => output.failure([...logs, ...strayLogs], error));
807
+ return;
808
+ }
809
+ if (message.value === void 0) {
810
+ finish(() => output.success([...logs, ...strayLogs]));
811
+ return;
812
+ }
813
+ const value = decodeWorkerJson(message.value);
814
+ if (value === void 0) finish(() => output.failure([...logs, ...strayLogs], {
815
+ kind: "invalid-output",
816
+ message: "program completion must be lossless JSON"
817
+ }));
818
+ else finish(() => output.success([...logs, ...strayLogs], value));
819
+ };
820
+ const onCall = (message) => {
821
+ if (message.type !== "call" || settled) return;
822
+ if (answered.has(message.id)) return;
823
+ answered.add(message.id);
824
+ const reply = (payload) => {
825
+ if (settled) return;
826
+ worker.postMessage(payload);
827
+ };
828
+ const record = bindings.get(message.global)?.functions;
829
+ const fn = record && Object.hasOwn(record, message.name) ? record[message.name] : void 0;
830
+ if (typeof fn !== "function") {
831
+ reply({
832
+ type: "reply",
833
+ id: message.id,
834
+ ok: false,
835
+ message: `unknown binding ${JSON.stringify(`${message.global}.${message.name}`)}`
836
+ });
837
+ return;
838
+ }
839
+ const args = decodeWorkerJson(message.args);
840
+ if (args === void 0) {
841
+ reply({
842
+ type: "reply",
843
+ id: message.id,
844
+ ok: false,
845
+ message: "binding arguments must be lossless JSON"
846
+ });
847
+ return;
848
+ }
849
+ (async () => {
850
+ try {
851
+ const resolved = await fn(args);
852
+ let value;
853
+ try {
854
+ value = snapshotJsonValue(resolved);
855
+ } catch {
856
+ value = void 0;
857
+ }
858
+ if (value === void 0) reply({
859
+ type: "reply",
860
+ id: message.id,
861
+ ok: false,
862
+ message: "binding resolution must be lossless JSON"
863
+ });
864
+ else reply({
865
+ type: "reply",
866
+ id: message.id,
867
+ ok: true,
868
+ value: encodeWorkerJson(value)
869
+ });
870
+ } catch (error) {
871
+ reply({
872
+ type: "reply",
873
+ id: message.id,
874
+ ok: false,
875
+ message: messageOf(error)
876
+ });
877
+ }
878
+ })();
879
+ };
880
+ worker.on("message", (raw) => {
881
+ const message = parseWorkerMessage(raw);
882
+ if (!message) return;
883
+ if (message.type === "log" && !settled && !output.admit(message.text, logs)) {
884
+ finish(output.limit([
885
+ ...logs,
886
+ ...strayLogs,
887
+ message.text
888
+ ]));
889
+ return;
890
+ }
891
+ if (message.type === "output-limit" && !settled) {
892
+ finish(output.limit([...logs, ...strayLogs]));
893
+ return;
894
+ }
895
+ onCall(message);
896
+ onDone(message);
897
+ });
898
+ worker.on("error", (error) => {
899
+ finish(() => output.failure([...logs, ...strayLogs], {
900
+ kind: "worker-exit",
901
+ message: `worker error: ${error.message}`
902
+ }));
903
+ });
904
+ worker.on("exit", (exitCode) => {
905
+ finish(() => output.failure([...logs, ...strayLogs], {
906
+ kind: "worker-exit",
907
+ message: `worker exited with code ${exitCode} before completing`
908
+ }));
909
+ });
910
+ const eluTimer = setInterval(() => {
911
+ if (worker.performance.eventLoopUtilization().active > this.config.computeMs) finish(() => output.failure([...logs, ...strayLogs], {
912
+ kind: "timeout",
913
+ message: `compute budget exhausted (${this.config.computeMs}ms busy)`
914
+ }));
915
+ }, ELU_POLL_INTERVAL_MS);
916
+ const wallTimer = setTimeout(() => {
917
+ finish(() => output.failure([...logs, ...strayLogs], {
918
+ kind: "timeout",
919
+ message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)`
920
+ }));
921
+ }, this.config.maxWallMs);
922
+ const onAbort = () => {
923
+ finish(() => output.failure([...logs, ...strayLogs], {
924
+ kind: "abort",
925
+ message: String(request.signal?.reason)
926
+ }));
927
+ };
928
+ request.signal?.addEventListener("abort", onAbort, { once: true });
929
+ const live = {
930
+ worker,
931
+ finished,
932
+ settle: (failure) => {
933
+ finish(() => output.failure([...logs, ...strayLogs], failure));
934
+ }
935
+ };
936
+ this.live.add(live);
937
+ });
938
+ }
939
+ };
940
+ //#endregion
941
+ export { WorkerThreadCodeRuntime, WorkerThreadCodeRuntime as default };