evolcore 0.0.2 → 0.0.3

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 (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. 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' };
@@ -91,32 +135,52 @@ export function checkRoleAccess(role, selfAid) {
91
135
  return false;
92
136
  if (isManagementRole(role))
93
137
  return true;
94
- const roleDef = getRoleDefinition(role, selfAid);
95
- if (!roleDef)
96
- return false;
97
- return roleDef.allowAccess ?? true;
138
+ const definition = getRoleDefinition(role, selfAid);
139
+ return !!definition && (definition.allowAccess ?? true);
140
+ }
141
+ function selectHighestRank(candidates, selfAid) {
142
+ let winner = null;
143
+ for (const candidate of candidates) {
144
+ if (!normalizeRelationRole(candidate.role, selfAid) && candidate.role !== 'owner')
145
+ continue;
146
+ if (!winner) {
147
+ winner = candidate;
148
+ continue;
149
+ }
150
+ const rank = getRoleRank(candidate.role, selfAid);
151
+ const winnerRank = getRoleRank(winner.role, selfAid);
152
+ if (rank > winnerRank || (rank === winnerRank && candidate.sourcePriority > winner.sourcePriority)) {
153
+ winner = candidate;
154
+ }
155
+ }
156
+ return winner;
157
+ }
158
+ function resolveActorDirectRole(selfAid, actor, channelType, actorId) {
159
+ const role = hasTrustedPrincipal(actor)
160
+ ? readDirectRelationRole(selfAid, 'aun', actor.principalId, 'private')
161
+ : actor.principalStatus === 'unmapped'
162
+ ? readDirectRelationRole(selfAid, channelType, actorId, 'private')
163
+ : null;
164
+ return normalizeRelationRole(role, selfAid);
98
165
  }
99
- function resultFor(role, source, actor, selfAid, forceAccess = false) {
166
+ function resultFor(role, source, actor, selfAid) {
100
167
  const exists = role ? (isManagementRole(role) || roleExists(role, selfAid)) : false;
101
168
  const roleDef = role ? getRoleDefinition(role, selfAid) : null;
102
169
  return {
103
170
  effectiveRole: exists ? role : null,
104
171
  source: exists ? source : 'none',
105
172
  isAuthenticated: hasTrustedPrincipal(actor),
106
- allowAccess: forceAccess || !!(roleDef && (roleDef.allowAccess ?? true)),
173
+ allowAccess: !!(roleDef && (roleDef.allowAccess ?? true)),
107
174
  roleExists: exists,
108
175
  actor,
109
176
  };
110
177
  }
111
- function listAgentAids(aid, field, opts = {}) {
178
+ function listAgentOwners(aid, opts = {}) {
112
179
  if (!aid)
113
180
  return [];
114
181
  try {
115
182
  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)));
183
+ return Array.from(new Set((config?.owners ?? []).map(value => String(value || '').trim()).filter(Boolean)));
120
184
  }
121
185
  catch {
122
186
  return [];
@@ -125,15 +189,30 @@ function listAgentAids(aid, field, opts = {}) {
125
189
  function readAgentDefaultRole(aid, chatType) {
126
190
  return getStoredRoleRegistry(aid).defaultRoles[chatType] ?? null;
127
191
  }
128
- function readRelationRole(selfAid, channelType, channelId) {
192
+ function readDirectRelationRole(selfAid, channelType, channelId, expectedTargetKind) {
129
193
  if (!selfAid || !channelType || !channelId)
130
194
  return undefined;
195
+ if (expectedTargetKind === 'private' && channelType === 'aun' && isExplicitGroupId(channelId))
196
+ return undefined;
131
197
  const peerKey = formatPeerKey(channelType, channelId);
132
198
  const config = read(ConfigTarget.Relation, { self: selfAid, peerKey }, { cache: true });
199
+ if (expectedTargetKind && config?.targetKind && config.targetKind !== expectedTargetKind)
200
+ return undefined;
133
201
  return config?.role;
134
202
  }
135
- function normalizeUserRole(value, selfAid) {
136
- if (typeof value !== 'string' || !roleExists(value, selfAid))
203
+ function normalizeRelationRole(value, selfAid) {
204
+ if (typeof value !== 'string' || !value || value === 'owner')
137
205
  return null;
138
- return value;
206
+ if (value === 'admin')
207
+ return getRoleDefinition('admin', selfAid) ? 'admin' : null;
208
+ return roleExists(value, selfAid) ? value : null;
209
+ }
210
+ function fileRevision(file) {
211
+ try {
212
+ const stat = fs.statSync(file);
213
+ return [stat.size, stat.mtimeMs].join(':');
214
+ }
215
+ catch {
216
+ return 'missing';
217
+ }
139
218
  }
@@ -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
+ }
@@ -478,7 +478,6 @@ export async function migrateProject(oldPath, newPath) {
478
478
  const active = readJsonFile(activePath);
479
479
  if (active && active.projectPath === oldAbs) {
480
480
  active.projectPath = newAbs;
481
- active.updatedAt = Date.now();
482
481
  atomicWriteJson(activePath, active);
483
482
  updated++;
484
483
  }
@@ -489,7 +488,6 @@ export async function migrateProject(oldPath, newPath) {
489
488
  const meta = readLastJsonlLine(metaPath);
490
489
  if (meta && meta.projectPath === oldAbs) {
491
490
  meta.projectPath = newAbs;
492
- meta.updatedAt = Date.now();
493
491
  appendJsonl(metaPath, meta);
494
492
  updated++;
495
493
  }
@@ -47,7 +47,16 @@ function redactIdentifier(value) {
47
47
  return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
48
48
  }
49
49
  function logAuditEvent(record) {
50
- const level = record.decision === 'deny' ? 'warn' : 'info';
50
+ // A role-management snapshot fans out into several successful read checks.
51
+ // Keep those available for verbose ECWeb diagnostics without flooding INFO.
52
+ const routineEcwebRoleRead = record.source === 'ecweb'
53
+ && !record.dangerous
54
+ && (record.operation === 'role.policy.read' || record.operation === 'role.relation.read');
55
+ const level = record.decision === 'deny'
56
+ ? 'warn'
57
+ : routineEcwebRoleRead
58
+ ? 'debug'
59
+ : 'info';
51
60
  const marker = record.decision === 'deny' ? 'DENY' : record.dangerous ? 'DANGEROUS_ALLOW' : 'ALLOW';
52
61
  const message = [
53
62
  `[CommandAudit:${marker}]`,
@@ -55,6 +55,8 @@ export const USER_PLANE_CAPABILITY_CEILING = {
55
55
  ]),
56
56
  denyNamespaces: new Set([
57
57
  'role',
58
+ 'connect',
59
+ 'contact',
58
60
  'agent',
59
61
  'system',
60
62
  'storage',
@@ -304,6 +304,62 @@ const OPERATIONS = [
304
304
  description: 'Modify role definitions, command permissions, default roles, or usage limits',
305
305
  sources: ['menu', 'ecweb', 'control'],
306
306
  },
307
+ {
308
+ id: 'contact.read',
309
+ category: 'read',
310
+ dangerous: false,
311
+ defaultScopes: ['agent'],
312
+ description: 'Read the Agent contact block list',
313
+ sources: ['agent-tool'],
314
+ },
315
+ {
316
+ id: 'contact.block',
317
+ category: 'write-agent',
318
+ dangerous: false,
319
+ defaultScopes: ['agent'],
320
+ description: 'Block or unblock an AUN contact',
321
+ sources: ['agent-tool'],
322
+ },
323
+ {
324
+ id: 'connect.read',
325
+ category: 'read',
326
+ dangerous: false,
327
+ defaultScopes: ['agent'],
328
+ description: 'Read contacts, groups, assignments, or contact details',
329
+ sources: ['menu', 'ecweb', 'control'],
330
+ },
331
+ {
332
+ id: 'connect.assign',
333
+ category: 'write-agent',
334
+ dangerous: false,
335
+ defaultScopes: ['agent'],
336
+ description: 'Assign a role to a relation',
337
+ sources: ['menu', 'ecweb', 'control'],
338
+ },
339
+ {
340
+ id: 'connect.revoke',
341
+ category: 'write-agent',
342
+ dangerous: false,
343
+ defaultScopes: ['agent'],
344
+ description: 'Revoke a role assignment from a relation',
345
+ sources: ['menu', 'ecweb', 'control'],
346
+ },
347
+ {
348
+ id: 'connect.admin',
349
+ category: 'write-agent',
350
+ dangerous: false,
351
+ defaultScopes: ['agent'],
352
+ description: 'Grant or revoke admin status',
353
+ sources: ['menu', 'ecweb', 'control'],
354
+ },
355
+ {
356
+ id: 'connect.write',
357
+ category: 'write-agent',
358
+ dangerous: false,
359
+ defaultScopes: ['agent'],
360
+ description: 'Add contacts or modify contact status (block/unblock)',
361
+ sources: ['menu', 'ecweb', 'control'],
362
+ },
307
363
  // Agent Operations
308
364
  {
309
365
  id: 'agent.list',