@zhin.js/runtime 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config-composer.d.ts +228 -5
- package/lib/config-composer.js +45 -34
- package/lib/generation-assets.d.ts +1 -1
- package/lib/generation-assets.js +9 -5
- package/lib/host-config-schema.json +92 -3
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/missing-tool-package.d.ts +5 -0
- package/lib/missing-tool-package.js +21 -0
- package/lib/native-development-runtime.js +27 -3
- package/lib/plugin-config-schema.d.ts +6 -0
- package/lib/plugin-config-schema.js +44 -0
- package/lib/plugin-scope-assembler.d.ts +4 -1
- package/lib/plugin-scope-assembler.js +20 -7
- package/lib/root-runtime.d.ts +5 -0
- package/lib/root-runtime.js +71 -18
- package/lib/slot-generation-preparer.js +5 -9
- package/lib/subtree-generation-preparer.d.ts +1 -2
- package/lib/subtree-generation-preparer.js +18 -3
- package/lib/topology-generation-preparer.d.ts +1 -2
- package/lib/topology-generation-preparer.js +1 -1
- package/package.json +16 -16
package/lib/config-composer.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RuntimeConfigDocument, PluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { type ErrorObject } from 'ajv/dist/2020.js';
|
|
2
3
|
import type { ProjectGraph } from './project-graph.js';
|
|
4
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
3
5
|
export declare const HOST_CONFIG_SCHEMA: {
|
|
4
6
|
$schema: string;
|
|
5
7
|
type: string;
|
|
@@ -204,6 +206,20 @@ export declare const HOST_CONFIG_SCHEMA: {
|
|
|
204
206
|
description: string;
|
|
205
207
|
"x-descriptionZh": string;
|
|
206
208
|
};
|
|
209
|
+
ttlMs: {
|
|
210
|
+
type: string;
|
|
211
|
+
minimum: number;
|
|
212
|
+
default: number;
|
|
213
|
+
description: string;
|
|
214
|
+
"x-descriptionZh": string;
|
|
215
|
+
};
|
|
216
|
+
maxQueuedTurns: {
|
|
217
|
+
type: string;
|
|
218
|
+
minimum: number;
|
|
219
|
+
default: number;
|
|
220
|
+
description: string;
|
|
221
|
+
"x-descriptionZh": string;
|
|
222
|
+
};
|
|
207
223
|
};
|
|
208
224
|
};
|
|
209
225
|
execSecurity: {
|
|
@@ -218,25 +234,224 @@ export declare const HOST_CONFIG_SCHEMA: {
|
|
|
218
234
|
description: string;
|
|
219
235
|
"x-descriptionZh": string;
|
|
220
236
|
};
|
|
221
|
-
|
|
237
|
+
network: {
|
|
238
|
+
type: string;
|
|
239
|
+
additionalProperties: boolean;
|
|
240
|
+
description: string;
|
|
241
|
+
"x-descriptionZh": string;
|
|
242
|
+
properties: {
|
|
243
|
+
enabled: {
|
|
244
|
+
type: string;
|
|
245
|
+
default: boolean;
|
|
246
|
+
description: string;
|
|
247
|
+
"x-descriptionZh": string;
|
|
248
|
+
};
|
|
249
|
+
allowedDomains: {
|
|
250
|
+
type: string;
|
|
251
|
+
items: {
|
|
252
|
+
type: string;
|
|
253
|
+
};
|
|
254
|
+
description: string;
|
|
255
|
+
"x-descriptionZh": string;
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
masterShell: {
|
|
222
260
|
type: string;
|
|
223
261
|
enum: string[];
|
|
262
|
+
default: string;
|
|
224
263
|
description: string;
|
|
225
264
|
"x-descriptionZh": string;
|
|
226
265
|
};
|
|
227
|
-
|
|
266
|
+
toolBudget: {
|
|
267
|
+
type: string;
|
|
268
|
+
description: string;
|
|
269
|
+
"x-descriptionZh": string;
|
|
270
|
+
properties: {
|
|
271
|
+
extendBy: {
|
|
272
|
+
type: string;
|
|
273
|
+
minimum: number;
|
|
274
|
+
default: number;
|
|
275
|
+
description: string;
|
|
276
|
+
"x-descriptionZh": string;
|
|
277
|
+
};
|
|
278
|
+
progressWindow: {
|
|
279
|
+
type: string;
|
|
280
|
+
minimum: number;
|
|
281
|
+
default: number;
|
|
282
|
+
description: string;
|
|
283
|
+
"x-descriptionZh": string;
|
|
284
|
+
};
|
|
285
|
+
maxTotalIterations: {
|
|
286
|
+
type: string;
|
|
287
|
+
minimum: number;
|
|
288
|
+
default: number;
|
|
289
|
+
description: string;
|
|
290
|
+
"x-descriptionZh": string;
|
|
291
|
+
};
|
|
292
|
+
maxSubagentTotalIterations: {
|
|
293
|
+
type: string;
|
|
294
|
+
minimum: number;
|
|
295
|
+
default: number;
|
|
296
|
+
description: string;
|
|
297
|
+
"x-descriptionZh": string;
|
|
298
|
+
};
|
|
299
|
+
maxWallClockMs: {
|
|
300
|
+
type: string;
|
|
301
|
+
minimum: number;
|
|
302
|
+
default: number;
|
|
303
|
+
description: string;
|
|
304
|
+
"x-descriptionZh": string;
|
|
305
|
+
};
|
|
306
|
+
maxTotalTokens: {
|
|
307
|
+
type: string;
|
|
308
|
+
minimum: number;
|
|
309
|
+
default: number;
|
|
310
|
+
description: string;
|
|
311
|
+
"x-descriptionZh": string;
|
|
312
|
+
};
|
|
313
|
+
failureStreakLimit: {
|
|
314
|
+
type: string;
|
|
315
|
+
minimum: number;
|
|
316
|
+
default: number;
|
|
317
|
+
description: string;
|
|
318
|
+
"x-descriptionZh": string;
|
|
319
|
+
};
|
|
320
|
+
repeatLimit: {
|
|
321
|
+
type: string;
|
|
322
|
+
minimum: number;
|
|
323
|
+
default: number;
|
|
324
|
+
description: string;
|
|
325
|
+
"x-descriptionZh": string;
|
|
326
|
+
};
|
|
327
|
+
autoContinue: {
|
|
328
|
+
type: string;
|
|
329
|
+
enum: string[];
|
|
330
|
+
default: string;
|
|
331
|
+
description: string;
|
|
332
|
+
"x-descriptionZh": string;
|
|
333
|
+
};
|
|
334
|
+
maxContinuations: {
|
|
335
|
+
type: string;
|
|
336
|
+
minimum: number;
|
|
337
|
+
default: number;
|
|
338
|
+
description: string;
|
|
339
|
+
"x-descriptionZh": string;
|
|
340
|
+
};
|
|
341
|
+
progressUpdates: {
|
|
342
|
+
type: string;
|
|
343
|
+
enum: string[];
|
|
344
|
+
default: string;
|
|
345
|
+
description: string;
|
|
346
|
+
"x-descriptionZh": string;
|
|
347
|
+
};
|
|
348
|
+
progressMinIntervalMs: {
|
|
349
|
+
type: string;
|
|
350
|
+
minimum: number;
|
|
351
|
+
default: number;
|
|
352
|
+
description: string;
|
|
353
|
+
"x-descriptionZh": string;
|
|
354
|
+
};
|
|
355
|
+
progressMaxUpdates: {
|
|
356
|
+
type: string;
|
|
357
|
+
minimum: number;
|
|
358
|
+
default: number;
|
|
359
|
+
description: string;
|
|
360
|
+
"x-descriptionZh": string;
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
injectionGuard: {
|
|
365
|
+
type: string;
|
|
366
|
+
description: string;
|
|
367
|
+
"x-descriptionZh": string;
|
|
368
|
+
properties: {
|
|
369
|
+
confirmHighRisk: {
|
|
370
|
+
type: string;
|
|
371
|
+
enum: string[];
|
|
372
|
+
default: string;
|
|
373
|
+
description: string;
|
|
374
|
+
"x-descriptionZh": string;
|
|
375
|
+
};
|
|
376
|
+
outboundCheck: {
|
|
377
|
+
type: string;
|
|
378
|
+
enum: string[];
|
|
379
|
+
default: string;
|
|
380
|
+
description: string;
|
|
381
|
+
"x-descriptionZh": string;
|
|
382
|
+
};
|
|
383
|
+
trustedOutputTools: {
|
|
384
|
+
type: string;
|
|
385
|
+
items: {
|
|
386
|
+
type: string;
|
|
387
|
+
};
|
|
388
|
+
default: never[];
|
|
389
|
+
description: string;
|
|
390
|
+
"x-descriptionZh": string;
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
usageQuota: {
|
|
395
|
+
type: string;
|
|
396
|
+
description: string;
|
|
397
|
+
"x-descriptionZh": string;
|
|
398
|
+
properties: {
|
|
399
|
+
windowMs: {
|
|
400
|
+
type: string;
|
|
401
|
+
minimum: number;
|
|
402
|
+
default: number;
|
|
403
|
+
description: string;
|
|
404
|
+
"x-descriptionZh": string;
|
|
405
|
+
};
|
|
406
|
+
perUserTokens: {
|
|
407
|
+
type: string;
|
|
408
|
+
minimum: number;
|
|
409
|
+
default: number;
|
|
410
|
+
description: string;
|
|
411
|
+
"x-descriptionZh": string;
|
|
412
|
+
};
|
|
413
|
+
perGroupTokens: {
|
|
414
|
+
type: string;
|
|
415
|
+
minimum: number;
|
|
416
|
+
default: number;
|
|
417
|
+
description: string;
|
|
418
|
+
"x-descriptionZh": string;
|
|
419
|
+
};
|
|
420
|
+
perGroupRequestsPerMinute: {
|
|
421
|
+
type: string;
|
|
422
|
+
minimum: number;
|
|
423
|
+
default: number;
|
|
424
|
+
description: string;
|
|
425
|
+
"x-descriptionZh": string;
|
|
426
|
+
};
|
|
427
|
+
exemptOwner: {
|
|
428
|
+
type: string;
|
|
429
|
+
default: boolean;
|
|
430
|
+
description: string;
|
|
431
|
+
"x-descriptionZh": string;
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
subagentTurnMode: {
|
|
228
436
|
type: string;
|
|
229
437
|
enum: string[];
|
|
438
|
+
default: string;
|
|
230
439
|
description: string;
|
|
231
440
|
"x-descriptionZh": string;
|
|
232
441
|
};
|
|
233
|
-
|
|
442
|
+
execApprovalMode: {
|
|
234
443
|
type: string;
|
|
235
444
|
enum: string[];
|
|
236
445
|
description: string;
|
|
237
446
|
"x-descriptionZh": string;
|
|
238
447
|
};
|
|
239
|
-
|
|
448
|
+
subagentExecApprovalMode: {
|
|
449
|
+
type: string;
|
|
450
|
+
enum: string[];
|
|
451
|
+
description: string;
|
|
452
|
+
"x-descriptionZh": string;
|
|
453
|
+
};
|
|
454
|
+
workerExecApprovalMode: {
|
|
240
455
|
type: string;
|
|
241
456
|
enum: string[];
|
|
242
457
|
description: string;
|
|
@@ -639,6 +854,7 @@ export interface ComposedConfig {
|
|
|
639
854
|
readonly effectiveSchema: JsonSchema;
|
|
640
855
|
readonly document: RuntimeConfigDocument;
|
|
641
856
|
readonly views: ReadonlyMap<PluginId, unknown>;
|
|
857
|
+
readonly ownSchemas: ReadonlyMap<PluginId, JsonSchema>;
|
|
642
858
|
}
|
|
643
859
|
export declare class ConfigSchemaCollisionError extends Error {
|
|
644
860
|
readonly plugin: PluginId;
|
|
@@ -655,8 +871,15 @@ export declare class ConfigValidationError extends Error {
|
|
|
655
871
|
}
|
|
656
872
|
export declare class ConfigComposer {
|
|
657
873
|
private readonly inactivePluginInstanceKeys;
|
|
658
|
-
|
|
874
|
+
private readonly modules?;
|
|
875
|
+
constructor(inactivePluginInstanceKeys?: readonly string[], modules?: Pick<ModuleRuntime, "load"> | undefined);
|
|
659
876
|
compose(graph: ProjectGraph, input?: RuntimeConfigDocument,
|
|
660
877
|
/** Source config file name, annotated on ConfigValidationError. */
|
|
661
878
|
source?: string): Promise<ComposedConfig>;
|
|
662
879
|
}
|
|
880
|
+
/** Reuses Runtime validation/defaults without mutating the caller's configuration. */
|
|
881
|
+
export declare function parsePluginConfigSchema(schema: JsonSchema, input: unknown): Readonly<{
|
|
882
|
+
value: unknown;
|
|
883
|
+
errors: readonly ErrorObject[];
|
|
884
|
+
}>;
|
|
885
|
+
export declare function validatePluginConfigSchema(schema: JsonSchema, input: unknown): readonly ErrorObject[];
|
package/lib/config-composer.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
2
|
import hostConfigSchema from './host-config-schema.json' with { type: 'json' };
|
|
3
|
+
import { readPluginConfigSchema } from './plugin-config-schema.js';
|
|
5
4
|
function deepFreeze(value) {
|
|
6
5
|
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
7
6
|
for (const nested of Object.values(value))
|
|
@@ -37,8 +36,10 @@ export class ConfigValidationError extends Error {
|
|
|
37
36
|
}
|
|
38
37
|
export class ConfigComposer {
|
|
39
38
|
inactivePluginInstanceKeys;
|
|
40
|
-
|
|
39
|
+
modules;
|
|
40
|
+
constructor(inactivePluginInstanceKeys = [], modules) {
|
|
41
41
|
this.inactivePluginInstanceKeys = inactivePluginInstanceKeys;
|
|
42
|
+
this.modules = modules;
|
|
42
43
|
}
|
|
43
44
|
async compose(graph, input = {},
|
|
44
45
|
/** Source config file name, annotated on ConfigValidationError. */
|
|
@@ -46,11 +47,11 @@ export class ConfigComposer {
|
|
|
46
47
|
// Effective schemas include child namespaces for whole-tree validation;
|
|
47
48
|
// ownSchemas retain each package's private configuration contract.
|
|
48
49
|
const ownSchemas = new Map();
|
|
49
|
-
const rootOwn = await readOwnSchema(graph.root);
|
|
50
|
+
const rootOwn = await readOwnSchema(graph.root, this.modules);
|
|
50
51
|
ownSchemas.set(graph.root.id, rootOwn);
|
|
51
52
|
const childSchemas = await Promise.all(graph.root.children.map(async (child) => [
|
|
52
53
|
child.instanceKey,
|
|
53
|
-
await composeNode(child, ownSchemas),
|
|
54
|
+
await composeNode(child, ownSchemas, this.modules),
|
|
54
55
|
]));
|
|
55
56
|
// Host-level keys (`http`, `database`, `ai`, `mcp`, `a2a`, `speech`,
|
|
56
57
|
// `htmlRenderer`, `assistant`, `log_level`) are consumed by CLI Root installers /
|
|
@@ -74,17 +75,7 @@ export class ConfigComposer {
|
|
|
74
75
|
},
|
|
75
76
|
});
|
|
76
77
|
const document = structuredClone(input);
|
|
77
|
-
const ajv =
|
|
78
|
-
allErrors: true,
|
|
79
|
-
useDefaults: true,
|
|
80
|
-
strict: true,
|
|
81
|
-
// 当前内置 schema 无需 union(log_level 的 ['string','number'] 是 type 数组,
|
|
82
|
-
// 不触发 allowUnionTypes);保留此项是面向未来插件 schema 可能出现的
|
|
83
|
-
// anyOf/oneOf 标量 union,避免届时 Ajv strict 模式直接报错。
|
|
84
|
-
allowUnionTypes: true,
|
|
85
|
-
});
|
|
86
|
-
ajv.addKeyword({ keyword: 'x-descriptionZh', schemaType: 'string' });
|
|
87
|
-
ajv.addKeyword({ keyword: 'x-keyPlaceholder', schemaType: 'string' });
|
|
78
|
+
const ajv = configurationValidator();
|
|
88
79
|
const validate = ajv.compile(effectiveSchema);
|
|
89
80
|
if (!validate(document)) {
|
|
90
81
|
throw new ConfigValidationError(formatErrors(validate.errors ?? []), source);
|
|
@@ -98,6 +89,7 @@ export class ConfigComposer {
|
|
|
98
89
|
effectiveSchema,
|
|
99
90
|
document: Object.freeze(document),
|
|
100
91
|
views,
|
|
92
|
+
ownSchemas,
|
|
101
93
|
});
|
|
102
94
|
}
|
|
103
95
|
}
|
|
@@ -153,8 +145,8 @@ function injectFrameworkRoleSchema(properties) {
|
|
|
153
145
|
}),
|
|
154
146
|
});
|
|
155
147
|
}
|
|
156
|
-
async function composeNode(node, ownSchemas) {
|
|
157
|
-
const own = await readOwnSchema(node);
|
|
148
|
+
async function composeNode(node, ownSchemas, modules) {
|
|
149
|
+
const own = await readOwnSchema(node, modules);
|
|
158
150
|
ownSchemas.set(node.id, own);
|
|
159
151
|
const properties = { ...schemaProperties(own) };
|
|
160
152
|
// Adapter plugins (schemas declaring array-typed `endpoints`) get
|
|
@@ -167,24 +159,14 @@ async function composeNode(node, ownSchemas) {
|
|
|
167
159
|
if (Object.hasOwn(properties, child.instanceKey)) {
|
|
168
160
|
throw new ConfigSchemaCollisionError(node.id, child.instanceKey);
|
|
169
161
|
}
|
|
170
|
-
properties[child.instanceKey] = withDefault(await composeNode(child, ownSchemas));
|
|
162
|
+
properties[child.instanceKey] = withDefault(await composeNode(child, ownSchemas, modules));
|
|
171
163
|
}
|
|
172
164
|
return Object.freeze({ ...own, properties });
|
|
173
165
|
}
|
|
174
|
-
async function readOwnSchema(node) {
|
|
175
|
-
const file =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
value = JSON.parse(await readFile(file, 'utf8'));
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
182
|
-
value = { type: 'object', additionalProperties: false, properties: {} };
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
166
|
+
async function readOwnSchema(node, modules) {
|
|
167
|
+
const file = `${node.package.name} config`;
|
|
168
|
+
const value = await readPluginConfigSchema(node.package, modules)
|
|
169
|
+
?? { type: 'object', additionalProperties: false, properties: {} };
|
|
188
170
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
189
171
|
throw new TypeError(`${file} must contain a JSON Schema object`);
|
|
190
172
|
}
|
|
@@ -204,7 +186,7 @@ async function readOwnSchema(node) {
|
|
|
204
186
|
if (Object.hasOwn(properties, 'commandNamespace')) {
|
|
205
187
|
throw new ConfigSchemaCollisionError(node.id, 'commandNamespace');
|
|
206
188
|
}
|
|
207
|
-
return
|
|
189
|
+
return deepFreeze({
|
|
208
190
|
$id: `urn:zhin:plugin-config:${encodeURIComponent(String(node.id))}`,
|
|
209
191
|
type: 'object',
|
|
210
192
|
additionalProperties: false,
|
|
@@ -275,3 +257,32 @@ function formatErrors(errors) {
|
|
|
275
257
|
return base;
|
|
276
258
|
});
|
|
277
259
|
}
|
|
260
|
+
/** Reuses Runtime validation/defaults without mutating the caller's configuration. */
|
|
261
|
+
export function parsePluginConfigSchema(schema, input) {
|
|
262
|
+
const validate = configurationValidator().compile({
|
|
263
|
+
type: 'object',
|
|
264
|
+
...(schema.$defs ? { $defs: schema.$defs } : {}),
|
|
265
|
+
properties: { candidate: schema },
|
|
266
|
+
});
|
|
267
|
+
const candidate = { candidate: structuredClone(input === undefined ? schema.default ?? {} : input) };
|
|
268
|
+
const valid = validate(candidate);
|
|
269
|
+
return Object.freeze({ value: candidate.candidate, errors: valid ? [] : structuredClone(validate.errors ?? []) });
|
|
270
|
+
}
|
|
271
|
+
export function validatePluginConfigSchema(schema, input) {
|
|
272
|
+
return parsePluginConfigSchema(schema, input).errors;
|
|
273
|
+
}
|
|
274
|
+
function configurationValidator() {
|
|
275
|
+
const ajv = new Ajv2020({
|
|
276
|
+
allErrors: true,
|
|
277
|
+
useDefaults: true,
|
|
278
|
+
strict: true,
|
|
279
|
+
// 当前内置 schema 无需 union(log_level 的 ['string','number'] 是 type 数组,
|
|
280
|
+
// 不触发 allowUnionTypes);保留此项是面向未来插件 schema 可能出现的
|
|
281
|
+
// anyOf/oneOf 标量 union,避免届时 Ajv strict 模式直接报错。
|
|
282
|
+
allowUnionTypes: true,
|
|
283
|
+
});
|
|
284
|
+
ajv.addKeyword({ keyword: 'x-descriptionZh', schemaType: 'string' });
|
|
285
|
+
ajv.addKeyword({ keyword: 'x-keyPlaceholder', schemaType: 'string' });
|
|
286
|
+
ajv.addKeyword({ keyword: 'x-zhin-config', schemaType: 'object' });
|
|
287
|
+
return ajv;
|
|
288
|
+
}
|
|
@@ -4,6 +4,6 @@ export declare class GenerationAssets {
|
|
|
4
4
|
private constructor();
|
|
5
5
|
static create(scopeDisposers: Iterable<readonly [PluginId, Dispose]>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
6
6
|
replaceProjections(features: Iterable<FeatureId>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
7
|
-
replaceScopes(scopeOrder: readonly PluginId[], replacements: ReadonlyMap<PluginId, Dispose>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
7
|
+
replaceScopes(scopeOrder: readonly PluginId[], replacements: ReadonlyMap<PluginId, Dispose>, projectedFeatures: Iterable<FeatureId>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
8
8
|
dispose(): Promise<void>;
|
|
9
9
|
}
|
package/lib/generation-assets.js
CHANGED
|
@@ -48,7 +48,7 @@ export class GenerationAssets {
|
|
|
48
48
|
}
|
|
49
49
|
return new GenerationAssets([...this.#scopeLifetimes.keys()], this.#scopeLifetimes, lifetimes);
|
|
50
50
|
}
|
|
51
|
-
replaceScopes(scopeOrder, replacements, projectionDisposers) {
|
|
51
|
+
replaceScopes(scopeOrder, replacements, projectedFeatures, projectionDisposers) {
|
|
52
52
|
const owners = new Set(scopeOrder);
|
|
53
53
|
for (const owner of replacements.keys()) {
|
|
54
54
|
if (!owners.has(owner))
|
|
@@ -64,10 +64,14 @@ export class GenerationAssets {
|
|
|
64
64
|
throw new Error(`Cannot retain unknown Plugin Scope: ${owner}`);
|
|
65
65
|
lifetimes.set(owner, lifetime);
|
|
66
66
|
}
|
|
67
|
-
const projectionLifetimes = new Map(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const projectionLifetimes = new Map(this.#projectionLifetimes);
|
|
68
|
+
for (const feature of projectedFeatures) {
|
|
69
|
+
const dispose = projectionDisposers.get(feature);
|
|
70
|
+
if (dispose)
|
|
71
|
+
projectionLifetimes.set(feature, new SharedLifetime(dispose));
|
|
72
|
+
else
|
|
73
|
+
projectionLifetimes.delete(feature);
|
|
74
|
+
}
|
|
71
75
|
return new GenerationAssets(scopeOrder, lifetimes, projectionLifetimes);
|
|
72
76
|
}
|
|
73
77
|
dispose() {
|
|
@@ -121,8 +121,22 @@
|
|
|
121
121
|
"groupMode": {
|
|
122
122
|
"type": "string",
|
|
123
123
|
"enum": ["supersede", "fifo"],
|
|
124
|
-
"description": "
|
|
125
|
-
"x-descriptionZh": "
|
|
124
|
+
"description": "supersede (default): a sender's new message replaces their own unanswered turn; fifo: every message waits in arrival order. Other members' messages always queue.",
|
|
125
|
+
"x-descriptionZh": "supersede(默认):同一发起人的新消息会替换他自己尚未回复的回合;fifo:全部按到达顺序排队。其他成员的消息总是排队。"
|
|
126
|
+
},
|
|
127
|
+
"ttlMs": {
|
|
128
|
+
"type": "integer",
|
|
129
|
+
"minimum": 0,
|
|
130
|
+
"default": 180000,
|
|
131
|
+
"description": "A queued turn that waited longer than this is dropped with a short notice. 0 disables expiry.",
|
|
132
|
+
"x-descriptionZh": "排队超过该时长的回合会被丢弃并简短提示;0 表示不过期。"
|
|
133
|
+
},
|
|
134
|
+
"maxQueuedTurns": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"minimum": 0,
|
|
137
|
+
"default": 5,
|
|
138
|
+
"description": "Turns allowed to wait behind the running turn of one conversation; further messages get a short busy reply.",
|
|
139
|
+
"x-descriptionZh": "同一会话中允许排在当前回合之后的回合数;超出时回复一句“正在忙”。"
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
},
|
|
@@ -138,10 +152,85 @@
|
|
|
138
152
|
"description": "Command allowlist preset used outside full mode.",
|
|
139
153
|
"x-descriptionZh": "非 full 模式使用的命令白名单预设。"
|
|
140
154
|
},
|
|
155
|
+
"network": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"description": "Network access for IM turns (web_fetch / web_search and networked commands). Off by default; enabled allows HTTPS egress, optionally limited to allowedDomains. When enabled is unset, execPreset: network still enables it for compatibility.",
|
|
159
|
+
"x-descriptionZh": "IM 回合的出网权限(web_fetch / web_search 与联网命令)。默认关闭;enabled 开启 HTTPS 出网,allowedDomains 非空时只允许这些域名。未设置 enabled 时兼容旧配置:execPreset: network 视为开启。",
|
|
160
|
+
"properties": {
|
|
161
|
+
"enabled": {
|
|
162
|
+
"type": "boolean",
|
|
163
|
+
"default": false,
|
|
164
|
+
"description": "Allow HTTPS egress for IM turns.",
|
|
165
|
+
"x-descriptionZh": "是否允许 IM 回合 HTTPS 出网。"
|
|
166
|
+
},
|
|
167
|
+
"allowedDomains": {
|
|
168
|
+
"type": "array",
|
|
169
|
+
"items": { "type": "string" },
|
|
170
|
+
"description": "When non-empty, only these domains (and their subdomains) are reachable.",
|
|
171
|
+
"x-descriptionZh": "非空时只允许访问这些域名(含子域名)。"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"masterShell": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"enum": ["off", "isolated"],
|
|
178
|
+
"default": "off",
|
|
179
|
+
"description": "Shell access for the master in IM private chats. off: no bash in IM turns. isolated: master DMs run bash inside a Docker container (workspace writable, no network, development allowlist, approval for anything else); fails closed when Docker is unavailable.",
|
|
180
|
+
"x-descriptionZh": "IM 私聊中 master 的 Shell 权限。off:IM 会话不提供 bash;isolated:master 私聊在 Docker 容器内执行 bash(工作区可写、无网络、development 白名单、其余命令需审批),宿主无 Docker 时直接失败。"
|
|
181
|
+
},
|
|
182
|
+
"toolBudget": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"description": "Progress-aware tool-call budget. maxIterations is a soft budget extended while the turn makes progress; hard caps always apply.",
|
|
185
|
+
"x-descriptionZh": "进度感知的工具调用预算。maxIterations 为软上限,回合有进展时自动延长;硬上限始终生效。",
|
|
186
|
+
"properties": {
|
|
187
|
+
"extendBy": { "type": "integer", "minimum": 0, "default": 10, "description": "Steps added per extension when progressing; 0 disables.", "x-descriptionZh": "有进展时每次延长的步数;0 关闭。" },
|
|
188
|
+
"progressWindow": { "type": "integer", "minimum": 0, "default": 5, "description": "Progress must have happened within this many recent steps.", "x-descriptionZh": "最近多少步内有进展才延长。" },
|
|
189
|
+
"maxTotalIterations": { "type": "integer", "minimum": 0, "default": 40, "description": "Hard step cap per turn segment.", "x-descriptionZh": "每段回合的硬步数上限。" },
|
|
190
|
+
"maxSubagentTotalIterations": { "type": "integer", "minimum": 0, "default": 30, "description": "Hard step cap for an in-turn sub-agent.", "x-descriptionZh": "回合内子 Agent 的硬步数上限。" },
|
|
191
|
+
"maxWallClockMs": { "type": "integer", "minimum": 0, "default": 600000, "description": "Wall-clock cap for the whole turn in ms; 0 disables.", "x-descriptionZh": "整个回合的时间上限(毫秒);0 关闭。" },
|
|
192
|
+
"maxTotalTokens": { "type": "integer", "minimum": 0, "default": 0, "description": "Token cap for the whole turn; 0 disables.", "x-descriptionZh": "整个回合的 Token 上限;0 关闭。" },
|
|
193
|
+
"failureStreakLimit": { "type": "integer", "minimum": 0, "default": 3, "description": "Consecutive tool failures that stop extensions.", "x-descriptionZh": "连续失败多少次后不再延长。" },
|
|
194
|
+
"repeatLimit": { "type": "integer", "minimum": 0, "default": 2, "description": "Consecutive identical repeated calls that stop extensions.", "x-descriptionZh": "连续重复相同调用多少次后不再延长。" },
|
|
195
|
+
"autoContinue": { "type": "string", "enum": ["off", "master_dm", "on"], "default": "master_dm", "description": "Auto-continue a progressing turn that hit its budget: off, only in the master's private chat, or always. Continuations reuse the same turn (same policy and approvals).", "x-descriptionZh": "回合有进展但预算用尽时自动续跑:off 关闭;master_dm 仅主人私聊;on 总是。续跑沿用同一回合的策略与审批。" },
|
|
196
|
+
"maxContinuations": { "type": "integer", "minimum": 0, "default": 2, "description": "Maximum automatic continuations per turn.", "x-descriptionZh": "每回合最多自动续跑次数。" },
|
|
197
|
+
"progressUpdates": { "type": "string", "enum": ["off", "private", "all"], "default": "private", "description": "Brief natural-language progress updates during long turns: off, private chats only, or all chats (groups opt in).", "x-descriptionZh": "长回合中的简短进度更新:off 关闭;private 仅私聊;all 包括群聊(群聊需显式开启)。" },
|
|
198
|
+
"progressMinIntervalMs": { "type": "integer", "minimum": 0, "default": 45000, "description": "Minimum time between progress updates in ms.", "x-descriptionZh": "两次进度更新的最小间隔(毫秒)。" },
|
|
199
|
+
"progressMaxUpdates": { "type": "integer", "minimum": 0, "default": 3, "description": "Maximum progress updates per turn; 0 disables.", "x-descriptionZh": "每回合最多进度更新次数;0 关闭。" }
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"injectionGuard": {
|
|
203
|
+
"type": "object",
|
|
204
|
+
"description": "Prompt-injection defences. After lower-trust content (tool results, web pages, files, sub-agent reports, background messages) enters a turn, high-risk actions (shell, messaging other conversations, config changes, scheduling) need approval; replies are checked for system-prompt, secret and other-session memory leaks.",
|
|
205
|
+
"x-descriptionZh": "注入防护。外部内容(工具结果、网页、文件、子 agent 报告、群聊背景消息)进入回合后,高危动作(执行命令、向其他会话发消息、改配置、定时任务)必须审批;出站回复检查系统提示、密钥与其他会话记忆泄露。",
|
|
206
|
+
"properties": {
|
|
207
|
+
"confirmHighRisk": { "type": "string", "enum": ["external_content", "off"], "default": "external_content", "description": "external_content: high-risk actions after external content always ask for approval (denied in unattended turns). off: disabled.", "x-descriptionZh": "external_content:读取外部内容后的高危动作一律审批(无人值守回合直接拒绝);off:关闭。" },
|
|
208
|
+
"outboundCheck": { "type": "string", "enum": ["redact", "off"], "default": "redact", "description": "redact: mask leaked system-prompt lines, secrets and other sessions' memory in replies and outbound tool messages. off: disabled.", "x-descriptionZh": "redact:在回复和外发工具消息中屏蔽泄露的系统提示、密钥和其他会话记忆;off:关闭。" },
|
|
209
|
+
"trustedOutputTools": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Tools whose results do not count as external content.", "x-descriptionZh": "结果不算外部内容的工具。" }
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"usageQuota": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"description": "Per-user and per-group token quotas and a per-group rate limit. All limits are off (0) by default; the owner is exempt by default. Counters are kept in memory.",
|
|
215
|
+
"x-descriptionZh": "按用户 / 按群的 token 配额与群速率限制。默认全部关闭(0),主人默认不受限;计数保存在内存中。",
|
|
216
|
+
"properties": {
|
|
217
|
+
"windowMs": { "type": "integer", "minimum": 60000, "default": 86400000, "description": "Quota window in ms.", "x-descriptionZh": "配额周期(毫秒)。" },
|
|
218
|
+
"perUserTokens": { "type": "integer", "minimum": 0, "default": 0, "description": "Tokens one user may use per window; 0 = unlimited.", "x-descriptionZh": "每个用户每周期可用的 token;0 不限。" },
|
|
219
|
+
"perGroupTokens": { "type": "integer", "minimum": 0, "default": 0, "description": "Tokens one group may use per window; 0 = unlimited.", "x-descriptionZh": "每个群每周期可用的 token;0 不限。" },
|
|
220
|
+
"perGroupRequestsPerMinute": { "type": "integer", "minimum": 0, "default": 0, "description": "Turns one group may start per minute; 0 = unlimited.", "x-descriptionZh": "每个群每分钟可发起的回合数;0 不限。" },
|
|
221
|
+
"exemptOwner": { "type": "boolean", "default": true, "description": "The owner is never limited.", "x-descriptionZh": "主人不受限制。" }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"subagentTurnMode": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"enum": ["off", "sync"],
|
|
227
|
+
"default": "off",
|
|
228
|
+
"description": "Sub-agents in IM turns. off: no spawn_task. sync: spawn_task runs a sub-agent inside the current turn with at most the parent's permissions (never wider; no bash unless the parent could run it) and approvals routed to the same conversation.",
|
|
229
|
+
"x-descriptionZh": "IM 回合中的子 Agent。off:不提供 spawn_task;sync:spawn_task 在当前回合内同步运行子 Agent,权限不超过父回合(父回合不能运行 bash 时子 Agent 也没有),审批发往同一会话。"
|
|
230
|
+
},
|
|
141
231
|
"execApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for main Agent commands.", "x-descriptionZh": "主 Agent 命令的审批策略。" },
|
|
142
232
|
"subagentExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for sub-Agent commands.", "x-descriptionZh": "子 Agent 命令的审批策略。" },
|
|
143
233
|
"workerExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for worker commands.", "x-descriptionZh": "Worker 命令的审批策略。" },
|
|
144
|
-
"taskExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for task commands.", "x-descriptionZh": "Task 命令的审批策略。" },
|
|
145
234
|
"toolExecution": {
|
|
146
235
|
"type": "string",
|
|
147
236
|
"enum": ["parallel", "sequential", "tiered"],
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const TOOL_PACKAGE_INSTALL_HINT = "\u7F3A\u5C11\u4F9D\u8D56 @zhin.js/tool\uFF08tools/ \u4E0B\u7684 defineAgentTool \u9700\u8981\u5B83\uFF09\u3002\u8BF7\u8FD0\u884C: pnpm add @zhin.js/tool";
|
|
2
|
+
/** True only for ERR_MODULE_NOT_FOUND naming the `@zhin.js/tool` package root. */
|
|
3
|
+
export declare function isToolPackageMissing(error: unknown): boolean;
|
|
4
|
+
/** Return an error that carries the exact install command when `@zhin.js/tool` is missing. */
|
|
5
|
+
export declare function withToolPackageInstallHint(error: unknown): unknown;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const TOOL_PACKAGE = '@zhin.js/tool';
|
|
2
|
+
const TOOL_ROOT_SPECIFIER = /['"]@zhin\.js\/tool['"]/u;
|
|
3
|
+
export const TOOL_PACKAGE_INSTALL_HINT = `缺少依赖 ${TOOL_PACKAGE}(tools/ 下的 defineAgentTool 需要它)。请运行: pnpm add ${TOOL_PACKAGE}`;
|
|
4
|
+
/** True only for ERR_MODULE_NOT_FOUND naming the `@zhin.js/tool` package root. */
|
|
5
|
+
export function isToolPackageMissing(error) {
|
|
6
|
+
if (!error || typeof error !== 'object')
|
|
7
|
+
return false;
|
|
8
|
+
const { code, message } = error;
|
|
9
|
+
return code === 'ERR_MODULE_NOT_FOUND'
|
|
10
|
+
&& typeof message === 'string'
|
|
11
|
+
&& TOOL_ROOT_SPECIFIER.test(message);
|
|
12
|
+
}
|
|
13
|
+
/** Return an error that carries the exact install command when `@zhin.js/tool` is missing. */
|
|
14
|
+
export function withToolPackageInstallHint(error) {
|
|
15
|
+
if (!isToolPackageMissing(error))
|
|
16
|
+
return error;
|
|
17
|
+
const original = error;
|
|
18
|
+
const augmented = new Error(`${TOOL_PACKAGE_INSTALL_HINT}\n原始错误: ${original.message}`, { cause: error });
|
|
19
|
+
augmented.code = original.code;
|
|
20
|
+
return augmented;
|
|
21
|
+
}
|
|
@@ -3,6 +3,7 @@ import { readdirSync, statSync, watch as watchDirectory, } from 'node:fs';
|
|
|
3
3
|
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { ModuleDependencyIndex, } from './module-dependency-index.js';
|
|
6
|
+
import { withToolPackageInstallHint } from './missing-tool-package.js';
|
|
6
7
|
import { ensureTypeScriptSpecifierRemap } from './typescript-specifier-remap.js';
|
|
7
8
|
const ignoredDirectories = new Set([
|
|
8
9
|
'.git', '.zhin', 'coverage', 'data', 'dist', 'lib', 'node_modules',
|
|
@@ -27,6 +28,7 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
27
28
|
#pendingDependencies;
|
|
28
29
|
#watchRoots;
|
|
29
30
|
#revision = 0;
|
|
31
|
+
#entryRevisions = new Map();
|
|
30
32
|
#closed = false;
|
|
31
33
|
constructor(options) {
|
|
32
34
|
ensureTypeScriptSpecifierRemap();
|
|
@@ -43,16 +45,23 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
43
45
|
const analysis = this.#pendingDependencies?.get(normalized)
|
|
44
46
|
?? await this.#dependencies.analyze(normalized);
|
|
45
47
|
const url = pathToFileURL(normalized);
|
|
46
|
-
|
|
48
|
+
if (!this.#entryRevisions.has(normalized))
|
|
49
|
+
this.#entryRevisions.set(normalized, this.#revision);
|
|
50
|
+
url.searchParams.set('zhin-generation', String(this.#entryRevisions.get(normalized)));
|
|
47
51
|
let loaded;
|
|
48
52
|
try {
|
|
49
53
|
loaded = await import(url.href);
|
|
50
54
|
}
|
|
51
55
|
catch (error) {
|
|
52
56
|
if (!normalized.endsWith('.tsx') || !isUnsupportedTsxError(error) || !this.#tsxLoader) {
|
|
53
|
-
throw error;
|
|
57
|
+
throw withToolPackageInstallHint(error);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
loaded = await this.#tsxLoader.load(url.href, pathToFileURL(`${this.#projectRoot}${sep}`).href);
|
|
61
|
+
}
|
|
62
|
+
catch (tsxError) {
|
|
63
|
+
throw withToolPackageInstallHint(tsxError);
|
|
54
64
|
}
|
|
55
|
-
loaded = await this.#tsxLoader.load(url.href, pathToFileURL(`${this.#projectRoot}${sep}`).href);
|
|
56
65
|
}
|
|
57
66
|
if (this.#pendingDependencies)
|
|
58
67
|
this.#pendingDependencies.set(normalized, analysis);
|
|
@@ -61,8 +70,12 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
61
70
|
return loaded;
|
|
62
71
|
}
|
|
63
72
|
invalidate(source) {
|
|
73
|
+
const impacted = this.#dependencies.affectedSources(source);
|
|
64
74
|
this.#dependencies.invalidate(source);
|
|
65
75
|
this.#revision += 1;
|
|
76
|
+
for (const entry of new Set([normalizeSourcePath(source), ...impacted])) {
|
|
77
|
+
this.#entryRevisions.set(entry, this.#revision);
|
|
78
|
+
}
|
|
66
79
|
}
|
|
67
80
|
affectedSources(source) {
|
|
68
81
|
return this.#dependencies.affectedSources(source);
|
|
@@ -86,6 +99,11 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
86
99
|
// complete project-local import closure.
|
|
87
100
|
if (this.affectedSources(normalized).length > 1)
|
|
88
101
|
return false;
|
|
102
|
+
// An already loaded ESM entry has the same revisioned import closure as a
|
|
103
|
+
// capability entry. Its manifest may name plugin.ts or any other filename;
|
|
104
|
+
// Runtime ownership, rather than directory spelling, chooses its reload.
|
|
105
|
+
if (this.#entryRevisions.has(normalized) && isExecutableSource(normalized))
|
|
106
|
+
return false;
|
|
89
107
|
const parts = relative(packageRoot, normalized).split(sep);
|
|
90
108
|
const capability = parts.findIndex((part) => capabilityRoots.has(part));
|
|
91
109
|
if (capability < 0)
|
|
@@ -134,6 +152,11 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
134
152
|
}
|
|
135
153
|
this.#pendingDependencies = undefined;
|
|
136
154
|
this.#dependencies.retain(sources);
|
|
155
|
+
const retained = new Set(sources.map(normalizeSourcePath));
|
|
156
|
+
for (const entry of this.#entryRevisions.keys()) {
|
|
157
|
+
if (!retained.has(entry))
|
|
158
|
+
this.#entryRevisions.delete(entry);
|
|
159
|
+
}
|
|
137
160
|
}
|
|
138
161
|
rollbackGeneration() {
|
|
139
162
|
this.#pendingDependencies = undefined;
|
|
@@ -170,6 +193,7 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
170
193
|
this.#watchers.clear();
|
|
171
194
|
this.#pendingDependencies = undefined;
|
|
172
195
|
this.#dependencies.clear();
|
|
196
|
+
this.#entryRevisions.clear();
|
|
173
197
|
await this.#tsxLoader?.close?.();
|
|
174
198
|
}
|
|
175
199
|
#assertOpen() {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
2
|
+
import type { ResolvedPackage } from './package-resolver.js';
|
|
3
|
+
/** One source of truth per package: executable declaration or JSON Schema. */
|
|
4
|
+
export declare function readPluginConfigSchema(pkg: ResolvedPackage, modules?: Pick<ModuleRuntime, 'load'>): Promise<Readonly<Record<string, unknown>> | null>;
|
|
5
|
+
/** Detached declaration metadata for preflight and custom-resolver startup views. */
|
|
6
|
+
export declare function immutableConfigSchema(schema: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
/** One source of truth per package: executable declaration or JSON Schema. */
|
|
4
|
+
export async function readPluginConfigSchema(pkg, modules) {
|
|
5
|
+
const file = resolve(pkg.root, 'schema.json');
|
|
6
|
+
let fileSchema;
|
|
7
|
+
try {
|
|
8
|
+
fileSchema = JSON.parse(await readFile(file, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
// Isolated plugin code never executes in the composition process.
|
|
15
|
+
const manifest = pkg.packageJson.zhin;
|
|
16
|
+
let declaration;
|
|
17
|
+
if (modules && manifest.type === 'plugin' && manifest.runtime !== 'isolated') {
|
|
18
|
+
const module = await modules.load(resolve(pkg.root, manifest.entry));
|
|
19
|
+
declaration = module.default?.config;
|
|
20
|
+
}
|
|
21
|
+
if (declaration && fileSchema !== undefined) {
|
|
22
|
+
throw new TypeError(`${pkg.name} declares both plugin config and schema.json; choose one configuration schema`);
|
|
23
|
+
}
|
|
24
|
+
const schema = declaration ? declaration.toJsonSchema() : fileSchema;
|
|
25
|
+
if (schema === undefined)
|
|
26
|
+
return null;
|
|
27
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
28
|
+
throw new TypeError(`${pkg.name} configuration must contain a JSON Schema object`);
|
|
29
|
+
}
|
|
30
|
+
return immutableConfigSchema(schema);
|
|
31
|
+
}
|
|
32
|
+
/** Detached declaration metadata for preflight and custom-resolver startup views. */
|
|
33
|
+
export function immutableConfigSchema(schema) {
|
|
34
|
+
const copy = structuredClone(schema);
|
|
35
|
+
const freeze = (value) => {
|
|
36
|
+
if (!value || typeof value !== 'object')
|
|
37
|
+
return;
|
|
38
|
+
for (const child of Object.values(value))
|
|
39
|
+
freeze(child);
|
|
40
|
+
Object.freeze(value);
|
|
41
|
+
};
|
|
42
|
+
freeze(copy);
|
|
43
|
+
return copy;
|
|
44
|
+
}
|
|
@@ -6,7 +6,10 @@ import type { PluginGraphNode } from './project-graph.js';
|
|
|
6
6
|
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
7
7
|
import { type PrimaryConfig } from './primary-config.js';
|
|
8
8
|
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
9
|
-
export
|
|
9
|
+
export interface PluginConfigResolver {
|
|
10
|
+
(node: PluginGraphNode): unknown;
|
|
11
|
+
schema?(node: PluginGraphNode): Readonly<Record<string, unknown>> | undefined;
|
|
12
|
+
}
|
|
10
13
|
export interface RootResourceContext {
|
|
11
14
|
/** Exact shadow generation being assembled; never inferred from a latest snapshot. */
|
|
12
15
|
readonly generation: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import { GenerationHandoffStack, Scope, capabilityId, createGenerationAdmissionGate, createToken, PluginDatabaseHost, PluginScheduleHost, databaseHostToken, databaseRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { GenerationHandoffStack, Scope, capabilityId, createGenerationAdmissionGate, createToken, PluginDatabaseHost, PluginOutboundHost, PluginScheduleHost, databaseHostToken, databaseRootHostToken, outboundHostToken, outboundRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, } from '@zhin.js/plugin-runtime';
|
|
3
3
|
import { runtimeEnvironmentToken } from './environment.js';
|
|
4
4
|
import { EnvStoreFactory, envStoreToken, } from './environment-store.js';
|
|
5
5
|
import { createPrimaryConfig, primaryConfigToken, } from './primary-config.js';
|
|
@@ -98,6 +98,9 @@ export class PluginScopeAssembler {
|
|
|
98
98
|
role: node.parent ? 'child' : 'root',
|
|
99
99
|
});
|
|
100
100
|
let metadata;
|
|
101
|
+
// Schema composition is Host-owned even when Plugin execution is isolated.
|
|
102
|
+
// Commit the validated JSON declaration with both kinds of Plugin snapshot.
|
|
103
|
+
const configSchema = this.configResolver.schema?.(node);
|
|
101
104
|
if (manifest.runtime === 'isolated') {
|
|
102
105
|
if (!node.parent)
|
|
103
106
|
throw new Error('Root Plugin cannot use runtime: isolated');
|
|
@@ -165,9 +168,11 @@ export class PluginScopeAssembler {
|
|
|
165
168
|
instanceKey: node.instanceKey,
|
|
166
169
|
packageName: node.package.name,
|
|
167
170
|
packageRoot: node.package.root,
|
|
171
|
+
...(typeof node.package.packageJson.version === 'string' ? { packageVersion: node.package.packageJson.version } : {}),
|
|
168
172
|
parent: node.parent,
|
|
169
173
|
children: Object.freeze(node.children.map((child) => child.id)),
|
|
170
174
|
metadata,
|
|
175
|
+
...(configSchema ? { configSchema } : {}),
|
|
171
176
|
}));
|
|
172
177
|
this.config.set(node.id, config);
|
|
173
178
|
this.resources.set(node.id, scope.snapshot());
|
|
@@ -226,17 +231,25 @@ export class PluginScopeAssembler {
|
|
|
226
231
|
}
|
|
227
232
|
/** Root resources are process-owned; every Plugin Scope receives an owner-bound facade. */
|
|
228
233
|
installOwnerScopedHosts(scope, owner) {
|
|
229
|
-
const
|
|
230
|
-
|
|
234
|
+
const root = this.scopes.get(rootPluginId());
|
|
235
|
+
if (!root)
|
|
236
|
+
throw new Error('Missing root resource scope');
|
|
237
|
+
// A retired subtree must close its facades even when the parent remains active.
|
|
238
|
+
const admission = createGenerationAdmissionGate();
|
|
239
|
+
if (root.has(outboundRootHostToken)) {
|
|
240
|
+
scope.provide(outboundHostToken, new PluginOutboundHost(owner, root.use(outboundRootHostToken), admission));
|
|
241
|
+
}
|
|
242
|
+
const database = root.has(databaseRootHostToken)
|
|
243
|
+
? root.use(databaseRootHostToken)
|
|
231
244
|
: undefined;
|
|
232
245
|
if (database) {
|
|
233
|
-
scope.provide(databaseHostToken, new PluginDatabaseHost(owner, database));
|
|
246
|
+
scope.provide(databaseHostToken, new PluginDatabaseHost(owner, database, admission));
|
|
234
247
|
}
|
|
235
|
-
const schedule =
|
|
236
|
-
?
|
|
248
|
+
const schedule = root.has(scheduleRootHostToken)
|
|
249
|
+
? root.use(scheduleRootHostToken)
|
|
237
250
|
: undefined;
|
|
238
251
|
if (schedule) {
|
|
239
|
-
scope.provide(scheduleHostToken, new PluginScheduleHost(owner, schedule));
|
|
252
|
+
scope.provide(scheduleHostToken, new PluginScheduleHost(owner, schedule, admission));
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
}
|
package/lib/root-runtime.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ControlErrorHandler, type ConfigDocumentPort, type ConfigPatch, type GenerationCommitListener, type PluginId, type RuntimeConfigDocument, type RuntimeSnapshot, type SnapshotReader } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { type JsonSchema } from './config-composer.js';
|
|
2
3
|
import { type RuntimeEnvironment } from './environment.js';
|
|
3
4
|
import { type EnvironmentLayers, type EnvironmentLayersPort } from './environment-store.js';
|
|
4
5
|
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
@@ -27,6 +28,10 @@ export declare class RootRuntime {
|
|
|
27
28
|
constructor(options: RootRuntimeOptions);
|
|
28
29
|
get snapshot(): RuntimeSnapshot;
|
|
29
30
|
get snapshots(): SnapshotReader;
|
|
31
|
+
/** Committed metadata, or validated startup declarations while first login is pending. */
|
|
32
|
+
get configurationSchemas(): ReadonlyMap<string, JsonSchema>;
|
|
33
|
+
/** Metadata readiness is independent of account login and generation activation. */
|
|
34
|
+
whenConfigurationReady(): Promise<void>;
|
|
30
35
|
onGenerationCommit(listener: GenerationCommitListener<RuntimeGenerationState>): () => void;
|
|
31
36
|
get sourceOwnership(): SourceOwnershipIndex;
|
|
32
37
|
start(): Promise<RuntimeSnapshot>;
|
package/lib/root-runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
4
4
|
import { DisposeStack, GenerationHandoffStack, RootController, ConfigDocumentDivergenceError, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
5
5
|
import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
|
|
6
6
|
import { ConfigComposer } from './config-composer.js';
|
|
7
|
+
import { immutableConfigSchema } from './plugin-config-schema.js';
|
|
7
8
|
import { ConfigPatchPlanner } from './config-patch-planner.js';
|
|
8
9
|
import { defineRuntimeEnvironment } from './environment.js';
|
|
9
10
|
import { createEnvStore, defineEnvironmentLayers, } from './environment-store.js';
|
|
@@ -39,9 +40,17 @@ export class RootRuntime {
|
|
|
39
40
|
#isolation;
|
|
40
41
|
#configPatchTail = Promise.resolve();
|
|
41
42
|
#stopResult;
|
|
43
|
+
#initialConfigSchemas = new Map();
|
|
44
|
+
#resolveConfigurationReady;
|
|
45
|
+
#rejectConfigurationReady;
|
|
46
|
+
#configurationReady = new Promise((resolve, reject) => {
|
|
47
|
+
this.#resolveConfigurationReady = resolve;
|
|
48
|
+
this.#rejectConfigurationReady = reject;
|
|
49
|
+
});
|
|
42
50
|
#controller;
|
|
43
51
|
#disabledPluginInstanceKeys;
|
|
44
52
|
constructor(options) {
|
|
53
|
+
void this.#configurationReady.catch(() => undefined);
|
|
45
54
|
this.#projectRoot = normalizeSourcePath(options.projectRoot);
|
|
46
55
|
this.#modules = options.modules;
|
|
47
56
|
this.#environment = defineRuntimeEnvironment(options.environment);
|
|
@@ -68,6 +77,17 @@ export class RootRuntime {
|
|
|
68
77
|
get snapshots() {
|
|
69
78
|
return this.#controller.snapshots;
|
|
70
79
|
}
|
|
80
|
+
/** Committed metadata, or validated startup declarations while first login is pending. */
|
|
81
|
+
get configurationSchemas() {
|
|
82
|
+
const snapshot = this.snapshot;
|
|
83
|
+
return snapshot.generation === 0 ? new Map(this.#initialConfigSchemas)
|
|
84
|
+
: new Map([...snapshot.tree.values()].flatMap(node => node.configSchema
|
|
85
|
+
? [[node.packageRoot, node.configSchema]] : []));
|
|
86
|
+
}
|
|
87
|
+
/** Metadata readiness is independent of account login and generation activation. */
|
|
88
|
+
whenConfigurationReady() {
|
|
89
|
+
return this.#configurationReady;
|
|
90
|
+
}
|
|
71
91
|
onGenerationCommit(listener) {
|
|
72
92
|
return this.#controller.onGenerationCommit(listener);
|
|
73
93
|
}
|
|
@@ -75,18 +95,24 @@ export class RootRuntime {
|
|
|
75
95
|
return this.#controller.committed.state.ownership;
|
|
76
96
|
}
|
|
77
97
|
async start() {
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
try {
|
|
99
|
+
if (this.#environmentSource) {
|
|
100
|
+
this.#environmentLayers = defineEnvironmentLayers(await this.#environmentSource.read());
|
|
101
|
+
}
|
|
102
|
+
if (this.#configPort) {
|
|
103
|
+
const snapshot = await this.#configPort.read();
|
|
104
|
+
this.#configSnapshot = snapshot;
|
|
105
|
+
this.#configDocument = structuredClone(snapshot.document);
|
|
106
|
+
}
|
|
107
|
+
const snapshot = await this.#controller.start(async (current, signal) => {
|
|
108
|
+
return (await this.#prepare(current, signal)).generation;
|
|
109
|
+
});
|
|
110
|
+
return snapshot;
|
|
80
111
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.#configDocument = structuredClone(snapshot.document);
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.#rejectConfigurationReady(error);
|
|
114
|
+
throw error;
|
|
85
115
|
}
|
|
86
|
-
const snapshot = await this.#controller.start(async (current, signal) => {
|
|
87
|
-
return (await this.#prepare(current, signal)).generation;
|
|
88
|
-
});
|
|
89
|
-
return snapshot;
|
|
90
116
|
}
|
|
91
117
|
async reload(target = rootPluginId()) {
|
|
92
118
|
const snapshot = await this.#controller.reload(target, async (current, signal) => {
|
|
@@ -118,6 +144,7 @@ export class RootRuntime {
|
|
|
118
144
|
return new RootProcessRestartExecutor(this, adapter);
|
|
119
145
|
}
|
|
120
146
|
stop() {
|
|
147
|
+
this.#rejectConfigurationReady(new Error('Runtime stopped before configuration initialization'));
|
|
121
148
|
if (this.#stopResult)
|
|
122
149
|
return this.#stopResult;
|
|
123
150
|
const result = (async () => {
|
|
@@ -200,7 +227,7 @@ export class RootRuntime {
|
|
|
200
227
|
const environmentLayers = environmentChanged && this.#environmentSource
|
|
201
228
|
? defineEnvironmentLayers(await this.#environmentSource.read())
|
|
202
229
|
: this.#environmentLayers;
|
|
203
|
-
const inputPlan = await new RuntimeInputPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys)).plan({
|
|
230
|
+
const inputPlan = await new RuntimeInputPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys, this.#modules)).plan({
|
|
204
231
|
graph: this.#model.graph,
|
|
205
232
|
environment: this.#environment,
|
|
206
233
|
previousDocument: requireConfigDocument(this.#configDocument),
|
|
@@ -285,18 +312,39 @@ export class RootRuntime {
|
|
|
285
312
|
await this.#refreshConfigDocument();
|
|
286
313
|
const activeDocument = document ?? this.#configDocument;
|
|
287
314
|
if (this.#configResolver) {
|
|
315
|
+
const customResolver = this.#configResolver;
|
|
316
|
+
const schemas = new Map(graphNodes(graph.root).flatMap(node => {
|
|
317
|
+
const schema = customResolver.schema?.(node);
|
|
318
|
+
return schema ? [[node.id, immutableConfigSchema(schema)]] : [];
|
|
319
|
+
}));
|
|
320
|
+
const configResolver = node => customResolver(node);
|
|
321
|
+
configResolver.schema = node => schemas.get(node.id);
|
|
322
|
+
if (this.snapshot.generation === 0) {
|
|
323
|
+
this.#initialConfigSchemas = new Map(graphNodes(graph.root).flatMap(node => {
|
|
324
|
+
const schema = schemas.get(node.id);
|
|
325
|
+
return schema ? [[node.package.root, schema]] : [];
|
|
326
|
+
}));
|
|
327
|
+
this.#resolveConfigurationReady();
|
|
328
|
+
}
|
|
288
329
|
return {
|
|
289
330
|
graph,
|
|
290
|
-
configResolver
|
|
331
|
+
configResolver,
|
|
291
332
|
primaryConfigDocument: Object.freeze({}),
|
|
292
333
|
environmentLayers,
|
|
293
334
|
};
|
|
294
335
|
}
|
|
295
|
-
const composed = await new ConfigComposer(this.#disabledPluginInstanceKeys)
|
|
336
|
+
const composed = await new ConfigComposer(this.#disabledPluginInstanceKeys, this.#modules)
|
|
296
337
|
.compose(graph, activeDocument);
|
|
338
|
+
if (this.snapshot.generation === 0) {
|
|
339
|
+
this.#initialConfigSchemas = new Map(graphNodes(graph.root).flatMap(node => {
|
|
340
|
+
const schema = composed.ownSchemas.get(node.id);
|
|
341
|
+
return schema ? [[node.package.root, schema]] : [];
|
|
342
|
+
}));
|
|
343
|
+
this.#resolveConfigurationReady();
|
|
344
|
+
}
|
|
297
345
|
return {
|
|
298
346
|
graph,
|
|
299
|
-
configResolver: this.#configViewResolver(composed.views, environmentLayers),
|
|
347
|
+
configResolver: this.#configViewResolver(composed.views, environmentLayers, composed.ownSchemas),
|
|
300
348
|
primaryConfigDocument: composed.document,
|
|
301
349
|
environmentLayers,
|
|
302
350
|
};
|
|
@@ -317,14 +365,16 @@ export class RootRuntime {
|
|
|
317
365
|
this.#configSnapshot = snapshot;
|
|
318
366
|
this.#configDocument = structuredClone(snapshot.document);
|
|
319
367
|
}
|
|
320
|
-
#configViewResolver(views, environmentLayers = this.#environmentLayers) {
|
|
321
|
-
|
|
368
|
+
#configViewResolver(views, environmentLayers = this.#environmentLayers, schemas) {
|
|
369
|
+
const resolver = (node) => {
|
|
322
370
|
const view = views.get(node.id);
|
|
323
371
|
if (view === undefined)
|
|
324
372
|
return undefined;
|
|
325
373
|
return createEnvStore(node.id, this.#environment, environmentLayers)
|
|
326
374
|
.expandMissingAsEmpty(view);
|
|
327
375
|
};
|
|
376
|
+
resolver.schema = (node) => schemas?.get(node.id);
|
|
377
|
+
return resolver;
|
|
328
378
|
}
|
|
329
379
|
async #applyConfigPatches(patches) {
|
|
330
380
|
if (!this.#configDocument) {
|
|
@@ -344,7 +394,7 @@ export class RootRuntime {
|
|
|
344
394
|
const graph = await new ProjectGraphService(resolver, {
|
|
345
395
|
disabledPluginInstanceKeys: this.#disabledPluginInstanceKeys,
|
|
346
396
|
}).inspect(this.#projectRoot);
|
|
347
|
-
const planned = await new ConfigPatchPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys)).plan(graph, currentDocument, patches);
|
|
397
|
+
const planned = await new ConfigPatchPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys, this.#modules)).plan(graph, currentDocument, patches);
|
|
348
398
|
plan = planned;
|
|
349
399
|
if (!planned.documentChanged)
|
|
350
400
|
return undefined;
|
|
@@ -369,7 +419,7 @@ export class RootRuntime {
|
|
|
369
419
|
}
|
|
370
420
|
const inspected = {
|
|
371
421
|
graph,
|
|
372
|
-
configResolver: this.#configViewResolver(planned.views),
|
|
422
|
+
configResolver: this.#configViewResolver(planned.views, this.#environmentLayers, planned.ownSchemas),
|
|
373
423
|
primaryConfigDocument: planned.document,
|
|
374
424
|
environmentLayers: this.#environmentLayers,
|
|
375
425
|
};
|
|
@@ -590,6 +640,9 @@ class GenerationAssembler {
|
|
|
590
640
|
};
|
|
591
641
|
}
|
|
592
642
|
}
|
|
643
|
+
function graphNodes(node) {
|
|
644
|
+
return [node, ...node.children.flatMap(graphNodes)];
|
|
645
|
+
}
|
|
593
646
|
function emptyState() {
|
|
594
647
|
return {
|
|
595
648
|
root: rootPluginId(),
|
|
@@ -30,14 +30,10 @@ export class SlotGenerationPreparer {
|
|
|
30
30
|
for (const slot of replacements)
|
|
31
31
|
capabilities.set(slot.id, slot);
|
|
32
32
|
}
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
const providers = [...
|
|
36
|
-
|
|
37
|
-
if (!provider)
|
|
38
|
-
throw new Error(`Missing Feature provider for ${feature}`);
|
|
39
|
-
return provider;
|
|
40
|
-
});
|
|
33
|
+
// Snapshot-capturing execution indexes need a current view even when their
|
|
34
|
+
// own definitions were not edited. Retain only explicitly shareable resources.
|
|
35
|
+
const providers = [...this.model.providers.values()].filter((provider) => selectedByFeature.has(provider.id) || provider.runtime.retain !== 'when-owner-unchanged');
|
|
36
|
+
const projectedFeatures = providers.map((provider) => provider.id);
|
|
41
37
|
const projected = await new FeatureProjector(providers).project(current.generation + 1, {
|
|
42
38
|
root: current.root,
|
|
43
39
|
tree: current.tree,
|
|
@@ -48,7 +44,7 @@ export class SlotGenerationPreparer {
|
|
|
48
44
|
try {
|
|
49
45
|
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
50
46
|
const ownership = SourceOwnershipIndex.fromGeneration(this.model.graph, snapshot, this.model.featureIdsByPackageRoot);
|
|
51
|
-
const assets = this.model.assets.replaceProjections(
|
|
47
|
+
const assets = this.model.assets.replaceProjections(projectedFeatures, projected.disposers);
|
|
52
48
|
return prepareRuntimeGeneration({
|
|
53
49
|
snapshot: projected.state,
|
|
54
50
|
dispose: () => assets.dispose(),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { type PluginId, type RuntimeSnapshot, type RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { RuntimeEnvironment } from './environment.js';
|
|
3
3
|
import type { EnvironmentLayers } from './environment-store.js';
|
|
4
|
-
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
5
4
|
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
6
5
|
import type { ModuleRuntime } from './module-runtime.js';
|
|
7
6
|
import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
|
|
@@ -71,13 +71,28 @@ export class SubtreeGenerationPreparer {
|
|
|
71
71
|
for (const slot of slots)
|
|
72
72
|
addCapabilitySlot(capabilities, slot);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
// Most indexes capture a complete construction snapshot and must rebuild.
|
|
75
|
+
// Only an explicitly shareable projection may retain unchanged transports.
|
|
76
|
+
const affectedFeatures = new Set([...this.model.providers.values()]
|
|
77
|
+
.filter((provider) => provider.runtime.retain !== 'when-owner-unchanged')
|
|
78
|
+
.map((provider) => provider.id));
|
|
79
|
+
for (const [feature, featureRoots] of this.model.rootsByFeature) {
|
|
80
|
+
if (featureRoots.some((entry) => roots.some((root) => isWithin(entry.owner, root)))) {
|
|
81
|
+
affectedFeatures.add(feature);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const slot of [...current.capabilities.values(), ...capabilities.values()]) {
|
|
85
|
+
if (roots.some((root) => isWithin(slot.owner, root)))
|
|
86
|
+
affectedFeatures.add(slot.feature);
|
|
87
|
+
}
|
|
88
|
+
const providers = [...this.model.providers.values()].filter((provider) => affectedFeatures.has(provider.id));
|
|
89
|
+
const projected = await new FeatureProjector(providers).project(current.generation + 1, {
|
|
75
90
|
root: current.root,
|
|
76
91
|
tree: plugins.tree,
|
|
77
92
|
config: plugins.config,
|
|
78
93
|
resources: plugins.resources,
|
|
79
94
|
capabilities,
|
|
80
|
-
}, signal);
|
|
95
|
+
}, signal, current.projections);
|
|
81
96
|
for (const [feature, dispose] of projected.disposers) {
|
|
82
97
|
projectionDisposers.set(feature, dispose);
|
|
83
98
|
}
|
|
@@ -87,7 +102,7 @@ export class SubtreeGenerationPreparer {
|
|
|
87
102
|
const replacements = new Map(plugins.createdScopeDisposers());
|
|
88
103
|
// Local prepare still commits a complete immutable generation. The
|
|
89
104
|
// replacement map controls lifetime ownership, not snapshot granularity.
|
|
90
|
-
const assets = this.model.assets.replaceScopes([...nodes.keys()], replacements, projectionDisposers);
|
|
105
|
+
const assets = this.model.assets.replaceScopes([...nodes.keys()], replacements, affectedFeatures, projectionDisposers);
|
|
91
106
|
return prepareRuntimeGeneration({
|
|
92
107
|
snapshot: projected.state,
|
|
93
108
|
dispose: () => assets.dispose(),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type CapabilityId, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { type CapabilityId, type PluginId, type RuntimeSnapshot, type RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { RuntimeEnvironment } from './environment.js';
|
|
3
3
|
import type { EnvironmentLayers } from './environment-store.js';
|
|
4
|
-
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
5
4
|
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
6
5
|
import type { ModuleRuntime } from './module-runtime.js';
|
|
7
6
|
import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
|
|
@@ -92,7 +92,7 @@ export class TopologyGenerationPreparer {
|
|
|
92
92
|
plugins.bindCandidateSnapshot(snapshot);
|
|
93
93
|
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
|
|
94
94
|
const replacements = new Map(plugins.createdScopeDisposers());
|
|
95
|
-
const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, projectionDisposers);
|
|
95
|
+
const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, new Set([...this.model.providers.keys(), ...featureTopology.providers.keys()]), projectionDisposers);
|
|
96
96
|
return prepareRuntimeGeneration({
|
|
97
97
|
snapshot: projected.state,
|
|
98
98
|
dispose: () => assets.dispose(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/runtime",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
|
-
"development": "./src/index.ts",
|
|
12
11
|
"import": "./lib/index.js"
|
|
13
12
|
}
|
|
14
13
|
},
|
|
@@ -20,24 +19,25 @@
|
|
|
20
19
|
"es-module-lexer": "2.3.1",
|
|
21
20
|
"import-meta-resolve": "4.2.0",
|
|
22
21
|
"semver": "7.8.5",
|
|
23
|
-
"@zhin.js/
|
|
24
|
-
"@zhin.js/
|
|
22
|
+
"@zhin.js/feature-kit": "1.1.5",
|
|
23
|
+
"@zhin.js/plugin-runtime": "1.1.14"
|
|
25
24
|
},
|
|
26
25
|
"devDependencies": {
|
|
27
26
|
"@types/node": "^26.1.2",
|
|
28
27
|
"tsx": "^4.23.1",
|
|
29
28
|
"typescript": "^6.0.3",
|
|
30
|
-
"@zhin.js/
|
|
31
|
-
"@zhin.js/
|
|
32
|
-
"@zhin.js/
|
|
33
|
-
"@zhin.js/component": "1.1.
|
|
34
|
-
"@zhin.js/
|
|
35
|
-
"@zhin.js/
|
|
36
|
-
"@zhin.js/
|
|
37
|
-
"@zhin.js/
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
29
|
+
"@zhin.js/agent-feature": "1.1.6",
|
|
30
|
+
"@zhin.js/command": "1.1.7",
|
|
31
|
+
"@zhin.js/adapter": "1.1.19",
|
|
32
|
+
"@zhin.js/component": "1.1.6",
|
|
33
|
+
"@zhin.js/handler": "1.1.5",
|
|
34
|
+
"@zhin.js/layout": "1.1.6",
|
|
35
|
+
"@zhin.js/mcp-feature": "1.1.6",
|
|
36
|
+
"@zhin.js/middleware": "1.1.6",
|
|
37
|
+
"@zhin.js/page": "1.1.6",
|
|
38
|
+
"@zhin.js/prompt-section": "1.1.5",
|
|
39
|
+
"@zhin.js/skill": "1.1.6",
|
|
40
|
+
"@zhin.js/tool": "1.1.6"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"registry": "https://registry.npmjs.org"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
|
-
"node": "
|
|
52
|
+
"node": ">=22.19.0"
|
|
53
53
|
},
|
|
54
54
|
"license": "MIT",
|
|
55
55
|
"scripts": {
|