evolcore 0.0.2 → 0.0.4
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/CHANGELOG.md +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -12,17 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import crypto from 'crypto';
|
|
15
16
|
import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agentRelationConfig, agentDir, agentRelationsDir, agentRolesIndex, agentRoleConfig, } from '../paths.js';
|
|
16
17
|
import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
|
|
17
18
|
import { fileCache } from '../core/daemon-file-cache.js';
|
|
18
19
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
19
|
-
import {
|
|
20
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
21
|
+
import { parseContactAlias } from './contact-alias.js';
|
|
22
|
+
import { loadSchema, listSchemaVersions, currentVersion, isSchemaName, } from './schema-registry.js';
|
|
20
23
|
import { mergeLayers, expandVars, buildEnvResolver } from './merge.js';
|
|
21
24
|
import { normalizeAgentLifecycle } from './lifecycle.js';
|
|
22
25
|
import { isBehaviorConfigFieldPath } from './config-field-policy.js';
|
|
23
26
|
import { clearRoleStoreCache, isRoleName } from './role-store.js';
|
|
24
27
|
import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
|
|
28
|
+
import { mutateContactBookTransactionSync } from './contact-book-store.js';
|
|
25
29
|
import { DAEMON_SCHEMA_NAME } from '../product.js';
|
|
30
|
+
import { validateAccessPolicyDomain } from './access-policy-domain.js';
|
|
26
31
|
export var ConfigTarget;
|
|
27
32
|
(function (ConfigTarget) {
|
|
28
33
|
ConfigTarget["Process"] = "process";
|
|
@@ -34,6 +39,8 @@ export var ConfigTarget;
|
|
|
34
39
|
ConfigTarget["Role"] = "role";
|
|
35
40
|
})(ConfigTarget || (ConfigTarget = {}));
|
|
36
41
|
const configWriteListeners = new Set();
|
|
42
|
+
const CONFIG_MUTATION_LOCK_WAIT_MS = 5_000;
|
|
43
|
+
const CONFIG_MUTATION_LOCK_RETRY_MS = 20;
|
|
37
44
|
/** Observe successful in-process config writes after cache invalidation. */
|
|
38
45
|
export function onConfigWrite(listener) {
|
|
39
46
|
configWriteListeners.add(listener);
|
|
@@ -76,10 +83,7 @@ export function assertRoleConfigV4Ready() {
|
|
|
76
83
|
if (!fs.existsSync(agentsRoot))
|
|
77
84
|
return;
|
|
78
85
|
const issues = [];
|
|
79
|
-
const
|
|
80
|
-
const agentSchemaV4 = loadSchema('agent-config', 4);
|
|
81
|
-
const relationSchema = loadSchema('relation-config');
|
|
82
|
-
const inspect = (file, schema, legacyAgent = false) => {
|
|
86
|
+
const inspect = (file, logicalName, minimumVersion) => {
|
|
83
87
|
let value;
|
|
84
88
|
try {
|
|
85
89
|
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -88,18 +92,25 @@ export function assertRoleConfigV4Ready() {
|
|
|
88
92
|
issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
89
93
|
return;
|
|
90
94
|
}
|
|
91
|
-
const expectedVersion = schema.version;
|
|
92
95
|
const actualVersion = value && typeof value === 'object'
|
|
93
96
|
? value.$schema_version
|
|
94
97
|
: undefined;
|
|
95
|
-
|
|
96
|
-
if (actualVersion !== expectedVersion && !acceptedLegacyAgent) {
|
|
98
|
+
if (typeof actualVersion !== 'number' || actualVersion < minimumVersion) {
|
|
97
99
|
issues.push({
|
|
98
100
|
file,
|
|
99
|
-
detail: `$schema_version must be ${
|
|
101
|
+
detail: `$schema_version must be at least ${minimumVersion}, got ${String(actualVersion)}`,
|
|
100
102
|
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let schema;
|
|
106
|
+
try {
|
|
107
|
+
schema = loadSchema(logicalName, actualVersion);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
issues.push({ file, detail: `unsupported ${logicalName} schema version ${actualVersion}` });
|
|
111
|
+
return;
|
|
101
112
|
}
|
|
102
|
-
if (!schema.validate(value)
|
|
113
|
+
if (!schema.validate(value)) {
|
|
103
114
|
const detail = (schema.validate.errors ?? []).map(error => {
|
|
104
115
|
const extra = typeof error.params?.additionalProperty === 'string'
|
|
105
116
|
? ` (additionalProperty=${error.params.additionalProperty})`
|
|
@@ -128,12 +139,10 @@ export function assertRoleConfigV4Ready() {
|
|
|
128
139
|
const agentDirPath = path.join(agentsRoot, entry.name);
|
|
129
140
|
const agentFile = path.join(agentDirPath, 'config.json');
|
|
130
141
|
if (fs.existsSync(agentFile)) {
|
|
131
|
-
|
|
132
|
-
const legacyVersion = typeof raw?.$schema_version === 'number' && raw.$schema_version <= 4;
|
|
133
|
-
inspect(agentFile, legacyVersion ? agentSchemaV4 : agentSchema, raw?.$schema_version === 4);
|
|
142
|
+
inspect(agentFile, 'agent-config', 4);
|
|
134
143
|
}
|
|
135
144
|
for (const relationFile of walkRelationConfigs(path.join(agentDirPath, 'relations'))) {
|
|
136
|
-
inspect(relationFile,
|
|
145
|
+
inspect(relationFile, 'relation-config', 4);
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
if (!issues.length)
|
|
@@ -156,7 +165,6 @@ export function assertRoleConfigV5Ready() {
|
|
|
156
165
|
if (!fs.existsSync(agentsRoot))
|
|
157
166
|
return;
|
|
158
167
|
const issues = [];
|
|
159
|
-
const agentSchema = loadSchema('agent-config', 5);
|
|
160
168
|
const registrySchema = loadSchema('role-registry');
|
|
161
169
|
const readJson = (file) => {
|
|
162
170
|
try {
|
|
@@ -177,9 +185,22 @@ export function assertRoleConfigV5Ready() {
|
|
|
177
185
|
if (!fs.existsSync(agentFile))
|
|
178
186
|
continue;
|
|
179
187
|
const agent = readJson(agentFile);
|
|
180
|
-
|
|
188
|
+
const agentVersion = agent?.$schema_version;
|
|
189
|
+
let agentSchema = null;
|
|
190
|
+
if (typeof agentVersion === 'number' && agentVersion >= 5) {
|
|
191
|
+
try {
|
|
192
|
+
agentSchema = loadSchema('agent-config', agentVersion);
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
if (!agent || !agentSchema || !agentSchema.validate(agent)) {
|
|
181
197
|
if (agent)
|
|
182
|
-
issues.push({
|
|
198
|
+
issues.push({
|
|
199
|
+
file: agentFile,
|
|
200
|
+
detail: agentSchema
|
|
201
|
+
? schemaErrors(agentSchema)
|
|
202
|
+
: `agent schema version must be supported and >= 5, got ${String(agentVersion)}`,
|
|
203
|
+
});
|
|
183
204
|
continue;
|
|
184
205
|
}
|
|
185
206
|
const registryFile = agentRolesIndex(aid);
|
|
@@ -201,6 +222,195 @@ export function assertRoleConfigV5Ready() {
|
|
|
201
222
|
`node kits/migrations/migrate-role-config-v5.mjs --home ${JSON.stringify(root)}\n` +
|
|
202
223
|
`node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(root)}`, { issues });
|
|
203
224
|
}
|
|
225
|
+
/** Final startup gate for the contact-book authorization migration. */
|
|
226
|
+
export function assertContactBookV2Ready() {
|
|
227
|
+
const root = resolvePaths().root;
|
|
228
|
+
const agentsRoot = resolvePaths().agentsDir;
|
|
229
|
+
if (!fs.existsSync(agentsRoot))
|
|
230
|
+
return;
|
|
231
|
+
const issues = [];
|
|
232
|
+
const supportedSchemas = (name, minimumVersion) => (listSchemaVersions(name)
|
|
233
|
+
.filter(version => version >= minimumVersion)
|
|
234
|
+
.map(version => loadSchema(name, version)));
|
|
235
|
+
const agentSchemas = supportedSchemas('agent-config', 6);
|
|
236
|
+
const contactSchemas = supportedSchemas('contact-book', 2);
|
|
237
|
+
const relationSchemas = supportedSchemas('relation-config', 5);
|
|
238
|
+
const inspect = (file, schemas) => {
|
|
239
|
+
let value;
|
|
240
|
+
try {
|
|
241
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const candidates = Array.isArray(schemas) ? schemas : [schemas];
|
|
248
|
+
if (!candidates.some(schema => schema.validate(value))) {
|
|
249
|
+
const detail = (candidates.at(-1)?.validate.errors ?? [])
|
|
250
|
+
.map(error => `${error.instancePath || '/'} ${error.message}`)
|
|
251
|
+
.join('; ');
|
|
252
|
+
issues.push({ file, detail });
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const walkRelationConfigs = (dir) => {
|
|
256
|
+
if (!fs.existsSync(dir))
|
|
257
|
+
return [];
|
|
258
|
+
const files = [];
|
|
259
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
260
|
+
const full = path.join(dir, entry.name);
|
|
261
|
+
if (entry.isDirectory())
|
|
262
|
+
files.push(...walkRelationConfigs(full));
|
|
263
|
+
else if (entry.isFile() && entry.name === 'config.json')
|
|
264
|
+
files.push(full);
|
|
265
|
+
}
|
|
266
|
+
return files;
|
|
267
|
+
};
|
|
268
|
+
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
269
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
270
|
+
continue;
|
|
271
|
+
const dir = path.join(agentsRoot, entry.name);
|
|
272
|
+
const agentFile = path.join(dir, 'config.json');
|
|
273
|
+
if (!fs.existsSync(agentFile))
|
|
274
|
+
continue;
|
|
275
|
+
inspect(agentFile, agentSchemas);
|
|
276
|
+
const contactFile = path.join(dir, 'contact.json');
|
|
277
|
+
if (fs.existsSync(contactFile)) {
|
|
278
|
+
inspect(contactFile, contactSchemas);
|
|
279
|
+
inspectContactBookDomain(contactFile, agentFile, issues);
|
|
280
|
+
}
|
|
281
|
+
for (const relationFile of walkRelationConfigs(path.join(dir, 'relations'))) {
|
|
282
|
+
inspect(relationFile, relationSchemas);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const correctionReceipt = path.join(root, 'data', 'contact-book-v2-relation-role-authority.json');
|
|
286
|
+
if (!hasContactRoleAuthorityReceipt(correctionReceipt)) {
|
|
287
|
+
const recoverable = findRelationRoleRemovedByExperimentalMigration(root);
|
|
288
|
+
if (recoverable) {
|
|
289
|
+
issues.push({
|
|
290
|
+
file: recoverable,
|
|
291
|
+
detail: 'experimental contact-book migration backup must be reviewed and relation role authority correction recorded',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!issues.length)
|
|
296
|
+
return;
|
|
297
|
+
const shown = issues.slice(0, 20)
|
|
298
|
+
.map(issue => `- ${path.relative(root, issue.file)}: ${issue.detail}`)
|
|
299
|
+
.join('\n');
|
|
300
|
+
const omitted = issues.length > 20 ? `\n- ... ${issues.length - 20} more issue(s)` : '';
|
|
301
|
+
throw new ConfigError('CONTACT_BOOK_V2_MIGRATION_REQUIRED', `Contact book authorization migration is required before daemon startup:\n${shown}${omitted}\n` +
|
|
302
|
+
`node kits/migrations/migrate-contact-book-v2.mjs --home ${JSON.stringify(root)}\n` +
|
|
303
|
+
`node kits/migrations/migrate-contact-book-v2.mjs --apply --home ${JSON.stringify(root)}`, { issues });
|
|
304
|
+
}
|
|
305
|
+
function inspectContactBookDomain(contactFile, agentFile, issues) {
|
|
306
|
+
let contact;
|
|
307
|
+
let agent;
|
|
308
|
+
try {
|
|
309
|
+
contact = JSON.parse(fs.readFileSync(contactFile, 'utf8'));
|
|
310
|
+
agent = JSON.parse(fs.readFileSync(agentFile, 'utf8'));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const owners = new Set(Array.isArray(agent?.owners) ? agent.owners.map(String) : []);
|
|
316
|
+
const configuredChannelKeys = new Set((Array.isArray(agent?.channels) ? agent.channels : [])
|
|
317
|
+
.filter((channel) => channel && typeof channel === 'object' && channel.type !== 'aun')
|
|
318
|
+
.map((channel) => {
|
|
319
|
+
const type = String(channel.type || '').trim().toLowerCase();
|
|
320
|
+
const name = String(channel.name || '').trim();
|
|
321
|
+
return type && name ? `${type}#${String(agent?.aid || '').trim()}#${name}` : '';
|
|
322
|
+
})
|
|
323
|
+
.filter(Boolean));
|
|
324
|
+
const aliases = new Map();
|
|
325
|
+
for (const [primaryId, entry] of Object.entries(contact?.contacts ?? {})) {
|
|
326
|
+
if (isExplicitGroupId(primaryId)) {
|
|
327
|
+
issues.push({ file: contactFile, detail: `group contact ${primaryId} is not supported` });
|
|
328
|
+
}
|
|
329
|
+
if (entry?.status === 'blocked' && owners.has(primaryId)) {
|
|
330
|
+
issues.push({ file: contactFile, detail: `owner contact ${primaryId} cannot be blocked` });
|
|
331
|
+
}
|
|
332
|
+
for (const alias of Array.isArray(entry?.aliases) ? entry.aliases : []) {
|
|
333
|
+
const parsed = parseContactAlias(alias, agent?.aid);
|
|
334
|
+
if (!parsed) {
|
|
335
|
+
issues.push({ file: contactFile, detail: `invalid Contact alias ${String(alias)}; expected <channelKey>:<urlEncode(peerId)>` });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!configuredChannelKeys.has(parsed.channelKey)) {
|
|
339
|
+
issues.push({ file: contactFile, detail: `alias ${alias} references an unconfigured channel instance` });
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const previous = aliases.get(parsed.alias);
|
|
343
|
+
if (previous && previous !== primaryId) {
|
|
344
|
+
issues.push({ file: contactFile, detail: `alias ${parsed.alias} is assigned to both ${previous} and ${primaryId}` });
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
aliases.set(parsed.alias, primaryId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function hasContactRoleAuthorityReceipt(file) {
|
|
353
|
+
try {
|
|
354
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
355
|
+
return value?.migration === 'contact-book-v2-relation-role-authority' && value?.designVersion === 2;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function findRelationRoleRemovedByExperimentalMigration(root) {
|
|
362
|
+
const backupsRoot = path.join(root, 'backups', 'contact-book-v2-migration');
|
|
363
|
+
if (!fs.existsSync(backupsRoot))
|
|
364
|
+
return null;
|
|
365
|
+
for (const stamp of fs.readdirSync(backupsRoot, { withFileTypes: true })) {
|
|
366
|
+
if (!stamp.isDirectory())
|
|
367
|
+
continue;
|
|
368
|
+
try {
|
|
369
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(backupsRoot, stamp.name, 'manifest.json'), 'utf8'));
|
|
370
|
+
if (manifest?.designVersion >= 2)
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
catch { }
|
|
374
|
+
const agentsRoot = path.join(backupsRoot, stamp.name, 'files', 'agents');
|
|
375
|
+
if (!fs.existsSync(agentsRoot))
|
|
376
|
+
continue;
|
|
377
|
+
for (const agent of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
378
|
+
if (!agent.isDirectory() || !isValidAid(agent.name))
|
|
379
|
+
continue;
|
|
380
|
+
const relationsRoot = path.join(agentsRoot, agent.name, 'relations');
|
|
381
|
+
if (!fs.existsSync(relationsRoot))
|
|
382
|
+
continue;
|
|
383
|
+
for (const relation of fs.readdirSync(relationsRoot, { withFileTypes: true })) {
|
|
384
|
+
if (!relation.isDirectory() || relation.name.startsWith('_'))
|
|
385
|
+
continue;
|
|
386
|
+
const backupFile = path.join(relationsRoot, relation.name, 'config.json');
|
|
387
|
+
if (!fs.existsSync(backupFile))
|
|
388
|
+
continue;
|
|
389
|
+
let backupValue;
|
|
390
|
+
try {
|
|
391
|
+
backupValue = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (typeof backupValue?.role !== 'string' || backupValue.role === 'owner')
|
|
397
|
+
continue;
|
|
398
|
+
const currentFile = path.join(root, 'agents', agent.name, 'relations', relation.name, 'config.json');
|
|
399
|
+
let currentValue;
|
|
400
|
+
try {
|
|
401
|
+
currentValue = JSON.parse(fs.readFileSync(currentFile, 'utf8'));
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (typeof currentValue?.$schema_version === 'number' && currentValue.$schema_version >= 5) {
|
|
407
|
+
return currentFile;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
204
414
|
/**
|
|
205
415
|
* 启动时检查 schema 版本(daemon 启动时调用)。
|
|
206
416
|
* 扫描所有配置文件,如有版本不匹配则警告一次。
|
|
@@ -420,7 +630,12 @@ export function write(target, value, sel, opts = {}) {
|
|
|
420
630
|
finishConfigWrite(target, sel, file);
|
|
421
631
|
return;
|
|
422
632
|
}
|
|
423
|
-
const
|
|
633
|
+
const requestedVersion = value && typeof value === 'object'
|
|
634
|
+
? Number(value.$schema_version)
|
|
635
|
+
: NaN;
|
|
636
|
+
const schema = target === ConfigTarget.Contact && requestedVersion === 2
|
|
637
|
+
? loadSchema('contact-book', 2)
|
|
638
|
+
: loadSchema(TARGET_SCHEMA[target]);
|
|
424
639
|
const withVer = ensureSchemaVersion(value, schema.version);
|
|
425
640
|
const migrated = withVer;
|
|
426
641
|
// Agent config 写入规范化(aid 校验、projects 字段清理)
|
|
@@ -434,15 +649,166 @@ export function write(target, value, sel, opts = {}) {
|
|
|
434
649
|
validateOrThrow(schema, normalized, target);
|
|
435
650
|
// 1b. responseModeParams 桶内容校验:主 schema 把 responseModeParams 当黑箱,
|
|
436
651
|
// 此处使用“模式桶 schema”逐桶校验内容(桶键 = 已登记的响应模式 id)。
|
|
437
|
-
if (target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
|
|
652
|
+
if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
|
|
438
653
|
validateResponseModeParams(normalized?.responseModeParams, target);
|
|
439
654
|
}
|
|
655
|
+
if (target === ConfigTarget.Agent) {
|
|
656
|
+
const selfAid = sel?.self || String(normalized?.aid || '').trim();
|
|
657
|
+
const issue = validateAccessPolicyDomain(selfAid, normalized);
|
|
658
|
+
if (issue)
|
|
659
|
+
throw new ConfigError(issue.code, issue.message);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Owner 是实际联系人。将 Agent 配置和缺失的 Owner 联系人写入同一可恢复事务,
|
|
663
|
+
// 避免渠道身份绑定承担联系人创建职责。
|
|
664
|
+
const ownerIds = target === ConfigTarget.Agent && Array.isArray(normalized.owners)
|
|
665
|
+
? [...new Set(normalized.owners
|
|
666
|
+
.map((value) => String(value || '').trim())
|
|
667
|
+
.filter((value) => isValidAid(value)))]
|
|
668
|
+
: [];
|
|
669
|
+
if (target === ConfigTarget.Agent && sel?.self && ownerIds.length > 0) {
|
|
670
|
+
const before = readTextOrNull(file);
|
|
671
|
+
const after = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
672
|
+
mutateContactBookTransactionSync({
|
|
673
|
+
selfAid: sel.self,
|
|
674
|
+
actor: 'agent-owner-sync',
|
|
675
|
+
mutation: { type: 'ensure-contacts', primaryIds: ownerIds },
|
|
676
|
+
}, [{ file, before, after }]);
|
|
677
|
+
finishConfigWrite(target, sel, file);
|
|
678
|
+
return;
|
|
440
679
|
}
|
|
441
680
|
// 2. 写入文件
|
|
442
681
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
443
682
|
atomicWriteJson(file, normalized);
|
|
444
683
|
finishConfigWrite(target, sel, file);
|
|
445
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Mutate one configuration document while holding a file-scoped, cross-process lock.
|
|
687
|
+
*
|
|
688
|
+
* Callers must keep the callback synchronous. This is the safe primitive for
|
|
689
|
+
* read-modify-write paths; direct `read()` followed by `write()` cannot protect
|
|
690
|
+
* unrelated fields from another process writing the same config document.
|
|
691
|
+
*/
|
|
692
|
+
export function mutateConfig(target, sel, mutate) {
|
|
693
|
+
if (target === ConfigTarget.RoleRegistry || target === ConfigTarget.Role || target === ConfigTarget.Contact) {
|
|
694
|
+
throw new ConfigError('MUTATION_UNSUPPORTED', `mutateConfig does not support ${target}`);
|
|
695
|
+
}
|
|
696
|
+
const file = targetPath(target, sel);
|
|
697
|
+
const release = acquireConfigMutationLock(file);
|
|
698
|
+
try {
|
|
699
|
+
const current = read(target, sel, { cache: false });
|
|
700
|
+
const next = current ? cloneConfigValue(current) : configSkeleton(target, sel);
|
|
701
|
+
const before = stableConfigValue(current ?? next);
|
|
702
|
+
const value = mutate(next);
|
|
703
|
+
const changed = before !== stableConfigValue(next);
|
|
704
|
+
if (changed)
|
|
705
|
+
write(target, next, sel);
|
|
706
|
+
return { value, changed };
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
release();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function cloneConfigValue(value) {
|
|
713
|
+
return JSON.parse(JSON.stringify(value));
|
|
714
|
+
}
|
|
715
|
+
function stableConfigValue(value) {
|
|
716
|
+
return JSON.stringify(sortConfigValue(value));
|
|
717
|
+
}
|
|
718
|
+
function sortConfigValue(value) {
|
|
719
|
+
if (Array.isArray(value))
|
|
720
|
+
return value.map(sortConfigValue);
|
|
721
|
+
if (!value || typeof value !== 'object')
|
|
722
|
+
return value;
|
|
723
|
+
return Object.fromEntries(Object.entries(value)
|
|
724
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
725
|
+
.map(([key, nested]) => [key, sortConfigValue(nested)]));
|
|
726
|
+
}
|
|
727
|
+
function acquireConfigMutationLock(file) {
|
|
728
|
+
const lockDir = `${file}.mutation.lock`;
|
|
729
|
+
const ownerFile = path.join(lockDir, 'owner.json');
|
|
730
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
731
|
+
const candidate = `${lockDir}.candidate-${process.pid}-${token}`;
|
|
732
|
+
const candidateOwner = path.join(candidate, 'owner.json');
|
|
733
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
734
|
+
fs.mkdirSync(candidate);
|
|
735
|
+
fs.writeFileSync(candidateOwner, `${JSON.stringify({ pid: process.pid, token })}\n`, { mode: 0o600 });
|
|
736
|
+
let acquired = false;
|
|
737
|
+
const deadline = Date.now() + CONFIG_MUTATION_LOCK_WAIT_MS;
|
|
738
|
+
try {
|
|
739
|
+
while (true) {
|
|
740
|
+
try {
|
|
741
|
+
fs.renameSync(candidate, lockDir);
|
|
742
|
+
acquired = true;
|
|
743
|
+
return () => releaseConfigMutationLock(lockDir, ownerFile, token);
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
|
|
747
|
+
throw error;
|
|
748
|
+
if (reclaimStaleConfigMutationLock(lockDir, ownerFile))
|
|
749
|
+
continue;
|
|
750
|
+
if (Date.now() >= deadline) {
|
|
751
|
+
throw new ConfigError('LOCK_TIMEOUT', `Timed out waiting for config mutation lock: ${file}`);
|
|
752
|
+
}
|
|
753
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, CONFIG_MUTATION_LOCK_RETRY_MS);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
if (!acquired)
|
|
759
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
|
|
763
|
+
let raw;
|
|
764
|
+
let pid;
|
|
765
|
+
try {
|
|
766
|
+
raw = fs.readFileSync(ownerFile, 'utf8');
|
|
767
|
+
pid = Number(JSON.parse(raw).pid);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
773
|
+
return false;
|
|
774
|
+
try {
|
|
775
|
+
process.kill(pid, 0);
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
if (error?.code !== 'ESRCH')
|
|
780
|
+
return false;
|
|
781
|
+
const reclaimed = `${lockDir}.reclaimed-${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
|
|
782
|
+
try {
|
|
783
|
+
fs.renameSync(lockDir, reclaimed);
|
|
784
|
+
fs.rmSync(reclaimed, { recursive: true, force: true });
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
catch (renameError) {
|
|
788
|
+
return renameError?.code === 'ENOENT';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function releaseConfigMutationLock(lockDir, ownerFile, token) {
|
|
793
|
+
try {
|
|
794
|
+
const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
|
|
795
|
+
if (owner.token === token)
|
|
796
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
// A replaced lock is not ours to release.
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function readTextOrNull(file) {
|
|
803
|
+
try {
|
|
804
|
+
return fs.readFileSync(file, 'utf8');
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
if (error?.code === 'ENOENT')
|
|
808
|
+
return null;
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
446
812
|
function finishConfigWrite(target, sel, file) {
|
|
447
813
|
if (fileCacheAvailable())
|
|
448
814
|
fileCache.invalidate(file);
|
|
@@ -459,6 +825,10 @@ function finishConfigWrite(target, sel, file) {
|
|
|
459
825
|
}
|
|
460
826
|
}
|
|
461
827
|
}
|
|
828
|
+
/** Notify cache and security listeners after a caller-owned atomic transaction commits. */
|
|
829
|
+
export function notifyConfigWrite(target, sel) {
|
|
830
|
+
finishConfigWrite(target, sel, targetPath(target, sel));
|
|
831
|
+
}
|
|
462
832
|
function ensureSchemaVersion(value, version) {
|
|
463
833
|
return value && typeof value === 'object'
|
|
464
834
|
? { ...value, $schema_version: version }
|
|
@@ -483,10 +853,9 @@ function validateOrThrow(schema, value, target) {
|
|
|
483
853
|
function validateProcessOnlyFields(target, value) {
|
|
484
854
|
if (target === ConfigTarget.Process || !value || typeof value !== 'object')
|
|
485
855
|
return [];
|
|
486
|
-
|
|
487
|
-
if (!debug || typeof debug !== 'object' || !Object.prototype.hasOwnProperty.call(debug, 'eckSnapshots'))
|
|
856
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'debug'))
|
|
488
857
|
return [];
|
|
489
|
-
return ['/debug
|
|
858
|
+
return ['/debug is only allowed in process config'];
|
|
490
859
|
}
|
|
491
860
|
/**
|
|
492
861
|
* responseModeParams 桶专项校验。
|
|
@@ -531,7 +900,12 @@ function validateResponseModeParams(params, target) {
|
|
|
531
900
|
* 供 config-store.validateAgentConfig 等只校验不落盘的场景使用。
|
|
532
901
|
*/
|
|
533
902
|
export function validateConfig(target, value) {
|
|
534
|
-
const
|
|
903
|
+
const requestedVersion = value && typeof value === 'object'
|
|
904
|
+
? Number(value.$schema_version)
|
|
905
|
+
: NaN;
|
|
906
|
+
const schema = target === ConfigTarget.Contact && requestedVersion === 2
|
|
907
|
+
? loadSchema('contact-book', 2)
|
|
908
|
+
: loadSchema(TARGET_SCHEMA[target]);
|
|
535
909
|
const withVer = ensureSchemaVersion(value, schema.version);
|
|
536
910
|
const ok = schema.validate(withVer);
|
|
537
911
|
const errors = validateProcessOnlyFields(target, withVer);
|
|
@@ -567,6 +941,11 @@ export function ensureFile(target, sel) {
|
|
|
567
941
|
const file = targetPath(target, sel);
|
|
568
942
|
if (fs.existsSync(file))
|
|
569
943
|
return;
|
|
944
|
+
const skeleton = configSkeleton(target, sel);
|
|
945
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
946
|
+
atomicWriteJson(file, skeleton);
|
|
947
|
+
}
|
|
948
|
+
function configSkeleton(target, sel) {
|
|
570
949
|
const schema = loadSchema(TARGET_SCHEMA[target]);
|
|
571
950
|
const skeleton = { $schema_version: schema.version };
|
|
572
951
|
const props = schema.raw.properties || {};
|
|
@@ -586,8 +965,7 @@ export function ensureFile(target, sel) {
|
|
|
586
965
|
else if (spec?.type === 'object')
|
|
587
966
|
skeleton[key] = {};
|
|
588
967
|
}
|
|
589
|
-
|
|
590
|
-
atomicWriteJson(file, skeleton);
|
|
968
|
+
return skeleton;
|
|
591
969
|
}
|
|
592
970
|
// ── schema 版本迁移 ───────────────────────────────────────────────────────
|
|
593
971
|
//
|
|
@@ -618,9 +996,11 @@ export function resolveAgentConfig(sel, opts = {}) {
|
|
|
618
996
|
function stripStaticOwnerField(config) {
|
|
619
997
|
if (!config)
|
|
620
998
|
return config;
|
|
621
|
-
if (!Object.prototype.hasOwnProperty.call(config, 'owners')
|
|
999
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'owners')
|
|
1000
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'admins')
|
|
1001
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'access'))
|
|
622
1002
|
return config;
|
|
623
|
-
const { owners: _owners, admins: _admins, ...rest } = config;
|
|
1003
|
+
const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
|
|
624
1004
|
return rest;
|
|
625
1005
|
}
|
|
626
1006
|
function stripRelationRoleData(config) {
|
|
@@ -643,13 +1023,11 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
643
1023
|
lifecycle: config.lifecycle,
|
|
644
1024
|
initialized: config.initialized,
|
|
645
1025
|
owners: config.owners,
|
|
646
|
-
admins: config.admins,
|
|
647
1026
|
aun: config.aun,
|
|
648
1027
|
channels: config.channels ?? [],
|
|
649
1028
|
models: config.models,
|
|
650
1029
|
projects: config.projects,
|
|
651
1030
|
capabilities: config.capabilities,
|
|
652
|
-
debug: config.debug,
|
|
653
1031
|
observable: config.observable,
|
|
654
1032
|
extra_backup: config.extra_backup,
|
|
655
1033
|
// Runtime configuration parameters
|
|
@@ -664,6 +1042,7 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
664
1042
|
debounce: config.debounce,
|
|
665
1043
|
show_activities: config.show_activities,
|
|
666
1044
|
session_renew: config.session_renew,
|
|
1045
|
+
groupRules: config.groupRules,
|
|
667
1046
|
render: config.render,
|
|
668
1047
|
sessionManifests: config.sessionManifests,
|
|
669
1048
|
enable_rich_content: config.enable_rich_content,
|
|
@@ -691,6 +1070,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
|
|
|
691
1070
|
'debounce',
|
|
692
1071
|
'show_activities',
|
|
693
1072
|
'session_renew',
|
|
1073
|
+
'groupRules',
|
|
694
1074
|
'render',
|
|
695
1075
|
'sessionManifests',
|
|
696
1076
|
'enable_rich_content',
|