evolcore 0.0.6 → 0.0.8

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.
@@ -15,6 +15,7 @@ import { WEB_PACKAGE_NAME } from '../product.js';
15
15
  import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
16
16
  import { rotateStdoutLog } from '../utils/log-writer.js';
17
17
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
18
+ import { serviceProxyNeedsEcweb, startEcwebIfEnabled } from './daemon-commands.js';
18
19
  const execFileAsync = promisify(execFile);
19
20
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
20
21
  function cleanEnv() {
@@ -92,6 +93,37 @@ export async function cmdRestartMonitor() {
92
93
  await sleep(500);
93
94
  }
94
95
  }
96
+ // A stale instance record can disappear while the real Windows process is
97
+ // still alive. Refuse to create a second daemon when an unregistered process
98
+ // from this package is found; a duplicate would connect AUN and kick the
99
+ // existing session before failing on IPC/port ownership.
100
+ const runtimeOrphans = findOrphanProcesses().filter(o => {
101
+ if (o.confirmedTestDaemon)
102
+ return false;
103
+ const homeMatches = o.evolcoreHome && normalizePath(o.evolcoreHome) === normalizePath(p.root);
104
+ const packageMatches = !o.evolcoreHome && normalizePath(o.cmdline).includes(normalizePath(getPackageRoot()));
105
+ return Boolean(homeMatches || packageMatches);
106
+ });
107
+ if (runtimeOrphans.length > 0) {
108
+ const pids = runtimeOrphans.map(o => o.pid).join(', ');
109
+ log(`Stopping unregistered EvolCore process(es): ${pids}`);
110
+ for (const orphan of runtimeOrphans) {
111
+ platform.killProcess(orphan.pid, false);
112
+ }
113
+ const exited = await Promise.all(runtimeOrphans.map(async (orphan) => {
114
+ if (await platform.waitForProcessExit(orphan.pid, 10_000))
115
+ return true;
116
+ platform.killProcess(orphan.pid, true);
117
+ return platform.waitForProcessExit(orphan.pid, 5_000);
118
+ }));
119
+ if (exited.some(result => !result)) {
120
+ const message = `未登记的 EvolCore 进程仍未退出(PID: ${pids}),已停止重启以避免重复连接 AUN。`;
121
+ log(message);
122
+ await notifyDaemonOwners(p, `❌ ${message}`, log);
123
+ process.exit(1);
124
+ }
125
+ cleanupInstances();
126
+ }
95
127
  // restart-pending.json 只留给新主进程发送发起会话内的“重启成功”回执。
96
128
  const pendingFile = path.join(daemonControlDir(), 'restart-pending.json');
97
129
  // 等待所有活 main 进程退出(可能不止一个)
@@ -107,24 +139,24 @@ export async function cmdRestartMonitor() {
107
139
  }
108
140
  catch { }
109
141
  }
110
- await Promise.all(oldPids.map(oldPid => new Promise((resolve) => {
111
- let waited = 0;
112
- const interval = setInterval(() => {
113
- waited++;
114
- if (!platform.isProcessRunning(oldPid)) {
115
- clearInterval(interval);
116
- log(`Process ${oldPid} has exited`);
117
- resolve();
118
- return;
119
- }
120
- if (waited >= 30) {
121
- clearInterval(interval);
122
- log(`ERROR: Process ${oldPid} still running after 30s, force killing`);
123
- platform.killProcess(oldPid, true);
124
- resolve();
125
- }
126
- }, 1000);
127
- })));
142
+ const exited = await Promise.all(oldPids.map(async (oldPid) => {
143
+ if (await platform.waitForProcessExit(oldPid, 30_000)) {
144
+ log(`Process ${oldPid} has exited`);
145
+ return true;
146
+ }
147
+ log(`ERROR: Process ${oldPid} still running after 30s, force killing`);
148
+ platform.killProcess(oldPid, true);
149
+ const forced = await platform.waitForProcessExit(oldPid, 5_000);
150
+ if (!forced)
151
+ log(`ERROR: Process ${oldPid} survived forced termination`);
152
+ return forced;
153
+ }));
154
+ if (exited.some(result => !result)) {
155
+ log('❌ Existing daemon process could not be stopped; refusing to spawn a replacement');
156
+ await notifyDaemonOwners(p, '❌ 旧 EvolCore 进程未能退出,已停止重启,避免重复连接 AUN。', log);
157
+ cleanupPendingFile(pendingFile, log);
158
+ process.exit(1);
159
+ }
128
160
  await sleep(3000);
129
161
  cleanupInstances();
130
162
  }
@@ -177,9 +209,22 @@ export async function cmdRestartMonitor() {
177
209
  await notifyDaemonOwners(p, `⚠️ ${WEB_PACKAGE_NAME} 升级失败,使用当前版本继续\n${ecwebUpgrade.error ?? ''}`.trim(), log);
178
210
  break;
179
211
  }
212
+ // source=ecweb 的 Service Proxy 只在 daemon 启动后的短窗口内发现实例。
213
+ // restart-monitor 直接 spawn daemon,必须在此先启动独立的 ec-web,不能绕过
214
+ // cmdStart 的 ECWeb 生命周期逻辑。
215
+ const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
216
+ ? await startEcwebIfEnabled(p)
217
+ : false;
218
+ if (ecwebStartedBeforeDaemon)
219
+ log('ECWeb started before daemon');
180
220
  // 启动并检测 ready signal
181
221
  let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
182
222
  if (started) {
223
+ // 没有 Service Proxy 时可以等 daemon ready 后再启动;有代理时已在上方启动,
224
+ // 避免重新拉起造成实例记录和反向隧道短暂失配。
225
+ if (!ecwebStartedBeforeDaemon && await startEcwebIfEnabled(p)) {
226
+ log('ECWeb started after daemon');
227
+ }
183
228
  log('✓ Service restarted successfully');
184
229
  archiveSelfHealLog(p, log);
185
230
  // 发起会话内的“重启成功”通知由新进程自行发送,此处只负责失败/自愈告警。
@@ -192,10 +237,25 @@ export async function cmdRestartMonitor() {
192
237
  cleanupPendingFile(pendingFile, log);
193
238
  process.exit(1);
194
239
  }
240
+ if (hasStartupResourceConflict(p)) {
241
+ const message = '服务启动失败:IPC 管道或端口仍被占用,已跳过自动修复,避免重复连接 AUN。';
242
+ log(message);
243
+ await notifyDaemonOwners(p, `❌ ${message}`, log);
244
+ cleanupPendingFile(pendingFile, log);
245
+ process.exit(1);
246
+ }
195
247
  // 启动失败,进入 self-heal 循环
196
248
  log('❌ Service failed to start, entering self-heal loop');
197
249
  eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
198
250
  await notifyDaemonOwners(p, '⚠️ 服务启动失败,正在尝试自动修复...', log);
251
+ const claudePath = platform.resolveCommandPath('claude');
252
+ if (!claudePath) {
253
+ const message = '找不到 claude CLI,已停止自动修复;请检查后台进程 PATH。';
254
+ log(message);
255
+ await notifyDaemonOwners(p, `❌ ${message}`, log);
256
+ cleanupPendingFile(pendingFile, log);
257
+ process.exit(1);
258
+ }
199
259
  for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
200
260
  // 前置检查:服务可能已被上一轮 claude 修复并启动
201
261
  if (isServiceAlive()) {
@@ -209,7 +269,7 @@ export async function cmdRestartMonitor() {
209
269
  log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
210
270
  eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
211
271
  await notifyDaemonOwners(p, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
212
- const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
272
+ const healed = await invokeClaude(claudePath, p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
213
273
  // 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
214
274
  if (isServiceAlive()) {
215
275
  log(`✓ Service is running after attempt ${attempt}`);
@@ -276,6 +336,30 @@ async function sendHealSummary(p, attempts, log) {
276
336
  function sleep(ms) {
277
337
  return new Promise(resolve => setTimeout(resolve, ms));
278
338
  }
339
+ function normalizePath(value) {
340
+ return value.replace(/[\\/]+/g, '/').toLowerCase();
341
+ }
342
+ function hasStartupResourceConflict(p) {
343
+ let daemonLog = null;
344
+ try {
345
+ daemonLog = fs.readdirSync(p.logs, { withFileTypes: true })
346
+ .filter(entry => entry.isFile() && /^daemon-.*\.log$/.test(entry.name))
347
+ .sort((a, b) => b.name.localeCompare(a.name))
348
+ .slice(0, 1)
349
+ .map(entry => path.join(p.logs, entry.name))[0] ?? null;
350
+ }
351
+ catch { }
352
+ const files = [path.join(p.logs, 'stdout.log'), ...(daemonLog ? [daemonLog] : [])];
353
+ return files.some(file => {
354
+ try {
355
+ const content = fs.readFileSync(file, 'utf-8');
356
+ return /EADDRINUSE|address already in use|pipe-.*already in use/i.test(content.slice(-64 * 1024));
357
+ }
358
+ catch {
359
+ return false;
360
+ }
361
+ });
362
+ }
279
363
  function cleanupPendingFile(filePath, log) {
280
364
  try {
281
365
  if (fs.existsSync(filePath)) {
@@ -339,9 +423,17 @@ async function spawnAndWaitReady(p, log, timeout) {
339
423
  }
340
424
  }
341
425
  log(`Ready signal not received within ${timeout / 1000}s`);
342
- // 超时后杀掉进程
426
+ // A timeout is still a live-process race: do not let self-heal spawn a
427
+ // replacement while the timed-out daemon owns the IPC endpoint or a port.
343
428
  if (platform.isProcessRunning(childPid)) {
344
429
  platform.killProcess(childPid);
430
+ if (!await platform.waitForProcessExit(childPid, 5_000)) {
431
+ log(`Process ${childPid} survived graceful termination, forcing kill`);
432
+ platform.killProcess(childPid, true);
433
+ if (!await platform.waitForProcessExit(childPid, 5_000)) {
434
+ log(`ERROR: Process ${childPid} survived forced termination`);
435
+ }
436
+ }
345
437
  }
346
438
  cleanupInstances();
347
439
  return false;
@@ -349,7 +441,7 @@ async function spawnAndWaitReady(p, log, timeout) {
349
441
  /**
350
442
  * 调用 claude CLI 进行自动修复
351
443
  */
352
- async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
444
+ async function invokeClaude(claudePath, p, attempt, maxAttempts, timeout, log) {
353
445
  const projectDir = getPackageRoot();
354
446
  const selfHealLog = p.selfHealLog;
355
447
  const stdoutLog = path.join(p.logs, 'stdout.log');
@@ -387,7 +479,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
387
479
  注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
388
480
  try {
389
481
  log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
390
- const { stdout, stderr } = await execFileAsync('claude', [
482
+ const { stdout, stderr } = await execFileAsync(claudePath, [
391
483
  '-p', prompt,
392
484
  '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
393
485
  '--output-format', 'text',
@@ -395,6 +487,8 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
395
487
  ], {
396
488
  cwd: projectDir,
397
489
  timeout,
490
+ // Windows npm CLIs are .cmd shims; execFile needs a shell to run them.
491
+ shell: platform.isWindows,
398
492
  env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
399
493
  maxBuffer: 10 * 1024 * 1024,
400
494
  });
@@ -17,6 +17,7 @@ import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agent
17
17
  import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
18
18
  import { fileCache } from '../core/daemon-file-cache.js';
19
19
  import { isValidAid } from '../aun/aid/validation.js';
20
+ import { normalizeAidDomain } from '../aun/aid/domain.js';
20
21
  import { isExplicitGroupId } from '../aun/group-identity.js';
21
22
  import { parseContactAlias } from './contact-alias.js';
22
23
  import { loadSchema, listSchemaVersions, currentVersion, isSchemaName, } from './schema-registry.js';
@@ -571,6 +572,20 @@ function normalizeAgentConfigForWrite(value) {
571
572
  }
572
573
  return mutable;
573
574
  }
575
+ function normalizeProcessConfigForWrite(value) {
576
+ const configuredAidDomain = value.aun?.defaultAidDomain;
577
+ if (configuredAidDomain === undefined)
578
+ return value;
579
+ try {
580
+ return {
581
+ ...value,
582
+ aun: { ...value.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
583
+ };
584
+ }
585
+ catch (error) {
586
+ throw new ConfigError('VALIDATION_ERROR', error instanceof Error ? error.message : String(error));
587
+ }
588
+ }
574
589
  function normalizeRelationConfigForWrite(value) {
575
590
  const mutable = { ...value };
576
591
  normalizeShowActivitiesCompat(mutable);
@@ -643,7 +658,9 @@ export function write(target, value, sel, opts = {}) {
643
658
  ? normalizeAgentConfigForWrite(migrated)
644
659
  : target === ConfigTarget.Relation
645
660
  ? normalizeRelationConfigForWrite(migrated)
646
- : migrated;
661
+ : target === ConfigTarget.Process
662
+ ? normalizeProcessConfigForWrite(migrated)
663
+ : migrated;
647
664
  // 1. Schema 校验
648
665
  if (!opts.skipValidate) {
649
666
  validateOrThrow(schema, normalized, target);
@@ -836,7 +853,7 @@ function ensureSchemaVersion(value, version) {
836
853
  }
837
854
  function validateOrThrow(schema, value, target) {
838
855
  const ok = schema.validate(value);
839
- const scopeErrors = validateProcessOnlyFields(target, value);
856
+ const scopeErrors = [...validateProcessOnlyFields(target, value), ...validateProcessAidDomain(target, value)];
840
857
  if (!ok || scopeErrors.length > 0) {
841
858
  const errs = [
842
859
  ...scopeErrors,
@@ -857,6 +874,20 @@ function validateProcessOnlyFields(target, value) {
857
874
  return [];
858
875
  return ['/debug is only allowed in process config'];
859
876
  }
877
+ function validateProcessAidDomain(target, value) {
878
+ if (target !== ConfigTarget.Process || !value || typeof value !== 'object')
879
+ return [];
880
+ const configuredAidDomain = value?.aun?.defaultAidDomain;
881
+ if (configuredAidDomain === undefined)
882
+ return [];
883
+ try {
884
+ normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain');
885
+ return [];
886
+ }
887
+ catch (error) {
888
+ return [`/aun/defaultAidDomain ${error instanceof Error ? error.message : String(error)}`];
889
+ }
890
+ }
860
891
  /**
861
892
  * responseModeParams 桶专项校验。
862
893
  *
@@ -908,7 +939,7 @@ export function validateConfig(target, value) {
908
939
  : loadSchema(TARGET_SCHEMA[target]);
909
940
  const withVer = ensureSchemaVersion(value, schema.version);
910
941
  const ok = schema.validate(withVer);
911
- const errors = validateProcessOnlyFields(target, withVer);
942
+ const errors = [...validateProcessOnlyFields(target, withVer), ...validateProcessAidDomain(target, withVer)];
912
943
  if (ok && errors.length === 0)
913
944
  return [];
914
945
  return [
@@ -29,6 +29,47 @@ const DISPLAY_NAMES = {
29
29
  };
30
30
  // ── 掩码 ──
31
31
  const ENV_PREFIX = '$ENV:';
32
+ // Claude Code supports both credential variable names. Keep AUTH_TOKEN first
33
+ // to match the runtime resolver's precedence.
34
+ const standardEnvKeys = {
35
+ apiKey: ['ANTHROPIC_AUTH_TOKEN'],
36
+ baseUrl: ['ANTHROPIC_BASE_URL'],
37
+ };
38
+ function envKeysForGateway(type, field) {
39
+ if (type === 'claude' && field === 'apiKey')
40
+ return ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY'];
41
+ return standardEnvKeys[field];
42
+ }
43
+ function firstEnvValue(env, names) {
44
+ for (const name of names) {
45
+ const value = env[name];
46
+ if (value)
47
+ return { name, value };
48
+ }
49
+ return undefined;
50
+ }
51
+ const DEBUG_SECRET_KEY = /(?:key|token|secret|password|authorization|credential|private)/i;
52
+ function redactDebugValue(value, key) {
53
+ if (typeof value === 'string') {
54
+ if (value === '(未设置)')
55
+ return value;
56
+ if (key && DEBUG_SECRET_KEY.test(key) && !value.startsWith(ENV_PREFIX))
57
+ return '***';
58
+ return value;
59
+ }
60
+ if (Array.isArray(value))
61
+ return value.map(item => redactDebugValue(item));
62
+ if (value && typeof value === 'object') {
63
+ return Object.fromEntries(Object.entries(value)
64
+ .map(([childKey, childValue]) => [childKey, redactDebugValue(childValue, childKey)]));
65
+ }
66
+ return value;
67
+ }
68
+ function redactMismatchValue(value, field) {
69
+ if (!value)
70
+ return value;
71
+ return field === 'apiKey' && !value.startsWith(ENV_PREFIX) ? '***' : value;
72
+ }
32
73
  function maskApiKey(raw) {
33
74
  if (typeof raw !== 'string' || !raw)
34
75
  return { mask: undefined, isEnvRef: false };
@@ -203,21 +244,17 @@ function detectEnvMismatch(defaults, aids) {
203
244
  // 读取全局 .env 文件
204
245
  const rootEnvPath = path.join(resolvePaths().root, '.env');
205
246
  const rootEnvVars = parseEnvFileSync(rootEnvPath);
206
- // 标准环境变量映射
207
- const standardEnvKeys = {
208
- apiKey: 'ANTHROPIC_AUTH_TOKEN',
209
- baseUrl: 'ANTHROPIC_BASE_URL',
210
- };
211
247
  // 记录进程环境变量
212
- const processEnvDebug = {
248
+ const processEnvDebug = redactDebugValue({
213
249
  ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '(未设置)',
214
250
  ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || '(未设置)',
215
- };
251
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '(未设置)',
252
+ });
216
253
  // 收集调试信息返回给前端
217
254
  result.debug = {
218
255
  processEnv: processEnvDebug,
219
- rootEnvFile: rootEnvVars,
220
- defaultsConfig: defaultsBa,
256
+ rootEnvFile: redactDebugValue(rootEnvVars),
257
+ defaultsConfig: redactDebugValue(defaultsBa),
221
258
  };
222
259
  // 检测全局默认配置中的环境变量
223
260
  for (const type of GATEWAY_TYPES) {
@@ -243,23 +280,25 @@ function detectEnvMismatch(defaults, aids) {
243
280
  }
244
281
  }
245
282
  // 情况2:配置值是实际值(非 $ENV 引用),检查是否应该使用环境变量
246
- else if (typeof val === 'string' && val && standardEnvKeys[field]) {
247
- const envVarName = standardEnvKeys[field];
248
- const fileValue = rootEnvVars[envVarName];
249
- const processValue = process.env[envVarName];
283
+ else if (typeof val === 'string' && val && envKeysForGateway(type, field)) {
284
+ const envKeys = envKeysForGateway(type, field);
285
+ const envValue = firstEnvValue(process.env, envKeys);
286
+ const fileValue = firstEnvValue(rootEnvVars, envKeys);
287
+ const envVarName = envValue?.name;
288
+ const processValue = envValue?.value;
250
289
  // 如果进程环境中有值,但配置中的值与进程环境不一致
251
- if (processValue && val !== processValue) {
290
+ if (processValue && val !== processValue && envVarName) {
252
291
  // 同时检查 .env 文件:如果 .env 也没有这个值,说明需要同步
253
- if (!fileValue || fileValue !== processValue) {
292
+ if (!fileValue || fileValue.value !== processValue) {
254
293
  result.hasMismatch = true;
255
294
  result.mismatches.push({
256
295
  aid: 'defaults',
257
296
  type,
258
297
  field,
259
- envValue: fileValue || '(.env 中未设置)',
260
- configValue: val,
298
+ envValue: redactMismatchValue(fileValue?.value, field) || '(.env 中未设置)',
299
+ configValue: redactMismatchValue(val, field) || val,
261
300
  envVarName,
262
- processValue,
301
+ processValue: redactMismatchValue(processValue, field),
263
302
  });
264
303
  }
265
304
  }
@@ -295,18 +334,20 @@ function detectEnvMismatch(defaults, aids) {
295
334
  }
296
335
  }
297
336
  // 情况2:配置值是实际值
298
- else if (typeof val === 'string' && val && standardEnvKeys[field]) {
299
- const envVarName = standardEnvKeys[field];
300
- const fileValue = agentEnvVars[envVarName];
301
- const processValue = process.env[envVarName];
302
- if (processValue && val !== processValue) {
303
- if (!fileValue || fileValue !== processValue) {
337
+ else if (typeof val === 'string' && val && envKeysForGateway(type, field)) {
338
+ const envKeys = envKeysForGateway(type, field);
339
+ const envValue = firstEnvValue(process.env, envKeys);
340
+ const fileValue = firstEnvValue(agentEnvVars, envKeys);
341
+ const envVarName = envValue?.name;
342
+ const processValue = envValue?.value;
343
+ if (processValue && val !== processValue && envVarName) {
344
+ if (!fileValue || fileValue.value !== processValue) {
304
345
  result.hasMismatch = true;
305
346
  result.mismatches.push({
306
347
  aid,
307
348
  type,
308
349
  field,
309
- envValue: fileValue || '(.env 中未设置)',
350
+ envValue: redactMismatchValue(fileValue?.value, field) || '(.env 中未设置)',
310
351
  configValue: `配置值与环境变量 ${envVarName} 不一致`,
311
352
  });
312
353
  }
@@ -707,11 +748,13 @@ export async function gatewaySyncEnv(args) {
707
748
  }
708
749
  // 情况2:实际值型,但与进程环境变量不一致
709
750
  else {
710
- const standardEnvKey = field === 'apiKey' ? 'ANTHROPIC_AUTH_TOKEN' : field === 'baseUrl' ? 'ANTHROPIC_BASE_URL' : null;
711
- if (standardEnvKey) {
712
- const processValue = process.env[standardEnvKey];
713
- if (processValue && val !== processValue) {
714
- block[field] = processValue;
751
+ const envKeys = envKeysForGateway(type, field);
752
+ const envValue = envKeys
753
+ ? firstEnvValue(process.env, envKeys)
754
+ : undefined;
755
+ if (envValue) {
756
+ if (val !== envValue.value) {
757
+ block[field] = envValue.value;
715
758
  hasChanges = true;
716
759
  }
717
760
  }
@@ -751,11 +794,13 @@ export async function gatewaySyncEnv(args) {
751
794
  }
752
795
  // 情况2:实际值型
753
796
  else {
754
- const standardEnvKey = field === 'apiKey' ? 'ANTHROPIC_AUTH_TOKEN' : field === 'baseUrl' ? 'ANTHROPIC_BASE_URL' : null;
755
- if (standardEnvKey) {
756
- const processValue = process.env[standardEnvKey];
757
- if (processValue && val !== processValue) {
758
- block[field] = processValue;
797
+ const envKeys = envKeysForGateway(type, field);
798
+ const envValue = envKeys
799
+ ? firstEnvValue(process.env, envKeys)
800
+ : undefined;
801
+ if (envValue) {
802
+ if (val !== envValue.value) {
803
+ block[field] = envValue.value;
759
804
  hasChanges = true;
760
805
  }
761
806
  }
@@ -20,6 +20,7 @@ import path from 'path';
20
20
  import { resolvePaths, agentDir, } from './paths.js';
21
21
  import { atomicReadJson, atomicWriteJson } from './utils/atomic-write.js';
22
22
  import { checkAgentDir, isValidAid } from './aun/aid/validation.js';
23
+ import { normalizeAidDomain } from './aun/aid/domain.js';
23
24
  import { isValidChannelName } from './core/channel-loader.js';
24
25
  import { CONFIG_SCHEMA_VERSION } from './types.js';
25
26
  import { ConfigTarget, read as cfgRead, write as cfgWrite } from './config/config-manager.js';
@@ -29,7 +30,7 @@ import { parseStableSemver } from './utils/stable-semver.js';
29
30
  /** 读 {root}/daemon.json。文件不存在返回 {},不报错。 */
30
31
  export function loadDaemonConfig() {
31
32
  const raw = atomicReadJson(resolvePaths().daemonConfig);
32
- return validateDaemonConfig(raw ?? {});
33
+ return validateDaemonConfig(raw ?? {}, false);
33
34
  }
34
35
  let eckSnapshotsConfigCache;
35
36
  /** Parse the process-level snapshot gate once for the current daemon lifecycle. */
@@ -50,9 +51,16 @@ export function isEckSnapshotsEnabled() {
50
51
  }
51
52
  /** 原子写入 {root}/daemon.json。调用方负责传完整对象(含要保留的字段)。 */
52
53
  export function saveDaemonConfig(value) {
53
- atomicWriteJson(resolvePaths().daemonConfig, validateDaemonConfig(value));
54
+ const configPath = resolvePaths().daemonConfig;
55
+ const current = atomicReadJson(configPath);
56
+ // A legacy default is irrelevant while an existing control AID is retained.
57
+ // Validate the field when it is created or changed, but do not make unrelated
58
+ // daemon.json updates fail because of a persisted, unused legacy value.
59
+ const validateAidDomain = current === null
60
+ || current.aun?.defaultAidDomain !== value.aun?.defaultAidDomain;
61
+ atomicWriteJson(configPath, validateDaemonConfig(value, validateAidDomain));
54
62
  }
55
- function validateDaemonConfig(value) {
63
+ function validateDaemonConfig(value, validateAidDomain) {
56
64
  const minEvolVersion = value.aun?.minEvolVersion;
57
65
  if (minEvolVersion !== undefined && !parseStableSemver(minEvolVersion)) {
58
66
  throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
@@ -61,6 +69,10 @@ function validateDaemonConfig(value) {
61
69
  if (menuTokenRequired !== undefined && typeof menuTokenRequired !== 'boolean') {
62
70
  throw new Error('daemon.json.aun.menuTokenRequired must be a boolean');
63
71
  }
72
+ if (validateAidDomain && value.aun?.defaultAidDomain !== undefined) {
73
+ const defaultAidDomain = normalizeAidDomain(value.aun.defaultAidDomain, 'daemon.json.aun.defaultAidDomain');
74
+ return { ...value, aun: { ...value.aun, defaultAidDomain } };
75
+ }
64
76
  return value;
65
77
  }
66
78
  const SUPPORTED_CHANNEL_TYPES = new Set([
@@ -20,16 +20,16 @@ export class AgentLoader {
20
20
  * Iterate over all EvolAgents in the registry × all registered plugins.
21
21
  * Each successful (agent, plugin) pair yields one runner instance.
22
22
  */
23
- createAll(registry, callbacks) {
23
+ createAll(registry, callbacks, creationErrors) {
24
24
  const instances = [];
25
25
  const allAgents = registry.runnableAgents();
26
26
  for (const agent of allAgents) {
27
- instances.push(...this.createForAgent(agent, callbacks));
27
+ instances.push(...this.createForAgent(agent, callbacks, creationErrors));
28
28
  }
29
29
  return instances;
30
30
  }
31
31
  /** Create runners for one EvolAgent, used by runtime hot-load. */
32
- createForAgent(agent, callbacks) {
32
+ createForAgent(agent, callbacks, creationErrors) {
33
33
  const instances = [];
34
34
  for (const [pluginName, plugin] of this.plugins) {
35
35
  if (!plugin.isEnabled(agent)) {
@@ -46,6 +46,8 @@ export class AgentLoader {
46
46
  logger.info(`✓ Runner created: agent=${instance.evolagentName} baseagent=${instance.baseagent}`);
47
47
  }
48
48
  catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ creationErrors?.push({ evolagentName: agent.name, baseagent: pluginName, message });
49
51
  logger.error(`✗ Failed to create runner for agent='${agent.name}' baseagent='${pluginName}':`, error);
50
52
  }
51
53
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { execFileSync } from 'child_process';
4
+ import { execCodexCliSync } from '../../../utils/codex-cli.js';
5
5
  function listDirectories(dir) {
6
6
  try {
7
7
  if (!fs.existsSync(dir))
@@ -34,7 +34,7 @@ function discoverSkills(ctx) {
34
34
  }
35
35
  function runCodexJson(args) {
36
36
  try {
37
- const output = execFileSync('codex', args, {
37
+ const output = execCodexCliSync(args, {
38
38
  encoding: 'utf-8',
39
39
  timeout: 3000,
40
40
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -61,6 +61,8 @@ export class ChannelLoader {
61
61
  aid: agent.aid,
62
62
  owner: agent.getOwner(aunEffName),
63
63
  enabled: true,
64
+ keystorePath: agent.config.aun?.keystorePath,
65
+ gatewayUrl: agent.config.aun?.gatewayUrl,
64
66
  };
65
67
  const configInsts = [aunInst];
66
68
  for (const inst of agent.config.channels) {
@@ -240,7 +242,14 @@ export function buildReloadHooks(deps) {
240
242
  const isImplicitAun = channelName === aunEffName;
241
243
  // Find config instance: implicit AUN gets a synthetic entry; others scan channels[].
242
244
  const cfgInst = isImplicitAun
243
- ? { type: 'aun', name: aunEffName, aid, enabled: true }
245
+ ? {
246
+ type: 'aun',
247
+ name: aunEffName,
248
+ aid,
249
+ enabled: true,
250
+ keystorePath: agent.config?.aun?.keystorePath,
251
+ gatewayUrl: agent.config?.aun?.gatewayUrl,
252
+ }
244
253
  : (() => {
245
254
  const agentChannels = agent.config?.channels ?? [];
246
255
  return agentChannels.find((i) => {
@@ -4,6 +4,7 @@ import { modelMatches } from '../model/model-catalog.js';
4
4
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
5
5
  import { hasModelSwitcher } from '../../agents/runner-types.js';
6
6
  import { getCodexEfforts } from '../../agents/codex-runner.js';
7
+ import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
7
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
8
9
  import { buildEnvelope } from '../message/message-utils.js';
9
10
  import path from 'path';
@@ -45,15 +46,25 @@ import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from '.
45
46
  */
46
47
  function getBaseagentVersion(cmd) {
47
48
  try {
48
- const output = execFileSync(cmd, ['--version'], {
49
- encoding: 'utf-8',
50
- timeout: 3000,
51
- stdio: ['ignore', 'pipe', 'pipe'],
52
- }).trim();
49
+ const executable = cmd === 'codex' ? resolveCodexCliPath() : cmd;
50
+ if (!executable)
51
+ return null;
52
+ const output = cmd === 'codex'
53
+ ? execCodexCliSync(['--version'], {
54
+ encoding: 'utf-8',
55
+ timeout: 3000,
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ })
58
+ : execFileSync(executable, ['--version'], {
59
+ encoding: 'utf-8',
60
+ timeout: 3000,
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ });
63
+ const versionOutput = String(output).trim();
53
64
  // claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
54
65
  // gemini: "0.38.0" → 直接返回
55
66
  // codex: "codex-cli 0.142.0" → 提取 "0.142.0"
56
- const match = output.match(/(\d+\.\d+\.\d+)/);
67
+ const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
57
68
  return match ? match[1] : null;
58
69
  }
59
70
  catch {