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,5 +1,6 @@
1
1
  import { createHash } from 'crypto';
2
2
  export const MENU_REQUEST_TYPES = new Set([
3
+ 'menu.token.request',
3
4
  'menu.list',
4
5
  'menu.query',
5
6
  'menu.options',
@@ -12,6 +13,7 @@ const EXPLICIT_SCOPE_UPDATE_NAMES = new Set([
12
13
  'chatmode',
13
14
  'dispatch',
14
15
  'mentionmode',
16
+ 'group',
15
17
  'permission',
16
18
  ]);
17
19
  const EXPLICIT_SCOPE_UPDATE_COMMANDS = new Set([
@@ -20,6 +22,7 @@ const EXPLICIT_SCOPE_UPDATE_COMMANDS = new Set([
20
22
  '/chatmode',
21
23
  '/dispatch',
22
24
  '/mentionmode',
25
+ '/group',
23
26
  '/perm',
24
27
  ]);
25
28
  function firstCommandToken(value) {
@@ -78,6 +81,8 @@ export function validateMenuRequest(request) {
78
81
  if (!MENU_REQUEST_TYPES.has(type)) {
79
82
  return { code: 'METHOD_NOT_FOUND', message: `Unknown menu request type: ${type}` };
80
83
  }
84
+ if (type === 'menu.token.request')
85
+ return null;
81
86
  const hasName = typeof request.name === 'string' && request.name.trim().length > 0;
82
87
  const hasCompatCmd = typeof request.cmd === 'string' && request.cmd.trim().length > 0;
83
88
  if (type !== 'menu.list' && !hasName && !hasCompatCmd) {
@@ -144,8 +149,9 @@ const INVALID_ARGUMENT_CODES = new Set([
144
149
  const STABLE_CODES = new Set([
145
150
  'ROLE_ACCESS_DENIED', 'PERMISSION_DENIED', 'UNAUTHORIZED', 'FORBIDDEN',
146
151
  'INVALID_ARGUMENT', 'NOT_ALLOWED', 'NOT_SUPPORTED', 'UNSUPPORTED',
147
- 'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXECUTION_TIMEOUT',
148
- 'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE',
152
+ 'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXPIRED', 'EXECUTION_TIMEOUT',
153
+ 'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
154
+ 'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
149
155
  ]);
150
156
  export function normalizeMenuError(error) {
151
157
  const source = error && typeof error === 'object' ? error : {};
@@ -0,0 +1,102 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'crypto';
2
+ import path from 'path';
3
+ import { agentDataDir } from '../../paths.js';
4
+ import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
5
+ export const DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS = 30 * 24 * 60 * 60;
6
+ export const MAX_MENU_TOKENS_PER_AGENT = 1_000;
7
+ function hashToken(token) {
8
+ return createHash('sha256').update(token).digest('hex');
9
+ }
10
+ function isTokenRecord(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value))
12
+ return false;
13
+ const record = value;
14
+ return typeof record.peerAid === 'string'
15
+ && record.peerAid.trim().length > 0
16
+ && typeof record.tokenHash === 'string'
17
+ && /^[a-f0-9]{64}$/.test(record.tokenHash)
18
+ && typeof record.requestedAt === 'number'
19
+ && Number.isFinite(record.requestedAt)
20
+ && record.requestedAt >= 0
21
+ && typeof record.lastUsedAt === 'number'
22
+ && Number.isFinite(record.lastUsedAt)
23
+ && record.lastUsedAt >= 0
24
+ && typeof record.idleTimeout === 'number'
25
+ && Number.isInteger(record.idleTimeout)
26
+ && record.idleTimeout > 0;
27
+ }
28
+ function isExpired(record, now) {
29
+ return now - record.lastUsedAt >= record.idleTimeout * 1000;
30
+ }
31
+ function hashesEqual(left, right) {
32
+ const leftBytes = Buffer.from(left, 'hex');
33
+ const rightBytes = Buffer.from(right, 'hex');
34
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
35
+ }
36
+ export function menuTokenStorePath(selfAid) {
37
+ return path.join(agentDataDir(selfAid), 'menu-tokens.json');
38
+ }
39
+ export class MenuTokenStore {
40
+ maxRecords;
41
+ constructor(maxRecords = MAX_MENU_TOKENS_PER_AGENT) {
42
+ this.maxRecords = maxRecords;
43
+ }
44
+ issue(selfAid, peerAid, metadata = {}, now = Date.now()) {
45
+ const file = this.read(selfAid);
46
+ const idleTimeout = DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS;
47
+ const tokens = file.tokens.filter(record => record.peerAid !== peerAid && !isExpired(record, now));
48
+ if (tokens.length >= this.maxRecords) {
49
+ if (tokens.length !== file.tokens.length)
50
+ this.write(selfAid, tokens);
51
+ return null;
52
+ }
53
+ const menuToken = `mt_${randomBytes(32).toString('base64url')}`;
54
+ tokens.push({
55
+ peerAid,
56
+ tokenHash: hashToken(menuToken),
57
+ requestedAt: now,
58
+ lastUsedAt: now,
59
+ idleTimeout,
60
+ ...(metadata.clientName ? { clientName: metadata.clientName } : {}),
61
+ ...(metadata.clientVersion ? { clientVersion: metadata.clientVersion } : {}),
62
+ ...(metadata.evolVersion ? { evolVersion: metadata.evolVersion } : {}),
63
+ });
64
+ this.write(selfAid, tokens);
65
+ return { menuToken, idleTimeout };
66
+ }
67
+ use(selfAid, peerAid, menuToken, now = Date.now()) {
68
+ const file = this.read(selfAid);
69
+ const activeTokens = file.tokens.filter(record => !isExpired(record, now));
70
+ const tokenHash = typeof menuToken === 'string' && menuToken.length > 0 && menuToken.length <= 256
71
+ ? hashToken(menuToken)
72
+ : undefined;
73
+ const matchingIndex = tokenHash
74
+ ? activeTokens.findIndex(record => record.peerAid === peerAid && hashesEqual(record.tokenHash, tokenHash))
75
+ : -1;
76
+ if (matchingIndex < 0) {
77
+ if (activeTokens.length !== file.tokens.length)
78
+ this.write(selfAid, activeTokens);
79
+ return false;
80
+ }
81
+ activeTokens[matchingIndex] = { ...activeTokens[matchingIndex], lastUsedAt: now };
82
+ this.write(selfAid, activeTokens);
83
+ return true;
84
+ }
85
+ read(selfAid) {
86
+ try {
87
+ const value = atomicReadJson(menuTokenStorePath(selfAid));
88
+ if (!value || typeof value !== 'object' || Array.isArray(value))
89
+ return { schemaVersion: 1, tokens: [] };
90
+ const file = value;
91
+ if (file.schemaVersion !== 1 || !Array.isArray(file.tokens))
92
+ return { schemaVersion: 1, tokens: [] };
93
+ return { schemaVersion: 1, tokens: file.tokens.filter(isTokenRecord) };
94
+ }
95
+ catch {
96
+ return { schemaVersion: 1, tokens: [] };
97
+ }
98
+ }
99
+ write(selfAid, tokens) {
100
+ atomicWriteJson(menuTokenStorePath(selfAid), { schemaVersion: 1, tokens });
101
+ }
102
+ }
@@ -3,8 +3,8 @@ import path from 'path';
3
3
  import crypto from 'crypto';
4
4
  import { ConfigTarget, read, resolveEffectiveWithSources, write } from '../../config/config-manager.js';
5
5
  import { getBuiltinRolesConfig, getRoleDefinition, isValidUserRoleName, readRolesConfig, } from '../../config/roles.js';
6
- import { BUILTIN_USER_ROLES, MANAGEMENT_ROLES, isManagementRole } from '../../config/builtin-roles.js';
7
- import { checkRoleAccess, isStaticAgentAdmin, isStaticAgentOwner, resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
6
+ import { BUILTIN_USER_ROLES, MANAGEMENT_ROLES, expectedRoleRank, isManagementRole } from '../../config/builtin-roles.js';
7
+ import { checkRoleAccess, isStaticAgentOwner, resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
8
8
  import { relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
9
9
  import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
10
10
  import { listOperations } from '../auth/operation-catalog.js';
@@ -55,8 +55,10 @@ export function roleMenuOperation(kind, args, value) {
55
55
  }
56
56
  if (kind === 'action')
57
57
  return 'role.policy.write';
58
- if (args?.resource !== 'assignment')
58
+ if (args?.resource !== 'assignment' && args?.resource !== 'admin')
59
59
  return 'role.policy.write';
60
+ if (args?.resource === 'admin')
61
+ return 'role.assign';
60
62
  try {
61
63
  return JSON.parse(value ?? '') === null ? 'role.revoke' : 'role.assign';
62
64
  }
@@ -168,9 +170,11 @@ export async function roleMenuUpdate(context, args, rawValue) {
168
170
  result = updateDefinition(context.self, args, value);
169
171
  }
170
172
  else if (resource === 'defaultRoles')
171
- result = updateDefaultRoles(context.self, args, value);
173
+ result = updateDefaultRoles(context, args, value);
172
174
  else if (resource === 'assignment')
173
175
  result = await updateAssignment(context, args, value);
176
+ else if (resource === 'admin')
177
+ result = await updateAdminAssignment(context, args, value);
174
178
  else
175
179
  throw roleError('NOT_SUPPORTED', 'The requested Role resource is not supported', context.self, { resource: resource || null });
176
180
  auditMutation(context, args, before, result, resource === 'defaultRoles' ? 'default-update' : resource === 'assignment' ? 'assignment' : 'update');
@@ -260,6 +264,8 @@ function auditMutation(context, args, before, result, operation) {
260
264
  const afterRelation = relationFile ? afterFiles[relationFile] : undefined;
261
265
  const roleId = (stringArg(args?.roleId) || undefined)
262
266
  ?? result?.roleId
267
+ ?? result?.assignment?.explicitRole
268
+ ?? result?.assignment?.explicitDefaultRole
263
269
  ?? (isObject(afterRelation) && typeof afterRelation.role === 'string' ? afterRelation.role : undefined)
264
270
  ?? (isObject(beforeRelation) && typeof beforeRelation.role === 'string' ? beforeRelation.role : undefined);
265
271
  const primaryFile = before.relation
@@ -300,7 +306,7 @@ export function recoverAllRoleMutationsSync() {
300
306
  if (!fs.existsSync(agentsDir))
301
307
  return;
302
308
  for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
303
- if (!entry.isDirectory())
309
+ if (!entry.isDirectory() || !isValidAid(entry.name))
304
310
  continue;
305
311
  if (!fs.existsSync(roleJournalPath(entry.name)))
306
312
  continue;
@@ -447,7 +453,8 @@ function updateDefinition(self, args, value) {
447
453
  ...(args?.remove ? { removed: normalizeRemove(args.remove) } : {}),
448
454
  };
449
455
  }
450
- function updateDefaultRoles(self, args, value) {
456
+ function updateDefaultRoles(context, args, value) {
457
+ const self = context.self;
451
458
  if (!isObject(value))
452
459
  throw invalidJsonType(self, 'defaultRoles', 'object');
453
460
  assertOnlyKeys(value, ['private', 'group'], 'defaultRoles', self);
@@ -460,6 +467,13 @@ function updateDefaultRoles(self, args, value) {
460
467
  };
461
468
  assertPolicyRevision(self, args);
462
469
  const registry = readRoleRegistry(self);
470
+ const changesAdminDefault = ['private', 'group'].some(scene => registry.defaultRoles[scene] !== nextDefaults[scene]
471
+ && (registry.defaultRoles[scene] === 'admin' || nextDefaults[scene] === 'admin'));
472
+ if (context.actorRole !== 'owner' && changesAdminDefault) {
473
+ throw roleError('NOT_ALLOWED', 'Only an owner can set or replace an admin default role', self, {
474
+ kind: 'admin_owner_only',
475
+ });
476
+ }
463
477
  const nextRegistry = { ...registry, defaultRoles: nextDefaults };
464
478
  const changed = stableStringify(registry) !== stableStringify(nextRegistry);
465
479
  if (changed)
@@ -484,30 +498,79 @@ async function updateAssignment(context, args, value) {
484
498
  if (roleId !== null)
485
499
  target = await validateTargetExists(context, target);
486
500
  const targetAid = target.kind === 'private' ? target.peerAid : null;
487
- if (targetAid && (isStaticAgentOwner(self, targetAid) || isStaticAgentAdmin(self, targetAid))) {
488
- throw roleError('NOT_ALLOWED', 'Static Agent owner/admin assignments cannot be changed through relations', self, {
489
- kind: 'protected_management_assignment',
501
+ if (targetAid && isStaticAgentOwner(self, targetAid)) {
502
+ throw roleError('NOT_ALLOWED', 'Owner assignments cannot be changed through relations', self, {
503
+ kind: 'owner_protected',
490
504
  targetAid,
491
505
  });
492
506
  }
493
507
  const current = readRelation(target.peerKey, self);
508
+ if (context.actorRole !== 'owner' && (roleId === 'admin' || current?.config.role === 'admin')) {
509
+ throw roleError('NOT_ALLOWED', 'Only an owner can grant or revoke admin', self, {
510
+ kind: 'admin_owner_only',
511
+ target: target.peerKey,
512
+ });
513
+ }
494
514
  assertRelationRevision(self, args, current?.revision ?? null, target.peerKey);
495
- if (!current && roleId === null) {
515
+ if (!current && roleId === null)
496
516
  return assignmentResponse(self, target, null, false, null);
497
- }
498
517
  const relation = clone(current?.config ?? { $schema_version: currentVersion('relation-config') });
499
- relation.targetKind = target.kind === 'private' ? 'private' : 'group';
518
+ relation.targetKind = target.kind;
500
519
  if (roleId === null)
501
520
  delete relation.role;
502
521
  else
503
522
  relation.role = roleId;
504
- const normalized = normalizeRelationForScene(relation, target.kind === 'private' ? 'private' : 'group');
523
+ const normalized = normalizeRelationForScene(relation, target.kind);
505
524
  const changed = stableStringify(current?.config ?? null) !== stableStringify(normalized);
506
525
  if (changed)
507
526
  write(ConfigTarget.Relation, normalized, { self, peerKey: target.peerKey });
508
527
  const nextRevision = changed ? revision(normalized) : current?.revision ?? null;
509
528
  return assignmentResponse(self, target, normalized, changed, nextRevision);
510
529
  }
530
+ async function updateAdminAssignment(context, args, value) {
531
+ const self = context.self;
532
+ if (context.actorRole !== 'owner') {
533
+ throw roleError('NOT_ALLOWED', 'Only an owner can grant or revoke admin', self, { kind: 'admin_owner_only' });
534
+ }
535
+ let target = normalizeTarget(args?.target, self);
536
+ if (target.kind !== 'private')
537
+ throw invalidTargetValue(self, 'target.kind');
538
+ if (isStaticAgentOwner(self, target.peerAid)) {
539
+ throw roleError('NOT_ALLOWED', 'Owner assignments cannot be changed through contact admin', self, { kind: 'owner_protected' });
540
+ }
541
+ const enabled = value === true || value === 'grant'
542
+ ? true
543
+ : value === false || value === null || value === 'revoke'
544
+ ? false
545
+ : undefined;
546
+ if (enabled === undefined)
547
+ throw invalidJsonType(self, 'admin', 'boolean, grant, revoke, or null');
548
+ if (enabled)
549
+ target = await validateTargetExists(context, target);
550
+ const current = readRelation(target.peerKey, self);
551
+ assertRelationRevision(self, args, current?.revision ?? null, target.peerKey);
552
+ if (!current && !enabled) {
553
+ return {
554
+ $schema_version: ROLE_MENU_SCHEMA_VERSION, self, resource: 'admin', target,
555
+ changed: false, relationRevision: null, admin: false,
556
+ };
557
+ }
558
+ const relation = clone(current?.config ?? { $schema_version: currentVersion('relation-config') });
559
+ relation.targetKind = 'private';
560
+ if (enabled)
561
+ relation.role = 'admin';
562
+ else if (relation.role === 'admin')
563
+ delete relation.role;
564
+ const normalized = normalizeRelationForScene(relation, 'private');
565
+ const changed = stableStringify(current?.config ?? null) !== stableStringify(normalized);
566
+ if (changed)
567
+ write(ConfigTarget.Relation, normalized, { self, peerKey: target.peerKey });
568
+ const nextRevision = changed ? revision(normalized) : current?.revision ?? null;
569
+ return {
570
+ $schema_version: ROLE_MENU_SCHEMA_VERSION, self, resource: 'admin', target,
571
+ changed, relationRevision: nextRevision, admin: normalized.role === 'admin',
572
+ };
573
+ }
511
574
  function createRole(self, args) {
512
575
  const roleId = requiredRoleId(args, self);
513
576
  if (!isValidUserRoleName(roleId)) {
@@ -520,6 +583,10 @@ function createRole(self, args) {
520
583
  if (!isObject(args?.definition))
521
584
  throw validationFailed(self, roleId, [{ field: 'definition', code: 'INVALID_TYPE', message: 'definition must be an object' }]);
522
585
  const definition = clone(args.definition);
586
+ if (definition.rank !== undefined && definition.rank !== expectedRoleRank(roleId)) {
587
+ throw validationFailed(self, roleId, [{ field: 'definition.rank', code: 'INVALID_VALUE', message: 'custom role rank is fixed at 500' }]);
588
+ }
589
+ definition.rank = expectedRoleRank(roleId);
523
590
  const errors = validateRoleConfig(definition);
524
591
  if (errors.length)
525
592
  throw validationFailed(self, roleId, errors);
@@ -596,6 +663,11 @@ async function deleteRole(context, args) {
596
663
  const replacement = hasReplacement
597
664
  ? validateAssignableRole(args?.replacementRoleId, self, 'replacementRoleId')
598
665
  : undefined;
666
+ if (replacement === 'admin' && context.actorRole !== 'owner') {
667
+ throw roleError('NOT_ALLOWED', 'Only an owner can replace role assignments with admin', self, {
668
+ kind: 'admin_owner_only',
669
+ });
670
+ }
599
671
  if (replacement === roleId) {
600
672
  throw validationFailed(self, roleId, [{ field: 'replacementRoleId', code: 'INVALID_VALUE', message: 'replacementRoleId must differ from roleId' }]);
601
673
  }
@@ -646,11 +718,13 @@ async function assignmentInventory(context, args, policyRevision) {
646
718
  if (args?.roleId !== undefined && !roleFilter)
647
719
  throw validationFailed(self, undefined, [{ field: 'roleId', code: 'INVALID_VALUE', message: 'roleId must be a non-empty string' }]);
648
720
  const items = [];
649
- const defaults = effectiveDefaultRoles(self);
650
- for (const scene of ['private', 'group']) {
651
- const roleId = defaults[scene];
652
- if (roleId && (!roleFilter || roleFilter === roleId)) {
653
- items.push({ target: { kind: 'default', scene }, roleId, relationRevision: null });
721
+ if (args?.includeDefaults !== false) {
722
+ const defaults = effectiveDefaultRoles(self);
723
+ for (const scene of ['private', 'group']) {
724
+ const roleId = defaults[scene];
725
+ if (roleId && (!roleFilter || roleFilter === roleId)) {
726
+ items.push({ target: { kind: 'default', scene }, roleId, relationRevision: null });
727
+ }
654
728
  }
655
729
  }
656
730
  let complete = true;
@@ -930,11 +1004,13 @@ function normalizeTarget(value, self) {
930
1004
  const groupId = stringArg(value.groupId);
931
1005
  if (!groupId)
932
1006
  throw invalidTarget(self, 'target.groupId');
1007
+ if (!isValidAid(groupId) && !isExplicitGroupId(groupId))
1008
+ throw invalidTargetValue(self, 'target.groupId');
933
1009
  return { kind, groupId, peerKey: formatPeerKey('aun', groupId) };
934
1010
  }
935
1011
  throw validationFailed(self, undefined, [{ field: 'target.kind', code: 'INVALID_VALUE', message: 'target.kind must be private or group' }]);
936
1012
  }
937
- function targetRoleView(self, target, relation) {
1013
+ function targetRoleView(self, target, relation = null) {
938
1014
  if (target.kind === 'private') {
939
1015
  const explicitRole = relation?.role ?? null;
940
1016
  const detail = resolvePeerRoleDetail({
@@ -1086,7 +1162,15 @@ function policyDocument(agent) {
1086
1162
  function roleMeta(self, roleId) {
1087
1163
  if (isManagementRole(roleId)) {
1088
1164
  const overridden = fs.existsSync(agentRoleConfig(self, roleId));
1089
- return { origin: 'builtin', overridden, kind: 'management', assignable: false, editable: true, deletable: false, resettable: true };
1165
+ return {
1166
+ origin: 'builtin',
1167
+ overridden,
1168
+ kind: 'management',
1169
+ assignable: roleId === 'admin',
1170
+ editable: true,
1171
+ deletable: false,
1172
+ resettable: true,
1173
+ };
1090
1174
  }
1091
1175
  const builtin = Object.prototype.hasOwnProperty.call(getBuiltinRolesConfig().roles, roleId);
1092
1176
  const overridden = fs.existsSync(agentRoleConfig(self, roleId));
@@ -1131,7 +1215,7 @@ function parseUpdateValue(rawValue, self) {
1131
1215
  function validateAssignableRole(value, self, field) {
1132
1216
  if (value === null)
1133
1217
  return null;
1134
- if (typeof value !== 'string' || !value.trim() || isManagementRole(value) || !getRoleDefinition(value, self)) {
1218
+ if (typeof value !== 'string' || !value.trim() || value === 'owner' || !getRoleDefinition(value, self)) {
1135
1219
  throw validationFailed(self, undefined, [{ field, code: 'INVALID_VALUE', message: 'value must be null or an existing assignable user role' }]);
1136
1220
  }
1137
1221
  return value;
@@ -1288,11 +1372,15 @@ function validateRoleDomainTransactionChanges(self, registry, changes) {
1288
1372
  for (const change of changes) {
1289
1373
  if (!change.after || !change.file.endsWith('.json') || change.file.endsWith('index.json'))
1290
1374
  continue;
1291
- if (!change.file.includes(`${path.sep}relations${path.sep}`)) {
1292
- const roleSchema = loadSchema('role-config');
1293
- const value = JSON.parse(change.after);
1294
- if (!roleSchema.validate(value))
1295
- throw validationFailed(self, undefined, [{ field: 'role', code: 'SCHEMA_INVALID', message: 'invalid role definition' }]);
1375
+ const isRelation = change.file.includes(`${path.sep}relations${path.sep}`);
1376
+ const schema = loadSchema(isRelation ? 'relation-config' : 'role-config');
1377
+ const value = JSON.parse(change.after);
1378
+ if (!schema.validate(value)) {
1379
+ throw validationFailed(self, undefined, [{
1380
+ field: isRelation ? 'relation' : 'role',
1381
+ code: 'SCHEMA_INVALID',
1382
+ message: isRelation ? 'invalid relation config' : 'invalid role definition',
1383
+ }]);
1296
1384
  }
1297
1385
  }
1298
1386
  }
@@ -1337,12 +1425,23 @@ function removeJournalFiles(journalFile) {
1337
1425
  }
1338
1426
  function validateJournal(self, journal) {
1339
1427
  const root = path.resolve(agentDir(self));
1340
- if (journal.$schema_version !== 1 || journal.self !== self || !Array.isArray(journal.changes))
1428
+ if (journal.$schema_version !== 1 || journal.self !== self
1429
+ || (journal.state !== 'prepared' && journal.state !== 'committed')
1430
+ || !Array.isArray(journal.changes))
1341
1431
  throw new Error('Invalid Role mutation journal');
1432
+ const seen = new Set();
1342
1433
  for (const change of journal.changes) {
1434
+ if (!change || typeof change !== 'object' || typeof change.file !== 'string'
1435
+ || (change.before !== null && typeof change.before !== 'string')
1436
+ || (change.after !== null && typeof change.after !== 'string')) {
1437
+ throw new Error('Role mutation journal contains an invalid change');
1438
+ }
1343
1439
  const file = path.resolve(change.file);
1344
- if (file !== root && !file.startsWith(root + path.sep))
1440
+ if (file === root || !file.startsWith(root + path.sep))
1345
1441
  throw new Error('Role mutation journal contains an unsafe path');
1442
+ if (seen.has(file))
1443
+ throw new Error('Role mutation journal contains duplicate file changes');
1444
+ seen.add(file);
1346
1445
  }
1347
1446
  }
1348
1447
  function applyFileContent(file, content) {
@@ -1498,7 +1597,7 @@ function operationLabel(id) {
1498
1597
  return labels[id] ?? title(id);
1499
1598
  }
1500
1599
  function validRoleOrNull(value, self) {
1501
- return typeof value === 'string' && !isManagementRole(value) && getRoleDefinition(value, self) ? value : null;
1600
+ return typeof value === 'string' && value !== 'owner' && getRoleDefinition(value, self) ? value : null;
1502
1601
  }
1503
1602
  function getNested(value, field) {
1504
1603
  let cursor = value;
@@ -71,7 +71,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
71
71
  const userGroupCommands = [
72
72
  '/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
73
73
  '/model', '/effort', '/baseagent', '/perm', '/activity', '/stop',
74
- '/resume', '/trigger',
74
+ '/resume', '/trigger', '/file',
75
75
  ];
76
76
  const userCommands = activeChatType === 'group' && !isAdmin
77
77
  ? userGroupCommands