evolcore 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -106,13 +106,13 @@ export function parseReturn(args) {
106
106
  function printReturn(result) {
107
107
  if (result.ok) {
108
108
  if (result.code === 'HANDOFF_RETURN_ALREADY_APPLIED') {
109
- console.log(`✓ handoff ${result.handoff_id} 已接收过相同结果,未重复处理`);
109
+ console.log(`✓ handoff ${result.handoff_id} 已接收过相同结果,未重复处理${result.result_id ? ` result_id=${result.result_id}` : ''}`);
110
110
  }
111
111
  else if (result.state === 'completed') {
112
112
  console.log(`✓ handoff ${result.handoff_id} 已确认完成,不回流来源会话`);
113
113
  }
114
114
  else {
115
- console.log(`✓ handoff ${result.handoff_id} 回流结果已接收`);
115
+ console.log(`✓ handoff ${result.handoff_id} 回流结果已接收${result.result_id ? ` result_id=${result.result_id}` : ''};后续仍可用同一 ID 追加结果`);
116
116
  }
117
117
  return;
118
118
  }
@@ -156,8 +156,10 @@ export async function cmdHandoff(args) {
156
156
  console.log(`用法: ec handoff <command> [args]
157
157
 
158
158
  Commands:
159
- return [handoff-id] [content] 确认完成;required 策略需提供回流内容
159
+ return [handoff-id] [content] 提交一个回流结果;required 可用同一 ID 追加多个结果
160
160
  return [handoff-id] --text-from-file <path>
161
+ recover-return <handoff-id> [content] owner-authorized recovery after target session replacement
162
+ recover-return <handoff-id> --text-from-file <path>
161
163
  status <handoff-id> 查询 handoff 状态
162
164
  list [--state <state>] [--session <id>] [--limit <n>] [--agent <aid>]
163
165
  trace <handoff-id> [--limit <n>] [--agent <aid>]`);
@@ -194,6 +196,36 @@ Commands:
194
196
  printReturn(result);
195
197
  return;
196
198
  }
199
+ if (sub === 'recover-return') {
200
+ let parsed;
201
+ try {
202
+ parsed = parseReturn(args.slice(1));
203
+ }
204
+ catch (error) {
205
+ console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+ if (!parsed.handoffId) {
210
+ console.error('✗ recover-return 必须指定 handoff ID');
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+ const result = await ipcQuery(resolvePaths().socket, {
215
+ type: 'handoff-recover-return',
216
+ sessionId: process.env.EVOLCORE_SESSION_ID,
217
+ handoffId: parsed.handoffId,
218
+ content: parsed.content,
219
+ delegationToken: process.env[AGENT_DELEGATION_TOKEN_ENV],
220
+ }, 5000);
221
+ if (!result) {
222
+ console.error('✗ daemon 暂时不可用');
223
+ process.exitCode = 1;
224
+ return;
225
+ }
226
+ printReturn(result);
227
+ return;
228
+ }
197
229
  if (sub === 'status') {
198
230
  const handoffId = args[1];
199
231
  if (!handoffId || !HANDOFF_ID_RE.test(handoffId)) {
package/dist/cli/index.js CHANGED
@@ -47,7 +47,8 @@ AUN:
47
47
  storage 底层对象存储命令
48
48
  rpc 调用底层 AUN RPC
49
49
  menu-client 向远程 daemon 发送标准 Menu 请求
50
- contact 联系人黑名单管理
50
+ diag 邀请远程诊断 Agent,或 stop 撤销诊断授权
51
+ contact 联系人、待审申请与黑名单管理
51
52
  net AUN 网络链路诊断
52
53
 
53
54
  配置与诊断:
@@ -351,6 +352,11 @@ export async function main(args) {
351
352
  await cmdRpc(args.slice(1));
352
353
  break;
353
354
  }
355
+ case 'diag': {
356
+ const { cmdDiag } = await import('./diag.js');
357
+ await cmdDiag(args.slice(1));
358
+ break;
359
+ }
354
360
  case 'menu-client': {
355
361
  const { suppressSdkLogs } = await import('../aun/aid/index.js');
356
362
  suppressSdkLogs();
@@ -5,6 +5,9 @@ const MANAGED_TASK_COMMANDS = new Set([
5
5
  'config',
6
6
  'contact',
7
7
  'ctl',
8
+ // These commands are authenticated again by their daemon IPC entrypoints.
9
+ 'diag',
10
+ 'menu-client',
8
11
  'fs',
9
12
  'group',
10
13
  'handoff',
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { getArgValue, wantsHelp } from './help.js';
3
3
  import { executeMenuClientRequest, MenuClientError } from '../aun/menu-client.js';
4
+ import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
4
5
  function parseDurationMs(value) {
5
6
  if (value === undefined)
6
7
  return undefined;
@@ -11,14 +12,14 @@ function parseDurationMs(value) {
11
12
  const unit = match[2].toLowerCase();
12
13
  return amount * (unit === 'ms' ? 1 : unit === 's' ? 1_000 : unit === 'm' ? 60_000 : 3_600_000);
13
14
  }
14
- function readRequest(args) {
15
+ function readRequest(args, readFile = (file) => fs.readFileSync(file, 'utf8')) {
15
16
  const file = getArgValue(args, '--request-file');
16
17
  const inline = getArgValue(args, '--request-json');
17
18
  if (!!file === !!inline)
18
19
  throw new MenuClientError('INVALID_ARGUMENT', '--request-file and --request-json are mutually exclusive');
19
20
  let raw;
20
21
  try {
21
- raw = file ? fs.readFileSync(file, 'utf8') : inline;
22
+ raw = file ? readFile(file) : inline;
22
23
  }
23
24
  catch (error) {
24
25
  throw new MenuClientError('INVALID_ARGUMENT', `Unable to read request: ${error?.message ?? String(error)}`);
@@ -36,8 +37,12 @@ function readRequest(args) {
36
37
  function validateOptions(args) {
37
38
  const valueOptions = new Set(['--request-file', '--request-json', '--timeout', '--remote-token', '--ecweb-token', '--app', '--aun-path', '--format']);
38
39
  const booleanOptions = new Set(['--encrypt', '--no-encrypt']);
40
+ const seen = new Set();
39
41
  for (let index = 0; index < args.length; index += 1) {
40
42
  const option = args[index];
43
+ if (seen.has(option))
44
+ throw new MenuClientError('INVALID_ARGUMENT', `Duplicate option: ${option}`);
45
+ seen.add(option);
41
46
  if (booleanOptions.has(option))
42
47
  continue;
43
48
  if (!valueOptions.has(option))
@@ -54,6 +59,23 @@ function validateOptions(args) {
54
59
  if (format !== undefined && format !== 'json')
55
60
  throw new MenuClientError('INVALID_ARGUMENT', '--format currently supports only json');
56
61
  }
62
+ export function parseMenuClientArgs(args, readFile) {
63
+ if (args[0] !== 'request')
64
+ throw new MenuClientError('INVALID_ARGUMENT', 'menu-client currently supports only the request subcommand');
65
+ const fromAid = args[1]?.trim();
66
+ const targetAid = args[2]?.trim();
67
+ if (!fromAid || !targetAid)
68
+ throw new MenuClientError('INVALID_ARGUMENT', 'request requires <from-aid> and <daemon-aid>');
69
+ const options = args.slice(3);
70
+ validateOptions(options);
71
+ return {
72
+ fromAid, targetAid, request: readRequest(options, readFile),
73
+ timeoutMs: parseDurationMs(getArgValue(options, '--timeout')),
74
+ encrypt: !options.includes('--no-encrypt'),
75
+ remoteAccessToken: getArgValue(options, '--remote-token') ?? getArgValue(options, '--ecweb-token'),
76
+ slotId: getArgValue(options, '--app'), aunPath: getArgValue(options, '--aun-path'),
77
+ };
78
+ }
57
79
  export async function cmdMenuClient(args) {
58
80
  if (wantsHelp(args)) {
59
81
  console.log(`用法: ec menu-client request <from-aid> <daemon-aid> [选项]
@@ -70,26 +92,19 @@ export async function cmdMenuClient(args) {
70
92
  --format json 输出标准 menu.response JSON`);
71
93
  return;
72
94
  }
73
- if (args[0] !== 'request')
74
- throw new MenuClientError('INVALID_ARGUMENT', 'menu-client currently supports only the request subcommand');
75
- const fromAid = args[1]?.trim();
76
- const targetAid = args[2]?.trim();
77
- if (!fromAid || !targetAid)
78
- throw new MenuClientError('INVALID_ARGUMENT', 'request requires <from-aid> and <daemon-aid>');
79
- const options = args.slice(3);
80
- validateOptions(options);
81
- const request = readRequest(options);
82
- const timeoutMs = parseDurationMs(getArgValue(args, '--timeout'));
83
- const encrypt = args.includes('--no-encrypt') ? false : true;
84
- const response = await executeMenuClientRequest({
85
- fromAid,
86
- targetAid,
87
- request,
88
- timeoutMs,
89
- encrypt,
90
- remoteAccessToken: getArgValue(args, '--remote-token') ?? getArgValue(args, '--ecweb-token'),
91
- slotId: getArgValue(args, '--app'),
92
- aunPath: getArgValue(args, '--aun-path'),
93
- });
95
+ let response;
96
+ if (process.env.EVOLCORE_SESSION_ID) {
97
+ const { ipcQuery } = await import('../ipc.js');
98
+ const { resolvePaths } = await import('../paths.js');
99
+ const result = await ipcQuery(resolvePaths().socket, {
100
+ type: 'menu-client.request', argv: args, sessionId: process.env.EVOLCORE_SESSION_ID,
101
+ delegationToken: process.env[AGENT_DELEGATION_TOKEN_ENV],
102
+ }, 250_000);
103
+ if (!result?.ok)
104
+ throw new MenuClientError(result?.code ?? 'DAEMON_UNAVAILABLE', result?.error ?? 'Managed Menu daemon is unavailable');
105
+ response = result.response;
106
+ }
107
+ else
108
+ response = await executeMenuClientRequest(parseMenuClientArgs(args));
94
109
  console.log(JSON.stringify(response, null, 2));
95
110
  }
@@ -15,6 +15,7 @@ import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
15
15
  import { rotateStdoutLog } from '../utils/log-writer.js';
16
16
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
17
17
  import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown } from './daemon-commands.js';
18
+ import { getProcessStartTime, startTimeMatches } from '../utils/process-introspect.js';
18
19
  const execFileAsync = promisify(execFile);
19
20
  /**
20
21
  * The package this monitor operates on, which is not always the one it runs
@@ -43,6 +44,15 @@ function cleanEnv() {
43
44
  delete process.env[key];
44
45
  }
45
46
  }
47
+ function isPidReused(pid, expectedStartedAt) {
48
+ if (expectedStartedAt === undefined)
49
+ return false;
50
+ const actualStartedAt = getProcessStartTime(pid);
51
+ return actualStartedAt !== null && !startTimeMatches(expectedStartedAt, actualStartedAt);
52
+ }
53
+ function reportPidReused(pid, log) {
54
+ log(`Process ${pid} no longer refers to the recorded daemon; treating the original process as exited`);
55
+ }
46
56
  export async function cmdRestartMonitor() {
47
57
  // Defense in depth: a unit test that accidentally executes the real restart
48
58
  // path must not detach a persistent daemon from the test worker.
@@ -117,9 +127,11 @@ export async function cmdRestartMonitor() {
117
127
  }
118
128
  // Test daemons live in one temporary HOME per test, so the current HOME's
119
129
  // instance registry cannot see them. Their strict environment markers make
120
- // them safe to reap globally on every real restart.
130
+ // them safe to reap globally on every real restart. Keep one process-table
131
+ // snapshot for both orphan categories; each Windows CIM scan is expensive.
132
+ const orphanSnapshot = findOrphanProcesses();
121
133
  {
122
- const leakedTestDaemons = findOrphanProcesses().filter(o => o.confirmedTestDaemon);
134
+ const leakedTestDaemons = orphanSnapshot.filter(o => o.confirmedTestDaemon);
123
135
  if (leakedTestDaemons.length > 0) {
124
136
  const killed = killOrphans(leakedTestDaemons);
125
137
  log(`Cleaned ${killed.length} leaked test daemon(s): ${killed.join(', ')}`);
@@ -130,7 +142,7 @@ export async function cmdRestartMonitor() {
130
142
  // still alive. Refuse to create a second daemon when an unregistered process
131
143
  // from this package is found; a duplicate would connect AUN and kick the
132
144
  // existing session before failing on IPC/port ownership.
133
- const runtimeOrphans = findOrphanProcesses().filter(o => {
145
+ const runtimeOrphans = orphanSnapshot.filter(o => {
134
146
  if (o.confirmedTestDaemon)
135
147
  return false;
136
148
  const homeMatches = o.evolcoreHome && normalizePath(o.evolcoreHome) === normalizePath(p.root);
@@ -165,6 +177,7 @@ export async function cmdRestartMonitor() {
165
177
  const aliveMains = oldStatus.mains.filter(m => m.alive);
166
178
  if (aliveMains.length > 0) {
167
179
  const oldPids = aliveMains.map(m => m.record.pid);
180
+ const expectedStarts = new Map(aliveMains.map(m => [m.record.pid, m.record.startedAt]));
168
181
  log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
169
182
  const gracefulPid = await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'restart-monitor', new Set(oldPids));
170
183
  // 没有收到 IPC shutdown 确认的进程才走信号兜底
@@ -177,15 +190,39 @@ export async function cmdRestartMonitor() {
177
190
  }
178
191
  }
179
192
  const exited = await Promise.all(oldPids.map(async (oldPid) => {
193
+ const expectedStartedAt = expectedStarts.get(oldPid);
194
+ if (isPidReused(oldPid, expectedStartedAt)) {
195
+ reportPidReused(oldPid, log);
196
+ return true;
197
+ }
180
198
  if (await platform.waitForProcessExit(oldPid, 30_000)) {
199
+ if (isPidReused(oldPid, expectedStartedAt)) {
200
+ reportPidReused(oldPid, log);
201
+ return true;
202
+ }
181
203
  log(`Process ${oldPid} has exited`);
182
204
  return true;
183
205
  }
184
- log(`ERROR: Process ${oldPid} still running after 30s, force killing`);
206
+ if (isPidReused(oldPid, expectedStartedAt)) {
207
+ reportPidReused(oldPid, log);
208
+ return true;
209
+ }
210
+ const state = platform.getProcessState(oldPid);
211
+ log(`ERROR: Process ${oldPid} still running after 30s${state ? ` (state=${state})` : ''}, force killing`);
212
+ if (isPidReused(oldPid, expectedStartedAt)) {
213
+ reportPidReused(oldPid, log);
214
+ return true;
215
+ }
185
216
  platform.killProcess(oldPid, true);
186
217
  const forced = await platform.waitForProcessExit(oldPid, 5_000);
187
- if (!forced)
188
- log(`ERROR: Process ${oldPid} survived forced termination`);
218
+ if (isPidReused(oldPid, expectedStartedAt)) {
219
+ reportPidReused(oldPid, log);
220
+ return true;
221
+ }
222
+ if (!forced) {
223
+ const finalState = platform.getProcessState(oldPid);
224
+ log(`ERROR: Process ${oldPid} survived forced termination${finalState ? ` (state=${finalState})` : ''}`);
225
+ }
189
226
  return forced;
190
227
  }));
191
228
  if (exited.some(result => !result)) {
@@ -194,7 +231,9 @@ export async function cmdRestartMonitor() {
194
231
  cleanupPendingFile(pendingFile, log);
195
232
  process.exit(1);
196
233
  }
197
- await sleep(3000);
234
+ // waitForProcessExit above already observed the old daemon exit. A fixed
235
+ // three-second delay only serialized the restart path and was not needed
236
+ // for instance-file cleanup.
198
237
  cleanupInstances();
199
238
  }
200
239
  // 版本检查与自动升级
@@ -202,7 +241,10 @@ export async function cmdRestartMonitor() {
202
241
  // 升级会原地替换整包,先留一个可回退的产物副本。备份放 EVOLCORE_HOME 而非包目录:
203
242
  // Linux/macOS 的全局目录常常不可写,而 HOME 一定可写。
204
243
  const upgradeBackup = await backupDistForUpgrade(p, log);
205
- const upgrade = await tryUpgrade();
244
+ const [upgrade, ecwebUpgrade] = await Promise.all([
245
+ tryUpgrade(),
246
+ tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME),
247
+ ]);
206
248
  let upgradeRolledBack;
207
249
  switch (upgrade.status) {
208
250
  case 'upgraded':
@@ -243,8 +285,8 @@ export async function cmdRestartMonitor() {
243
285
  await notifyDaemonOwners(p, `⚠️ evolcore 升级失败,使用当前版本继续\n${upgrade.error ?? ''}`.trim(), log);
244
286
  break;
245
287
  }
246
- // EC Web 是独立全局包,已安装时单独检查升级。
247
- const ecwebUpgrade = await tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME);
288
+ // EC Web 是独立全局包,已安装时单独检查升级;与 EvolCore registry
289
+ // 查询并行执行,npm 安装阶段由 npm-ops 的全局锁串行化。
248
290
  switch (ecwebUpgrade.status) {
249
291
  case 'upgraded':
250
292
  log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
@@ -710,6 +752,11 @@ async function revertToPreviousBuild(log) {
710
752
  * back up or the copy failed — the caller then knows rollback is unavailable.
711
753
  */
712
754
  async function backupDistForUpgrade(p, log) {
755
+ // Linked/source checkouts cannot be upgraded by npm. Copying dist/ here was
756
+ // pure restart overhead and made every source restart rewrite the artifact
757
+ // tree even when no build was needed.
758
+ if (fs.existsSync(path.join(targetPackageRoot(), 'src')))
759
+ return null;
713
760
  const distDir = path.join(targetPackageRoot(), 'dist');
714
761
  if (!distArtifactIsUsable(distDir))
715
762
  return null;
@@ -34,6 +34,7 @@ export function getManagementCommandPermissions(role) {
34
34
  'contact.read': { allow: true, scopes: ['agent'] },
35
35
  'contact.add': { allow: true, scopes: ['relation'], constraints: { ownPeerOnly: true, privateOnly: true } },
36
36
  'contact.block': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
37
+ 'contact.review': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
37
38
  'connect.write': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
38
39
  'connect.access.write': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
39
40
  'role.assign': { allow: true, scopes: ['agent'] },
@@ -51,6 +52,7 @@ export function getManagementCommandPermissions(role) {
51
52
  'ec.aid.lookup': { allow: true, scopes: ['agent'] },
52
53
  'ec.aid.agentmd.get': { allow: true, scopes: ['agent'] },
53
54
  'ec.aid.agentmd.put': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
55
+ 'ec.aid.avatar': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentOwner: true } },
54
56
  'ec.fs.mkdir': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
55
57
  'ec.fs.upload': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
56
58
  'ec.fs.copy': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
@@ -679,6 +679,21 @@ function groupFor(target, sel) {
679
679
  function normalizeAgentConfigForWrite(value) {
680
680
  const mutable = normalizeBaseagentEffortCompat({ ...value });
681
681
  normalizeShowActivitiesCompat(mutable);
682
+ if (Object.prototype.hasOwnProperty.call(mutable, 'owners')) {
683
+ if (!Array.isArray(mutable.owners)) {
684
+ throw new ConfigError('VALIDATION_ERROR', 'owners must be an array');
685
+ }
686
+ const owners = [];
687
+ for (const rawOwner of mutable.owners) {
688
+ if (typeof rawOwner !== 'string' || !rawOwner.trim() || !isValidAid(rawOwner.trim())) {
689
+ throw new ConfigError('VALIDATION_ERROR', `Invalid owner AID: ${JSON.stringify(rawOwner)}`);
690
+ }
691
+ const owner = rawOwner.trim();
692
+ if (!owners.includes(owner))
693
+ owners.push(owner);
694
+ }
695
+ mutable.owners = owners;
696
+ }
682
697
  // AID 格式校验
683
698
  if (mutable.aid && !isValidAid(mutable.aid)) {
684
699
  throw new ConfigError('VALIDATION_ERROR', `Invalid aid "${mutable.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
@@ -5,6 +5,9 @@ import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, list
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { resolvePaths, agentDir, agentRelationConfig } from '../paths.js';
8
+ import { roleMenuQuery, roleMenuUpdate } from '../core/command/role-menu.js';
9
+ import { parsePeerKey } from '../core/relation/peer-identity.js';
10
+ import { isExplicitGroupId } from '../aun/group-identity.js';
8
11
  export const AUTHENTICATED_CONFIG_ROLE_ENV = 'EVOLCORE_AUTHENTICATED_CONFIG_ROLE';
9
12
  export function executeResolvedConfigCommand(command, options = {}) {
10
13
  try {
@@ -23,6 +26,134 @@ export function executeResolvedConfigCommand(command, options = {}) {
23
26
  return { ok: false, code: 'CONFIG_ERROR', error: error instanceof Error ? error.message : String(error) };
24
27
  }
25
28
  }
29
+ /**
30
+ * Execute a config command that may use an asynchronous domain service.
31
+ * Relation role assignments are intentionally handled here instead of by
32
+ * mutateConfig(), so Role Menu remains the single write path for validation,
33
+ * revisions, journaling, and audit.
34
+ */
35
+ export async function executeResolvedConfigCommandAsync(command, options = {}) {
36
+ try {
37
+ if (isRelationRoleCommand(command)) {
38
+ return await executeRelationRoleCommand(command, options);
39
+ }
40
+ return executeResolvedConfigCommand(command, options);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof ConfigError)
44
+ return { ok: false, code: error.code, error: error.message, ...(error.data !== undefined ? { data: error.data } : {}) };
45
+ const roleError = error;
46
+ return {
47
+ ok: false,
48
+ code: roleError?.code || 'CONFIG_ERROR',
49
+ error: roleError?.message || String(error),
50
+ ...(roleError?.data !== undefined ? { data: roleError.data } : {}),
51
+ };
52
+ }
53
+ }
54
+ function isRelationRoleCommand(command) {
55
+ return command.kind === 'field'
56
+ && command.field === 'role'
57
+ && command.configScope === 'relation'
58
+ && (command.operationId === 'role.relation.read'
59
+ || command.operationId === 'role.assign'
60
+ || command.operationId === 'role.revoke');
61
+ }
62
+ async function executeRelationRoleCommand(op, options) {
63
+ if (!op.self || !op.peerKey) {
64
+ return { ok: false, code: 'PEER_REQUIRED', error: 'role requires --self <aid> --peer <peer-aid>' };
65
+ }
66
+ const target = roleTargetFromPeerKey(op.peerKey);
67
+ const context = options.roleContext ?? {
68
+ self: op.self,
69
+ actorRole: options.role || 'owner',
70
+ actorRoleSource: 'local-cli',
71
+ isDaemonOwner: false,
72
+ fromControlChannel: false,
73
+ aunAvailable: true,
74
+ };
75
+ if (context.self !== op.self) {
76
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Role target does not match the receiving Agent' };
77
+ }
78
+ const targetArgs = { self: op.self, view: 'target', target };
79
+ const current = await roleMenuQuery(context, targetArgs);
80
+ const explicitRole = current?.assignment?.explicitRole
81
+ ?? current?.assignment?.explicitDefaultRole
82
+ ?? null;
83
+ const effectiveRole = current?.effectiveRole?.roleId
84
+ ?? current?.baselineRole?.roleId
85
+ ?? null;
86
+ const effectiveRoleSource = current?.effectiveRole?.source
87
+ ?? current?.baselineRole?.source
88
+ ?? 'none';
89
+ const roleDetails = { explicitRole, effectiveRole, effectiveRoleSource };
90
+ if (op.subcommand === 'get') {
91
+ return {
92
+ ok: true,
93
+ subcommand: 'get',
94
+ field: op.field,
95
+ value: explicitRole,
96
+ scope: op.configScope,
97
+ roleDetails,
98
+ };
99
+ }
100
+ const nextValue = op.subcommand === 'unset' ? null : op.value;
101
+ const updated = await roleMenuUpdate(context, {
102
+ self: op.self,
103
+ resource: 'assignment',
104
+ target,
105
+ expectedPolicyRevision: current.policyRevision,
106
+ expectedRelationRevision: current.relationRevision,
107
+ }, JSON.stringify(nextValue));
108
+ const updatedExplicitRole = updated?.assignment?.explicitRole
109
+ ?? updated?.assignment?.explicitDefaultRole
110
+ ?? null;
111
+ const updatedEffectiveRole = updated?.effectiveRole?.roleId
112
+ ?? updated?.baselineRole?.roleId
113
+ ?? null;
114
+ const updatedEffectiveRoleSource = updated?.effectiveRole?.source
115
+ ?? updated?.baselineRole?.source
116
+ ?? 'none';
117
+ const updatedDetails = {
118
+ explicitRole: updatedExplicitRole,
119
+ effectiveRole: updatedEffectiveRole,
120
+ effectiveRoleSource: updatedEffectiveRoleSource,
121
+ };
122
+ if (op.subcommand === 'unset') {
123
+ return {
124
+ ok: true,
125
+ subcommand: 'unset',
126
+ field: op.field,
127
+ removed: updated.changed === true,
128
+ scope: op.configScope,
129
+ roleDetails: updatedDetails,
130
+ };
131
+ }
132
+ return {
133
+ ok: true,
134
+ subcommand: 'set',
135
+ field: op.field,
136
+ value: updatedExplicitRole,
137
+ scope: op.configScope,
138
+ file: 'relation-config',
139
+ roleDetails: updatedDetails,
140
+ };
141
+ }
142
+ function roleTargetFromPeerKey(peerKey) {
143
+ let parsed;
144
+ try {
145
+ parsed = parsePeerKey(peerKey);
146
+ }
147
+ catch {
148
+ throw new ConfigError('INVALID_SELECTOR', `Invalid relation peer key: ${peerKey}`);
149
+ }
150
+ if (parsed.channelType !== 'aun') {
151
+ throw new ConfigError('NOT_SUPPORTED', 'Relation role assignments currently support AUN peers only');
152
+ }
153
+ return isExplicitGroupId(parsed.channelId)
154
+ ? { kind: 'group', groupId: parsed.channelId }
155
+ : { kind: 'private', peerAid: parsed.channelId };
156
+ }
26
157
  export function executeResolvedConfigOperation(op, options = {}) {
27
158
  try {
28
159
  const missingAgent = selectedAgentError(op);
@@ -298,10 +429,25 @@ function getWithSchemaDefault(op, absent, note) {
298
429
  return { ok: true, subcommand: 'get', field: op.field, value: absent, scope: op.configScope, ...nt };
299
430
  }
300
431
  function executeSet(op, options) {
432
+ if (op.field === 'role') {
433
+ return {
434
+ ok: false,
435
+ code: 'ROLE_POLICY_MANAGED',
436
+ error: 'Relation role assignments require the asynchronous role assignment service',
437
+ };
438
+ }
301
439
  if (!op.route)
302
440
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
303
441
  const selector = selectorFor(op, options);
304
- mutateConfig(op.route.target, selector, existing => {
442
+ const mutation = mutateConfig(op.route.target, selector, existing => {
443
+ if (op.field === 'owners' && op.route.target === ConfigTarget.Agent) {
444
+ const nextOwners = Array.isArray(op.value) ? op.value : [];
445
+ if (nextOwners.length === 0) {
446
+ throw new ConfigError('LAST_OWNER_PROTECTED', 'owners must contain at least one owner');
447
+ }
448
+ setNested(existing, op.field, nextOwners);
449
+ return nextOwners;
450
+ }
305
451
  if (op.route.merge === 'list') {
306
452
  const previous = getNested(existing, op.field);
307
453
  const previousItems = Array.isArray(previous) ? previous : [];
@@ -316,25 +462,41 @@ function executeSet(op, options) {
316
462
  }
317
463
  });
318
464
  setNested(existing, op.field, merged);
465
+ return merged;
319
466
  }
320
467
  else {
321
468
  setNested(existing, op.field, op.value);
469
+ return op.value;
322
470
  }
323
471
  });
324
472
  return {
325
473
  ok: true,
326
474
  subcommand: 'set',
327
475
  field: op.field,
328
- value: op.value,
476
+ value: mutation.value,
329
477
  scope: op.configScope,
330
478
  file: op.route.schema,
331
479
  };
332
480
  }
333
481
  function executeUnset(op, options) {
482
+ if (op.field === 'role') {
483
+ return {
484
+ ok: false,
485
+ code: 'ROLE_POLICY_MANAGED',
486
+ error: 'Relation role assignments require the asynchronous role assignment service',
487
+ };
488
+ }
334
489
  if (!op.route)
335
490
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
336
491
  const selector = selectorFor(op, options);
337
- const { value: removed } = mutateConfig(op.route.target, selector, existing => {
492
+ const target = op.route.target;
493
+ const { value: removed } = mutateConfig(target, selector, existing => {
494
+ if (op.field === 'owners' && target === ConfigTarget.Agent) {
495
+ const current = getNested(existing, op.field);
496
+ if (Array.isArray(current) && current.length > 0) {
497
+ throw new ConfigError('LAST_OWNER_PROTECTED', 'cannot unset owners while an Agent still has owners; use owners replace with another owner');
498
+ }
499
+ }
338
500
  const parts = op.field.split('.');
339
501
  let current = existing;
340
502
  for (let i = 0; i < parts.length - 1; i++) {