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