@deepseek-ai/dsh-ptc-runtime-node 0.2.1-alpha.1 → 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/README.i18n.yaml +19 -16
- package/README.md +38 -11
- package/README.zh.md +38 -11
- package/lib/index.js +1178 -535
- package/lib/persistent-process.js +1186 -0
- package/lib/process.js +14 -6
- package/lib/types/bindings.d.ts +10 -1
- package/lib/types/bootstrap.d.ts +13 -4
- package/lib/types/config.d.ts +34 -0
- package/lib/types/environment.d.ts +7 -0
- package/lib/types/index.d.ts +15 -26
- package/lib/types/launch.d.ts +22 -8
- package/lib/types/output-ledger.d.ts +2 -1
- package/lib/types/output-stream.d.ts +8 -0
- package/lib/types/persistent-process-entry.d.ts +2 -0
- package/lib/types/persistent-process.d.ts +18 -0
- package/lib/types/persistent.d.ts +44 -0
- package/lib/types/policy-processes.d.ts +30 -0
- package/locale/en.json +5 -0
- package/locale/zh.json +6 -0
- package/package.json +28 -25
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
|
-
import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
15
|
-
* @
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
45
|
-
const
|
|
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
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
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
|
-
/**
|
|
84
|
-
function
|
|
85
|
-
return intrinsicReflectApply$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
|
-
/**
|
|
88
|
-
function
|
|
89
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
115
|
-
* @param value -
|
|
116
|
-
* @
|
|
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
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
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 (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* @param
|
|
199
|
-
* @
|
|
200
|
-
|
|
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
|
|
508
|
+
return end === text.length ? text : intrinsicReflectApply(intrinsicStringSlice, text, [0, end]);
|
|
215
509
|
}
|
|
216
510
|
//#endregion
|
|
217
511
|
//#region lib/types/channel.js
|
|
@@ -340,39 +634,186 @@ var JsonChannel = class {
|
|
|
340
634
|
//#region lib/types/launch.js
|
|
341
635
|
/** Select source or built bootstrap assets in the mounted execution world. */
|
|
342
636
|
/**
|
|
637
|
+
* Resolve the local carrier default or validate an explicit execution-world invocation.
|
|
638
|
+
* @param config - explicit remote or local worker configuration; omitted selects this carrier.
|
|
639
|
+
* @returns the worker invocation without guessing from the execution world's paths.
|
|
640
|
+
*/
|
|
641
|
+
function resolveLaunch(config) {
|
|
642
|
+
const launch = config ?? {
|
|
643
|
+
kind: "pkg" in process ? "embedded" : "node-script",
|
|
644
|
+
executable: process.execPath
|
|
645
|
+
};
|
|
646
|
+
if (launch.executable.length === 0) throw new Error("ptc-runtime-node: launch.executable must be non-empty");
|
|
647
|
+
if (launch.kind === "node-script" && launch.bootstrapPath !== void 0 && !isAbsolute(launch.bootstrapPath)) throw new Error("ptc-runtime-node: launch.bootstrapPath must be absolute");
|
|
648
|
+
return launch;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
343
651
|
* Select explicit arguments without inheriting host loader or inspector flags.
|
|
344
652
|
* @param fs - Filesystem mapping host bootstrap assets into the process world.
|
|
345
|
-
* @param config -
|
|
653
|
+
* @param config - resolved worker invocation.
|
|
346
654
|
* @param maxMessageBytes - Validated frame and queued-write limit.
|
|
655
|
+
* @param persistent - Select the in-memory REPL bootstrap instead of the one-shot evaluator.
|
|
347
656
|
* @returns Arguments following the resolved Node executable.
|
|
348
657
|
*/
|
|
349
|
-
function bootstrapArgs(fs, config, maxMessageBytes) {
|
|
350
|
-
|
|
351
|
-
|
|
658
|
+
function bootstrapArgs(fs, config, maxMessageBytes, persistent = false) {
|
|
659
|
+
switch (config.kind) {
|
|
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);
|
|
664
|
+
/* v8 ignore next -- Callers supply the resolved, closed LaunchConfig union. */
|
|
665
|
+
default: return assertNever(config);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function scriptBootstrapArgs(fs, bootstrapPath, maxMessageBytes, persistent) {
|
|
669
|
+
const moduleName = persistent ? "persistent-process" : "process";
|
|
670
|
+
if (bootstrapPath !== void 0) return [bootstrapPath, String(maxMessageBytes)];
|
|
352
671
|
const mapped = (path) => {
|
|
353
672
|
const result = fs.processPathFromHostPath(path);
|
|
354
673
|
if (result === void 0) throw new Error(`PTC runtime bootstrap is unavailable in the subprocess execution world: ${path}`);
|
|
355
674
|
return result;
|
|
356
675
|
};
|
|
357
676
|
/* 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(
|
|
359
|
-
const entry = mapped(fileURLToPath(new URL(
|
|
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)));
|
|
360
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";
|
|
361
681
|
return [
|
|
362
682
|
"--input-type=module",
|
|
363
683
|
"--eval",
|
|
364
|
-
`const {openInheritedControlChannel}=await import(${JSON.stringify(pathToFileURL(helper).href)});const {
|
|
684
|
+
`const {openInheritedControlChannel}=await import(${JSON.stringify(pathToFileURL(helper).href)});const {${main}}=await import(${JSON.stringify(pathToFileURL(entry).href)});await ${main}(openInheritedControlChannel(),${maxMessageBytes},process);`
|
|
365
685
|
];
|
|
366
686
|
}
|
|
367
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
|
+
]);
|
|
806
|
+
//#endregion
|
|
368
807
|
//#region lib/types/output-ledger.js
|
|
369
808
|
/** One run's combined outer-output ledger; binding values never enter it. */
|
|
370
809
|
var OutputLedger = class {
|
|
371
810
|
maxBytes;
|
|
811
|
+
overflowMessage;
|
|
372
812
|
bytes = 2;
|
|
373
813
|
entries = 0;
|
|
374
|
-
constructor(maxBytes) {
|
|
814
|
+
constructor(maxBytes, overflowMessage) {
|
|
375
815
|
this.maxBytes = maxBytes;
|
|
816
|
+
this.overflowMessage = overflowMessage;
|
|
376
817
|
}
|
|
377
818
|
/** Admit one exact log entry, or report that the hard cap was crossed. */
|
|
378
819
|
admit(text, sink) {
|
|
@@ -402,7 +843,7 @@ var OutputLedger = class {
|
|
|
402
843
|
}
|
|
403
844
|
/** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
|
|
404
845
|
limit(logs) {
|
|
405
|
-
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`;
|
|
846
|
+
const fullMessage = this.overflowMessage ?? `outer output exceeded ${this.maxBytes} bytes`;
|
|
406
847
|
const messageBytes = fullMessage.length + 2;
|
|
407
848
|
const retained = [];
|
|
408
849
|
let retainedBytes = 2;
|
|
@@ -415,343 +856,545 @@ var OutputLedger = class {
|
|
|
415
856
|
retained.push(text);
|
|
416
857
|
retainedBytes += stringBytes + separatorBytes;
|
|
417
858
|
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);
|
|
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;
|
|
661
869
|
}
|
|
662
870
|
return {
|
|
663
|
-
|
|
664
|
-
|
|
871
|
+
logs: retained,
|
|
872
|
+
error: {
|
|
873
|
+
kind: "output-limit",
|
|
874
|
+
message: truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
|
|
875
|
+
}
|
|
665
876
|
};
|
|
666
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
|
+
});
|
|
667
914
|
}
|
|
668
915
|
/**
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* @
|
|
673
|
-
* @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.
|
|
674
920
|
*/
|
|
675
|
-
function
|
|
921
|
+
async function terminateProcess(handle, graceMs) {
|
|
676
922
|
try {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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()));
|
|
689
984
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
});
|
|
698
1010
|
}
|
|
699
|
-
|
|
700
|
-
|
|
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);
|
|
701
1028
|
};
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
|
724
1240
|
};
|
|
1241
|
+
setImmediate(() => {
|
|
1242
|
+
evaluation.finish(failure, void 0, failure.kind === "output-limit");
|
|
1243
|
+
});
|
|
725
1244
|
} else {
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
};
|
|
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
|
+
});
|
|
735
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;
|
|
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;
|
|
736
1283
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
+
});
|
|
745
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);
|
|
746
1380
|
}
|
|
747
|
-
return frames.length === 0 ? root : void 0;
|
|
748
|
-
} catch {
|
|
749
|
-
return;
|
|
750
1381
|
}
|
|
751
|
-
|
|
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
|
+
};
|
|
752
1395
|
//#endregion
|
|
753
1396
|
//#region lib/types/index.js
|
|
754
|
-
/**
|
|
1397
|
+
/** Node PTC execution with configurable per-call processes or retained in-memory REPL state. */
|
|
755
1398
|
const STRIP_PREFIX = "async function __dsh_program__() {\n";
|
|
756
1399
|
const STRIP_SUFFIX = "\n}";
|
|
757
1400
|
function messageOf(error) {
|
|
@@ -760,7 +1403,7 @@ function messageOf(error) {
|
|
|
760
1403
|
function record(value) {
|
|
761
1404
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
762
1405
|
}
|
|
763
|
-
/** Node
|
|
1406
|
+
/** Node execution under file policy, with optional owner-scoped retained memory. */
|
|
764
1407
|
var NodePtcRuntime = class extends PtcRuntime {
|
|
765
1408
|
static inject = [
|
|
766
1409
|
"fs",
|
|
@@ -768,21 +1411,17 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
768
1411
|
"sandbox",
|
|
769
1412
|
"sandboxPolicy"
|
|
770
1413
|
];
|
|
771
|
-
static Config =
|
|
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
|
-
});
|
|
1414
|
+
static Config = NodeConfig;
|
|
782
1415
|
language = "typescript";
|
|
783
1416
|
isolation = "process";
|
|
1417
|
+
inMemory;
|
|
1418
|
+
processes;
|
|
1419
|
+
get programFormat() {
|
|
1420
|
+
return this.inMemory ? "repl" : "function-body";
|
|
1421
|
+
}
|
|
784
1422
|
get executionInstructions() {
|
|
785
|
-
|
|
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.";
|
|
786
1425
|
}
|
|
787
1426
|
config;
|
|
788
1427
|
live = /* @__PURE__ */ new Set();
|
|
@@ -791,20 +1430,11 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
791
1430
|
super(ctx);
|
|
792
1431
|
this.config = {
|
|
793
1432
|
...config,
|
|
794
|
-
|
|
1433
|
+
launch: resolveLaunch(config.launch)
|
|
795
1434
|
};
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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");
|
|
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);
|
|
808
1438
|
ctx.effect(() => async () => {
|
|
809
1439
|
this.disposed = true;
|
|
810
1440
|
const active = [...this.live];
|
|
@@ -815,6 +1445,9 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
815
1445
|
get sandboxMode() {
|
|
816
1446
|
return this.ctx.sandboxPolicy.defaultMode;
|
|
817
1447
|
}
|
|
1448
|
+
release(owner) {
|
|
1449
|
+
return this.inMemory ? this.inMemory.release(owner) : super.release(owner);
|
|
1450
|
+
}
|
|
818
1451
|
get timeout() {
|
|
819
1452
|
return {
|
|
820
1453
|
defaultMs: Math.min(this.config.timeoutMs, this.config.maxTimeoutMs),
|
|
@@ -822,11 +1455,12 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
822
1455
|
};
|
|
823
1456
|
}
|
|
824
1457
|
/**
|
|
825
|
-
* Resolve
|
|
1458
|
+
* Resolve directory, deadline and file policy for the selected execution mode.
|
|
826
1459
|
* @param request - Program, bindings, optional cwd/deadline, and resolved authority.
|
|
827
1460
|
* @returns Complete execution inputs with a capped numeric budget or an explicit null deadline.
|
|
828
1461
|
*/
|
|
829
1462
|
resolve(request) {
|
|
1463
|
+
if (this.inMemory) return this.inMemory.resolve(request);
|
|
830
1464
|
if (this.disposed) throw new Error("ptc-runtime-node: resolve after disposal");
|
|
831
1465
|
const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
832
1466
|
const cwd = request.cwd ?? sandboxPolicy.workspaceRoot;
|
|
@@ -839,30 +1473,45 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
839
1473
|
};
|
|
840
1474
|
}
|
|
841
1475
|
/**
|
|
842
|
-
* Run a resolved program
|
|
843
|
-
*
|
|
844
|
-
* @
|
|
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.
|
|
845
1480
|
*/
|
|
846
1481
|
async run(spec) {
|
|
847
1482
|
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
1483
|
const controller = new AbortController();
|
|
852
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
|
+
});
|
|
853
1489
|
const live = {
|
|
854
1490
|
controller,
|
|
855
1491
|
finished: completion.promise
|
|
856
1492
|
};
|
|
857
1493
|
this.live.add(live);
|
|
858
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);
|
|
859
1506
|
return await this.execute(spec, spec.sandboxPolicy, bindings, controller);
|
|
860
1507
|
} finally {
|
|
1508
|
+
unregister?.();
|
|
861
1509
|
this.live.delete(live);
|
|
862
1510
|
completion.resolve();
|
|
863
1511
|
}
|
|
864
1512
|
}
|
|
865
1513
|
async execute(spec, policy, bindings, controller) {
|
|
1514
|
+
const replyBinding = AsyncLocalStorage.bind(replyToBinding);
|
|
866
1515
|
const output = new OutputLedger(this.config.maxOutputBytes);
|
|
867
1516
|
const logs = [];
|
|
868
1517
|
const sandbox = {
|
|
@@ -884,6 +1533,18 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
884
1533
|
timedOut = true;
|
|
885
1534
|
controller.abort("execution deadline reached");
|
|
886
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
|
+
};
|
|
887
1548
|
const finish = (failure, value) => {
|
|
888
1549
|
if (settled) return;
|
|
889
1550
|
settled = true;
|
|
@@ -891,10 +1552,8 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
891
1552
|
signal.removeEventListener("abort", onAbort);
|
|
892
1553
|
channel?.close();
|
|
893
1554
|
(async () => {
|
|
894
|
-
|
|
895
|
-
|
|
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 = {
|
|
1555
|
+
try {
|
|
1556
|
+
if (!await stop() && failure === void 0) failure = {
|
|
898
1557
|
kind: "worker-exit",
|
|
899
1558
|
message: "Node process output did not close cleanly"
|
|
900
1559
|
};
|
|
@@ -903,9 +1562,6 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
903
1562
|
kind: "worker-exit",
|
|
904
1563
|
message: `managed process cleanup failed: ${messageOf(error)}`
|
|
905
1564
|
};
|
|
906
|
-
} finally {
|
|
907
|
-
handle.stdout?.destroy();
|
|
908
|
-
handle.stderr?.destroy();
|
|
909
1565
|
}
|
|
910
1566
|
const outcome = outputOverflow ? overflowResult ?? output.limit(logs) : failure === void 0 ? output.success(logs, value) : output.failure(logs, failure);
|
|
911
1567
|
result.resolve({
|
|
@@ -923,6 +1579,10 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
923
1579
|
message: messageOf(signal.reason)
|
|
924
1580
|
});
|
|
925
1581
|
};
|
|
1582
|
+
const unregister = this.processes.register(policy, async () => {
|
|
1583
|
+
controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
|
|
1584
|
+
await stop();
|
|
1585
|
+
});
|
|
926
1586
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
927
1587
|
if (signal.aborted) onAbort();
|
|
928
1588
|
try {
|
|
@@ -938,14 +1598,14 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
938
1598
|
})),
|
|
939
1599
|
maxOutputBytes: this.config.maxOutputBytes
|
|
940
1600
|
};
|
|
941
|
-
const executable = await this.ctx.subprocess.resolveExecutable(this.config.
|
|
1601
|
+
const executable = await this.ctx.subprocess.resolveExecutable(this.config.launch.executable, void 0, signal);
|
|
942
1602
|
if (settled) return await result.promise;
|
|
943
|
-
const packaged =
|
|
1603
|
+
const packaged = this.config.launch.kind === "embedded";
|
|
944
1604
|
const heapFlag = `--max-old-space-size=${this.config.maxOldGenerationSizeMb}`;
|
|
945
1605
|
const argv = [
|
|
946
1606
|
executable,
|
|
947
1607
|
...packaged ? [] : [heapFlag],
|
|
948
|
-
...bootstrapArgs(this.ctx.fs, this.config, this.config.maxMessageBytes)
|
|
1608
|
+
...bootstrapArgs(this.ctx.fs, this.config.launch, this.config.maxMessageBytes)
|
|
949
1609
|
];
|
|
950
1610
|
confined = policy.mode === "danger-full-access" ? void 0 : await this.ctx.sandbox.confine(argv, {
|
|
951
1611
|
...policy,
|
|
@@ -1112,27 +1772,10 @@ var NodePtcRuntime = class extends PtcRuntime {
|
|
|
1112
1772
|
}
|
|
1113
1773
|
const id = raw.id;
|
|
1114
1774
|
(async () => {
|
|
1115
|
-
|
|
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 {
|
|
1775
|
+
const reply = await replyBinding(id, fn, args).finally(() => {
|
|
1133
1776
|
pending -= 1;
|
|
1134
1777
|
pendingBytes -= bytes;
|
|
1135
|
-
}
|
|
1778
|
+
});
|
|
1136
1779
|
if (!settled) await transport.send(reply);
|
|
1137
1780
|
})().catch((error) => {
|
|
1138
1781
|
protocolFailure(messageOf(error));
|