evolcore 0.0.18 → 0.0.20

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 (51) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +2 -0
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/claude-runner.js +4 -3
  5. package/dist/agents/codex-runner.js +25 -20
  6. package/dist/agents/ecagent-runner.js +3 -2
  7. package/dist/aun/aid/agentmd.js +7 -0
  8. package/dist/aun/msg/group.js +14 -3
  9. package/dist/aun/msg/p2p.js +21 -11
  10. package/dist/aun/outbox.js +144 -19
  11. package/dist/channels/aun.js +621 -211
  12. package/dist/cli/daemon-commands.js +41 -8
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/init.js +55 -15
  15. package/dist/cli/restart-monitor.js +3 -3
  16. package/dist/config/aun-gateway-config.js +2 -0
  17. package/dist/config/config-manager.js +92 -8
  18. package/dist/config/config-operation-service.js +1 -2
  19. package/dist/config/gateway-config.js +9 -7
  20. package/dist/config/lifecycle.js +16 -5
  21. package/dist/config-store.js +13 -6
  22. package/dist/core/auth/authorization-audit.js +5 -2
  23. package/dist/core/bootstrap-messages.js +2 -2
  24. package/dist/core/bootstrap-service.js +21 -36
  25. package/dist/core/channel-loader.js +0 -2
  26. package/dist/core/data-migration.js +10 -4
  27. package/dist/core/evolagent.js +5 -4
  28. package/dist/core/message/message-bridge.js +6 -11
  29. package/dist/core/message/response-engine.js +62 -6
  30. package/dist/core/permission/ec-command-parser.js +203 -24
  31. package/dist/core/permission/sandbox-runtime.js +46 -12
  32. package/dist/core/permission/tool-policy.js +116 -47
  33. package/dist/core/relation/peer-identity.js +18 -0
  34. package/dist/eck/kit-renderer.js +17 -8
  35. package/dist/index.js +30 -19
  36. package/dist/ipc.js +6 -1
  37. package/dist/paths.js +0 -3
  38. package/dist/utils/stats.js +52 -18
  39. package/dist/utils/welcome.js +2 -2
  40. package/kits/docs/path-registry.md +1 -1
  41. package/kits/rules/01-overview.md +1 -1
  42. package/kits/rules/02-navigation.md +2 -2
  43. package/kits/rules/03-identity.md +1 -1
  44. package/kits/rules/05-venue.md +1 -1
  45. package/kits/schemas/_meta.json +7 -4
  46. package/kits/schemas/agent-config.schema.10.json +2 -1
  47. package/kits/schemas/agent-config.schema.11.json +408 -0
  48. package/kits/schemas/daemon.schema.5.json +136 -0
  49. package/kits/schemas/defaults.schema.5.json +107 -0
  50. package/package.json +2 -1
  51. package/dist/core/message/pause-controller.js +0 -53
@@ -58,15 +58,21 @@ function printNoSelfAgentHints(options) {
58
58
  console.log(` - ${skippedAgent.dirName}: ${skippedAgent.reason}`);
59
59
  }
60
60
  }
61
- async function probeDaemon(socketPath, timeoutMs = 1000) {
61
+ // Windows named-pipe connection setup can take a couple of seconds on a busy
62
+ // host (especially on the first request after the daemon starts). A one-second
63
+ // probe causes `ec stop` to skip the IPC shutdown path and fall back to
64
+ // taskkill, even though the daemon is healthy and able to shut down gracefully.
65
+ const DAEMON_PROBE_TIMEOUT_MS = platform.isWindows ? 5_000 : 1_000;
66
+ export const DAEMON_SHUTDOWN_TIMEOUT_MS = platform.isWindows ? 10_000 : 3_000;
67
+ async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS) {
62
68
  const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
63
69
  if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
64
70
  return null;
65
71
  return response;
66
72
  }
67
73
  /** Ask the daemon to run its own graceful shutdown lifecycle. */
68
- export async function requestDaemonShutdown(socketPath, timeoutMs = 3_000, reason = 'cli', expectedPids) {
69
- const daemon = await probeDaemon(socketPath, Math.min(timeoutMs, 1_000));
74
+ export async function requestDaemonShutdown(socketPath, timeoutMs = DAEMON_SHUTDOWN_TIMEOUT_MS, reason = 'cli', expectedPids) {
75
+ const daemon = await probeDaemon(socketPath, Math.min(timeoutMs, DAEMON_PROBE_TIMEOUT_MS));
70
76
  if (!daemon || (expectedPids && !expectedPids.has(daemon.pid)))
71
77
  return null;
72
78
  const response = await ipcQuery(socketPath, {
@@ -218,6 +224,33 @@ function formatLocalTime(ms) {
218
224
  const d = new Date(ms);
219
225
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
220
226
  }
227
+ function formatRunningDuration(ms) {
228
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
229
+ const seconds = totalSeconds % 60;
230
+ const totalMinutes = Math.floor(totalSeconds / 60);
231
+ if (totalMinutes === 0)
232
+ return `${seconds}秒`;
233
+ const minutes = totalMinutes % 60;
234
+ const totalHours = Math.floor(totalMinutes / 60);
235
+ if (totalHours === 0)
236
+ return `${minutes}分${seconds}秒`;
237
+ const hours = totalHours % 24;
238
+ const days = Math.floor(totalHours / 24);
239
+ if (days === 0)
240
+ return `${hours}小时${minutes}分${seconds}秒`;
241
+ return `${days}天${hours}小时${minutes}分${seconds}秒`;
242
+ }
243
+ function resolveRunningDuration(pid, uptime) {
244
+ if (typeof uptime === 'number' && Number.isFinite(uptime) && uptime >= 0) {
245
+ return formatRunningDuration(uptime);
246
+ }
247
+ const processStartedAt = getProcessStartTime(pid);
248
+ if (processStartedAt !== null) {
249
+ return formatRunningDuration(Date.now() - processStartedAt);
250
+ }
251
+ const instance = scanInstances().mains.find(entry => entry.alive && entry.record.pid === pid);
252
+ return instance ? formatRunningDuration(Date.now() - instance.record.startedAt) : '未知';
253
+ }
221
254
  function printStartupInfo(opts = {}) {
222
255
  const pkgRoot = getPackageRoot();
223
256
  const isNpmInstall = pkgRoot.includes('node_modules');
@@ -273,7 +306,7 @@ export async function cmdStart(opts = {}) {
273
306
  const existingDaemon = await probeDaemon(p.socket);
274
307
  if (existingDaemon) {
275
308
  console.log(` EvolCore is already running (PID: ${existingDaemon.pid}, IPC reachable)`);
276
- console.log(' 已在运行;部署/脚本可直接复用当前实例');
309
+ console.log(` 已在运行;已运行时长: ${resolveRunningDuration(existingDaemon.pid, existingDaemon.uptime)};部署/脚本可直接复用当前实例`);
277
310
  return;
278
311
  }
279
312
  ensureDataDirs();
@@ -373,7 +406,7 @@ export async function cmdStart(opts = {}) {
373
406
  console.log(` ${symbol} ${aid} — 最后活动 ${ago} (${info.event})`);
374
407
  }
375
408
  }
376
- console.log(' 已在运行;部署/脚本可继续轮询 IPC ready,或直接复用当前实例');
409
+ console.log(` 已在运行;已运行时长: ${formatRunningDuration(Date.now() - first.record.startedAt)};部署/脚本可继续轮询 IPC ready,或直接复用当前实例`);
377
410
  console.log(' 使用 ec restart 重启,或 ec stop 先停止');
378
411
  return;
379
412
  }
@@ -627,7 +660,7 @@ export async function cmdStop() {
627
660
  console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(orphan => orphan.pid).join(', ')}`);
628
661
  }
629
662
  const gracefulPid = ping && pids.has(ping.pid)
630
- ? await requestDaemonShutdown(p.socket, 3_000, 'ec stop', pids)
663
+ ? await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec stop', pids)
631
664
  : null;
632
665
  const stopped = await Promise.all([...pids].map(pid => stopPid(pid, pid === gracefulPid)));
633
666
  if (stopped.some(result => !result)) {
@@ -699,7 +732,7 @@ export async function cmdRestart(opts = {}) {
699
732
  const mainPids = new Set(aliveMains.map(entry => entry.record.pid));
700
733
  const currentPing = await probeDaemon(socketPath);
701
734
  const gracefulPid = currentPing && mainPids.has(currentPing.pid)
702
- ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', mainPids)
735
+ ? await requestDaemonShutdown(socketPath, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec restart', mainPids)
703
736
  : null;
704
737
  const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid, m.record.pid === gracefulPid)));
705
738
  if (stopped.some(result => !result)) {
@@ -732,7 +765,7 @@ export async function cmdRestart(opts = {}) {
732
765
  const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
733
766
  const currentPing = await probeDaemon(socketPath);
734
767
  const gracefulPid = currentPing && orphanPids.has(currentPing.pid)
735
- ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', orphanPids)
768
+ ? await requestDaemonShutdown(socketPath, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec restart', orphanPids)
736
769
  : null;
737
770
  const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid, o.pid === gracefulPid)));
738
771
  if (stopped.some(result => !result)) {
package/dist/cli/index.js CHANGED
@@ -118,6 +118,7 @@ export async function main(args) {
118
118
 
119
119
  仅初始化 defaults.json:
120
120
  ec init 交互式(写完 defaults.json 后嵌套 agent new)
121
+ 选择 ecagent 时会继续询问 API Key 和 Base URL
121
122
  ec init --non-interactive [选项]
122
123
  --baseagent <claude|codex|gemini|ecagent> 默认: PATH 中第一个可用项;ecagent 需显式选择
123
124
  --force 已存在 defaults.json 时覆盖
package/dist/cli/init.js CHANGED
@@ -8,7 +8,7 @@ import { scanInstances } from '../utils/instance-registry.js';
8
8
  import { saveDefaultsSafe, loadAllAgents, loadDaemonConfig, saveDaemonConfig } from '../config-store.js';
9
9
  import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
10
10
  import { getCodexAppServerAvailability } from '../agents/codex-runner.js';
11
- import { resolveEcagentConfig } from '../agents/baseagent.js';
11
+ import { DEFAULT_ECAGENT_BASE_URL, resolveEcagentConfig } from '../agents/baseagent.js';
12
12
  import { defaultProjectsRoot } from '../utils/project-path.js';
13
13
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
14
14
  import { autostartInstalled, autostartPlatformLabel, configureAutostart } from '../utils/autostart.js';
@@ -79,14 +79,14 @@ function ecagentUnavailableReason() {
79
79
  return error instanceof Error ? error.message : String(error);
80
80
  }
81
81
  }
82
- function buildDefaults(chosen, available, projectsDefaultPath) {
82
+ function buildDefaults(chosen, available, projectsDefaultPath, ecagentConfig) {
83
83
  const baseagents = {};
84
84
  for (const b of available) {
85
85
  baseagents[b] = {};
86
86
  }
87
87
  // ecagent is bundled with EvolCore, so declare it even when the caller
88
88
  // supplies a reduced availability list (for example, a test probe).
89
- baseagents.ecagent ??= {};
89
+ baseagents.ecagent = ecagentConfig ?? baseagents.ecagent ?? {};
90
90
  return {
91
91
  $schema_version: 1,
92
92
  active_baseagent: chosen,
@@ -94,8 +94,8 @@ function buildDefaults(chosen, available, projectsDefaultPath) {
94
94
  ...(projectsDefaultPath ? { projects: { defaultPath: projectsDefaultPath } } : {}),
95
95
  };
96
96
  }
97
- function writeDefaults(chosen, available, projectsDefaultPath) {
98
- saveDefaultsSafe(buildDefaults(chosen, available, projectsDefaultPath));
97
+ function writeDefaults(chosen, available, projectsDefaultPath, ecagentConfig) {
98
+ saveDefaultsSafe(buildDefaults(chosen, available, projectsDefaultPath, ecagentConfig));
99
99
  }
100
100
  function hasCompletedInitConfig() {
101
101
  const p = resolvePaths();
@@ -263,7 +263,7 @@ export async function cmdInit(options) {
263
263
  }
264
264
  }
265
265
  const available = detectAvailable(codexAvailability);
266
- if (available.length === 0 || (available.length === 1 && available[0] === 'ecagent' && ecagentUnavailableReason())) {
266
+ if (available.length === 0) {
267
267
  console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
268
268
  console.log(' - claude CLI');
269
269
  console.log(' - gemini CLI');
@@ -361,13 +361,6 @@ export async function cmdInit(options) {
361
361
  console.log(` ${input} 当前环境不可用(可用: ${available.join('/')})`);
362
362
  continue;
363
363
  }
364
- if (input === 'ecagent') {
365
- const reason = ecagentUnavailableReason();
366
- if (reason) {
367
- console.log(` ecagent 当前环境不可用:${reason}`);
368
- continue;
369
- }
370
- }
371
364
  chosen = input;
372
365
  }
373
366
  return chosen;
@@ -405,13 +398,59 @@ export async function cmdInit(options) {
405
398
  return resolved;
406
399
  }
407
400
  }
401
+ async function askEcagentConfig() {
402
+ let configured;
403
+ try {
404
+ const raw = JSON.parse(fs.readFileSync(defaultsPath, 'utf8'));
405
+ const candidate = raw?.baseagents?.ecagent;
406
+ if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
407
+ configured = candidate;
408
+ }
409
+ }
410
+ catch {
411
+ // A malformed or missing defaults file should not prevent the user
412
+ // from supplying a fresh ecagent configuration during init.
413
+ configured = undefined;
414
+ }
415
+ const usable = (value) => {
416
+ if (typeof value !== 'string')
417
+ return false;
418
+ const normalized = value.trim();
419
+ if (!normalized)
420
+ return false;
421
+ const lower = normalized.toLowerCase();
422
+ return !lower.includes('your-') && !lower.includes('placeholder');
423
+ };
424
+ const configuredApiKey = usable(configured?.apiKey) ? configured.apiKey.trim() : undefined;
425
+ const environmentApiKeyName = !configuredApiKey
426
+ ? (usable(process.env.ECAGENT_API_KEY)
427
+ ? 'ECAGENT_API_KEY'
428
+ : usable(process.env.OPENAI_API_KEY) ? 'OPENAI_API_KEY' : undefined)
429
+ : undefined;
430
+ let apiKey = '';
431
+ while (!apiKey) {
432
+ const suffix = configuredApiKey || environmentApiKeyName ? '(回车保留当前配置)' : '(必填)';
433
+ const input = (await ask(rl, `ecagent API Key ${suffix}: `)).trim();
434
+ apiKey = input || configuredApiKey || (environmentApiKeyName ? '${' + environmentApiKeyName + '}' : '');
435
+ if (!apiKey)
436
+ console.log(' API Key 不能为空,请重新输入');
437
+ }
438
+ const configuredBaseUrl = [
439
+ configured?.baseUrl,
440
+ process.env.ECAGENT_BASE_URL,
441
+ DEFAULT_ECAGENT_BASE_URL,
442
+ ].find(usable) || DEFAULT_ECAGENT_BASE_URL;
443
+ const baseUrl = (await ask(rl, `ecagent Base URL [${configuredBaseUrl}]: `)).trim() || configuredBaseUrl;
444
+ return { apiKey, baseUrl };
445
+ }
408
446
  try {
409
447
  if (defaultsExisted) {
410
448
  const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
411
449
  if (ans === 'y' || ans === 'yes') {
412
450
  const chosen = await askBaseagent();
451
+ const ecagentConfig = chosen === 'ecagent' ? await askEcagentConfig() : undefined;
413
452
  const projectsDefaultPath = await askProjectsDefaultPath();
414
- writeDefaults(chosen, available, projectsDefaultPath);
453
+ writeDefaults(chosen, available, projectsDefaultPath, ecagentConfig);
415
454
  console.log(`\n✓ 已覆盖: ${defaultsPath}`);
416
455
  console.log(` active_baseagent: ${chosen}\n`);
417
456
  }
@@ -421,8 +460,9 @@ export async function cmdInit(options) {
421
460
  }
422
461
  else {
423
462
  const chosen = await askBaseagent();
463
+ const ecagentConfig = chosen === 'ecagent' ? await askEcagentConfig() : undefined;
424
464
  const projectsDefaultPath = await askProjectsDefaultPath();
425
- writeDefaults(chosen, available, projectsDefaultPath);
465
+ writeDefaults(chosen, available, projectsDefaultPath, ecagentConfig);
426
466
  console.log(`\n✓ 已创建: ${defaultsPath}`);
427
467
  console.log(` active_baseagent: ${chosen}\n`);
428
468
  }
@@ -14,7 +14,7 @@ import { WEB_PACKAGE_NAME } from '../product.js';
14
14
  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
- import { requestDaemonShutdown } from './daemon-commands.js';
17
+ import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown } from './daemon-commands.js';
18
18
  const execFileAsync = promisify(execFile);
19
19
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
20
20
  function cleanEnv() {
@@ -107,7 +107,7 @@ export async function cmdRestartMonitor() {
107
107
  const pids = runtimeOrphans.map(o => o.pid).join(', ');
108
108
  log(`Stopping unregistered EvolCore process(es): ${pids}`);
109
109
  const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
110
- const ping = await requestDaemonShutdown(p.socket, 3_000, 'restart-monitor orphan cleanup', orphanPids);
110
+ const ping = await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'restart-monitor orphan cleanup', orphanPids);
111
111
  for (const orphan of runtimeOrphans) {
112
112
  if (orphan.pid !== ping)
113
113
  platform.killProcess(orphan.pid, false);
@@ -134,7 +134,7 @@ export async function cmdRestartMonitor() {
134
134
  if (aliveMains.length > 0) {
135
135
  const oldPids = aliveMains.map(m => m.record.pid);
136
136
  log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
137
- const gracefulPid = await requestDaemonShutdown(p.socket, 3_000, 'restart-monitor', new Set(oldPids));
137
+ const gracefulPid = await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'restart-monitor', new Set(oldPids));
138
138
  // 没有收到 IPC shutdown 确认的进程才走信号兜底
139
139
  for (const pid of oldPids) {
140
140
  if (pid !== gracefulPid) {
@@ -0,0 +1,2 @@
1
+ /** Compatibility exports for retired AUN gateway override handling. */
2
+ export { legacyAunGatewayConfigPaths, stripLegacyAunGatewayConfig, } from './config-manager.js';
@@ -29,6 +29,74 @@ import { clearRoleStoreCache, isRoleName } from './role-store.js';
29
29
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
30
30
  import { mutateContactBookTransactionSync } from './contact-book-store.js';
31
31
  import { DAEMON_SCHEMA_NAME } from '../product.js';
32
+ // AUN gateway discovery belongs to fastaun. Keep the retired gatewayUrl
33
+ // compatibility handling beside the rest of the configuration read/write
34
+ // policy so all config callers share one implementation.
35
+ const warnedLegacyGatewayFiles = new Set();
36
+ export function legacyAunGatewayConfigPaths(value) {
37
+ if (!value || typeof value !== 'object' || Array.isArray(value))
38
+ return [];
39
+ const record = value;
40
+ const paths = [];
41
+ if (record.aun && typeof record.aun === 'object' && !Array.isArray(record.aun)
42
+ && Object.prototype.hasOwnProperty.call(record.aun, 'gatewayUrl')) {
43
+ paths.push('/aun/gatewayUrl');
44
+ }
45
+ if (Array.isArray(record.channels)) {
46
+ record.channels.forEach((channel, index) => {
47
+ if (channel && typeof channel === 'object'
48
+ && channel.type === 'aun'
49
+ && Object.prototype.hasOwnProperty.call(channel, 'gatewayUrl')) {
50
+ paths.push(`/channels/${index}/gatewayUrl`);
51
+ }
52
+ });
53
+ }
54
+ return paths;
55
+ }
56
+ export function legacyAunGatewayConfigError(file, paths) {
57
+ return `${file}: AUN gatewayUrl at ${paths.join(', ')} ${paths.length === 1 ? 'is' : 'are'} no longer supported; `
58
+ + 'fastaun owns gateway discovery';
59
+ }
60
+ /** Reject newly submitted gateway overrides. Legacy persisted values are handled on read. */
61
+ export function assertNoLegacyAunGatewayConfig(value, file) {
62
+ const paths = legacyAunGatewayConfigPaths(value);
63
+ if (paths.length > 0)
64
+ throw new Error(legacyAunGatewayConfigError(file, paths));
65
+ }
66
+ /** Remove retired AUN gateway overrides without mutating the input. */
67
+ export function stripLegacyAunGatewayConfig(value, file, warn) {
68
+ if (!value || typeof value !== 'object' || Array.isArray(value))
69
+ return value;
70
+ const record = value;
71
+ const next = { ...record };
72
+ let changed = false;
73
+ if (record.aun && typeof record.aun === 'object' && !Array.isArray(record.aun)
74
+ && Object.prototype.hasOwnProperty.call(record.aun, 'gatewayUrl')) {
75
+ const { gatewayUrl: _legacyGatewayUrl, ...aun } = record.aun;
76
+ next.aun = aun;
77
+ changed = true;
78
+ }
79
+ if (Array.isArray(record.channels)) {
80
+ const channels = record.channels.map(channel => {
81
+ if (!channel || typeof channel !== 'object'
82
+ || channel.type !== 'aun'
83
+ || !Object.prototype.hasOwnProperty.call(channel, 'gatewayUrl'))
84
+ return channel;
85
+ const { gatewayUrl: _legacyGatewayUrl, ...rest } = channel;
86
+ changed = true;
87
+ return rest;
88
+ });
89
+ if (changed)
90
+ next.channels = channels;
91
+ }
92
+ if (!changed)
93
+ return value;
94
+ if (!warnedLegacyGatewayFiles.has(file)) {
95
+ warnedLegacyGatewayFiles.add(file);
96
+ warn(`[config] ${file}: AUN gatewayUrl is no longer supported and will be ignored; fastaun owns gateway discovery`);
97
+ }
98
+ return next;
99
+ }
32
100
  export function shouldFailFastForMissingOwners(env = process.env) {
33
101
  const value = (env.EVOLCORE_REQUIRE_OWNERS || '').trim().toLowerCase();
34
102
  return value === '1' || value === 'true' || value === 'yes';
@@ -114,6 +182,7 @@ export function assertRoleConfigV4Ready() {
114
182
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
115
183
  return;
116
184
  }
185
+ value = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
117
186
  const actualVersion = value && typeof value === 'object'
118
187
  ? value.$schema_version
119
188
  : undefined;
@@ -207,7 +276,10 @@ export function assertRoleConfigV5Ready() {
207
276
  if (!fs.existsSync(agentFile))
208
277
  continue;
209
278
  const agent = readJson(agentFile);
210
- const agentVersion = agent?.$schema_version;
279
+ const sanitizedAgent = agent
280
+ ? stripLegacyAunGatewayConfig(agent, agentFile, message => configWarn(message))
281
+ : agent;
282
+ const agentVersion = sanitizedAgent?.$schema_version;
211
283
  let agentSchema = null;
212
284
  if (typeof agentVersion === 'number' && agentVersion >= 5) {
213
285
  try {
@@ -215,8 +287,8 @@ export function assertRoleConfigV5Ready() {
215
287
  }
216
288
  catch { }
217
289
  }
218
- if (!agent || !agentSchema || !agentSchema.validate(agent)) {
219
- if (agent)
290
+ if (!sanitizedAgent || !agentSchema || !agentSchema.validate(sanitizedAgent)) {
291
+ if (sanitizedAgent)
220
292
  issues.push({
221
293
  file: agentFile,
222
294
  detail: agentSchema
@@ -266,6 +338,7 @@ export function assertContactBookV2Ready() {
266
338
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
267
339
  return;
268
340
  }
341
+ value = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
269
342
  const candidates = Array.isArray(schemas) ? schemas : [schemas];
270
343
  if (!candidates.some(schema => schema.validate(value))) {
271
344
  const detail = (candidates.at(-1)?.validate.errors ?? [])
@@ -519,6 +592,7 @@ export function read(target, sel, opts = {}) {
519
592
  }
520
593
  if (raw === null)
521
594
  return null;
595
+ raw = stripLegacyAunGatewayConfig(raw, file, message => configWarn(message));
522
596
  // schema 版本迁移(read 时若 $schema_version < current)
523
597
  const migrated = migrateIfNeeded(target, raw, file);
524
598
  const normalized = target === ConfigTarget.Agent
@@ -702,9 +776,12 @@ export function write(target, value, sel, opts = {}) {
702
776
  const schema = target === ConfigTarget.Contact && requestedVersion === 2
703
777
  ? loadSchema('contact-book', 2)
704
778
  : loadSchema(TARGET_SCHEMA[target]);
779
+ const retiredGatewayPaths = legacyAunGatewayConfigPaths(value);
780
+ if (retiredGatewayPaths.length > 0) {
781
+ throw new ConfigError('AUN_GATEWAY_URL_REMOVED', legacyAunGatewayConfigError(file, retiredGatewayPaths), { paths: retiredGatewayPaths });
782
+ }
705
783
  const withVer = ensureSchemaVersion(value, schema.version);
706
- const migrated = withVer;
707
- const canonicalized = normalizeBaseagentEffortCompat(migrated);
784
+ const canonicalized = normalizeBaseagentEffortCompat(withVer);
708
785
  // Agent config 写入规范化(aid 校验、projects 字段清理)
709
786
  const normalized = target === ConfigTarget.Agent
710
787
  ? normalizeAgentConfigForWrite(canonicalized)
@@ -1014,10 +1091,14 @@ export function validateConfig(target, value) {
1014
1091
  ];
1015
1092
  }
1016
1093
  export function validateConfigFile(target, sel) {
1017
- const value = atomicReadJson(targetPath(target, sel));
1094
+ const file = targetPath(target, sel);
1095
+ const value = atomicReadJson(file);
1018
1096
  if (value === null)
1019
1097
  return { exists: false, errors: [] };
1020
- return { exists: true, errors: validateConfig(target, value) };
1098
+ // Persisted pre-removal files remain readable: validate the same sanitized
1099
+ // view used by runtime reads, while new writes are still rejected by write().
1100
+ const sanitized = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
1101
+ return { exists: true, errors: validateConfig(target, sanitized) };
1021
1102
  }
1022
1103
  function fileCacheAvailable() {
1023
1104
  try {
@@ -1108,6 +1189,7 @@ function stripRelationRoleData(config) {
1108
1189
  */
1109
1190
  export function resolveEffective(sel, opts = {}) {
1110
1191
  const config = resolveAgentConfig(sel, opts);
1192
+ const observableDefault = loadSchema('agent-config').fields.get('observable')?.default;
1111
1193
  const effective = {
1112
1194
  $schema_version: config.$schema_version ?? currentVersion('agent-config'),
1113
1195
  aid: config.aid ?? sel.self ?? '',
@@ -1121,7 +1203,9 @@ export function resolveEffective(sel, opts = {}) {
1121
1203
  projects: config.projects,
1122
1204
  capabilities: config.capabilities,
1123
1205
  readonlySourceDiagnostics: config.readonlySourceDiagnostics,
1124
- observable: config.observable,
1206
+ // Schema defaults are not applied by AJV during reads; apply the current
1207
+ // factory default here so omitted observable fields are enabled at runtime.
1208
+ observable: config.observable ?? (typeof observableDefault === 'boolean' ? observableDefault : true),
1125
1209
  extra_backup: config.extra_backup,
1126
1210
  // Runtime configuration parameters
1127
1211
  active_baseagent: config.active_baseagent,
@@ -287,8 +287,7 @@ function relPath(absolute) {
287
287
  }
288
288
  /**
289
289
  * 各存储层都无值时的兜底:若 schema 为该字段声明了 `default`,展示出厂默认并标记
290
- * schemaDefault=true(仅影响 `ec config get` 的展示;运行时 resolveEffective 不受影响,
291
- * 未设值仍为 undefined,以保留协议层回退等既有语义)。
290
+ * schemaDefault=true。运行时 resolveEffective 也会应用 Agent schema observable 默认值。
292
291
  */
293
292
  function getWithSchemaDefault(op, absent, note) {
294
293
  const nt = note ? { note } : {};
@@ -20,7 +20,7 @@ import { buildModelRequestHeaders } from '../agents/request-identity.js';
20
20
  import { resolvePriceRow } from '../stats/billing.js';
21
21
  import { ipcQuery } from '../ipc.js';
22
22
  import { logger } from '../utils/logger.js';
23
- import { ConfigTarget, read as readConfig } from './config-manager.js';
23
+ import { ConfigTarget, read as readConfig, stripLegacyAunGatewayConfig } from './config-manager.js';
24
24
  /** 已知 baseagent 类型(网关可管理范围)。 */
25
25
  const GATEWAY_TYPES = ['claude', 'codex', 'gemini', 'ecagent'];
26
26
  const DISPLAY_NAMES = {
@@ -84,8 +84,9 @@ function readDefaultsRaw() {
84
84
  // loadDefaults 会展开 $ENV,掩码逻辑需看原始引用,故直接读盘
85
85
  try {
86
86
  const p = path.join(resolvePaths().agentsDir, 'defaults.json');
87
- if (fs.existsSync(p))
88
- return JSON.parse(fs.readFileSync(p, 'utf-8'));
87
+ if (fs.existsSync(p)) {
88
+ return stripLegacyAunGatewayConfig(JSON.parse(fs.readFileSync(p, 'utf-8')), p, message => logger.warn(message));
89
+ }
89
90
  }
90
91
  catch (e) {
91
92
  logger.warn(`[gateway] read defaults.json failed: ${e}`);
@@ -95,8 +96,9 @@ function readDefaultsRaw() {
95
96
  function readAgentRaw(aid) {
96
97
  try {
97
98
  const p = path.join(resolvePaths().agentsDir, aid, 'config.json');
98
- if (fs.existsSync(p))
99
- return JSON.parse(fs.readFileSync(p, 'utf-8'));
99
+ if (fs.existsSync(p)) {
100
+ return stripLegacyAunGatewayConfig(JSON.parse(fs.readFileSync(p, 'utf-8')), p, message => logger.warn(message));
101
+ }
100
102
  }
101
103
  catch (e) {
102
104
  logger.warn(`[gateway] read agents/${aid}/config.json failed: ${e}`);
@@ -780,7 +782,7 @@ export async function gatewaySyncEnv(args) {
780
782
  }
781
783
  if (hasChanges) {
782
784
  const defaultsPath = path.join(resolvePaths().agentsDir, 'defaults.json');
783
- fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2), 'utf-8');
785
+ saveDefaultsSafe(stripLegacyAunGatewayConfig(defaults, defaultsPath, message => logger.warn(message)));
784
786
  synced.push('全局配置 (defaults.json)');
785
787
  }
786
788
  }
@@ -826,7 +828,7 @@ export async function gatewaySyncEnv(args) {
826
828
  }
827
829
  if (hasChanges) {
828
830
  const agentConfigPath = path.join(resolvePaths().agentsDir, aid, 'config.json');
829
- fs.writeFileSync(agentConfigPath, JSON.stringify(agentRaw, null, 2), 'utf-8');
831
+ saveAgent(stripLegacyAunGatewayConfig(agentRaw, agentConfigPath, message => logger.warn(message)));
830
832
  synced.push(`${aid} (config.json)`);
831
833
  }
832
834
  }
@@ -1,13 +1,24 @@
1
1
  import { CONFIG_SCHEMA_VERSION } from '../types.js';
2
2
  const VALID_LIFECYCLES = new Set(['created', 'bootstrapping', 'active']);
3
- export function normalizeAgentLifecycle(config) {
3
+ export function isAgentLifecycle(value) {
4
+ return typeof value === 'string' && VALID_LIFECYCLES.has(value);
5
+ }
6
+ /** Resolve the effective lifecycle. Null means an explicit invalid value. */
7
+ export function resolveAgentLifecycle(config) {
4
8
  const current = config.lifecycle;
5
- if (current && VALID_LIFECYCLES.has(current)) {
9
+ if (isAgentLifecycle(current))
10
+ return current;
11
+ if (current !== undefined)
12
+ return null;
13
+ return config.initialized === false ? 'created' : 'active';
14
+ }
15
+ export function normalizeAgentLifecycle(config) {
16
+ const lifecycle = resolveAgentLifecycle(config);
17
+ // Preserve explicit invalid values so validation and runtime gates can fail
18
+ // closed. Only an omitted lifecycle receives the compatibility default.
19
+ if (lifecycle === null || config.lifecycle === lifecycle) {
6
20
  return config;
7
21
  }
8
- const lifecycle = config.initialized === true ? 'active'
9
- : config.initialized === false ? 'created'
10
- : 'active';
11
22
  return { ...config, lifecycle };
12
23
  }
13
24
  export function withLifecycleForWrite(config, lifecycle) {
@@ -23,14 +23,15 @@ import { checkAgentDir, isValidAid } from './aun/aid/validation.js';
23
23
  import { normalizeAidDomain } from './aun/aid/domain.js';
24
24
  import { isValidChannelName } from './core/channel-loader.js';
25
25
  import { CONFIG_SCHEMA_VERSION } from './types.js';
26
- import { ConfigTarget, read as cfgRead, write as cfgWrite } from './config/config-manager.js';
26
+ import { ConfigTarget, assertNoLegacyAunGatewayConfig, read as cfgRead, stripLegacyAunGatewayConfig, write as cfgWrite, } from './config/config-manager.js';
27
27
  import { expandVars, buildEnvResolver } from './config/merge.js';
28
+ import { isAgentLifecycle } from './config/lifecycle.js';
28
29
  import { logger } from './utils/logger.js';
29
30
  import { parseStableSemver } from './utils/stable-semver.js';
30
31
  /** 读 {root}/daemon.json。文件不存在返回 {},不报错。 */
31
32
  export function loadDaemonConfig() {
32
33
  const raw = atomicReadJson(resolvePaths().daemonConfig);
33
- return validateDaemonConfig(raw ?? {}, false);
34
+ return validateDaemonConfig(stripLegacyAunGatewayConfig(raw ?? {}, resolvePaths().daemonConfig, message => logger.warn(message)), false);
34
35
  }
35
36
  let eckSnapshotsConfigCache;
36
37
  /** Parse the process-level snapshot gate once for the current daemon lifecycle. */
@@ -52,13 +53,14 @@ export function isEckSnapshotsEnabled() {
52
53
  /** 原子写入 {root}/daemon.json。调用方负责传完整对象(含要保留的字段)。 */
53
54
  export function saveDaemonConfig(value) {
54
55
  const configPath = resolvePaths().daemonConfig;
56
+ assertNoLegacyAunGatewayConfig(value, configPath);
55
57
  const current = atomicReadJson(configPath);
56
58
  // A legacy default is irrelevant while an existing control AID is retained.
57
59
  // Validate the field when it is created or changed, but do not make unrelated
58
60
  // daemon.json updates fail because of a persisted, unused legacy value.
59
61
  const validateAidDomain = current === null
60
62
  || current.aun?.defaultAidDomain !== value.aun?.defaultAidDomain;
61
- atomicWriteJson(configPath, validateDaemonConfig(value, validateAidDomain));
63
+ atomicWriteJson(configPath, validateDaemonConfig(stripLegacyAunGatewayConfig(value, configPath, message => logger.warn(message)), validateAidDomain));
62
64
  }
63
65
  function validateDaemonConfig(value, validateAidDomain) {
64
66
  const minEvolVersion = value.aun?.minEvolVersion;
@@ -105,11 +107,12 @@ export function loadDefaults() {
105
107
  if (typeof raw.$schema_version !== 'number') {
106
108
  logger.warn(`[config] ${p}: missing $schema_version, treating as ${CONFIG_SCHEMA_VERSION}`);
107
109
  }
108
- return expandEnvRefs(raw);
110
+ return expandEnvRefs(stripLegacyAunGatewayConfig(raw, p, message => logger.warn(message)));
109
111
  }
110
112
  export function saveDefaults(value) {
113
+ assertNoLegacyAunGatewayConfig(value, resolvePaths().defaultsConfig);
111
114
  backupDefaults(resolvePaths().defaultsConfig);
112
- atomicWriteJson(resolvePaths().defaultsConfig, value);
115
+ atomicWriteJson(resolvePaths().defaultsConfig, stripLegacyAunGatewayConfig(value, resolvePaths().defaultsConfig, message => logger.warn(message)));
113
116
  }
114
117
  /**
115
118
  * 备份 defaults.json 为 defaults_YYYYMMDDhhmmss.json。文件不存在时为 no-op。
@@ -141,6 +144,7 @@ function backupDefaults(filePath) {
141
144
  */
142
145
  export function saveDefaultsSafe(patch) {
143
146
  const p = resolvePaths().defaultsConfig;
147
+ assertNoLegacyAunGatewayConfig(patch, p);
144
148
  let existing = null;
145
149
  try {
146
150
  existing = atomicReadJson(p);
@@ -153,7 +157,7 @@ export function saveDefaultsSafe(patch) {
153
157
  const merged = existing
154
158
  ? deepMergeObject(existing, patch)
155
159
  : { $schema_version: CONFIG_SCHEMA_VERSION, ...patch };
156
- atomicWriteJson(p, merged);
160
+ atomicWriteJson(p, stripLegacyAunGatewayConfig(merged, p, message => logger.warn(message)));
157
161
  }
158
162
  /** 递归对象合并:overlay 覆盖 base;标量与数组按 overlay 替换;plain object 递归。
159
163
  * saveDefaultsSafe 内部用(保留现有"补丁式写 defaults"语义,与覆盖链合并无关)。 */
@@ -300,6 +304,9 @@ export function validateAgentConfig(cfg) {
300
304
  const errs = [];
301
305
  if (!cfg.aid || !isValidAid(cfg.aid))
302
306
  errs.push(`invalid aid: ${cfg.aid}`);
307
+ if (cfg.lifecycle !== undefined && !isAgentLifecycle(cfg.lifecycle)) {
308
+ errs.push(`invalid lifecycle: ${String(cfg.lifecycle)}`);
309
+ }
303
310
  if (!Array.isArray(cfg.channels)) {
304
311
  errs.push('channels must be an array');
305
312
  return errs;
@@ -37,7 +37,10 @@ export function auditToolPreflightDenial(input) {
37
37
  toolName: input.toolName,
38
38
  policyCode: input.policyCode,
39
39
  protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
40
- matchedPath: input.matchedPath ?? extractAuditPath(input.summary),
40
+ // Path provenance must come from the policy parser. Do not infer it from
41
+ // localized summaries: words such as `daemon.json` or `.lock` may only be
42
+ // regex/string literals in an otherwise harmless diagnostic command.
43
+ matchedPath: input.matchedPath,
41
44
  reason: input.reason,
42
45
  argsSummary: input.summary ? { summary: input.summary } : undefined,
43
46
  sessionId: input.sessionId ?? 'unknown',
@@ -341,7 +344,7 @@ function protectionClassForPolicy(policyCode) {
341
344
  return 'L';
342
345
  return undefined;
343
346
  }
344
- function extractAuditPath(summary) {
347
+ function extractAuditPath_UNUSED(summary) {
345
348
  if (!summary)
346
349
  return undefined;
347
350
  const match = summary.match(/(?:^|\s)(\/[^\s'"`;]+|[A-Za-z]:[\\/][^\s'"`;]+|(?:src|ecagent|ecweb|scripts|tests?)\/[^\s'"`;]+)/);
@@ -70,11 +70,11 @@ export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId) {
70
70
  return false;
71
71
  if (entry.context?.sessionId === sessionId)
72
72
  return true;
73
- return outbox.replace(aid, {
73
+ return outbox.replaceIfRouteMatches(aid, entry, {
74
74
  ...entry,
75
75
  context: {
76
76
  ...(entry.context ?? {}),
77
77
  sessionId,
78
78
  },
79
- });
79
+ }) === 'replaced';
80
80
  }