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.
Files changed (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -1,47 +1,77 @@
1
1
  import crypto from 'crypto';
2
2
  import fs from 'fs';
3
- import { agentConfig, agentContactConfig } from '../paths.js';
4
- import { formatPeerKey } from '../core/relation/peer-identity.js';
3
+ import path from 'path';
4
+ import { agentConfig, agentContactConfig, agentRelationsDir } from '../paths.js';
5
+ import { formatPeerKey, parsePeerKey } from '../core/relation/peer-identity.js';
5
6
  import { hasTrustedPrincipal } from '../core/auth/authenticated-actor.js';
6
7
  import { ConfigTarget, read, write } from './config-manager.js';
7
8
  import { isManagementRole } from './builtin-roles.js';
8
- import { normalizeDefaultRole, roleExists, getRoleDefinition } from './roles.js';
9
+ import { getRoleDefinition, getRoleRank, normalizeDefaultRole, roleExists } from './roles.js';
9
10
  import { getStoredRoleRegistry, roleRegistryExists } from './role-store.js';
10
11
  import { resolvePrincipal } from './contact-book.js';
11
12
  import { isValidAid } from '../aun/aid/validation.js';
13
+ import { isExplicitGroupId } from '../aun/group-identity.js';
12
14
  export function isAuthenticated(userId) {
13
15
  return isValidAid(String(userId || '').trim());
14
16
  }
15
17
  export function listStaticAgentOwners(aid, opts = {}) {
16
- return listAgentAids(aid, 'owners', opts);
18
+ return listAgentOwners(aid, opts);
17
19
  }
20
+ /** Direct AUN relations with role=admin are the addressable Agent admin set. */
18
21
  export function listStaticAgentAdmins(aid, opts = {}) {
19
- return listAgentAids(aid, 'admins', opts);
22
+ if (!aid)
23
+ return [];
24
+ const relations = agentRelationsDir(aid);
25
+ if (!fs.existsSync(relations))
26
+ return [];
27
+ const admins = new Set();
28
+ for (const entry of fs.readdirSync(relations, { withFileTypes: true })) {
29
+ if (!entry.isDirectory() || entry.name.startsWith('_'))
30
+ continue;
31
+ let parsed;
32
+ try {
33
+ parsed = parsePeerKey(entry.name);
34
+ }
35
+ catch {
36
+ continue;
37
+ }
38
+ if (parsed.channelType !== 'aun'
39
+ || !isValidAid(parsed.channelId)
40
+ || isExplicitGroupId(parsed.channelId))
41
+ continue;
42
+ const config = read(ConfigTarget.Relation, { self: aid, peerKey: entry.name }, { cache: opts.fresh !== true });
43
+ if (config?.targetKind === 'group')
44
+ continue;
45
+ if (config?.role === 'admin')
46
+ admins.add(parsed.channelId);
47
+ }
48
+ return [...admins].sort();
20
49
  }
21
50
  export function getFirstStaticAgentOwner(aid) {
22
51
  return listStaticAgentOwners(aid)[0];
23
52
  }
24
- /** Revision for security-sensitive Agent/contact files, including transient rewrites. */
53
+ /** Revision for security-sensitive owner/contact/relation authorization inputs. */
25
54
  export function authorizationConfigRevision(aid) {
26
- const fileRevision = (file) => {
27
- try {
28
- const stat = fs.statSync(file);
29
- return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
30
- }
31
- catch {
32
- return 'missing';
55
+ const fingerprints = [fileRevision(agentConfig(aid)), fileRevision(agentContactConfig(aid))];
56
+ const relations = agentRelationsDir(aid);
57
+ try {
58
+ for (const entry of fs.readdirSync(relations, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
59
+ if (!entry.isDirectory() || entry.name.startsWith('_'))
60
+ continue;
61
+ fingerprints.push(entry.name, fileRevision(path.join(relations, entry.name, 'config.json')));
33
62
  }
34
- };
35
- return crypto.createHash('sha256').update(JSON.stringify([
36
- fileRevision(agentConfig(aid)),
37
- fileRevision(agentContactConfig(aid)),
38
- ])).digest('hex');
63
+ }
64
+ catch { }
65
+ return crypto.createHash('sha256').update(JSON.stringify(fingerprints)).digest('hex');
39
66
  }
40
67
  export function isStaticAgentOwner(aid, actorId) {
41
68
  return !!aid && !!actorId && listStaticAgentOwners(aid).includes(actorId);
42
69
  }
43
70
  export function isStaticAgentAdmin(aid, actorId) {
44
- return !!aid && !!actorId && listStaticAgentAdmins(aid).includes(actorId);
71
+ if (!aid || !isValidAid(actorId) || isExplicitGroupId(actorId))
72
+ return false;
73
+ const config = read(ConfigTarget.Relation, { self: aid, peerKey: formatPeerKey('aun', actorId) }, { cache: true });
74
+ return config?.targetKind !== 'group' && config?.role === 'admin';
45
75
  }
46
76
  export function addStaticAgentOwner(aid, ownerAid) {
47
77
  if (!aid || !ownerAid)
@@ -56,32 +86,46 @@ export function hasStaticAgentOwner(aid) {
56
86
  }
57
87
  export function resolvePeerRoleDetail(ctx) {
58
88
  const operatorId = String(ctx.actorId || '').trim();
59
- const operatorChannelType = String(ctx.channelType || '').trim().toLowerCase();
60
- const operatorChannelKey = String(ctx.channelKey || '').trim();
61
- const principal = resolvePrincipal(ctx.selfAid, operatorChannelType, operatorId, operatorChannelKey || undefined);
89
+ const channelType = String(ctx.channelType || '').trim().toLowerCase();
90
+ const channelKey = String(ctx.channelKey || '').trim();
91
+ const principal = resolvePrincipal(ctx.selfAid, channelType, operatorId, channelKey || undefined);
62
92
  const actor = {
63
93
  operatorId,
64
- operatorChannelKey: operatorChannelKey || operatorChannelType,
65
- operatorChannelType,
94
+ operatorChannelKey: channelKey || channelType,
95
+ operatorChannelType: channelType,
66
96
  ...principal,
67
97
  };
68
- const checkId = actor.principalId ?? '';
69
- if (isStaticAgentOwner(ctx.selfAid, checkId)) {
70
- return resultFor('owner', 'agent-owner', actor, ctx.selfAid, true);
98
+ const candidates = [];
99
+ if (hasTrustedPrincipal(actor) && isStaticAgentOwner(ctx.selfAid, actor.principalId)) {
100
+ candidates.push({ role: 'owner', source: 'agent-owner', sourcePriority: 100 });
71
101
  }
72
- if (isStaticAgentAdmin(ctx.selfAid, checkId)) {
73
- return resultFor('admin', 'agent-admin', actor, ctx.selfAid, true);
102
+ const actorRole = resolveActorDirectRole(ctx.selfAid, actor, channelType, operatorId);
103
+ if (ctx.chatType === 'private') {
104
+ if (actorRole)
105
+ candidates.push({ role: actorRole, source: 'relation-actor', sourcePriority: 80 });
106
+ const defaultRole = normalizeDefaultRole(readAgentDefaultRole(ctx.selfAid, 'private'), ctx.selfAid);
107
+ if (defaultRole)
108
+ candidates.push({ role: defaultRole, source: 'agent-default', sourcePriority: 40 });
74
109
  }
75
- const relationId = ctx.chatType === 'group' ? ctx.conversationId : ctx.actorId;
76
- const assigned = normalizeUserRole(readRelationRole(ctx.selfAid, ctx.channelType, relationId), ctx.selfAid);
77
- if (assigned)
78
- return resultFor(assigned, 'relation', actor, ctx.selfAid);
79
- const fallback = normalizeDefaultRole(readAgentDefaultRole(ctx.selfAid, ctx.chatType), ctx.selfAid);
80
- return fallback
81
- ? resultFor(fallback, 'agent-default', actor, ctx.selfAid)
82
- : roleRegistryExists(ctx.selfAid) && roleExists('visitor', ctx.selfAid)
83
- ? resultFor('visitor', 'builtin-fallback', actor, ctx.selfAid)
84
- : resultFor(null, 'none', actor, ctx.selfAid);
110
+ else {
111
+ if (actorRole)
112
+ candidates.push({ role: actorRole, source: 'relation-actor', sourcePriority: 80 });
113
+ const groupRole = normalizeRelationRole(readDirectRelationRole(ctx.selfAid, channelType, String(ctx.conversationId || '').trim(), 'group'), ctx.selfAid);
114
+ if (groupRole) {
115
+ candidates.push({ role: groupRole, source: 'relation-peer', sourcePriority: 60 });
116
+ }
117
+ else {
118
+ const defaultRole = normalizeDefaultRole(readAgentDefaultRole(ctx.selfAid, 'group'), ctx.selfAid);
119
+ if (defaultRole)
120
+ candidates.push({ role: defaultRole, source: 'agent-default', sourcePriority: 40 });
121
+ }
122
+ }
123
+ const winner = selectHighestRank(candidates, ctx.selfAid);
124
+ if (winner)
125
+ return resultFor(winner.role, winner.source, actor, ctx.selfAid);
126
+ return roleRegistryExists(ctx.selfAid) && roleExists('visitor', ctx.selfAid)
127
+ ? resultFor('visitor', 'builtin-fallback', actor, ctx.selfAid)
128
+ : resultFor(null, 'none', actor, ctx.selfAid);
85
129
  }
86
130
  export function roleToSessionIdentity(role) {
87
131
  return { role: role ?? 'none', mode: 'interactive' };
@@ -89,34 +133,56 @@ export function roleToSessionIdentity(role) {
89
133
  export function checkRoleAccess(role, selfAid) {
90
134
  if (!role)
91
135
  return false;
92
- if (isManagementRole(role))
136
+ // Owner access is an invariant. Admin access is role policy, just like the
137
+ // user roles, so a local admin definition may explicitly disable it.
138
+ if (role === 'owner')
93
139
  return true;
94
- const roleDef = getRoleDefinition(role, selfAid);
95
- if (!roleDef)
96
- return false;
97
- return roleDef.allowAccess ?? true;
140
+ const definition = getRoleDefinition(role, selfAid);
141
+ return !!definition && (definition.allowAccess ?? true);
142
+ }
143
+ function selectHighestRank(candidates, selfAid) {
144
+ let winner = null;
145
+ for (const candidate of candidates) {
146
+ if (!normalizeRelationRole(candidate.role, selfAid) && candidate.role !== 'owner')
147
+ continue;
148
+ if (!winner) {
149
+ winner = candidate;
150
+ continue;
151
+ }
152
+ const rank = getRoleRank(candidate.role, selfAid);
153
+ const winnerRank = getRoleRank(winner.role, selfAid);
154
+ if (rank > winnerRank || (rank === winnerRank && candidate.sourcePriority > winner.sourcePriority)) {
155
+ winner = candidate;
156
+ }
157
+ }
158
+ return winner;
159
+ }
160
+ function resolveActorDirectRole(selfAid, actor, channelType, actorId) {
161
+ const role = hasTrustedPrincipal(actor)
162
+ ? readDirectRelationRole(selfAid, 'aun', actor.principalId, 'private')
163
+ : actor.principalStatus === 'unmapped'
164
+ ? readDirectRelationRole(selfAid, channelType, actorId, 'private')
165
+ : null;
166
+ return normalizeRelationRole(role, selfAid);
98
167
  }
99
- function resultFor(role, source, actor, selfAid, forceAccess = false) {
168
+ function resultFor(role, source, actor, selfAid) {
100
169
  const exists = role ? (isManagementRole(role) || roleExists(role, selfAid)) : false;
101
170
  const roleDef = role ? getRoleDefinition(role, selfAid) : null;
102
171
  return {
103
172
  effectiveRole: exists ? role : null,
104
173
  source: exists ? source : 'none',
105
174
  isAuthenticated: hasTrustedPrincipal(actor),
106
- allowAccess: forceAccess || !!(roleDef && (roleDef.allowAccess ?? true)),
175
+ allowAccess: !!(roleDef && (roleDef.allowAccess ?? true)),
107
176
  roleExists: exists,
108
177
  actor,
109
178
  };
110
179
  }
111
- function listAgentAids(aid, field, opts = {}) {
180
+ function listAgentOwners(aid, opts = {}) {
112
181
  if (!aid)
113
182
  return [];
114
183
  try {
115
184
  const config = read(ConfigTarget.Agent, { self: aid }, { cache: opts.fresh !== true });
116
- const values = config?.[field];
117
- if (!Array.isArray(values))
118
- return [];
119
- return Array.from(new Set(values.map(value => String(value || '').trim()).filter(Boolean)));
185
+ return Array.from(new Set((config?.owners ?? []).map(value => String(value || '').trim()).filter(Boolean)));
120
186
  }
121
187
  catch {
122
188
  return [];
@@ -125,15 +191,30 @@ function listAgentAids(aid, field, opts = {}) {
125
191
  function readAgentDefaultRole(aid, chatType) {
126
192
  return getStoredRoleRegistry(aid).defaultRoles[chatType] ?? null;
127
193
  }
128
- function readRelationRole(selfAid, channelType, channelId) {
194
+ function readDirectRelationRole(selfAid, channelType, channelId, expectedTargetKind) {
129
195
  if (!selfAid || !channelType || !channelId)
130
196
  return undefined;
197
+ if (expectedTargetKind === 'private' && channelType === 'aun' && isExplicitGroupId(channelId))
198
+ return undefined;
131
199
  const peerKey = formatPeerKey(channelType, channelId);
132
200
  const config = read(ConfigTarget.Relation, { self: selfAid, peerKey }, { cache: true });
201
+ if (expectedTargetKind && config?.targetKind && config.targetKind !== expectedTargetKind)
202
+ return undefined;
133
203
  return config?.role;
134
204
  }
135
- function normalizeUserRole(value, selfAid) {
136
- if (typeof value !== 'string' || !roleExists(value, selfAid))
205
+ function normalizeRelationRole(value, selfAid) {
206
+ if (typeof value !== 'string' || !value || value === 'owner')
137
207
  return null;
138
- return value;
208
+ if (value === 'admin')
209
+ return getRoleDefinition('admin', selfAid) ? 'admin' : null;
210
+ return roleExists(value, selfAid) ? value : null;
211
+ }
212
+ function fileRevision(file) {
213
+ try {
214
+ const stat = fs.statSync(file);
215
+ return [stat.size, stat.mtimeMs].join(':');
216
+ }
217
+ catch {
218
+ return 'missing';
219
+ }
139
220
  }
@@ -0,0 +1,18 @@
1
+ export const ROLE_RANKS = Object.freeze({
2
+ owner: 900,
3
+ admin: 700,
4
+ custom: 500,
5
+ member: 300,
6
+ visitor: 100,
7
+ });
8
+ export function expectedRoleRank(role) {
9
+ if (role === 'owner')
10
+ return ROLE_RANKS.owner;
11
+ if (role === 'admin')
12
+ return ROLE_RANKS.admin;
13
+ if (role === 'member')
14
+ return ROLE_RANKS.member;
15
+ if (role === 'visitor')
16
+ return ROLE_RANKS.visitor;
17
+ return ROLE_RANKS.custom;
18
+ }
@@ -1,10 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
4
- import { agentRoleConfig, agentRolesIndex, kitsRoleTemplatesDir, resolvePaths } from '../paths.js';
4
+ import { agentRelationConfig, agentRelationsDir, agentRoleConfig, agentRolesIndex } from '../paths.js';
5
5
  import { loadSchema } from './schema-registry.js';
6
6
  import { clearRoleStoreCache, getStoredRoleRegistry, isRoleName, readRoleStore, roleRegistryExists, } from './role-store.js';
7
- import { isManagementRole } from './builtin-roles.js';
7
+ import { expectedRoleRank, isManagementRole } from './builtin-roles.js';
8
+ import { readBuiltinRoleTemplate } from './builtin-role-templates.js';
8
9
  import { validateRoleConfig } from './role-schema.js';
9
10
  export class RoleServiceError extends Error {
10
11
  code;
@@ -42,7 +43,7 @@ export function writeRoleRegistry(selfAid, value) {
42
43
  export function writeRoleDefinition(selfAid, roleId, value) {
43
44
  if (!isRoleName(roleId))
44
45
  throw new RoleServiceError('INVALID_ROLE_ID', `Invalid role id: ${roleId}`);
45
- const normalized = { ...value, $schema_version: 1 };
46
+ const normalized = { ...value, rank: expectedRoleRank(roleId), $schema_version: 1 };
46
47
  validateRoleDefinition(normalized);
47
48
  const registry = readRoleRegistry(selfAid);
48
49
  if (!registry.roles.includes(roleId)) {
@@ -92,7 +93,7 @@ export function listRoleDiagnostics(selfAid) {
92
93
  return readRoleStore(selfAid).diagnostics;
93
94
  }
94
95
  function writeRoleDefinitionFile(selfAid, roleId, value) {
95
- const normalized = { ...value, $schema_version: 1 };
96
+ const normalized = { ...value, rank: expectedRoleRank(roleId), $schema_version: 1 };
96
97
  validateRoleDefinition(normalized);
97
98
  const file = agentRoleConfig(selfAid, roleId);
98
99
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -108,7 +109,7 @@ function validateRegistry(selfAid, value) {
108
109
  throw new RoleServiceError('VISITOR_REQUIRED', 'visitor must remain registered');
109
110
  for (const role of ['private', 'group']) {
110
111
  const selected = registry.defaultRoles[role];
111
- if (selected !== null && (isManagementRole(selected) || !registry.roles.includes(selected))) {
112
+ if (selected !== null && (selected === 'owner' || !registry.roles.includes(selected))) {
112
113
  throw new RoleServiceError('INVALID_DEFAULT_ROLE', `defaultRoles.${role} must reference an assignable registered role`);
113
114
  }
114
115
  if (selected !== null && !hasValidRoleDefinition(selfAid, selected)) {
@@ -117,18 +118,14 @@ function validateRegistry(selfAid, value) {
117
118
  }
118
119
  }
119
120
  function hasValidRoleDefinition(selfAid, roleId) {
120
- for (const file of [
121
- agentRoleConfig(selfAid, roleId),
122
- path.join(kitsRoleTemplatesDir(), `${roleId}.json`),
123
- ]) {
124
- try {
125
- const value = atomicReadJson(file);
126
- if (value !== null && validateRoleConfig(value).length === 0)
127
- return true;
128
- }
129
- catch { }
121
+ try {
122
+ const value = atomicReadJson(agentRoleConfig(selfAid, roleId));
123
+ if (value !== null && validateRoleConfig(value).length === 0
124
+ && (value.rank === undefined || value.rank === expectedRoleRank(roleId)))
125
+ return true;
130
126
  }
131
- return false;
127
+ catch { }
128
+ return !!readBuiltinRoleTemplate(roleId).definition;
132
129
  }
133
130
  function validateRoleDefinition(value) {
134
131
  const issues = validateRoleConfig(value);
@@ -142,13 +139,13 @@ function findRoleReferences(selfAid, roleId, registry) {
142
139
  references.push('defaultRoles.private');
143
140
  if (registry.defaultRoles.group === roleId)
144
141
  references.push('defaultRoles.group');
145
- const relations = path.join(resolvePaths().agentsDir, selfAid, 'relations');
142
+ const relations = agentRelationsDir(selfAid);
146
143
  if (!fs.existsSync(relations))
147
144
  return references;
148
145
  for (const entry of fs.readdirSync(relations, { withFileTypes: true })) {
149
- if (!entry.isDirectory())
146
+ if (!entry.isDirectory() || entry.name.startsWith('_'))
150
147
  continue;
151
- const config = atomicReadJson(path.join(relations, entry.name, 'config.json'));
148
+ const config = atomicReadJson(agentRelationConfig(selfAid, entry.name));
152
149
  if (config?.role === roleId)
153
150
  references.push(`relations/${entry.name}/role`);
154
151
  }
@@ -2,12 +2,11 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { atomicReadJson } from '../utils/atomic-write.js';
4
4
  import { agentRoleConfig, agentRolesDir, agentRolesIndex, kitsRoleTemplatesDir, resolvePaths, } from '../paths.js';
5
- import { applyManagementRoleCeiling, getManagementRoleDefinition } from './builtin-roles.js';
5
+ import { applyManagementRoleCeiling, expectedRoleRank, getManagementRoleDefinition } from './builtin-roles.js';
6
6
  import { readBuiltinRoleTemplate } from './builtin-role-templates.js';
7
7
  import { loadSchema } from './schema-registry.js';
8
8
  import { validateRoleConfig } from './role-schema.js';
9
9
  const ROLE_NAME_RE = /^[a-z0-9_-]+$/;
10
- const RESERVED_ROLES = new Set(['owner', 'admin']);
11
10
  const BUILTIN_ROLES = ['owner', 'admin', 'member', 'visitor'];
12
11
  const cache = new Map();
13
12
  export function clearRoleStoreCache(selfAid) {
@@ -79,9 +78,11 @@ export function getStoredRoleRegistry(selfAid) {
79
78
  return readRoleStore(selfAid).registry;
80
79
  }
81
80
  export function roleIsAssignable(selfAid, roleId) {
82
- if (!roleId || !isRoleName(roleId) || RESERVED_ROLES.has(roleId))
81
+ if (!roleId || !isRoleName(roleId) || roleId === 'owner')
83
82
  return false;
84
83
  const snapshot = readRoleStore(selfAid);
84
+ if (roleId === 'admin')
85
+ return snapshot.registry.roles.includes(roleId) && !!getStoredRoleDefinition(selfAid, roleId);
85
86
  return snapshot.registry.roles.includes(roleId) && !!snapshot.definitions[roleId];
86
87
  }
87
88
  function readRegistry(selfAid, diagnostics) {
@@ -106,7 +107,7 @@ function readRegistry(selfAid, diagnostics) {
106
107
  const defaultRoles = { ...value.defaultRoles };
107
108
  for (const scene of ['private', 'group']) {
108
109
  const selected = defaultRoles[scene];
109
- if (selected === null || (roles.includes(selected) && !RESERVED_ROLES.has(selected)))
110
+ if (selected === null || (roles.includes(selected) && selected !== 'owner'))
110
111
  continue;
111
112
  diagnostics.push({
112
113
  code: 'INVALID_ROLE_ASSIGNMENT',
@@ -156,7 +157,17 @@ function readDefinition(selfAid, roleId, diagnostics) {
156
157
  const issues = validateRoleConfig(raw);
157
158
  if (issues.length === 0) {
158
159
  const { $schema_version: _schemaVersion, ...definition } = raw;
159
- return definition;
160
+ const expectedRank = expectedRoleRank(roleId);
161
+ if (definition.rank !== undefined && definition.rank !== expectedRank) {
162
+ diagnostics.push({
163
+ code: 'INVALID_ROLE_FILE',
164
+ roleId,
165
+ file: path.relative(resolvePaths().root, file),
166
+ message: `Role definition ${roleId} rank must be ${expectedRank}`,
167
+ });
168
+ continue;
169
+ }
170
+ return { ...definition, rank: expectedRank };
160
171
  }
161
172
  diagnostics.push({
162
173
  code: file === local ? 'INVALID_ROLE_FILE' : 'INVALID_ROLE_FILE',
@@ -1,4 +1,4 @@
1
- import { getBuiltinRolesConfig, getManagementRoleDefinition, isManagementRole, isReservedRoleName, } from './builtin-roles.js';
1
+ import { getBuiltinRolesConfig, getManagementRoleDefinition, expectedRoleRank, isManagementRole, isReservedRoleName, } from './builtin-roles.js';
2
2
  import { clearRoleStoreCache, getStoredRoleDefinition, readRoleStore, roleIsAssignable, } from './role-store.js';
3
3
  const ROLE_NAME_RE = /^[a-z0-9_-]+$/;
4
4
  export { getBuiltinRolesConfig };
@@ -51,5 +51,14 @@ export function normalizeDefaultRole(value, selfAid) {
51
51
  return null;
52
52
  if (typeof value !== 'string')
53
53
  return null;
54
+ if (value === 'owner')
55
+ return null;
56
+ if (value === 'admin')
57
+ return getRoleDefinition(value, selfAid) ? value : null;
54
58
  return roleExists(value, selfAid) ? value : null;
55
59
  }
60
+ export function getRoleRank(role, selfAid) {
61
+ if (!role)
62
+ return 0;
63
+ return getRoleDefinition(role, selfAid)?.rank ?? expectedRoleRank(role);
64
+ }
@@ -14,6 +14,7 @@ import fs from 'fs';
14
14
  import path from 'path';
15
15
  import Ajv from 'ajv';
16
16
  import { kitsSchemasDir } from '../paths.js';
17
+ import { createErrorCapturingReader, fileCache } from '../core/daemon-file-cache.js';
17
18
  let _ajv = null;
18
19
  function ajv() {
19
20
  if (!_ajv) {
@@ -21,13 +22,23 @@ function ajv() {
21
22
  }
22
23
  return _ajv;
23
24
  }
24
- let _meta = null;
25
+ /**
26
+ * schema 随包发布,运行期不查盘 → fileCache 的 on-reload 策略,归入 'kits' 组
27
+ * (与 manifest/fragment 同组,一次 invalidateGroup('kits') 全刷)。
28
+ * 刷新时机只有三个:进程重启、`ec agent reload`、resync——后两者在调 registry.reload
29
+ * 之前先 invalidateKitCache()。带外改 schema 文件而不做上述任一动作,进程不会看见。
30
+ */
31
+ const SCHEMA_CACHE_OPTS = { policy: 'on-reload', group: 'kits' };
25
32
  export function loadMeta() {
26
- if (_meta)
27
- return _meta;
28
33
  const p = path.join(kitsSchemasDir(), '_meta.json');
29
- _meta = JSON.parse(fs.readFileSync(p, 'utf-8'));
30
- return _meta;
34
+ const reader = createErrorCapturingReader();
35
+ return fileCache.get(p, (raw) => {
36
+ // errno 文案自带路径,不再重复拼 p;原始异常挂 cause,保住 code/path/errno
37
+ if (raw === null) {
38
+ throw new Error(`[schema] cannot read meta: ${reader.lastError()}`, { cause: reader.lastErrorCause() });
39
+ }
40
+ return JSON.parse(raw);
41
+ }, { ...SCHEMA_CACHE_OPTS, read: reader.read });
31
42
  }
32
43
  /** 某 schema 的当前版本号(来自 _meta.json)。 */
33
44
  export function currentVersion(name) {
@@ -84,7 +95,6 @@ export function readRawSchema(name, version) {
84
95
  }
85
96
  return JSON.parse(fs.readFileSync(file, 'utf-8'));
86
97
  }
87
- const _cache = new Map();
88
98
  function schemaFilePath(name, version) {
89
99
  return path.join(kitsSchemasDir(), `${name}.schema.${version}.json`);
90
100
  }
@@ -117,22 +127,35 @@ function inferMerge(spec) {
117
127
  /** 加载指定版本的 schema entry(带 ajv 编译 + 字段表)。 */
118
128
  export function loadSchema(name, version) {
119
129
  const ver = version ?? currentVersion(name);
120
- const key = `${name}@${ver}`;
121
- const cached = _cache.get(key);
122
- if (cached)
123
- return cached;
124
130
  const file = schemaFilePath(name, ver);
125
- const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
126
- const entry = {
127
- logicalName: name,
128
- version: ver,
129
- scope: raw['x-scope'] || name,
130
- raw,
131
- validate: ajv().compile(raw),
132
- fields: extractFields(raw),
133
- };
134
- _cache.set(key, entry);
135
- return entry;
131
+ const reader = createErrorCapturingReader();
132
+ return fileCache.get(file, (rawText) => {
133
+ // errno 文案自带路径,不再重复拼 file;原始异常挂 cause,保住 code/path/errno
134
+ if (rawText === null) {
135
+ throw new Error(`[schema] cannot read schema file: ${reader.lastError()}`, { cause: reader.lastErrorCause() });
136
+ }
137
+ const raw = JSON.parse(rawText);
138
+ return {
139
+ logicalName: name,
140
+ version: ver,
141
+ scope: raw['x-scope'] || name,
142
+ raw,
143
+ validate: compileSchema(raw),
144
+ fields: extractFields(raw),
145
+ };
146
+ }, { ...SCHEMA_CACHE_OPTS, read: reader.read });
147
+ }
148
+ /**
149
+ * 每个 schema 文件都带 `$id`,而 ajv 对同一实例重复注册同名 `$id` 会抛
150
+ * "schema with key or id ... already exists"。缓存失效后会重新 JSON.parse 出
151
+ * 一个新对象(ajv 的对象级缓存认不出),因此编译前先按 `$id` 撤销旧注册,
152
+ * 让 reload → 重新编译这条路径可重复执行。
153
+ */
154
+ function compileSchema(raw) {
155
+ const id = raw?.$id;
156
+ if (typeof id === 'string')
157
+ ajv().removeSchema(id);
158
+ return ajv().compile(raw);
136
159
  }
137
160
  /** extra_backup 不得指向 .env(构建期校验)——遍历声明里的 pattern。 */
138
161
  export function assertExtraBackupNotEnv(extraBackup) {
@@ -146,9 +169,11 @@ export function assertExtraBackupNotEnv(extraBackup) {
146
169
  }
147
170
  }
148
171
  }
149
- /** 测试用:清空缓存。 */
172
+ /**
173
+ * 测试用:清空缓存。schema 与 _meta 都存在 fileCache 的 'kits' 组,
174
+ * 连同 ajv 实例一起重建(丢弃已注册的 $id,避免跨用例串味)。
175
+ */
150
176
  export function _resetSchemaCache() {
151
- _cache.clear();
152
- _meta = null;
177
+ fileCache.invalidateGroup('kits');
153
178
  _ajv = null;
154
179
  }
@@ -25,10 +25,11 @@ import { CONFIG_SCHEMA_VERSION } from './types.js';
25
25
  import { ConfigTarget, read as cfgRead, write as cfgWrite } from './config/config-manager.js';
26
26
  import { expandVars, buildEnvResolver } from './config/merge.js';
27
27
  import { logger } from './utils/logger.js';
28
+ import { parseStableSemver } from './utils/stable-semver.js';
28
29
  /** 读 {root}/daemon.json。文件不存在返回 {},不报错。 */
29
30
  export function loadDaemonConfig() {
30
31
  const raw = atomicReadJson(resolvePaths().daemonConfig);
31
- return raw ?? {};
32
+ return validateDaemonConfig(raw ?? {});
32
33
  }
33
34
  let eckSnapshotsConfigCache;
34
35
  /** Parse the process-level snapshot gate once for the current daemon lifecycle. */
@@ -49,7 +50,18 @@ export function isEckSnapshotsEnabled() {
49
50
  }
50
51
  /** 原子写入 {root}/daemon.json。调用方负责传完整对象(含要保留的字段)。 */
51
52
  export function saveDaemonConfig(value) {
52
- atomicWriteJson(resolvePaths().daemonConfig, value);
53
+ atomicWriteJson(resolvePaths().daemonConfig, validateDaemonConfig(value));
54
+ }
55
+ function validateDaemonConfig(value) {
56
+ const minEvolVersion = value.aun?.minEvolVersion;
57
+ if (minEvolVersion !== undefined && !parseStableSemver(minEvolVersion)) {
58
+ throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
59
+ }
60
+ const menuTokenRequired = value.aun?.menuTokenRequired;
61
+ if (menuTokenRequired !== undefined && typeof menuTokenRequired !== 'boolean') {
62
+ throw new Error('daemon.json.aun.menuTokenRequired must be a boolean');
63
+ }
64
+ return value;
53
65
  }
54
66
  const SUPPORTED_CHANNEL_TYPES = new Set([
55
67
  'aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom',
@@ -478,7 +490,6 @@ export async function migrateProject(oldPath, newPath) {
478
490
  const active = readJsonFile(activePath);
479
491
  if (active && active.projectPath === oldAbs) {
480
492
  active.projectPath = newAbs;
481
- active.updatedAt = Date.now();
482
493
  atomicWriteJson(activePath, active);
483
494
  updated++;
484
495
  }
@@ -489,7 +500,6 @@ export async function migrateProject(oldPath, newPath) {
489
500
  const meta = readLastJsonlLine(metaPath);
490
501
  if (meta && meta.projectPath === oldAbs) {
491
502
  meta.projectPath = newAbs;
492
- meta.updatedAt = Date.now();
493
503
  appendJsonl(metaPath, meta);
494
504
  updated++;
495
505
  }