@deepseek-ai/dsh-ptc-runtime-node 0.2.1-alpha.2 → 0.2.1-alpha.3

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 CHANGED
@@ -1,55 +1,39 @@
1
1
  import { stripTypeScriptTypes } from "node:module";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { dirname, isAbsolute, resolve } from "node:path";
3
- import z from "@deepseek-ai/schemastery";
4
4
  import { DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, PtcRuntime, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from "@deepseek-ai/dsh-ptc-runtime";
5
5
  import { MAX_TIMER_DELAY_MS, clampTimeout } from "@deepseek-ai/dsh-timeout";
6
6
  import { SandboxUnavailableError, classifyRunnerFailure, isRunnerSpawnFailure } from "@deepseek-ai/dsh-sandbox";
7
7
  import { assertNever, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
8
8
  import { Buffer as Buffer$1 } from "node:buffer";
9
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_]*$/;
10
+ import z from "@deepseek-ai/schemastery";
11
+ //#region lib/types/json-wire.js
13
12
  /**
14
- * Reject unusable namespaces before starting a process.
15
- * @param request - Host-owned binding declarations.
16
- * @returns Namespaces keyed by their declared global.
13
+ * Lossless-JSON snapshots for the dependency-free source bootstrap closure.
14
+ * @module @deepseek-ai/dsh-ptc-runtime-node/json-wire
17
15
  */
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;
16
+ const intrinsicFunctionToString = Reflect.get(Function.prototype, "toString");
17
+ const intrinsicReflectApply$1 = Reflect.get(Reflect, "apply");
18
+ const IntrinsicError = Error;
19
+ const IntrinsicSet = Set;
43
20
  const intrinsicArrayIsArray$1 = Array.isArray;
44
- const IntrinsicBuffer = Buffer;
45
- const intrinsicBufferByteLength = Reflect.get(Buffer, "byteLength");
21
+ const intrinsicArrayPrototype = Array.prototype;
22
+ const intrinsicNumberIsFinite = Number.isFinite;
23
+ const intrinsicNumberIsSafeInteger = Number.isSafeInteger;
46
24
  const intrinsicObjectCreate$1 = Object.create;
47
25
  const intrinsicObjectDefineProperty$1 = Object.defineProperty;
26
+ const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
27
+ const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf;
28
+ const intrinsicObjectHasOwn = Object.hasOwn;
29
+ const intrinsicObjectIs = Object.is;
48
30
  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");
31
+ const intrinsicObjectPrototype = Object.prototype;
32
+ const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, "propertyIsEnumerable");
33
+ const intrinsicReflectOwnKeys = Reflect.ownKeys;
34
+ const intrinsicSetAdd = Reflect.get(Set.prototype, "add");
35
+ Reflect.get(Set.prototype, "delete");
36
+ const intrinsicSetHas = Reflect.get(Set.prototype, "has");
53
37
  /** Build a data descriptor that cannot inherit model-defined accessor fields. */
54
38
  function dataDescriptor$1(value) {
55
39
  const descriptor = intrinsicObjectCreate$1(null);
@@ -64,10 +48,6 @@ function defineEnumerableDataProperty$1(target, key, value) {
64
48
  descriptor.writable = true;
65
49
  intrinsicObjectDefineProperty$1(target, key, descriptor);
66
50
  }
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
51
  /** Append without consulting a model-mutated `Array.prototype`. */
72
52
  function append$1(target, value) {
73
53
  defineEnumerableDataProperty$1(target, target.length, value);
@@ -80,124 +60,438 @@ function takeLast$1(target) {
80
60
  intrinsicObjectDefineProperty$1(target, "length", dataDescriptor$1(index));
81
61
  return value;
82
62
  }
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)]);
63
+ /** Whether one captured-intrinsic Set contains a value. */
64
+ function setHas(target, value) {
65
+ return intrinsicReflectApply$1(intrinsicSetHas, target, [value]);
86
66
  }
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);
67
+ /** Add to one captured-intrinsic Set. */
68
+ function setAdd(target, value) {
69
+ intrinsicReflectApply$1(intrinsicSetAdd, target, [value]);
95
70
  }
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;
71
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
72
+ function hasIntrinsicConstructor(prototype, name) {
73
+ const constructor = intrinsicObjectGetOwnPropertyDescriptor(prototype, "constructor")?.value;
74
+ if (typeof constructor !== "function") return false;
75
+ try {
76
+ return constructor.name === name && constructor.prototype === prototype && intrinsicReflectApply$1(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`;
77
+ } catch {
78
+ return false;
110
79
  }
111
- return bytes;
80
+ }
81
+ /** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
82
+ function isForeignIntrinsicObjectPrototype(value) {
83
+ return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
84
+ }
85
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
86
+ function hasPlainArrayPrototype(value) {
87
+ const prototype = intrinsicObjectGetPrototypeOf(value);
88
+ if (prototype === intrinsicArrayPrototype) return true;
89
+ if (!intrinsicArrayIsArray$1(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
90
+ const objectPrototype = intrinsicObjectGetPrototypeOf(prototype);
91
+ return typeof objectPrototype === "object" && objectPrototype !== null && isForeignIntrinsicObjectPrototype(objectPrototype);
92
+ }
93
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
94
+ function hasPlainObjectPrototype(value) {
95
+ const prototype = intrinsicObjectGetPrototypeOf(value);
96
+ return prototype === null || prototype === intrinsicObjectPrototype || typeof prototype === "object" && isForeignIntrinsicObjectPrototype(prototype);
97
+ }
98
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
99
+ function enumerableStringKeys(value) {
100
+ const keys = intrinsicReflectOwnKeys(value);
101
+ for (let index = 0; index < keys.length; index++) {
102
+ const key = keys[index];
103
+ if (typeof key !== "string" || !intrinsicReflectApply$1(intrinsicObjectPropertyIsEnumerable, value, [key])) return void 0;
104
+ }
105
+ return keys;
112
106
  }
113
107
  /**
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.
108
+ * Flatten one validated JSON value for the process control channel.
109
+ * @param value - the lossless JSON value to transport.
110
+ * @returns a pre-order token stream whose own nesting is bounded.
118
111
  */
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
- }
112
+ function encodePtcJsonWire(value) {
113
+ const wire = [];
114
+ const pending = [value];
115
+ for (let current = takeLast$1(pending); current !== void 0; current = takeLast$1(pending)) {
116
+ if (current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string") {
117
+ append$1(wire, current);
159
118
  continue;
160
119
  }
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
120
+ if (intrinsicArrayIsArray$1(current)) {
121
+ append$1(wire, {
122
+ kind: "array",
123
+ length: current.length
172
124
  });
125
+ for (let index = current.length - 1; index >= 0; index--) {
126
+ const item = current[index];
127
+ if (item === void 0) throw new IntrinsicError("cannot encode a sparse JSON array");
128
+ append$1(pending, item);
129
+ }
173
130
  continue;
174
131
  }
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
132
+ const keys = intrinsicObjectKeys$1(current);
133
+ append$1(wire, {
134
+ kind: "object",
135
+ keys
190
136
  });
137
+ for (let index = keys.length - 1; index >= 0; index--) {
138
+ const key = keys[index];
139
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
140
+ if (key === void 0) throw new IntrinsicError("cannot encode a missing JSON object key");
141
+ const item = current[key];
142
+ if (item === void 0) throw new IntrinsicError("cannot encode an undefined JSON object property");
143
+ append$1(pending, item);
144
+ }
145
+ }
146
+ return wire;
147
+ }
148
+ /** Whether an array contains exactly its dense indexed slots and `length`. */
149
+ function isDenseArray(value) {
150
+ if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false;
151
+ for (let index = 0; index < value.length; index++) if (!intrinsicObjectHasOwn(value, index)) return false;
152
+ return true;
153
+ }
154
+ /** Whether one exact string-key list contains a key, without consulting its prototype. */
155
+ function keysContain(keys, expected) {
156
+ for (let index = 0; index < keys.length; index++) if (keys[index] === expected) return true;
157
+ return false;
158
+ }
159
+ /** Return one exact container marker, or reject any extra/missing fields. */
160
+ function containerToken(value) {
161
+ if (intrinsicArrayIsArray$1(value) || !hasPlainObjectPrototype(value)) return void 0;
162
+ const keys = enumerableStringKeys(value);
163
+ if (keys === void 0) return void 0;
164
+ const token = value;
165
+ if (token.kind === "array") {
166
+ if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "length")) return void 0;
167
+ const length = token.length;
168
+ return typeof length === "number" && intrinsicNumberIsSafeInteger(length) && length >= 0 ? {
169
+ kind: "array",
170
+ length
171
+ } : void 0;
172
+ }
173
+ if (token.kind === "object") {
174
+ if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "keys")) return void 0;
175
+ const objectKeys = token.keys;
176
+ if (!intrinsicArrayIsArray$1(objectKeys) || !isDenseArray(objectKeys)) return void 0;
177
+ const unique = new IntrinsicSet();
178
+ const normalizedKeys = [];
179
+ const objectKeyValues = objectKeys;
180
+ for (let index = 0; index < objectKeyValues.length; index++) {
181
+ const key = objectKeyValues[index];
182
+ if (typeof key !== "string" || setHas(unique, key)) return void 0;
183
+ setAdd(unique, key);
184
+ append$1(normalizedKeys, key);
185
+ }
186
+ return {
187
+ kind: "object",
188
+ keys: normalizedKeys
189
+ };
191
190
  }
192
- return bytes;
193
191
  }
194
192
  /**
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.
193
+ * Rebuild one lossless JSON value from the flat process wire format.
194
+ * Malformed or incomplete traffic returns `undefined`; traversal is iterative
195
+ * and therefore independent of the transported value's application depth.
196
+ * @param input - untrusted message-port payload.
197
+ * @returns the detached JSON value, or `undefined` when the wire is invalid.
198
+ */
199
+ function decodePtcJsonWire(input) {
200
+ try {
201
+ if (!intrinsicArrayIsArray$1(input) || !isDenseArray(input) || input.length === 0) return void 0;
202
+ const wire = input;
203
+ const frames = [];
204
+ let root;
205
+ let rootAssigned = false;
206
+ const attach = (value) => {
207
+ const parent = frames[frames.length - 1];
208
+ if (!parent) {
209
+ if (rootAssigned) return false;
210
+ root = value;
211
+ rootAssigned = true;
212
+ return true;
213
+ }
214
+ /* v8 ignore next -- completed frames are popped before another token can attach. */
215
+ if (parent.index >= (parent.kind === "array" ? parent.length : parent.keys.length)) return false;
216
+ if (parent.kind === "array") append$1(parent.target, value);
217
+ else {
218
+ const key = parent.keys[parent.index];
219
+ /* v8 ignore next -- object frames are built from validated keys and their exact length. */
220
+ if (key === void 0) return false;
221
+ defineEnumerableDataProperty$1(parent.target, key, value);
222
+ }
223
+ parent.index += 1;
224
+ return true;
225
+ };
226
+ for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
227
+ const token = wire[tokenIndex];
228
+ let value;
229
+ let frame;
230
+ if (token === null || typeof token === "boolean" || typeof token === "string") value = token;
231
+ else if (typeof token === "number") {
232
+ if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return void 0;
233
+ value = token;
234
+ } else {
235
+ if (typeof token !== "object") return void 0;
236
+ const marker = containerToken(token);
237
+ if (!marker) return void 0;
238
+ const remainingTokens = wire.length - tokenIndex - 1;
239
+ if (marker.kind === "array") {
240
+ if (marker.length > remainingTokens) return void 0;
241
+ const target = [];
242
+ value = target;
243
+ if (marker.length > 0) frame = {
244
+ kind: "array",
245
+ target,
246
+ length: marker.length,
247
+ index: 0
248
+ };
249
+ } else {
250
+ if (marker.keys.length > remainingTokens) return void 0;
251
+ const target = {};
252
+ value = target;
253
+ if (marker.keys.length > 0) frame = {
254
+ kind: "object",
255
+ target,
256
+ keys: marker.keys,
257
+ index: 0
258
+ };
259
+ }
260
+ }
261
+ if (!attach(value)) return void 0;
262
+ if (frame) append$1(frames, frame);
263
+ while (frames.length > 0) {
264
+ const current = frames[frames.length - 1];
265
+ /* v8 ignore next -- the loop condition guarantees a final frame. */
266
+ if (current === void 0) break;
267
+ if (current.index < (current.kind === "array" ? current.length : current.keys.length)) break;
268
+ takeLast$1(frames);
269
+ }
270
+ }
271
+ return frames.length === 0 ? root : void 0;
272
+ } catch {
273
+ return;
274
+ }
275
+ }
276
+ //#endregion
277
+ //#region lib/types/bindings.js
278
+ /** Validate and capture the binding names available to one program. */
279
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
280
+ /**
281
+ * Reject unusable namespaces before starting a process.
282
+ * @param request - Host-owned binding declarations.
283
+ * @returns Namespaces keyed by their declared global.
284
+ */
285
+ function validateBindings(request) {
286
+ const bindings = /* @__PURE__ */ new Map();
287
+ for (const namespace of request.bindings) {
288
+ 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`);
289
+ if (RESERVED_BINDING_GLOBALS.has(namespace.global)) throw new Error(`dsh-ptc-runtime-node: reserved binding global ${JSON.stringify(namespace.global)}`);
290
+ if (bindings.has(namespace.global)) throw new Error(`dsh-ptc-runtime-node: duplicate binding global ${JSON.stringify(namespace.global)}`);
291
+ bindings.set(namespace.global, namespace);
292
+ }
293
+ const errorClassNames = /* @__PURE__ */ new Set();
294
+ for (const namespace of request.bindings) {
295
+ const descriptor = namespace.errorClass;
296
+ if (!descriptor) continue;
297
+ 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`);
298
+ if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) throw new Error(`dsh-ptc-runtime-node: reserved binding global ${JSON.stringify(descriptor.name)}`);
299
+ if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) throw new Error(`dsh-ptc-runtime-node: duplicate injected global ${JSON.stringify(descriptor.name)}`);
300
+ const member = descriptor.memberNameProperty;
301
+ 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`);
302
+ errorClassNames.add(descriptor.name);
303
+ }
304
+ return bindings;
305
+ }
306
+ /**
307
+ * Invoke a host binding and encode its lossless result or bounded-channel rejection.
308
+ * @param id - Program-issued correlation identifier.
309
+ * @param binding - Current evaluation's declared function, absent if it was removed.
310
+ * @param args - Validated JSON arguments detached from the program.
311
+ * @returns A reply suitable for the runtime's bounded control channel.
312
+ */
313
+ async function replyToBinding(id, binding, args) {
314
+ try {
315
+ if (!binding) throw new Error("binding is unavailable in this evaluation; reacquire the current tool");
316
+ const value = snapshotJsonValue(await binding(args));
317
+ if (value === void 0) throw new Error("binding resolution must be lossless JSON");
318
+ return {
319
+ type: "reply",
320
+ id,
321
+ ok: true,
322
+ value: encodePtcJsonWire(value)
323
+ };
324
+ } catch (error) {
325
+ return {
326
+ type: "reply",
327
+ id,
328
+ ok: false,
329
+ message: error instanceof Error ? error.message : String(error)
330
+ };
331
+ }
332
+ }
333
+ //#endregion
334
+ //#region lib/types/output-json.js
335
+ /** JSON string-prefix accounting for the outer-output ledger. @module @deepseek-ai/dsh-ptc-runtime-node/output-json */
336
+ const intrinsicReflectApply = Reflect.apply;
337
+ const intrinsicArrayIsArray = Array.isArray;
338
+ const IntrinsicBuffer = Buffer;
339
+ const intrinsicBufferByteLength = Reflect.get(Buffer, "byteLength");
340
+ const intrinsicObjectCreate = Object.create;
341
+ const intrinsicObjectDefineProperty = Object.defineProperty;
342
+ const intrinsicObjectKeys = Object.keys;
343
+ const intrinsicString = String;
344
+ const intrinsicStringCharCodeAt = Reflect.get(String.prototype, "charCodeAt");
345
+ const intrinsicStringCodePointAt = Reflect.get(String.prototype, "codePointAt");
346
+ const intrinsicStringSlice = Reflect.get(String.prototype, "slice");
347
+ /** Build a data descriptor that cannot inherit model-defined accessor fields. */
348
+ function dataDescriptor(value) {
349
+ const descriptor = intrinsicObjectCreate(null);
350
+ descriptor.value = value;
351
+ return descriptor;
352
+ }
353
+ /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
354
+ function defineEnumerableDataProperty(target, key, value) {
355
+ const descriptor = dataDescriptor(value);
356
+ descriptor.enumerable = true;
357
+ descriptor.configurable = true;
358
+ descriptor.writable = true;
359
+ intrinsicObjectDefineProperty(target, key, descriptor);
360
+ }
361
+ /** UTF-8 byte length through the module-captured Node intrinsic. */
362
+ function byteLength(text) {
363
+ return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, "utf8"]);
364
+ }
365
+ /** Append without consulting a model-mutated `Array.prototype`. */
366
+ function append(target, value) {
367
+ defineEnumerableDataProperty(target, target.length, value);
368
+ }
369
+ /** Pop without consulting a model-mutated `Array.prototype`. */
370
+ function takeLast(target) {
371
+ if (target.length === 0) return void 0;
372
+ const index = target.length - 1;
373
+ const value = target[index];
374
+ intrinsicObjectDefineProperty(target, "length", dataDescriptor(index));
375
+ return value;
376
+ }
377
+ /** One code-point-aligned character from a string. */
378
+ function characterAt(text, index) {
379
+ return intrinsicReflectApply(intrinsicStringSlice, text, [index, index + (intrinsicReflectApply(intrinsicStringCodePointAt, text, [index]) > 65535 ? 2 : 1)]);
380
+ }
381
+ /** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
382
+ function serializedCharacterBytes(character) {
383
+ if (character.length === 2) return 4;
384
+ if (character === "\"" || character === "\\") return 2;
385
+ const code = intrinsicReflectApply(intrinsicStringCharCodeAt, character, [0]);
386
+ if (code >= 55296 && code <= 57343) return 6;
387
+ if (code < 32) return code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
388
+ return byteLength(character);
389
+ }
390
+ /**
391
+ * Measure one JSON string without materializing its complete escaped form.
392
+ * @param text - the candidate string.
393
+ * @param maxBytes - largest serialized size the caller can admit.
394
+ * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
395
+ */
396
+ function jsonStringBytesUpTo(text, maxBytes) {
397
+ if (maxBytes < 2) return void 0;
398
+ let bytes = 2;
399
+ for (let index = 0; index < text.length;) {
400
+ const character = characterAt(text, index);
401
+ bytes += serializedCharacterBytes(character);
402
+ if (bytes > maxBytes) return void 0;
403
+ index += character.length;
404
+ }
405
+ return bytes;
406
+ }
407
+ /**
408
+ * Measure one lossless JSON value without allocating its serialized form.
409
+ * @param value - already validated lossless JSON.
410
+ * @param maxBytes - largest serialized size the caller can admit.
411
+ * @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
412
+ */
413
+ function jsonValueBytesUpTo(value, maxBytes) {
414
+ let bytes = 0;
415
+ const add = (cost) => {
416
+ bytes += cost;
417
+ return bytes <= maxBytes;
418
+ };
419
+ const tasks = [{
420
+ kind: "value",
421
+ value
422
+ }];
423
+ for (let task = takeLast(tasks); task !== void 0; task = takeLast(tasks)) {
424
+ if (task.kind === "value") {
425
+ const current = task.value;
426
+ if (current === null) {
427
+ if (!add(4)) return void 0;
428
+ } else if (typeof current === "string") {
429
+ const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes);
430
+ if (stringBytes === void 0) return void 0;
431
+ bytes += stringBytes;
432
+ } else if (typeof current === "number") {
433
+ if (!add(byteLength(intrinsicString(current)))) return void 0;
434
+ } else if (typeof current === "boolean") {
435
+ if (!add(current ? 4 : 5)) return void 0;
436
+ } else if (intrinsicArrayIsArray(current)) {
437
+ if (!add(2)) return void 0;
438
+ if (current.length > 0) append(tasks, {
439
+ kind: "array",
440
+ value: current,
441
+ index: 0
442
+ });
443
+ } else {
444
+ if (!add(2)) return void 0;
445
+ const keys = intrinsicObjectKeys(current);
446
+ if (keys.length > 0) append(tasks, {
447
+ kind: "object",
448
+ value: current,
449
+ keys,
450
+ index: 0
451
+ });
452
+ }
453
+ continue;
454
+ }
455
+ if (task.index > 0 && !add(1)) return void 0;
456
+ if (task.kind === "array") {
457
+ const item = task.value[task.index];
458
+ if (item === void 0) return void 0;
459
+ if (task.index + 1 < task.value.length) append(tasks, {
460
+ ...task,
461
+ index: task.index + 1
462
+ });
463
+ append(tasks, {
464
+ kind: "value",
465
+ value: item
466
+ });
467
+ continue;
468
+ }
469
+ const key = task.keys[task.index];
470
+ /* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
471
+ if (key === void 0) return void 0;
472
+ const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes);
473
+ if (keyBytes === void 0) return void 0;
474
+ if (!add(keyBytes + 1)) return void 0;
475
+ const item = task.value[key];
476
+ if (item === void 0) return void 0;
477
+ if (task.index + 1 < task.keys.length) append(tasks, {
478
+ ...task,
479
+ index: task.index + 1
480
+ });
481
+ append(tasks, {
482
+ kind: "value",
483
+ value: item
484
+ });
485
+ }
486
+ return bytes;
487
+ }
488
+ /**
489
+ * Return the longest code-point-aligned prefix whose JSON string encoding,
490
+ * including its surrounding quotes, fits `maxBytes`.
491
+ *
492
+ * @param text - the candidate string.
493
+ * @param maxBytes - serialized JSON-string bytes available.
494
+ * @returns the fitting prefix, or an empty string when even useful content cannot fit.
201
495
  */
202
496
  function truncateJsonStringBytes(text, maxBytes) {
203
497
  if (maxBytes < 2) return "";
@@ -211,7 +505,7 @@ function truncateJsonStringBytes(text, maxBytes) {
211
505
  end += character.length;
212
506
  index += character.length;
213
507
  }
214
- return end === text.length ? text : intrinsicReflectApply$1(intrinsicStringSlice, text, [0, end]);
508
+ return end === text.length ? text : intrinsicReflectApply(intrinsicStringSlice, text, [0, end]);
215
509
  }
216
510
  //#endregion
217
511
  //#region lib/types/channel.js
@@ -358,42 +652,168 @@ function resolveLaunch(config) {
358
652
  * @param fs - Filesystem mapping host bootstrap assets into the process world.
359
653
  * @param config - resolved worker invocation.
360
654
  * @param maxMessageBytes - Validated frame and queued-write limit.
655
+ * @param persistent - Select the in-memory REPL bootstrap instead of the one-shot evaluator.
361
656
  * @returns Arguments following the resolved Node executable.
362
657
  */
363
- function bootstrapArgs(fs, config, maxMessageBytes) {
658
+ function bootstrapArgs(fs, config, maxMessageBytes, persistent = false) {
364
659
  switch (config.kind) {
365
- case "embedded": return [String(maxMessageBytes)];
366
- case "node-script": return scriptBootstrapArgs(fs, config.bootstrapPath, maxMessageBytes);
660
+ case "embedded":
661
+ if (persistent) throw new Error("Persistent PTC requires a node-script launch");
662
+ return [String(maxMessageBytes)];
663
+ case "node-script": return scriptBootstrapArgs(fs, config.bootstrapPath, maxMessageBytes, persistent);
367
664
  /* v8 ignore next -- Callers supply the resolved, closed LaunchConfig union. */
368
665
  default: return assertNever(config);
369
666
  }
370
- }
371
- function scriptBootstrapArgs(fs, bootstrapPath, maxMessageBytes) {
372
- if (bootstrapPath !== void 0) return [bootstrapPath, String(maxMessageBytes)];
373
- const mapped = (path) => {
374
- const result = fs.processPathFromHostPath(path);
375
- if (result === void 0) throw new Error(`PTC runtime bootstrap is unavailable in the subprocess execution world: ${path}`);
376
- return result;
377
- };
378
- /* v8 ignore next 3 -- built-lib.e2e.ts executes the bundled provider and sibling process.js under plain Node. */
379
- if (!new URL(import.meta.url).pathname.endsWith(".ts")) return [mapped(fileURLToPath(new URL("./process.js", import.meta.url))), String(maxMessageBytes)];
380
- const entry = mapped(fileURLToPath(new URL("./process.ts", import.meta.url)));
381
- const helper = mapped(resolve(dirname(fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-subprocess/package.json"))), "src/control.ts"));
382
- return [
383
- "--input-type=module",
384
- "--eval",
385
- `const {openInheritedControlChannel}=await import(${JSON.stringify(pathToFileURL(helper).href)});const {runNodeMain}=await import(${JSON.stringify(pathToFileURL(entry).href)});await runNodeMain(openInheritedControlChannel(),${maxMessageBytes},process);`
386
- ];
387
- }
667
+ }
668
+ function scriptBootstrapArgs(fs, bootstrapPath, maxMessageBytes, persistent) {
669
+ const moduleName = persistent ? "persistent-process" : "process";
670
+ if (bootstrapPath !== void 0) return [bootstrapPath, String(maxMessageBytes)];
671
+ const mapped = (path) => {
672
+ const result = fs.processPathFromHostPath(path);
673
+ if (result === void 0) throw new Error(`PTC runtime bootstrap is unavailable in the subprocess execution world: ${path}`);
674
+ return result;
675
+ };
676
+ /* v8 ignore next 3 -- built-lib.e2e.ts executes the bundled provider and sibling process.js under plain Node. */
677
+ if (!new URL(import.meta.url).pathname.endsWith(".ts")) return [mapped(fileURLToPath(new URL(`./${moduleName}.js`, import.meta.url))), String(maxMessageBytes)];
678
+ const entry = mapped(fileURLToPath(new URL(`./${moduleName}.ts`, import.meta.url)));
679
+ const helper = mapped(resolve(dirname(fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-subprocess/package.json"))), "src/control.ts"));
680
+ const main = persistent ? "runPersistentMain" : "runNodeMain";
681
+ return [
682
+ "--input-type=module",
683
+ "--eval",
684
+ `const {openInheritedControlChannel}=await import(${JSON.stringify(pathToFileURL(helper).href)});const {${main}}=await import(${JSON.stringify(pathToFileURL(entry).href)});await ${main}(openInheritedControlChannel(),${maxMessageBytes},process);`
685
+ ];
686
+ }
687
+ //#endregion
688
+ //#region lib/types/config.js
689
+ /** Execution mode and resource limits for the Node PTC provider. */
690
+ /** Loader defaults for the Node PTC provider. */
691
+ const NodeConfig = z.object({
692
+ executionMode: z.union([z.const("per-call"), z.const("persistent-in-memory")]).default("per-call"),
693
+ timeoutMs: z.number().default(12e4),
694
+ maxTimeoutMs: z.number().default(6e5),
695
+ maxOutputBytes: z.number().default(67108864),
696
+ maxOldGenerationSizeMb: z.number().default(512),
697
+ maxMessageBytes: z.number().default(134217728),
698
+ maxPendingCalls: z.number().default(128),
699
+ graceMs: z.number().default(3e3),
700
+ launch: z.union([z.object({
701
+ kind: z.const("node-script").required(),
702
+ executable: z.string().required(),
703
+ bootstrapPath: z.string()
704
+ }), z.object({
705
+ kind: z.const("embedded").required(),
706
+ executable: z.string().required()
707
+ })])
708
+ });
709
+ /**
710
+ * Validate resolved Node limits and paths.
711
+ * @param config - Loader-defaulted fields with the selected executable.
712
+ */
713
+ function validateConfig(config) {
714
+ for (const [key, value] of Object.entries(config)) if (typeof value === "number" && (!Number.isFinite(value) || value <= 0)) throw new Error(`ptc-runtime-node: ${key} must be positive and finite`);
715
+ for (const key of [
716
+ "timeoutMs",
717
+ "maxTimeoutMs",
718
+ "graceMs"
719
+ ]) if (config[key] > MAX_TIMER_DELAY_MS) throw new Error(`ptc-runtime-node: ${key} exceeds the supported timer range`);
720
+ if (!Number.isSafeInteger(config.maxOutputBytes) || config.maxOutputBytes < 4) throw new Error("ptc-runtime-node: maxOutputBytes must be an integer of at least 4");
721
+ if (!Number.isSafeInteger(config.maxMessageBytes) || config.maxMessageBytes > 4294967295) throw new Error("ptc-runtime-node: maxMessageBytes must fit an unsigned 32-bit frame length");
722
+ if (!Number.isSafeInteger(config.maxPendingCalls)) throw new Error("ptc-runtime-node: maxPendingCalls must be an integer");
723
+ if (!Number.isSafeInteger(config.maxOldGenerationSizeMb)) throw new Error("ptc-runtime-node: maxOldGenerationSizeMb must be an integer");
724
+ }
725
+ //#endregion
726
+ //#region lib/types/policy-processes.js
727
+ /** Owns asynchronous process cleanup without delaying permission setters. */
728
+ var PolicyProcesses = class {
729
+ ctx;
730
+ processes = /* @__PURE__ */ new Set();
731
+ /**
732
+ * Begin cleanup on policy changes, even when no subsequent program runs.
733
+ * @param ctx - Node provider lifecycle and Session services.
734
+ */
735
+ constructor(ctx) {
736
+ this.ctx = ctx;
737
+ ctx.on("session/event", (session, event) => {
738
+ if (event.type !== "sandbox/mode") return;
739
+ for (const process of this.processes) {
740
+ if (process.sessionId !== session.id || process.mode === event.data.mode) continue;
741
+ process.invalidated = true;
742
+ this.release(process).catch((error) => {
743
+ ctx.logger.warn(error);
744
+ });
745
+ }
746
+ }, { global: true });
747
+ ctx.effect(() => async () => {
748
+ await this.settle([...this.processes]);
749
+ }, "Node PTC policy cleanup");
750
+ }
751
+ /**
752
+ * Track an admitted call or retained process until its owner confirms cleanup.
753
+ * @param policy - Resolved process policy, including an optional Session identity.
754
+ * @param release - Idempotent cancellation and awaited cleanup.
755
+ * @returns Removal after successful process cleanup.
756
+ */
757
+ register(policy, release) {
758
+ if (policy.sessionId === void 0) return () => {};
759
+ const session = this.ctx.get("sessions")?.get(policy.sessionId);
760
+ const process = {
761
+ sessionId: policy.sessionId,
762
+ mode: session === void 0 ? policy.mode : this.ctx.sandboxPolicy.resolve({ session }).mode,
763
+ release,
764
+ invalidated: false
765
+ };
766
+ this.processes.add(process);
767
+ return () => {
768
+ this.processes.delete(process);
769
+ };
770
+ }
771
+ /**
772
+ * Wait for invalidated processes before admitting another program.
773
+ * @param sessionId - Session carried by the resolved policy.
774
+ * @returns Completion after prior cleanup; a failure rejects and remains retryable.
775
+ */
776
+ async wait(sessionId) {
777
+ await this.settle([...this.processes].filter((process) => process.sessionId === sessionId && process.invalidated));
778
+ }
779
+ release(process) {
780
+ if (process.cleanup !== void 0) return process.cleanup;
781
+ process.cleanup = process.release().then(() => {
782
+ this.processes.delete(process);
783
+ }, (error) => {
784
+ delete process.cleanup;
785
+ throw error;
786
+ });
787
+ return process.cleanup;
788
+ }
789
+ async settle(processes) {
790
+ const failed = (await Promise.allSettled(processes.map((process) => this.release(process)))).find((result) => result.status === "rejected");
791
+ if (failed?.status === "rejected") throw failed.reason;
792
+ }
793
+ };
794
+ //#endregion
795
+ //#region lib/types/environment.js
796
+ /** Startup variables required by native executables before model evaluation. */
797
+ /** Native executable search, Windows system paths, and sandbox temporary paths retained in the OS environment. */
798
+ const STARTUP_ENVIRONMENT_NAMES = new Set([
799
+ "PATH",
800
+ "PATHEXT",
801
+ "SYSTEMROOT",
802
+ "WINDIR",
803
+ "TEMP",
804
+ "TMP"
805
+ ]);
388
806
  //#endregion
389
807
  //#region lib/types/output-ledger.js
390
808
  /** One run's combined outer-output ledger; binding values never enter it. */
391
809
  var OutputLedger = class {
392
810
  maxBytes;
811
+ overflowMessage;
393
812
  bytes = 2;
394
813
  entries = 0;
395
- constructor(maxBytes) {
814
+ constructor(maxBytes, overflowMessage) {
396
815
  this.maxBytes = maxBytes;
816
+ this.overflowMessage = overflowMessage;
397
817
  }
398
818
  /** Admit one exact log entry, or report that the hard cap was crossed. */
399
819
  admit(text, sink) {
@@ -423,7 +843,7 @@ var OutputLedger = class {
423
843
  }
424
844
  /** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
425
845
  limit(logs) {
426
- const fullMessage = `outer output exceeded ${this.maxBytes} bytes`;
846
+ const fullMessage = this.overflowMessage ?? `outer output exceeded ${this.maxBytes} bytes`;
427
847
  const messageBytes = fullMessage.length + 2;
428
848
  const retained = [];
429
849
  let retainedBytes = 2;
@@ -435,344 +855,546 @@ var OutputLedger = class {
435
855
  if (stringBytes !== void 0) {
436
856
  retained.push(text);
437
857
  retainedBytes += stringBytes + separatorBytes;
438
- continue;
439
- }
440
- const prefix = truncateJsonStringBytes(text, availableBytes);
441
- if (prefix.length > 0) {
442
- const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes);
443
- /* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
444
- if (prefixBytes === void 0) throw new Error("output ledger produced an oversized log prefix");
445
- retained.push(prefix);
446
- retainedBytes += prefixBytes + separatorBytes;
447
- }
448
- break;
449
- }
450
- return {
451
- logs: retained,
452
- error: {
453
- kind: "output-limit",
454
- message: truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
455
- }
456
- };
457
- }
458
- };
459
- //#endregion
460
- //#region lib/types/output-stream.js
461
- /**
462
- * Wait for queued bytes without letting an inherited descriptor retain a run forever.
463
- * @param stream - Caller-owned raw process output, when provided.
464
- * @param graceMs - Maximum wait after managed process termination.
465
- * @returns Whether the complete stream ended without a transport error.
466
- */
467
- function drainOutput(stream, graceMs) {
468
- if (stream === void 0 || stream.readableEnded) return Promise.resolve(true);
469
- if (stream.destroyed) return Promise.resolve(false);
470
- return new Promise((resolve) => {
471
- const finish = (complete) => {
472
- clearTimeout(timer);
473
- stream.off("end", onEnd);
474
- stream.off("close", onClose);
475
- stream.off("error", onError);
476
- resolve(complete);
477
- };
478
- const onEnd = () => {
479
- finish(true);
480
- };
481
- const onClose = () => {
482
- finish(false);
483
- };
484
- const onError = () => {
485
- finish(false);
486
- };
487
- const timer = setTimeout(() => {
488
- finish(false);
489
- }, graceMs);
490
- stream.once("end", onEnd);
491
- stream.once("close", onClose);
492
- stream.once("error", onError);
493
- });
494
- }
495
- //#endregion
496
- //#region lib/types/environment.js
497
- /** Startup variables required by native executables before model evaluation. */
498
- /** Native executable search, Windows system paths, and sandbox temporary paths retained in the OS environment. */
499
- const STARTUP_ENVIRONMENT_NAMES = new Set([
500
- "PATH",
501
- "PATHEXT",
502
- "SYSTEMROOT",
503
- "WINDIR",
504
- "TEMP",
505
- "TMP"
506
- ]);
507
- //#endregion
508
- //#region lib/types/json-wire.js
509
- /**
510
- * Lossless-JSON snapshots for the dependency-free source bootstrap closure.
511
- * @module @deepseek-ai/dsh-ptc-runtime-node/json-wire
512
- */
513
- const intrinsicFunctionToString = Reflect.get(Function.prototype, "toString");
514
- const intrinsicReflectApply = Reflect.get(Reflect, "apply");
515
- const IntrinsicError = Error;
516
- const IntrinsicSet = Set;
517
- const intrinsicArrayIsArray = Array.isArray;
518
- const intrinsicArrayPrototype = Array.prototype;
519
- const intrinsicNumberIsFinite = Number.isFinite;
520
- const intrinsicNumberIsSafeInteger = Number.isSafeInteger;
521
- const intrinsicObjectCreate = Object.create;
522
- const intrinsicObjectDefineProperty = Object.defineProperty;
523
- const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
524
- const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf;
525
- const intrinsicObjectHasOwn = Object.hasOwn;
526
- const intrinsicObjectIs = Object.is;
527
- const intrinsicObjectKeys = Object.keys;
528
- const intrinsicObjectPrototype = Object.prototype;
529
- const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, "propertyIsEnumerable");
530
- const intrinsicReflectOwnKeys = Reflect.ownKeys;
531
- const intrinsicSetAdd = Reflect.get(Set.prototype, "add");
532
- Reflect.get(Set.prototype, "delete");
533
- const intrinsicSetHas = Reflect.get(Set.prototype, "has");
534
- /** Build a data descriptor that cannot inherit model-defined accessor fields. */
535
- function dataDescriptor(value) {
536
- const descriptor = intrinsicObjectCreate(null);
537
- descriptor.value = value;
538
- return descriptor;
539
- }
540
- /** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
541
- function defineEnumerableDataProperty(target, key, value) {
542
- const descriptor = dataDescriptor(value);
543
- descriptor.enumerable = true;
544
- descriptor.configurable = true;
545
- descriptor.writable = true;
546
- intrinsicObjectDefineProperty(target, key, descriptor);
547
- }
548
- /** Append without consulting a model-mutated `Array.prototype`. */
549
- function append(target, value) {
550
- defineEnumerableDataProperty(target, target.length, value);
551
- }
552
- /** Pop without consulting a model-mutated `Array.prototype`. */
553
- function takeLast(target) {
554
- if (target.length === 0) return void 0;
555
- const index = target.length - 1;
556
- const value = target[index];
557
- intrinsicObjectDefineProperty(target, "length", dataDescriptor(index));
558
- return value;
559
- }
560
- /** Whether one captured-intrinsic Set contains a value. */
561
- function setHas(target, value) {
562
- return intrinsicReflectApply(intrinsicSetHas, target, [value]);
563
- }
564
- /** Add to one captured-intrinsic Set. */
565
- function setAdd(target, value) {
566
- intrinsicReflectApply(intrinsicSetAdd, target, [value]);
567
- }
568
- /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
569
- function hasIntrinsicConstructor(prototype, name) {
570
- const constructor = intrinsicObjectGetOwnPropertyDescriptor(prototype, "constructor")?.value;
571
- if (typeof constructor !== "function") return false;
572
- try {
573
- return constructor.name === name && constructor.prototype === prototype && intrinsicReflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`;
574
- } catch {
575
- return false;
576
- }
577
- }
578
- /** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
579
- function isForeignIntrinsicObjectPrototype(value) {
580
- return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
581
- }
582
- /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
583
- function hasPlainArrayPrototype(value) {
584
- const prototype = intrinsicObjectGetPrototypeOf(value);
585
- if (prototype === intrinsicArrayPrototype) return true;
586
- if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
587
- const objectPrototype = intrinsicObjectGetPrototypeOf(prototype);
588
- return typeof objectPrototype === "object" && objectPrototype !== null && isForeignIntrinsicObjectPrototype(objectPrototype);
589
- }
590
- /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
591
- function hasPlainObjectPrototype(value) {
592
- const prototype = intrinsicObjectGetPrototypeOf(value);
593
- return prototype === null || prototype === intrinsicObjectPrototype || typeof prototype === "object" && isForeignIntrinsicObjectPrototype(prototype);
594
- }
595
- /** Return every JSON-visible object key, or reject own data JSON would discard. */
596
- function enumerableStringKeys(value) {
597
- const keys = intrinsicReflectOwnKeys(value);
598
- for (let index = 0; index < keys.length; index++) {
599
- const key = keys[index];
600
- if (typeof key !== "string" || !intrinsicReflectApply(intrinsicObjectPropertyIsEnumerable, value, [key])) return void 0;
601
- }
602
- return keys;
603
- }
604
- /**
605
- * Flatten one validated JSON value for the process control channel.
606
- * @param value - the lossless JSON value to transport.
607
- * @returns a pre-order token stream whose own nesting is bounded.
608
- */
609
- function encodePtcJsonWire(value) {
610
- const wire = [];
611
- const pending = [value];
612
- for (let current = takeLast(pending); current !== void 0; current = takeLast(pending)) {
613
- if (current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string") {
614
- append(wire, current);
615
- continue;
616
- }
617
- if (intrinsicArrayIsArray(current)) {
618
- append(wire, {
619
- kind: "array",
620
- length: current.length
621
- });
622
- for (let index = current.length - 1; index >= 0; index--) {
623
- const item = current[index];
624
- if (item === void 0) throw new IntrinsicError("cannot encode a sparse JSON array");
625
- append(pending, item);
626
- }
627
- continue;
628
- }
629
- const keys = intrinsicObjectKeys(current);
630
- append(wire, {
631
- kind: "object",
632
- keys
633
- });
634
- for (let index = keys.length - 1; index >= 0; index--) {
635
- const key = keys[index];
636
- /* v8 ignore next -- the loop is bounded by the captured key count. */
637
- if (key === void 0) throw new IntrinsicError("cannot encode a missing JSON object key");
638
- const item = current[key];
639
- if (item === void 0) throw new IntrinsicError("cannot encode an undefined JSON object property");
640
- append(pending, item);
641
- }
642
- }
643
- return wire;
644
- }
645
- /** Whether an array contains exactly its dense indexed slots and `length`. */
646
- function isDenseArray(value) {
647
- if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false;
648
- for (let index = 0; index < value.length; index++) if (!intrinsicObjectHasOwn(value, index)) return false;
649
- return true;
650
- }
651
- /** Whether one exact string-key list contains a key, without consulting its prototype. */
652
- function keysContain(keys, expected) {
653
- for (let index = 0; index < keys.length; index++) if (keys[index] === expected) return true;
654
- return false;
655
- }
656
- /** Return one exact container marker, or reject any extra/missing fields. */
657
- function containerToken(value) {
658
- if (intrinsicArrayIsArray(value) || !hasPlainObjectPrototype(value)) return void 0;
659
- const keys = enumerableStringKeys(value);
660
- if (keys === void 0) return void 0;
661
- const token = value;
662
- if (token.kind === "array") {
663
- if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "length")) return void 0;
664
- const length = token.length;
665
- return typeof length === "number" && intrinsicNumberIsSafeInteger(length) && length >= 0 ? {
666
- kind: "array",
667
- length
668
- } : void 0;
669
- }
670
- if (token.kind === "object") {
671
- if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "keys")) return void 0;
672
- const objectKeys = token.keys;
673
- if (!intrinsicArrayIsArray(objectKeys) || !isDenseArray(objectKeys)) return void 0;
674
- const unique = new IntrinsicSet();
675
- const normalizedKeys = [];
676
- const objectKeyValues = objectKeys;
677
- for (let index = 0; index < objectKeyValues.length; index++) {
678
- const key = objectKeyValues[index];
679
- if (typeof key !== "string" || setHas(unique, key)) return void 0;
680
- setAdd(unique, key);
681
- append(normalizedKeys, key);
858
+ continue;
859
+ }
860
+ const prefix = truncateJsonStringBytes(text, availableBytes);
861
+ if (prefix.length > 0) {
862
+ const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes);
863
+ /* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
864
+ if (prefixBytes === void 0) throw new Error("output ledger produced an oversized log prefix");
865
+ retained.push(prefix);
866
+ retainedBytes += prefixBytes + separatorBytes;
867
+ }
868
+ break;
682
869
  }
683
870
  return {
684
- kind: "object",
685
- keys: normalizedKeys
871
+ logs: retained,
872
+ error: {
873
+ kind: "output-limit",
874
+ message: truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
875
+ }
686
876
  };
687
877
  }
878
+ };
879
+ //#endregion
880
+ //#region lib/types/output-stream.js
881
+ /**
882
+ * Wait for queued bytes without letting an inherited descriptor retain a run forever.
883
+ * @param stream - Caller-owned raw process output, when provided.
884
+ * @param graceMs - Maximum wait after managed process termination.
885
+ * @returns Whether the complete stream ended without a transport error.
886
+ */
887
+ function drainOutput(stream, graceMs) {
888
+ if (stream === void 0 || stream.readableEnded) return Promise.resolve(true);
889
+ if (stream.destroyed) return Promise.resolve(false);
890
+ return new Promise((resolve) => {
891
+ const finish = (complete) => {
892
+ clearTimeout(timer);
893
+ stream.off("end", onEnd);
894
+ stream.off("close", onClose);
895
+ stream.off("error", onError);
896
+ resolve(complete);
897
+ };
898
+ const onEnd = () => {
899
+ finish(true);
900
+ };
901
+ const onClose = () => {
902
+ finish(false);
903
+ };
904
+ const onError = () => {
905
+ finish(false);
906
+ };
907
+ const timer = setTimeout(() => {
908
+ finish(false);
909
+ }, graceMs);
910
+ stream.once("end", onEnd);
911
+ stream.once("close", onClose);
912
+ stream.once("error", onError);
913
+ });
688
914
  }
689
915
  /**
690
- * Rebuild one lossless JSON value from the flat process wire format.
691
- * Malformed or incomplete traffic returns `undefined`; traversal is iterative
692
- * and therefore independent of the transported value's application depth.
693
- * @param input - untrusted message-port payload.
694
- * @returns the detached JSON value, or `undefined` when the wire is invalid.
916
+ * Terminate a managed process and drain its pipes, including failures before output listeners attach.
917
+ * @param handle - Process and streams owned by the runtime.
918
+ * @param graceMs - Maximum pipe-drain wait after managed exit.
919
+ * @returns Whether both output streams ended cleanly.
695
920
  */
696
- function decodePtcJsonWire(input) {
921
+ async function terminateProcess(handle, graceMs) {
697
922
  try {
698
- if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return void 0;
699
- const wire = input;
700
- const frames = [];
701
- let root;
702
- let rootAssigned = false;
703
- const attach = (value) => {
704
- const parent = frames[frames.length - 1];
705
- if (!parent) {
706
- if (rootAssigned) return false;
707
- root = value;
708
- rootAssigned = true;
709
- return true;
923
+ handle.terminate();
924
+ handle.stdout?.resume();
925
+ handle.stderr?.resume();
926
+ await Promise.all([handle.done.catch(() => {}), handle.waitForExit()]);
927
+ return !(await Promise.all([drainOutput(handle.stdout, graceMs), drainOutput(handle.stderr, graceMs)])).includes(false);
928
+ } finally {
929
+ handle.stdout?.destroy();
930
+ handle.stderr?.destroy();
931
+ }
932
+ }
933
+ //#endregion
934
+ //#region lib/types/persistent.js
935
+ /** Node PTC in-memory execution with retained processes scoped to caller-owned objects. */
936
+ const messageOf$1 = (error) => error instanceof Error ? error.message : String(error);
937
+ const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
938
+ function persistentOutput(maxBytes) {
939
+ return new OutputLedger(maxBytes, `retained variables were cleared; outer output exceeded ${maxBytes} bytes`);
940
+ }
941
+ /** One managed process; all mutable program state belongs to this process alone. */
942
+ var Kernel = class {
943
+ ctx;
944
+ config;
945
+ policy;
946
+ unregister;
947
+ confined;
948
+ handle;
949
+ channel;
950
+ active;
951
+ /** Admission remains reserved until the result includes completed process cleanup. */
952
+ running = false;
953
+ cleanupPending = false;
954
+ ready = false;
955
+ sequence = 0;
956
+ nextCall = 1;
957
+ stderr = "";
958
+ stopped;
959
+ starting;
960
+ exitOutcome;
961
+ controller = new AbortController();
962
+ closed = false;
963
+ constructor(ctx, config, policy, processes) {
964
+ this.ctx = ctx;
965
+ this.config = config;
966
+ this.policy = policy;
967
+ this.unregister = processes.register(policy, () => this.dispose());
968
+ }
969
+ assertIdle() {
970
+ if (this.running) throw new Error("persistent PTC does not accept concurrent runs for one state owner");
971
+ if (this.cleanupPending) throw new Error("persistent PTC environment cleanup is incomplete; release the owner before running another program");
972
+ }
973
+ async run(spec, bindings, code) {
974
+ this.running = true;
975
+ const sandbox = {
976
+ mode: this.policy.mode,
977
+ denied: false
978
+ };
979
+ try {
980
+ const result = await this.evaluate(spec, bindings, code);
981
+ if (this.confined !== void 0) {
982
+ sandbox.enforcement = this.confined.enforcement;
983
+ sandbox.denied = result.error?.kind !== "sandbox-unavailable" && this.confined.denialSignatures.some((signature) => result.error?.message.toLowerCase().includes(signature.toLowerCase()));
710
984
  }
711
- /* v8 ignore next -- completed frames are popped before another token can attach. */
712
- if (parent.index >= (parent.kind === "array" ? parent.length : parent.keys.length)) return false;
713
- if (parent.kind === "array") append(parent.target, value);
714
- else {
715
- const key = parent.keys[parent.index];
716
- /* v8 ignore next -- object frames are built from validated keys and their exact length. */
717
- if (key === void 0) return false;
718
- defineEnumerableDataProperty(parent.target, key, value);
985
+ return {
986
+ ...result,
987
+ sandbox
988
+ };
989
+ } finally {
990
+ this.running = false;
991
+ }
992
+ }
993
+ async evaluate(spec, bindings, code) {
994
+ if (this.exitOutcome !== void 0) {
995
+ const output = persistentOutput(this.config.maxOutputBytes);
996
+ const failure = spec.signal?.aborted ? {
997
+ kind: "abort",
998
+ message: "execution canceled; retained variables were cleared"
999
+ } : {
1000
+ kind: "worker-exit",
1001
+ message: `${this.exitMessage(this.exitOutcome)}; retained variables were cleared`
1002
+ };
1003
+ try {
1004
+ await this.stop();
1005
+ } catch (error) {
1006
+ return output.failure([], {
1007
+ kind: "worker-exit",
1008
+ message: `managed process cleanup failed: ${messageOf$1(error)}`
1009
+ });
719
1010
  }
720
- parent.index += 1;
721
- return true;
1011
+ return output.failure([], failure);
1012
+ }
1013
+ this.stderr = "";
1014
+ const output = persistentOutput(this.config.maxOutputBytes);
1015
+ const logs = [];
1016
+ const result = Promise.withResolvers();
1017
+ const timer = spec.timeoutMs === null ? void 0 : setTimeout(() => {
1018
+ evaluation.finish({
1019
+ kind: "timeout",
1020
+ message: `execution deadline reached (${spec.timeoutMs}ms); retained variables were cleared`
1021
+ }, void 0, true);
1022
+ }, spec.timeoutMs);
1023
+ const onAbort = () => {
1024
+ evaluation.finish({
1025
+ kind: "abort",
1026
+ message: "execution canceled; retained variables were cleared"
1027
+ }, void 0, true);
722
1028
  };
723
- for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
724
- const token = wire[tokenIndex];
725
- let value;
726
- let frame;
727
- if (token === null || typeof token === "boolean" || typeof token === "string") value = token;
728
- else if (typeof token === "number") {
729
- if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return void 0;
730
- value = token;
731
- } else {
732
- if (typeof token !== "object") return void 0;
733
- const marker = containerToken(token);
734
- if (!marker) return void 0;
735
- const remainingTokens = wire.length - tokenIndex - 1;
736
- if (marker.kind === "array") {
737
- if (marker.length > remainingTokens) return void 0;
738
- const target = [];
739
- value = target;
740
- if (marker.length > 0) frame = {
741
- kind: "array",
742
- target,
743
- length: marker.length,
744
- index: 0
1029
+ const evaluation = {
1030
+ id: ++this.sequence,
1031
+ data: {
1032
+ code,
1033
+ maxOutputBytes: this.config.maxOutputBytes,
1034
+ namespaces: [...bindings.values()].map((binding) => ({
1035
+ global: binding.global,
1036
+ names: Object.keys(binding.functions),
1037
+ ...binding.errorClass ? { errorClass: binding.errorClass } : {}
1038
+ }))
1039
+ },
1040
+ spec,
1041
+ bindings,
1042
+ reply: AsyncLocalStorage.bind(replyToBinding),
1043
+ pending: 0,
1044
+ pendingBytes: 0,
1045
+ admit: (text) => {
1046
+ if (!output.admit(text, logs)) {
1047
+ logs.push(text);
1048
+ evaluation.finish({
1049
+ kind: "output-limit",
1050
+ message: "output limit exceeded"
1051
+ }, void 0, true);
1052
+ }
1053
+ },
1054
+ finish: (error, value, discard = false) => {
1055
+ if (this.active !== evaluation) return;
1056
+ clearTimeout(timer);
1057
+ spec.signal?.removeEventListener("abort", onAbort);
1058
+ this.active = void 0;
1059
+ const outcome = error?.kind === "output-limit" ? output.limit(logs) : error ? output.failure(logs, error) : output.success(logs, value);
1060
+ (discard || outcome.error?.kind === "output-limit" || spec.stateOwner === void 0 ? this.stop() : Promise.resolve(true)).then((drained) => {
1061
+ result.resolve(drained ? outcome : output.failure(logs, {
1062
+ kind: "worker-exit",
1063
+ message: "managed process exited but its output did not drain cleanly"
1064
+ }));
1065
+ }, (failure) => {
1066
+ result.resolve(output.failure(logs, {
1067
+ kind: "worker-exit",
1068
+ message: `managed process cleanup failed: ${messageOf$1(failure)}`
1069
+ }));
1070
+ });
1071
+ }
1072
+ };
1073
+ this.active = evaluation;
1074
+ spec.signal?.addEventListener("abort", onAbort, { once: true });
1075
+ if (spec.signal?.aborted) onAbort();
1076
+ else if (this.ready) this.dispatch(evaluation);
1077
+ else {
1078
+ this.starting = this.start(spec.cwd);
1079
+ this.starting.catch((error) => {
1080
+ this.fail(error instanceof SandboxUnavailableError || this.confined !== void 0 && isRunnerSpawnFailure(error, this.confined.argv[0], spec.cwd) ? "sandbox-unavailable" : "worker-exit", messageOf$1(error));
1081
+ });
1082
+ }
1083
+ return await result.promise;
1084
+ }
1085
+ async dispose() {
1086
+ this.active?.finish({
1087
+ kind: "abort",
1088
+ message: "PTC environment disposed; retained variables were cleared"
1089
+ }, void 0, true);
1090
+ await this.stop();
1091
+ }
1092
+ stop() {
1093
+ if (this.stopped) return this.stopped;
1094
+ this.closed = true;
1095
+ this.cleanupPending = true;
1096
+ this.controller.abort();
1097
+ this.channel?.close();
1098
+ this.stopped = (async () => {
1099
+ await this.starting?.catch(() => {});
1100
+ const handle = this.handle;
1101
+ return handle === void 0 || await terminateProcess(handle, this.config.graceMs);
1102
+ })().then((drained) => {
1103
+ this.unregister?.();
1104
+ this.cleanupPending = false;
1105
+ if (!drained) this.ctx.logger.warn("persistent Node process exited but its output did not drain cleanly");
1106
+ return drained;
1107
+ }, (error) => {
1108
+ this.stopped = void 0;
1109
+ throw error;
1110
+ });
1111
+ return this.stopped;
1112
+ }
1113
+ fail(kind, message) {
1114
+ if (this.active) this.active.finish({
1115
+ kind,
1116
+ message: `${message}; retained variables were cleared`
1117
+ }, void 0, true);
1118
+ else {
1119
+ if (!this.closed) this.ctx.logger.warn(message);
1120
+ this.stop().catch((error) => {
1121
+ this.ctx.logger.warn(messageOf$1(error));
1122
+ });
1123
+ }
1124
+ }
1125
+ dispatch(evaluation) {
1126
+ this.channel?.send({
1127
+ type: "run",
1128
+ id: evaluation.id,
1129
+ cwd: evaluation.spec.cwd,
1130
+ data: evaluation.data
1131
+ }).catch((error) => {
1132
+ this.fail("protocol", messageOf$1(error));
1133
+ });
1134
+ }
1135
+ exitMessage(outcome) {
1136
+ return `Node process exited (${String(outcome.exitCode)}, signal ${String(outcome.signal)})${this.stderr ? `: ${this.stderr}` : ""}`;
1137
+ }
1138
+ async start(cwd) {
1139
+ const executable = await this.ctx.subprocess.resolveExecutable(this.config.launch.executable, void 0, this.controller.signal);
1140
+ if (this.closed) return;
1141
+ const argv = [
1142
+ executable,
1143
+ `--max-old-space-size=${this.config.maxOldGenerationSizeMb}`,
1144
+ ...bootstrapArgs(this.ctx.fs, this.config.launch, this.config.maxMessageBytes, true)
1145
+ ];
1146
+ const policy = this.policy;
1147
+ this.confined = policy.mode === "danger-full-access" ? void 0 : await this.ctx.sandbox.confine(argv, {
1148
+ ...policy,
1149
+ mode: policy.mode
1150
+ }, this.controller.signal);
1151
+ if (this.closed) return;
1152
+ const env = Object.fromEntries(Object.keys(process.env).filter((key) => !STARTUP_ENVIRONMENT_NAMES.has(key.toUpperCase()) && key.toUpperCase() !== "ELECTRON_RUN_AS_NODE").map((key) => [key, void 0]));
1153
+ const handle = this.ctx.subprocess.spawn({
1154
+ argv: this.confined?.argv ?? argv,
1155
+ cwd,
1156
+ env,
1157
+ stdio: {
1158
+ stdin: "ignore",
1159
+ stdout: "pipe",
1160
+ stderr: "pipe",
1161
+ control: "pipe"
1162
+ },
1163
+ graceMs: this.config.graceMs
1164
+ });
1165
+ this.handle = handle;
1166
+ if (!handle.control || !handle.stdout || !handle.stderr) throw new Error("subprocess provider did not supply persistent PTC pipes");
1167
+ for (const stream of [handle.stdout, handle.stderr]) {
1168
+ stream.setEncoding("utf8");
1169
+ stream.on("data", (text) => {
1170
+ if (stream === handle.stderr) this.stderr = (this.stderr + text).slice(-this.config.maxOutputBytes);
1171
+ this.active?.admit(text);
1172
+ });
1173
+ stream.on("error", (error) => {
1174
+ this.fail("worker-exit", error.message);
1175
+ });
1176
+ }
1177
+ this.channel = new JsonChannel(handle.control, this.config.maxMessageBytes, (raw, bytes) => {
1178
+ this.receive(raw, bytes);
1179
+ }, (error, kind) => {
1180
+ if (kind === "protocol") this.fail("protocol", error.message);
1181
+ else setImmediate(() => {
1182
+ if (!this.closed && this.exitOutcome === void 0) handle.terminate();
1183
+ });
1184
+ });
1185
+ handle.done.then(async (outcome) => {
1186
+ this.exitOutcome = outcome;
1187
+ await Promise.all([drainOutput(handle.stdout, this.config.graceMs), drainOutput(handle.stderr, this.config.graceMs)]);
1188
+ setImmediate(() => {
1189
+ if (!this.closed) this.fail(this.confined !== void 0 && classifyRunnerFailure(outcome.exitCode, this.stderr, this.confined.runnerFailureRules) !== void 0 ? "sandbox-unavailable" : "worker-exit", this.exitMessage(outcome));
1190
+ });
1191
+ }, (error) => {
1192
+ this.fail("worker-exit", messageOf$1(error));
1193
+ });
1194
+ }
1195
+ receive(raw, bytes) {
1196
+ if (!record$1(raw)) {
1197
+ this.fail("protocol", "invalid persistent PTC frame");
1198
+ return;
1199
+ }
1200
+ const evaluation = this.active;
1201
+ if (!evaluation) {
1202
+ this.fail("protocol", "frame arrived without an active PTC evaluation");
1203
+ return;
1204
+ }
1205
+ if (!this.ready) {
1206
+ if (raw.type !== "ready") {
1207
+ this.fail("protocol", "expected persistent PTC readiness");
1208
+ return;
1209
+ }
1210
+ this.ready = true;
1211
+ this.dispatch(evaluation);
1212
+ return;
1213
+ }
1214
+ if (raw.id !== evaluation.id || !record$1(raw.payload)) {
1215
+ this.fail("protocol", "frame does not belong to the active PTC evaluation");
1216
+ return;
1217
+ }
1218
+ const payload = raw.payload;
1219
+ switch (payload.type) {
1220
+ case "log":
1221
+ if (typeof payload.text === "string") evaluation.admit(payload.text);
1222
+ else this.fail("protocol", "invalid persistent PTC log");
1223
+ return;
1224
+ case "output-limit":
1225
+ evaluation.finish({
1226
+ kind: "output-limit",
1227
+ message: "output limit exceeded"
1228
+ }, void 0, true);
1229
+ return;
1230
+ case "done":
1231
+ if (payload.error !== void 0) {
1232
+ const error = payload.error;
1233
+ if (!record$1(error) || typeof error.message !== "string" || error.kind !== "exception" && error.kind !== "invalid-output" && error.kind !== "output-limit") {
1234
+ this.fail("protocol", "invalid persistent PTC failure");
1235
+ return;
1236
+ }
1237
+ const failure = {
1238
+ kind: error.kind,
1239
+ message: error.message
745
1240
  };
1241
+ setImmediate(() => {
1242
+ evaluation.finish(failure, void 0, failure.kind === "output-limit");
1243
+ });
746
1244
  } else {
747
- if (marker.keys.length > remainingTokens) return void 0;
748
- const target = {};
749
- value = target;
750
- if (marker.keys.length > 0) frame = {
751
- kind: "object",
752
- target,
753
- keys: marker.keys,
754
- index: 0
755
- };
1245
+ const value = payload.value === void 0 ? void 0 : decodePtcJsonWire(payload.value);
1246
+ const failure = payload.value !== void 0 && value === void 0 ? {
1247
+ kind: "invalid-output",
1248
+ message: "program completion must be lossless JSON"
1249
+ } : void 0;
1250
+ setImmediate(() => {
1251
+ evaluation.finish(failure, value);
1252
+ });
1253
+ }
1254
+ return;
1255
+ case "call": {
1256
+ if (payload.id !== this.nextCall || typeof payload.global !== "string" || typeof payload.name !== "string") {
1257
+ this.fail("protocol", "invalid persistent PTC binding identity");
1258
+ return;
1259
+ }
1260
+ this.nextCall += 1;
1261
+ const args = decodePtcJsonWire(payload.args);
1262
+ if (args === void 0) {
1263
+ this.fail("protocol", "binding arguments must be lossless JSON");
1264
+ return;
1265
+ }
1266
+ if (++evaluation.pending > this.config.maxPendingCalls || (evaluation.pendingBytes += bytes) > this.config.maxMessageBytes) {
1267
+ this.fail("protocol", "pending binding calls exceed configured limits");
1268
+ return;
756
1269
  }
1270
+ const functions = evaluation.bindings.get(payload.global)?.functions;
1271
+ const fn = functions && Object.hasOwn(functions, payload.name) ? functions[payload.name] : void 0;
1272
+ const id = payload.id;
1273
+ (async () => {
1274
+ const reply = await evaluation.reply(id, fn, args).finally(() => {
1275
+ evaluation.pending -= 1;
1276
+ evaluation.pendingBytes -= bytes;
1277
+ });
1278
+ if (this.active === evaluation) await this.channel?.send(reply);
1279
+ })().catch((error) => {
1280
+ this.fail("protocol", messageOf$1(error));
1281
+ });
1282
+ return;
757
1283
  }
758
- if (!attach(value)) return void 0;
759
- if (frame) append(frames, frame);
760
- while (frames.length > 0) {
761
- const current = frames[frames.length - 1];
762
- /* v8 ignore next -- the loop condition guarantees a final frame. */
763
- if (current === void 0) break;
764
- if (current.index < (current.kind === "array" ? current.length : current.keys.length)) break;
765
- takeLast(frames);
1284
+ default: this.fail("protocol", "unknown persistent PTC message");
1285
+ }
1286
+ }
1287
+ };
1288
+ /** Sandboxed, best-effort retained state; restart, disposal, and interrupted execution clear it. */
1289
+ var InMemoryExecution = class {
1290
+ ctx;
1291
+ config;
1292
+ processes;
1293
+ /**
1294
+ * Model-facing rules for retained variables, authority, output, and recovery.
1295
+ * @returns Usage guidance appended to the run_code schema.
1296
+ */
1297
+ get executionInstructions() {
1298
+ return "The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Changing the session sandbox mode clears retained variables. An approved sandbox escalation runs in a separate temporary environment: it cannot access retained variables, and its declarations are discarded afterward. Restart, Agent disposal, cancellation, timeout, output overflow, or process failure clears the environment. If a variable is undefined, recreate it and reacquire its resources. State is not restored from the conversation, and failed evaluations can leave earlier mutations in memory. Redeclaring an existing let, const, or class name fails; assign to a mutable binding or use a new name. Await all tool calls; background callbacks cannot call tools after their evaluation ends. Await child-process completion before ending an evaluation; raw subprocess output belongs to whichever evaluation is active when it arrives.";
1299
+ }
1300
+ owners = /* @__PURE__ */ new WeakMap();
1301
+ kernels = /* @__PURE__ */ new Set();
1302
+ disposed = false;
1303
+ /**
1304
+ * Own retained execution resources under the existing Node provider.
1305
+ * @param ctx - Provider lifecycle and execution services.
1306
+ * @param config - Validated limits and selected executable.
1307
+ * @param processes - Provider-owned policy invalidation and cleanup.
1308
+ */
1309
+ constructor(ctx, config, processes) {
1310
+ this.ctx = ctx;
1311
+ this.config = config;
1312
+ this.processes = processes;
1313
+ if (this.config.launch.kind === "embedded") throw new Error("Persistent PTC requires a node-script launch");
1314
+ ctx.effect(() => async () => {
1315
+ this.disposed = true;
1316
+ const results = await Promise.allSettled([...this.kernels].map((kernel) => kernel.dispose()));
1317
+ this.kernels.clear();
1318
+ const failed = results.find((result) => result.status === "rejected");
1319
+ if (failed?.status === "rejected") throw failed.reason;
1320
+ }, "Persistent PTC environments");
1321
+ }
1322
+ /**
1323
+ * Resolve the file policy, directory and deadline for a retained REPL.
1324
+ * @param request - Program, bindings, owner, directory and elapsed budget.
1325
+ * @returns Resolved inputs with the selected file policy.
1326
+ */
1327
+ resolve(request) {
1328
+ if (this.disposed) throw new Error("persistent PTC: resolve after disposal");
1329
+ const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
1330
+ const cwd = request.cwd ?? sandboxPolicy.workspaceRoot;
1331
+ if (!isAbsolute(cwd)) throw new Error("persistent PTC: cwd must be absolute");
1332
+ return {
1333
+ ...request,
1334
+ sandboxPolicy,
1335
+ cwd,
1336
+ timeoutMs: request.timeoutMs === null ? null : clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, "persistent PTC timeoutMs")
1337
+ };
1338
+ }
1339
+ /**
1340
+ * Evaluate statements using this owner's process, or a temporary process without an owner.
1341
+ * @param spec - Resolved request; concurrent calls for one owner are rejected.
1342
+ * @returns Logs, a final JSON expression, or a structured execution failure.
1343
+ */
1344
+ async run(spec) {
1345
+ if (this.disposed) throw new Error("persistent PTC: run after disposal");
1346
+ if (spec.sandboxPolicy === void 0 || !isAbsolute(spec.cwd) || spec.timeoutMs !== null && (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0 || spec.timeoutMs > this.config.maxTimeoutMs)) throw new Error("persistent PTC: run requires resolved inputs with a sandbox policy");
1347
+ const bindings = validateBindings(spec);
1348
+ let kernel = spec.stateOwner === void 0 ? void 0 : this.owners.get(spec.stateOwner);
1349
+ kernel?.assertIdle();
1350
+ if (kernel && !kernel.closed && (kernel.policy.mode !== spec.sandboxPolicy.mode || kernel.policy.workspaceRoot !== spec.sandboxPolicy.workspaceRoot || kernel.policy.sessionId !== spec.sandboxPolicy.sessionId)) throw new Error("persistent PTC: release the retained environment before changing its sandbox policy");
1351
+ let code;
1352
+ try {
1353
+ code = stripTypeScriptTypes(spec.program);
1354
+ } catch (error) {
1355
+ const output = persistentOutput(this.config.maxOutputBytes);
1356
+ const outcome = output.failure([], {
1357
+ kind: "exception",
1358
+ message: messageOf$1(error)
1359
+ });
1360
+ if (outcome.error?.kind === "output-limit" && spec.stateOwner !== void 0) try {
1361
+ await this.release(spec.stateOwner);
1362
+ } catch (failure) {
1363
+ return output.failure([], {
1364
+ kind: "worker-exit",
1365
+ message: `managed process cleanup failed: ${messageOf$1(failure)}`
1366
+ });
766
1367
  }
1368
+ return outcome;
1369
+ }
1370
+ if (!kernel || kernel.closed) {
1371
+ if (kernel) this.kernels.delete(kernel);
1372
+ kernel = new Kernel(this.ctx, this.config, { ...spec.sandboxPolicy }, this.processes);
1373
+ this.kernels.add(kernel);
1374
+ if (spec.stateOwner !== void 0) this.owners.set(spec.stateOwner, kernel);
1375
+ }
1376
+ try {
1377
+ return await kernel.run(spec, bindings, code);
1378
+ } finally {
1379
+ if (spec.stateOwner === void 0) this.kernels.delete(kernel);
767
1380
  }
768
- return frames.length === 0 ? root : void 0;
769
- } catch {
770
- return;
771
1381
  }
772
- }
1382
+ /**
1383
+ * Release retained state and await its process cleanup.
1384
+ * @param owner - Exact object supplied to run.
1385
+ * @returns Completion after the owned process and output streams stop.
1386
+ */
1387
+ async release(owner) {
1388
+ const kernel = this.owners.get(owner);
1389
+ if (!kernel) return;
1390
+ await kernel.dispose();
1391
+ if (this.owners.get(owner) === kernel) this.owners.delete(owner);
1392
+ this.kernels.delete(kernel);
1393
+ }
1394
+ };
773
1395
  //#endregion
774
1396
  //#region lib/types/index.js
775
- /** Confined Node programs with host-owned bindings, output limits, and managed process cleanup. */
1397
+ /** Node PTC execution with configurable per-call processes or retained in-memory REPL state. */
776
1398
  const STRIP_PREFIX = "async function __dsh_program__() {\n";
777
1399
  const STRIP_SUFFIX = "\n}";
778
1400
  function messageOf(error) {
@@ -781,7 +1403,7 @@ function messageOf(error) {
781
1403
  function record(value) {
782
1404
  return typeof value === "object" && value !== null && !Array.isArray(value);
783
1405
  }
784
- /** Node provider; direct file effects use the same sandbox service as Bash. */
1406
+ /** Node execution under file policy, with optional owner-scoped retained memory. */
785
1407
  var NodePtcRuntime = class extends PtcRuntime {
786
1408
  static inject = [
787
1409
  "fs",
@@ -789,27 +1411,17 @@ var NodePtcRuntime = class extends PtcRuntime {
789
1411
  "sandbox",
790
1412
  "sandboxPolicy"
791
1413
  ];
792
- static Config = z.object({
793
- timeoutMs: z.number().default(12e4),
794
- maxTimeoutMs: z.number().default(6e5),
795
- maxOutputBytes: z.number().default(67108864),
796
- maxOldGenerationSizeMb: z.number().default(512),
797
- maxMessageBytes: z.number().default(134217728),
798
- maxPendingCalls: z.number().default(128),
799
- graceMs: z.number().default(3e3),
800
- launch: z.union([z.object({
801
- kind: z.const("node-script").required(),
802
- executable: z.string().required(),
803
- bootstrapPath: z.string()
804
- }), z.object({
805
- kind: z.const("embedded").required(),
806
- executable: z.string().required()
807
- })])
808
- });
1414
+ static Config = NodeConfig;
809
1415
  language = "typescript";
810
1416
  isolation = "process";
1417
+ inMemory;
1418
+ processes;
1419
+ get programFormat() {
1420
+ return this.inMemory ? "repl" : "function-body";
1421
+ }
811
1422
  get executionInstructions() {
812
- 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.";
1423
+ if (this.inMemory) return this.inMemory.executionInstructions;
1424
+ return "The program is the body of an async TypeScript function; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level await and return are supported. Emit output with console.log(...) or return a lossless JSON value. Only printed values and the returned value become program output. 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.";
813
1425
  }
814
1426
  config;
815
1427
  live = /* @__PURE__ */ new Set();
@@ -820,16 +1432,9 @@ var NodePtcRuntime = class extends PtcRuntime {
820
1432
  ...config,
821
1433
  launch: resolveLaunch(config.launch)
822
1434
  };
823
- 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`);
824
- for (const key of [
825
- "timeoutMs",
826
- "maxTimeoutMs",
827
- "graceMs"
828
- ]) if (this.config[key] > MAX_TIMER_DELAY_MS) throw new Error(`ptc-runtime-node: ${key} exceeds the supported timer range`);
829
- 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");
830
- 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");
831
- if (!Number.isSafeInteger(this.config.maxPendingCalls)) throw new Error("ptc-runtime-node: maxPendingCalls must be an integer");
832
- if (!Number.isSafeInteger(this.config.maxOldGenerationSizeMb)) throw new Error("ptc-runtime-node: maxOldGenerationSizeMb must be an integer");
1435
+ validateConfig(this.config);
1436
+ this.processes = new PolicyProcesses(ctx);
1437
+ if (this.config.executionMode === "persistent-in-memory") this.inMemory = new InMemoryExecution(ctx, this.config, this.processes);
833
1438
  ctx.effect(() => async () => {
834
1439
  this.disposed = true;
835
1440
  const active = [...this.live];
@@ -840,6 +1445,9 @@ var NodePtcRuntime = class extends PtcRuntime {
840
1445
  get sandboxMode() {
841
1446
  return this.ctx.sandboxPolicy.defaultMode;
842
1447
  }
1448
+ release(owner) {
1449
+ return this.inMemory ? this.inMemory.release(owner) : super.release(owner);
1450
+ }
843
1451
  get timeout() {
844
1452
  return {
845
1453
  defaultMs: Math.min(this.config.timeoutMs, this.config.maxTimeoutMs),
@@ -847,11 +1455,12 @@ var NodePtcRuntime = class extends PtcRuntime {
847
1455
  };
848
1456
  }
849
1457
  /**
850
- * Resolve an execution under explicit or deployment policy.
1458
+ * Resolve directory, deadline and file policy for the selected execution mode.
851
1459
  * @param request - Program, bindings, optional cwd/deadline, and resolved authority.
852
1460
  * @returns Complete execution inputs with a capped numeric budget or an explicit null deadline.
853
1461
  */
854
1462
  resolve(request) {
1463
+ if (this.inMemory) return this.inMemory.resolve(request);
855
1464
  if (this.disposed) throw new Error("ptc-runtime-node: resolve after disposal");
856
1465
  const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
857
1466
  const cwd = request.cwd ?? sandboxPolicy.workspaceRoot;
@@ -864,30 +1473,45 @@ var NodePtcRuntime = class extends PtcRuntime {
864
1473
  };
865
1474
  }
866
1475
  /**
867
- * Run a resolved program in a fresh managed and confined Node process.
868
- * @param spec - Inputs returned by resolve; missing authority is caller misuse.
869
- * @returns Output and file-confinement facts after managed cleanup.
1476
+ * Run a resolved program under the configured execution mode.
1477
+ * Session mode changes cancel admitted calls before process launch as well as active programs.
1478
+ * @param spec - Inputs returned by resolve for the configured mode.
1479
+ * @returns Logs, JSON completion or failure; retained processes remain until release or invalidation.
870
1480
  */
871
1481
  async run(spec) {
872
1482
  if (this.disposed) throw new Error("ptc-runtime-node: run after disposal");
873
- if (spec.sandboxPolicy === void 0) throw new Error("ptc-runtime-node: run requires a resolved sandbox policy");
874
- 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");
875
- const bindings = validateBindings(spec);
876
1483
  const controller = new AbortController();
877
1484
  const completion = Promise.withResolvers();
1485
+ const unregister = spec.sandboxPolicy === void 0 ? void 0 : this.processes.register(spec.sandboxPolicy, async () => {
1486
+ controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
1487
+ await completion.promise;
1488
+ });
878
1489
  const live = {
879
1490
  controller,
880
1491
  finished: completion.promise
881
1492
  };
882
1493
  this.live.add(live);
883
1494
  try {
1495
+ await this.processes.wait(spec.sandboxPolicy?.sessionId);
1496
+ if (this.inMemory) {
1497
+ const signal = spec.signal === void 0 ? controller.signal : AbortSignal.any([spec.signal, controller.signal]);
1498
+ return await this.inMemory.run({
1499
+ ...spec,
1500
+ signal
1501
+ });
1502
+ }
1503
+ if (spec.sandboxPolicy === void 0) throw new Error("ptc-runtime-node: run requires a resolved sandbox policy");
1504
+ 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");
1505
+ const bindings = validateBindings(spec);
884
1506
  return await this.execute(spec, spec.sandboxPolicy, bindings, controller);
885
1507
  } finally {
1508
+ unregister?.();
886
1509
  this.live.delete(live);
887
1510
  completion.resolve();
888
1511
  }
889
1512
  }
890
1513
  async execute(spec, policy, bindings, controller) {
1514
+ const replyBinding = AsyncLocalStorage.bind(replyToBinding);
891
1515
  const output = new OutputLedger(this.config.maxOutputBytes);
892
1516
  const logs = [];
893
1517
  const sandbox = {
@@ -909,6 +1533,18 @@ var NodePtcRuntime = class extends PtcRuntime {
909
1533
  timedOut = true;
910
1534
  controller.abort("execution deadline reached");
911
1535
  }, spec.timeoutMs);
1536
+ let stopping;
1537
+ const stop = () => {
1538
+ if (stopping !== void 0) return stopping;
1539
+ stopping = (handle === void 0 ? Promise.resolve(true) : terminateProcess(handle, this.config.graceMs)).then((drained) => {
1540
+ unregister();
1541
+ return drained;
1542
+ }, (error) => {
1543
+ stopping = void 0;
1544
+ throw error;
1545
+ });
1546
+ return stopping;
1547
+ };
912
1548
  const finish = (failure, value) => {
913
1549
  if (settled) return;
914
1550
  settled = true;
@@ -916,10 +1552,8 @@ var NodePtcRuntime = class extends PtcRuntime {
916
1552
  signal.removeEventListener("abort", onAbort);
917
1553
  channel?.close();
918
1554
  (async () => {
919
- if (handle !== void 0) try {
920
- handle.terminate();
921
- await Promise.all([handle.done.catch(() => {}), handle.waitForExit()]);
922
- if ((await Promise.all([drainOutput(handle.stdout, this.config.graceMs), drainOutput(handle.stderr, this.config.graceMs)])).includes(false) && failure === void 0) failure = {
1555
+ try {
1556
+ if (!await stop() && failure === void 0) failure = {
923
1557
  kind: "worker-exit",
924
1558
  message: "Node process output did not close cleanly"
925
1559
  };
@@ -928,9 +1562,6 @@ var NodePtcRuntime = class extends PtcRuntime {
928
1562
  kind: "worker-exit",
929
1563
  message: `managed process cleanup failed: ${messageOf(error)}`
930
1564
  };
931
- } finally {
932
- handle.stdout?.destroy();
933
- handle.stderr?.destroy();
934
1565
  }
935
1566
  const outcome = outputOverflow ? overflowResult ?? output.limit(logs) : failure === void 0 ? output.success(logs, value) : output.failure(logs, failure);
936
1567
  result.resolve({
@@ -948,6 +1579,10 @@ var NodePtcRuntime = class extends PtcRuntime {
948
1579
  message: messageOf(signal.reason)
949
1580
  });
950
1581
  };
1582
+ const unregister = this.processes.register(policy, async () => {
1583
+ controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
1584
+ await stop();
1585
+ });
951
1586
  signal.addEventListener("abort", onAbort, { once: true });
952
1587
  if (signal.aborted) onAbort();
953
1588
  try {
@@ -1137,27 +1772,10 @@ var NodePtcRuntime = class extends PtcRuntime {
1137
1772
  }
1138
1773
  const id = raw.id;
1139
1774
  (async () => {
1140
- let reply;
1141
- try {
1142
- const value = snapshotJsonValue(await fn(args));
1143
- if (value === void 0) throw new Error("binding resolution must be lossless JSON");
1144
- reply = {
1145
- type: "reply",
1146
- id,
1147
- ok: true,
1148
- value: encodePtcJsonWire(value)
1149
- };
1150
- } catch (error) {
1151
- reply = {
1152
- type: "reply",
1153
- id,
1154
- ok: false,
1155
- message: messageOf(error)
1156
- };
1157
- } finally {
1775
+ const reply = await replyBinding(id, fn, args).finally(() => {
1158
1776
  pending -= 1;
1159
1777
  pendingBytes -= bytes;
1160
- }
1778
+ });
1161
1779
  if (!settled) await transport.send(reply);
1162
1780
  })().catch((error) => {
1163
1781
  protocolFailure(messageOf(error));