@deepseek-ai/dsh-ptc-runtime-node 0.1.6-alpha.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.
package/lib/index.js ADDED
@@ -0,0 +1,1178 @@
1
+ import { stripTypeScriptTypes } from "node:module";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, PtcRuntime, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from "@deepseek-ai/dsh-ptc-runtime";
5
+ import { MAX_TIMER_DELAY_MS, clampTimeout } from "@deepseek-ai/dsh-timeout";
6
+ import { SandboxUnavailableError, classifyRunnerFailure, isRunnerSpawnFailure } from "@deepseek-ai/dsh-sandbox";
7
+ import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
8
+ import { Buffer as Buffer$1 } from "node:buffer";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ //#region lib/types/bindings.js
11
+ /** Validate and capture the binding names available to one program. */
12
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+ /**
14
+ * Reject unusable namespaces before starting a process.
15
+ * @param request - Host-owned binding declarations.
16
+ * @returns Namespaces keyed by their declared global.
17
+ */
18
+ function validateBindings(request) {
19
+ const bindings = /* @__PURE__ */ new Map();
20
+ for (const namespace of request.bindings) {
21
+ if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) throw new Error(`dsh-ptc-runtime-node: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`);
22
+ if (RESERVED_BINDING_GLOBALS.has(namespace.global)) throw new Error(`dsh-ptc-runtime-node: reserved binding global ${JSON.stringify(namespace.global)}`);
23
+ if (bindings.has(namespace.global)) throw new Error(`dsh-ptc-runtime-node: duplicate binding global ${JSON.stringify(namespace.global)}`);
24
+ bindings.set(namespace.global, namespace);
25
+ }
26
+ const errorClassNames = /* @__PURE__ */ new Set();
27
+ for (const namespace of request.bindings) {
28
+ const descriptor = namespace.errorClass;
29
+ if (!descriptor) continue;
30
+ if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) throw new Error(`dsh-ptc-runtime-node: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`);
31
+ if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) throw new Error(`dsh-ptc-runtime-node: reserved binding global ${JSON.stringify(descriptor.name)}`);
32
+ if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) throw new Error(`dsh-ptc-runtime-node: duplicate injected global ${JSON.stringify(descriptor.name)}`);
33
+ const member = descriptor.memberNameProperty;
34
+ if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) throw new Error(`dsh-ptc-runtime-node: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`);
35
+ errorClassNames.add(descriptor.name);
36
+ }
37
+ return bindings;
38
+ }
39
+ //#endregion
40
+ //#region lib/types/output-json.js
41
+ /** JSON string-prefix accounting for the outer-output ledger. @module @deepseek-ai/dsh-ptc-runtime-node/output-json */
42
+ const intrinsicReflectApply$1 = Reflect.apply;
43
+ const intrinsicArrayIsArray$1 = Array.isArray;
44
+ const IntrinsicBuffer = Buffer;
45
+ const intrinsicBufferByteLength = Reflect.get(Buffer, "byteLength");
46
+ const intrinsicObjectCreate$1 = Object.create;
47
+ const intrinsicObjectDefineProperty$1 = Object.defineProperty;
48
+ const intrinsicObjectKeys$1 = Object.keys;
49
+ const intrinsicString = String;
50
+ const intrinsicStringCharCodeAt = Reflect.get(String.prototype, "charCodeAt");
51
+ const intrinsicStringCodePointAt = Reflect.get(String.prototype, "codePointAt");
52
+ const intrinsicStringSlice = Reflect.get(String.prototype, "slice");
53
+ /** Build a data descriptor that cannot inherit model-defined accessor fields. */
54
+ function dataDescriptor$1(value) {
55
+ const descriptor = intrinsicObjectCreate$1(null);
56
+ descriptor.value = value;
57
+ return descriptor;
58
+ }
59
+ /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
60
+ function defineEnumerableDataProperty$1(target, key, value) {
61
+ const descriptor = dataDescriptor$1(value);
62
+ descriptor.enumerable = true;
63
+ descriptor.configurable = true;
64
+ descriptor.writable = true;
65
+ intrinsicObjectDefineProperty$1(target, key, descriptor);
66
+ }
67
+ /** UTF-8 byte length through the module-captured Node intrinsic. */
68
+ function byteLength(text) {
69
+ return intrinsicReflectApply$1(intrinsicBufferByteLength, IntrinsicBuffer, [text, "utf8"]);
70
+ }
71
+ /** Append without consulting a model-mutated `Array.prototype`. */
72
+ function append$1(target, value) {
73
+ defineEnumerableDataProperty$1(target, target.length, value);
74
+ }
75
+ /** Pop without consulting a model-mutated `Array.prototype`. */
76
+ function takeLast$1(target) {
77
+ if (target.length === 0) return void 0;
78
+ const index = target.length - 1;
79
+ const value = target[index];
80
+ intrinsicObjectDefineProperty$1(target, "length", dataDescriptor$1(index));
81
+ return value;
82
+ }
83
+ /** One code-point-aligned character from a string. */
84
+ function characterAt(text, index) {
85
+ return intrinsicReflectApply$1(intrinsicStringSlice, text, [index, index + (intrinsicReflectApply$1(intrinsicStringCodePointAt, text, [index]) > 65535 ? 2 : 1)]);
86
+ }
87
+ /** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
88
+ function serializedCharacterBytes(character) {
89
+ if (character.length === 2) return 4;
90
+ if (character === "\"" || character === "\\") return 2;
91
+ const code = intrinsicReflectApply$1(intrinsicStringCharCodeAt, character, [0]);
92
+ if (code >= 55296 && code <= 57343) return 6;
93
+ if (code < 32) return code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
94
+ return byteLength(character);
95
+ }
96
+ /**
97
+ * Measure one JSON string without materializing its complete escaped form.
98
+ * @param text - the candidate string.
99
+ * @param maxBytes - largest serialized size the caller can admit.
100
+ * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
101
+ */
102
+ function jsonStringBytesUpTo(text, maxBytes) {
103
+ if (maxBytes < 2) return void 0;
104
+ let bytes = 2;
105
+ for (let index = 0; index < text.length;) {
106
+ const character = characterAt(text, index);
107
+ bytes += serializedCharacterBytes(character);
108
+ if (bytes > maxBytes) return void 0;
109
+ index += character.length;
110
+ }
111
+ return bytes;
112
+ }
113
+ /**
114
+ * Measure one lossless JSON value without allocating its serialized form.
115
+ * @param value - already validated lossless JSON.
116
+ * @param maxBytes - largest serialized size the caller can admit.
117
+ * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
118
+ */
119
+ function jsonValueBytesUpTo(value, maxBytes) {
120
+ let bytes = 0;
121
+ const add = (cost) => {
122
+ bytes += cost;
123
+ return bytes <= maxBytes;
124
+ };
125
+ const tasks = [{
126
+ kind: "value",
127
+ value
128
+ }];
129
+ for (let task = takeLast$1(tasks); task !== void 0; task = takeLast$1(tasks)) {
130
+ if (task.kind === "value") {
131
+ const current = task.value;
132
+ if (current === null) {
133
+ if (!add(4)) return void 0;
134
+ } else if (typeof current === "string") {
135
+ const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes);
136
+ if (stringBytes === void 0) return void 0;
137
+ bytes += stringBytes;
138
+ } else if (typeof current === "number") {
139
+ if (!add(byteLength(intrinsicString(current)))) return void 0;
140
+ } else if (typeof current === "boolean") {
141
+ if (!add(current ? 4 : 5)) return void 0;
142
+ } else if (intrinsicArrayIsArray$1(current)) {
143
+ if (!add(2)) return void 0;
144
+ if (current.length > 0) append$1(tasks, {
145
+ kind: "array",
146
+ value: current,
147
+ index: 0
148
+ });
149
+ } else {
150
+ if (!add(2)) return void 0;
151
+ const keys = intrinsicObjectKeys$1(current);
152
+ if (keys.length > 0) append$1(tasks, {
153
+ kind: "object",
154
+ value: current,
155
+ keys,
156
+ index: 0
157
+ });
158
+ }
159
+ continue;
160
+ }
161
+ if (task.index > 0 && !add(1)) return void 0;
162
+ if (task.kind === "array") {
163
+ const item = task.value[task.index];
164
+ if (item === void 0) return void 0;
165
+ if (task.index + 1 < task.value.length) append$1(tasks, {
166
+ ...task,
167
+ index: task.index + 1
168
+ });
169
+ append$1(tasks, {
170
+ kind: "value",
171
+ value: item
172
+ });
173
+ continue;
174
+ }
175
+ const key = task.keys[task.index];
176
+ /* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
177
+ if (key === void 0) return void 0;
178
+ const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes);
179
+ if (keyBytes === void 0) return void 0;
180
+ if (!add(keyBytes + 1)) return void 0;
181
+ const item = task.value[key];
182
+ if (item === void 0) return void 0;
183
+ if (task.index + 1 < task.keys.length) append$1(tasks, {
184
+ ...task,
185
+ index: task.index + 1
186
+ });
187
+ append$1(tasks, {
188
+ kind: "value",
189
+ value: item
190
+ });
191
+ }
192
+ return bytes;
193
+ }
194
+ /**
195
+ * Return the longest code-point-aligned prefix whose JSON string encoding,
196
+ * including its surrounding quotes, fits `maxBytes`.
197
+ *
198
+ * @param text - the candidate string.
199
+ * @param maxBytes - serialized JSON-string bytes available.
200
+ * @returns the fitting prefix, or an empty string when even useful content cannot fit.
201
+ */
202
+ function truncateJsonStringBytes(text, maxBytes) {
203
+ if (maxBytes < 2) return "";
204
+ let bytes = 2;
205
+ let end = 0;
206
+ for (let index = 0; index < text.length;) {
207
+ const character = characterAt(text, index);
208
+ const cost = serializedCharacterBytes(character);
209
+ if (bytes + cost > maxBytes) break;
210
+ bytes += cost;
211
+ end += character.length;
212
+ index += character.length;
213
+ }
214
+ return end === text.length ? text : intrinsicReflectApply$1(intrinsicStringSlice, text, [0, end]);
215
+ }
216
+ //#endregion
217
+ //#region lib/types/channel.js
218
+ /** Length-prefixed JSON transport with bounded input and queued writes. */
219
+ const stringify = JSON.stringify;
220
+ const parse = JSON.parse;
221
+ /** One co-shipped process channel; the consumer owns frame validation and terminal outcomes. */
222
+ var JsonChannel = class {
223
+ stream;
224
+ maxBytes;
225
+ receive;
226
+ failure;
227
+ header = Buffer$1.alloc(4);
228
+ headerBytes = 0;
229
+ payload;
230
+ payloadBytes = 0;
231
+ queuedBytes = 0;
232
+ closed = false;
233
+ writes = /* @__PURE__ */ new Set();
234
+ constructor(stream, maxBytes, receive, failure) {
235
+ this.stream = stream;
236
+ this.maxBytes = maxBytes;
237
+ this.receive = receive;
238
+ this.failure = failure;
239
+ stream.on("data", this.onData);
240
+ stream.on("error", this.onError);
241
+ stream.on("end", this.onEnd);
242
+ stream.on("close", this.onClose);
243
+ }
244
+ onError = (error) => {
245
+ this.finishWrites(error);
246
+ if (!this.closed) this.failure(error, "io");
247
+ };
248
+ onClose = () => {
249
+ this.finishWrites(/* @__PURE__ */ new Error("control channel closed during a write"));
250
+ };
251
+ onEnd = () => {
252
+ if (!this.closed) this.failure(/* @__PURE__ */ new Error("control channel ended before the program settled"), "io");
253
+ };
254
+ onData = (chunk) => {
255
+ if (this.closed) return;
256
+ try {
257
+ let offset = 0;
258
+ while (offset < chunk.length && !this.closed) {
259
+ if (this.payload === void 0) {
260
+ const bytes = Math.min(4 - this.headerBytes, chunk.length - offset);
261
+ chunk.copy(this.header, this.headerBytes, offset, offset + bytes);
262
+ this.headerBytes += bytes;
263
+ offset += bytes;
264
+ if (this.headerBytes !== 4) continue;
265
+ const length = this.header.readUInt32BE(0);
266
+ if (length === 0 || length > this.maxBytes) throw new Error(`control frame exceeds ${this.maxBytes} bytes or is empty`);
267
+ this.payload = Buffer$1.allocUnsafe(length);
268
+ this.payloadBytes = 0;
269
+ this.headerBytes = 0;
270
+ }
271
+ const bytes = Math.min(this.payload.length - this.payloadBytes, chunk.length - offset);
272
+ chunk.copy(this.payload, this.payloadBytes, offset, offset + bytes);
273
+ this.payloadBytes += bytes;
274
+ offset += bytes;
275
+ if (this.payloadBytes !== this.payload.length) continue;
276
+ const frame = this.payload;
277
+ this.payload = void 0;
278
+ this.payloadBytes = 0;
279
+ this.receive(parse(new TextDecoder("utf-8", { fatal: true }).decode(frame)), frame.length);
280
+ }
281
+ } catch (error) {
282
+ this.failure(error instanceof Error ? error : new Error(String(error)), "protocol");
283
+ }
284
+ };
285
+ /**
286
+ * Submit a bounded frame immediately and await the stream's write receipt.
287
+ * @param message - JSON-only co-shipped protocol value.
288
+ * @returns Resolves when this frame has been written, or rejects after transport failure.
289
+ */
290
+ send(message) {
291
+ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("control channel is closed"));
292
+ const size = jsonValueBytesUpTo(message, this.maxBytes);
293
+ if (size === void 0 || this.queuedBytes + size > this.maxBytes) return Promise.reject(/* @__PURE__ */ new Error(`control output exceeds ${this.maxBytes} queued bytes`));
294
+ const body = Buffer$1.from(stringify(message), "utf8");
295
+ const header = Buffer$1.alloc(4);
296
+ header.writeUInt32BE(body.length);
297
+ this.queuedBytes += body.length;
298
+ const completion = Promise.withResolvers();
299
+ const write = {
300
+ promise: completion.promise,
301
+ finish: (error) => {
302
+ if (!this.writes.delete(write)) return;
303
+ this.queuedBytes -= body.length;
304
+ if (error) completion.reject(error);
305
+ else completion.resolve();
306
+ }
307
+ };
308
+ this.writes.add(write);
309
+ try {
310
+ this.stream.cork();
311
+ this.stream.write(header);
312
+ this.stream.write(body, (error) => {
313
+ write.finish(error ?? void 0);
314
+ });
315
+ this.stream.uncork();
316
+ } catch (error) {
317
+ write.finish(error instanceof Error ? error : new Error(String(error)));
318
+ }
319
+ return completion.promise;
320
+ }
321
+ finishWrites(error) {
322
+ for (const write of this.writes) write.finish(error);
323
+ }
324
+ /** Stop reads and close the owned endpoint; pending writes reject on closure. */
325
+ close() {
326
+ if (this.closed) return;
327
+ this.closed = true;
328
+ this.payload = void 0;
329
+ this.finishWrites(/* @__PURE__ */ new Error("control channel is closed"));
330
+ this.stream.off("data", this.onData);
331
+ this.stream.off("end", this.onEnd);
332
+ this.stream.destroy();
333
+ }
334
+ /** Wait for accepted writes to finish or fail. */
335
+ async drain() {
336
+ await Promise.allSettled([...this.writes].map((write) => write.promise));
337
+ }
338
+ };
339
+ //#endregion
340
+ //#region lib/types/launch.js
341
+ /** Select source or built bootstrap assets in the mounted execution world. */
342
+ /**
343
+ * Select explicit arguments without inheriting host loader or inspector flags.
344
+ * @param fs - Filesystem mapping host bootstrap assets into the process world.
345
+ * @param config - Optional preinstalled built bootstrap.
346
+ * @param maxMessageBytes - Validated frame and queued-write limit.
347
+ * @returns Arguments following the resolved Node executable.
348
+ */
349
+ function bootstrapArgs(fs, config, maxMessageBytes) {
350
+ if (config.bootstrapPath !== void 0) return [config.bootstrapPath, String(maxMessageBytes)];
351
+ if ("pkg" in process) return [String(maxMessageBytes)];
352
+ const mapped = (path) => {
353
+ const result = fs.processPathFromHostPath(path);
354
+ if (result === void 0) throw new Error(`PTC runtime bootstrap is unavailable in the subprocess execution world: ${path}`);
355
+ return result;
356
+ };
357
+ /* v8 ignore next 3 -- built-lib.e2e.ts executes the bundled provider and sibling process.js under plain Node. */
358
+ if (!new URL(import.meta.url).pathname.endsWith(".ts")) return [mapped(fileURLToPath(new URL("./process.js", import.meta.url))), String(maxMessageBytes)];
359
+ const entry = mapped(fileURLToPath(new URL("./process.ts", import.meta.url)));
360
+ const helper = mapped(resolve(dirname(fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-subprocess/package.json"))), "src/control.ts"));
361
+ return [
362
+ "--input-type=module",
363
+ "--eval",
364
+ `const {openInheritedControlChannel}=await import(${JSON.stringify(pathToFileURL(helper).href)});const {runNodeMain}=await import(${JSON.stringify(pathToFileURL(entry).href)});await runNodeMain(openInheritedControlChannel(),${maxMessageBytes},process);`
365
+ ];
366
+ }
367
+ //#endregion
368
+ //#region lib/types/output-ledger.js
369
+ /** One run's combined outer-output ledger; binding values never enter it. */
370
+ var OutputLedger = class {
371
+ maxBytes;
372
+ bytes = 2;
373
+ entries = 0;
374
+ constructor(maxBytes) {
375
+ this.maxBytes = maxBytes;
376
+ }
377
+ /** Admit one exact log entry, or report that the hard cap was crossed. */
378
+ admit(text, sink) {
379
+ const separatorBytes = this.entries > 0 ? 1 : 0;
380
+ const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes);
381
+ if (stringBytes === void 0) return false;
382
+ this.bytes += stringBytes + separatorBytes;
383
+ this.entries += 1;
384
+ sink.push(text);
385
+ return true;
386
+ }
387
+ /** Finalize a successful absent-or-JSON completion against the combined cap. */
388
+ success(logs, value) {
389
+ if (value !== void 0 && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === void 0) return this.limit(logs);
390
+ return {
391
+ logs,
392
+ ...value !== void 0 ? { value } : {}
393
+ };
394
+ }
395
+ /** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
396
+ failure(logs, error) {
397
+ if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === void 0) return this.limit(logs);
398
+ return {
399
+ logs,
400
+ error
401
+ };
402
+ }
403
+ /** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
404
+ limit(logs) {
405
+ const fullMessage = `outer output exceeded ${this.maxBytes} bytes`;
406
+ const messageBytes = fullMessage.length + 2;
407
+ const retained = [];
408
+ let retainedBytes = 2;
409
+ const logBudget = this.maxBytes - messageBytes;
410
+ for (const text of logs) {
411
+ const separatorBytes = retained.length > 0 ? 1 : 0;
412
+ const availableBytes = logBudget - retainedBytes - separatorBytes;
413
+ const stringBytes = jsonStringBytesUpTo(text, availableBytes);
414
+ if (stringBytes !== void 0) {
415
+ retained.push(text);
416
+ retainedBytes += stringBytes + separatorBytes;
417
+ continue;
418
+ }
419
+ const prefix = truncateJsonStringBytes(text, availableBytes);
420
+ if (prefix.length > 0) {
421
+ const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes);
422
+ /* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
423
+ if (prefixBytes === void 0) throw new Error("output ledger produced an oversized log prefix");
424
+ retained.push(prefix);
425
+ retainedBytes += prefixBytes + separatorBytes;
426
+ }
427
+ break;
428
+ }
429
+ return {
430
+ logs: retained,
431
+ error: {
432
+ kind: "output-limit",
433
+ message: truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
434
+ }
435
+ };
436
+ }
437
+ };
438
+ //#endregion
439
+ //#region lib/types/output-stream.js
440
+ /**
441
+ * Wait for queued bytes without letting an inherited descriptor retain a run forever.
442
+ * @param stream - Caller-owned raw process output, when provided.
443
+ * @param graceMs - Maximum wait after managed process termination.
444
+ * @returns Whether the complete stream ended without a transport error.
445
+ */
446
+ function drainOutput(stream, graceMs) {
447
+ if (stream === void 0 || stream.readableEnded) return Promise.resolve(true);
448
+ if (stream.destroyed) return Promise.resolve(false);
449
+ return new Promise((resolve) => {
450
+ const finish = (complete) => {
451
+ clearTimeout(timer);
452
+ stream.off("end", onEnd);
453
+ stream.off("close", onClose);
454
+ stream.off("error", onError);
455
+ resolve(complete);
456
+ };
457
+ const onEnd = () => {
458
+ finish(true);
459
+ };
460
+ const onClose = () => {
461
+ finish(false);
462
+ };
463
+ const onError = () => {
464
+ finish(false);
465
+ };
466
+ const timer = setTimeout(() => {
467
+ finish(false);
468
+ }, graceMs);
469
+ stream.once("end", onEnd);
470
+ stream.once("close", onClose);
471
+ stream.once("error", onError);
472
+ });
473
+ }
474
+ //#endregion
475
+ //#region lib/types/environment.js
476
+ /** Startup variables required by native executables before model evaluation. */
477
+ /** Native executable search, Windows system paths, and sandbox temporary paths retained in the OS environment. */
478
+ const STARTUP_ENVIRONMENT_NAMES = new Set([
479
+ "PATH",
480
+ "PATHEXT",
481
+ "SYSTEMROOT",
482
+ "WINDIR",
483
+ "TEMP",
484
+ "TMP"
485
+ ]);
486
+ //#endregion
487
+ //#region lib/types/json-wire.js
488
+ /**
489
+ * Lossless-JSON snapshots for the dependency-free source bootstrap closure.
490
+ * @module @deepseek-ai/dsh-ptc-runtime-node/json-wire
491
+ */
492
+ const intrinsicFunctionToString = Reflect.get(Function.prototype, "toString");
493
+ const intrinsicReflectApply = Reflect.get(Reflect, "apply");
494
+ const IntrinsicError = Error;
495
+ const IntrinsicSet = Set;
496
+ const intrinsicArrayIsArray = Array.isArray;
497
+ const intrinsicArrayPrototype = Array.prototype;
498
+ const intrinsicNumberIsFinite = Number.isFinite;
499
+ const intrinsicNumberIsSafeInteger = Number.isSafeInteger;
500
+ const intrinsicObjectCreate = Object.create;
501
+ const intrinsicObjectDefineProperty = Object.defineProperty;
502
+ const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
503
+ const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf;
504
+ const intrinsicObjectHasOwn = Object.hasOwn;
505
+ const intrinsicObjectIs = Object.is;
506
+ const intrinsicObjectKeys = Object.keys;
507
+ const intrinsicObjectPrototype = Object.prototype;
508
+ const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, "propertyIsEnumerable");
509
+ const intrinsicReflectOwnKeys = Reflect.ownKeys;
510
+ const intrinsicSetAdd = Reflect.get(Set.prototype, "add");
511
+ Reflect.get(Set.prototype, "delete");
512
+ const intrinsicSetHas = Reflect.get(Set.prototype, "has");
513
+ /** Build a data descriptor that cannot inherit model-defined accessor fields. */
514
+ function dataDescriptor(value) {
515
+ const descriptor = intrinsicObjectCreate(null);
516
+ descriptor.value = value;
517
+ return descriptor;
518
+ }
519
+ /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
520
+ function defineEnumerableDataProperty(target, key, value) {
521
+ const descriptor = dataDescriptor(value);
522
+ descriptor.enumerable = true;
523
+ descriptor.configurable = true;
524
+ descriptor.writable = true;
525
+ intrinsicObjectDefineProperty(target, key, descriptor);
526
+ }
527
+ /** Append without consulting a model-mutated `Array.prototype`. */
528
+ function append(target, value) {
529
+ defineEnumerableDataProperty(target, target.length, value);
530
+ }
531
+ /** Pop without consulting a model-mutated `Array.prototype`. */
532
+ function takeLast(target) {
533
+ if (target.length === 0) return void 0;
534
+ const index = target.length - 1;
535
+ const value = target[index];
536
+ intrinsicObjectDefineProperty(target, "length", dataDescriptor(index));
537
+ return value;
538
+ }
539
+ /** Whether one captured-intrinsic Set contains a value. */
540
+ function setHas(target, value) {
541
+ return intrinsicReflectApply(intrinsicSetHas, target, [value]);
542
+ }
543
+ /** Add to one captured-intrinsic Set. */
544
+ function setAdd(target, value) {
545
+ intrinsicReflectApply(intrinsicSetAdd, target, [value]);
546
+ }
547
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
548
+ function hasIntrinsicConstructor(prototype, name) {
549
+ const constructor = intrinsicObjectGetOwnPropertyDescriptor(prototype, "constructor")?.value;
550
+ if (typeof constructor !== "function") return false;
551
+ try {
552
+ return constructor.name === name && constructor.prototype === prototype && intrinsicReflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`;
553
+ } catch {
554
+ return false;
555
+ }
556
+ }
557
+ /** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
558
+ function isForeignIntrinsicObjectPrototype(value) {
559
+ return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
560
+ }
561
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
562
+ function hasPlainArrayPrototype(value) {
563
+ const prototype = intrinsicObjectGetPrototypeOf(value);
564
+ if (prototype === intrinsicArrayPrototype) return true;
565
+ if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
566
+ const objectPrototype = intrinsicObjectGetPrototypeOf(prototype);
567
+ return typeof objectPrototype === "object" && objectPrototype !== null && isForeignIntrinsicObjectPrototype(objectPrototype);
568
+ }
569
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
570
+ function hasPlainObjectPrototype(value) {
571
+ const prototype = intrinsicObjectGetPrototypeOf(value);
572
+ return prototype === null || prototype === intrinsicObjectPrototype || typeof prototype === "object" && isForeignIntrinsicObjectPrototype(prototype);
573
+ }
574
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
575
+ function enumerableStringKeys(value) {
576
+ const keys = intrinsicReflectOwnKeys(value);
577
+ for (let index = 0; index < keys.length; index++) {
578
+ const key = keys[index];
579
+ if (typeof key !== "string" || !intrinsicReflectApply(intrinsicObjectPropertyIsEnumerable, value, [key])) return void 0;
580
+ }
581
+ return keys;
582
+ }
583
+ /**
584
+ * Flatten one validated JSON value for the process control channel.
585
+ * @param value - the lossless JSON value to transport.
586
+ * @returns a pre-order token stream whose own nesting is bounded.
587
+ */
588
+ function encodePtcJsonWire(value) {
589
+ const wire = [];
590
+ const pending = [value];
591
+ for (let current = takeLast(pending); current !== void 0; current = takeLast(pending)) {
592
+ if (current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string") {
593
+ append(wire, current);
594
+ continue;
595
+ }
596
+ if (intrinsicArrayIsArray(current)) {
597
+ append(wire, {
598
+ kind: "array",
599
+ length: current.length
600
+ });
601
+ for (let index = current.length - 1; index >= 0; index--) {
602
+ const item = current[index];
603
+ if (item === void 0) throw new IntrinsicError("cannot encode a sparse JSON array");
604
+ append(pending, item);
605
+ }
606
+ continue;
607
+ }
608
+ const keys = intrinsicObjectKeys(current);
609
+ append(wire, {
610
+ kind: "object",
611
+ keys
612
+ });
613
+ for (let index = keys.length - 1; index >= 0; index--) {
614
+ const key = keys[index];
615
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
616
+ if (key === void 0) throw new IntrinsicError("cannot encode a missing JSON object key");
617
+ const item = current[key];
618
+ if (item === void 0) throw new IntrinsicError("cannot encode an undefined JSON object property");
619
+ append(pending, item);
620
+ }
621
+ }
622
+ return wire;
623
+ }
624
+ /** Whether an array contains exactly its dense indexed slots and `length`. */
625
+ function isDenseArray(value) {
626
+ if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false;
627
+ for (let index = 0; index < value.length; index++) if (!intrinsicObjectHasOwn(value, index)) return false;
628
+ return true;
629
+ }
630
+ /** Whether one exact string-key list contains a key, without consulting its prototype. */
631
+ function keysContain(keys, expected) {
632
+ for (let index = 0; index < keys.length; index++) if (keys[index] === expected) return true;
633
+ return false;
634
+ }
635
+ /** Return one exact container marker, or reject any extra/missing fields. */
636
+ function containerToken(value) {
637
+ if (intrinsicArrayIsArray(value) || !hasPlainObjectPrototype(value)) return void 0;
638
+ const keys = enumerableStringKeys(value);
639
+ if (keys === void 0) return void 0;
640
+ const token = value;
641
+ if (token.kind === "array") {
642
+ if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "length")) return void 0;
643
+ const length = token.length;
644
+ return typeof length === "number" && intrinsicNumberIsSafeInteger(length) && length >= 0 ? {
645
+ kind: "array",
646
+ length
647
+ } : void 0;
648
+ }
649
+ if (token.kind === "object") {
650
+ if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "keys")) return void 0;
651
+ const objectKeys = token.keys;
652
+ if (!intrinsicArrayIsArray(objectKeys) || !isDenseArray(objectKeys)) return void 0;
653
+ const unique = new IntrinsicSet();
654
+ const normalizedKeys = [];
655
+ const objectKeyValues = objectKeys;
656
+ for (let index = 0; index < objectKeyValues.length; index++) {
657
+ const key = objectKeyValues[index];
658
+ if (typeof key !== "string" || setHas(unique, key)) return void 0;
659
+ setAdd(unique, key);
660
+ append(normalizedKeys, key);
661
+ }
662
+ return {
663
+ kind: "object",
664
+ keys: normalizedKeys
665
+ };
666
+ }
667
+ }
668
+ /**
669
+ * Rebuild one lossless JSON value from the flat process wire format.
670
+ * Malformed or incomplete traffic returns `undefined`; traversal is iterative
671
+ * and therefore independent of the transported value's application depth.
672
+ * @param input - untrusted message-port payload.
673
+ * @returns the detached JSON value, or `undefined` when the wire is invalid.
674
+ */
675
+ function decodePtcJsonWire(input) {
676
+ try {
677
+ if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return void 0;
678
+ const wire = input;
679
+ const frames = [];
680
+ let root;
681
+ let rootAssigned = false;
682
+ const attach = (value) => {
683
+ const parent = frames[frames.length - 1];
684
+ if (!parent) {
685
+ if (rootAssigned) return false;
686
+ root = value;
687
+ rootAssigned = true;
688
+ return true;
689
+ }
690
+ /* v8 ignore next -- completed frames are popped before another token can attach. */
691
+ if (parent.index >= (parent.kind === "array" ? parent.length : parent.keys.length)) return false;
692
+ if (parent.kind === "array") append(parent.target, value);
693
+ else {
694
+ const key = parent.keys[parent.index];
695
+ /* v8 ignore next -- object frames are built from validated keys and their exact length. */
696
+ if (key === void 0) return false;
697
+ defineEnumerableDataProperty(parent.target, key, value);
698
+ }
699
+ parent.index += 1;
700
+ return true;
701
+ };
702
+ for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
703
+ const token = wire[tokenIndex];
704
+ let value;
705
+ let frame;
706
+ if (token === null || typeof token === "boolean" || typeof token === "string") value = token;
707
+ else if (typeof token === "number") {
708
+ if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return void 0;
709
+ value = token;
710
+ } else {
711
+ if (typeof token !== "object") return void 0;
712
+ const marker = containerToken(token);
713
+ if (!marker) return void 0;
714
+ const remainingTokens = wire.length - tokenIndex - 1;
715
+ if (marker.kind === "array") {
716
+ if (marker.length > remainingTokens) return void 0;
717
+ const target = [];
718
+ value = target;
719
+ if (marker.length > 0) frame = {
720
+ kind: "array",
721
+ target,
722
+ length: marker.length,
723
+ index: 0
724
+ };
725
+ } else {
726
+ if (marker.keys.length > remainingTokens) return void 0;
727
+ const target = {};
728
+ value = target;
729
+ if (marker.keys.length > 0) frame = {
730
+ kind: "object",
731
+ target,
732
+ keys: marker.keys,
733
+ index: 0
734
+ };
735
+ }
736
+ }
737
+ if (!attach(value)) return void 0;
738
+ if (frame) append(frames, frame);
739
+ while (frames.length > 0) {
740
+ const current = frames[frames.length - 1];
741
+ /* v8 ignore next -- the loop condition guarantees a final frame. */
742
+ if (current === void 0) break;
743
+ if (current.index < (current.kind === "array" ? current.length : current.keys.length)) break;
744
+ takeLast(frames);
745
+ }
746
+ }
747
+ return frames.length === 0 ? root : void 0;
748
+ } catch {
749
+ return;
750
+ }
751
+ }
752
+ //#endregion
753
+ //#region lib/types/index.js
754
+ /** Confined Node programs with host-owned bindings, output limits, and managed process cleanup. */
755
+ const STRIP_PREFIX = "async function __dsh_program__() {\n";
756
+ const STRIP_SUFFIX = "\n}";
757
+ function messageOf(error) {
758
+ return error instanceof Error ? error.message : String(error);
759
+ }
760
+ function record(value) {
761
+ return typeof value === "object" && value !== null && !Array.isArray(value);
762
+ }
763
+ /** Node provider; direct file effects use the same sandbox service as Bash. */
764
+ var NodePtcRuntime = class extends PtcRuntime {
765
+ static inject = [
766
+ "fs",
767
+ "subprocess",
768
+ "sandbox",
769
+ "sandboxPolicy"
770
+ ];
771
+ static Config = z.object({
772
+ timeoutMs: z.number().default(12e4),
773
+ maxTimeoutMs: z.number().default(6e5),
774
+ maxOutputBytes: z.number().default(67108864),
775
+ maxOldGenerationSizeMb: z.number().default(512),
776
+ maxMessageBytes: z.number().default(134217728),
777
+ maxPendingCalls: z.number().default(128),
778
+ graceMs: z.number().default(3e3),
779
+ nodeExecutable: z.string(),
780
+ bootstrapPath: z.string()
781
+ });
782
+ language = "typescript";
783
+ isolation = "process";
784
+ get executionInstructions() {
785
+ return "Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution's sandbox policy.";
786
+ }
787
+ config;
788
+ live = /* @__PURE__ */ new Set();
789
+ disposed = false;
790
+ constructor(ctx, config) {
791
+ super(ctx);
792
+ this.config = {
793
+ ...config,
794
+ nodeExecutable: config.nodeExecutable ?? process.execPath
795
+ };
796
+ for (const [key, value] of Object.entries(this.config)) if (typeof value === "number" && (!Number.isFinite(value) || value <= 0)) throw new Error(`ptc-runtime-node: ${key} must be positive and finite`);
797
+ for (const key of [
798
+ "timeoutMs",
799
+ "maxTimeoutMs",
800
+ "graceMs"
801
+ ]) if (this.config[key] > MAX_TIMER_DELAY_MS) throw new Error(`ptc-runtime-node: ${key} exceeds the supported timer range`);
802
+ if (!Number.isSafeInteger(this.config.maxOutputBytes) || this.config.maxOutputBytes < 4) throw new Error("ptc-runtime-node: maxOutputBytes must be an integer of at least 4");
803
+ if (!Number.isSafeInteger(this.config.maxMessageBytes) || this.config.maxMessageBytes > 4294967295) throw new Error("ptc-runtime-node: maxMessageBytes must fit an unsigned 32-bit frame length");
804
+ if (!Number.isSafeInteger(this.config.maxPendingCalls)) throw new Error("ptc-runtime-node: maxPendingCalls must be an integer");
805
+ if (!Number.isSafeInteger(this.config.maxOldGenerationSizeMb)) throw new Error("ptc-runtime-node: maxOldGenerationSizeMb must be an integer");
806
+ if (this.config.nodeExecutable.length === 0) throw new Error("ptc-runtime-node: nodeExecutable must be non-empty");
807
+ if (this.config.bootstrapPath !== void 0 && !isAbsolute(this.config.bootstrapPath)) throw new Error("ptc-runtime-node: bootstrapPath must be absolute");
808
+ ctx.effect(() => async () => {
809
+ this.disposed = true;
810
+ const active = [...this.live];
811
+ for (const run of active) run.controller.abort("runtime disposed");
812
+ await Promise.all(active.map((run) => run.finished));
813
+ }, "Node ptc-runtime cleanup");
814
+ }
815
+ get sandboxMode() {
816
+ return this.ctx.sandboxPolicy.defaultMode;
817
+ }
818
+ get timeout() {
819
+ return {
820
+ defaultMs: Math.min(this.config.timeoutMs, this.config.maxTimeoutMs),
821
+ maxMs: this.config.maxTimeoutMs
822
+ };
823
+ }
824
+ /**
825
+ * Resolve an execution under explicit or deployment policy.
826
+ * @param request - Program, bindings, optional cwd/deadline, and resolved authority.
827
+ * @returns Complete execution inputs with a capped numeric budget or an explicit null deadline.
828
+ */
829
+ resolve(request) {
830
+ if (this.disposed) throw new Error("ptc-runtime-node: resolve after disposal");
831
+ const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
832
+ const cwd = request.cwd ?? sandboxPolicy.workspaceRoot;
833
+ if (!isAbsolute(cwd)) throw new Error("ptc-runtime-node: cwd must be absolute");
834
+ return {
835
+ ...request,
836
+ cwd,
837
+ timeoutMs: request.timeoutMs === null ? null : clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, "ptc-runtime-node: timeoutMs"),
838
+ sandboxPolicy
839
+ };
840
+ }
841
+ /**
842
+ * Run a resolved program in a fresh managed and confined Node process.
843
+ * @param spec - Inputs returned by resolve; missing authority is caller misuse.
844
+ * @returns Output and file-confinement facts after managed cleanup.
845
+ */
846
+ async run(spec) {
847
+ if (this.disposed) throw new Error("ptc-runtime-node: run after disposal");
848
+ if (spec.sandboxPolicy === void 0) throw new Error("ptc-runtime-node: run requires a resolved sandbox policy");
849
+ if (!isAbsolute(spec.cwd) || spec.timeoutMs !== null && (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0 || spec.timeoutMs > this.config.maxTimeoutMs)) throw new Error("ptc-runtime-node: run requires resolved cwd and timeout");
850
+ const bindings = validateBindings(spec);
851
+ const controller = new AbortController();
852
+ const completion = Promise.withResolvers();
853
+ const live = {
854
+ controller,
855
+ finished: completion.promise
856
+ };
857
+ this.live.add(live);
858
+ try {
859
+ return await this.execute(spec, spec.sandboxPolicy, bindings, controller);
860
+ } finally {
861
+ this.live.delete(live);
862
+ completion.resolve();
863
+ }
864
+ }
865
+ async execute(spec, policy, bindings, controller) {
866
+ const output = new OutputLedger(this.config.maxOutputBytes);
867
+ const logs = [];
868
+ const sandbox = {
869
+ mode: policy.mode,
870
+ denied: false
871
+ };
872
+ const result = Promise.withResolvers();
873
+ const signal = spec.signal === void 0 ? controller.signal : AbortSignal.any([spec.signal, controller.signal]);
874
+ let handle;
875
+ let channel;
876
+ let confined;
877
+ let settled = false;
878
+ let timedOut = false;
879
+ let outputOverflow = false;
880
+ let overflowResult;
881
+ let stderr = "";
882
+ let parsing = true;
883
+ const wallTimer = spec.timeoutMs === null ? void 0 : setTimeout(() => {
884
+ timedOut = true;
885
+ controller.abort("execution deadline reached");
886
+ }, spec.timeoutMs);
887
+ const finish = (failure, value) => {
888
+ if (settled) return;
889
+ settled = true;
890
+ clearTimeout(wallTimer);
891
+ signal.removeEventListener("abort", onAbort);
892
+ channel?.close();
893
+ (async () => {
894
+ if (handle !== void 0) try {
895
+ handle.terminate();
896
+ await Promise.all([handle.done.catch(() => {}), handle.waitForExit()]);
897
+ if ((await Promise.all([drainOutput(handle.stdout, this.config.graceMs), drainOutput(handle.stderr, this.config.graceMs)])).includes(false) && failure === void 0) failure = {
898
+ kind: "worker-exit",
899
+ message: "Node process output did not close cleanly"
900
+ };
901
+ } catch (error) {
902
+ failure = {
903
+ kind: "worker-exit",
904
+ message: `managed process cleanup failed: ${messageOf(error)}`
905
+ };
906
+ } finally {
907
+ handle.stdout?.destroy();
908
+ handle.stderr?.destroy();
909
+ }
910
+ const outcome = outputOverflow ? overflowResult ?? output.limit(logs) : failure === void 0 ? output.success(logs, value) : output.failure(logs, failure);
911
+ result.resolve({
912
+ ...outcome,
913
+ sandbox: { ...sandbox }
914
+ });
915
+ })();
916
+ };
917
+ const onAbort = () => {
918
+ finish(timedOut ? {
919
+ kind: "timeout",
920
+ message: `execution deadline reached (${spec.timeoutMs}ms)`
921
+ } : {
922
+ kind: "abort",
923
+ message: messageOf(signal.reason)
924
+ });
925
+ };
926
+ signal.addEventListener("abort", onAbort, { once: true });
927
+ if (signal.aborted) onAbort();
928
+ try {
929
+ if (settled) return await result.promise;
930
+ const stripped = stripTypeScriptTypes(STRIP_PREFIX + spec.program + STRIP_SUFFIX);
931
+ parsing = false;
932
+ const data = {
933
+ code: stripped.slice(35, stripped.length - 2),
934
+ namespaces: [...bindings.values()].map((binding) => ({
935
+ global: binding.global,
936
+ names: Object.keys(binding.functions),
937
+ ...binding.errorClass ? { errorClass: binding.errorClass } : {}
938
+ })),
939
+ maxOutputBytes: this.config.maxOutputBytes
940
+ };
941
+ const executable = await this.ctx.subprocess.resolveExecutable(this.config.nodeExecutable, void 0, signal);
942
+ if (settled) return await result.promise;
943
+ const packaged = "pkg" in process && this.config.bootstrapPath === void 0;
944
+ const heapFlag = `--max-old-space-size=${this.config.maxOldGenerationSizeMb}`;
945
+ const argv = [
946
+ executable,
947
+ ...packaged ? [] : [heapFlag],
948
+ ...bootstrapArgs(this.ctx.fs, this.config, this.config.maxMessageBytes)
949
+ ];
950
+ confined = policy.mode === "danger-full-access" ? void 0 : await this.ctx.sandbox.confine(argv, {
951
+ ...policy,
952
+ mode: policy.mode
953
+ }, signal);
954
+ if (settled) return await result.promise;
955
+ if (confined !== void 0) sandbox.enforcement = confined.enforcement;
956
+ const env = Object.fromEntries(Object.keys(process.env).filter((key) => !STARTUP_ENVIRONMENT_NAMES.has(key.toUpperCase())).map((key) => [key, void 0]));
957
+ if (packaged) {
958
+ env.DSH_PTC_RUNTIME_NODE = "1";
959
+ env.NODE_OPTIONS = heapFlag;
960
+ }
961
+ handle = this.ctx.subprocess.spawn({
962
+ argv: confined?.argv ?? argv,
963
+ cwd: spec.cwd,
964
+ env,
965
+ stdio: {
966
+ stdin: "ignore",
967
+ stdout: "pipe",
968
+ stderr: "pipe",
969
+ control: "pipe"
970
+ },
971
+ graceMs: this.config.graceMs,
972
+ signal
973
+ });
974
+ const launched = handle;
975
+ if (launched.control === void 0 || launched.stdout === void 0 || launched.stderr === void 0) throw new Error("subprocess provider did not supply the requested control and output pipes");
976
+ const admit = (text) => {
977
+ if (outputOverflow) return;
978
+ if (!output.admit(text, logs)) {
979
+ outputOverflow = true;
980
+ overflowResult = output.limit([...logs, text]);
981
+ finish({
982
+ kind: "output-limit",
983
+ message: `outer output exceeded ${this.config.maxOutputBytes} bytes`
984
+ });
985
+ }
986
+ };
987
+ const stdoutDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
988
+ const stderrDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
989
+ handle.stdout?.on("data", (chunk) => {
990
+ const text = stdoutDecoder.decode(chunk, { stream: true });
991
+ if (text.length > 0) admit(text);
992
+ });
993
+ handle.stdout?.on("end", () => {
994
+ const text = stdoutDecoder.decode();
995
+ if (text.length > 0) admit(text);
996
+ });
997
+ handle.stdout?.on("error", (error) => {
998
+ finish({
999
+ kind: "worker-exit",
1000
+ message: messageOf(error)
1001
+ });
1002
+ });
1003
+ handle.stderr?.on("data", (chunk) => {
1004
+ const text = stderrDecoder.decode(chunk, { stream: true });
1005
+ stderr = (stderr + text).slice(-this.config.maxOutputBytes);
1006
+ if (text.length > 0) admit(text);
1007
+ });
1008
+ handle.stderr?.on("end", () => {
1009
+ const text = stderrDecoder.decode();
1010
+ if (text.length > 0) admit(text);
1011
+ });
1012
+ handle.stderr?.on("error", (error) => {
1013
+ finish({
1014
+ kind: "worker-exit",
1015
+ message: messageOf(error)
1016
+ });
1017
+ });
1018
+ let ready = false;
1019
+ let nextId = 1;
1020
+ let pending = 0;
1021
+ let pendingBytes = 0;
1022
+ const protocolFailure = (message) => {
1023
+ finish({
1024
+ kind: "protocol",
1025
+ message
1026
+ });
1027
+ };
1028
+ const processFinished = (outcome) => {
1029
+ if (settled) return;
1030
+ finish(confined !== void 0 && classifyRunnerFailure(outcome.exitCode, stderr, confined.runnerFailureRules) !== void 0 ? {
1031
+ kind: "sandbox-unavailable",
1032
+ message: stderr
1033
+ } : {
1034
+ kind: "worker-exit",
1035
+ message: `Node process exited before completing (${String(outcome.exitCode)})${stderr ? `: ${stderr}` : ""}`
1036
+ });
1037
+ };
1038
+ const transport = new JsonChannel(launched.control, this.config.maxMessageBytes, (raw, bytes) => {
1039
+ if (!record(raw)) {
1040
+ protocolFailure("invalid control frame");
1041
+ return;
1042
+ }
1043
+ if (!ready) {
1044
+ if (raw.type !== "ready") {
1045
+ protocolFailure("program frame arrived before bootstrap readiness");
1046
+ return;
1047
+ }
1048
+ ready = true;
1049
+ transport.send({
1050
+ type: "boot",
1051
+ data
1052
+ }).catch((error) => {
1053
+ protocolFailure(messageOf(error));
1054
+ });
1055
+ return;
1056
+ }
1057
+ switch (raw.type) {
1058
+ case "log":
1059
+ if (typeof raw.text !== "string") {
1060
+ protocolFailure("invalid log frame");
1061
+ return;
1062
+ }
1063
+ admit(raw.text);
1064
+ return;
1065
+ case "output-limit":
1066
+ outputOverflow = true;
1067
+ finish();
1068
+ return;
1069
+ case "done": {
1070
+ if (raw.error !== void 0) {
1071
+ if (!record(raw.error) || typeof raw.error.message !== "string" || raw.error.kind !== "exception" && raw.error.kind !== "invalid-output" && raw.error.kind !== "output-limit") {
1072
+ protocolFailure("invalid terminal error");
1073
+ return;
1074
+ }
1075
+ const failure = {
1076
+ kind: raw.error.kind,
1077
+ message: raw.error.message
1078
+ };
1079
+ if (confined !== void 0) sandbox.denied = confined.denialSignatures.some((signature) => failure.message.toLowerCase().includes(signature.toLowerCase()));
1080
+ if (failure.kind === "output-limit") outputOverflow = true;
1081
+ finish(failure);
1082
+ return;
1083
+ }
1084
+ const value = raw.value === void 0 ? void 0 : decodePtcJsonWire(raw.value);
1085
+ if (raw.value !== void 0 && value === void 0) finish({
1086
+ kind: "invalid-output",
1087
+ message: "program completion must be lossless JSON"
1088
+ });
1089
+ else finish(void 0, value);
1090
+ return;
1091
+ }
1092
+ case "call": {
1093
+ if (!Number.isSafeInteger(raw.id) || raw.id !== nextId || typeof raw.global !== "string" || typeof raw.name !== "string") {
1094
+ protocolFailure("invalid binding call identity");
1095
+ return;
1096
+ }
1097
+ nextId += 1;
1098
+ const functions = bindings.get(raw.global)?.functions;
1099
+ const fn = functions !== void 0 && Object.hasOwn(functions, raw.name) ? functions[raw.name] : void 0;
1100
+ if (typeof fn !== "function") {
1101
+ protocolFailure("program requested an undeclared binding");
1102
+ return;
1103
+ }
1104
+ const args = decodePtcJsonWire(raw.args);
1105
+ if (args === void 0) {
1106
+ protocolFailure("binding arguments must be lossless JSON");
1107
+ return;
1108
+ }
1109
+ if (++pending > this.config.maxPendingCalls || (pendingBytes += bytes) > this.config.maxMessageBytes) {
1110
+ protocolFailure("pending binding calls exceed configured limits");
1111
+ return;
1112
+ }
1113
+ const id = raw.id;
1114
+ (async () => {
1115
+ let reply;
1116
+ try {
1117
+ const value = snapshotJsonValue(await fn(args));
1118
+ if (value === void 0) throw new Error("binding resolution must be lossless JSON");
1119
+ reply = {
1120
+ type: "reply",
1121
+ id,
1122
+ ok: true,
1123
+ value: encodePtcJsonWire(value)
1124
+ };
1125
+ } catch (error) {
1126
+ reply = {
1127
+ type: "reply",
1128
+ id,
1129
+ ok: false,
1130
+ message: messageOf(error)
1131
+ };
1132
+ } finally {
1133
+ pending -= 1;
1134
+ pendingBytes -= bytes;
1135
+ }
1136
+ if (!settled) await transport.send(reply);
1137
+ })().catch((error) => {
1138
+ protocolFailure(messageOf(error));
1139
+ });
1140
+ return;
1141
+ }
1142
+ default: protocolFailure("unknown control message");
1143
+ }
1144
+ }, (error, kind) => {
1145
+ if (kind === "protocol") protocolFailure(messageOf(error));
1146
+ else if (ready) finish({
1147
+ kind: "worker-exit",
1148
+ message: messageOf(error)
1149
+ });
1150
+ else launched.done.then(processFinished, (failure) => {
1151
+ finish({
1152
+ kind: "worker-exit",
1153
+ message: messageOf(failure)
1154
+ });
1155
+ });
1156
+ });
1157
+ channel = transport;
1158
+ launched.done.then((outcome) => {
1159
+ setImmediate(() => {
1160
+ processFinished(outcome);
1161
+ });
1162
+ }, (error) => {
1163
+ finish({
1164
+ kind: confined !== void 0 && isRunnerSpawnFailure(error, confined.argv[0], spec.cwd) ? "sandbox-unavailable" : "worker-exit",
1165
+ message: messageOf(error)
1166
+ });
1167
+ });
1168
+ } catch (error) {
1169
+ finish({
1170
+ kind: error instanceof SandboxUnavailableError ? "sandbox-unavailable" : parsing ? "exception" : "worker-exit",
1171
+ message: messageOf(error)
1172
+ });
1173
+ }
1174
+ return await result.promise;
1175
+ }
1176
+ };
1177
+ //#endregion
1178
+ export { NodePtcRuntime, NodePtcRuntime as default };