@deepseek-ai/dsh-cordis-host-runner 0.0.1-rc.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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +72 -0
- package/README.zh.md +72 -0
- package/lib/index.js +2573 -0
- package/lib/invariant.js +25 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1897 -0
- package/lib/typert.remote-client.d.ts +55 -0
- package/lib/typert.remote-client.js +769 -0
- package/lib/types/guard.d.ts +71 -0
- package/lib/types/guard.js +766 -0
- package/lib/types/index.d.ts +270 -0
- package/lib/types/index.js +1151 -0
- package/lib/types/inspect-registry.d.ts +72 -0
- package/lib/types/inspect-registry.js +192 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +24 -0
- package/lib/types/lifecycle.d.ts +29 -0
- package/lib/types/lifecycle.js +49 -0
- package/lib/types/registry.d.ts +225 -0
- package/lib/types/registry.js +131 -0
- package/lib/types/sandbox.d.ts +93 -0
- package/lib/types/sandbox.js +223 -0
- package/lib/types/types.d.ts +359 -0
- package/lib/types/types.js +6 -0
- package/package.json +78 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2573 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
5
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
6
|
+
import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
|
|
7
|
+
import { snapshotJsonValue } from "@deepseek-ai/dsh-session";
|
|
8
|
+
import { Script, createContext, runInContext } from "node:vm";
|
|
9
|
+
//#region lib/types/guard.js
|
|
10
|
+
/**
|
|
11
|
+
* The registration boundary between a sandboxed host half and the real runtime: ParameterSchemaSpec
|
|
12
|
+
* normalization + validation with teaching errors, the marker-guarded `harness.defineTool` /
|
|
13
|
+
* `harness.registerTool` pair, the `harness.handle` invoke-handler normalizer, the SANDBOX CONTEXT
|
|
14
|
+
* FAÇADE a running plugin's `apply` receives in place of the real `ctx`, and the plugin-shape
|
|
15
|
+
* helpers the run lifecycle narrows sandbox return values with. The façade is a whitelist of
|
|
16
|
+
* lifecycle-safe verbs and declared services; framework internals and context-valued service
|
|
17
|
+
* returns are denied.
|
|
18
|
+
*
|
|
19
|
+
* VM-realm schemas and canonical values are rebuilt as host objects, while rendered content and
|
|
20
|
+
* presentation metadata are shape-checked before entering the registry. Common JSON-Schema spellings are normalized when they
|
|
21
|
+
* have one meaning; invalid vocabulary fails during registration with a teaching error.
|
|
22
|
+
* @module @deepseek-ai/dsh-cordis-host-runner/guard
|
|
23
|
+
*/
|
|
24
|
+
const DYNAMIC_TOOL = Symbol("cordis-host-runner.dynamic-tool");
|
|
25
|
+
const SCHEMA_TYPES = new Set([
|
|
26
|
+
"string",
|
|
27
|
+
"number",
|
|
28
|
+
"integer",
|
|
29
|
+
"boolean",
|
|
30
|
+
"null",
|
|
31
|
+
"object",
|
|
32
|
+
"array",
|
|
33
|
+
"json"
|
|
34
|
+
]);
|
|
35
|
+
const VALID_TYPES = "'string' | 'number' | 'integer' | 'boolean' | 'null' | 'object' | 'array' | 'json'";
|
|
36
|
+
const ANNOTATION_KEYS = [
|
|
37
|
+
"description",
|
|
38
|
+
"title",
|
|
39
|
+
"default",
|
|
40
|
+
"examples"
|
|
41
|
+
];
|
|
42
|
+
function isPlainRecord(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
44
|
+
const prototype = Object.getPrototypeOf(value);
|
|
45
|
+
return prototype === null || typeof prototype === "object" && Object.getPrototypeOf(prototype) === null && hasIntrinsicConstructor(prototype, "Object");
|
|
46
|
+
}
|
|
47
|
+
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
|
48
|
+
function hasIntrinsicConstructor(prototype, name) {
|
|
49
|
+
const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
|
|
50
|
+
if (typeof constructor !== "function") return false;
|
|
51
|
+
try {
|
|
52
|
+
return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Whether an array uses one realm's intrinsic Array prototype rather than a subclass. */
|
|
58
|
+
function hasPlainArrayPrototype(value) {
|
|
59
|
+
const prototype = Object.getPrototypeOf(value);
|
|
60
|
+
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
|
|
61
|
+
const objectPrototype = Object.getPrototypeOf(prototype);
|
|
62
|
+
return typeof objectPrototype === "object" && objectPrototype !== null && Object.getPrototypeOf(objectPrototype) === null && hasIntrinsicConstructor(objectPrototype, "Object");
|
|
63
|
+
}
|
|
64
|
+
/** Whether a schema list is a dense intrinsic array with no JSON-invisible decorations. */
|
|
65
|
+
function isDensePlainArray(value) {
|
|
66
|
+
if (!Array.isArray(value) || !hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) return false;
|
|
67
|
+
for (let index = 0; index < value.length; index++) if (!Object.hasOwn(value, index)) return false;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** Reject schema records whose declarations would disappear from object enumeration. */
|
|
71
|
+
function assertSchemaContainerKeys(value, path) {
|
|
72
|
+
if (Reflect.ownKeys(value).some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) throw new Error(`harness.defineTool ${path} must contain only own enumerable string keys`);
|
|
73
|
+
}
|
|
74
|
+
/** Materialize realm-foreign lossless JSON without allowing JSON.stringify coercions; `path` carries the caller's own error prefix. */
|
|
75
|
+
function cloneJson(value, path) {
|
|
76
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
77
|
+
let root;
|
|
78
|
+
const assign = (destination, item) => {
|
|
79
|
+
if (destination.kind === "root") {
|
|
80
|
+
root = item;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (destination.kind === "array") {
|
|
84
|
+
destination.target[destination.index] = item;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
Object.defineProperty(destination.target, destination.key, {
|
|
88
|
+
value: item,
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
writable: true
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const reject = (at) => {
|
|
95
|
+
throw new Error(`${at} must be lossless JSON data (objects, arrays, strings, numbers, booleans, null) — not a class instance, function, Map/Set, Date, or undefined. Return a plain object built from the values you need, or \`return null\` when the caller needs no value back.`);
|
|
96
|
+
};
|
|
97
|
+
const tasks = [{
|
|
98
|
+
kind: "visit",
|
|
99
|
+
value,
|
|
100
|
+
path,
|
|
101
|
+
destination: { kind: "root" }
|
|
102
|
+
}];
|
|
103
|
+
for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
|
|
104
|
+
if (task.kind === "leave") {
|
|
105
|
+
ancestors.delete(task.source);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (task.kind === "array-item") {
|
|
109
|
+
if (!Object.hasOwn(task.source, task.index)) reject(task.path);
|
|
110
|
+
tasks.push({
|
|
111
|
+
kind: "visit",
|
|
112
|
+
value: task.source[task.index],
|
|
113
|
+
path: `${task.path}[${task.index}]`,
|
|
114
|
+
destination: {
|
|
115
|
+
kind: "array",
|
|
116
|
+
target: task.target,
|
|
117
|
+
index: task.index
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const current = task.value;
|
|
123
|
+
if (current === null || typeof current === "string" || typeof current === "boolean") {
|
|
124
|
+
assign(task.destination, current);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof current === "number") {
|
|
128
|
+
if (!Number.isFinite(current) || Object.is(current, -0)) reject(task.path);
|
|
129
|
+
assign(task.destination, current);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (typeof current !== "object" || ancestors.has(current)) reject(task.path);
|
|
133
|
+
if (Array.isArray(current)) {
|
|
134
|
+
if (!hasPlainArrayPrototype(current) || Reflect.ownKeys(current).length !== current.length + 1) reject(task.path);
|
|
135
|
+
const output = [];
|
|
136
|
+
assign(task.destination, output);
|
|
137
|
+
ancestors.add(current);
|
|
138
|
+
tasks.push({
|
|
139
|
+
kind: "leave",
|
|
140
|
+
source: current
|
|
141
|
+
});
|
|
142
|
+
for (let index = current.length - 1; index >= 0; index--) tasks.push({
|
|
143
|
+
kind: "array-item",
|
|
144
|
+
source: current,
|
|
145
|
+
index,
|
|
146
|
+
path: task.path,
|
|
147
|
+
target: output
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!isPlainRecord(current)) reject(task.path);
|
|
152
|
+
const record = current;
|
|
153
|
+
if (Reflect.ownKeys(record).some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(record, key))) reject(task.path);
|
|
154
|
+
const output = {};
|
|
155
|
+
assign(task.destination, output);
|
|
156
|
+
ancestors.add(record);
|
|
157
|
+
tasks.push({
|
|
158
|
+
kind: "leave",
|
|
159
|
+
source: record
|
|
160
|
+
});
|
|
161
|
+
const entries = Object.entries(record);
|
|
162
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
163
|
+
const entry = entries[index];
|
|
164
|
+
/* v8 ignore next -- the loop is bounded by the captured entry count. */
|
|
165
|
+
if (entry === void 0) continue;
|
|
166
|
+
tasks.push({
|
|
167
|
+
kind: "visit",
|
|
168
|
+
value: entry[1],
|
|
169
|
+
path: `${task.path}.${entry[0]}`,
|
|
170
|
+
destination: {
|
|
171
|
+
kind: "object",
|
|
172
|
+
target: output,
|
|
173
|
+
key: entry[0]
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return root;
|
|
179
|
+
}
|
|
180
|
+
/** Copy and realm-materialize the shared annotation vocabulary. */
|
|
181
|
+
function copyAnnotations(value, output, path) {
|
|
182
|
+
if (Object.hasOwn(value, "description")) output.description = value.description;
|
|
183
|
+
if (Object.hasOwn(value, "title")) output.title = value.title;
|
|
184
|
+
if (Object.hasOwn(value, "default")) output.default = cloneJson(value.default, `harness.defineTool ${path}.default`);
|
|
185
|
+
if (Object.hasOwn(value, "examples")) output.examples = cloneJson(value.examples, `harness.defineTool ${path}.examples`);
|
|
186
|
+
}
|
|
187
|
+
/** Reject sandbox schema keys that the unified DSL would otherwise ignore. */
|
|
188
|
+
function assertSchemaKeys(value, path, allowed) {
|
|
189
|
+
assertSchemaContainerKeys(value, path);
|
|
190
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new Error(`harness.defineTool ${path}.${key} is not supported by the unified schema DSL`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Normalize a sandbox-provided `parameters` value into a fresh host-realm
|
|
194
|
+
* ParameterSchemaSpec. A raw JSON-Schema object wrapper retains its open root
|
|
195
|
+
* default, while the direct DSL is already an implicit open property map.
|
|
196
|
+
*/
|
|
197
|
+
function normalizeParameterSchemaSpec(value, path = "parameters") {
|
|
198
|
+
if (!isPlainRecord(value)) throw new Error(`harness.defineTool ${path} must be a ParameterSchemaSpec object`);
|
|
199
|
+
if (value.type === "object") {
|
|
200
|
+
assertSchemaKeys(value, path, [
|
|
201
|
+
"type",
|
|
202
|
+
"properties",
|
|
203
|
+
"required",
|
|
204
|
+
"additionalProperties",
|
|
205
|
+
...ANNOTATION_KEYS
|
|
206
|
+
]);
|
|
207
|
+
if (!isPlainRecord(value.properties)) throw new Error(`harness.defineTool ${path}.properties must be an object of schemas`);
|
|
208
|
+
if (Object.hasOwn(value, "additionalProperties") && value.additionalProperties !== true) throw new Error(`harness.defineTool ${path}.additionalProperties must be true or omitted because the implicit parameter root is open`);
|
|
209
|
+
if (Object.hasOwn(value, "required") && value.required === void 0) throw new Error(`harness.defineTool ${path}.required must be an array of declared property names`);
|
|
210
|
+
const required = normalizeRequiredNames(value.required, value.properties, `${path}.required`);
|
|
211
|
+
const rootAnnotations = {};
|
|
212
|
+
copyAnnotations(value, rootAnnotations, path);
|
|
213
|
+
return {
|
|
214
|
+
spec: normalizePropertyMap(value.properties, path, required, true),
|
|
215
|
+
...Object.keys(rootAnnotations).length === 0 ? {} : { rootAnnotations }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { spec: normalizePropertyMap(value, path, /* @__PURE__ */ new Set(), false) };
|
|
219
|
+
}
|
|
220
|
+
/** Validate raw required names and return their lookup set. */
|
|
221
|
+
function normalizeRequiredNames(value, properties, path) {
|
|
222
|
+
if (value === void 0) return /* @__PURE__ */ new Set();
|
|
223
|
+
if (!isDensePlainArray(value)) throw new Error(`harness.defineTool ${path} must be an array of declared property names`);
|
|
224
|
+
const names = /* @__PURE__ */ new Set();
|
|
225
|
+
for (let index = 0; index < value.length; index++) {
|
|
226
|
+
const name = value[index];
|
|
227
|
+
if (typeof name !== "string") throw new Error(`harness.defineTool ${path} must be an array of declared property names`);
|
|
228
|
+
names.add(name);
|
|
229
|
+
if (!Object.hasOwn(properties, name)) throw new Error(`harness.defineTool ${path} names undeclared property ${JSON.stringify(name)}`);
|
|
230
|
+
}
|
|
231
|
+
return names;
|
|
232
|
+
}
|
|
233
|
+
/** Install one normalized node without `__proto__` assignment semantics. */
|
|
234
|
+
function assignNormalizedValue(destination, value) {
|
|
235
|
+
if (destination.kind === "property") Object.defineProperty(destination.target, destination.key, {
|
|
236
|
+
value,
|
|
237
|
+
enumerable: true,
|
|
238
|
+
configurable: true,
|
|
239
|
+
writable: true
|
|
240
|
+
});
|
|
241
|
+
else if (destination.kind === "item") destination.target.items = value;
|
|
242
|
+
else destination.target[destination.index] = value;
|
|
243
|
+
}
|
|
244
|
+
/** Install one normalized property map at its root or containing object. */
|
|
245
|
+
function assignNormalizedMap(destination, value) {
|
|
246
|
+
if (destination.kind === "root") destination.holder.value = value;
|
|
247
|
+
else destination.target.properties = value;
|
|
248
|
+
}
|
|
249
|
+
/** Normalize one implicit property map and all descendants with explicit work frames. */
|
|
250
|
+
function normalizePropertyMap(entries, path, requiredNames, raw) {
|
|
251
|
+
const holder = {};
|
|
252
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
253
|
+
const tasks = [{
|
|
254
|
+
kind: "map",
|
|
255
|
+
entries,
|
|
256
|
+
path,
|
|
257
|
+
requiredNames,
|
|
258
|
+
raw,
|
|
259
|
+
destination: {
|
|
260
|
+
kind: "root",
|
|
261
|
+
holder
|
|
262
|
+
}
|
|
263
|
+
}];
|
|
264
|
+
for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
|
|
265
|
+
if (task.kind === "leave") {
|
|
266
|
+
ancestors.delete(task.value);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (task.kind === "map") {
|
|
270
|
+
if (ancestors.has(task.entries)) throw new Error(`harness.defineTool ${task.path} is circular`);
|
|
271
|
+
assertSchemaContainerKeys(task.entries, task.path);
|
|
272
|
+
ancestors.add(task.entries);
|
|
273
|
+
const spec = {};
|
|
274
|
+
assignNormalizedMap(task.destination, spec);
|
|
275
|
+
tasks.push({
|
|
276
|
+
kind: "leave",
|
|
277
|
+
value: task.entries
|
|
278
|
+
});
|
|
279
|
+
const mapEntries = Object.entries(task.entries);
|
|
280
|
+
for (let index = mapEntries.length - 1; index >= 0; index--) {
|
|
281
|
+
const entry = mapEntries[index];
|
|
282
|
+
/* v8 ignore next -- the loop is bounded by the captured entry count. */
|
|
283
|
+
if (entry === void 0) continue;
|
|
284
|
+
tasks.push({
|
|
285
|
+
kind: "value",
|
|
286
|
+
value: entry[1],
|
|
287
|
+
path: `${task.path}.${entry[0]}`,
|
|
288
|
+
forceRequired: task.requiredNames.has(entry[0]),
|
|
289
|
+
raw: task.raw,
|
|
290
|
+
parameterProperty: true,
|
|
291
|
+
destination: {
|
|
292
|
+
kind: "property",
|
|
293
|
+
target: spec,
|
|
294
|
+
key: entry[0]
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const { value, path } = task;
|
|
301
|
+
if (!isPlainRecord(value)) throw new Error(`harness.defineTool ${path} must be a ParameterSchemaSpec property object`);
|
|
302
|
+
assertSchemaContainerKeys(value, path);
|
|
303
|
+
if (ancestors.has(value)) throw new Error(`harness.defineTool ${path} is circular`);
|
|
304
|
+
ancestors.add(value);
|
|
305
|
+
const requiredKey = task.parameterProperty && !task.raw ? ["required"] : [];
|
|
306
|
+
if (task.parameterProperty && task.raw && Object.hasOwn(value, "required") && value.type !== "object") throw new Error(`harness.defineTool ${path}.required belongs to the containing raw object schema`);
|
|
307
|
+
if (task.parameterProperty && !task.raw && Object.hasOwn(value, "required") && value.required !== true) throw new Error(`harness.defineTool ${path}.required must be true when present`);
|
|
308
|
+
const prop = {};
|
|
309
|
+
assignNormalizedValue(task.destination, prop);
|
|
310
|
+
tasks.push({
|
|
311
|
+
kind: "leave",
|
|
312
|
+
value
|
|
313
|
+
});
|
|
314
|
+
if (task.forceRequired || value.required === true) prop.required = true;
|
|
315
|
+
copyAnnotations(value, prop, path);
|
|
316
|
+
if (Object.hasOwn(value, "oneOf")) {
|
|
317
|
+
assertSchemaKeys(value, path, [
|
|
318
|
+
"oneOf",
|
|
319
|
+
...requiredKey,
|
|
320
|
+
...ANNOTATION_KEYS
|
|
321
|
+
]);
|
|
322
|
+
if (!isDensePlainArray(value.oneOf) || value.oneOf.length < 2) throw new Error(`harness.defineTool ${path}.oneOf must contain at least two schemas`);
|
|
323
|
+
const oneOf = [];
|
|
324
|
+
prop.oneOf = oneOf;
|
|
325
|
+
for (let index = value.oneOf.length - 1; index >= 0; index--) tasks.push({
|
|
326
|
+
kind: "value",
|
|
327
|
+
value: value.oneOf[index],
|
|
328
|
+
path: `${path}.oneOf[${index}]`,
|
|
329
|
+
forceRequired: false,
|
|
330
|
+
raw: task.raw,
|
|
331
|
+
parameterProperty: false,
|
|
332
|
+
destination: {
|
|
333
|
+
kind: "one-of",
|
|
334
|
+
target: oneOf,
|
|
335
|
+
index
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (task.raw && !Object.hasOwn(value, "type")) {
|
|
341
|
+
assertSchemaKeys(value, path, ANNOTATION_KEYS);
|
|
342
|
+
prop.type = "json";
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!SCHEMA_TYPES.has(value.type) || task.raw && value.type === "json") throw new Error(`harness.defineTool ${path} must declare a valid type: ${VALID_TYPES} (got ${JSON.stringify(value.type)})`);
|
|
346
|
+
const type = value.type;
|
|
347
|
+
prop.type = type;
|
|
348
|
+
switch (type) {
|
|
349
|
+
case "object":
|
|
350
|
+
assertSchemaKeys(value, path, [
|
|
351
|
+
"type",
|
|
352
|
+
"properties",
|
|
353
|
+
"additionalProperties",
|
|
354
|
+
...requiredKey,
|
|
355
|
+
...task.raw ? ["required"] : [],
|
|
356
|
+
...ANNOTATION_KEYS
|
|
357
|
+
]);
|
|
358
|
+
if (!task.raw && (!Object.hasOwn(value, "additionalProperties") || typeof value.additionalProperties !== "boolean")) throw new Error(`harness.defineTool ${path}.additionalProperties must be explicitly true or false`);
|
|
359
|
+
if (task.raw && Object.hasOwn(value, "additionalProperties") && typeof value.additionalProperties !== "boolean") throw new Error(`harness.defineTool ${path}.additionalProperties must be a boolean`);
|
|
360
|
+
if (task.raw && Object.hasOwn(value, "required") && value.required === void 0) throw new Error(`harness.defineTool ${path}.required must be an array of declared property names`);
|
|
361
|
+
prop.additionalProperties = task.raw ? value.additionalProperties ?? true : value.additionalProperties;
|
|
362
|
+
if (Object.hasOwn(value, "properties")) {
|
|
363
|
+
const properties = value.properties;
|
|
364
|
+
if (!isPlainRecord(properties)) throw new Error(`harness.defineTool ${path}.properties must be an object of schemas`);
|
|
365
|
+
const nestedRequired = task.raw ? normalizeRequiredNames(value.required, properties, `${path}.required`) : /* @__PURE__ */ new Set();
|
|
366
|
+
tasks.push({
|
|
367
|
+
kind: "map",
|
|
368
|
+
entries: properties,
|
|
369
|
+
path: `${path}.properties`,
|
|
370
|
+
requiredNames: nestedRequired,
|
|
371
|
+
raw: task.raw,
|
|
372
|
+
destination: {
|
|
373
|
+
kind: "properties",
|
|
374
|
+
target: prop
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
} else if (task.raw && value.required !== void 0) normalizeRequiredNames(value.required, {}, `${path}.required`);
|
|
378
|
+
break;
|
|
379
|
+
case "array":
|
|
380
|
+
assertSchemaKeys(value, path, [
|
|
381
|
+
"type",
|
|
382
|
+
"items",
|
|
383
|
+
...requiredKey,
|
|
384
|
+
...ANNOTATION_KEYS
|
|
385
|
+
]);
|
|
386
|
+
if (Object.hasOwn(value, "items")) tasks.push({
|
|
387
|
+
kind: "value",
|
|
388
|
+
value: value.items,
|
|
389
|
+
path: `${path}.items`,
|
|
390
|
+
forceRequired: false,
|
|
391
|
+
raw: task.raw,
|
|
392
|
+
parameterProperty: false,
|
|
393
|
+
destination: {
|
|
394
|
+
kind: "item",
|
|
395
|
+
target: prop
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
break;
|
|
399
|
+
case "string":
|
|
400
|
+
case "number":
|
|
401
|
+
case "integer":
|
|
402
|
+
case "boolean":
|
|
403
|
+
case "null":
|
|
404
|
+
assertSchemaKeys(value, path, [
|
|
405
|
+
"type",
|
|
406
|
+
"enum",
|
|
407
|
+
"const",
|
|
408
|
+
...requiredKey,
|
|
409
|
+
...ANNOTATION_KEYS
|
|
410
|
+
]);
|
|
411
|
+
if (Object.hasOwn(value, "enum")) {
|
|
412
|
+
if (!isDensePlainArray(value.enum) || value.enum.length === 0) throw new Error(`harness.defineTool ${path}.enum must be a non-empty array`);
|
|
413
|
+
prop.enum = cloneJson(value.enum, `harness.defineTool ${path}.enum`);
|
|
414
|
+
}
|
|
415
|
+
if (Object.hasOwn(value, "const")) prop.const = cloneJson(value.const, `harness.defineTool ${path}.const`);
|
|
416
|
+
break;
|
|
417
|
+
case "json":
|
|
418
|
+
assertSchemaKeys(value, path, [
|
|
419
|
+
"type",
|
|
420
|
+
...requiredKey,
|
|
421
|
+
...ANNOTATION_KEYS
|
|
422
|
+
]);
|
|
423
|
+
break;
|
|
424
|
+
/* v8 ignore next 2 -- SCHEMA_TYPES narrows this closed switch before dispatch. */
|
|
425
|
+
default: throw new Error(`harness.defineTool ${path} must declare a valid type: ${VALID_TYPES}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/* v8 ignore next -- the root map task assigns before scheduling descendants. */
|
|
429
|
+
return holder.value ?? {};
|
|
430
|
+
}
|
|
431
|
+
function markDynamicTool(tool) {
|
|
432
|
+
Object.defineProperty(tool, DYNAMIC_TOOL, { value: true });
|
|
433
|
+
return tool;
|
|
434
|
+
}
|
|
435
|
+
function assertDynamicTool(tool) {
|
|
436
|
+
if (!isPlainRecord(tool) || tool[DYNAMIC_TOOL] !== true) throw new Error("dynamic tool registration must use a tool returned by harness.defineTool(...)");
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Structurally a content block, checked AFTER the JSON round-trip: a plain
|
|
440
|
+
* object carrying a string `type` tag. Deliberately nothing deeper — the
|
|
441
|
+
* ContentBlock union is merge-extensible (an unknown tag must pass), and every
|
|
442
|
+
* downstream consumer dispatches on `type` and falls through unknowns.
|
|
443
|
+
*/
|
|
444
|
+
function isContentBlockShape(value) {
|
|
445
|
+
return isPlainRecord(value) && typeof value.type === "string";
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* How much of an invalid execute return the teaching error echoes back — a
|
|
449
|
+
* huge blob would burn the model turn the error is trying to save.
|
|
450
|
+
*/
|
|
451
|
+
const RETURN_PREVIEW_LIMIT = 120;
|
|
452
|
+
/**
|
|
453
|
+
* Compact JSON preview of an invalid execute return for the teaching error
|
|
454
|
+
* (`String(…)` for the un-stringifiable undefined case), truncated to
|
|
455
|
+
* {@link RETURN_PREVIEW_LIMIT}.
|
|
456
|
+
*/
|
|
457
|
+
function describeReturn(value) {
|
|
458
|
+
const json = JSON.stringify(value);
|
|
459
|
+
return json.length > RETURN_PREVIEW_LIMIT ? `${json.slice(0, RETURN_PREVIEW_LIMIT)}…` : json;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Validate and host-materialize a sandbox renderer's content blocks.
|
|
463
|
+
*/
|
|
464
|
+
function assertRenderedContent(value) {
|
|
465
|
+
if (Array.isArray(value) && value.every(isContentBlockShape)) return value;
|
|
466
|
+
throw new Error(`output.render returned ${describeReturn(value)} — it must return an ARRAY of content blocks:\n ✓ return [{ type: 'text', text: String(value) }]`);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The `harness.defineTool` handed into the sandbox: the real DSL, with `parameters` normalized
|
|
470
|
+
* into a fresh host-realm ParameterSchemaSpec (raw object wrappers unwrapped,
|
|
471
|
+
* required arrays mapped, and explicit DSL object openness enforced) and the tool's `execute` return normalized into the host realm
|
|
472
|
+
* via a JSON round-trip. Non-JSON or wrong-shape output fails that call instead of poisoning
|
|
473
|
+
* the session log.
|
|
474
|
+
* @param options - the standard `defineTool` options; `parameters` may be the ParameterSchemaSpec DSL or a JSON-Schema-style wrapper.
|
|
475
|
+
* @returns the marker-tagged definition `harness.registerTool` (and the guarded `ctx.tools.register`) accepts.
|
|
476
|
+
*/
|
|
477
|
+
function sandboxDefineTool(options) {
|
|
478
|
+
if (!isPlainRecord(options)) throw new Error("harness.defineTool options must be an object");
|
|
479
|
+
const normalized = normalizeParameterSchemaSpec(options.parameters);
|
|
480
|
+
if (!isPlainRecord(options.output)) throw new Error("harness.defineTool output must declare { schema, render, presentationMeta? }");
|
|
481
|
+
const output = options.output;
|
|
482
|
+
if (typeof output.render !== "function") throw new Error("harness.defineTool output.render must be a function");
|
|
483
|
+
if (output.presentationMeta !== void 0 && typeof output.presentationMeta !== "function") throw new Error("harness.defineTool output.presentationMeta must be a function when present");
|
|
484
|
+
if (typeof options.execute !== "function") throw new Error("harness.defineTool execute must be a function");
|
|
485
|
+
const schema = cloneJson(output.schema, "harness.defineTool output.schema");
|
|
486
|
+
const rawExecute = options.execute;
|
|
487
|
+
const rawRender = output.render;
|
|
488
|
+
const rawPresentationMeta = output.presentationMeta;
|
|
489
|
+
const tool = defineTool({
|
|
490
|
+
...options,
|
|
491
|
+
parameters: normalized.spec,
|
|
492
|
+
output: {
|
|
493
|
+
schema,
|
|
494
|
+
render(args, value) {
|
|
495
|
+
return assertRenderedContent(cloneJson(rawRender(args, value), "harness.defineTool output.render result"));
|
|
496
|
+
},
|
|
497
|
+
...rawPresentationMeta !== void 0 ? { presentationMeta(args, value) {
|
|
498
|
+
return cloneJson(rawPresentationMeta(args, value), "harness.defineTool output.presentationMeta result");
|
|
499
|
+
} } : {}
|
|
500
|
+
},
|
|
501
|
+
async execute(args, exec) {
|
|
502
|
+
return cloneJson(await rawExecute(args, exec), "harness.defineTool execute result");
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const parameters = {
|
|
506
|
+
...tool.parameters,
|
|
507
|
+
...normalized.rootAnnotations
|
|
508
|
+
};
|
|
509
|
+
assertSupportedJsonSchema(parameters);
|
|
510
|
+
return markDynamicTool({
|
|
511
|
+
...tool,
|
|
512
|
+
parameters
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Normalize one `harness.handle` registration at the sandbox boundary: the
|
|
517
|
+
* method name must be a non-empty string and the handler a function whose
|
|
518
|
+
* result is host-materialized through the same cross-realm JSON clone as tool
|
|
519
|
+
* `execute` returns (a VM-realm object would otherwise escape the wire's
|
|
520
|
+
* plain-object contract).
|
|
521
|
+
* @param method - handler name the package's browser half calls through `host.call`.
|
|
522
|
+
* @param fn - sandbox handler receiving the wire-decoded JSON arguments.
|
|
523
|
+
* @returns the validated name and the clone-wrapped handler.
|
|
524
|
+
*/
|
|
525
|
+
function normalizeHandler(method, fn) {
|
|
526
|
+
if (typeof method !== "string" || method.length === 0) throw new Error("harness.handle(method, fn) needs a non-empty string method name");
|
|
527
|
+
if (typeof fn !== "function") throw new Error(`harness.handle("${method}") needs a handler function as its second argument`);
|
|
528
|
+
const rawHandler = fn;
|
|
529
|
+
return {
|
|
530
|
+
method,
|
|
531
|
+
handler: async (args) => cloneJson(await rawHandler(args), `harness.handle("${method}") result`)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The `harness.registerTool` handed into the sandbox: registers a
|
|
536
|
+
* marker-verified dynamic tool on the given context's registry.
|
|
537
|
+
* @param ctx - the (guarded) context whose `tools` service receives the tool.
|
|
538
|
+
* @param tool - a definition produced by {@link sandboxDefineTool}; anything else is rejected.
|
|
539
|
+
* @returns the registry disposer for the registration.
|
|
540
|
+
*/
|
|
541
|
+
function sandboxRegisterTool(ctx, tool) {
|
|
542
|
+
assertDynamicTool(tool);
|
|
543
|
+
return ctx.tools.register(tool);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The verbs a running host half may reach through the sandbox `ctx` façade, beyond its injected
|
|
547
|
+
* services. `on`/`once` observe events, `provide` exposes a service to other packages, and the
|
|
548
|
+
* timer helpers schedule work — each a fiber effect that unwinds when the package stops.
|
|
549
|
+
*/
|
|
550
|
+
const CTX_VERBS = new Set([
|
|
551
|
+
"effect",
|
|
552
|
+
"on",
|
|
553
|
+
"once",
|
|
554
|
+
"provide",
|
|
555
|
+
"timeout",
|
|
556
|
+
"interval",
|
|
557
|
+
"setTimeout",
|
|
558
|
+
"setInterval",
|
|
559
|
+
"throttle",
|
|
560
|
+
"debounce"
|
|
561
|
+
]);
|
|
562
|
+
const TIMER_VERBS = new Set([
|
|
563
|
+
"timeout",
|
|
564
|
+
"interval",
|
|
565
|
+
"setTimeout",
|
|
566
|
+
"setInterval",
|
|
567
|
+
"throttle",
|
|
568
|
+
"debounce"
|
|
569
|
+
]);
|
|
570
|
+
/**
|
|
571
|
+
* The tool-registry façade: `register` (marker-guarded) plus READ-ONLY
|
|
572
|
+
* metadata (`schemas`, and `get` returning a schema view, never the live
|
|
573
|
+
* `ToolDefinition`). Exposing the raw definition would hand package code the
|
|
574
|
+
* tool's `execute` function, letting it call another tool directly and bypass
|
|
575
|
+
* `ToolRuntime.execute` — identity protection, pre-policy, monotonic guards,
|
|
576
|
+
* around dispatch, post-policy, final observation, and result normalization. So `get` returns the same
|
|
577
|
+
* name/description/parameters view as `schemas()`, and nothing invocable.
|
|
578
|
+
*/
|
|
579
|
+
function sandboxTools(ctx) {
|
|
580
|
+
return {
|
|
581
|
+
register: (tool) => sandboxRegisterTool(ctx, tool),
|
|
582
|
+
schemas: () => ctx.tools.schemas(scopeOf(ctx)),
|
|
583
|
+
get: (name) => ctx.tools.schemas(scopeOf(ctx)).find((schema) => schema.name === name)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Reject any injected-service return that is a cordis `Context`. Harness
|
|
588
|
+
* services return data, never a context; a value that is one would be a
|
|
589
|
+
* fresh, unguarded handle back into the runtime — the exact escape the façade
|
|
590
|
+
* exists to close — so it fails loud instead of reaching sandbox code.
|
|
591
|
+
*/
|
|
592
|
+
function denyContext(value, service, reportFailure) {
|
|
593
|
+
if (value instanceof Context) return rejectGuard(reportFailure, `service "${service}" returned a cordis Context, which the sandbox does not expose. Operate through your own plugin ctx (ctx.on / ctx.provide / ctx.tools.register) and the services you inject — never another context.`);
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Wrap an injected service so its methods forward to the real instance but
|
|
598
|
+
* their return values pass through {@link denyContext}. Non-function members
|
|
599
|
+
* (plain data) pass through as-is; a returned Promise is guarded on resolve.
|
|
600
|
+
*/
|
|
601
|
+
function guardedService(service, name, reportFailure) {
|
|
602
|
+
return new Proxy(service, { get(target, prop) {
|
|
603
|
+
const value = Reflect.get(target, prop, target);
|
|
604
|
+
if (typeof value !== "function") return denyContext(value, name, reportFailure);
|
|
605
|
+
return (...args) => {
|
|
606
|
+
const result = Reflect.apply(value, target, args);
|
|
607
|
+
if (result instanceof Promise) return result.then((v) => denyContext(v, name, reportFailure));
|
|
608
|
+
return denyContext(result, name, reportFailure);
|
|
609
|
+
};
|
|
610
|
+
} });
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The service names a plugin declared in `inject`, as a lookup set. Whatever
|
|
614
|
+
* declaration style the plugin used — an `inject: ['bash', 'tools']` array or
|
|
615
|
+
* the `{ required, optional }` object form — cordis resolves it into a single
|
|
616
|
+
* name-keyed map on the fiber before `apply` runs (`{ bash: null, tools: null }`),
|
|
617
|
+
* so the gate just reads that map's keys. A host half may reach only the services
|
|
618
|
+
* it declared — that is what lets cordis park it when a declared provider
|
|
619
|
+
* goes away.
|
|
620
|
+
*/
|
|
621
|
+
function declaredInjects(ctx) {
|
|
622
|
+
return new Set(Object.keys(ctx.fiber.inject));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Whitelist context for running host halves: lifecycle-safe verbs, guarded
|
|
626
|
+
* tools, optional `ctx.get()` lookup, and declared-service property access.
|
|
627
|
+
* Framework plumbing is denied, and service methods cannot return a Context.
|
|
628
|
+
*/
|
|
629
|
+
function sandboxContext(ctx, reportFailure) {
|
|
630
|
+
const tools = sandboxTools(ctx);
|
|
631
|
+
const declared = declaredInjects(ctx);
|
|
632
|
+
const denyRead = (prop) => {
|
|
633
|
+
if (ctx.get(prop) !== void 0) return rejectGuard(reportFailure, `service "${prop}" is not injected. Declare it: inject: ['${prop}', …] on your plugin, so cordis parks this dynamic package if the provider later goes away.`);
|
|
634
|
+
return rejectGuard(reportFailure, `sandbox ctx does not expose "${prop}". Available: ctx.tools.register / ctx.on / ctx.provide / the timer helpers after injecting timer, and any service you declared in inject. Framework internals (root, fiber, registry, extend, plugin, …) are withheld by design.`);
|
|
635
|
+
};
|
|
636
|
+
const readService = (name, requireDeclaration) => {
|
|
637
|
+
if (name === "tools") return tools;
|
|
638
|
+
if (requireDeclaration && !declared.has(name)) return denyRead(name);
|
|
639
|
+
const service = denyContext(ctx.get(name), name, reportFailure);
|
|
640
|
+
if (service === null || typeof service !== "object" && typeof service !== "function") return service;
|
|
641
|
+
return guardedService(service, name, reportFailure);
|
|
642
|
+
};
|
|
643
|
+
const get = (name) => readService(name, false);
|
|
644
|
+
return new Proxy({}, {
|
|
645
|
+
get(_target, prop) {
|
|
646
|
+
if (prop === "tools") return tools;
|
|
647
|
+
if (prop === "get") return get;
|
|
648
|
+
if (typeof prop !== "string") return void 0;
|
|
649
|
+
if (CTX_VERBS.has(prop)) return (...args) => {
|
|
650
|
+
if (TIMER_VERBS.has(prop) && !declared.has("timer")) return denyRead("timer");
|
|
651
|
+
const method = ctx[prop];
|
|
652
|
+
return Reflect.apply(method, ctx, args);
|
|
653
|
+
};
|
|
654
|
+
return readService(prop, true);
|
|
655
|
+
},
|
|
656
|
+
set(_target, prop) {
|
|
657
|
+
return rejectGuard(reportFailure, `sandbox ctx is read-only; cannot assign "${String(prop)}"`);
|
|
658
|
+
},
|
|
659
|
+
has: (_target, prop) => prop === "tools" || prop === "get" || typeof prop === "string" && (CTX_VERBS.has(prop) && (!TIMER_VERBS.has(prop) || declared.has("timer")) || declared.has(prop))
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Narrow an arbitrary sandbox return value to a runnable cordis plugin: a
|
|
664
|
+
* function, or an object with an `apply` function. (A bare function passes the
|
|
665
|
+
* first arm, so the object arm never sees `Function.prototype.apply`.)
|
|
666
|
+
* @param value - whatever the host half returned.
|
|
667
|
+
* @returns whether the value can be started via `ctx.plugin`.
|
|
668
|
+
*/
|
|
669
|
+
function isPlugin(value) {
|
|
670
|
+
if (typeof value === "function") return true;
|
|
671
|
+
return typeof value === "object" && value !== null && typeof value.apply === "function";
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Wrap a plugin so `apply` receives the sandbox context while preserving injection metadata.
|
|
675
|
+
* @param plugin - the plugin the host half returned.
|
|
676
|
+
* @param reportFailure - reports a guard rejection to the owning Agent.
|
|
677
|
+
* @returns an equivalent plugin whose `apply` sees the sandbox context façade.
|
|
678
|
+
*/
|
|
679
|
+
function guardedPlugin(plugin, reportFailure) {
|
|
680
|
+
if (typeof plugin === "function") {
|
|
681
|
+
const functionPlugin = plugin;
|
|
682
|
+
return {
|
|
683
|
+
name: pluginName(plugin),
|
|
684
|
+
apply(ctx, config) {
|
|
685
|
+
return functionPlugin(sandboxContext(ctx, reportFailure), config);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const objectPlugin = plugin;
|
|
690
|
+
return {
|
|
691
|
+
...plugin,
|
|
692
|
+
apply(ctx, config) {
|
|
693
|
+
return objectPlugin.apply(sandboxContext(ctx, reportFailure), config);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function rejectGuard(reportFailure, message) {
|
|
698
|
+
const error = new Error(message);
|
|
699
|
+
reportFailure(error);
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Display name for a running plugin: its `name` property, else anonymous.
|
|
704
|
+
* @param plugin - the plugin the host half returned.
|
|
705
|
+
* @returns the human-readable name used in run results and inspect output.
|
|
706
|
+
*/
|
|
707
|
+
function pluginName(plugin) {
|
|
708
|
+
const named = plugin.name;
|
|
709
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
710
|
+
return "<anonymous>";
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region lib/types/inspect-registry.js
|
|
714
|
+
/** Host registry for model-visible, read-only Cordis capability queries. */
|
|
715
|
+
/** Registry and cross-page router behind the two model-facing inspect tools. */
|
|
716
|
+
var CordisInspectRegistryService = class extends Service {
|
|
717
|
+
providers = /* @__PURE__ */ new Map();
|
|
718
|
+
pending = /* @__PURE__ */ new Map();
|
|
719
|
+
clientManifest;
|
|
720
|
+
nextRequest = 1;
|
|
721
|
+
/** Register the process-global Host registry. */
|
|
722
|
+
constructor(ctx) {
|
|
723
|
+
super(ctx, "cordisInspect");
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Register one Host provider.
|
|
727
|
+
* @param registration - manifest and local query handler.
|
|
728
|
+
* @returns idempotent disposer.
|
|
729
|
+
*/
|
|
730
|
+
register(registration) {
|
|
731
|
+
const manifest = validateManifest(registration.manifest);
|
|
732
|
+
if (this.providers.has(manifest.id)) throw new Error(`Host Cordis inspect provider "${manifest.id}" is already registered`);
|
|
733
|
+
const stored = {
|
|
734
|
+
...registration,
|
|
735
|
+
manifest
|
|
736
|
+
};
|
|
737
|
+
this.providers.set(manifest.id, stored);
|
|
738
|
+
return () => {
|
|
739
|
+
if (this.providers.get(manifest.id) === stored) this.providers.delete(manifest.id);
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Replace the mirrored Client provider directory.
|
|
744
|
+
* @param providers - complete Client manifest snapshot.
|
|
745
|
+
*/
|
|
746
|
+
syncClientManifest(providers) {
|
|
747
|
+
const ids = /* @__PURE__ */ new Set();
|
|
748
|
+
const validated = providers.map((provider) => {
|
|
749
|
+
const manifest = validateManifest(provider);
|
|
750
|
+
if (ids.has(manifest.id)) throw new Error(`Client Cordis inspect manifest repeats provider "${manifest.id}"`);
|
|
751
|
+
ids.add(manifest.id);
|
|
752
|
+
return manifest;
|
|
753
|
+
});
|
|
754
|
+
this.clientManifest = Object.freeze(validated);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Return the complete known Host and Client provider directory.
|
|
758
|
+
* @returns Host providers followed by the Client providers.
|
|
759
|
+
*/
|
|
760
|
+
list() {
|
|
761
|
+
return [...[...this.providers.values()].map((provider) => view("host", provider.manifest)), ...(this.clientManifest ?? []).map((provider) => view("client", provider))];
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Execute one provider query on its owning platform.
|
|
765
|
+
* @param platform - Host or Client runtime.
|
|
766
|
+
* @param providerId - provider selected from {@link list}.
|
|
767
|
+
* @param methodName - declared method name.
|
|
768
|
+
* @param input - optional lossless JSON input.
|
|
769
|
+
* @param agent - requesting Agent and scope.
|
|
770
|
+
* @param signal - tool-call cancellation.
|
|
771
|
+
* @returns provider JSON data.
|
|
772
|
+
*/
|
|
773
|
+
async query(platform, providerId, methodName, input, agent, signal) {
|
|
774
|
+
if (platform === "host") {
|
|
775
|
+
const registration = this.providers.get(providerId);
|
|
776
|
+
if (registration === void 0) throw new Error(`Host Cordis inspect provider "${providerId}" is not registered`);
|
|
777
|
+
const method = findMethod(registration.manifest, methodName);
|
|
778
|
+
validateInput("Host", providerId, method, input);
|
|
779
|
+
signal.throwIfAborted();
|
|
780
|
+
const data = await registration.query(methodName, input, {
|
|
781
|
+
agent,
|
|
782
|
+
signal
|
|
783
|
+
});
|
|
784
|
+
signal.throwIfAborted();
|
|
785
|
+
return validateOutput("Host", providerId, method, data);
|
|
786
|
+
}
|
|
787
|
+
return await this.queryClient(providerId, methodName, input, agent, signal);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Accept the first valid Client response for a pending query.
|
|
791
|
+
* @param agent - Agent whose Session owns the query.
|
|
792
|
+
* @param requestId - Pending Client query identity.
|
|
793
|
+
* @param resolution - Client provider result or failure.
|
|
794
|
+
* @returns whether this response settled the still-pending query.
|
|
795
|
+
*/
|
|
796
|
+
resolveClientQuery(agent, requestId, resolution) {
|
|
797
|
+
const pending = this.pending.get(requestId);
|
|
798
|
+
if (pending === void 0 || pending.request.agentId !== agent.id) return { accepted: false };
|
|
799
|
+
if (!resolution.ok) return { accepted: false };
|
|
800
|
+
try {
|
|
801
|
+
resolution = {
|
|
802
|
+
ok: true,
|
|
803
|
+
data: validateOutput("Client", pending.request.provider, pending.method, resolution.data)
|
|
804
|
+
};
|
|
805
|
+
} catch {
|
|
806
|
+
return { accepted: false };
|
|
807
|
+
}
|
|
808
|
+
this.pending.delete(requestId);
|
|
809
|
+
pending.settle(resolution);
|
|
810
|
+
this.ctx.emit("cordis/inspect-query-resolved", { requestId });
|
|
811
|
+
return { accepted: true };
|
|
812
|
+
}
|
|
813
|
+
async queryClient(providerId, methodName, input, agent, signal) {
|
|
814
|
+
const provider = this.clientManifest?.find((candidate) => candidate.id === providerId);
|
|
815
|
+
if (provider === void 0) throw new Error(`Client Cordis inspect provider "${providerId}" is not registered`);
|
|
816
|
+
const method = findMethod(provider, methodName);
|
|
817
|
+
validateInput("Client", providerId, method, input);
|
|
818
|
+
signal.throwIfAborted();
|
|
819
|
+
const requestId = `inspect-${this.nextRequest++}`;
|
|
820
|
+
const request = {
|
|
821
|
+
requestId,
|
|
822
|
+
agentId: agent.id,
|
|
823
|
+
provider: providerId,
|
|
824
|
+
method: methodName,
|
|
825
|
+
...input === void 0 ? {} : { input }
|
|
826
|
+
};
|
|
827
|
+
const result = new Promise((resolve) => {
|
|
828
|
+
this.pending.set(requestId, {
|
|
829
|
+
request,
|
|
830
|
+
method,
|
|
831
|
+
settle: resolve
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
const onAbort = () => {
|
|
835
|
+
const pending = this.pending.get(requestId);
|
|
836
|
+
if (pending === void 0) return;
|
|
837
|
+
this.pending.delete(requestId);
|
|
838
|
+
pending.settle({
|
|
839
|
+
ok: false,
|
|
840
|
+
reason: "cancelled",
|
|
841
|
+
message: `Client inspect query ${providerId}.${methodName} was cancelled`
|
|
842
|
+
});
|
|
843
|
+
this.ctx.emit("cordis/inspect-query-resolved", { requestId });
|
|
844
|
+
};
|
|
845
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
846
|
+
if (signal.aborted) onAbort();
|
|
847
|
+
else this.ctx.emit("cordis/inspect-query", request);
|
|
848
|
+
try {
|
|
849
|
+
const resolution = await result;
|
|
850
|
+
if (!resolution.ok) throw new Error(`${providerId}.${methodName}: ${resolution.message}`);
|
|
851
|
+
return resolution.data;
|
|
852
|
+
} finally {
|
|
853
|
+
signal.removeEventListener("abort", onAbort);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
function view(platform, manifest) {
|
|
858
|
+
return {
|
|
859
|
+
platform,
|
|
860
|
+
...manifest,
|
|
861
|
+
methods: [...manifest.methods]
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function validateManifest(manifest) {
|
|
865
|
+
if (manifest.id.trim() === "") throw new Error("Cordis inspect provider id must not be empty");
|
|
866
|
+
if (manifest.description.trim() === "") throw new Error(`Cordis inspect provider "${manifest.id}" needs a description`);
|
|
867
|
+
const names = /* @__PURE__ */ new Set();
|
|
868
|
+
const methods = manifest.methods.map((method) => {
|
|
869
|
+
if (method.name.trim() === "") throw new Error(`Cordis inspect provider "${manifest.id}" has an empty method name`);
|
|
870
|
+
if (names.has(method.name)) throw new Error(`Cordis inspect provider "${manifest.id}" repeats method "${method.name}"`);
|
|
871
|
+
if (method.description.trim() === "") throw new Error(`Cordis inspect method ${manifest.id}.${method.name} needs a description`);
|
|
872
|
+
assertSupportedJsonSchema(method.inputSchema);
|
|
873
|
+
assertSupportedJsonSchema(method.outputSchema);
|
|
874
|
+
names.add(method.name);
|
|
875
|
+
return Object.freeze({ ...method });
|
|
876
|
+
});
|
|
877
|
+
return Object.freeze({
|
|
878
|
+
...manifest,
|
|
879
|
+
methods: Object.freeze(methods)
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function findMethod(manifest, name) {
|
|
883
|
+
const method = manifest.methods.find((candidate) => candidate.name === name);
|
|
884
|
+
if (method === void 0) throw new Error(`Cordis inspect provider "${manifest.id}" has no method "${name}"`);
|
|
885
|
+
return method;
|
|
886
|
+
}
|
|
887
|
+
function validateInput(platform, provider, method, input) {
|
|
888
|
+
const violations = validateJsonSchemaValue(method.inputSchema, input ?? {}, "input");
|
|
889
|
+
if (violations.length > 0) throw new Error(`${platform} Cordis inspect ${provider}.${method.name} rejected input: ${violations.join("; ")}`);
|
|
890
|
+
}
|
|
891
|
+
function validateOutput(platform, provider, method, data) {
|
|
892
|
+
const snapshot = snapshotJsonValue(data);
|
|
893
|
+
if (snapshot === void 0) throw new Error(`${platform} Cordis inspect ${provider}.${method.name} returned a non-JSON value`);
|
|
894
|
+
const violations = validateJsonSchemaValue(method.outputSchema, snapshot, "output");
|
|
895
|
+
if (violations.length > 0) throw new Error(`${platform} Cordis inspect ${provider}.${method.name} returned invalid output: ${violations.join("; ")}`);
|
|
896
|
+
return snapshot;
|
|
897
|
+
}
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region lib/types/lifecycle.js
|
|
900
|
+
/**
|
|
901
|
+
* Host-half fiber lifecycle over the `cordis-dynamic` group: settle a
|
|
902
|
+
* sandbox-produced plugin as a child fiber (never leaving a failed fiber
|
|
903
|
+
* mounted), and report the services a settled-but-pending fiber still waits
|
|
904
|
+
* for. Stopping needs no helper — a host half unwinds through an ordinary
|
|
905
|
+
* awaited `fiber.dispose()`, because everything the plugin registered is an
|
|
906
|
+
* effect on its fiber.
|
|
907
|
+
* @module @deepseek-ai/dsh-cordis-host-runner/lifecycle
|
|
908
|
+
*/
|
|
909
|
+
/**
|
|
910
|
+
* Await the group, start and settle one guarded child, and dispose it before rethrowing any
|
|
911
|
+
* startup failure so a failed run never lingers. A valid unresolved inject may remain pending.
|
|
912
|
+
* @param group - the `cordis-dynamic` group fiber every host half hangs under.
|
|
913
|
+
* @param plugin - the plugin the sandbox returned; wrapped with the registration guard before starting.
|
|
914
|
+
* @param reportGuardFailure - reports post-activation Host guard rejections to the owning Agent.
|
|
915
|
+
* @returns the settled child fiber (possibly pending on unsatisfied `inject`).
|
|
916
|
+
*/
|
|
917
|
+
async function startHostHalf(group, plugin, reportGuardFailure) {
|
|
918
|
+
await group.await();
|
|
919
|
+
const fiber = group.ctx.plugin(guardedPlugin(plugin, reportGuardFailure));
|
|
920
|
+
try {
|
|
921
|
+
await fiber.await();
|
|
922
|
+
} catch (error) {
|
|
923
|
+
await fiber.dispose();
|
|
924
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
925
|
+
if (message.includes("already registered")) throw new Error(`${message} — to REPLACE something an earlier dynamic package registered, first cordis_stop that package's id (find it with cordis_runtime_inspect what:"temporary"), then run the new version.`);
|
|
926
|
+
throw error instanceof Error ? error : new Error(message);
|
|
927
|
+
}
|
|
928
|
+
return fiber;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* The services a fiber declared in `inject` that do not exist yet — a settled
|
|
932
|
+
* fiber that is not active is waiting on exactly these (legal cordis
|
|
933
|
+
* semantics: it activates when the service appears).
|
|
934
|
+
* @param ctx - the context to resolve service existence against.
|
|
935
|
+
* @param fiber - the host-half fiber whose `inject` declarations are checked.
|
|
936
|
+
* @returns the missing service names, in declaration order.
|
|
937
|
+
*/
|
|
938
|
+
function missingServices(ctx, fiber) {
|
|
939
|
+
return Object.keys(fiber.inject).filter((service) => ctx.get(service) === void 0);
|
|
940
|
+
}
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region lib/types/registry.js
|
|
943
|
+
/**
|
|
944
|
+
* Process-local dynamic Plugin registry and its opaque identity mints.
|
|
945
|
+
* @module @deepseek-ai/dsh-cordis-host-runner/registry
|
|
946
|
+
*/
|
|
947
|
+
/** Registry, identity mints, and pending approval index. */
|
|
948
|
+
var DynamicCordisRegistry = class {
|
|
949
|
+
plugins = /* @__PURE__ */ new Map();
|
|
950
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
951
|
+
nextPlugin = 1;
|
|
952
|
+
nextPackage = 1;
|
|
953
|
+
nextRun = 1;
|
|
954
|
+
nextApproval = 1;
|
|
955
|
+
/**
|
|
956
|
+
* Mint a semantic plugin ID without reusing a prior suffix.
|
|
957
|
+
* @param prefix - validated lowercase semantic prefix proposed by the model.
|
|
958
|
+
* @returns a process-unique Plugin ID.
|
|
959
|
+
*/
|
|
960
|
+
mintPluginId(prefix) {
|
|
961
|
+
let id;
|
|
962
|
+
do
|
|
963
|
+
id = `${prefix}-${this.nextPlugin++}`;
|
|
964
|
+
while (this.plugins.has(id));
|
|
965
|
+
return id;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Mint an immutable package ID.
|
|
969
|
+
* @returns a process-unique Package ID.
|
|
970
|
+
*/
|
|
971
|
+
mintPackageId() {
|
|
972
|
+
return `pkg-${this.nextPackage++}`;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Mint an activation ID.
|
|
976
|
+
* @returns a process-unique Plugin Run ID.
|
|
977
|
+
*/
|
|
978
|
+
mintPluginRunId() {
|
|
979
|
+
return `run-${this.nextRun++}`;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Mint an approval ID.
|
|
983
|
+
* @returns a process-unique approval request ID.
|
|
984
|
+
*/
|
|
985
|
+
mintApprovalRequestId() {
|
|
986
|
+
return `approval-${this.nextApproval++}`;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Add one stable plugin.
|
|
990
|
+
* @param plugin - Plugin record to retain under its stable ID.
|
|
991
|
+
*/
|
|
992
|
+
add(plugin) {
|
|
993
|
+
this.plugins.set(plugin.pluginId, plugin);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Read one plugin.
|
|
997
|
+
* @param id - stable Plugin ID.
|
|
998
|
+
* @returns the Plugin record, or `undefined` when absent.
|
|
999
|
+
*/
|
|
1000
|
+
get(id) {
|
|
1001
|
+
return this.plugins.get(id);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Delete one plugin and all package versions.
|
|
1005
|
+
* @param id - stable Plugin ID to remove.
|
|
1006
|
+
* @returns whether a Plugin record was removed.
|
|
1007
|
+
*/
|
|
1008
|
+
delete(id) {
|
|
1009
|
+
return this.plugins.delete(id);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Read all plugins in creation order.
|
|
1013
|
+
* @returns a snapshot of every Plugin record.
|
|
1014
|
+
*/
|
|
1015
|
+
all() {
|
|
1016
|
+
return [...this.plugins.values()];
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Read one session's plugins in creation order.
|
|
1020
|
+
* @param sessionId - owning session to filter by.
|
|
1021
|
+
* @returns a snapshot of matching Plugin records.
|
|
1022
|
+
*/
|
|
1023
|
+
ofSession(sessionId) {
|
|
1024
|
+
return this.all().filter((plugin) => plugin.sessionId === sessionId);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Publish one pending approval.
|
|
1028
|
+
* @param id - approval request ID.
|
|
1029
|
+
* @param pending - resolver and Plugin metadata retained until settlement.
|
|
1030
|
+
*/
|
|
1031
|
+
armRequest(id, pending) {
|
|
1032
|
+
this.pendingRequests.set(id, pending);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Read one pending approval without claiming it.
|
|
1036
|
+
* @param id - approval request ID.
|
|
1037
|
+
* @returns the pending request, or `undefined` when absent.
|
|
1038
|
+
*/
|
|
1039
|
+
peekRequest(id) {
|
|
1040
|
+
return this.pendingRequests.get(id);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Claim one pending approval; first answer wins.
|
|
1044
|
+
* @param id - approval request ID.
|
|
1045
|
+
* @returns the claimed request, or `undefined` when already settled.
|
|
1046
|
+
*/
|
|
1047
|
+
claimRequest(id) {
|
|
1048
|
+
const pending = this.pendingRequests.get(id);
|
|
1049
|
+
if (pending !== void 0) this.pendingRequests.delete(id);
|
|
1050
|
+
return pending;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Cancel one pending approval.
|
|
1054
|
+
* @param id - approval request ID to remove.
|
|
1055
|
+
*/
|
|
1056
|
+
disarmRequest(id) {
|
|
1057
|
+
this.pendingRequests.delete(id);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Find a pending approval for one Plugin.
|
|
1061
|
+
* @param pluginId - stable Plugin ID.
|
|
1062
|
+
* @returns its approval request ID, or `undefined` when none is pending.
|
|
1063
|
+
*/
|
|
1064
|
+
pendingRequestFor(pluginId) {
|
|
1065
|
+
for (const [requestId, request] of this.pendingRequests) if (request.pluginId === pluginId) return requestId;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
//#endregion
|
|
1069
|
+
//#region lib/types/sandbox.js
|
|
1070
|
+
/**
|
|
1071
|
+
* The `node:vm` sandbox a dynamic package's HOST half evaluates in: a fresh realm whose globals
|
|
1072
|
+
* are a tagged write-through console, the `harness` registration helpers, the encoding primitives
|
|
1073
|
+
* a bare vm context lacks, and callable traps over the Node APIs the sandbox deliberately
|
|
1074
|
+
* withholds. Traps steer filesystem, network, process, and timer work to `ctx.fs`, `ctx.web`,
|
|
1075
|
+
* `ctx.bash`, and Cordis timers. This keeps cooperative packages inspectable and disposable but
|
|
1076
|
+
* is not containment: host-realm helper functions remain an escape route.
|
|
1077
|
+
*
|
|
1078
|
+
* The browser half never reaches this module — it is evaluated by the client-side runner in a
|
|
1079
|
+
* closure, with its own facade.
|
|
1080
|
+
* @module @deepseek-ai/dsh-cordis-host-runner/sandbox
|
|
1081
|
+
*/
|
|
1082
|
+
/** Exact Host closure symbols exposed by the sandbox and guarded Context. */
|
|
1083
|
+
const HOST_BUILTIN_INSPECTION = [
|
|
1084
|
+
{
|
|
1085
|
+
name: "ctx",
|
|
1086
|
+
description: "Restricted Cordis Context. Prefer ctx.get(name) with an undefined check; use inject for hard dependencies.",
|
|
1087
|
+
signatures: [
|
|
1088
|
+
"ctx.get(name: string): unknown | undefined",
|
|
1089
|
+
"ctx.on(name: string, listener: Function): () => void",
|
|
1090
|
+
"ctx.provide(name: string, value: unknown): () => void",
|
|
1091
|
+
"ctx.effect(callback: Function, label?: string): () => void"
|
|
1092
|
+
]
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
name: "harness",
|
|
1096
|
+
description: "Host helpers for Package-private Client RPC and model-visible dynamic Tools.",
|
|
1097
|
+
signatures: [
|
|
1098
|
+
"harness.handle(method: string, handler: (args: JsonValue) => JsonValue | Promise<JsonValue>): () => void",
|
|
1099
|
+
"harness.defineTool(definition: ToolDefinition): ToolDefinition",
|
|
1100
|
+
"harness.registerTool(ctx: Context, tool: ToolDefinition): () => void"
|
|
1101
|
+
]
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
name: "console",
|
|
1105
|
+
description: "Package-tagged Host logging.",
|
|
1106
|
+
signatures: ["console.log(...values): void", "console.error(...values): void"]
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: "btoa",
|
|
1110
|
+
description: "Encode UTF-8 text as base64.",
|
|
1111
|
+
signatures: ["btoa(value: string): string"]
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
name: "atob",
|
|
1115
|
+
description: "Decode base64 as UTF-8 text.",
|
|
1116
|
+
signatures: ["atob(value: string): string"]
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
name: "TextEncoder",
|
|
1120
|
+
description: "Standard UTF-8 encoder constructor.",
|
|
1121
|
+
signatures: ["new TextEncoder()"]
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
name: "TextDecoder",
|
|
1125
|
+
description: "Standard text decoder constructor.",
|
|
1126
|
+
signatures: ["new TextDecoder(label?: string)"]
|
|
1127
|
+
}
|
|
1128
|
+
];
|
|
1129
|
+
/**
|
|
1130
|
+
* A write-through console for one package, tagging every line with the package
|
|
1131
|
+
* id. Write-through (host stdout/stderr), NOT buffered into the tool result:
|
|
1132
|
+
* a registered listener fires long after the run call returned, and its output
|
|
1133
|
+
* must land somewhere the user can see — for a terminal entry point, the host terminal.
|
|
1134
|
+
*/
|
|
1135
|
+
function taggedConsole(id) {
|
|
1136
|
+
const tag = `[cordis:${id}]`;
|
|
1137
|
+
const log = (...args) => {
|
|
1138
|
+
console.log(tag, ...args);
|
|
1139
|
+
};
|
|
1140
|
+
const error = (...args) => {
|
|
1141
|
+
console.error(tag, ...args);
|
|
1142
|
+
};
|
|
1143
|
+
return {
|
|
1144
|
+
log,
|
|
1145
|
+
info: log,
|
|
1146
|
+
warn: log,
|
|
1147
|
+
debug: log,
|
|
1148
|
+
error
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Patch only VM constructors so `instanceof` accepts both VM values and host values passed as
|
|
1153
|
+
* arguments, events, or service results; host intrinsics remain untouched.
|
|
1154
|
+
*/
|
|
1155
|
+
const DUAL_REALM_INSTANCEOF_PRELUDE = `
|
|
1156
|
+
(hostIntrinsics) => {
|
|
1157
|
+
'use strict'
|
|
1158
|
+
const ordinary = Function.prototype[Symbol.hasInstance]
|
|
1159
|
+
for (const name of Object.keys(hostIntrinsics)) {
|
|
1160
|
+
const VmCtor = globalThis[name]
|
|
1161
|
+
const HostCtor = hostIntrinsics[name]
|
|
1162
|
+
if (typeof VmCtor !== 'function' || typeof HostCtor !== 'function') continue
|
|
1163
|
+
Object.defineProperty(VmCtor, Symbol.hasInstance, {
|
|
1164
|
+
value: (instance) => ordinary.call(VmCtor, instance) || ordinary.call(HostCtor, instance),
|
|
1165
|
+
configurable: true,
|
|
1166
|
+
})
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
`;
|
|
1170
|
+
/** Run {@link DUAL_REALM_INSTANCEOF_PRELUDE} in a freshly created sandbox, handing it the host intrinsics to pair up. */
|
|
1171
|
+
function patchDualRealmInstanceof(sandbox) {
|
|
1172
|
+
runInContext(DUAL_REALM_INSTANCEOF_PRELUDE, sandbox)({
|
|
1173
|
+
Object,
|
|
1174
|
+
Array,
|
|
1175
|
+
Function,
|
|
1176
|
+
Error,
|
|
1177
|
+
TypeError,
|
|
1178
|
+
RangeError,
|
|
1179
|
+
SyntaxError,
|
|
1180
|
+
Promise,
|
|
1181
|
+
RegExp,
|
|
1182
|
+
Date,
|
|
1183
|
+
Map,
|
|
1184
|
+
Set
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
const TIMER_REDIRECT = "Node timers are unavailable. Use the cordis timer service instead: declare inject: ['timer'] on your plugin and call ctx.timeout / ctx.interval after querying Host Service.listService for the exact overloads. Those calls are fiber effects, cleaned up automatically when stopped.";
|
|
1188
|
+
/**
|
|
1189
|
+
* The callable Node APIs the sandbox deliberately disables, each mapped to the
|
|
1190
|
+
* cordis alternative its trap error names. Only function-valued globals are
|
|
1191
|
+
* trapped; a data-valued global such as `process` stays `undefined`, because a
|
|
1192
|
+
* throwing accessor would detonate the common `typeof process` feature probe
|
|
1193
|
+
* at resolution time.
|
|
1194
|
+
*/
|
|
1195
|
+
const NODE_API_REDIRECTS = {
|
|
1196
|
+
require: "Node modules are unavailable. Use the cordis services on ctx instead — e.g. inject: ['fs'] for files, ['web'] for HTTP, ['bash'] for processes; query Service.listService with cordis_inspect_query first.",
|
|
1197
|
+
setTimeout: TIMER_REDIRECT,
|
|
1198
|
+
setInterval: TIMER_REDIRECT,
|
|
1199
|
+
setImmediate: TIMER_REDIRECT,
|
|
1200
|
+
clearTimeout: TIMER_REDIRECT,
|
|
1201
|
+
clearInterval: TIMER_REDIRECT,
|
|
1202
|
+
fetch: "Network access goes through the cordis web service: declare inject: ['web'] and call ctx.web (query Host Service.listService with cordis_inspect_query for its methods)."
|
|
1203
|
+
};
|
|
1204
|
+
/** Build the trap functions for {@link NODE_API_REDIRECTS}: calling one throws the redirect. */
|
|
1205
|
+
function nodeApiTraps() {
|
|
1206
|
+
const traps = {};
|
|
1207
|
+
for (const [name, redirect] of Object.entries(NODE_API_REDIRECTS)) traps[name] = () => {
|
|
1208
|
+
throw new Error(`${name} is not available in the dynamic package sandbox — ${redirect}`);
|
|
1209
|
+
};
|
|
1210
|
+
return traps;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Build the vm context one host half evaluates in: the tagged console, the
|
|
1214
|
+
* `harness` registration helpers, the encoding primitives, the Node-API traps,
|
|
1215
|
+
* and the dual-realm `instanceof` patch, already `createContext`-ed.
|
|
1216
|
+
* @param id - the package id (`dyn-<n>`), used as the console tag and filename stem.
|
|
1217
|
+
* @param harnessExtras - per-package `harness` verbs beyond the registration pair (`handle`).
|
|
1218
|
+
* @returns the contextified sandbox object to pass to {@link evaluateHostCode}.
|
|
1219
|
+
*/
|
|
1220
|
+
function createSandbox(id, harnessExtras = {}) {
|
|
1221
|
+
const sandbox = {
|
|
1222
|
+
...nodeApiTraps(),
|
|
1223
|
+
console: taggedConsole(id),
|
|
1224
|
+
harness: {
|
|
1225
|
+
defineTool: sandboxDefineTool,
|
|
1226
|
+
registerTool: sandboxRegisterTool,
|
|
1227
|
+
...harnessExtras
|
|
1228
|
+
},
|
|
1229
|
+
btoa: (s) => Buffer.from(s, "utf-8").toString("base64"),
|
|
1230
|
+
atob: (s) => Buffer.from(s, "base64").toString("utf-8"),
|
|
1231
|
+
TextEncoder,
|
|
1232
|
+
TextDecoder
|
|
1233
|
+
};
|
|
1234
|
+
createContext(sandbox);
|
|
1235
|
+
patchDualRealmInstanceof(sandbox);
|
|
1236
|
+
return sandbox;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Cross-realm SyntaxError detection: a compile failure inside `runInContext`
|
|
1240
|
+
* constructs its error in the SANDBOX realm, so a host `instanceof
|
|
1241
|
+
* SyntaxError` is silently false — the `name` property is the realm-safe tag.
|
|
1242
|
+
*/
|
|
1243
|
+
function isSyntaxError(error) {
|
|
1244
|
+
return typeof error === "object" && error !== null && error.name === "SyntaxError";
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* The parse-failure context a vm `SyntaxError` carries: the vm prints the
|
|
1248
|
+
* offending source line and a caret before the message, which is exactly what
|
|
1249
|
+
* a model needs to self-correct — surface it instead of the bare message.
|
|
1250
|
+
* Falls back to `String(error)` when the stack carries no such prelude.
|
|
1251
|
+
* @param error - the `SyntaxError` (host- or sandbox-realm) thrown while compiling package code.
|
|
1252
|
+
* @returns the stack prefix up to and including the `SyntaxError: …` line.
|
|
1253
|
+
*/
|
|
1254
|
+
function syntaxErrorContext(error) {
|
|
1255
|
+
const lines = (error.stack ?? "").split("\n");
|
|
1256
|
+
const messageIndex = lines.findIndex((line) => line.startsWith("SyntaxError"));
|
|
1257
|
+
if (messageIndex === -1) return String(error);
|
|
1258
|
+
return lines.slice(0, messageIndex + 1).join("\n");
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* The teaching text one parse failure produces, shared by the define-time
|
|
1262
|
+
* precheck and the run-time evaluation so a model reads the same diagnosis
|
|
1263
|
+
* whichever verb caught it.
|
|
1264
|
+
* @param half - which half failed to parse, named as the define argument that carried it.
|
|
1265
|
+
* @param context - the {@link syntaxErrorContext} of the failure.
|
|
1266
|
+
* @returns the model-facing error message.
|
|
1267
|
+
*/
|
|
1268
|
+
function parseErrorMessage(half, context) {
|
|
1269
|
+
const offendingLine = context.split("\n")[1] ?? "";
|
|
1270
|
+
if (/\bas\b/.test(offendingLine)) return `dynamic package \`${half}\` failed to parse:\n${context}\nThe sandbox runs plain JavaScript, not TypeScript. Remove type annotations:
|
|
1271
|
+
✗ { type: 'text' as const, text: x }
|
|
1272
|
+
✓ { type: 'text', text: x }`;
|
|
1273
|
+
return `dynamic package \`${half}\` failed to parse:\n${context}\nNote: it runs as the BODY of an async function (line numbers are offset by the 1-line wrapper). Check bracket balance — ending the returned plugin object with \`});\` closes a call that was never opened; a plain \`return { … }\` ends with \`}\` (an optional \`;\`), never \`)\`.`;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Parse one half's source without running it: the define-time precheck that
|
|
1277
|
+
* keeps unparseable code out of the registry, so a model fixes it and defines
|
|
1278
|
+
* again instead of discovering the failure at run time. Compiling through `vm`
|
|
1279
|
+
* rather than `new Function` is what makes the two agree — same wrapper, same
|
|
1280
|
+
* compiler, and the same source-line-and-caret prelude in the failure.
|
|
1281
|
+
* @param code - the model-written function body.
|
|
1282
|
+
* @param half - which define argument carried it, for the error text.
|
|
1283
|
+
* @throws when the body does not parse, with the offending line and a teaching hint.
|
|
1284
|
+
*/
|
|
1285
|
+
function precheckCode(code, half) {
|
|
1286
|
+
try {
|
|
1287
|
+
new Script(`(async () => {\n${code}\n})()`, { filename: `cordis-dyn-${half}.js` });
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (!isSyntaxError(error)) throw error;
|
|
1290
|
+
throw new Error(parseErrorMessage(half, syntaxErrorContext(error)));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Evaluate a host half as the body of an async function inside the sandbox. `vmTimeoutMs` only
|
|
1295
|
+
* bounds the SYNCHRONOUS portion; an async body escapes it — acceptable under the module's
|
|
1296
|
+
* trust stance. Parse errors include the offending line and a TypeScript-removal or bracket-
|
|
1297
|
+
* balance hint.
|
|
1298
|
+
* @param sandbox - the contextified object from {@link createSandbox}.
|
|
1299
|
+
* @param code - the model-written function body; must `return` a plugin.
|
|
1300
|
+
* @param id - the package id, used as the vm filename (`cordis-dyn-<id>.js`).
|
|
1301
|
+
* @param vmTimeoutMs - the synchronous evaluation bound in milliseconds.
|
|
1302
|
+
* @returns whatever the code returned, still un-narrowed (the run lifecycle checks plugin shape).
|
|
1303
|
+
*/
|
|
1304
|
+
async function evaluateHostCode(sandbox, code, id, vmTimeoutMs) {
|
|
1305
|
+
try {
|
|
1306
|
+
return await runInContext(`(async () => {\n${code}\n})()`, sandbox, {
|
|
1307
|
+
filename: `cordis-dyn-${id}.js`,
|
|
1308
|
+
timeout: vmTimeoutMs
|
|
1309
|
+
});
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (!isSyntaxError(error)) throw error;
|
|
1312
|
+
throw new Error(parseErrorMessage("code.host", syntaxErrorContext(error)));
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
//#endregion
|
|
1316
|
+
//#region lib/types/index.js
|
|
1317
|
+
/**
|
|
1318
|
+
* Dynamic Cordis Plugin service: immutable package definitions, one active run
|
|
1319
|
+
* per Plugin, human-approved Client activation, and Host/Client invocation.
|
|
1320
|
+
* @module @deepseek-ai/dsh-cordis-host-runner
|
|
1321
|
+
*/
|
|
1322
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
1323
|
+
var useValue = arguments.length > 2;
|
|
1324
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
1325
|
+
return useValue ? value : void 0;
|
|
1326
|
+
};
|
|
1327
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
1328
|
+
function accept(f) {
|
|
1329
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
1330
|
+
return f;
|
|
1331
|
+
}
|
|
1332
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
1333
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
1334
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
1335
|
+
var _, done = false;
|
|
1336
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1337
|
+
var context = {};
|
|
1338
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
1339
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
1340
|
+
context.addInitializer = function(f) {
|
|
1341
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
1342
|
+
extraInitializers.push(accept(f || null));
|
|
1343
|
+
};
|
|
1344
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
1345
|
+
get: descriptor.get,
|
|
1346
|
+
set: descriptor.set
|
|
1347
|
+
} : descriptor[key], context);
|
|
1348
|
+
if (kind === "accessor") {
|
|
1349
|
+
if (result === void 0) continue;
|
|
1350
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
1351
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
1352
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
1353
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
1354
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
1355
|
+
else descriptor[key] = _;
|
|
1356
|
+
}
|
|
1357
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
1358
|
+
done = true;
|
|
1359
|
+
};
|
|
1360
|
+
/**
|
|
1361
|
+
* Brand a Host-minted Plugin ID.
|
|
1362
|
+
* @param id - opaque identifier minted by the Host registry.
|
|
1363
|
+
* @returns the branded Plugin identifier.
|
|
1364
|
+
*/
|
|
1365
|
+
function CordisDynamicPluginId(id) {
|
|
1366
|
+
return id;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Brand a Host-minted Package ID.
|
|
1370
|
+
* @param id - opaque identifier minted by the Host registry.
|
|
1371
|
+
* @returns the branded Package identifier.
|
|
1372
|
+
*/
|
|
1373
|
+
function CordisDynamicPackageId(id) {
|
|
1374
|
+
return id;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Brand a Host-minted Plugin Run ID.
|
|
1378
|
+
* @param id - opaque identifier minted by the Host registry.
|
|
1379
|
+
* @returns the branded Plugin Run identifier.
|
|
1380
|
+
*/
|
|
1381
|
+
function CordisDynamicPluginRunId(id) {
|
|
1382
|
+
return id;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Brand a Host-minted approval request ID.
|
|
1386
|
+
* @param id - opaque identifier minted by the Host registry.
|
|
1387
|
+
* @returns the branded approval request identifier.
|
|
1388
|
+
*/
|
|
1389
|
+
function ApprovalRequestId(id) {
|
|
1390
|
+
return id;
|
|
1391
|
+
}
|
|
1392
|
+
/** Dynamic Plugin registry and Host-half lifecycle. */
|
|
1393
|
+
let DynamicCordisRunnerService = (() => {
|
|
1394
|
+
let _classSuper = TypertRemoteService;
|
|
1395
|
+
let _instanceExtraInitializers = [];
|
|
1396
|
+
let _undefineFromPanel_decorators;
|
|
1397
|
+
let _runHostHalf_decorators;
|
|
1398
|
+
let _getClientCode_decorators;
|
|
1399
|
+
let _resolveRequestRun_decorators;
|
|
1400
|
+
let _settleUserRun_decorators;
|
|
1401
|
+
let _stopFromPanel_decorators;
|
|
1402
|
+
let _syncInspectManifest_decorators;
|
|
1403
|
+
let _resolveInspectQuery_decorators;
|
|
1404
|
+
let _inventory_decorators;
|
|
1405
|
+
let _reportRenderFailure_decorators;
|
|
1406
|
+
let _reportClientGuardFailure_decorators;
|
|
1407
|
+
let _invoke_decorators;
|
|
1408
|
+
return class DynamicCordisRunnerService extends _classSuper {
|
|
1409
|
+
static {
|
|
1410
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1411
|
+
_undefineFromPanel_decorators = [Remote("undefineFromPanel")];
|
|
1412
|
+
_runHostHalf_decorators = [Remote("runHostHalf")];
|
|
1413
|
+
_getClientCode_decorators = [Remote("getClientCode")];
|
|
1414
|
+
_resolveRequestRun_decorators = [Remote("resolveRequestRun")];
|
|
1415
|
+
_settleUserRun_decorators = [Remote("settleUserRun")];
|
|
1416
|
+
_stopFromPanel_decorators = [Remote("stopFromPanel")];
|
|
1417
|
+
_syncInspectManifest_decorators = [Remote("syncInspectManifest")];
|
|
1418
|
+
_resolveInspectQuery_decorators = [Remote("resolveInspectQuery")];
|
|
1419
|
+
_inventory_decorators = [Remote("inventory")];
|
|
1420
|
+
_reportRenderFailure_decorators = [Remote("reportRenderFailure")];
|
|
1421
|
+
_reportClientGuardFailure_decorators = [Remote("reportClientGuardFailure")];
|
|
1422
|
+
_invoke_decorators = [Remote("invoke")];
|
|
1423
|
+
__esDecorate(this, null, _undefineFromPanel_decorators, {
|
|
1424
|
+
kind: "method",
|
|
1425
|
+
name: "undefineFromPanel",
|
|
1426
|
+
static: false,
|
|
1427
|
+
private: false,
|
|
1428
|
+
access: {
|
|
1429
|
+
has: (obj) => "undefineFromPanel" in obj,
|
|
1430
|
+
get: (obj) => obj.undefineFromPanel
|
|
1431
|
+
},
|
|
1432
|
+
metadata: _metadata
|
|
1433
|
+
}, null, _instanceExtraInitializers);
|
|
1434
|
+
__esDecorate(this, null, _runHostHalf_decorators, {
|
|
1435
|
+
kind: "method",
|
|
1436
|
+
name: "runHostHalf",
|
|
1437
|
+
static: false,
|
|
1438
|
+
private: false,
|
|
1439
|
+
access: {
|
|
1440
|
+
has: (obj) => "runHostHalf" in obj,
|
|
1441
|
+
get: (obj) => obj.runHostHalf
|
|
1442
|
+
},
|
|
1443
|
+
metadata: _metadata
|
|
1444
|
+
}, null, _instanceExtraInitializers);
|
|
1445
|
+
__esDecorate(this, null, _getClientCode_decorators, {
|
|
1446
|
+
kind: "method",
|
|
1447
|
+
name: "getClientCode",
|
|
1448
|
+
static: false,
|
|
1449
|
+
private: false,
|
|
1450
|
+
access: {
|
|
1451
|
+
has: (obj) => "getClientCode" in obj,
|
|
1452
|
+
get: (obj) => obj.getClientCode
|
|
1453
|
+
},
|
|
1454
|
+
metadata: _metadata
|
|
1455
|
+
}, null, _instanceExtraInitializers);
|
|
1456
|
+
__esDecorate(this, null, _resolveRequestRun_decorators, {
|
|
1457
|
+
kind: "method",
|
|
1458
|
+
name: "resolveRequestRun",
|
|
1459
|
+
static: false,
|
|
1460
|
+
private: false,
|
|
1461
|
+
access: {
|
|
1462
|
+
has: (obj) => "resolveRequestRun" in obj,
|
|
1463
|
+
get: (obj) => obj.resolveRequestRun
|
|
1464
|
+
},
|
|
1465
|
+
metadata: _metadata
|
|
1466
|
+
}, null, _instanceExtraInitializers);
|
|
1467
|
+
__esDecorate(this, null, _settleUserRun_decorators, {
|
|
1468
|
+
kind: "method",
|
|
1469
|
+
name: "settleUserRun",
|
|
1470
|
+
static: false,
|
|
1471
|
+
private: false,
|
|
1472
|
+
access: {
|
|
1473
|
+
has: (obj) => "settleUserRun" in obj,
|
|
1474
|
+
get: (obj) => obj.settleUserRun
|
|
1475
|
+
},
|
|
1476
|
+
metadata: _metadata
|
|
1477
|
+
}, null, _instanceExtraInitializers);
|
|
1478
|
+
__esDecorate(this, null, _stopFromPanel_decorators, {
|
|
1479
|
+
kind: "method",
|
|
1480
|
+
name: "stopFromPanel",
|
|
1481
|
+
static: false,
|
|
1482
|
+
private: false,
|
|
1483
|
+
access: {
|
|
1484
|
+
has: (obj) => "stopFromPanel" in obj,
|
|
1485
|
+
get: (obj) => obj.stopFromPanel
|
|
1486
|
+
},
|
|
1487
|
+
metadata: _metadata
|
|
1488
|
+
}, null, _instanceExtraInitializers);
|
|
1489
|
+
__esDecorate(this, null, _syncInspectManifest_decorators, {
|
|
1490
|
+
kind: "method",
|
|
1491
|
+
name: "syncInspectManifest",
|
|
1492
|
+
static: false,
|
|
1493
|
+
private: false,
|
|
1494
|
+
access: {
|
|
1495
|
+
has: (obj) => "syncInspectManifest" in obj,
|
|
1496
|
+
get: (obj) => obj.syncInspectManifest
|
|
1497
|
+
},
|
|
1498
|
+
metadata: _metadata
|
|
1499
|
+
}, null, _instanceExtraInitializers);
|
|
1500
|
+
__esDecorate(this, null, _resolveInspectQuery_decorators, {
|
|
1501
|
+
kind: "method",
|
|
1502
|
+
name: "resolveInspectQuery",
|
|
1503
|
+
static: false,
|
|
1504
|
+
private: false,
|
|
1505
|
+
access: {
|
|
1506
|
+
has: (obj) => "resolveInspectQuery" in obj,
|
|
1507
|
+
get: (obj) => obj.resolveInspectQuery
|
|
1508
|
+
},
|
|
1509
|
+
metadata: _metadata
|
|
1510
|
+
}, null, _instanceExtraInitializers);
|
|
1511
|
+
__esDecorate(this, null, _inventory_decorators, {
|
|
1512
|
+
kind: "method",
|
|
1513
|
+
name: "inventory",
|
|
1514
|
+
static: false,
|
|
1515
|
+
private: false,
|
|
1516
|
+
access: {
|
|
1517
|
+
has: (obj) => "inventory" in obj,
|
|
1518
|
+
get: (obj) => obj.inventory
|
|
1519
|
+
},
|
|
1520
|
+
metadata: _metadata
|
|
1521
|
+
}, null, _instanceExtraInitializers);
|
|
1522
|
+
__esDecorate(this, null, _reportRenderFailure_decorators, {
|
|
1523
|
+
kind: "method",
|
|
1524
|
+
name: "reportRenderFailure",
|
|
1525
|
+
static: false,
|
|
1526
|
+
private: false,
|
|
1527
|
+
access: {
|
|
1528
|
+
has: (obj) => "reportRenderFailure" in obj,
|
|
1529
|
+
get: (obj) => obj.reportRenderFailure
|
|
1530
|
+
},
|
|
1531
|
+
metadata: _metadata
|
|
1532
|
+
}, null, _instanceExtraInitializers);
|
|
1533
|
+
__esDecorate(this, null, _reportClientGuardFailure_decorators, {
|
|
1534
|
+
kind: "method",
|
|
1535
|
+
name: "reportClientGuardFailure",
|
|
1536
|
+
static: false,
|
|
1537
|
+
private: false,
|
|
1538
|
+
access: {
|
|
1539
|
+
has: (obj) => "reportClientGuardFailure" in obj,
|
|
1540
|
+
get: (obj) => obj.reportClientGuardFailure
|
|
1541
|
+
},
|
|
1542
|
+
metadata: _metadata
|
|
1543
|
+
}, null, _instanceExtraInitializers);
|
|
1544
|
+
__esDecorate(this, null, _invoke_decorators, {
|
|
1545
|
+
kind: "method",
|
|
1546
|
+
name: "invoke",
|
|
1547
|
+
static: false,
|
|
1548
|
+
private: false,
|
|
1549
|
+
access: {
|
|
1550
|
+
has: (obj) => "invoke" in obj,
|
|
1551
|
+
get: (obj) => obj.invoke
|
|
1552
|
+
},
|
|
1553
|
+
metadata: _metadata
|
|
1554
|
+
}, null, _instanceExtraInitializers);
|
|
1555
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
1556
|
+
enumerable: true,
|
|
1557
|
+
configurable: true,
|
|
1558
|
+
writable: true,
|
|
1559
|
+
value: _metadata
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
static inject = ["tools"];
|
|
1563
|
+
static Config = z.object({ vmTimeoutMs: z.number().min(1).default(5e3) });
|
|
1564
|
+
rootCtx = __runInitializers(this, _instanceExtraInitializers);
|
|
1565
|
+
registry = new DynamicCordisRegistry();
|
|
1566
|
+
inspectRegistry;
|
|
1567
|
+
starting = /* @__PURE__ */ new Map();
|
|
1568
|
+
resolved;
|
|
1569
|
+
group;
|
|
1570
|
+
/** Create the service under the Host composition. */
|
|
1571
|
+
constructor(ctx, config) {
|
|
1572
|
+
super(ctx, "dynamicCordisRunner");
|
|
1573
|
+
this.rootCtx = ctx;
|
|
1574
|
+
this.resolved = config;
|
|
1575
|
+
this.inspectRegistry = new CordisInspectRegistryService(ctx);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Define a new Plugin's first Package or append a Package to an existing Plugin.
|
|
1579
|
+
* @param request - Session ownership, Plugin selection, metadata, and source code.
|
|
1580
|
+
* @returns Host-minted Plugin and Package identities with declared-half metadata.
|
|
1581
|
+
*/
|
|
1582
|
+
define(request) {
|
|
1583
|
+
const name = request.name.trim();
|
|
1584
|
+
const purpose = request.purpose.trim();
|
|
1585
|
+
if (name.length === 0) throw new Error("cordis_define needs a non-empty `name`");
|
|
1586
|
+
if (purpose.length === 0) throw new Error("cordis_define needs a non-empty `purpose`");
|
|
1587
|
+
if (request.code.host === void 0 && request.code.client === void 0) throw new Error("cordis_define needs `code.host`, `code.client`, or both");
|
|
1588
|
+
if (request.code.host !== void 0) precheckCode(request.code.host, "code.host");
|
|
1589
|
+
if (request.code.client !== void 0) precheckCode(request.code.client, "code.client");
|
|
1590
|
+
let plugin;
|
|
1591
|
+
if (request.plugin.kind === "new") {
|
|
1592
|
+
const prefix = request.plugin.idPrefix.trim();
|
|
1593
|
+
if (!/^[a-z]{3,6}$/.test(prefix)) throw new Error("cordis_define `plugin.idPrefix` must contain 3–6 lowercase English letters");
|
|
1594
|
+
plugin = {
|
|
1595
|
+
pluginId: CordisDynamicPluginId(this.registry.mintPluginId(prefix)),
|
|
1596
|
+
sessionId: request.sessionId,
|
|
1597
|
+
packages: /* @__PURE__ */ new Map(),
|
|
1598
|
+
approvedClientPackages: /* @__PURE__ */ new Set(),
|
|
1599
|
+
clientVersionUpdatesApproved: false
|
|
1600
|
+
};
|
|
1601
|
+
this.registry.add(plugin);
|
|
1602
|
+
} else {
|
|
1603
|
+
const found = this.registry.get(request.plugin.pluginId);
|
|
1604
|
+
if (found === void 0 || found.sessionId !== request.sessionId) throw new Error(missingPluginMessage(request.plugin.pluginId));
|
|
1605
|
+
plugin = found;
|
|
1606
|
+
}
|
|
1607
|
+
const packageId = CordisDynamicPackageId(this.registry.mintPackageId());
|
|
1608
|
+
const definition = {
|
|
1609
|
+
packageId,
|
|
1610
|
+
name,
|
|
1611
|
+
purpose,
|
|
1612
|
+
...request.code.host === void 0 ? {} : { hostCode: request.code.host },
|
|
1613
|
+
...request.code.client === void 0 ? {} : { clientCode: request.code.client }
|
|
1614
|
+
};
|
|
1615
|
+
plugin.packages.set(packageId, definition);
|
|
1616
|
+
return {
|
|
1617
|
+
pluginId: plugin.pluginId,
|
|
1618
|
+
packageId,
|
|
1619
|
+
name,
|
|
1620
|
+
purpose,
|
|
1621
|
+
hasHostHalf: definition.hostCode !== void 0,
|
|
1622
|
+
hasClientHalf: definition.clientCode !== void 0
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Remove a Plugin, its active run, and all immutable Packages.
|
|
1627
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1628
|
+
* @param pluginId - Stable Plugin identity to remove.
|
|
1629
|
+
* @returns Whether removal succeeded and whether it stopped an active run.
|
|
1630
|
+
*/
|
|
1631
|
+
async undefine(agent, pluginId) {
|
|
1632
|
+
const plugin = this.owned(agent, pluginId);
|
|
1633
|
+
if (plugin === void 0) return {
|
|
1634
|
+
ok: false,
|
|
1635
|
+
reason: "plugin-missing",
|
|
1636
|
+
message: missingPluginMessage(pluginId)
|
|
1637
|
+
};
|
|
1638
|
+
const wasRunning = plugin.run !== void 0;
|
|
1639
|
+
this.cancelPending(pluginId, `dynamic plugin "${pluginId}" was removed before approval`);
|
|
1640
|
+
if (plugin.run !== void 0) await this.retract(plugin);
|
|
1641
|
+
this.registry.delete(pluginId);
|
|
1642
|
+
return {
|
|
1643
|
+
ok: true,
|
|
1644
|
+
wasRunning
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Remove a Plugin from the user panel and queue the resulting state change for the model's next step.
|
|
1649
|
+
* @param agent - Agent whose Session owns the Plugin and receives the context.
|
|
1650
|
+
* @param pluginId - Stable Plugin identity to remove.
|
|
1651
|
+
* @returns Whether removal succeeded and whether it stopped an active run.
|
|
1652
|
+
*/
|
|
1653
|
+
async undefineFromPanel(agent, pluginId) {
|
|
1654
|
+
const result = await this.undefine(agent, pluginId);
|
|
1655
|
+
if (result.ok) this.injectUserContext(agent, `The user removed Cordis Plugin ${pluginId} and all of its Packages. The Plugin no longer exists.`);
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Start or update one Package for a model tool call. An unauthorized Client
|
|
1660
|
+
* Package waits for approval; Plugin-wide authorization covers later versions.
|
|
1661
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1662
|
+
* @param pluginId - Stable Plugin identity to activate.
|
|
1663
|
+
* @param packageId - Immutable Package version to activate.
|
|
1664
|
+
* @param mode - Whether to run the current version or switch versions.
|
|
1665
|
+
* @param signal - Tool-call cancellation signal while the activation request is being created.
|
|
1666
|
+
* @returns The successful activation identity or an actionable refusal.
|
|
1667
|
+
*/
|
|
1668
|
+
async run(agent, pluginId, packageId, mode, signal) {
|
|
1669
|
+
const plan = this.resolvePlan(agent, pluginId, packageId, mode);
|
|
1670
|
+
if (!plan.ok) return plan.response;
|
|
1671
|
+
if (signal?.aborted === true) return {
|
|
1672
|
+
ok: false,
|
|
1673
|
+
reason: "cancelled",
|
|
1674
|
+
message: `the run request for dynamic plugin "${pluginId}" was cancelled before activation`
|
|
1675
|
+
};
|
|
1676
|
+
if (this.registry.pendingRequestFor(pluginId) !== void 0) return {
|
|
1677
|
+
ok: false,
|
|
1678
|
+
reason: "transition-in-flight",
|
|
1679
|
+
message: `dynamic plugin "${pluginId}" already has a pending run request`
|
|
1680
|
+
};
|
|
1681
|
+
const attempt = this.createAttempt(plan);
|
|
1682
|
+
plan.plugin.nextPackageId = packageId;
|
|
1683
|
+
plan.plugin.latestRun = attempt;
|
|
1684
|
+
if (plan.definition.clientCode === void 0) {
|
|
1685
|
+
const started = await this.activate(plan, void 0, false, attempt);
|
|
1686
|
+
if (started.ok) return this.runResponse(plan.plugin, started);
|
|
1687
|
+
this.failAttempt(plan.plugin, attempt, "host-load", started);
|
|
1688
|
+
return {
|
|
1689
|
+
...started,
|
|
1690
|
+
reason: "host-half-failed"
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
const requestId = ApprovalRequestId(this.registry.mintApprovalRequestId());
|
|
1694
|
+
const requiresApproval = !plan.plugin.clientVersionUpdatesApproved && !plan.plugin.approvedClientPackages.has(packageId);
|
|
1695
|
+
attempt.approvalRequestId = requestId;
|
|
1696
|
+
attempt.requiresApproval = requiresApproval;
|
|
1697
|
+
attempt.status = requiresApproval ? "awaiting-approval" : "starting-host";
|
|
1698
|
+
this.registry.armRequest(requestId, {
|
|
1699
|
+
agentId: agent.id,
|
|
1700
|
+
pluginId,
|
|
1701
|
+
packageId,
|
|
1702
|
+
pluginRunId: attempt.pluginRunId,
|
|
1703
|
+
mode,
|
|
1704
|
+
requiresApproval
|
|
1705
|
+
});
|
|
1706
|
+
this.ctx.emit("cordis/request-run", {
|
|
1707
|
+
requestId,
|
|
1708
|
+
agentId: agent.id,
|
|
1709
|
+
pluginId,
|
|
1710
|
+
packageId,
|
|
1711
|
+
mode,
|
|
1712
|
+
name: plan.definition.name,
|
|
1713
|
+
purpose: plan.definition.purpose,
|
|
1714
|
+
requiresApproval
|
|
1715
|
+
});
|
|
1716
|
+
return {
|
|
1717
|
+
ok: true,
|
|
1718
|
+
status: requiresApproval ? "awaiting-approval" : "starting",
|
|
1719
|
+
pluginId,
|
|
1720
|
+
packageId,
|
|
1721
|
+
pluginRunId: attempt.pluginRunId,
|
|
1722
|
+
mode,
|
|
1723
|
+
waitingFor: [],
|
|
1724
|
+
...plan.plugin.currentPackageId === void 0 ? {} : { currentPackageId: plan.plugin.currentPackageId },
|
|
1725
|
+
nextPackageId: packageId
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Start Host code for an approved request or a direct panel gesture.
|
|
1730
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1731
|
+
* @param pluginId - Stable Plugin identity to activate.
|
|
1732
|
+
* @param packageId - Immutable Package version to activate.
|
|
1733
|
+
* @param mode - Whether to run the current version or switch versions.
|
|
1734
|
+
* @param requestId - Model-driven request identity, or null for a direct user gesture.
|
|
1735
|
+
* @param approveFutureVersions - Whether this approval covers later Packages of the same Plugin.
|
|
1736
|
+
* @returns The exact Host activation or a failure message.
|
|
1737
|
+
*/
|
|
1738
|
+
async runHostHalf(agent, pluginId, packageId, mode, requestId, approveFutureVersions) {
|
|
1739
|
+
const plan = this.resolvePlan(agent, pluginId, packageId, mode, requestId === null);
|
|
1740
|
+
if (!plan.ok) return {
|
|
1741
|
+
ok: false,
|
|
1742
|
+
message: plan.response.message
|
|
1743
|
+
};
|
|
1744
|
+
let attempt;
|
|
1745
|
+
if (requestId !== null) {
|
|
1746
|
+
const pending = this.registry.peekRequest(requestId);
|
|
1747
|
+
if (pending === void 0 || pending.pluginId !== pluginId || pending.packageId !== packageId || pending.mode !== mode) return {
|
|
1748
|
+
ok: false,
|
|
1749
|
+
message: `run request "${requestId}" does not authorize ${pluginId}/${packageId}`
|
|
1750
|
+
};
|
|
1751
|
+
const latest = plan.plugin.latestRun;
|
|
1752
|
+
const expectedStatus = pending.requiresApproval ? "awaiting-approval" : "starting-host";
|
|
1753
|
+
if (latest === void 0 || latest.pluginRunId !== pending.pluginRunId || latest.status !== expectedStatus && !pending.requiresApproval && latest.status !== "client-pending") return {
|
|
1754
|
+
ok: false,
|
|
1755
|
+
message: `run request "${requestId}" no longer identifies the latest run of ${pluginId}`
|
|
1756
|
+
};
|
|
1757
|
+
attempt = latest;
|
|
1758
|
+
if (pending.requiresApproval) {
|
|
1759
|
+
plan.plugin.approvedClientPackages.add(packageId);
|
|
1760
|
+
if (approveFutureVersions) plan.plugin.clientVersionUpdatesApproved = true;
|
|
1761
|
+
}
|
|
1762
|
+
} else {
|
|
1763
|
+
const pending = this.registry.pendingRequestFor(pluginId);
|
|
1764
|
+
if (pending !== void 0) return {
|
|
1765
|
+
ok: false,
|
|
1766
|
+
message: `dynamic plugin "${pluginId}" has pending run request ${pending}`
|
|
1767
|
+
};
|
|
1768
|
+
const attached = plan.plugin.run?.packageId === packageId && plan.plugin.latestRun?.pluginRunId === plan.plugin.run.pluginRunId ? plan.plugin.latestRun : void 0;
|
|
1769
|
+
attempt = attached ?? this.createAttempt(plan);
|
|
1770
|
+
if (attached === void 0) {
|
|
1771
|
+
plan.plugin.nextPackageId = packageId;
|
|
1772
|
+
plan.plugin.latestRun = attempt;
|
|
1773
|
+
}
|
|
1774
|
+
if (plan.definition.clientCode !== void 0) plan.plugin.approvedClientPackages.add(packageId);
|
|
1775
|
+
}
|
|
1776
|
+
const attaching = attempt.pluginRunId === plan.plugin.run?.pluginRunId;
|
|
1777
|
+
if (!attaching) {
|
|
1778
|
+
attempt.status = "starting-host";
|
|
1779
|
+
if (attempt.host.status !== "absent") attempt.host = {
|
|
1780
|
+
status: "pending",
|
|
1781
|
+
waitingFor: []
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
const started = await this.activate(plan, requestId ?? void 0, attaching, attempt);
|
|
1785
|
+
if (!started.ok) this.failAttempt(plan.plugin, attempt, "host-load", started);
|
|
1786
|
+
return started;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Fetch Client code for the exact active run.
|
|
1790
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1791
|
+
* @param pluginId - Stable Plugin identity to read.
|
|
1792
|
+
* @param pluginRunId - Exact active run authorized to receive source.
|
|
1793
|
+
* @returns Client source and its Plugin, Package, and run identities.
|
|
1794
|
+
*/
|
|
1795
|
+
getClientCode(agent, pluginId, pluginRunId) {
|
|
1796
|
+
const plugin = this.owned(agent, pluginId);
|
|
1797
|
+
if (plugin === void 0) throw new Error(missingPluginMessage(pluginId));
|
|
1798
|
+
const run = plugin.run;
|
|
1799
|
+
if (run === void 0 || run.pluginRunId !== pluginRunId) throw new Error(`dynamic plugin "${pluginId}" is not running activation "${pluginRunId}"`);
|
|
1800
|
+
const definition = plugin.packages.get(run.packageId);
|
|
1801
|
+
if (definition?.clientCode === void 0) throw new Error(`package "${run.packageId}" has no Client half`);
|
|
1802
|
+
return {
|
|
1803
|
+
code: definition.clientCode,
|
|
1804
|
+
name: definition.name,
|
|
1805
|
+
pluginId,
|
|
1806
|
+
packageId: run.packageId,
|
|
1807
|
+
pluginRunId
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Resolve one model-driven Client activation request.
|
|
1812
|
+
* @param requestId - Request identity to settle once.
|
|
1813
|
+
* @param resolution - Browser refusal or exact Client activation result.
|
|
1814
|
+
* @returns Whether the still-pending request accepted this resolution.
|
|
1815
|
+
*/
|
|
1816
|
+
async resolveRequestRun(requestId, resolution) {
|
|
1817
|
+
const pending = this.registry.peekRequest(requestId);
|
|
1818
|
+
if (pending === void 0) return { accepted: false };
|
|
1819
|
+
const plugin = this.registry.get(pending.pluginId);
|
|
1820
|
+
if (resolution.ok && plugin?.run?.pluginRunId !== resolution.pluginRunId) return { accepted: false };
|
|
1821
|
+
if (!resolution.ok && resolution.pluginRunId !== void 0 && plugin?.run?.pluginRunId !== resolution.pluginRunId) return { accepted: false };
|
|
1822
|
+
this.registry.claimRequest(requestId);
|
|
1823
|
+
const settled = await this.settleActivation(plugin, resolution, requestId);
|
|
1824
|
+
this.announceResolved(requestId, resolution, pending.requiresApproval ? void 0 : "completed");
|
|
1825
|
+
this.steerRunOutcome(pending, settled);
|
|
1826
|
+
return { accepted: true };
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Settle a direct panel run after this page loaded or failed its Client half.
|
|
1830
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1831
|
+
* @param pluginId - Stable Plugin identity being settled.
|
|
1832
|
+
* @param resolution - Exact Client activation result from the acting page.
|
|
1833
|
+
* @returns The committed activation or its failure.
|
|
1834
|
+
*/
|
|
1835
|
+
async settleUserRun(agent, pluginId, resolution) {
|
|
1836
|
+
const plugin = this.owned(agent, pluginId);
|
|
1837
|
+
if (plugin === void 0) return {
|
|
1838
|
+
ok: false,
|
|
1839
|
+
reason: "plugin-missing",
|
|
1840
|
+
message: missingPluginMessage(pluginId)
|
|
1841
|
+
};
|
|
1842
|
+
const settled = await this.settleActivation(plugin, resolution);
|
|
1843
|
+
this.injectUserRunOutcome(agent, pluginId, settled);
|
|
1844
|
+
return settled;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Stop the active run while retaining every Package version.
|
|
1848
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1849
|
+
* @param pluginId - Stable Plugin identity to stop.
|
|
1850
|
+
* @returns Success or the reason no run was stopped.
|
|
1851
|
+
*/
|
|
1852
|
+
async stop(agent, pluginId) {
|
|
1853
|
+
const plugin = this.owned(agent, pluginId);
|
|
1854
|
+
if (plugin === void 0) return {
|
|
1855
|
+
ok: false,
|
|
1856
|
+
reason: "plugin-missing",
|
|
1857
|
+
message: missingPluginMessage(pluginId)
|
|
1858
|
+
};
|
|
1859
|
+
const pending = this.registry.pendingRequestFor(pluginId);
|
|
1860
|
+
if (plugin.run === void 0 && pending === void 0) return {
|
|
1861
|
+
ok: false,
|
|
1862
|
+
reason: "not-running",
|
|
1863
|
+
message: `dynamic plugin "${pluginId}" is not running`
|
|
1864
|
+
};
|
|
1865
|
+
if (pending !== void 0) this.cancelPending(pluginId, `dynamic plugin "${pluginId}" was stopped before approval`);
|
|
1866
|
+
if (plugin.run !== void 0) await this.retract(plugin);
|
|
1867
|
+
if (plugin.latestRun !== void 0) {
|
|
1868
|
+
plugin.latestRun.status = "stopped";
|
|
1869
|
+
if (plugin.latestRun.host.status !== "absent") plugin.latestRun.host = {
|
|
1870
|
+
status: "stopped",
|
|
1871
|
+
waitingFor: []
|
|
1872
|
+
};
|
|
1873
|
+
if (plugin.latestRun.client.status !== "absent") plugin.latestRun.client = {
|
|
1874
|
+
status: "stopped",
|
|
1875
|
+
waitingFor: []
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
return { ok: true };
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Stop a Plugin from the user panel and queue the resulting state change for the model's next step.
|
|
1882
|
+
* @param agent - Agent whose Session owns the Plugin and receives the context.
|
|
1883
|
+
* @param pluginId - Stable Plugin identity to stop.
|
|
1884
|
+
* @returns Success or the reason no run was stopped.
|
|
1885
|
+
*/
|
|
1886
|
+
async stopFromPanel(agent, pluginId) {
|
|
1887
|
+
const result = await this.stop(agent, pluginId);
|
|
1888
|
+
if (!result.ok) return result;
|
|
1889
|
+
const plugin = this.owned(agent, pluginId);
|
|
1890
|
+
this.injectUserContext(agent, `The user stopped Cordis Plugin ${pluginId}. Its Packages remain defined; currentPackageId is ${plugin?.currentPackageId ?? "none"}.`);
|
|
1891
|
+
return result;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Replace the Host mirror of the Client inspect provider directory.
|
|
1895
|
+
* @param providers - complete Client provider manifest.
|
|
1896
|
+
* @returns null after accepting the manifest.
|
|
1897
|
+
*/
|
|
1898
|
+
syncInspectManifest(providers) {
|
|
1899
|
+
this.inspectRegistry.syncClientManifest(providers);
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Claim one pending Client inspect query with its live result.
|
|
1904
|
+
* @param agent - Session that owns the query.
|
|
1905
|
+
* @param requestId - exact pending query identity.
|
|
1906
|
+
* @param resolution - provider result or structured refusal.
|
|
1907
|
+
* @returns whether this answer won the query.
|
|
1908
|
+
*/
|
|
1909
|
+
resolveInspectQuery(agent, requestId, resolution) {
|
|
1910
|
+
return this.inspectRegistry.resolveClientQuery(agent, requestId, resolution);
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Frame-wide inventory, grouped as one row per stable Plugin.
|
|
1914
|
+
* @returns Source-free metadata for every process-local Plugin.
|
|
1915
|
+
*/
|
|
1916
|
+
inventory() {
|
|
1917
|
+
return this.registry.all().map((plugin) => ({
|
|
1918
|
+
pluginId: plugin.pluginId,
|
|
1919
|
+
agentId: plugin.sessionId,
|
|
1920
|
+
packages: [...plugin.packages.values()].map((definition) => ({
|
|
1921
|
+
packageId: definition.packageId,
|
|
1922
|
+
name: definition.name,
|
|
1923
|
+
purpose: definition.purpose,
|
|
1924
|
+
hasHostHalf: definition.hostCode !== void 0,
|
|
1925
|
+
hasClientHalf: definition.clientCode !== void 0
|
|
1926
|
+
})),
|
|
1927
|
+
...plugin.currentPackageId === void 0 ? {} : { currentPackageId: plugin.currentPackageId },
|
|
1928
|
+
...plugin.nextPackageId === void 0 ? {} : { nextPackageId: plugin.nextPackageId },
|
|
1929
|
+
...plugin.run === void 0 ? {} : { activeRun: {
|
|
1930
|
+
pluginRunId: plugin.run.pluginRunId,
|
|
1931
|
+
packageId: plugin.run.packageId
|
|
1932
|
+
} },
|
|
1933
|
+
...plugin.latestRun === void 0 ? {} : { latestRun: cloneAttempt(plugin.latestRun) }
|
|
1934
|
+
}));
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Read one Session's Host-rich state for inspection and result rendering.
|
|
1938
|
+
* @param agent - Agent whose Session selects visible Plugins.
|
|
1939
|
+
* @returns Plugin versions, active runs, Host fibers, and render failures.
|
|
1940
|
+
*/
|
|
1941
|
+
snapshot(agent) {
|
|
1942
|
+
return this.registry.ofSession(agent.id).map((plugin) => ({
|
|
1943
|
+
pluginId: plugin.pluginId,
|
|
1944
|
+
...plugin.currentPackageId === void 0 ? {} : { currentPackageId: plugin.currentPackageId },
|
|
1945
|
+
...plugin.nextPackageId === void 0 ? {} : { nextPackageId: plugin.nextPackageId },
|
|
1946
|
+
packages: [...plugin.packages.values()].map((definition) => ({
|
|
1947
|
+
packageId: definition.packageId,
|
|
1948
|
+
name: definition.name,
|
|
1949
|
+
purpose: definition.purpose,
|
|
1950
|
+
hasHostHalf: definition.hostCode !== void 0,
|
|
1951
|
+
hasClientHalf: definition.clientCode !== void 0
|
|
1952
|
+
})),
|
|
1953
|
+
...plugin.run === void 0 ? {} : { activeRun: {
|
|
1954
|
+
pluginRunId: plugin.run.pluginRunId,
|
|
1955
|
+
packageId: plugin.run.packageId,
|
|
1956
|
+
...plugin.run.fiber === void 0 ? {} : { fiber: plugin.run.fiber },
|
|
1957
|
+
handlers: [...plugin.run.handlers.keys()],
|
|
1958
|
+
...plugin.run.renderFailure === void 0 ? {} : { renderFailure: plugin.run.renderFailure }
|
|
1959
|
+
} },
|
|
1960
|
+
...plugin.latestRun === void 0 ? {} : { latestRun: cloneAttempt(plugin.latestRun) }
|
|
1961
|
+
}));
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Read source-free context for an explicit `@pluginId` user gesture.
|
|
1965
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
1966
|
+
* @param pluginId - Stable Plugin identity referenced by the user.
|
|
1967
|
+
* @returns The preferred modification base, or undefined when unavailable.
|
|
1968
|
+
*/
|
|
1969
|
+
reference(agent, pluginId) {
|
|
1970
|
+
const plugin = this.owned(agent, pluginId);
|
|
1971
|
+
if (plugin === void 0) return void 0;
|
|
1972
|
+
const packageId = plugin.nextPackageId ?? plugin.currentPackageId ?? [...plugin.packages.keys()].at(-1);
|
|
1973
|
+
if (packageId === void 0) return void 0;
|
|
1974
|
+
const definition = plugin.packages.get(packageId);
|
|
1975
|
+
if (definition === void 0) return void 0;
|
|
1976
|
+
return {
|
|
1977
|
+
pluginId,
|
|
1978
|
+
packageId,
|
|
1979
|
+
name: definition.name,
|
|
1980
|
+
purpose: definition.purpose,
|
|
1981
|
+
...plugin.currentPackageId === void 0 ? {} : { currentPackageId: plugin.currentPackageId },
|
|
1982
|
+
...plugin.nextPackageId === void 0 ? {} : { nextPackageId: plugin.nextPackageId },
|
|
1983
|
+
...plugin.run === void 0 ? {} : { activeRun: {
|
|
1984
|
+
pluginRunId: plugin.run.pluginRunId,
|
|
1985
|
+
packageId: plugin.run.packageId
|
|
1986
|
+
} },
|
|
1987
|
+
...plugin.latestRun === void 0 ? {} : { latestRun: cloneAttempt(plugin.latestRun) }
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* List source-free Plugin summaries owned by one Session.
|
|
1992
|
+
* @param agent - Agent whose Session selects visible Plugins.
|
|
1993
|
+
* @returns one summary per Plugin in creation order.
|
|
1994
|
+
*/
|
|
1995
|
+
listPlugins(agent) {
|
|
1996
|
+
return this.registry.ofSession(agent.id).map((plugin) => this.inspectPlugin(agent, plugin.pluginId));
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Inspect one Plugin without returning Package source.
|
|
2000
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
2001
|
+
* @param pluginId - stable Plugin identity.
|
|
2002
|
+
* @returns version pointers, latest run, and all Package summaries.
|
|
2003
|
+
*/
|
|
2004
|
+
inspectPlugin(agent, pluginId) {
|
|
2005
|
+
const plugin = this.owned(agent, pluginId);
|
|
2006
|
+
if (plugin === void 0) throw new Error(missingPluginMessage(pluginId));
|
|
2007
|
+
const reference = this.reference(agent, pluginId);
|
|
2008
|
+
if (reference === void 0) throw new Error(`dynamic plugin "${pluginId}" has no package`);
|
|
2009
|
+
return {
|
|
2010
|
+
...reference,
|
|
2011
|
+
packages: [...plugin.packages.values()].map((definition) => ({
|
|
2012
|
+
packageId: definition.packageId,
|
|
2013
|
+
name: definition.name,
|
|
2014
|
+
purpose: definition.purpose,
|
|
2015
|
+
hasHostHalf: definition.hostCode !== void 0,
|
|
2016
|
+
hasClientHalf: definition.clientCode !== void 0
|
|
2017
|
+
}))
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Read one exact immutable Package and its Host and Client source.
|
|
2022
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
2023
|
+
* @param pluginId - Stable Plugin identity that owns the Package.
|
|
2024
|
+
* @param packageId - Exact immutable Package identity to inspect.
|
|
2025
|
+
* @returns Package metadata, source, and the Plugin's lifecycle pointers.
|
|
2026
|
+
*/
|
|
2027
|
+
inspectPackage(agent, pluginId, packageId) {
|
|
2028
|
+
const plugin = this.owned(agent, pluginId);
|
|
2029
|
+
if (plugin === void 0) throw new Error(missingPluginMessage(pluginId));
|
|
2030
|
+
const definition = plugin.packages.get(packageId);
|
|
2031
|
+
if (definition === void 0) throw new Error(`dynamic package "${packageId}" does not exist on plugin "${pluginId}"`);
|
|
2032
|
+
return {
|
|
2033
|
+
pluginId,
|
|
2034
|
+
packageId,
|
|
2035
|
+
name: definition.name,
|
|
2036
|
+
purpose: definition.purpose,
|
|
2037
|
+
code: {
|
|
2038
|
+
...definition.hostCode === void 0 ? {} : { host: definition.hostCode },
|
|
2039
|
+
...definition.clientCode === void 0 ? {} : { client: definition.clientCode }
|
|
2040
|
+
},
|
|
2041
|
+
...plugin.currentPackageId === void 0 ? {} : { currentPackageId: plugin.currentPackageId },
|
|
2042
|
+
...plugin.nextPackageId === void 0 ? {} : { nextPackageId: plugin.nextPackageId },
|
|
2043
|
+
...plugin.run === void 0 ? {} : { activeRun: {
|
|
2044
|
+
pluginRunId: plugin.run.pluginRunId,
|
|
2045
|
+
packageId: plugin.run.packageId
|
|
2046
|
+
} },
|
|
2047
|
+
...plugin.latestRun === void 0 ? {} : { latestRun: cloneAttempt(plugin.latestRun) }
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Record a post-load render failure for the exact active run.
|
|
2052
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
2053
|
+
* @param pluginId - Stable Plugin identity that rendered.
|
|
2054
|
+
* @param pluginRunId - Exact active run that produced the failure.
|
|
2055
|
+
* @param failure - Slot, message, and entry-retirement result.
|
|
2056
|
+
* @returns Null after recording or ignoring a stale report.
|
|
2057
|
+
*/
|
|
2058
|
+
async reportRenderFailure(agent, pluginId, pluginRunId, failure) {
|
|
2059
|
+
const plugin = this.owned(agent, pluginId);
|
|
2060
|
+
if (plugin?.run?.pluginRunId === pluginRunId) {
|
|
2061
|
+
const run = plugin.run;
|
|
2062
|
+
const definition = plugin.packages.get(plugin.run.packageId);
|
|
2063
|
+
const shouldSteer = run.renderFailure === void 0;
|
|
2064
|
+
run.renderFailure = failure;
|
|
2065
|
+
const attempt = plugin.latestRun;
|
|
2066
|
+
if (attempt?.pluginRunId === pluginRunId) {
|
|
2067
|
+
attempt.error = this.diagnostic(plugin, attempt, "client-render", failure);
|
|
2068
|
+
attempt.client = {
|
|
2069
|
+
status: "failed",
|
|
2070
|
+
waitingFor: attempt.client.waitingFor,
|
|
2071
|
+
error: failure.message
|
|
2072
|
+
};
|
|
2073
|
+
attempt.status = "failed";
|
|
2074
|
+
}
|
|
2075
|
+
if (definition !== void 0 && shouldSteer) this.steerRenderFailure(agent, plugin, definition, pluginRunId, failure);
|
|
2076
|
+
}
|
|
2077
|
+
return await Promise.resolve(null);
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Report a Client guard rejection that happened after the Package completed activation.
|
|
2081
|
+
* @param agent - Agent whose Session must own the Plugin.
|
|
2082
|
+
* @param pluginId - Stable Plugin identity whose Client code was rejected.
|
|
2083
|
+
* @param pluginRunId - Exact active run that produced the rejection.
|
|
2084
|
+
* @param failure - Original guard message and stack.
|
|
2085
|
+
* @returns Null after reporting or ignoring a stale/startup failure.
|
|
2086
|
+
*/
|
|
2087
|
+
async reportClientGuardFailure(agent, pluginId, pluginRunId, failure) {
|
|
2088
|
+
const plugin = this.owned(agent, pluginId);
|
|
2089
|
+
const run = plugin?.run;
|
|
2090
|
+
if (plugin !== void 0 && run?.pluginRunId === pluginRunId) this.steerGuardFailure(plugin, run, "Client", failure);
|
|
2091
|
+
return await Promise.resolve(null);
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Invoke an active Host method while rejecting stale Client runs.
|
|
2095
|
+
* @param pluginId - Stable Plugin identity that owns the method.
|
|
2096
|
+
* @param pluginRunId - Exact active run authorizing the call.
|
|
2097
|
+
* @param method - Registered Host handler name.
|
|
2098
|
+
* @param args - JSON argument delivered to the handler.
|
|
2099
|
+
* @returns The JSON result or a typed invocation failure.
|
|
2100
|
+
*/
|
|
2101
|
+
async invoke(pluginId, pluginRunId, method, args) {
|
|
2102
|
+
const plugin = this.registry.get(pluginId);
|
|
2103
|
+
if (plugin === void 0 || plugin.run === void 0) return {
|
|
2104
|
+
ok: false,
|
|
2105
|
+
code: "plugin-not-running",
|
|
2106
|
+
message: `dynamic plugin "${pluginId}" is not running`
|
|
2107
|
+
};
|
|
2108
|
+
const run = plugin.run;
|
|
2109
|
+
if (run.pluginRunId !== pluginRunId) return {
|
|
2110
|
+
ok: false,
|
|
2111
|
+
code: "stale-run",
|
|
2112
|
+
message: `activation "${pluginRunId}" is no longer active`
|
|
2113
|
+
};
|
|
2114
|
+
const handler = run.handlers.get(method);
|
|
2115
|
+
if (handler === void 0) return {
|
|
2116
|
+
ok: false,
|
|
2117
|
+
code: "method-not-found",
|
|
2118
|
+
message: `dynamic plugin "${pluginId}" registered no Host method "${method}"`
|
|
2119
|
+
};
|
|
2120
|
+
try {
|
|
2121
|
+
return {
|
|
2122
|
+
ok: true,
|
|
2123
|
+
value: await handler(args)
|
|
2124
|
+
};
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
const failure = errorDetails(error);
|
|
2127
|
+
this.steerHostHandlerFailure(plugin, run, method, failure);
|
|
2128
|
+
return {
|
|
2129
|
+
ok: false,
|
|
2130
|
+
code: "handler-error",
|
|
2131
|
+
...failure
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
resolvePlan(agent, pluginId, packageId, mode, allowActiveAttach = false) {
|
|
2136
|
+
const plugin = this.owned(agent, pluginId);
|
|
2137
|
+
if (plugin === void 0) return {
|
|
2138
|
+
ok: false,
|
|
2139
|
+
response: {
|
|
2140
|
+
ok: false,
|
|
2141
|
+
reason: "plugin-missing",
|
|
2142
|
+
message: missingPluginMessage(pluginId)
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
const definition = plugin.packages.get(packageId);
|
|
2146
|
+
if (definition === void 0) return {
|
|
2147
|
+
ok: false,
|
|
2148
|
+
response: {
|
|
2149
|
+
ok: false,
|
|
2150
|
+
reason: "package-missing",
|
|
2151
|
+
message: `plugin "${pluginId}" has no package "${packageId}"`
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
const current = plugin.currentPackageId;
|
|
2155
|
+
if (mode === "update" && (current === void 0 || current === packageId)) return {
|
|
2156
|
+
ok: false,
|
|
2157
|
+
response: {
|
|
2158
|
+
ok: false,
|
|
2159
|
+
reason: "invalid-mode",
|
|
2160
|
+
message: current === void 0 ? `plugin "${pluginId}" has no successful version yet; start "${packageId}" with mode "run"` : `package "${packageId}" is already current; use mode "run"`
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
if (mode === "run" && current !== void 0 && current !== packageId) return {
|
|
2164
|
+
ok: false,
|
|
2165
|
+
response: {
|
|
2166
|
+
ok: false,
|
|
2167
|
+
reason: "invalid-mode",
|
|
2168
|
+
message: `package "${packageId}" differs from current "${current}"; use mode "update"`
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
if (!allowActiveAttach && this.starting.has(pluginId)) return {
|
|
2172
|
+
ok: false,
|
|
2173
|
+
response: {
|
|
2174
|
+
ok: false,
|
|
2175
|
+
reason: "transition-in-flight",
|
|
2176
|
+
message: `plugin "${pluginId}" is already starting`
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
return {
|
|
2180
|
+
ok: true,
|
|
2181
|
+
plugin,
|
|
2182
|
+
definition,
|
|
2183
|
+
mode
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
activate(plan, requestId, allowActiveAttach, attempt) {
|
|
2187
|
+
const inFlight = this.starting.get(plan.plugin.pluginId);
|
|
2188
|
+
if (inFlight !== void 0) return inFlight;
|
|
2189
|
+
const starting = this.startFresh(plan, requestId, allowActiveAttach, attempt);
|
|
2190
|
+
this.starting.set(plan.plugin.pluginId, starting);
|
|
2191
|
+
return starting.finally(() => {
|
|
2192
|
+
this.starting.delete(plan.plugin.pluginId);
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
async startFresh(plan, requestId, allowActiveAttach, attempt) {
|
|
2196
|
+
const { plugin, definition, mode } = plan;
|
|
2197
|
+
if (allowActiveAttach && plugin.run?.packageId === definition.packageId && plugin.run.pluginRunId === attempt.pluginRunId) return {
|
|
2198
|
+
ok: true,
|
|
2199
|
+
pluginId: plugin.pluginId,
|
|
2200
|
+
packageId: definition.packageId,
|
|
2201
|
+
pluginRunId: plugin.run.pluginRunId,
|
|
2202
|
+
waitingFor: missingFor(this.ctx, plugin.run),
|
|
2203
|
+
startedHere: false
|
|
2204
|
+
};
|
|
2205
|
+
if (plugin.run !== void 0) await this.retract(plugin);
|
|
2206
|
+
if (mode === "update" || plugin.currentPackageId === void 0) plugin.nextPackageId = definition.packageId;
|
|
2207
|
+
const run = {
|
|
2208
|
+
pluginRunId: attempt.pluginRunId,
|
|
2209
|
+
packageId: definition.packageId,
|
|
2210
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
2211
|
+
handlerDisposers: [],
|
|
2212
|
+
reportedRuntimeErrors: /* @__PURE__ */ new Set(),
|
|
2213
|
+
...requestId === void 0 ? {} : { startedForRequest: requestId }
|
|
2214
|
+
};
|
|
2215
|
+
if (definition.hostCode !== void 0) {
|
|
2216
|
+
const failure = await this.startHost(plugin, definition.hostCode, run);
|
|
2217
|
+
if (failure !== void 0) return {
|
|
2218
|
+
ok: false,
|
|
2219
|
+
...failure
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
plugin.run = run;
|
|
2223
|
+
this.ctx.emit("cordis/dynamic-package", {
|
|
2224
|
+
pluginId: plugin.pluginId,
|
|
2225
|
+
packageId: definition.packageId,
|
|
2226
|
+
pluginRunId: run.pluginRunId,
|
|
2227
|
+
name: definition.name
|
|
2228
|
+
});
|
|
2229
|
+
attempt.host = {
|
|
2230
|
+
status: run.fiber === void 0 ? "absent" : missingFor(this.ctx, run).length === 0 ? "running" : "waiting",
|
|
2231
|
+
waitingFor: missingFor(this.ctx, run)
|
|
2232
|
+
};
|
|
2233
|
+
if (definition.clientCode === void 0) this.commitActivation(plugin, run);
|
|
2234
|
+
else {
|
|
2235
|
+
attempt.status = "client-pending";
|
|
2236
|
+
attempt.client = {
|
|
2237
|
+
status: "pending",
|
|
2238
|
+
waitingFor: []
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
return {
|
|
2242
|
+
ok: true,
|
|
2243
|
+
pluginId: plugin.pluginId,
|
|
2244
|
+
packageId: definition.packageId,
|
|
2245
|
+
pluginRunId: run.pluginRunId,
|
|
2246
|
+
waitingFor: missingFor(this.ctx, run),
|
|
2247
|
+
startedHere: true
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
async startHost(plugin, hostCode, run) {
|
|
2251
|
+
const handle = (method, fn) => {
|
|
2252
|
+
const normalized = normalizeHandler(method, fn);
|
|
2253
|
+
run.handlers.set(normalized.method, normalized.handler);
|
|
2254
|
+
const dispose = () => {
|
|
2255
|
+
if (run.handlers.get(normalized.method) === normalized.handler) run.handlers.delete(normalized.method);
|
|
2256
|
+
};
|
|
2257
|
+
run.handlerDisposers.push(dispose);
|
|
2258
|
+
return dispose;
|
|
2259
|
+
};
|
|
2260
|
+
try {
|
|
2261
|
+
const evaluated = await evaluateHostCode(createSandbox(plugin.pluginId, { handle }), hostCode, plugin.pluginId, this.resolved.vmTimeoutMs);
|
|
2262
|
+
if (!isPlugin(evaluated)) throw new Error(evaluated === void 0 ? "the Host half returned `undefined` — did you forget `return`?" : "the Host half must return a Plugin function or an object with apply(ctx)");
|
|
2263
|
+
run.fiber = await startHostHalf(this.requireGroup(), evaluated, (error) => {
|
|
2264
|
+
this.steerGuardFailure(plugin, run, "Host", errorDetails(error));
|
|
2265
|
+
});
|
|
2266
|
+
return;
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
for (const dispose of run.handlerDisposers.splice(0)) dispose();
|
|
2269
|
+
return errorDetails(error);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
async settleActivation(plugin, resolution, requestId) {
|
|
2273
|
+
if (plugin === void 0) return {
|
|
2274
|
+
ok: false,
|
|
2275
|
+
reason: "plugin-missing",
|
|
2276
|
+
message: "the dynamic plugin was removed during activation"
|
|
2277
|
+
};
|
|
2278
|
+
const attempt = plugin.latestRun;
|
|
2279
|
+
if (!resolution.ok) {
|
|
2280
|
+
if (resolution.reason === "rejected") {
|
|
2281
|
+
if (attempt !== void 0) {
|
|
2282
|
+
attempt.status = "rejected";
|
|
2283
|
+
attempt.error = this.diagnostic(plugin, attempt, "approval", resolution.message ?? "the run request was declined");
|
|
2284
|
+
attempt.client = {
|
|
2285
|
+
status: "stopped",
|
|
2286
|
+
waitingFor: []
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
return {
|
|
2290
|
+
ok: false,
|
|
2291
|
+
reason: "rejected",
|
|
2292
|
+
message: resolution.message ?? "the run request was declined"
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
const run = plugin.run;
|
|
2296
|
+
if (run !== void 0 && resolution.pluginRunId === run.pluginRunId && (requestId === void 0 || run.startedForRequest === requestId) && resolution.startedHere !== false) await this.retract(plugin);
|
|
2297
|
+
if (attempt !== void 0 && (resolution.pluginRunId === void 0 || attempt.pluginRunId === resolution.pluginRunId)) this.failAttempt(plugin, attempt, resolution.reason === "host-half-failed" ? "host-apply" : "client-apply", {
|
|
2298
|
+
message: resolution.message ?? resolution.reason,
|
|
2299
|
+
...resolution.stack === void 0 ? {} : { stack: resolution.stack }
|
|
2300
|
+
});
|
|
2301
|
+
return {
|
|
2302
|
+
ok: false,
|
|
2303
|
+
reason: resolution.reason,
|
|
2304
|
+
message: resolution.message ?? resolution.reason,
|
|
2305
|
+
...resolution.stack === void 0 ? {} : { stack: resolution.stack }
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
const run = plugin.run;
|
|
2309
|
+
if (run === void 0 || run.pluginRunId !== resolution.pluginRunId) return {
|
|
2310
|
+
ok: false,
|
|
2311
|
+
reason: "client-half-failed",
|
|
2312
|
+
message: `activation "${resolution.pluginRunId}" is no longer active`
|
|
2313
|
+
};
|
|
2314
|
+
if (attempt !== void 0 && attempt.pluginRunId === run.pluginRunId) attempt.client = {
|
|
2315
|
+
status: resolution.waitingFor === void 0 || resolution.waitingFor.length === 0 ? "running" : "waiting",
|
|
2316
|
+
waitingFor: resolution.waitingFor ?? []
|
|
2317
|
+
};
|
|
2318
|
+
this.commitActivation(plugin, run);
|
|
2319
|
+
return {
|
|
2320
|
+
...this.runResponse(plugin, {
|
|
2321
|
+
ok: true,
|
|
2322
|
+
pluginId: plugin.pluginId,
|
|
2323
|
+
packageId: run.packageId,
|
|
2324
|
+
pluginRunId: run.pluginRunId,
|
|
2325
|
+
waitingFor: missingFor(this.ctx, run),
|
|
2326
|
+
startedHere: false
|
|
2327
|
+
}),
|
|
2328
|
+
...resolution.waitingFor === void 0 ? {} : { clientWaitingFor: resolution.waitingFor }
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
commitActivation(plugin, run) {
|
|
2332
|
+
plugin.currentPackageId = run.packageId;
|
|
2333
|
+
delete plugin.nextPackageId;
|
|
2334
|
+
delete run.startedForRequest;
|
|
2335
|
+
const attempt = plugin.latestRun;
|
|
2336
|
+
if (attempt?.pluginRunId === run.pluginRunId) {
|
|
2337
|
+
attempt.status = attempt.host.status === "waiting" || attempt.client.status === "waiting" ? "waiting" : "running";
|
|
2338
|
+
delete attempt.approvalRequestId;
|
|
2339
|
+
delete attempt.requiresApproval;
|
|
2340
|
+
delete attempt.error;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
runResponse(plugin, started) {
|
|
2344
|
+
return {
|
|
2345
|
+
ok: true,
|
|
2346
|
+
status: "running",
|
|
2347
|
+
pluginId: plugin.pluginId,
|
|
2348
|
+
packageId: started.packageId,
|
|
2349
|
+
pluginRunId: started.pluginRunId,
|
|
2350
|
+
waitingFor: started.waitingFor,
|
|
2351
|
+
currentPackageId: started.packageId,
|
|
2352
|
+
mode: plugin.latestRun?.pluginRunId === started.pluginRunId ? plugin.latestRun.mode : "run"
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
announceResolved(requestId, resolution, override) {
|
|
2356
|
+
const outcome = override ?? (resolution.ok ? "approved" : resolution.reason === "rejected" ? "rejected" : "failed");
|
|
2357
|
+
this.ctx.emit("cordis/request-run-resolved", {
|
|
2358
|
+
requestId,
|
|
2359
|
+
outcome
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
steerRunOutcome(pending, settled) {
|
|
2363
|
+
const agent = this.rootCtx.get("agents")?.get(pending.agentId);
|
|
2364
|
+
if (agent === void 0) return;
|
|
2365
|
+
const plugin = this.registry.get(pending.pluginId);
|
|
2366
|
+
const identity = `${pending.pluginId}/${pending.packageId} (${pending.pluginRunId})`;
|
|
2367
|
+
let text;
|
|
2368
|
+
if (settled.ok) text = `Cordis ${pending.mode} ${identity} completed successfully. currentPackageId is ${settled.currentPackageId ?? pending.packageId}. Continue using the running Plugin.`;
|
|
2369
|
+
else if (settled.reason === "rejected") text = `The user rejected Cordis ${pending.mode} ${identity}. Do not request the same activation again unless the user asks.`;
|
|
2370
|
+
else {
|
|
2371
|
+
const returnedStatus = pending.requiresApproval ? "awaiting-approval" : "starting";
|
|
2372
|
+
text = `Cordis ${pending.mode} ${identity} failed after cordis_run returned ${returnedStatus}: ${settled.reason}\n${formatErrorDetails(settled)}\ncurrentPackageId: ${plugin?.currentPackageId ?? "none"}\nnextPackageId: ${plugin?.nextPackageId ?? pending.packageId}\nInspect the failed Package, correct it on the same Plugin when needed, and retry the activation autonomously.`;
|
|
2373
|
+
}
|
|
2374
|
+
agent.steer(createUserMessage({
|
|
2375
|
+
content: [{
|
|
2376
|
+
type: "text",
|
|
2377
|
+
text
|
|
2378
|
+
}],
|
|
2379
|
+
source: {
|
|
2380
|
+
kind: "plugin",
|
|
2381
|
+
plugin: "cordis-host-runner"
|
|
2382
|
+
}
|
|
2383
|
+
}));
|
|
2384
|
+
}
|
|
2385
|
+
steerRenderFailure(agent, plugin, definition, pluginRunId, failure) {
|
|
2386
|
+
agent.steer(createUserMessage({
|
|
2387
|
+
content: [{
|
|
2388
|
+
type: "text",
|
|
2389
|
+
text: `Cordis Client UI ${plugin.pluginId}/${definition.packageId} (${pluginRunId}) failed while rendering Slot "${failure.slot}" after activation.\n${formatErrorDetails(failure)}\nentryAbdicated: ${failure.abdicated}\nInspect the failed Package, fix the Client code by defining a new Package on the same Plugin, and activate that Package autonomously with cordis_run mode:"update".`
|
|
2390
|
+
}],
|
|
2391
|
+
source: {
|
|
2392
|
+
kind: "plugin",
|
|
2393
|
+
plugin: "cordis-host-runner"
|
|
2394
|
+
}
|
|
2395
|
+
}));
|
|
2396
|
+
}
|
|
2397
|
+
steerHostHandlerFailure(plugin, run, method, failure) {
|
|
2398
|
+
const reportKey = `Host\u0000handler\u0000${method}\u0000${failure.message}`;
|
|
2399
|
+
if (!this.claimRuntimeFailure(plugin, run, reportKey)) return;
|
|
2400
|
+
const agent = this.rootCtx.get("agents")?.get(plugin.sessionId);
|
|
2401
|
+
if (agent === void 0) return;
|
|
2402
|
+
agent.steer(createUserMessage({
|
|
2403
|
+
content: [{
|
|
2404
|
+
type: "text",
|
|
2405
|
+
text: `Cordis Host handler ${plugin.pluginId}/${run.packageId} (${run.pluginRunId}) failed when the Client called host.call(${JSON.stringify(method)}).\n${formatErrorDetails(failure)}\nThe Plugin remains running. Inspect this Package, correct the Host code on the same Plugin, and activate the new Package autonomously with cordis_run mode:"update". If the handler needs a Service, either declare that Service in the returned Plugin inject list or read it with ctx.get(name) and handle undefined.`
|
|
2406
|
+
}],
|
|
2407
|
+
source: {
|
|
2408
|
+
kind: "plugin",
|
|
2409
|
+
plugin: "cordis-host-runner"
|
|
2410
|
+
}
|
|
2411
|
+
}));
|
|
2412
|
+
}
|
|
2413
|
+
steerGuardFailure(plugin, run, platform, failure) {
|
|
2414
|
+
const reportKey = `${platform}\u0000guard\u0000${failure.message}`;
|
|
2415
|
+
if (!this.claimRuntimeFailure(plugin, run, reportKey)) return;
|
|
2416
|
+
const agent = this.rootCtx.get("agents")?.get(plugin.sessionId);
|
|
2417
|
+
if (agent === void 0) return;
|
|
2418
|
+
agent.steer(createUserMessage({
|
|
2419
|
+
content: [{
|
|
2420
|
+
type: "text",
|
|
2421
|
+
text: `Cordis ${platform} guard rejected runtime code in ${plugin.pluginId}/${run.packageId} (${run.pluginRunId}) after activation.\n${formatErrorDetails(failure)}\nThe Plugin remains running. Inspect this Package, define a corrected Package on the same Plugin, and activate it autonomously with cordis_run mode:"update".`
|
|
2422
|
+
}],
|
|
2423
|
+
source: {
|
|
2424
|
+
kind: "plugin",
|
|
2425
|
+
plugin: "cordis-host-runner"
|
|
2426
|
+
}
|
|
2427
|
+
}));
|
|
2428
|
+
}
|
|
2429
|
+
claimRuntimeFailure(plugin, run, key) {
|
|
2430
|
+
const attempt = plugin.latestRun;
|
|
2431
|
+
if (plugin.run !== run || attempt?.pluginRunId !== run.pluginRunId || attempt.status !== "running" && attempt.status !== "waiting") return false;
|
|
2432
|
+
if (run.reportedRuntimeErrors.has(key)) return false;
|
|
2433
|
+
run.reportedRuntimeErrors.add(key);
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
injectUserRunOutcome(agent, pluginId, settled) {
|
|
2437
|
+
const plugin = this.owned(agent, pluginId);
|
|
2438
|
+
let text;
|
|
2439
|
+
if (settled.ok) text = `The user manually ran Cordis Plugin ${pluginId}, Package ${settled.packageId}, as ${settled.pluginRunId}. The activation succeeded; currentPackageId is ${settled.currentPackageId}.`;
|
|
2440
|
+
else {
|
|
2441
|
+
const attempt = plugin?.latestRun;
|
|
2442
|
+
text = `The user manually ran Cordis Plugin ${pluginId}${attempt === void 0 ? "" : `, Package ${attempt.packageId}, as ${attempt.pluginRunId}`}, but it failed: ${settled.reason}\n${formatErrorDetails(settled)}\ncurrentPackageId: ${plugin?.currentPackageId ?? "none"}\nnextPackageId: ${plugin?.nextPackageId ?? "none"}`;
|
|
2443
|
+
}
|
|
2444
|
+
this.injectUserContext(agent, text);
|
|
2445
|
+
}
|
|
2446
|
+
injectUserContext(agent, text) {
|
|
2447
|
+
if (this.rootCtx.get("agents")?.get(agent.id) !== agent) return;
|
|
2448
|
+
agent.inject(createUserMessage({
|
|
2449
|
+
content: [{
|
|
2450
|
+
type: "text",
|
|
2451
|
+
text
|
|
2452
|
+
}],
|
|
2453
|
+
source: {
|
|
2454
|
+
kind: "plugin",
|
|
2455
|
+
plugin: "cordis-host-runner"
|
|
2456
|
+
}
|
|
2457
|
+
}));
|
|
2458
|
+
}
|
|
2459
|
+
cancelPending(pluginId, message) {
|
|
2460
|
+
const requestId = this.registry.pendingRequestFor(pluginId);
|
|
2461
|
+
if (requestId === void 0) return;
|
|
2462
|
+
const pending = this.registry.claimRequest(requestId);
|
|
2463
|
+
if (pending === void 0) return;
|
|
2464
|
+
const plugin = this.registry.get(pluginId);
|
|
2465
|
+
if (plugin?.latestRun?.pluginRunId === pending.pluginRunId) {
|
|
2466
|
+
plugin.latestRun.status = "cancelled";
|
|
2467
|
+
plugin.latestRun.error = this.diagnostic(plugin, plugin.latestRun, "approval", message);
|
|
2468
|
+
delete plugin.latestRun.approvalRequestId;
|
|
2469
|
+
delete plugin.latestRun.requiresApproval;
|
|
2470
|
+
}
|
|
2471
|
+
this.announceResolved(requestId, {
|
|
2472
|
+
ok: false,
|
|
2473
|
+
reason: "rejected"
|
|
2474
|
+
}, "cancelled");
|
|
2475
|
+
}
|
|
2476
|
+
createAttempt(plan) {
|
|
2477
|
+
return {
|
|
2478
|
+
pluginRunId: CordisDynamicPluginRunId(this.registry.mintPluginRunId()),
|
|
2479
|
+
packageId: plan.definition.packageId,
|
|
2480
|
+
mode: plan.mode,
|
|
2481
|
+
status: "starting-host",
|
|
2482
|
+
host: {
|
|
2483
|
+
status: plan.definition.hostCode === void 0 ? "absent" : "pending",
|
|
2484
|
+
waitingFor: []
|
|
2485
|
+
},
|
|
2486
|
+
client: {
|
|
2487
|
+
status: plan.definition.clientCode === void 0 ? "absent" : "pending",
|
|
2488
|
+
waitingFor: []
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
failAttempt(plugin, attempt, phase, failure) {
|
|
2493
|
+
attempt.status = "failed";
|
|
2494
|
+
attempt.error = this.diagnostic(plugin, attempt, phase, failure);
|
|
2495
|
+
if (phase.startsWith("host")) attempt.host = {
|
|
2496
|
+
status: "failed",
|
|
2497
|
+
waitingFor: [],
|
|
2498
|
+
error: failure.message
|
|
2499
|
+
};
|
|
2500
|
+
else attempt.client = {
|
|
2501
|
+
status: "failed",
|
|
2502
|
+
waitingFor: [],
|
|
2503
|
+
error: failure.message
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
diagnostic(plugin, attempt, phase, failure) {
|
|
2507
|
+
return {
|
|
2508
|
+
phase,
|
|
2509
|
+
...typeof failure === "string" ? { message: failure } : failure,
|
|
2510
|
+
pluginId: plugin.pluginId,
|
|
2511
|
+
packageId: attempt.packageId,
|
|
2512
|
+
pluginRunId: attempt.pluginRunId
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
async retract(plugin) {
|
|
2516
|
+
const run = plugin.run;
|
|
2517
|
+
if (run === void 0) return;
|
|
2518
|
+
delete plugin.run;
|
|
2519
|
+
for (const dispose of run.handlerDisposers.splice(0)) dispose();
|
|
2520
|
+
if (run.fiber !== void 0) await run.fiber.dispose();
|
|
2521
|
+
this.ctx.emit("cordis/dynamic-retract", {
|
|
2522
|
+
pluginId: plugin.pluginId,
|
|
2523
|
+
packageId: run.packageId,
|
|
2524
|
+
pluginRunId: run.pluginRunId
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
owned(agent, pluginId) {
|
|
2528
|
+
const plugin = this.registry.get(pluginId);
|
|
2529
|
+
return plugin?.sessionId === agent.id ? plugin : void 0;
|
|
2530
|
+
}
|
|
2531
|
+
requireGroup() {
|
|
2532
|
+
this.group ??= this.rootCtx.plugin({
|
|
2533
|
+
name: "cordis-dynamic",
|
|
2534
|
+
apply: () => {}
|
|
2535
|
+
});
|
|
2536
|
+
return this.group;
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
})();
|
|
2540
|
+
function missingFor(ctx, run) {
|
|
2541
|
+
return run.fiber === void 0 ? [] : missingServices(ctx, run.fiber);
|
|
2542
|
+
}
|
|
2543
|
+
function missingPluginMessage(id) {
|
|
2544
|
+
return `no dynamic plugin "${id}" in this process — it may have been removed or lost on DSH restart`;
|
|
2545
|
+
}
|
|
2546
|
+
function errorDetails(error) {
|
|
2547
|
+
if (typeof error !== "object" || error === null) return { message: String(error) };
|
|
2548
|
+
const message = "message" in error && typeof error.message === "string" ? error.message : Object.prototype.toString.call(error);
|
|
2549
|
+
const stack = "stack" in error && typeof error.stack === "string" ? error.stack : void 0;
|
|
2550
|
+
return {
|
|
2551
|
+
message,
|
|
2552
|
+
...stack === void 0 ? {} : { stack }
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
function formatErrorDetails(failure) {
|
|
2556
|
+
return `message: ${failure.message}` + (failure.stack === void 0 ? "" : `\nstack:\n${failure.stack}`);
|
|
2557
|
+
}
|
|
2558
|
+
function cloneAttempt(attempt) {
|
|
2559
|
+
return {
|
|
2560
|
+
...attempt,
|
|
2561
|
+
host: {
|
|
2562
|
+
...attempt.host,
|
|
2563
|
+
waitingFor: [...attempt.host.waitingFor]
|
|
2564
|
+
},
|
|
2565
|
+
client: {
|
|
2566
|
+
...attempt.client,
|
|
2567
|
+
waitingFor: [...attempt.client.waitingFor]
|
|
2568
|
+
},
|
|
2569
|
+
...attempt.error === void 0 ? {} : { error: { ...attempt.error } }
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
//#endregion
|
|
2573
|
+
export { ApprovalRequestId, CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId, CordisInspectRegistryService, DynamicCordisRunnerService, DynamicCordisRunnerService as default, HOST_BUILTIN_INSPECTION };
|