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
@@ -32,9 +32,11 @@ import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntim
32
32
  import { isManagementRole } from '../../config/builtin-roles.js';
33
33
  import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
34
34
  import { logger } from '../../utils/logger.js';
35
+ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
35
36
  import { dispatchToMentionMode } from '../../config/mention-mode.js';
36
37
  import { menuFailure, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest } from './menu-protocol.js';
37
38
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
39
+ import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
38
40
  /**
39
41
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
40
42
  * 失败返回 null(命令不存在或执行失败)。
@@ -885,7 +887,7 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
885
887
  return intent('agent.delete', 'control', { action });
886
888
  }
887
889
  if (cmdBase === '/trigger') {
888
- if (action === 'set')
890
+ if (action === 'create' || action === 'set')
889
891
  return intent('trigger.create', 'relation', { action });
890
892
  if (action === 'show')
891
893
  return intent('trigger.show', 'relation', { action });
@@ -1024,7 +1026,7 @@ async function authorizeRoleMenu(owner, params) {
1024
1026
  fromControlChannel: params.fromControlChannel,
1025
1027
  subject: params.subject,
1026
1028
  });
1027
- const operation = roleMenuOperation(params.kind, params.args, params.value);
1029
+ const operation = params.operation ?? roleMenuOperation(params.kind, params.args, params.value);
1028
1030
  const denied = await authorizeMenuIntent.call(owner, {
1029
1031
  intent: { operation, scope: 'agent', source: params.source, args: { ...(params.args ?? {}) } },
1030
1032
  identity: params.identity,
@@ -1248,6 +1250,25 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1248
1250
  throw authorized;
1249
1251
  return await roleMenuOptions(authorized.context, args);
1250
1252
  }
1253
+ if (cmdBase0 === '/connect') {
1254
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1255
+ const authorized = await authorizeRoleMenu(this, {
1256
+ kind: 'options', args, operation: connectMenuOperation('options', args), identity, subject: authSubject, session, channel, channelId,
1257
+ userId, fromControlChannel, source,
1258
+ });
1259
+ if ('error' in authorized)
1260
+ throw authorized;
1261
+ try {
1262
+ const result = await handleConnectMenu({ type: 'menu.options', subtype: 'options', args }, authorized.context);
1263
+ if ('error' in result)
1264
+ throw result.error;
1265
+ const opts = result.data?.options ?? [];
1266
+ return opts.map((opt) => ({ value: opt.option, label: opt.label, desc: opt.description }));
1267
+ }
1268
+ catch (error) {
1269
+ throw menuResultFailure(error);
1270
+ }
1271
+ }
1251
1272
  // ── /agent list(只读) ──
1252
1273
  // 控制 channel:验 evolcore.owners,返回全量;agent channel:放行但仅返回自身单条。
1253
1274
  if (cmd === '/agent') {
@@ -1590,6 +1611,28 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1590
1611
  return menuResultFailure(error);
1591
1612
  }
1592
1613
  }
1614
+ if (cmdBase === '/connect') {
1615
+ const authorized = await authorizeRoleMenu(this, {
1616
+ kind: 'query', args, operation: connectMenuOperation('query', args), identity, subject, session, channel, channelId,
1617
+ userId, fromControlChannel, source,
1618
+ });
1619
+ if ('error' in authorized)
1620
+ return authorized;
1621
+ try {
1622
+ const result = await handleConnectMenu({ type: 'menu.query', subtype: 'query', args }, authorized.context);
1623
+ if (result.error) {
1624
+ return {
1625
+ error: result.error.message,
1626
+ code: result.error.code,
1627
+ ...(result.error.data === undefined ? {} : { data: result.error.data }),
1628
+ };
1629
+ }
1630
+ return { data: result.data };
1631
+ }
1632
+ catch (error) {
1633
+ return menuResultFailure(error);
1634
+ }
1635
+ }
1593
1636
  const authDenied = await authorizeMenuIntent.call(this, {
1594
1637
  intent: buildMenuIntent('query', cmdBase, args, undefined, undefined, fromControlChannel),
1595
1638
  identity,
@@ -2088,6 +2131,28 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2088
2131
  return menuResultFailure(error);
2089
2132
  }
2090
2133
  }
2134
+ if (cmdBase === '/connect') {
2135
+ const authorized = await authorizeRoleMenu(this, {
2136
+ kind: 'update', args, value: arg, operation: connectMenuOperation('update', args, arg), identity, subject: authSubject, session, channel, channelId,
2137
+ userId, fromControlChannel, source,
2138
+ });
2139
+ if ('error' in authorized)
2140
+ return authorized;
2141
+ try {
2142
+ const result = await handleConnectMenu({ type: 'menu.update', subtype: 'update', args, value: arg }, authorized.context);
2143
+ if (result.error) {
2144
+ return {
2145
+ error: result.error.message,
2146
+ code: result.error.code,
2147
+ ...(result.error.data === undefined ? {} : { data: result.error.data }),
2148
+ };
2149
+ }
2150
+ return { data: result.data };
2151
+ }
2152
+ catch (error) {
2153
+ return menuResultFailure(error);
2154
+ }
2155
+ }
2091
2156
  const authDenied = await authorizeMenuIntent.call(this, {
2092
2157
  intent: buildMenuIntent('update', cmdBase, args, undefined, arg, fromControlChannel),
2093
2158
  identity,
@@ -2465,6 +2530,28 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2465
2530
  return menuResultFailure(error);
2466
2531
  }
2467
2532
  }
2533
+ if (cmdBase === '/connect') {
2534
+ const authorized = await authorizeRoleMenu(this, {
2535
+ kind: 'action', args, operation: connectMenuOperation('action', args), identity: authIdentity, subject, session: authSession,
2536
+ channel, channelId, userId, fromControlChannel, source,
2537
+ });
2538
+ if ('error' in authorized)
2539
+ return authorized;
2540
+ try {
2541
+ const result = await handleConnectMenu({ type: 'menu.action', subtype: 'action', action, args }, authorized.context);
2542
+ if (result.error) {
2543
+ return {
2544
+ error: result.error.message,
2545
+ code: result.error.code,
2546
+ ...(result.error.data === undefined ? {} : { data: result.error.data }),
2547
+ };
2548
+ }
2549
+ return { data: result.data };
2550
+ }
2551
+ catch (error) {
2552
+ return menuResultFailure(error);
2553
+ }
2554
+ }
2468
2555
  if (cmdBase === '/agent' && !fromControlChannel && !args?.aid) {
2469
2556
  const selfAid = this.getOwningAgent?.(channel)?.aid;
2470
2557
  if (selfAid)
@@ -2618,7 +2705,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2618
2705
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
2619
2706
  if (!triggerScheduler)
2620
2707
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
2621
- if (action === 'set') {
2708
+ if (action === 'create' || action === 'set') {
2622
2709
  // args 结构化 → 直接组装 ParsedTriggerSet(绕过 parseTriggerSet 文本解析,无注入风险)
2623
2710
  if (!args?.scheduleType || (args.scheduleType !== 'once' && !args?.scheduleValue) || !args?.prompt) {
2624
2711
  return { error: '缺少必填参数:scheduleType / scheduleValue / prompt', code: 'INVALID_ARGS' };
@@ -2652,6 +2739,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2652
2739
  effort: args.effort,
2653
2740
  permissionMode: args.permissionMode,
2654
2741
  triggerThread,
2742
+ baseagent: args.baseagent,
2655
2743
  };
2656
2744
  const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined, this.resolveMenuChatType(channel, channelId, explicitChatType), undefined, isAdmin);
2657
2745
  if (!r.ok)
@@ -3020,18 +3108,26 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3020
3108
  return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
3021
3109
  }
3022
3110
  if (action === 'restart') {
3023
- const restartInfo = { channel, channelId, timestamp: Date.now() };
3024
- const dataDir = resolvePaths().dataDir;
3025
- fs.mkdirSync(dataDir, { recursive: true });
3026
- fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3027
- const { spawn } = await import('child_process');
3028
- spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3029
- detached: true,
3030
- stdio: 'ignore',
3031
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
3032
- }).unref();
3111
+ const suppressRealRestart = shouldSuppressRealRestart();
3112
+ if (!suppressRealRestart) {
3113
+ const restartInfo = { channel, channelId, timestamp: Date.now() };
3114
+ const dataDir = resolvePaths().dataDir;
3115
+ fs.mkdirSync(dataDir, { recursive: true });
3116
+ fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3117
+ const { spawn } = await import('child_process');
3118
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3119
+ detached: true,
3120
+ stdio: 'ignore',
3121
+ env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
3122
+ }).unref();
3123
+ }
3124
+ else {
3125
+ logger.info('[System] Suppressed real menu restart in test runtime');
3126
+ }
3033
3127
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
3034
- setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
3128
+ if (!suppressRealRestart) {
3129
+ setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
3130
+ }
3035
3131
  return { data: { action: 'restart', success: true } };
3036
3132
  }
3037
3133
  if (action === 'check') {
@@ -3239,7 +3335,8 @@ async function execMenuForSystemControl(payload, context) {
3239
3335
  }
3240
3336
  const isProcessLevel = isProcessLevelMenu(name, payload?.cmd);
3241
3337
  const isRoleRequest = name === 'role' || payload?.cmd === '/role';
3242
- const requiresAgentTarget = isRoleRequest || [
3338
+ const isConnectRequest = name === 'connect' || payload?.cmd === '/connect';
3339
+ const requiresAgentTarget = isRoleRequest || isConnectRequest || [
3243
3340
  'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability',
3244
3341
  ].includes(name)
3245
3342
  || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability'].includes(payload?.cmd);
@@ -3342,7 +3439,8 @@ export async function execMenuForControl(payload, peerId) {
3342
3439
  return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
3343
3440
  const owners = loadDaemonConfig().owners ?? [];
3344
3441
  const isRoleRequest = name === 'role' || payload?.cmd === '/role';
3345
- if (!isRoleRequest && !isProcessLevelOwner(peerId, owners)) {
3442
+ const isConnectRequest = name === 'connect' || payload?.cmd === '/connect';
3443
+ if (!isRoleRequest && !isConnectRequest && !isProcessLevelOwner(peerId, owners)) {
3346
3444
  return menuFailure({ id, ...(name ? { name } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: '控制 channel 操作需要 owner 权限' });
3347
3445
  }
3348
3446
  return execMenuForSystemControl.call(this, payload, {
@@ -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;
@@ -3,6 +3,7 @@ import { getCodexEfforts } from '../../agents/codex-runner.js';
3
3
  import { buildEnvelope } from '../message/message-utils.js';
4
4
  import { resolvePaths, getPackageRoot } from '../../paths.js';
5
5
  import { logger } from '../../utils/logger.js';
6
+ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
6
7
  import crypto from 'crypto';
7
8
  import path from 'path';
8
9
  import fs from 'fs';
@@ -186,7 +187,7 @@ function triggerOperationForSlash(content) {
186
187
  return 'trigger.show';
187
188
  if (subcommand === 'history')
188
189
  return 'trigger.history';
189
- if (subcommand === 'set')
190
+ if (subcommand === 'create' || subcommand === 'set')
190
191
  return 'trigger.create';
191
192
  if (subcommand === 'update')
192
193
  return 'trigger.update';
@@ -351,7 +352,7 @@ async function getGitWorkingDirInfo(projectPath) {
351
352
  return null; // git 命令失败时静默返回 null
352
353
  }
353
354
  }
354
- export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject) {
355
+ export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject, replyContext) {
355
356
  // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
356
357
  // 不应覆盖 session 中已有的正确值
357
358
  if (source === 'card-trigger')
@@ -1628,11 +1629,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1628
1629
  }
1629
1630
  if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
1630
1631
  const activityArg = normalizedContent.slice(9).trim();
1632
+ const activitySession = await getExistingSessionForCommand();
1631
1633
  // 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
1632
1634
  if (activityArg && !isAdmin)
1633
1635
  return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
1634
1636
  // proactive 模式下流式输出全部静默,activity 配置无意义
1635
- if (getEffectiveChatmode(activeSession) === 'proactive') {
1637
+ if (getEffectiveChatmode(activitySession) === 'proactive') {
1636
1638
  return { kind: 'command.error', text: '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)' };
1637
1639
  }
1638
1640
  const modeMap = {
@@ -1655,7 +1657,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1655
1657
  type: 'interaction',
1656
1658
  id: `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1657
1659
  channelId,
1658
- sessionId: activeSession?.id || '',
1660
+ sessionId: activitySession?.id || '',
1659
1661
  initiatorId: userId,
1660
1662
  kind: {
1661
1663
  kind: 'command-card',
@@ -1669,7 +1671,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1669
1671
  })),
1670
1672
  },
1671
1673
  };
1672
- const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1674
+ const replyCtx = replyContext ?? (activitySession ? this.getReplyContext(activitySession) : undefined);
1673
1675
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1674
1676
  if (cardResult === null)
1675
1677
  return null;
@@ -2370,6 +2372,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2370
2372
  });
2371
2373
  // 执行重启逻辑(共用于卡片回调和文本确认)
2372
2374
  const executeRestart = async () => {
2375
+ const suppressRealRestart = shouldSuppressRealRestart();
2373
2376
  let replyContext;
2374
2377
  if (threadId) {
2375
2378
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
@@ -2382,15 +2385,20 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2382
2385
  timestamp: Date.now(),
2383
2386
  ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
2384
2387
  };
2385
- const dataDir = resolvePaths().dataDir;
2386
- fs.mkdirSync(dataDir, { recursive: true });
2387
- fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2388
- const { spawn } = await import('child_process');
2389
- spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2390
- detached: true,
2391
- stdio: 'ignore',
2392
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2393
- }).unref();
2388
+ if (!suppressRealRestart) {
2389
+ const dataDir = resolvePaths().dataDir;
2390
+ fs.mkdirSync(dataDir, { recursive: true });
2391
+ fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2392
+ const { spawn } = await import('child_process');
2393
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2394
+ detached: true,
2395
+ stdio: 'ignore',
2396
+ env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2397
+ }).unref();
2398
+ }
2399
+ else {
2400
+ logger.info('[System] Suppressed real restart in test runtime');
2401
+ }
2394
2402
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
2395
2403
  // 先发送重启反馈消息,等待发送完成后再 kill 进程
2396
2404
  // 避免消息还没发出去进程就退出了
@@ -2416,10 +2424,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2416
2424
  // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
2417
2425
  // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
2418
2426
  // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
2419
- setTimeout(() => {
2420
- logger.info('[System] Restarting by user command...');
2421
- process.kill(process.pid, 'SIGTERM');
2422
- }, 1000);
2427
+ if (!suppressRealRestart) {
2428
+ setTimeout(() => {
2429
+ logger.info('[System] Restarting by user command...');
2430
+ process.kill(process.pid, 'SIGTERM');
2431
+ }, 1000);
2432
+ }
2423
2433
  return true;
2424
2434
  };
2425
2435
  // 文本确认流程