@zhin.js/runtime 1.1.4 → 1.1.6
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 +275 -7
- package/lib/config-composer.js +46 -34
- package/lib/generation-assets.d.ts +1 -1
- package/lib/generation-assets.js +9 -5
- package/lib/host-config-schema.json +102 -3
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/native-development-runtime.js +19 -1
- 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 +15 -14
package/lib/config-composer.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import type
|
|
1
|
+
import type { RuntimeConfigDocument, PluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { type ErrorObject } from 'ajv/dist/2020.js';
|
|
3
3
|
import type { ProjectGraph } from './project-graph.js';
|
|
4
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
4
5
|
export declare const HOST_CONFIG_SCHEMA: {
|
|
5
6
|
$schema: string;
|
|
6
7
|
type: string;
|
|
@@ -72,6 +73,36 @@ export declare const HOST_CONFIG_SCHEMA: {
|
|
|
72
73
|
description: string;
|
|
73
74
|
"x-descriptionZh": string;
|
|
74
75
|
properties: {
|
|
76
|
+
decisions: {
|
|
77
|
+
type: string;
|
|
78
|
+
additionalProperties: boolean;
|
|
79
|
+
required: string[];
|
|
80
|
+
description: string;
|
|
81
|
+
"x-descriptionZh": string;
|
|
82
|
+
properties: {
|
|
83
|
+
provider: {
|
|
84
|
+
type: string;
|
|
85
|
+
pattern: string;
|
|
86
|
+
description: string;
|
|
87
|
+
"x-descriptionZh": string;
|
|
88
|
+
};
|
|
89
|
+
skills: {
|
|
90
|
+
$ref: string;
|
|
91
|
+
};
|
|
92
|
+
tools: {
|
|
93
|
+
$ref: string;
|
|
94
|
+
};
|
|
95
|
+
memory: {
|
|
96
|
+
$ref: string;
|
|
97
|
+
};
|
|
98
|
+
agents: {
|
|
99
|
+
$ref: string;
|
|
100
|
+
};
|
|
101
|
+
approval: {
|
|
102
|
+
$ref: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
75
106
|
workroom: {
|
|
76
107
|
type: string;
|
|
77
108
|
additionalProperties: boolean;
|
|
@@ -175,6 +206,20 @@ export declare const HOST_CONFIG_SCHEMA: {
|
|
|
175
206
|
description: string;
|
|
176
207
|
"x-descriptionZh": string;
|
|
177
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
|
+
};
|
|
178
223
|
};
|
|
179
224
|
};
|
|
180
225
|
execSecurity: {
|
|
@@ -189,25 +234,202 @@ export declare const HOST_CONFIG_SCHEMA: {
|
|
|
189
234
|
description: string;
|
|
190
235
|
"x-descriptionZh": string;
|
|
191
236
|
};
|
|
192
|
-
|
|
237
|
+
masterShell: {
|
|
193
238
|
type: string;
|
|
194
239
|
enum: string[];
|
|
240
|
+
default: string;
|
|
195
241
|
description: string;
|
|
196
242
|
"x-descriptionZh": string;
|
|
197
243
|
};
|
|
198
|
-
|
|
244
|
+
toolBudget: {
|
|
245
|
+
type: string;
|
|
246
|
+
description: string;
|
|
247
|
+
"x-descriptionZh": string;
|
|
248
|
+
properties: {
|
|
249
|
+
extendBy: {
|
|
250
|
+
type: string;
|
|
251
|
+
minimum: number;
|
|
252
|
+
default: number;
|
|
253
|
+
description: string;
|
|
254
|
+
"x-descriptionZh": string;
|
|
255
|
+
};
|
|
256
|
+
progressWindow: {
|
|
257
|
+
type: string;
|
|
258
|
+
minimum: number;
|
|
259
|
+
default: number;
|
|
260
|
+
description: string;
|
|
261
|
+
"x-descriptionZh": string;
|
|
262
|
+
};
|
|
263
|
+
maxTotalIterations: {
|
|
264
|
+
type: string;
|
|
265
|
+
minimum: number;
|
|
266
|
+
default: number;
|
|
267
|
+
description: string;
|
|
268
|
+
"x-descriptionZh": string;
|
|
269
|
+
};
|
|
270
|
+
maxSubagentTotalIterations: {
|
|
271
|
+
type: string;
|
|
272
|
+
minimum: number;
|
|
273
|
+
default: number;
|
|
274
|
+
description: string;
|
|
275
|
+
"x-descriptionZh": string;
|
|
276
|
+
};
|
|
277
|
+
maxWallClockMs: {
|
|
278
|
+
type: string;
|
|
279
|
+
minimum: number;
|
|
280
|
+
default: number;
|
|
281
|
+
description: string;
|
|
282
|
+
"x-descriptionZh": string;
|
|
283
|
+
};
|
|
284
|
+
maxTotalTokens: {
|
|
285
|
+
type: string;
|
|
286
|
+
minimum: number;
|
|
287
|
+
default: number;
|
|
288
|
+
description: string;
|
|
289
|
+
"x-descriptionZh": string;
|
|
290
|
+
};
|
|
291
|
+
failureStreakLimit: {
|
|
292
|
+
type: string;
|
|
293
|
+
minimum: number;
|
|
294
|
+
default: number;
|
|
295
|
+
description: string;
|
|
296
|
+
"x-descriptionZh": string;
|
|
297
|
+
};
|
|
298
|
+
repeatLimit: {
|
|
299
|
+
type: string;
|
|
300
|
+
minimum: number;
|
|
301
|
+
default: number;
|
|
302
|
+
description: string;
|
|
303
|
+
"x-descriptionZh": string;
|
|
304
|
+
};
|
|
305
|
+
autoContinue: {
|
|
306
|
+
type: string;
|
|
307
|
+
enum: string[];
|
|
308
|
+
default: string;
|
|
309
|
+
description: string;
|
|
310
|
+
"x-descriptionZh": string;
|
|
311
|
+
};
|
|
312
|
+
maxContinuations: {
|
|
313
|
+
type: string;
|
|
314
|
+
minimum: number;
|
|
315
|
+
default: number;
|
|
316
|
+
description: string;
|
|
317
|
+
"x-descriptionZh": string;
|
|
318
|
+
};
|
|
319
|
+
progressUpdates: {
|
|
320
|
+
type: string;
|
|
321
|
+
enum: string[];
|
|
322
|
+
default: string;
|
|
323
|
+
description: string;
|
|
324
|
+
"x-descriptionZh": string;
|
|
325
|
+
};
|
|
326
|
+
progressMinIntervalMs: {
|
|
327
|
+
type: string;
|
|
328
|
+
minimum: number;
|
|
329
|
+
default: number;
|
|
330
|
+
description: string;
|
|
331
|
+
"x-descriptionZh": string;
|
|
332
|
+
};
|
|
333
|
+
progressMaxUpdates: {
|
|
334
|
+
type: string;
|
|
335
|
+
minimum: number;
|
|
336
|
+
default: number;
|
|
337
|
+
description: string;
|
|
338
|
+
"x-descriptionZh": string;
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
injectionGuard: {
|
|
343
|
+
type: string;
|
|
344
|
+
description: string;
|
|
345
|
+
"x-descriptionZh": string;
|
|
346
|
+
properties: {
|
|
347
|
+
confirmHighRisk: {
|
|
348
|
+
type: string;
|
|
349
|
+
enum: string[];
|
|
350
|
+
default: string;
|
|
351
|
+
description: string;
|
|
352
|
+
"x-descriptionZh": string;
|
|
353
|
+
};
|
|
354
|
+
outboundCheck: {
|
|
355
|
+
type: string;
|
|
356
|
+
enum: string[];
|
|
357
|
+
default: string;
|
|
358
|
+
description: string;
|
|
359
|
+
"x-descriptionZh": string;
|
|
360
|
+
};
|
|
361
|
+
trustedOutputTools: {
|
|
362
|
+
type: string;
|
|
363
|
+
items: {
|
|
364
|
+
type: string;
|
|
365
|
+
};
|
|
366
|
+
default: never[];
|
|
367
|
+
description: string;
|
|
368
|
+
"x-descriptionZh": string;
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
usageQuota: {
|
|
373
|
+
type: string;
|
|
374
|
+
description: string;
|
|
375
|
+
"x-descriptionZh": string;
|
|
376
|
+
properties: {
|
|
377
|
+
windowMs: {
|
|
378
|
+
type: string;
|
|
379
|
+
minimum: number;
|
|
380
|
+
default: number;
|
|
381
|
+
description: string;
|
|
382
|
+
"x-descriptionZh": string;
|
|
383
|
+
};
|
|
384
|
+
perUserTokens: {
|
|
385
|
+
type: string;
|
|
386
|
+
minimum: number;
|
|
387
|
+
default: number;
|
|
388
|
+
description: string;
|
|
389
|
+
"x-descriptionZh": string;
|
|
390
|
+
};
|
|
391
|
+
perGroupTokens: {
|
|
392
|
+
type: string;
|
|
393
|
+
minimum: number;
|
|
394
|
+
default: number;
|
|
395
|
+
description: string;
|
|
396
|
+
"x-descriptionZh": string;
|
|
397
|
+
};
|
|
398
|
+
perGroupRequestsPerMinute: {
|
|
399
|
+
type: string;
|
|
400
|
+
minimum: number;
|
|
401
|
+
default: number;
|
|
402
|
+
description: string;
|
|
403
|
+
"x-descriptionZh": string;
|
|
404
|
+
};
|
|
405
|
+
exemptOwner: {
|
|
406
|
+
type: string;
|
|
407
|
+
default: boolean;
|
|
408
|
+
description: string;
|
|
409
|
+
"x-descriptionZh": string;
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
subagentTurnMode: {
|
|
199
414
|
type: string;
|
|
200
415
|
enum: string[];
|
|
416
|
+
default: string;
|
|
201
417
|
description: string;
|
|
202
418
|
"x-descriptionZh": string;
|
|
203
419
|
};
|
|
204
|
-
|
|
420
|
+
execApprovalMode: {
|
|
421
|
+
type: string;
|
|
422
|
+
enum: string[];
|
|
423
|
+
description: string;
|
|
424
|
+
"x-descriptionZh": string;
|
|
425
|
+
};
|
|
426
|
+
subagentExecApprovalMode: {
|
|
205
427
|
type: string;
|
|
206
428
|
enum: string[];
|
|
207
429
|
description: string;
|
|
208
430
|
"x-descriptionZh": string;
|
|
209
431
|
};
|
|
210
|
-
|
|
432
|
+
workerExecApprovalMode: {
|
|
211
433
|
type: string;
|
|
212
434
|
enum: string[];
|
|
213
435
|
description: string;
|
|
@@ -564,6 +786,44 @@ export declare const HOST_CONFIG_SCHEMA: {
|
|
|
564
786
|
"x-descriptionZh": string;
|
|
565
787
|
};
|
|
566
788
|
};
|
|
789
|
+
$defs: {
|
|
790
|
+
decisionTask: {
|
|
791
|
+
type: string;
|
|
792
|
+
additionalProperties: boolean;
|
|
793
|
+
required: string[];
|
|
794
|
+
properties: {
|
|
795
|
+
mode: {
|
|
796
|
+
type: string;
|
|
797
|
+
enum: string[];
|
|
798
|
+
};
|
|
799
|
+
timeoutMs: {
|
|
800
|
+
type: string;
|
|
801
|
+
minimum: number;
|
|
802
|
+
maximum: number;
|
|
803
|
+
};
|
|
804
|
+
minConfidence: {
|
|
805
|
+
type: string;
|
|
806
|
+
minimum: number;
|
|
807
|
+
maximum: number;
|
|
808
|
+
};
|
|
809
|
+
topK: {
|
|
810
|
+
type: string;
|
|
811
|
+
minimum: number;
|
|
812
|
+
maximum: number;
|
|
813
|
+
};
|
|
814
|
+
maxCandidates: {
|
|
815
|
+
type: string;
|
|
816
|
+
minimum: number;
|
|
817
|
+
maximum: number;
|
|
818
|
+
};
|
|
819
|
+
maxSelections: {
|
|
820
|
+
type: string;
|
|
821
|
+
minimum: number;
|
|
822
|
+
maximum: number;
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
};
|
|
826
|
+
};
|
|
567
827
|
};
|
|
568
828
|
/** Host configuration keys consumed by Runtime composition and Host installers. */
|
|
569
829
|
export declare const HOST_CONFIG_KEYS: readonly string[];
|
|
@@ -572,6 +832,7 @@ export interface ComposedConfig {
|
|
|
572
832
|
readonly effectiveSchema: JsonSchema;
|
|
573
833
|
readonly document: RuntimeConfigDocument;
|
|
574
834
|
readonly views: ReadonlyMap<PluginId, unknown>;
|
|
835
|
+
readonly ownSchemas: ReadonlyMap<PluginId, JsonSchema>;
|
|
575
836
|
}
|
|
576
837
|
export declare class ConfigSchemaCollisionError extends Error {
|
|
577
838
|
readonly plugin: PluginId;
|
|
@@ -588,8 +849,15 @@ export declare class ConfigValidationError extends Error {
|
|
|
588
849
|
}
|
|
589
850
|
export declare class ConfigComposer {
|
|
590
851
|
private readonly inactivePluginInstanceKeys;
|
|
591
|
-
|
|
852
|
+
private readonly modules?;
|
|
853
|
+
constructor(inactivePluginInstanceKeys?: readonly string[], modules?: Pick<ModuleRuntime, "load"> | undefined);
|
|
592
854
|
compose(graph: ProjectGraph, input?: RuntimeConfigDocument,
|
|
593
855
|
/** Source config file name, annotated on ConfigValidationError. */
|
|
594
856
|
source?: string): Promise<ComposedConfig>;
|
|
595
857
|
}
|
|
858
|
+
/** Reuses Runtime validation/defaults without mutating the caller's configuration. */
|
|
859
|
+
export declare function parsePluginConfigSchema(schema: JsonSchema, input: unknown): Readonly<{
|
|
860
|
+
value: unknown;
|
|
861
|
+
errors: readonly ErrorObject[];
|
|
862
|
+
}>;
|
|
863
|
+
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,17 +47,18 @@ 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 /
|
|
57
58
|
// start-command, not Plugin ConfigViews.
|
|
58
59
|
const effectiveSchema = Object.freeze({
|
|
59
60
|
type: 'object',
|
|
61
|
+
$defs: HOST_CONFIG_SCHEMA.$defs,
|
|
60
62
|
additionalProperties: false,
|
|
61
63
|
properties: {
|
|
62
64
|
...HOST_CONFIG_SCHEMA.properties,
|
|
@@ -73,17 +75,7 @@ export class ConfigComposer {
|
|
|
73
75
|
},
|
|
74
76
|
});
|
|
75
77
|
const document = structuredClone(input);
|
|
76
|
-
const ajv =
|
|
77
|
-
allErrors: true,
|
|
78
|
-
useDefaults: true,
|
|
79
|
-
strict: true,
|
|
80
|
-
// 当前内置 schema 无需 union(log_level 的 ['string','number'] 是 type 数组,
|
|
81
|
-
// 不触发 allowUnionTypes);保留此项是面向未来插件 schema 可能出现的
|
|
82
|
-
// anyOf/oneOf 标量 union,避免届时 Ajv strict 模式直接报错。
|
|
83
|
-
allowUnionTypes: true,
|
|
84
|
-
});
|
|
85
|
-
ajv.addKeyword({ keyword: 'x-descriptionZh', schemaType: 'string' });
|
|
86
|
-
ajv.addKeyword({ keyword: 'x-keyPlaceholder', schemaType: 'string' });
|
|
78
|
+
const ajv = configurationValidator();
|
|
87
79
|
const validate = ajv.compile(effectiveSchema);
|
|
88
80
|
if (!validate(document)) {
|
|
89
81
|
throw new ConfigValidationError(formatErrors(validate.errors ?? []), source);
|
|
@@ -97,6 +89,7 @@ export class ConfigComposer {
|
|
|
97
89
|
effectiveSchema,
|
|
98
90
|
document: Object.freeze(document),
|
|
99
91
|
views,
|
|
92
|
+
ownSchemas,
|
|
100
93
|
});
|
|
101
94
|
}
|
|
102
95
|
}
|
|
@@ -152,8 +145,8 @@ function injectFrameworkRoleSchema(properties) {
|
|
|
152
145
|
}),
|
|
153
146
|
});
|
|
154
147
|
}
|
|
155
|
-
async function composeNode(node, ownSchemas) {
|
|
156
|
-
const own = await readOwnSchema(node);
|
|
148
|
+
async function composeNode(node, ownSchemas, modules) {
|
|
149
|
+
const own = await readOwnSchema(node, modules);
|
|
157
150
|
ownSchemas.set(node.id, own);
|
|
158
151
|
const properties = { ...schemaProperties(own) };
|
|
159
152
|
// Adapter plugins (schemas declaring array-typed `endpoints`) get
|
|
@@ -166,24 +159,14 @@ async function composeNode(node, ownSchemas) {
|
|
|
166
159
|
if (Object.hasOwn(properties, child.instanceKey)) {
|
|
167
160
|
throw new ConfigSchemaCollisionError(node.id, child.instanceKey);
|
|
168
161
|
}
|
|
169
|
-
properties[child.instanceKey] = withDefault(await composeNode(child, ownSchemas));
|
|
162
|
+
properties[child.instanceKey] = withDefault(await composeNode(child, ownSchemas, modules));
|
|
170
163
|
}
|
|
171
164
|
return Object.freeze({ ...own, properties });
|
|
172
165
|
}
|
|
173
|
-
async function readOwnSchema(node) {
|
|
174
|
-
const file =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
value = JSON.parse(await readFile(file, 'utf8'));
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
181
|
-
value = { type: 'object', additionalProperties: false, properties: {} };
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
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: {} };
|
|
187
170
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
188
171
|
throw new TypeError(`${file} must contain a JSON Schema object`);
|
|
189
172
|
}
|
|
@@ -203,7 +186,7 @@ async function readOwnSchema(node) {
|
|
|
203
186
|
if (Object.hasOwn(properties, 'commandNamespace')) {
|
|
204
187
|
throw new ConfigSchemaCollisionError(node.id, 'commandNamespace');
|
|
205
188
|
}
|
|
206
|
-
return
|
|
189
|
+
return deepFreeze({
|
|
207
190
|
$id: `urn:zhin:plugin-config:${encodeURIComponent(String(node.id))}`,
|
|
208
191
|
type: 'object',
|
|
209
192
|
additionalProperties: false,
|
|
@@ -274,3 +257,32 @@ function formatErrors(errors) {
|
|
|
274
257
|
return base;
|
|
275
258
|
});
|
|
276
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() {
|
|
@@ -46,6 +46,21 @@
|
|
|
46
46
|
"description": "Providers, Agents, sessions, memory, tools, and execution security.",
|
|
47
47
|
"x-descriptionZh": "Provider、Agent、会话、记忆、工具与执行安全策略。",
|
|
48
48
|
"properties": {
|
|
49
|
+
"decisions": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"required": ["provider"],
|
|
53
|
+
"description": "Optional structured-decision service. Missing tasks are off; use shadow before active.",
|
|
54
|
+
"x-descriptionZh": "可选的结构化决策服务。未配置的任务关闭;建议先用 shadow 验证。",
|
|
55
|
+
"properties": {
|
|
56
|
+
"provider": { "type": "string", "pattern": "^root(/[a-z0-9][a-z0-9-]*)+$", "description": "Exact enabled DecisionProvider Plugin owner, such as root/typesafe.", "x-descriptionZh": "提供已启用 DecisionProvider 的精确插件身份,例如 root/typesafe。" },
|
|
57
|
+
"skills": { "$ref": "#/$defs/decisionTask" },
|
|
58
|
+
"tools": { "$ref": "#/$defs/decisionTask" },
|
|
59
|
+
"memory": { "$ref": "#/$defs/decisionTask" },
|
|
60
|
+
"agents": { "$ref": "#/$defs/decisionTask" },
|
|
61
|
+
"approval": { "$ref": "#/$defs/decisionTask" }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
49
64
|
"workroom": {
|
|
50
65
|
"type": "object",
|
|
51
66
|
"additionalProperties": true,
|
|
@@ -106,8 +121,22 @@
|
|
|
106
121
|
"groupMode": {
|
|
107
122
|
"type": "string",
|
|
108
123
|
"enum": ["supersede", "fifo"],
|
|
109
|
-
"description": "
|
|
110
|
-
"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": "同一会话中允许排在当前回合之后的回合数;超出时回复一句“正在忙”。"
|
|
111
140
|
}
|
|
112
141
|
}
|
|
113
142
|
},
|
|
@@ -123,10 +152,65 @@
|
|
|
123
152
|
"description": "Command allowlist preset used outside full mode.",
|
|
124
153
|
"x-descriptionZh": "非 full 模式使用的命令白名单预设。"
|
|
125
154
|
},
|
|
155
|
+
"masterShell": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"enum": ["off", "isolated"],
|
|
158
|
+
"default": "off",
|
|
159
|
+
"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.",
|
|
160
|
+
"x-descriptionZh": "IM 私聊中 master 的 Shell 权限。off:IM 会话不提供 bash;isolated:master 私聊在 Docker 容器内执行 bash(工作区可写、无网络、development 白名单、其余命令需审批),宿主无 Docker 时直接失败。"
|
|
161
|
+
},
|
|
162
|
+
"toolBudget": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"description": "Progress-aware tool-call budget. maxIterations is a soft budget extended while the turn makes progress; hard caps always apply.",
|
|
165
|
+
"x-descriptionZh": "进度感知的工具调用预算。maxIterations 为软上限,回合有进展时自动延长;硬上限始终生效。",
|
|
166
|
+
"properties": {
|
|
167
|
+
"extendBy": { "type": "integer", "minimum": 0, "default": 10, "description": "Steps added per extension when progressing; 0 disables.", "x-descriptionZh": "有进展时每次延长的步数;0 关闭。" },
|
|
168
|
+
"progressWindow": { "type": "integer", "minimum": 0, "default": 5, "description": "Progress must have happened within this many recent steps.", "x-descriptionZh": "最近多少步内有进展才延长。" },
|
|
169
|
+
"maxTotalIterations": { "type": "integer", "minimum": 0, "default": 40, "description": "Hard step cap per turn segment.", "x-descriptionZh": "每段回合的硬步数上限。" },
|
|
170
|
+
"maxSubagentTotalIterations": { "type": "integer", "minimum": 0, "default": 30, "description": "Hard step cap for an in-turn sub-agent.", "x-descriptionZh": "回合内子 Agent 的硬步数上限。" },
|
|
171
|
+
"maxWallClockMs": { "type": "integer", "minimum": 0, "default": 600000, "description": "Wall-clock cap for the whole turn in ms; 0 disables.", "x-descriptionZh": "整个回合的时间上限(毫秒);0 关闭。" },
|
|
172
|
+
"maxTotalTokens": { "type": "integer", "minimum": 0, "default": 0, "description": "Token cap for the whole turn; 0 disables.", "x-descriptionZh": "整个回合的 Token 上限;0 关闭。" },
|
|
173
|
+
"failureStreakLimit": { "type": "integer", "minimum": 0, "default": 3, "description": "Consecutive tool failures that stop extensions.", "x-descriptionZh": "连续失败多少次后不再延长。" },
|
|
174
|
+
"repeatLimit": { "type": "integer", "minimum": 0, "default": 2, "description": "Consecutive identical repeated calls that stop extensions.", "x-descriptionZh": "连续重复相同调用多少次后不再延长。" },
|
|
175
|
+
"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 总是。续跑沿用同一回合的策略与审批。" },
|
|
176
|
+
"maxContinuations": { "type": "integer", "minimum": 0, "default": 2, "description": "Maximum automatic continuations per turn.", "x-descriptionZh": "每回合最多自动续跑次数。" },
|
|
177
|
+
"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 包括群聊(群聊需显式开启)。" },
|
|
178
|
+
"progressMinIntervalMs": { "type": "integer", "minimum": 0, "default": 45000, "description": "Minimum time between progress updates in ms.", "x-descriptionZh": "两次进度更新的最小间隔(毫秒)。" },
|
|
179
|
+
"progressMaxUpdates": { "type": "integer", "minimum": 0, "default": 3, "description": "Maximum progress updates per turn; 0 disables.", "x-descriptionZh": "每回合最多进度更新次数;0 关闭。" }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"injectionGuard": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"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.",
|
|
185
|
+
"x-descriptionZh": "注入防护。外部内容(工具结果、网页、文件、子 agent 报告、群聊背景消息)进入回合后,高危动作(执行命令、向其他会话发消息、改配置、定时任务)必须审批;出站回复检查系统提示、密钥与其他会话记忆泄露。",
|
|
186
|
+
"properties": {
|
|
187
|
+
"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:关闭。" },
|
|
188
|
+
"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:关闭。" },
|
|
189
|
+
"trustedOutputTools": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Tools whose results do not count as external content.", "x-descriptionZh": "结果不算外部内容的工具。" }
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"usageQuota": {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"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.",
|
|
195
|
+
"x-descriptionZh": "按用户 / 按群的 token 配额与群速率限制。默认全部关闭(0),主人默认不受限;计数保存在内存中。",
|
|
196
|
+
"properties": {
|
|
197
|
+
"windowMs": { "type": "integer", "minimum": 60000, "default": 86400000, "description": "Quota window in ms.", "x-descriptionZh": "配额周期(毫秒)。" },
|
|
198
|
+
"perUserTokens": { "type": "integer", "minimum": 0, "default": 0, "description": "Tokens one user may use per window; 0 = unlimited.", "x-descriptionZh": "每个用户每周期可用的 token;0 不限。" },
|
|
199
|
+
"perGroupTokens": { "type": "integer", "minimum": 0, "default": 0, "description": "Tokens one group may use per window; 0 = unlimited.", "x-descriptionZh": "每个群每周期可用的 token;0 不限。" },
|
|
200
|
+
"perGroupRequestsPerMinute": { "type": "integer", "minimum": 0, "default": 0, "description": "Turns one group may start per minute; 0 = unlimited.", "x-descriptionZh": "每个群每分钟可发起的回合数;0 不限。" },
|
|
201
|
+
"exemptOwner": { "type": "boolean", "default": true, "description": "The owner is never limited.", "x-descriptionZh": "主人不受限制。" }
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"subagentTurnMode": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"enum": ["off", "sync"],
|
|
207
|
+
"default": "off",
|
|
208
|
+
"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.",
|
|
209
|
+
"x-descriptionZh": "IM 回合中的子 Agent。off:不提供 spawn_task;sync:spawn_task 在当前回合内同步运行子 Agent,权限不超过父回合(父回合不能运行 bash 时子 Agent 也没有),审批发往同一会话。"
|
|
210
|
+
},
|
|
126
211
|
"execApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for main Agent commands.", "x-descriptionZh": "主 Agent 命令的审批策略。" },
|
|
127
212
|
"subagentExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for sub-Agent commands.", "x-descriptionZh": "子 Agent 命令的审批策略。" },
|
|
128
213
|
"workerExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for worker commands.", "x-descriptionZh": "Worker 命令的审批策略。" },
|
|
129
|
-
"taskExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for task commands.", "x-descriptionZh": "Task 命令的审批策略。" },
|
|
130
214
|
"toolExecution": {
|
|
131
215
|
"type": "string",
|
|
132
216
|
"enum": ["parallel", "sequential", "tiered"],
|
|
@@ -264,5 +348,20 @@
|
|
|
264
348
|
"assistant": { "type": "object", "additionalProperties": true, "description": "Scheduled jobs, event ingress, and failure notifications.", "x-descriptionZh": "调度任务、事件入口和失败通知。" },
|
|
265
349
|
"log_level": { "type": ["string", "number"], "description": "Runtime log level.", "x-descriptionZh": "Runtime 日志级别。" },
|
|
266
350
|
"plugin": { "type": "object", "description": "Root Plugin configuration; replaced by its project schema during composition.", "x-descriptionZh": "Root Plugin 配置;组合时由项目 Schema 替换。" }
|
|
351
|
+
},
|
|
352
|
+
"$defs": {
|
|
353
|
+
"decisionTask": {
|
|
354
|
+
"type": "object",
|
|
355
|
+
"additionalProperties": false,
|
|
356
|
+
"required": ["mode"],
|
|
357
|
+
"properties": {
|
|
358
|
+
"mode": { "type": "string", "enum": ["off", "shadow", "active"] },
|
|
359
|
+
"timeoutMs": { "type": "integer", "minimum": 1, "maximum": 2147483647 },
|
|
360
|
+
"minConfidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
361
|
+
"topK": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
|
|
362
|
+
"maxCandidates": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
|
|
363
|
+
"maxSelections": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
267
366
|
}
|
|
268
367
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
27
27
|
#pendingDependencies;
|
|
28
28
|
#watchRoots;
|
|
29
29
|
#revision = 0;
|
|
30
|
+
#entryRevisions = new Map();
|
|
30
31
|
#closed = false;
|
|
31
32
|
constructor(options) {
|
|
32
33
|
ensureTypeScriptSpecifierRemap();
|
|
@@ -43,7 +44,9 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
43
44
|
const analysis = this.#pendingDependencies?.get(normalized)
|
|
44
45
|
?? await this.#dependencies.analyze(normalized);
|
|
45
46
|
const url = pathToFileURL(normalized);
|
|
46
|
-
|
|
47
|
+
if (!this.#entryRevisions.has(normalized))
|
|
48
|
+
this.#entryRevisions.set(normalized, this.#revision);
|
|
49
|
+
url.searchParams.set('zhin-generation', String(this.#entryRevisions.get(normalized)));
|
|
47
50
|
let loaded;
|
|
48
51
|
try {
|
|
49
52
|
loaded = await import(url.href);
|
|
@@ -61,8 +64,12 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
61
64
|
return loaded;
|
|
62
65
|
}
|
|
63
66
|
invalidate(source) {
|
|
67
|
+
const impacted = this.#dependencies.affectedSources(source);
|
|
64
68
|
this.#dependencies.invalidate(source);
|
|
65
69
|
this.#revision += 1;
|
|
70
|
+
for (const entry of new Set([normalizeSourcePath(source), ...impacted])) {
|
|
71
|
+
this.#entryRevisions.set(entry, this.#revision);
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
affectedSources(source) {
|
|
68
75
|
return this.#dependencies.affectedSources(source);
|
|
@@ -86,6 +93,11 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
86
93
|
// complete project-local import closure.
|
|
87
94
|
if (this.affectedSources(normalized).length > 1)
|
|
88
95
|
return false;
|
|
96
|
+
// An already loaded ESM entry has the same revisioned import closure as a
|
|
97
|
+
// capability entry. Its manifest may name plugin.ts or any other filename;
|
|
98
|
+
// Runtime ownership, rather than directory spelling, chooses its reload.
|
|
99
|
+
if (this.#entryRevisions.has(normalized) && isExecutableSource(normalized))
|
|
100
|
+
return false;
|
|
89
101
|
const parts = relative(packageRoot, normalized).split(sep);
|
|
90
102
|
const capability = parts.findIndex((part) => capabilityRoots.has(part));
|
|
91
103
|
if (capability < 0)
|
|
@@ -134,6 +146,11 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
134
146
|
}
|
|
135
147
|
this.#pendingDependencies = undefined;
|
|
136
148
|
this.#dependencies.retain(sources);
|
|
149
|
+
const retained = new Set(sources.map(normalizeSourcePath));
|
|
150
|
+
for (const entry of this.#entryRevisions.keys()) {
|
|
151
|
+
if (!retained.has(entry))
|
|
152
|
+
this.#entryRevisions.delete(entry);
|
|
153
|
+
}
|
|
137
154
|
}
|
|
138
155
|
rollbackGeneration() {
|
|
139
156
|
this.#pendingDependencies = undefined;
|
|
@@ -170,6 +187,7 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
170
187
|
this.#watchers.clear();
|
|
171
188
|
this.#pendingDependencies = undefined;
|
|
172
189
|
this.#dependencies.clear();
|
|
190
|
+
this.#entryRevisions.clear();
|
|
173
191
|
await this.#tsxLoader?.close?.();
|
|
174
192
|
}
|
|
175
193
|
#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.6",
|
|
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",
|
|
@@ -20,24 +20,25 @@
|
|
|
20
20
|
"es-module-lexer": "2.3.1",
|
|
21
21
|
"import-meta-resolve": "4.2.0",
|
|
22
22
|
"semver": "7.8.5",
|
|
23
|
-
"@zhin.js/
|
|
24
|
-
"@zhin.js/
|
|
23
|
+
"@zhin.js/feature-kit": "1.1.4",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.13"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
28
28
|
"tsx": "^4.23.1",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
|
-
"@zhin.js/adapter": "1.1.
|
|
31
|
-
"@zhin.js/agent-feature": "1.1.
|
|
32
|
-
"@zhin.js/command": "1.1.
|
|
33
|
-
"@zhin.js/component": "1.1.
|
|
34
|
-
"@zhin.js/
|
|
35
|
-
"@zhin.js/layout": "1.1.
|
|
36
|
-
"@zhin.js/
|
|
37
|
-
"@zhin.js/middleware": "1.1.
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
30
|
+
"@zhin.js/adapter": "1.1.18",
|
|
31
|
+
"@zhin.js/agent-feature": "1.1.5",
|
|
32
|
+
"@zhin.js/command": "1.1.6",
|
|
33
|
+
"@zhin.js/component": "1.1.5",
|
|
34
|
+
"@zhin.js/handler": "1.1.4",
|
|
35
|
+
"@zhin.js/layout": "1.1.5",
|
|
36
|
+
"@zhin.js/mcp-feature": "1.1.5",
|
|
37
|
+
"@zhin.js/middleware": "1.1.5",
|
|
38
|
+
"@zhin.js/page": "1.1.5",
|
|
39
|
+
"@zhin.js/prompt-section": "1.1.4",
|
|
40
|
+
"@zhin.js/skill": "1.1.5",
|
|
41
|
+
"@zhin.js/tool": "1.1.5"
|
|
41
42
|
},
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|