evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -18,7 +18,7 @@ import { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-
18
18
  import { ecwebVersionRequirementError, fetchEcwebPairCode, resolveEcwebLaunchCommand } from '../utils/ecweb-utils.js';
19
19
  import { isHostChinese } from '../utils/locale.js';
20
20
  import { scanInstances, cleanupInstances, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
21
- import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal } from './watch-logs.js';
21
+ import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal, isWatchLogFile } from './watch-logs.js';
22
22
  import { displaySessionTitle } from '../core/session/session-title.js';
23
23
  import { isNonAgentSessionChannel } from '../core/system-channels.js';
24
24
  import { printCodeStats } from './code-stats.js';
@@ -29,6 +29,7 @@ import { rotateStdoutLog } from '../utils/log-writer.js';
29
29
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
30
30
  import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
31
31
  import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../utils/codex-app-server-registry.js';
32
+ import { DEFAULT_ECWEB_PORT, ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
32
33
  const execFileAsync = promisify(execFile);
33
34
  async function runAutomaticInit(action) {
34
35
  try {
@@ -146,11 +147,6 @@ function buildDaemonEnv(p, opts) {
146
147
  ...(opts.bindBootstrap ? { EVOLCORE_BIND_BOOTSTRAP: '1' } : {}),
147
148
  };
148
149
  }
149
- export function serviceProxyNeedsEcweb(cfg) {
150
- if (!cfg.serviceProxy?.enabled)
151
- return false;
152
- return (cfg.serviceProxy.services ?? []).some(s => s.enabled !== false && s.source === 'ecweb');
153
- }
154
150
  function latestSourceMtime(dir) {
155
151
  let latest = 0;
156
152
  try {
@@ -437,8 +433,6 @@ export async function cmdStart(opts = {}) {
437
433
  console.log(` Rotated: stdout.log -> ${path.basename(stdoutRotation.rotatedPath)}`);
438
434
  }
439
435
  cleanEnv();
440
- // 在启动前确保 serviceProxy 配置完整(如果 ecweb 启用但 serviceProxy 未配置)
441
- ensureServiceProxyConfigBeforeStart(p);
442
436
  // 删除旧的 ready signal
443
437
  try {
444
438
  fs.unlinkSync(p.readySignal);
@@ -567,7 +561,7 @@ export async function cmdStart(opts = {}) {
567
561
  }
568
562
  console.log('');
569
563
  // 代码统计仅在开发环境显示(EVOLCORE_HOME 指向包目录)
570
- if (resolveRoot() === getPackageRoot()) {
564
+ if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
571
565
  printCodeStats(getPackageRoot(), p.logs);
572
566
  }
573
567
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
@@ -808,7 +802,7 @@ export async function cmdRestart(opts = {}) {
808
802
  console.log('🔄 Restart monitor started, waiting for service to come back online...');
809
803
  await sleep(2000);
810
804
  // 代码统计(开发环境)
811
- if (resolveRoot() === getPackageRoot()) {
805
+ if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
812
806
  console.log('');
813
807
  printCodeStats(getPackageRoot(), resolvePaths().logs);
814
808
  }
@@ -975,7 +969,17 @@ export async function cmdStatus() {
975
969
  const p = resolvePaths();
976
970
  const status = scanInstances();
977
971
  const aliveMains = status.mains.filter(m => m.alive);
978
- const ping = await probeDaemon(p.socket);
972
+ // Start all read-only daemon queries together. On a busy Windows host each
973
+ // query can otherwise wait behind the same IPC/event-loop delay and make the
974
+ // total `ec status` time the sum of several individual timeouts.
975
+ const pingPromise = probeDaemon(p.socket).catch(() => null);
976
+ const statusPromise = ipcQuery(p.socket, { type: 'status' }).catch(() => null);
977
+ const aidsPromise = ipcQuery(p.socket, { type: 'aun-aids' }).catch(() => null);
978
+ const agentsPromise = ipcQuery(p.socket, { type: 'evolagent.list' }).catch(() => null);
979
+ // Only the ping is needed to decide whether to show the live-daemon
980
+ // sections. The remaining responses continue in parallel while local
981
+ // process/session information is rendered.
982
+ const ping = await pingPromise;
979
983
  const pid = ping?.pid ?? (aliveMains.length > 0 ? aliveMains[0].record.pid : null);
980
984
  const processVisible = !!pid && aliveMains.some(entry => entry.record.pid === pid);
981
985
  if (aliveMains.length > 1) {
@@ -1131,42 +1135,47 @@ export async function cmdStatus() {
1131
1135
  }
1132
1136
  // Channel status. A running daemon is authoritative and avoids reading
1133
1137
  // protected config files from the CLI's potentially sandboxed process.
1138
+ let agentsResponse = null;
1134
1139
  if (pid) {
1135
1140
  console.log('');
1136
- const status = await ipcQuery(p.socket, { type: 'status' });
1137
- if (status) {
1141
+ const [statusResponse, aidsResponse, agentResponse] = await Promise.all([
1142
+ statusPromise,
1143
+ aidsPromise,
1144
+ agentsPromise,
1145
+ ]);
1146
+ agentsResponse = agentResponse;
1147
+ if (statusResponse) {
1138
1148
  // 🔑 AUN AIDs 表格(详细 AUN 实例状态)
1139
1149
  try {
1140
- const aidsResp = await ipcQuery(p.socket, { type: 'aun-aids' });
1141
- if (aidsResp?.ok && aidsResp.aids?.length > 0) {
1150
+ if (aidsResponse?.ok && aidsResponse.aids?.length > 0) {
1142
1151
  console.log('🔑 AUN AIDs:');
1143
- renderAunAidsTable(aidsResp.aids);
1152
+ renderAunAidsTable(aidsResponse.aids);
1144
1153
  }
1145
1154
  }
1146
1155
  catch { /* ignore */ }
1147
1156
  // 控制 AID(daemon 进程身份)状态
1148
- if (status.controlAid) {
1149
- const state = status.controlAid.connected ? 'connected' : 'disconnected';
1150
- console.log(`control: ${status.controlAid.aid} [${state}]`);
1157
+ if (statusResponse.controlAid) {
1158
+ const state = statusResponse.controlAid.connected ? 'connected' : 'disconnected';
1159
+ console.log(`control: ${statusResponse.controlAid.aid} [${state}]`);
1151
1160
  }
1152
1161
  else {
1153
1162
  console.log('control: not configured');
1154
1163
  }
1155
- if (status.stats) {
1164
+ if (statusResponse.stats) {
1156
1165
  console.log('');
1157
1166
  console.log('📊 Last hour:');
1158
- console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
1159
- if (status.stats.errors > 0)
1160
- console.log(` Errors: ${status.stats.errors}`);
1161
- if (status.stats.completed > 0)
1162
- console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
1167
+ console.log(` Messages: ${statusResponse.stats.received} received, ${statusResponse.stats.completed} completed`);
1168
+ if (statusResponse.stats.errors > 0)
1169
+ console.log(` Errors: ${statusResponse.stats.errors}`);
1170
+ if (statusResponse.stats.completed > 0)
1171
+ console.log(` Avg response: ${(statusResponse.stats.avgResponseMs / 1000).toFixed(1)}s`);
1163
1172
  }
1164
1173
  }
1165
1174
  else {
1166
1175
  // IPC unreachable but PID exists — fall back to local config if readable.
1167
1176
  const config = readStatusDefaultsConfig(p.defaultsConfig);
1168
1177
  if (config) {
1169
- console.log('🔌 Channels (IPC unreachable):');
1178
+ console.log('🔌 Channels (live status unavailable; configuration is stale):');
1170
1179
  showConfigChannels(config);
1171
1180
  }
1172
1181
  }
@@ -1182,7 +1191,7 @@ export async function cmdStatus() {
1182
1191
  // EvolAgent summary (via IPC, only when running)
1183
1192
  if (pid) {
1184
1193
  try {
1185
- const agentResult = await ipcQuery(p.socket, { type: 'evolagent.list' });
1194
+ const agentResult = agentsResponse;
1186
1195
  if (agentResult?.ok && agentResult.agents?.length > 0) {
1187
1196
  const agents = agentResult.agents;
1188
1197
  if (agents.length > 0) {
@@ -1196,6 +1205,22 @@ export async function cmdStatus() {
1196
1205
  }
1197
1206
  }
1198
1207
  }
1208
+ else {
1209
+ const { EvolAgentRegistry } = await import('../core/evolagent-registry.js');
1210
+ const diskRegistry = new EvolAgentRegistry(p.agentsDir);
1211
+ diskRegistry.loadAll();
1212
+ const agents = diskRegistry.list();
1213
+ if (agents.length > 0) {
1214
+ console.log('');
1215
+ console.log('🤖 EvolAgents (live status unavailable; stale configuration):');
1216
+ for (const a of agents) {
1217
+ const diskStatus = a.status === 'disabled' || a.status === 'error' ? a.status : 'unknown';
1218
+ const channels = summarizeChannelFingerprints(a.channels || []);
1219
+ const shortName = a.name.replace(/\.agentid\.pub$/, '');
1220
+ console.log(` ? ${shortName.padEnd(20)} ${`${diskStatus} (stale)`.padEnd(18)} ${channels}`);
1221
+ }
1222
+ }
1223
+ }
1199
1224
  }
1200
1225
  catch {
1201
1226
  // IPC query for agents failed — skip section
@@ -1789,10 +1814,10 @@ async function cmdWatchLogsFlow() {
1789
1814
  console.log(`❌ Log directory not found: ${p.logs}`);
1790
1815
  process.exit(1);
1791
1816
  }
1792
- const files = fs.readdirSync(p.logs).filter(f => f.endsWith('.log'));
1817
+ const files = fs.readdirSync(p.logs).filter(isWatchLogFile);
1793
1818
  const types = deriveLogTypes(files);
1794
1819
  if (types.length === 0) {
1795
- console.log(`⚠ ${p.logs} 下暂无 .log 文件`);
1820
+ console.log(`⚠ ${p.logs} 下暂无可监听日志文件`);
1796
1821
  return;
1797
1822
  }
1798
1823
  const fileCount = new Map();
@@ -1863,7 +1888,7 @@ function cmdWatch(filterTypes) {
1863
1888
  return c;
1864
1889
  };
1865
1890
  const listLogs = () => {
1866
- const all = fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
1891
+ const all = fs.readdirSync(p.logs).filter(isWatchLogFile).map(f => path.join(p.logs, f));
1867
1892
  return filterLogFiles(all, filterTypes);
1868
1893
  };
1869
1894
  const shortName = shortLogNameLocal;
@@ -1885,7 +1910,7 @@ function cmdWatch(filterTypes) {
1885
1910
  const content = formatWatchContent(line);
1886
1911
  return `${timeStr} ${paddedName} ${content}`;
1887
1912
  };
1888
- console.log(`🔭 Watching ${p.logs}/*.log (ESC to stop)\n`);
1913
+ console.log(`🔭 Watching ${p.logs}/* log files (ESC to stop)\n`);
1889
1914
  // 显示当前实例信息和 AID 状态
1890
1915
  const instStatus = scanInstances();
1891
1916
  const aliveMainEntries = instStatus.mains.filter(m => m.alive);
@@ -2508,9 +2533,9 @@ async function waitForExistingEcweb(p, timeoutMs = 3_000) {
2508
2533
  async function printEcwebStatus(p) {
2509
2534
  try {
2510
2535
  const cfg = loadDaemonConfig();
2511
- if (cfg.ecweb?.enabled === false) {
2536
+ if (ecwebService(cfg.services)?.enabled === false) {
2512
2537
  console.log('');
2513
- console.log('🔭 ECWeb: 已禁用 (daemon.json → ecweb.enabled: false)');
2538
+ console.log('🔭 ECWeb: 已禁用 (daemon.json → services[name=ecweb].enabled: false)');
2514
2539
  return;
2515
2540
  }
2516
2541
  }
@@ -2608,7 +2633,7 @@ export function stopCodexAppServerOrphans() {
2608
2633
  /** 若 ecweb 在运行则杀掉并确认 pid/端口都已释放。 */
2609
2634
  async function stopEcwebIfRunning(p) {
2610
2635
  const alive = findAliveEcweb(p);
2611
- const port = loadDaemonConfig().ecweb?.port ?? 42705;
2636
+ const port = ecwebService(loadDaemonConfig().services)?.port ?? DEFAULT_ECWEB_PORT;
2612
2637
  const pids = new Set([
2613
2638
  ...(alive ? [alive.pid] : []),
2614
2639
  ...platform.findProcessByPort(port),
@@ -2634,78 +2659,6 @@ async function waitForPortRelease(port, timeoutMs) {
2634
2659
  }
2635
2660
  return platform.findProcessByPort(port).length === 0;
2636
2661
  }
2637
- /**
2638
- * 后台 detached 启动 ecweb;若已运行则先停再启(确保加载最新代码)。
2639
- * 启动后轮询端口确认 HTTP 服务真正就绪,打印明确的成功/失败结论(而非模糊的「已在后台启动」状态描述)。
2640
- * 返回 true=本次确实启动成功,false=未启用/未安装/启动失败。
2641
- */
2642
- /**
2643
- * 启动前检查并补全 serviceProxy 配置(如果 ecweb 启用但 serviceProxy 未配置)
2644
- */
2645
- function ensureServiceProxyConfigBeforeStart(p) {
2646
- const cfg = loadDaemonConfig();
2647
- // 仅在 ecweb 启用 + 控制 AID 已配置 + serviceProxy 未配置 ecweb 时自动添加
2648
- if (!cfg.ecweb?.enabled || !cfg.aid)
2649
- return;
2650
- const hasEcwebService = (cfg.serviceProxy?.services ?? []).some(s => s.name === 'ecweb' || s.source === 'ecweb');
2651
- if (!hasEcwebService) {
2652
- const updatedCfg = {
2653
- ...cfg,
2654
- serviceProxy: {
2655
- enabled: true,
2656
- ...(cfg.serviceProxy ?? {}),
2657
- services: [
2658
- ...(cfg.serviceProxy?.services ?? []),
2659
- {
2660
- name: 'ecweb',
2661
- source: 'ecweb',
2662
- serviceType: 'http',
2663
- visibility: 'public',
2664
- enabled: true,
2665
- metadata: {
2666
- label: 'EvolCore Dashboard',
2667
- },
2668
- },
2669
- ],
2670
- },
2671
- };
2672
- saveDaemonConfig(updatedCfg);
2673
- console.log(`✓ 自动配置 Service Proxy: https://${cfg.aid}/proxy/ecweb/`);
2674
- }
2675
- }
2676
- /**
2677
- * 确保 serviceProxy 配置包含 ecweb 服务(ECWeb 启动成功后自动配置)
2678
- */
2679
- function ensureServiceProxyConfig(cfg, port) {
2680
- // 仅在控制 AID 已配置时自动添加 serviceProxy
2681
- if (!cfg.aid)
2682
- return;
2683
- const hasEcwebService = (cfg.serviceProxy?.services ?? []).some(s => s.name === 'ecweb' || s.source === 'ecweb');
2684
- if (!hasEcwebService) {
2685
- const updatedCfg = {
2686
- ...cfg,
2687
- serviceProxy: {
2688
- enabled: true,
2689
- ...(cfg.serviceProxy ?? {}),
2690
- services: [
2691
- ...(cfg.serviceProxy?.services ?? []),
2692
- {
2693
- name: 'ecweb',
2694
- source: 'ecweb',
2695
- serviceType: 'http',
2696
- visibility: 'public',
2697
- enabled: true,
2698
- metadata: {
2699
- label: 'EvolCore Dashboard',
2700
- },
2701
- },
2702
- ],
2703
- },
2704
- };
2705
- saveDaemonConfig(updatedCfg);
2706
- console.log(` 自动配置 Service Proxy: https://${cfg.aid}/proxy/ecweb/`);
2707
- }
2708
- }
2709
2662
  /**
2710
2663
  * Start the separately installed `ec-web` process for a configured runtime.
2711
2664
  * Kept public for restart-monitor, which launches the daemon directly and
@@ -2713,7 +2666,8 @@ function ensureServiceProxyConfig(cfg, port) {
2713
2666
  */
2714
2667
  export async function startEcwebIfEnabled(p, options = {}) {
2715
2668
  const cfg = loadDaemonConfig();
2716
- if (!cfg.ecweb?.enabled)
2669
+ const service = ecwebService(cfg.services);
2670
+ if (!service?.enabled)
2717
2671
  return false;
2718
2672
  // The daemon is the lifecycle owner. CLI callers normally only need to
2719
2673
  // ensure ECWeb is available, so adopt a healthy process instead of killing
@@ -2750,7 +2704,7 @@ export async function startEcwebIfEnabled(p, options = {}) {
2750
2704
  console.log(`❌ EC Web 旧进程未清理干净,取消启动: ${error instanceof Error ? error.message : String(error)}`);
2751
2705
  return false;
2752
2706
  }
2753
- const port = cfg.ecweb.port ?? 42705;
2707
+ const port = service.port ?? DEFAULT_ECWEB_PORT;
2754
2708
  const args = ['--home', p.root, '--port', String(port)];
2755
2709
  const launch = resolveEcwebLaunchCommand(args, { installedPkg });
2756
2710
  if (!launch) {
@@ -2880,10 +2834,9 @@ async function cmdWatchWeb() {
2880
2834
  }
2881
2835
  // 2. 启动(后台)并同步配置。默认行为是替换旧实例,确保新配置/新版静态资源生效。
2882
2836
  const cfg = loadDaemonConfig();
2883
- const port = cfg.ecweb?.port ?? 42705;
2884
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true, port } });
2885
- // 自动配置 serviceProxy(如果控制 AID 已配置)
2886
- ensureServiceProxyConfigBeforeStart(p);
2837
+ const currentService = ecwebService(cfg.services);
2838
+ const port = currentService?.port ?? DEFAULT_ECWEB_PORT;
2839
+ saveDaemonConfig({ ...cfg, services: setEcwebEnabled(cfg.services, true, port) });
2887
2840
  const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2888
2841
  if (!ok)
2889
2842
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
@@ -3011,8 +2964,9 @@ export async function cmdDiagnose() {
3011
2964
  // 6. 检查 EC Web 端口与实例记录是否一致。
3012
2965
  try {
3013
2966
  const cfg = loadDaemonConfig();
3014
- if (cfg.ecweb?.enabled) {
3015
- const port = cfg.ecweb.port ?? 42705;
2967
+ const service = ecwebService(cfg.services);
2968
+ if (service?.enabled) {
2969
+ const port = service.port ?? DEFAULT_ECWEB_PORT;
3016
2970
  const portPids = platform.findProcessByPort(port);
3017
2971
  const recordedPids = new Set(readEcwebInstanceEntries(p).map(entry => entry.record.pid));
3018
2972
  const unregisteredPids = portPids.filter(pid => !recordedPids.has(pid));
@@ -3083,7 +3037,7 @@ export async function cmdWatchCommand(args) {
3083
3037
  if (requested.length > 0) {
3084
3038
  const p2 = resolvePaths();
3085
3039
  const avail = fs.existsSync(p2.logs)
3086
- ? deriveLogTypes(fs.readdirSync(p2.logs).filter(f => f.endsWith('.log')))
3040
+ ? deriveLogTypes(fs.readdirSync(p2.logs).filter(isWatchLogFile))
3087
3041
  : [];
3088
3042
  const invalid = validateLogTypes(requested, avail);
3089
3043
  if (invalid.length > 0) {
package/dist/cli/init.js CHANGED
@@ -6,6 +6,7 @@ import { resolvePaths, ensureDataDirs } from '../paths.js';
6
6
  import { commandExists } from '../utils/cross-platform.js';
7
7
  import { scanInstances } from '../utils/instance-registry.js';
8
8
  import { saveDefaultsSafe, loadAllAgents, loadDaemonConfig, saveDaemonConfig } from '../config-store.js';
9
+ import { ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
9
10
  import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
10
11
  import { getCodexAppServerAvailability } from '../agents/codex-runner.js';
11
12
  import { DEFAULT_ECAGENT_BASE_URL, resolveEcagentConfig } from '../agents/baseagent.js';
@@ -421,6 +422,12 @@ export async function cmdInit(options) {
421
422
  const lower = normalized.toLowerCase();
422
423
  return !lower.includes('your-') && !lower.includes('placeholder');
423
424
  };
425
+ const configuredBaseUrl = [
426
+ configured?.baseUrl,
427
+ process.env.ECAGENT_BASE_URL,
428
+ DEFAULT_ECAGENT_BASE_URL,
429
+ ].find(usable) || DEFAULT_ECAGENT_BASE_URL;
430
+ const baseUrl = (await ask(rl, `ecagent Base URL [${configuredBaseUrl}]: `)).trim() || configuredBaseUrl;
424
431
  const configuredApiKey = usable(configured?.apiKey) ? configured.apiKey.trim() : undefined;
425
432
  const environmentApiKeyName = !configuredApiKey
426
433
  ? (usable(process.env.ECAGENT_API_KEY)
@@ -435,12 +442,6 @@ export async function cmdInit(options) {
435
442
  if (!apiKey)
436
443
  console.log(' API Key 不能为空,请重新输入');
437
444
  }
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
445
  return { apiKey, baseUrl };
445
446
  }
446
447
  try {
@@ -618,25 +619,34 @@ export async function initTail(options = {}) {
618
619
  }
619
620
  async function handleEcwebPrompt(rl) {
620
621
  const daemonConfigForEcweb = loadDaemonConfig();
621
- if (daemonConfigForEcweb.ecweb?.enabled === undefined) {
622
+ if (ecwebService(daemonConfigForEcweb.services)?.enabled === undefined) {
622
623
  const ans = (await ask(rl, '\n是否在 ec start 时自动启动 ECWeb 控制台?[y/N] ')).trim().toLowerCase();
623
624
  if (ans === 'y' || ans === 'yes') {
624
625
  const installResult = await ensureEcwebInstalledForInit();
625
626
  if (installResult.ok) {
626
627
  const cfg = loadDaemonConfig();
627
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true } });
628
+ saveDaemonConfig({
629
+ ...cfg,
630
+ services: setEcwebEnabled(cfg.services, true),
631
+ });
628
632
  console.log(' ✓ 已启用 ECWeb(ec start 将自动在后台启动)');
629
633
  console.log(' 提示:首次访问运行 ec watch web 查看配对码和 URL');
630
634
  }
631
635
  else {
632
636
  const cfg = loadDaemonConfig();
633
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: false } });
637
+ saveDaemonConfig({
638
+ ...cfg,
639
+ services: setEcwebEnabled(cfg.services, false),
640
+ });
634
641
  console.log(' 提示:安装完成后运行 ec start;如需立即打开控制台,运行 ec watch web');
635
642
  }
636
643
  }
637
644
  else {
638
645
  const cfg = loadDaemonConfig();
639
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: false } });
646
+ saveDaemonConfig({
647
+ ...cfg,
648
+ services: setEcwebEnabled(cfg.services, false),
649
+ });
640
650
  console.log(' 已跳过(可日后运行 ec watch web 手动启动,或编辑 daemon.json)');
641
651
  }
642
652
  }
@@ -658,7 +668,7 @@ export async function initTail(options = {}) {
658
668
  const finalCfg = loadDaemonConfig();
659
669
  console.log(` 控制 AID: ${finalCfg.aid || '(未配置)'}`);
660
670
  console.log(` 管理者: ${finalCfg.owners?.join(', ') || '(未配置)'}`);
661
- console.log(` ECWeb 自启动: ${finalCfg.ecweb?.enabled ? '已启用' : '已禁用'}`);
671
+ console.log(` ECWeb 自启动: ${ecwebService(finalCfg.services)?.enabled ? '已启用' : '已禁用'}`);
662
672
  const { agents: finalAgents } = loadAllAgents();
663
673
  if (finalAgents.length === 0) {
664
674
  console.log('\n📌 下一步:创建 agent');
@@ -826,11 +836,11 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
826
836
  const ensureEcweb = dependencies.ensureEcweb ?? (() => ensureEcwebInstalledForInit({ quiet: true }));
827
837
  const installResult = await ensureEcweb();
828
838
  if (!installResult.ok) {
829
- if (existingCfg.ecweb?.enabled === true) {
839
+ if (ecwebService(existingCfg.services)?.enabled === true) {
830
840
  try {
831
841
  saveDaemonConfig({
832
842
  ...existingCfg,
833
- ecweb: { ...(existingCfg.ecweb ?? {}), enabled: false },
843
+ services: setEcwebEnabled(existingCfg.services, false),
834
844
  });
835
845
  }
836
846
  catch (e) {
@@ -866,7 +876,9 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
866
876
  $schema_version: existingCfg.$schema_version ?? 1,
867
877
  aid: controlAid,
868
878
  owners: [ownerAid],
869
- ...(opts.ecweb === true ? { ecweb: { ...(existingCfg.ecweb ?? {}), enabled: true } } : {}),
879
+ ...(opts.ecweb === true
880
+ ? { services: setEcwebEnabled(existingCfg.services, true) }
881
+ : {}),
870
882
  };
871
883
  saveDaemonConfig(next);
872
884
  }
@@ -894,7 +906,7 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
894
906
  controlAid: controlAid,
895
907
  ownerAid,
896
908
  owners: [ownerAid],
897
- ecwebEnabled: opts.ecweb === true || existingCfg.ecweb?.enabled === true,
909
+ ecwebEnabled: opts.ecweb === true || ecwebService(existingCfg.services)?.enabled === true,
898
910
  baseagent: chosenBaseagent,
899
911
  projectsDefaultPath: projectsDefaultPath ?? null,
900
912
  defaultsPath: p.defaultsConfig,
@@ -32,6 +32,11 @@ function normalizeTaskRuntimeContext(value) {
32
32
  peerName: optionalString(value.peerName),
33
33
  peerType: optionalString(value.peerType),
34
34
  peerRole: optionalString(value.peerRole),
35
+ processRole: optionalProcessRole(value.processRole),
36
+ dataScope: optionalDataScope(value.dataScope),
37
+ authorizedBy: optionalString(value.authorizedBy),
38
+ executionSource: value.executionSource === 'fullaccess-command' || value.executionSource === 'trigger'
39
+ ? value.executionSource : undefined,
35
40
  threadId: optionalString(value.threadId),
36
41
  sessionRuntimeDir: optionalAbsolutePath(value.sessionRuntimeDir),
37
42
  runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
@@ -39,6 +44,13 @@ function normalizeTaskRuntimeContext(value) {
39
44
  causation: normalizeCausation(value.causation),
40
45
  };
41
46
  }
47
+ function optionalProcessRole(value) {
48
+ return value === 'daemon-owner' || value === 'daemon-service' || value === 'fullaccess-run' || value === 'none'
49
+ ? value : undefined;
50
+ }
51
+ function optionalDataScope(value) {
52
+ return value === 'relation' || value === 'agent' || value === 'daemon' ? value : undefined;
53
+ }
42
54
  /**
43
55
  * Codex's shell carrier can leave one JSON-style escape layer in a message
44
56
  * argument (for example, the two characters `\\` and `n`). Decode only that
@@ -198,6 +210,40 @@ export function isManagedSessionRuntimeDir(directory, managedRoot) {
198
210
  return false;
199
211
  return isPrivateDirectory(root) && isPrivateDirectory(resolved);
200
212
  }
213
+ /**
214
+ * Resolve the session-owned temporary root for a managed child process.
215
+ *
216
+ * A direct runner call without task context is intentionally left alone for
217
+ * backwards-compatible tests and low-level integrations. Once the daemon has
218
+ * injected the task-context marker, however, falling back to the daemon's
219
+ * process-wide TMPDIR would break session isolation. Require the injected
220
+ * value to be present, identical to TMPDIR, and still owned by a trusted
221
+ * managed namespace before any runner-side helper writes a file.
222
+ */
223
+ export function getManagedTaskTempDir(runtimeEnv) {
224
+ if (typeof runtimeEnv?.[TASK_RUNTIME_CONTEXT_ENV] !== 'string')
225
+ return undefined;
226
+ const configured = runtimeEnv[SESSION_RUNTIME_DIR_ENV]?.trim();
227
+ const inherited = runtimeEnv.TMPDIR?.trim();
228
+ const context = parseTaskRuntimeContext(runtimeEnv[TASK_RUNTIME_CONTEXT_ENV]);
229
+ const contextDir = context?.sessionRuntimeDir;
230
+ const processRoot = process.env.TMPDIR?.trim();
231
+ const trusted = configured
232
+ && inherited
233
+ && path.isAbsolute(configured)
234
+ && path.isAbsolute(inherited)
235
+ && path.resolve(configured) === path.resolve(inherited)
236
+ // The process root is shared by all sessions. It may contain a session
237
+ // directory, but must never itself become the task's TMPDIR capability.
238
+ && (!processRoot || !path.isAbsolute(processRoot) || path.resolve(configured) !== path.resolve(processRoot))
239
+ && contextDir
240
+ && path.resolve(contextDir) === path.resolve(configured)
241
+ && (isManagedSessionRuntimeDir(configured) || isRunnerOwnedSessionRuntimeDir(configured));
242
+ if (!trusted) {
243
+ throw new Error('managed session TMPDIR is unavailable or untrusted');
244
+ }
245
+ return path.resolve(configured);
246
+ }
201
247
  /**
202
248
  * Create a private runtime directory below the process-provided TMPDIR.
203
249
  * There is deliberately no os.tmpdir() fallback: managed sessions must not
@@ -110,7 +110,7 @@ Create 参数模式支持:
110
110
  --baseagent <name> (仅 --exec trigger-session)
111
111
  --model <模型> --effort <low|medium|high|xhigh|max>
112
112
  --max-runs <次数> --max-duration <时长: 30s|15m|2h|1d>
113
- --permission <readonly|auto|request|bypass>(省略则继承当前身份配置)
113
+ --permission <readonly|auto|request|bypass|fullaccess>(省略则继承当前身份配置;fullaccess 仅 daemon-owner 且 feature flag 开启时可用)
114
114
  --tz <时区> (仅 cron)
115
115
  --target-channel <channelKey> --target-channel-id <ID> (AUN 可简写为 aun)
116
116
  --target-chat-type <private|group> (AUN 目标必填;channelId 同时作为群聊 ID)
@@ -1,15 +1,22 @@
1
1
  import path from 'path';
2
+ const WATCH_LOG_FILE = /\.(?:log|jsonl|out)(?:\..+)?$/;
3
+ /** Whether a file is a supported input for the interactive log watcher. */
4
+ export function isWatchLogFile(file) {
5
+ return WATCH_LOG_FILE.test(path.basename(file));
6
+ }
2
7
  /** 去掉轮转后缀(按小时、按日及旧版带连字符日期)。入参可为文件名或绝对路径。 */
3
8
  export function shortLogName(file) {
4
- return path.basename(file, '.log')
9
+ const baseName = path.basename(file);
10
+ const stem = baseName.replace(WATCH_LOG_FILE, '');
11
+ return stem
5
12
  .replace(/-\d{8}(?:-\d{2})?$/, '') // -YYYYMMDD[-HH](按日/小时轮转)
6
13
  .replace(/-\d{4}-\d{2}-\d{2}$/, ''); // -YYYY-MM-DD(按日轮转,如 ts-sdk)
7
14
  }
8
- /** .log 文件名列表推导去重、字母序的类型列表。 */
15
+ /** 从支持的日志文件名列表推导去重、字母序的类型列表。 */
9
16
  export function deriveLogTypes(files) {
10
17
  const set = new Set();
11
18
  for (const f of files) {
12
- if (!f.endsWith('.log'))
19
+ if (!isWatchLogFile(f))
13
20
  continue;
14
21
  set.add(shortLogName(f));
15
22
  }
@@ -32,6 +32,7 @@ export function getBuiltinRolesConfig() {
32
32
  export function getManagementCommandPermissions(role) {
33
33
  return {
34
34
  'contact.read': { allow: true, scopes: ['agent'] },
35
+ 'contact.add': { allow: true, scopes: ['relation'], constraints: { ownPeerOnly: true, privateOnly: true } },
35
36
  'contact.block': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
36
37
  'connect.write': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
37
38
  'connect.access.write': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
@@ -17,6 +17,8 @@ const BEHAVIOR_TOP_FIELDS = new Set([
17
17
  const BASEAGENT_BEHAVIOR_FIELDS = new Set([
18
18
  'model',
19
19
  'effort',
20
+ 'auxiliaryModel',
21
+ 'auxiliaryEffort',
20
22
  'reasoning',
21
23
  'agentProgressSummaries',
22
24
  'excludeDynamicSections',
@@ -25,7 +27,7 @@ const BASEAGENT_BEHAVIOR_FIELDS = new Set([
25
27
  'mode',
26
28
  'useVertex',
27
29
  ]);
28
- const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent', 'hermes']);
30
+ const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent']);
29
31
  const SENSITIVE_TOP_FIELDS = new Set([
30
32
  '$schema_version',
31
33
  'aid',
@@ -75,10 +77,17 @@ export function resolveConfigFieldRule(fieldPath) {
75
77
  const [, baseagent, leaf] = parts;
76
78
  if (!SUPPORTED_BASEAGENTS.has(baseagent))
77
79
  return { class: 'unknown' };
78
- const permissionKey = `baseagents.${baseagent}.${leaf === 'reasoning' ? 'effort' : leaf}`;
79
- if (leaf === 'model' && parts.length === 3)
80
+ const normalizedLeaf = leaf === 'reasoning'
81
+ ? 'effort'
82
+ : leaf === 'auxiliaryModel'
83
+ ? 'model'
84
+ : leaf === 'auxiliaryEffort'
85
+ ? 'effort'
86
+ : leaf;
87
+ const permissionKey = `baseagents.${baseagent}.${normalizedLeaf}`;
88
+ if ((leaf === 'model' || leaf === 'auxiliaryModel') && parts.length === 3)
80
89
  return scalar(permissionKey, 'string');
81
- if ((leaf === 'effort' || leaf === 'reasoning') && parts.length === 3)
90
+ if ((leaf === 'effort' || leaf === 'reasoning' || leaf === 'auxiliaryEffort') && parts.length === 3)
82
91
  return scalar(permissionKey, 'effort');
83
92
  if (baseagent === 'claude' && (leaf === 'agentProgressSummaries' || leaf === 'excludeDynamicSections') && parts.length === 3) {
84
93
  return scalar(permissionKey, 'boolean');
@@ -199,7 +208,9 @@ export function relationFieldWriteOperation(fieldPath) {
199
208
  return 'mentionmode.update';
200
209
  if (/^baseagents\.[^.]+\.model$/.test(field))
201
210
  return 'model.use';
202
- if (/^baseagents\.[^.]+\.(?:effort|reasoning)$/.test(field)) {
211
+ if (/^baseagents\.[^.]+\.auxiliaryModel$/.test(field))
212
+ return 'model.use';
213
+ if (/^baseagents\.[^.]+\.(?:effort|reasoning|auxiliaryEffort)$/.test(field)) {
203
214
  return 'model.effort';
204
215
  }
205
216
  return undefined;