evolcore 0.0.25 → 0.0.26

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.
Files changed (231) hide show
  1. package/CHANGELOG.md +607 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +160 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent.js +96 -46
  10. package/dist/agents/claude-runner.js +50 -12
  11. package/dist/agents/codex/app-server-client.js +13 -5
  12. package/dist/agents/codex/catalog.js +1 -1
  13. package/dist/agents/codex/diagnostics.js +34 -1
  14. package/dist/agents/codex-runner.js +121 -38
  15. package/dist/agents/ecagent-runner.js +4 -9
  16. package/dist/agents/gemini-runner.js +2 -7
  17. package/dist/agents/runner-types.js +25 -17
  18. package/dist/aun/aid/agentmd.js +81 -8
  19. package/dist/aun/aid/client.js +1 -1
  20. package/dist/aun/aid/identity.js +1 -1
  21. package/dist/aun/aid/index.js +1 -1
  22. package/dist/aun/aid/store.js +17 -13
  23. package/dist/aun/menu-client.js +74 -0
  24. package/dist/aun/msg/group.js +1 -1
  25. package/dist/aun/msg/managed-operation.js +1 -1
  26. package/dist/aun/msg/p2p.js +6 -1
  27. package/dist/aun/msg/upload.js +30 -22
  28. package/dist/channels/aun.js +329 -94
  29. package/dist/cli/agent-command.js +105 -84
  30. package/dist/cli/agent-create.js +112 -10
  31. package/dist/cli/agent.js +16 -5
  32. package/dist/cli/aun-commands.js +297 -296
  33. package/dist/cli/bench.js +20 -20
  34. package/dist/cli/config.js +47 -28
  35. package/dist/cli/contact.js +70 -13
  36. package/dist/cli/ctl-command.js +22 -22
  37. package/dist/cli/daemon-commands.js +105 -40
  38. package/dist/cli/diag.js +31 -0
  39. package/dist/cli/fs-command.js +67 -67
  40. package/dist/cli/handoff-command.js +41 -9
  41. package/dist/cli/index.js +83 -77
  42. package/dist/cli/link-rules.js +21 -21
  43. package/dist/cli/managed-command-guard.js +3 -0
  44. package/dist/cli/menu-client.js +46 -31
  45. package/dist/cli/net-check.js +22 -22
  46. package/dist/cli/queue-command.js +29 -29
  47. package/dist/cli/response.js +30 -30
  48. package/dist/cli/restart-monitor.js +87 -40
  49. package/dist/cli/stats.js +51 -51
  50. package/dist/cli/trigger-command.js +56 -56
  51. package/dist/config/builtin-roles.js +1 -0
  52. package/dist/config/config-operation-service.js +145 -0
  53. package/dist/config/contact-operation-service.js +96 -7
  54. package/dist/config/resolved-config-op.js +27 -7
  55. package/dist/core/agent-application-service.js +2 -1
  56. package/dist/core/auth/authorization-audit.js +20 -12
  57. package/dist/core/auth/operation-authorizer.js +25 -4
  58. package/dist/core/auth/operation-catalog.js +35 -4
  59. package/dist/core/auth/remote-access-store.js +40 -1
  60. package/dist/core/bootstrap-service.js +1 -1
  61. package/dist/core/command/command-handler.js +83 -26
  62. package/dist/core/command/diag-service.js +118 -0
  63. package/dist/core/command/menu-catalog.js +12 -3
  64. package/dist/core/command/menu-handler.js +47 -26
  65. package/dist/core/command/menu-protocol.js +5 -0
  66. package/dist/core/command/menu-token-store.js +41 -8
  67. package/dist/core/command/slash-gate.js +34 -4
  68. package/dist/core/command/slash-handler.js +86 -21
  69. package/dist/core/handoff/runtime.js +205 -27
  70. package/dist/core/handoff/store.js +193 -4
  71. package/dist/core/message/im-renderer.js +72 -21
  72. package/dist/core/message/message-bridge.js +69 -9
  73. package/dist/core/message/message-queue.js +26 -0
  74. package/dist/core/message/message-utils.js +1 -0
  75. package/dist/core/message/response-engine.js +47 -28
  76. package/dist/core/model/catalog-service.js +79 -49
  77. package/dist/core/model/context-window-suffix.js +33 -0
  78. package/dist/core/model/model-catalog.js +1 -1
  79. package/dist/core/model/model-permission.js +12 -3
  80. package/dist/core/permission/ec-command-parser.js +51 -12
  81. package/dist/core/permission/sandbox-runtime.js +36 -14
  82. package/dist/core/permission/tool-policy.js +2 -1
  83. package/dist/index.js +280 -163
  84. package/dist/ipc.js +54 -2
  85. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  86. package/dist/stats/billing.js +41 -15
  87. package/dist/stats/db.js +159 -159
  88. package/dist/stats/eck-vars.js +9 -9
  89. package/dist/stats/price-resolver.js +28 -10
  90. package/dist/stats/query.js +139 -139
  91. package/dist/stats/role-budget.js +6 -6
  92. package/dist/stats/writer.js +51 -51
  93. package/dist/utils/cross-platform.js +30 -1
  94. package/dist/utils/error-dict.json +160 -160
  95. package/dist/utils/error-utils.js +21 -0
  96. package/dist/utils/evolcore-version.js +129 -0
  97. package/dist/utils/logger.js +3 -1
  98. package/dist/utils/macos-autostart.js +22 -22
  99. package/dist/utils/model-prices.jsonl +23 -23
  100. package/dist/utils/npm-ops.js +176 -35
  101. package/dist/utils/welcome.js +22 -22
  102. package/ecagent/LICENSE +21 -21
  103. package/ecagent/NOTICE +11 -11
  104. package/ecagent/README.md +61 -61
  105. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  106. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  107. package/ecagent/dist/harness/messages.js +7 -7
  108. package/kits/docs/GUIDE.md +20 -20
  109. package/kits/docs/INDEX.md +66 -66
  110. package/kits/docs/aun/CHEATSHEET.md +19 -19
  111. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  112. package/kits/docs/channels/aun.md +56 -56
  113. package/kits/docs/channels/feishu.md +56 -56
  114. package/kits/docs/context-assembly.md +364 -364
  115. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  116. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  117. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  118. package/kits/docs/eck_templates/runtime.template.md +19 -19
  119. package/kits/docs/evolcore/INDEX.md +71 -71
  120. package/kits/docs/evolcore/agent-create.md +170 -165
  121. package/kits/docs/evolcore/agent.md +66 -66
  122. package/kits/docs/evolcore/aid.md +60 -60
  123. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  124. package/kits/docs/evolcore/config.md +202 -195
  125. package/kits/docs/evolcore/contact.md +75 -68
  126. package/kits/docs/evolcore/ctl.md +46 -46
  127. package/kits/docs/evolcore/event.md +216 -216
  128. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  129. package/kits/docs/evolcore/fs.md +110 -110
  130. package/kits/docs/evolcore/group-collaboration.md +243 -243
  131. package/kits/docs/evolcore/group-fs.md +17 -17
  132. package/kits/docs/evolcore/group-rules.md +251 -251
  133. package/kits/docs/evolcore/group.md +152 -152
  134. package/kits/docs/evolcore/menu-client.md +42 -34
  135. package/kits/docs/evolcore/model.md +50 -50
  136. package/kits/docs/evolcore/msg.md +166 -165
  137. package/kits/docs/evolcore/response.md +75 -75
  138. package/kits/docs/evolcore/rpc.md +37 -37
  139. package/kits/docs/evolcore/self-summary.md +29 -29
  140. package/kits/docs/evolcore/stats.md +83 -83
  141. package/kits/docs/evolcore/storage.md +50 -50
  142. package/kits/docs/evolcore/trigger.md +575 -575
  143. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  144. package/kits/docs/identity/PATH_OPS.md +16 -16
  145. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  146. package/kits/docs/identity/identity-tools.md +26 -26
  147. package/kits/docs/path-registry.md +62 -62
  148. package/kits/docs/prompt-loading-architecture.md +269 -269
  149. package/kits/docs/venues/aun-group.md +45 -45
  150. package/kits/docs/venues/aun-private.md +10 -10
  151. package/kits/docs/venues/client-desktop.md +10 -10
  152. package/kits/docs/venues/client-mobile.md +10 -10
  153. package/kits/docs/venues/feishu-group.md +13 -13
  154. package/kits/docs/venues/feishu-private.md +9 -9
  155. package/kits/docs/venues/group.md +37 -25
  156. package/kits/docs/venues/private.md +10 -10
  157. package/kits/eck_manifest.auxiliary.json +43 -43
  158. package/kits/eck_manifest.json +190 -190
  159. package/kits/eck_message_manifest.json +74 -74
  160. package/kits/migrations/README-role-config-v4.md +32 -32
  161. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  162. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  163. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  164. package/kits/migrations/rename-config-file.mjs +99 -99
  165. package/kits/rules/01-overview.md +162 -162
  166. package/kits/rules/02-navigation.md +96 -96
  167. package/kits/rules/03-identity.md +36 -36
  168. package/kits/rules/04-relation.md +75 -75
  169. package/kits/rules/05-venue.md +60 -60
  170. package/kits/rules/06-channel.md +63 -63
  171. package/kits/schemas/_meta.json +53 -53
  172. package/kits/schemas/agent-config.schema.1.json +177 -177
  173. package/kits/schemas/agent-config.schema.10.json +395 -395
  174. package/kits/schemas/agent-config.schema.11.json +421 -421
  175. package/kits/schemas/agent-config.schema.12.json +427 -427
  176. package/kits/schemas/agent-config.schema.2.json +239 -239
  177. package/kits/schemas/agent-config.schema.3.json +119 -119
  178. package/kits/schemas/agent-config.schema.4.json +208 -208
  179. package/kits/schemas/agent-config.schema.5.json +326 -326
  180. package/kits/schemas/agent-config.schema.6.json +322 -322
  181. package/kits/schemas/agent-config.schema.7.json +303 -303
  182. package/kits/schemas/agent-config.schema.8.json +304 -304
  183. package/kits/schemas/agent-config.schema.9.json +364 -364
  184. package/kits/schemas/contact-book.schema.1.json +36 -36
  185. package/kits/schemas/contact-book.schema.2.json +43 -43
  186. package/kits/schemas/contact-book.schema.3.json +67 -67
  187. package/kits/schemas/daemon.schema.1.json +91 -91
  188. package/kits/schemas/daemon.schema.2.json +101 -101
  189. package/kits/schemas/daemon.schema.3.json +123 -123
  190. package/kits/schemas/daemon.schema.4.json +136 -136
  191. package/kits/schemas/daemon.schema.5.json +135 -135
  192. package/kits/schemas/daemon.schema.6.json +131 -131
  193. package/kits/schemas/defaults.schema.1.json +81 -81
  194. package/kits/schemas/defaults.schema.2.json +85 -85
  195. package/kits/schemas/defaults.schema.3.json +73 -73
  196. package/kits/schemas/defaults.schema.4.json +107 -107
  197. package/kits/schemas/defaults.schema.5.json +119 -119
  198. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  199. package/kits/schemas/migrations/README.md +30 -30
  200. package/kits/schemas/relation-config.schema.1.json +158 -158
  201. package/kits/schemas/relation-config.schema.2.json +73 -73
  202. package/kits/schemas/relation-config.schema.3.json +50 -50
  203. package/kits/schemas/relation-config.schema.4.json +47 -47
  204. package/kits/schemas/relation-config.schema.5.json +47 -47
  205. package/kits/schemas/relation-config.schema.6.json +46 -46
  206. package/kits/schemas/relation-config.schema.7.json +59 -59
  207. package/kits/schemas/relation-config.schema.8.json +103 -103
  208. package/kits/schemas/role-config.schema.1.json +201 -201
  209. package/kits/schemas/role-registry.schema.1.json +35 -35
  210. package/kits/schemas/single-session.schema.1.json +31 -31
  211. package/kits/schemas/single-session.schema.2.json +57 -57
  212. package/kits/schemas/single-session.schema.3.json +32 -32
  213. package/kits/templates/bootstrap-welcome.md +17 -17
  214. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  215. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  216. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  217. package/kits/templates/message-fragments/inject-default.md +2 -2
  218. package/kits/templates/message-fragments/item.md +2 -2
  219. package/kits/templates/roles/admin.json +92 -92
  220. package/kits/templates/roles/member.json +96 -96
  221. package/kits/templates/roles/owner.json +9 -9
  222. package/kits/templates/roles/visitor.json +85 -85
  223. package/kits/templates/system-fragments/baseagent.md +14 -14
  224. package/kits/templates/system-fragments/bootstrap.md +31 -29
  225. package/kits/templates/system-fragments/channel.md +64 -64
  226. package/kits/templates/system-fragments/commands.md +28 -28
  227. package/kits/templates/system-fragments/identity.md +13 -13
  228. package/kits/templates/system-fragments/relation.md +19 -19
  229. package/kits/templates/system-fragments/session.md +59 -59
  230. package/kits/templates/system-fragments/venue.md +31 -31
  231. package/package.json +100 -98
@@ -1,454 +1,454 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import Ajv from 'ajv';
6
-
7
- const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
8
- const SCHEMA_DIR = path.resolve(SCRIPT_DIR, '..', 'schemas');
9
- const PERMISSION_MODES = new Set(['readonly', 'auto', 'request', 'bypass']);
10
- const MODEL_FIELD_RE = /^baseagents\.[a-z0-9_-]+\.model$/;
11
- const ROLE_ENUM_FIELD_VALUES = {
12
- 'chatmode.private': new Set(['interactive', 'proactive']),
13
- 'chatmode.group': new Set(['interactive', 'proactive']),
14
- 'baseagents.claude.effort': new Set(['auto', 'low', 'medium', 'high', 'xhigh', 'max']),
15
- 'baseagents.codex.reasoning': new Set(['auto', 'low', 'medium', 'high', 'xhigh']),
16
- };
17
- const BUILTIN_ROLES = new Set(['member', 'visitor']);
18
- const ROLE_POLICY_KEYS = new Set(['defaultRoles', 'definitions']);
19
-
20
- function usage() {
21
- return `Usage:
22
- node kits/migrations/migrate-role-config-v4.mjs [--home <EVOLCORE_HOME>] [--json]
23
- node kits/migrations/migrate-role-config-v4.mjs --apply [--home <EVOLCORE_HOME>] [--json]
24
-
25
- The default mode is dry-run. Stop the EvolCore daemon before running this migration.
26
- Every applied migration creates a backup under:
27
- <EVOLCORE_HOME>/backups/role-v4-migration/<timestamp>/`;
28
- }
29
-
30
- function parseArgs(argv) {
31
- const options = { apply: false, json: false, home: undefined };
32
- for (let i = 0; i < argv.length; i += 1) {
33
- const arg = argv[i];
34
- if (arg === '--apply') options.apply = true;
35
- else if (arg === '--json') options.json = true;
36
- else if (arg === '--help' || arg === '-h') options.help = true;
37
- else if (arg === '--home') {
38
- const value = argv[++i];
39
- if (!value) throw new Error('--home requires a path');
40
- options.home = value;
41
- } else {
42
- throw new Error(`Unknown argument: ${arg}`);
43
- }
44
- }
45
- return options;
46
- }
47
-
48
- function resolveHome(explicitHome) {
49
- if (explicitHome) return path.resolve(explicitHome);
50
- if (process.env.EVOLCORE_HOME) return path.resolve(process.env.EVOLCORE_HOME);
51
- if (fs.existsSync(path.join(process.cwd(), 'agents'))) return process.cwd();
52
- return path.join(os.homedir(), '.evolcore');
53
- }
54
-
55
- function loadValidators() {
56
- const ajv = new Ajv({ allErrors: true, strict: false });
57
- const load = name => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, name), 'utf8'));
58
- return {
59
- agent: ajv.compile(load('agent-config.schema.4.json')),
60
- relation: ajv.compile(load('relation-config.schema.4.json')),
61
- };
62
- }
63
-
64
- function isRecord(value) {
65
- return !!value && typeof value === 'object' && !Array.isArray(value);
66
- }
67
-
68
- function isValidAid(name) {
69
- const labels = name.split('.');
70
- return labels.length >= 3
71
- && labels.every(label => /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label));
72
- }
73
-
74
- function clone(value) {
75
- return JSON.parse(JSON.stringify(value));
76
- }
77
-
78
- function normalizePermissionMode(value) {
79
- if (typeof value !== 'string') return null;
80
- if (PERMISSION_MODES.has(value)) return value;
81
- if (value === 'edit') return 'request';
82
- if (value === 'plan' || value === 'noask') return 'readonly';
83
- return null;
84
- }
85
-
86
- function wildcardMatch(pattern, value) {
87
- return pattern === '*' || (pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : pattern === value);
88
- }
89
-
90
- function migratePermissionModePolicy(value, fallback) {
91
- const policy = isRecord(value) ? value : {};
92
- const defaultValue = normalizePermissionMode(policy.default) ?? fallback;
93
- const allowedValues = Array.isArray(policy.allowedValues)
94
- ? [...new Set(policy.allowedValues.map(normalizePermissionMode).filter(Boolean))]
95
- : [];
96
- if (allowedValues.length && !allowedValues.includes(defaultValue)) allowedValues.unshift(defaultValue);
97
- return {
98
- default: defaultValue,
99
- allowOverride: false,
100
- ...(allowedValues.length ? { allowedValues } : {}),
101
- ...(typeof policy.reason === 'string' ? { reason: policy.reason } : {}),
102
- };
103
- }
104
-
105
- function migrateModelPolicy(field, value) {
106
- if (!isRecord(value)) throw new Error(`${field} must be an object`);
107
- const defaultModel = typeof value.default === 'string' ? value.default.trim() : '';
108
- if (!defaultModel) throw new Error(`${field}.default must be a non-empty string`);
109
- const allowedModels = Array.isArray(value.allowedModels)
110
- ? [...new Set(value.allowedModels.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim()))]
111
- : [];
112
- if (!allowedModels.some(pattern => wildcardMatch(pattern, defaultModel))) allowedModels.unshift(defaultModel);
113
- return {
114
- default: defaultModel,
115
- allowOverride: typeof value.allowOverride === 'boolean' ? value.allowOverride : false,
116
- allowedModels,
117
- ...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
118
- };
119
- }
120
-
121
- function migrateEnumPolicy(field, value) {
122
- const allowedOptions = ROLE_ENUM_FIELD_VALUES[field];
123
- if (!isRecord(value)) throw new Error(field + ' must be an object');
124
- const defaultValue = typeof value.default === 'string' ? value.default.trim() : '';
125
- if (!allowedOptions.has(defaultValue)) {
126
- throw new Error(field + '.default has unsupported value: ' + String(value.default));
127
- }
128
- const hasAllowedValues = Array.isArray(value.allowedValues);
129
- const allowedValues = hasAllowedValues
130
- ? [...new Set(value.allowedValues
131
- .filter(item => typeof item === 'string')
132
- .map(item => item.trim())
133
- .filter(item => allowedOptions.has(item)))]
134
- : [];
135
- if (hasAllowedValues && !allowedValues.includes(defaultValue)) allowedValues.unshift(defaultValue);
136
- return {
137
- default: defaultValue,
138
- allowOverride: typeof value.allowOverride === 'boolean' ? value.allowOverride : false,
139
- ...(hasAllowedValues ? { allowedValues } : {}),
140
- ...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
141
- };
142
- }
143
-
144
- function migrateRoleDefinition(roleId, definition, legacyAgentMode, summary) {
145
- if (!isRecord(definition)) throw new Error(`roles.definitions.${roleId} must be an object`);
146
- if (typeof definition.description !== 'string') throw new Error(`roles.definitions.${roleId}.description must be a string`);
147
- if (!isRecord(definition.permissions)) throw new Error(`roles.definitions.${roleId}.permissions must be an object`);
148
- if (definition.commandPermissions !== undefined && !isRecord(definition.commandPermissions)) {
149
- throw new Error(`roles.definitions.${roleId}.commandPermissions must be an object`);
150
- }
151
-
152
- const permissions = definition.permissions;
153
- const ownMode = isRecord(permissions.permissionMode)
154
- ? normalizePermissionMode(permissions.permissionMode.default)
155
- : null;
156
- const fallbackMode = ownMode
157
- ?? (roleId === 'member' ? 'auto' : roleId === 'visitor' ? 'readonly' : legacyAgentMode)
158
- ?? 'readonly';
159
- const migratedPermissions = {
160
- permissionMode: migratePermissionModePolicy(permissions.permissionMode, fallbackMode),
161
- };
162
- const commandPermissions = { ...(definition.commandPermissions ?? {}) };
163
-
164
- for (const [field, policy] of Object.entries(permissions)) {
165
- if (field === 'permissionMode') continue;
166
- if (MODEL_FIELD_RE.test(field)) {
167
- migratedPermissions[field] = migrateModelPolicy(`roles.definitions.${roleId}.permissions.${field}`, policy);
168
- continue;
169
- }
170
- if (Object.prototype.hasOwnProperty.call(ROLE_ENUM_FIELD_VALUES, field)) {
171
- migratedPermissions[field] = migrateEnumPolicy(field, policy);
172
- continue;
173
- }
174
- if (isRecord(policy) && typeof policy.allow === 'boolean') {
175
- if (field !== 'permission.update' && !Object.prototype.hasOwnProperty.call(commandPermissions, field)) {
176
- commandPermissions[field] = clone(policy);
177
- summary.movedCommandPermissions += 1;
178
- }
179
- } else {
180
- summary.droppedUnsupportedRoleConstraints += 1;
181
- }
182
- }
183
-
184
- if (Object.prototype.hasOwnProperty.call(commandPermissions, 'permission.update')) {
185
- delete commandPermissions['permission.update'];
186
- summary.removedObsoletePermissionUpdates += 1;
187
- }
188
-
189
- const migrated = {
190
- ...definition,
191
- permissions: migratedPermissions,
192
- };
193
- if (Object.keys(commandPermissions).length) migrated.commandPermissions = commandPermissions;
194
- else delete migrated.commandPermissions;
195
- if (JSON.stringify(definition) !== JSON.stringify(migrated)) summary.normalizedRoleDefinitions += 1;
196
- return migrated;
197
- }
198
-
199
- function legacyRoleDescription(roleId) {
200
- if (roleId === 'member') return 'Trusted user with own-scope access';
201
- if (roleId === 'visitor') return 'Low-trust visitor with minimal own-scope access';
202
- return `Migrated legacy role: ${roleId}`;
203
- }
204
-
205
- function migrateLegacyV2Role(roleId, override, legacyAgentMode, summary) {
206
- if (!isRecord(override)) throw new Error(`roles.${roleId} must be an object`);
207
-
208
- const explicitMode = override.permissionMode === undefined
209
- ? null
210
- : normalizePermissionMode(override.permissionMode);
211
- if (override.permissionMode !== undefined && !explicitMode) {
212
- throw new Error(`roles.${roleId}.permissionMode has unsupported value: ${String(override.permissionMode)}`);
213
- }
214
- const fallbackMode = explicitMode
215
- ?? (roleId === 'member' ? 'auto' : roleId === 'visitor' ? 'readonly' : legacyAgentMode)
216
- ?? 'readonly';
217
- const permissions = {
218
- permissionMode: {
219
- default: fallbackMode,
220
- allowOverride: false,
221
- },
222
- };
223
-
224
- if (override.baseagents !== undefined && !isRecord(override.baseagents)) {
225
- throw new Error(`roles.${roleId}.baseagents must be an object`);
226
- }
227
- for (const [baseagentId, baseagentOverride] of Object.entries(override.baseagents ?? {})) {
228
- if (!isRecord(baseagentOverride)) {
229
- summary.droppedUnsupportedRoleConstraints += 1;
230
- continue;
231
- }
232
- for (const [field, value] of Object.entries(baseagentOverride)) {
233
- if (field === 'model') {
234
- if (typeof value !== 'string' || !value.trim()) {
235
- throw new Error(`roles.${roleId}.baseagents.${baseagentId}.model must be a non-empty string`);
236
- }
237
- const model = value.trim();
238
- permissions[`baseagents.${baseagentId}.model`] = {
239
- default: model,
240
- allowOverride: false,
241
- allowedModels: [model],
242
- };
243
- continue;
244
- }
245
-
246
- const roleField = 'baseagents.' + baseagentId + '.' + field;
247
- const allowedOptions = ROLE_ENUM_FIELD_VALUES[roleField];
248
- if (allowedOptions) {
249
- if (typeof value !== 'string' || !allowedOptions.has(value.trim())) {
250
- throw new Error('roles.' + roleId + '.baseagents.' + baseagentId + '.' + field
251
- + ' has unsupported value: ' + String(value));
252
- }
253
- const defaultValue = value.trim();
254
- permissions[roleField] = {
255
- default: defaultValue,
256
- allowOverride: false,
257
- allowedValues: [defaultValue],
258
- };
259
- continue;
260
- }
261
-
262
- summary.droppedUnsupportedRoleConstraints += 1;
263
- }
264
- }
265
-
266
- summary.migratedLegacyV2Roles += 1;
267
- return {
268
- description: legacyRoleDescription(roleId),
269
- permissions,
270
- };
271
- }
272
-
273
- function removeLegacySessionRenewModel(config, summary) {
274
- if (!isRecord(config.session_renew) || !Object.prototype.hasOwnProperty.call(config.session_renew, 'model')) return;
275
- config.session_renew = { ...config.session_renew };
276
- delete config.session_renew.model;
277
- summary.removedSessionRenewModels += 1;
278
- }
279
-
280
- function migrateAgentConfig(input, summary) {
281
- if (!isRecord(input)) throw new Error('Agent config must be an object');
282
- if (typeof input.$schema_version === 'number' && input.$schema_version > 4) {
283
- throw new Error(`Unsupported future Agent schema version: ${input.$schema_version}`);
284
- }
285
- const output = clone(input);
286
- const legacyAgentMode = normalizePermissionMode(output.permissionMode);
287
- if (Object.prototype.hasOwnProperty.call(output, 'permissionMode')) {
288
- delete output.permissionMode;
289
- summary.removedAgentPermissionModes += 1;
290
- }
291
- removeLegacySessionRenewModel(output, summary);
292
- output.$schema_version = 4;
293
-
294
- if (output.roles !== undefined && !isRecord(output.roles)) throw new Error('roles must be an object');
295
- if (isRecord(output.roles)) {
296
- if (output.roles.definitions !== undefined && !isRecord(output.roles.definitions)) {
297
- throw new Error('roles.definitions must be an object');
298
- }
299
- const definitions = { ...(output.roles.definitions ?? {}) };
300
- for (const [roleId, override] of Object.entries(output.roles)) {
301
- if (ROLE_POLICY_KEYS.has(roleId)) continue;
302
- if (Object.prototype.hasOwnProperty.call(definitions, roleId)) {
303
- throw new Error(`roles.${roleId} conflicts with roles.definitions.${roleId}`);
304
- }
305
- definitions[roleId] = migrateLegacyV2Role(roleId, override, legacyAgentMode, summary);
306
- }
307
-
308
- const migratedRoles = {};
309
- if (output.roles.defaultRoles !== undefined) migratedRoles.defaultRoles = output.roles.defaultRoles;
310
- if (Object.keys(definitions).length) {
311
- migratedRoles.definitions = definitions;
312
- for (const [roleId, definition] of Object.entries(migratedRoles.definitions)) {
313
- migratedRoles.definitions[roleId] = migrateRoleDefinition(roleId, definition, legacyAgentMode, summary);
314
- }
315
- }
316
- output.roles = migratedRoles;
317
- }
318
-
319
- const roleNames = new Set([...BUILTIN_ROLES, ...Object.keys(output.roles?.definitions ?? {})]);
320
- for (const scene of ['private', 'group']) {
321
- const role = output.roles?.defaultRoles?.[scene];
322
- if (role !== undefined && role !== null && !roleNames.has(role)) {
323
- throw new Error(`roles.defaultRoles.${scene} references unknown role: ${String(role)}`);
324
- }
325
- }
326
- return { output, roleNames };
327
- }
328
-
329
- function migrateRelationConfig(input, roleNames, summary) {
330
- if (!isRecord(input)) throw new Error('Relation config must be an object');
331
- if (typeof input.$schema_version === 'number' && input.$schema_version > 4) {
332
- throw new Error(`Unsupported future Relation schema version: ${input.$schema_version}`);
333
- }
334
- const output = clone(input);
335
- const legacyRoles = isRecord(output.roles) ? output.roles : null;
336
- const assigned = legacyRoles && Object.prototype.hasOwnProperty.call(legacyRoles, 'assigned')
337
- ? legacyRoles.assigned
338
- : undefined;
339
- if (assigned !== undefined && output.role !== undefined && assigned !== output.role) {
340
- throw new Error(`role conflicts with legacy roles.assigned (${String(output.role)} != ${String(assigned)})`);
341
- }
342
- if (output.role === undefined && assigned !== undefined) {
343
- output.role = assigned;
344
- summary.migratedRelationAssignments += 1;
345
- }
346
- if (isRecord(legacyRoles?.members)) {
347
- summary.discardedMemberOverrides += Object.keys(legacyRoles.members).length;
348
- }
349
- if (Object.prototype.hasOwnProperty.call(output, 'roles')) delete output.roles;
350
- if (Object.prototype.hasOwnProperty.call(output, 'permissionMode')) {
351
- delete output.permissionMode;
352
- summary.removedRelationPermissionModes += 1;
353
- }
354
- removeLegacySessionRenewModel(output, summary);
355
- output.$schema_version = 4;
356
- if (output.role !== undefined && output.role !== null && !roleNames.has(output.role)) {
357
- throw new Error(`role references unknown role: ${String(output.role)}`);
358
- }
359
- return output;
360
- }
361
-
362
- function readJson(file) {
363
- try {
364
- const text = fs.readFileSync(file, 'utf8');
365
- return { value: JSON.parse(text), text };
366
- } catch (error) {
367
- throw new Error(`${file}: ${error instanceof Error ? error.message : String(error)}`);
368
- }
369
- }
370
-
371
- function walkRelationConfigs(root) {
372
- if (!fs.existsSync(root)) return [];
373
- const files = [];
374
- const visit = dir => {
375
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
376
- const full = path.join(dir, entry.name);
377
- if (entry.isDirectory()) visit(full);
378
- else if (entry.isFile() && entry.name === 'config.json') files.push(full);
379
- }
380
- };
381
- visit(root);
382
- return files.sort();
383
- }
384
-
385
- function validationError(validate) {
386
- return (validate.errors ?? []).map(error => {
387
- const extra = error.params?.additionalProperty ? ` (${error.params.additionalProperty})` : '';
388
- return `${error.instancePath || '/'} ${error.message}${extra}`;
389
- }).join('; ');
390
- }
391
-
392
- function planMigration(home) {
393
- const agentsRoot = path.join(home, 'agents');
394
- if (!fs.existsSync(agentsRoot)) throw new Error(`Agents directory not found: ${agentsRoot}`);
395
- const validators = loadValidators();
396
- const summary = {
397
- agentsScanned: 0,
398
- relationsScanned: 0,
399
- agentsChanged: 0,
400
- relationsChanged: 0,
401
- removedAgentPermissionModes: 0,
402
- removedRelationPermissionModes: 0,
403
- migratedRelationAssignments: 0,
404
- discardedMemberOverrides: 0,
405
- migratedLegacyV2Roles: 0,
406
- normalizedRoleDefinitions: 0,
407
- movedCommandPermissions: 0,
408
- droppedUnsupportedRoleConstraints: 0,
409
- removedObsoletePermissionUpdates: 0,
410
- removedSessionRenewModels: 0,
411
- };
412
- const changes = [];
413
-
414
- for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
415
- if (!entry.isDirectory() || !isValidAid(entry.name)) continue;
416
- const agentDir = path.join(agentsRoot, entry.name);
417
- const agentFile = path.join(agentDir, 'config.json');
418
- if (!fs.existsSync(agentFile)) continue;
419
- summary.agentsScanned += 1;
420
- const { value: agentInput, text: agentText } = readJson(agentFile);
421
- const { output: agentOutput, roleNames } = migrateAgentConfig(agentInput, summary);
422
- if (!validators.agent(agentOutput)) throw new Error(`${agentFile}: ${validationError(validators.agent)}`);
423
- if (JSON.stringify(agentInput) !== JSON.stringify(agentOutput)) {
424
- changes.push(changeRecord(home, agentFile, 'agent', agentText, agentOutput));
425
- summary.agentsChanged += 1;
426
- }
427
-
428
- for (const relationFile of walkRelationConfigs(path.join(agentDir, 'relations'))) {
429
- summary.relationsScanned += 1;
430
- const { value: relationInput, text: relationText } = readJson(relationFile);
431
- const relationOutput = migrateRelationConfig(relationInput, roleNames, summary);
432
- if (!validators.relation(relationOutput)) throw new Error(`${relationFile}: ${validationError(validators.relation)}`);
433
- if (JSON.stringify(relationInput) !== JSON.stringify(relationOutput)) {
434
- changes.push(changeRecord(home, relationFile, 'relation', relationText, relationOutput));
435
- summary.relationsChanged += 1;
436
- }
437
- }
438
- }
439
- return { home, changes, summary };
440
- }
441
-
442
- function changeRecord(home, file, kind, beforeText, after) {
443
- return {
444
- file,
445
- relative: path.relative(home, file),
446
- kind,
447
- beforeText,
448
- afterText: `${JSON.stringify(after, null, 2)}\n`,
449
- };
450
- }
451
-
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import Ajv from 'ajv';
6
+
7
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
8
+ const SCHEMA_DIR = path.resolve(SCRIPT_DIR, '..', 'schemas');
9
+ const PERMISSION_MODES = new Set(['readonly', 'auto', 'request', 'bypass']);
10
+ const MODEL_FIELD_RE = /^baseagents\.[a-z0-9_-]+\.model$/;
11
+ const ROLE_ENUM_FIELD_VALUES = {
12
+ 'chatmode.private': new Set(['interactive', 'proactive']),
13
+ 'chatmode.group': new Set(['interactive', 'proactive']),
14
+ 'baseagents.claude.effort': new Set(['auto', 'low', 'medium', 'high', 'xhigh', 'max']),
15
+ 'baseagents.codex.reasoning': new Set(['auto', 'low', 'medium', 'high', 'xhigh']),
16
+ };
17
+ const BUILTIN_ROLES = new Set(['member', 'visitor']);
18
+ const ROLE_POLICY_KEYS = new Set(['defaultRoles', 'definitions']);
19
+
20
+ function usage() {
21
+ return `Usage:
22
+ node kits/migrations/migrate-role-config-v4.mjs [--home <EVOLCORE_HOME>] [--json]
23
+ node kits/migrations/migrate-role-config-v4.mjs --apply [--home <EVOLCORE_HOME>] [--json]
24
+
25
+ The default mode is dry-run. Stop the EvolCore daemon before running this migration.
26
+ Every applied migration creates a backup under:
27
+ <EVOLCORE_HOME>/backups/role-v4-migration/<timestamp>/`;
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const options = { apply: false, json: false, home: undefined };
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+ if (arg === '--apply') options.apply = true;
35
+ else if (arg === '--json') options.json = true;
36
+ else if (arg === '--help' || arg === '-h') options.help = true;
37
+ else if (arg === '--home') {
38
+ const value = argv[++i];
39
+ if (!value) throw new Error('--home requires a path');
40
+ options.home = value;
41
+ } else {
42
+ throw new Error(`Unknown argument: ${arg}`);
43
+ }
44
+ }
45
+ return options;
46
+ }
47
+
48
+ function resolveHome(explicitHome) {
49
+ if (explicitHome) return path.resolve(explicitHome);
50
+ if (process.env.EVOLCORE_HOME) return path.resolve(process.env.EVOLCORE_HOME);
51
+ if (fs.existsSync(path.join(process.cwd(), 'agents'))) return process.cwd();
52
+ return path.join(os.homedir(), '.evolcore');
53
+ }
54
+
55
+ function loadValidators() {
56
+ const ajv = new Ajv({ allErrors: true, strict: false });
57
+ const load = name => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, name), 'utf8'));
58
+ return {
59
+ agent: ajv.compile(load('agent-config.schema.4.json')),
60
+ relation: ajv.compile(load('relation-config.schema.4.json')),
61
+ };
62
+ }
63
+
64
+ function isRecord(value) {
65
+ return !!value && typeof value === 'object' && !Array.isArray(value);
66
+ }
67
+
68
+ function isValidAid(name) {
69
+ const labels = name.split('.');
70
+ return labels.length >= 3
71
+ && labels.every(label => /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label));
72
+ }
73
+
74
+ function clone(value) {
75
+ return JSON.parse(JSON.stringify(value));
76
+ }
77
+
78
+ function normalizePermissionMode(value) {
79
+ if (typeof value !== 'string') return null;
80
+ if (PERMISSION_MODES.has(value)) return value;
81
+ if (value === 'edit') return 'request';
82
+ if (value === 'plan' || value === 'noask') return 'readonly';
83
+ return null;
84
+ }
85
+
86
+ function wildcardMatch(pattern, value) {
87
+ return pattern === '*' || (pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : pattern === value);
88
+ }
89
+
90
+ function migratePermissionModePolicy(value, fallback) {
91
+ const policy = isRecord(value) ? value : {};
92
+ const defaultValue = normalizePermissionMode(policy.default) ?? fallback;
93
+ const allowedValues = Array.isArray(policy.allowedValues)
94
+ ? [...new Set(policy.allowedValues.map(normalizePermissionMode).filter(Boolean))]
95
+ : [];
96
+ if (allowedValues.length && !allowedValues.includes(defaultValue)) allowedValues.unshift(defaultValue);
97
+ return {
98
+ default: defaultValue,
99
+ allowOverride: false,
100
+ ...(allowedValues.length ? { allowedValues } : {}),
101
+ ...(typeof policy.reason === 'string' ? { reason: policy.reason } : {}),
102
+ };
103
+ }
104
+
105
+ function migrateModelPolicy(field, value) {
106
+ if (!isRecord(value)) throw new Error(`${field} must be an object`);
107
+ const defaultModel = typeof value.default === 'string' ? value.default.trim() : '';
108
+ if (!defaultModel) throw new Error(`${field}.default must be a non-empty string`);
109
+ const allowedModels = Array.isArray(value.allowedModels)
110
+ ? [...new Set(value.allowedModels.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim()))]
111
+ : [];
112
+ if (!allowedModels.some(pattern => wildcardMatch(pattern, defaultModel))) allowedModels.unshift(defaultModel);
113
+ return {
114
+ default: defaultModel,
115
+ allowOverride: typeof value.allowOverride === 'boolean' ? value.allowOverride : false,
116
+ allowedModels,
117
+ ...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
118
+ };
119
+ }
120
+
121
+ function migrateEnumPolicy(field, value) {
122
+ const allowedOptions = ROLE_ENUM_FIELD_VALUES[field];
123
+ if (!isRecord(value)) throw new Error(field + ' must be an object');
124
+ const defaultValue = typeof value.default === 'string' ? value.default.trim() : '';
125
+ if (!allowedOptions.has(defaultValue)) {
126
+ throw new Error(field + '.default has unsupported value: ' + String(value.default));
127
+ }
128
+ const hasAllowedValues = Array.isArray(value.allowedValues);
129
+ const allowedValues = hasAllowedValues
130
+ ? [...new Set(value.allowedValues
131
+ .filter(item => typeof item === 'string')
132
+ .map(item => item.trim())
133
+ .filter(item => allowedOptions.has(item)))]
134
+ : [];
135
+ if (hasAllowedValues && !allowedValues.includes(defaultValue)) allowedValues.unshift(defaultValue);
136
+ return {
137
+ default: defaultValue,
138
+ allowOverride: typeof value.allowOverride === 'boolean' ? value.allowOverride : false,
139
+ ...(hasAllowedValues ? { allowedValues } : {}),
140
+ ...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
141
+ };
142
+ }
143
+
144
+ function migrateRoleDefinition(roleId, definition, legacyAgentMode, summary) {
145
+ if (!isRecord(definition)) throw new Error(`roles.definitions.${roleId} must be an object`);
146
+ if (typeof definition.description !== 'string') throw new Error(`roles.definitions.${roleId}.description must be a string`);
147
+ if (!isRecord(definition.permissions)) throw new Error(`roles.definitions.${roleId}.permissions must be an object`);
148
+ if (definition.commandPermissions !== undefined && !isRecord(definition.commandPermissions)) {
149
+ throw new Error(`roles.definitions.${roleId}.commandPermissions must be an object`);
150
+ }
151
+
152
+ const permissions = definition.permissions;
153
+ const ownMode = isRecord(permissions.permissionMode)
154
+ ? normalizePermissionMode(permissions.permissionMode.default)
155
+ : null;
156
+ const fallbackMode = ownMode
157
+ ?? (roleId === 'member' ? 'auto' : roleId === 'visitor' ? 'readonly' : legacyAgentMode)
158
+ ?? 'readonly';
159
+ const migratedPermissions = {
160
+ permissionMode: migratePermissionModePolicy(permissions.permissionMode, fallbackMode),
161
+ };
162
+ const commandPermissions = { ...(definition.commandPermissions ?? {}) };
163
+
164
+ for (const [field, policy] of Object.entries(permissions)) {
165
+ if (field === 'permissionMode') continue;
166
+ if (MODEL_FIELD_RE.test(field)) {
167
+ migratedPermissions[field] = migrateModelPolicy(`roles.definitions.${roleId}.permissions.${field}`, policy);
168
+ continue;
169
+ }
170
+ if (Object.prototype.hasOwnProperty.call(ROLE_ENUM_FIELD_VALUES, field)) {
171
+ migratedPermissions[field] = migrateEnumPolicy(field, policy);
172
+ continue;
173
+ }
174
+ if (isRecord(policy) && typeof policy.allow === 'boolean') {
175
+ if (field !== 'permission.update' && !Object.prototype.hasOwnProperty.call(commandPermissions, field)) {
176
+ commandPermissions[field] = clone(policy);
177
+ summary.movedCommandPermissions += 1;
178
+ }
179
+ } else {
180
+ summary.droppedUnsupportedRoleConstraints += 1;
181
+ }
182
+ }
183
+
184
+ if (Object.prototype.hasOwnProperty.call(commandPermissions, 'permission.update')) {
185
+ delete commandPermissions['permission.update'];
186
+ summary.removedObsoletePermissionUpdates += 1;
187
+ }
188
+
189
+ const migrated = {
190
+ ...definition,
191
+ permissions: migratedPermissions,
192
+ };
193
+ if (Object.keys(commandPermissions).length) migrated.commandPermissions = commandPermissions;
194
+ else delete migrated.commandPermissions;
195
+ if (JSON.stringify(definition) !== JSON.stringify(migrated)) summary.normalizedRoleDefinitions += 1;
196
+ return migrated;
197
+ }
198
+
199
+ function legacyRoleDescription(roleId) {
200
+ if (roleId === 'member') return 'Trusted user with own-scope access';
201
+ if (roleId === 'visitor') return 'Low-trust visitor with minimal own-scope access';
202
+ return `Migrated legacy role: ${roleId}`;
203
+ }
204
+
205
+ function migrateLegacyV2Role(roleId, override, legacyAgentMode, summary) {
206
+ if (!isRecord(override)) throw new Error(`roles.${roleId} must be an object`);
207
+
208
+ const explicitMode = override.permissionMode === undefined
209
+ ? null
210
+ : normalizePermissionMode(override.permissionMode);
211
+ if (override.permissionMode !== undefined && !explicitMode) {
212
+ throw new Error(`roles.${roleId}.permissionMode has unsupported value: ${String(override.permissionMode)}`);
213
+ }
214
+ const fallbackMode = explicitMode
215
+ ?? (roleId === 'member' ? 'auto' : roleId === 'visitor' ? 'readonly' : legacyAgentMode)
216
+ ?? 'readonly';
217
+ const permissions = {
218
+ permissionMode: {
219
+ default: fallbackMode,
220
+ allowOverride: false,
221
+ },
222
+ };
223
+
224
+ if (override.baseagents !== undefined && !isRecord(override.baseagents)) {
225
+ throw new Error(`roles.${roleId}.baseagents must be an object`);
226
+ }
227
+ for (const [baseagentId, baseagentOverride] of Object.entries(override.baseagents ?? {})) {
228
+ if (!isRecord(baseagentOverride)) {
229
+ summary.droppedUnsupportedRoleConstraints += 1;
230
+ continue;
231
+ }
232
+ for (const [field, value] of Object.entries(baseagentOverride)) {
233
+ if (field === 'model') {
234
+ if (typeof value !== 'string' || !value.trim()) {
235
+ throw new Error(`roles.${roleId}.baseagents.${baseagentId}.model must be a non-empty string`);
236
+ }
237
+ const model = value.trim();
238
+ permissions[`baseagents.${baseagentId}.model`] = {
239
+ default: model,
240
+ allowOverride: false,
241
+ allowedModels: [model],
242
+ };
243
+ continue;
244
+ }
245
+
246
+ const roleField = 'baseagents.' + baseagentId + '.' + field;
247
+ const allowedOptions = ROLE_ENUM_FIELD_VALUES[roleField];
248
+ if (allowedOptions) {
249
+ if (typeof value !== 'string' || !allowedOptions.has(value.trim())) {
250
+ throw new Error('roles.' + roleId + '.baseagents.' + baseagentId + '.' + field
251
+ + ' has unsupported value: ' + String(value));
252
+ }
253
+ const defaultValue = value.trim();
254
+ permissions[roleField] = {
255
+ default: defaultValue,
256
+ allowOverride: false,
257
+ allowedValues: [defaultValue],
258
+ };
259
+ continue;
260
+ }
261
+
262
+ summary.droppedUnsupportedRoleConstraints += 1;
263
+ }
264
+ }
265
+
266
+ summary.migratedLegacyV2Roles += 1;
267
+ return {
268
+ description: legacyRoleDescription(roleId),
269
+ permissions,
270
+ };
271
+ }
272
+
273
+ function removeLegacySessionRenewModel(config, summary) {
274
+ if (!isRecord(config.session_renew) || !Object.prototype.hasOwnProperty.call(config.session_renew, 'model')) return;
275
+ config.session_renew = { ...config.session_renew };
276
+ delete config.session_renew.model;
277
+ summary.removedSessionRenewModels += 1;
278
+ }
279
+
280
+ function migrateAgentConfig(input, summary) {
281
+ if (!isRecord(input)) throw new Error('Agent config must be an object');
282
+ if (typeof input.$schema_version === 'number' && input.$schema_version > 4) {
283
+ throw new Error(`Unsupported future Agent schema version: ${input.$schema_version}`);
284
+ }
285
+ const output = clone(input);
286
+ const legacyAgentMode = normalizePermissionMode(output.permissionMode);
287
+ if (Object.prototype.hasOwnProperty.call(output, 'permissionMode')) {
288
+ delete output.permissionMode;
289
+ summary.removedAgentPermissionModes += 1;
290
+ }
291
+ removeLegacySessionRenewModel(output, summary);
292
+ output.$schema_version = 4;
293
+
294
+ if (output.roles !== undefined && !isRecord(output.roles)) throw new Error('roles must be an object');
295
+ if (isRecord(output.roles)) {
296
+ if (output.roles.definitions !== undefined && !isRecord(output.roles.definitions)) {
297
+ throw new Error('roles.definitions must be an object');
298
+ }
299
+ const definitions = { ...(output.roles.definitions ?? {}) };
300
+ for (const [roleId, override] of Object.entries(output.roles)) {
301
+ if (ROLE_POLICY_KEYS.has(roleId)) continue;
302
+ if (Object.prototype.hasOwnProperty.call(definitions, roleId)) {
303
+ throw new Error(`roles.${roleId} conflicts with roles.definitions.${roleId}`);
304
+ }
305
+ definitions[roleId] = migrateLegacyV2Role(roleId, override, legacyAgentMode, summary);
306
+ }
307
+
308
+ const migratedRoles = {};
309
+ if (output.roles.defaultRoles !== undefined) migratedRoles.defaultRoles = output.roles.defaultRoles;
310
+ if (Object.keys(definitions).length) {
311
+ migratedRoles.definitions = definitions;
312
+ for (const [roleId, definition] of Object.entries(migratedRoles.definitions)) {
313
+ migratedRoles.definitions[roleId] = migrateRoleDefinition(roleId, definition, legacyAgentMode, summary);
314
+ }
315
+ }
316
+ output.roles = migratedRoles;
317
+ }
318
+
319
+ const roleNames = new Set([...BUILTIN_ROLES, ...Object.keys(output.roles?.definitions ?? {})]);
320
+ for (const scene of ['private', 'group']) {
321
+ const role = output.roles?.defaultRoles?.[scene];
322
+ if (role !== undefined && role !== null && !roleNames.has(role)) {
323
+ throw new Error(`roles.defaultRoles.${scene} references unknown role: ${String(role)}`);
324
+ }
325
+ }
326
+ return { output, roleNames };
327
+ }
328
+
329
+ function migrateRelationConfig(input, roleNames, summary) {
330
+ if (!isRecord(input)) throw new Error('Relation config must be an object');
331
+ if (typeof input.$schema_version === 'number' && input.$schema_version > 4) {
332
+ throw new Error(`Unsupported future Relation schema version: ${input.$schema_version}`);
333
+ }
334
+ const output = clone(input);
335
+ const legacyRoles = isRecord(output.roles) ? output.roles : null;
336
+ const assigned = legacyRoles && Object.prototype.hasOwnProperty.call(legacyRoles, 'assigned')
337
+ ? legacyRoles.assigned
338
+ : undefined;
339
+ if (assigned !== undefined && output.role !== undefined && assigned !== output.role) {
340
+ throw new Error(`role conflicts with legacy roles.assigned (${String(output.role)} != ${String(assigned)})`);
341
+ }
342
+ if (output.role === undefined && assigned !== undefined) {
343
+ output.role = assigned;
344
+ summary.migratedRelationAssignments += 1;
345
+ }
346
+ if (isRecord(legacyRoles?.members)) {
347
+ summary.discardedMemberOverrides += Object.keys(legacyRoles.members).length;
348
+ }
349
+ if (Object.prototype.hasOwnProperty.call(output, 'roles')) delete output.roles;
350
+ if (Object.prototype.hasOwnProperty.call(output, 'permissionMode')) {
351
+ delete output.permissionMode;
352
+ summary.removedRelationPermissionModes += 1;
353
+ }
354
+ removeLegacySessionRenewModel(output, summary);
355
+ output.$schema_version = 4;
356
+ if (output.role !== undefined && output.role !== null && !roleNames.has(output.role)) {
357
+ throw new Error(`role references unknown role: ${String(output.role)}`);
358
+ }
359
+ return output;
360
+ }
361
+
362
+ function readJson(file) {
363
+ try {
364
+ const text = fs.readFileSync(file, 'utf8');
365
+ return { value: JSON.parse(text), text };
366
+ } catch (error) {
367
+ throw new Error(`${file}: ${error instanceof Error ? error.message : String(error)}`);
368
+ }
369
+ }
370
+
371
+ function walkRelationConfigs(root) {
372
+ if (!fs.existsSync(root)) return [];
373
+ const files = [];
374
+ const visit = dir => {
375
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
376
+ const full = path.join(dir, entry.name);
377
+ if (entry.isDirectory()) visit(full);
378
+ else if (entry.isFile() && entry.name === 'config.json') files.push(full);
379
+ }
380
+ };
381
+ visit(root);
382
+ return files.sort();
383
+ }
384
+
385
+ function validationError(validate) {
386
+ return (validate.errors ?? []).map(error => {
387
+ const extra = error.params?.additionalProperty ? ` (${error.params.additionalProperty})` : '';
388
+ return `${error.instancePath || '/'} ${error.message}${extra}`;
389
+ }).join('; ');
390
+ }
391
+
392
+ function planMigration(home) {
393
+ const agentsRoot = path.join(home, 'agents');
394
+ if (!fs.existsSync(agentsRoot)) throw new Error(`Agents directory not found: ${agentsRoot}`);
395
+ const validators = loadValidators();
396
+ const summary = {
397
+ agentsScanned: 0,
398
+ relationsScanned: 0,
399
+ agentsChanged: 0,
400
+ relationsChanged: 0,
401
+ removedAgentPermissionModes: 0,
402
+ removedRelationPermissionModes: 0,
403
+ migratedRelationAssignments: 0,
404
+ discardedMemberOverrides: 0,
405
+ migratedLegacyV2Roles: 0,
406
+ normalizedRoleDefinitions: 0,
407
+ movedCommandPermissions: 0,
408
+ droppedUnsupportedRoleConstraints: 0,
409
+ removedObsoletePermissionUpdates: 0,
410
+ removedSessionRenewModels: 0,
411
+ };
412
+ const changes = [];
413
+
414
+ for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
415
+ if (!entry.isDirectory() || !isValidAid(entry.name)) continue;
416
+ const agentDir = path.join(agentsRoot, entry.name);
417
+ const agentFile = path.join(agentDir, 'config.json');
418
+ if (!fs.existsSync(agentFile)) continue;
419
+ summary.agentsScanned += 1;
420
+ const { value: agentInput, text: agentText } = readJson(agentFile);
421
+ const { output: agentOutput, roleNames } = migrateAgentConfig(agentInput, summary);
422
+ if (!validators.agent(agentOutput)) throw new Error(`${agentFile}: ${validationError(validators.agent)}`);
423
+ if (JSON.stringify(agentInput) !== JSON.stringify(agentOutput)) {
424
+ changes.push(changeRecord(home, agentFile, 'agent', agentText, agentOutput));
425
+ summary.agentsChanged += 1;
426
+ }
427
+
428
+ for (const relationFile of walkRelationConfigs(path.join(agentDir, 'relations'))) {
429
+ summary.relationsScanned += 1;
430
+ const { value: relationInput, text: relationText } = readJson(relationFile);
431
+ const relationOutput = migrateRelationConfig(relationInput, roleNames, summary);
432
+ if (!validators.relation(relationOutput)) throw new Error(`${relationFile}: ${validationError(validators.relation)}`);
433
+ if (JSON.stringify(relationInput) !== JSON.stringify(relationOutput)) {
434
+ changes.push(changeRecord(home, relationFile, 'relation', relationText, relationOutput));
435
+ summary.relationsChanged += 1;
436
+ }
437
+ }
438
+ }
439
+ return { home, changes, summary };
440
+ }
441
+
442
+ function changeRecord(home, file, kind, beforeText, after) {
443
+ return {
444
+ file,
445
+ relative: path.relative(home, file),
446
+ kind,
447
+ beforeText,
448
+ afterText: `${JSON.stringify(after, null, 2)}\n`,
449
+ };
450
+ }
451
+
452
452
  function assertDaemonStopped(home) {
453
453
  const candidates = [];
454
454
  const legacyPidFile = path.join(home, 'daemon.pid');
@@ -464,166 +464,166 @@ function assertDaemonStopped(home) {
464
464
  } catch (error) {
465
465
  if (error?.code !== 'ENOENT') throw error;
466
466
  }
467
-
468
- const instanceDir = path.join(home, 'data', 'instance');
469
- if (fs.existsSync(instanceDir)) {
470
- for (const name of fs.readdirSync(instanceDir)) {
471
- if (!/^main-\d+\.json$/.test(name)) continue;
472
- const file = path.join(instanceDir, name);
473
- try {
474
- const record = JSON.parse(fs.readFileSync(file, 'utf8'));
475
- candidates.push({ pid: Number(record?.pid), source: file });
476
- } catch {
477
- // A broken instance record is ignored here; daemon startup owns its cleanup.
478
- }
479
- }
480
- }
481
-
482
- for (const { pid, source } of candidates) {
483
- if (!Number.isInteger(pid) || pid <= 0) continue;
484
- try {
485
- process.kill(pid, 0);
486
- } catch (error) {
487
- if (error?.code === 'ESRCH') continue;
488
- if (error?.code !== 'EPERM') throw error;
489
- }
490
- throw new Error(`EvolCore daemon appears to be running (pid=${pid}, record=${source}); stop it before migration`);
491
- }
492
- }
493
-
494
- function acquireMigrationLock(home) {
495
- const lockDir = path.join(home, 'data', 'role-v4-migration.lock');
496
- const ownerFile = path.join(lockDir, 'owner.json');
497
- fs.mkdirSync(path.dirname(lockDir), { recursive: true });
498
-
499
- for (let attempt = 0; attempt < 2; attempt += 1) {
500
- try {
501
- fs.mkdirSync(lockDir);
502
- fs.writeFileSync(ownerFile, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2)}\n`);
503
- return () => fs.rmSync(lockDir, { recursive: true, force: true });
504
- } catch (error) {
505
- if (error?.code !== 'EEXIST') throw error;
506
- let ownerPid = null;
507
- try {
508
- ownerPid = Number(JSON.parse(fs.readFileSync(ownerFile, 'utf8'))?.pid);
509
- } catch {
510
- throw new Error(`Role config migration lock is unreadable: ${lockDir}`);
511
- }
512
- if (Number.isInteger(ownerPid) && ownerPid > 0) {
513
- try {
514
- process.kill(ownerPid, 0);
515
- throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
516
- } catch (ownerError) {
517
- if (ownerError?.code === 'EPERM') {
518
- throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
519
- }
520
- if (ownerError?.code !== 'ESRCH') throw ownerError;
521
- }
522
- }
523
- fs.rmSync(lockDir, { recursive: true, force: true });
524
- }
525
- }
526
- throw new Error(`Unable to acquire role config migration lock: ${lockDir}`);
527
- }
528
-
529
- function atomicWrite(file, content) {
530
- const mode = fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600;
531
- const temp = `${file}.role-v4-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
532
- fs.writeFileSync(temp, content, { mode });
533
- fs.renameSync(temp, file);
534
- }
535
-
536
- function createBackup(plan) {
537
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
538
- const backupRoot = path.join(plan.home, 'backups', 'role-v4-migration', timestamp);
539
- for (const change of plan.changes) {
540
- const destination = path.join(backupRoot, 'files', change.relative);
541
- fs.mkdirSync(path.dirname(destination), { recursive: true });
542
- fs.writeFileSync(destination, change.beforeText, 'utf8');
543
- }
544
- fs.writeFileSync(path.join(backupRoot, 'manifest.json'), `${JSON.stringify({
545
- schemaVersion: 1,
546
- createdAt: new Date().toISOString(),
547
- home: plan.home,
548
- summary: plan.summary,
549
- files: plan.changes.map(change => ({ path: change.relative, kind: change.kind })),
550
- }, null, 2)}\n`);
551
- return backupRoot;
552
- }
553
-
554
- function applyMigration(plan) {
555
- if (!plan.changes.length) return null;
556
- assertDaemonStopped(plan.home);
557
- const backup = createBackup(plan);
558
- const written = [];
559
- try {
560
- for (const change of plan.changes) {
561
- atomicWrite(change.file, change.afterText);
562
- written.push(change);
563
- }
564
- } catch (error) {
565
- for (const change of written.reverse()) atomicWrite(change.file, change.beforeText);
566
- throw error;
567
- }
568
- return backup;
569
- }
570
-
571
- function presentResult(plan, applied, backup) {
572
- return {
573
- ok: true,
574
- mode: applied ? 'applied' : 'dry-run',
575
- home: plan.home,
576
- changedFiles: plan.changes.length,
577
- backup,
578
- summary: plan.summary,
579
- };
580
- }
581
-
582
- function printHuman(result) {
583
- console.log(`Role config v4 migration (${result.mode})`);
584
- console.log(`Home: ${result.home}`);
585
- console.log(`Changed files: ${result.changedFiles}`);
586
- console.log(`Agents: ${result.summary.agentsChanged}/${result.summary.agentsScanned}`);
587
- console.log(`Relations: ${result.summary.relationsChanged}/${result.summary.relationsScanned}`);
588
- console.log(`Migrated legacy v2 roles: ${result.summary.migratedLegacyV2Roles}`);
589
- console.log(`Discarded member overrides: ${result.summary.discardedMemberOverrides}`);
590
- console.log(`Moved command permissions: ${result.summary.movedCommandPermissions}`);
591
- console.log(`Dropped unsupported role constraints: ${result.summary.droppedUnsupportedRoleConstraints}`);
592
- if (result.backup) console.log(`Backup: ${result.backup}`);
593
- if (result.mode === 'dry-run' && result.changedFiles) console.log('Run again with --apply after reviewing this result.');
594
- }
595
-
596
- export function runMigration(options = {}) {
597
- const home = resolveHome(options.home);
598
- assertDaemonStopped(home);
599
- const releaseLock = options.apply ? acquireMigrationLock(home) : null;
600
- try {
601
- const plan = planMigration(home);
602
- const backup = options.apply ? applyMigration(plan) : null;
603
- return presentResult(plan, !!options.apply, backup);
604
- } finally {
605
- releaseLock?.();
606
- }
607
- }
608
-
609
- async function main() {
610
- try {
611
- const options = parseArgs(process.argv.slice(2));
612
- if (options.help) {
613
- console.log(usage());
614
- return;
615
- }
616
- const result = runMigration(options);
617
- if (options.json) console.log(JSON.stringify(result));
618
- else printHuman(result);
619
- } catch (error) {
620
- const message = error instanceof Error ? error.message : String(error);
621
- if (process.argv.includes('--json')) console.log(JSON.stringify({ ok: false, error: message }));
622
- else console.error(`Migration failed: ${message}`);
623
- process.exitCode = 1;
624
- }
625
- }
626
-
627
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
628
- await main();
629
- }
467
+
468
+ const instanceDir = path.join(home, 'data', 'instance');
469
+ if (fs.existsSync(instanceDir)) {
470
+ for (const name of fs.readdirSync(instanceDir)) {
471
+ if (!/^main-\d+\.json$/.test(name)) continue;
472
+ const file = path.join(instanceDir, name);
473
+ try {
474
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
475
+ candidates.push({ pid: Number(record?.pid), source: file });
476
+ } catch {
477
+ // A broken instance record is ignored here; daemon startup owns its cleanup.
478
+ }
479
+ }
480
+ }
481
+
482
+ for (const { pid, source } of candidates) {
483
+ if (!Number.isInteger(pid) || pid <= 0) continue;
484
+ try {
485
+ process.kill(pid, 0);
486
+ } catch (error) {
487
+ if (error?.code === 'ESRCH') continue;
488
+ if (error?.code !== 'EPERM') throw error;
489
+ }
490
+ throw new Error(`EvolCore daemon appears to be running (pid=${pid}, record=${source}); stop it before migration`);
491
+ }
492
+ }
493
+
494
+ function acquireMigrationLock(home) {
495
+ const lockDir = path.join(home, 'data', 'role-v4-migration.lock');
496
+ const ownerFile = path.join(lockDir, 'owner.json');
497
+ fs.mkdirSync(path.dirname(lockDir), { recursive: true });
498
+
499
+ for (let attempt = 0; attempt < 2; attempt += 1) {
500
+ try {
501
+ fs.mkdirSync(lockDir);
502
+ fs.writeFileSync(ownerFile, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2)}\n`);
503
+ return () => fs.rmSync(lockDir, { recursive: true, force: true });
504
+ } catch (error) {
505
+ if (error?.code !== 'EEXIST') throw error;
506
+ let ownerPid = null;
507
+ try {
508
+ ownerPid = Number(JSON.parse(fs.readFileSync(ownerFile, 'utf8'))?.pid);
509
+ } catch {
510
+ throw new Error(`Role config migration lock is unreadable: ${lockDir}`);
511
+ }
512
+ if (Number.isInteger(ownerPid) && ownerPid > 0) {
513
+ try {
514
+ process.kill(ownerPid, 0);
515
+ throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
516
+ } catch (ownerError) {
517
+ if (ownerError?.code === 'EPERM') {
518
+ throw new Error(`Another role config migration is already running (pid=${ownerPid})`);
519
+ }
520
+ if (ownerError?.code !== 'ESRCH') throw ownerError;
521
+ }
522
+ }
523
+ fs.rmSync(lockDir, { recursive: true, force: true });
524
+ }
525
+ }
526
+ throw new Error(`Unable to acquire role config migration lock: ${lockDir}`);
527
+ }
528
+
529
+ function atomicWrite(file, content) {
530
+ const mode = fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600;
531
+ const temp = `${file}.role-v4-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
532
+ fs.writeFileSync(temp, content, { mode });
533
+ fs.renameSync(temp, file);
534
+ }
535
+
536
+ function createBackup(plan) {
537
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
538
+ const backupRoot = path.join(plan.home, 'backups', 'role-v4-migration', timestamp);
539
+ for (const change of plan.changes) {
540
+ const destination = path.join(backupRoot, 'files', change.relative);
541
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
542
+ fs.writeFileSync(destination, change.beforeText, 'utf8');
543
+ }
544
+ fs.writeFileSync(path.join(backupRoot, 'manifest.json'), `${JSON.stringify({
545
+ schemaVersion: 1,
546
+ createdAt: new Date().toISOString(),
547
+ home: plan.home,
548
+ summary: plan.summary,
549
+ files: plan.changes.map(change => ({ path: change.relative, kind: change.kind })),
550
+ }, null, 2)}\n`);
551
+ return backupRoot;
552
+ }
553
+
554
+ function applyMigration(plan) {
555
+ if (!plan.changes.length) return null;
556
+ assertDaemonStopped(plan.home);
557
+ const backup = createBackup(plan);
558
+ const written = [];
559
+ try {
560
+ for (const change of plan.changes) {
561
+ atomicWrite(change.file, change.afterText);
562
+ written.push(change);
563
+ }
564
+ } catch (error) {
565
+ for (const change of written.reverse()) atomicWrite(change.file, change.beforeText);
566
+ throw error;
567
+ }
568
+ return backup;
569
+ }
570
+
571
+ function presentResult(plan, applied, backup) {
572
+ return {
573
+ ok: true,
574
+ mode: applied ? 'applied' : 'dry-run',
575
+ home: plan.home,
576
+ changedFiles: plan.changes.length,
577
+ backup,
578
+ summary: plan.summary,
579
+ };
580
+ }
581
+
582
+ function printHuman(result) {
583
+ console.log(`Role config v4 migration (${result.mode})`);
584
+ console.log(`Home: ${result.home}`);
585
+ console.log(`Changed files: ${result.changedFiles}`);
586
+ console.log(`Agents: ${result.summary.agentsChanged}/${result.summary.agentsScanned}`);
587
+ console.log(`Relations: ${result.summary.relationsChanged}/${result.summary.relationsScanned}`);
588
+ console.log(`Migrated legacy v2 roles: ${result.summary.migratedLegacyV2Roles}`);
589
+ console.log(`Discarded member overrides: ${result.summary.discardedMemberOverrides}`);
590
+ console.log(`Moved command permissions: ${result.summary.movedCommandPermissions}`);
591
+ console.log(`Dropped unsupported role constraints: ${result.summary.droppedUnsupportedRoleConstraints}`);
592
+ if (result.backup) console.log(`Backup: ${result.backup}`);
593
+ if (result.mode === 'dry-run' && result.changedFiles) console.log('Run again with --apply after reviewing this result.');
594
+ }
595
+
596
+ export function runMigration(options = {}) {
597
+ const home = resolveHome(options.home);
598
+ assertDaemonStopped(home);
599
+ const releaseLock = options.apply ? acquireMigrationLock(home) : null;
600
+ try {
601
+ const plan = planMigration(home);
602
+ const backup = options.apply ? applyMigration(plan) : null;
603
+ return presentResult(plan, !!options.apply, backup);
604
+ } finally {
605
+ releaseLock?.();
606
+ }
607
+ }
608
+
609
+ async function main() {
610
+ try {
611
+ const options = parseArgs(process.argv.slice(2));
612
+ if (options.help) {
613
+ console.log(usage());
614
+ return;
615
+ }
616
+ const result = runMigration(options);
617
+ if (options.json) console.log(JSON.stringify(result));
618
+ else printHuman(result);
619
+ } catch (error) {
620
+ const message = error instanceof Error ? error.message : String(error);
621
+ if (process.argv.includes('--json')) console.log(JSON.stringify({ ok: false, error: message }));
622
+ else console.error(`Migration failed: ${message}`);
623
+ process.exitCode = 1;
624
+ }
625
+ }
626
+
627
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
628
+ await main();
629
+ }