evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. 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 {
@@ -178,14 +174,35 @@ function sourceBuildNeedsRefresh() {
178
174
  const sourceMtime = Math.max(latestSourceMtime(path.join(packageRoot, 'src')), latestSourceMtime(path.join(packageRoot, 'ecagent', 'src')));
179
175
  return sourceMtime > fs.statSync(daemonEntry).mtimeMs;
180
176
  }
181
- function requireFreshSourceBuild() {
177
+ function runSourceBuild() {
178
+ return new Promise(resolve => {
179
+ console.log('🔧 源码比 daemon 编译产物更新,自动重新构建...');
180
+ const child = spawn(platform.isWindows ? 'npm.cmd' : 'npm', ['run', 'build'], {
181
+ cwd: getPackageRoot(),
182
+ stdio: 'inherit',
183
+ shell: platform.isWindows,
184
+ });
185
+ child.on('error', err => {
186
+ console.error(`❌ 无法启动构建进程: ${err.message}`);
187
+ resolve(false);
188
+ });
189
+ child.on('exit', code => {
190
+ if (code === 0) {
191
+ console.log('✅ 构建完成');
192
+ resolve(true);
193
+ }
194
+ else {
195
+ console.error(`❌ 自动构建失败 (exit ${code}),服务未启动。`);
196
+ console.error(' 请手动执行: npm run build 排查错误');
197
+ resolve(false);
198
+ }
199
+ });
200
+ });
201
+ }
202
+ async function requireFreshSourceBuild() {
182
203
  if (!sourceBuildNeedsRefresh())
183
204
  return true;
184
- console.error('❌ 源码比 daemon 编译产物更新,服务未启动。');
185
- console.error(' 请先执行: ec stop && npm run build');
186
- console.error(' 然后执行: ec start');
187
- process.exitCode = 2;
188
- return false;
205
+ return await runSourceBuild();
189
206
  }
190
207
  function requireCompletedDataMigration(root) {
191
208
  const requirement = inspectDataMigrationRequirement(root);
@@ -310,7 +327,7 @@ export async function cmdStart(opts = {}) {
310
327
  return;
311
328
  }
312
329
  ensureDataDirs();
313
- if (!requireFreshSourceBuild())
330
+ if (!await requireFreshSourceBuild())
314
331
  return;
315
332
  // 旧配置自动迁移(daemon.json → 新结构)
316
333
  const { autoMigrateIfNeeded } = await import('../config-store.js');
@@ -437,8 +454,6 @@ export async function cmdStart(opts = {}) {
437
454
  console.log(` Rotated: stdout.log -> ${path.basename(stdoutRotation.rotatedPath)}`);
438
455
  }
439
456
  cleanEnv();
440
- // 在启动前确保 serviceProxy 配置完整(如果 ecweb 启用但 serviceProxy 未配置)
441
- ensureServiceProxyConfigBeforeStart(p);
442
457
  // 删除旧的 ready signal
443
458
  try {
444
459
  fs.unlinkSync(p.readySignal);
@@ -567,7 +582,7 @@ export async function cmdStart(opts = {}) {
567
582
  }
568
583
  console.log('');
569
584
  // 代码统计仅在开发环境显示(EVOLCORE_HOME 指向包目录)
570
- if (resolveRoot() === getPackageRoot()) {
585
+ if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
571
586
  printCodeStats(getPackageRoot(), p.logs);
572
587
  }
573
588
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
@@ -687,7 +702,7 @@ export async function cmdRestart(opts = {}) {
687
702
  if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
688
703
  return;
689
704
  }
690
- if (!requireFreshSourceBuild())
705
+ if (!await requireFreshSourceBuild())
691
706
  return;
692
707
  // 版本检查与自动升级
693
708
  console.log('📦 Checking for updates...');
@@ -808,7 +823,7 @@ export async function cmdRestart(opts = {}) {
808
823
  console.log('🔄 Restart monitor started, waiting for service to come back online...');
809
824
  await sleep(2000);
810
825
  // 代码统计(开发环境)
811
- if (resolveRoot() === getPackageRoot()) {
826
+ if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
812
827
  console.log('');
813
828
  printCodeStats(getPackageRoot(), resolvePaths().logs);
814
829
  }
@@ -975,7 +990,17 @@ export async function cmdStatus() {
975
990
  const p = resolvePaths();
976
991
  const status = scanInstances();
977
992
  const aliveMains = status.mains.filter(m => m.alive);
978
- const ping = await probeDaemon(p.socket);
993
+ // Start all read-only daemon queries together. On a busy Windows host each
994
+ // query can otherwise wait behind the same IPC/event-loop delay and make the
995
+ // total `ec status` time the sum of several individual timeouts.
996
+ const pingPromise = probeDaemon(p.socket).catch(() => null);
997
+ const statusPromise = ipcQuery(p.socket, { type: 'status' }).catch(() => null);
998
+ const aidsPromise = ipcQuery(p.socket, { type: 'aun-aids' }).catch(() => null);
999
+ const agentsPromise = ipcQuery(p.socket, { type: 'evolagent.list' }).catch(() => null);
1000
+ // Only the ping is needed to decide whether to show the live-daemon
1001
+ // sections. The remaining responses continue in parallel while local
1002
+ // process/session information is rendered.
1003
+ const ping = await pingPromise;
979
1004
  const pid = ping?.pid ?? (aliveMains.length > 0 ? aliveMains[0].record.pid : null);
980
1005
  const processVisible = !!pid && aliveMains.some(entry => entry.record.pid === pid);
981
1006
  if (aliveMains.length > 1) {
@@ -1131,42 +1156,47 @@ export async function cmdStatus() {
1131
1156
  }
1132
1157
  // Channel status. A running daemon is authoritative and avoids reading
1133
1158
  // protected config files from the CLI's potentially sandboxed process.
1159
+ let agentsResponse = null;
1134
1160
  if (pid) {
1135
1161
  console.log('');
1136
- const status = await ipcQuery(p.socket, { type: 'status' });
1137
- if (status) {
1162
+ const [statusResponse, aidsResponse, agentResponse] = await Promise.all([
1163
+ statusPromise,
1164
+ aidsPromise,
1165
+ agentsPromise,
1166
+ ]);
1167
+ agentsResponse = agentResponse;
1168
+ if (statusResponse) {
1138
1169
  // 🔑 AUN AIDs 表格(详细 AUN 实例状态)
1139
1170
  try {
1140
- const aidsResp = await ipcQuery(p.socket, { type: 'aun-aids' });
1141
- if (aidsResp?.ok && aidsResp.aids?.length > 0) {
1171
+ if (aidsResponse?.ok && aidsResponse.aids?.length > 0) {
1142
1172
  console.log('🔑 AUN AIDs:');
1143
- renderAunAidsTable(aidsResp.aids);
1173
+ renderAunAidsTable(aidsResponse.aids);
1144
1174
  }
1145
1175
  }
1146
1176
  catch { /* ignore */ }
1147
1177
  // 控制 AID(daemon 进程身份)状态
1148
- if (status.controlAid) {
1149
- const state = status.controlAid.connected ? 'connected' : 'disconnected';
1150
- console.log(`control: ${status.controlAid.aid} [${state}]`);
1178
+ if (statusResponse.controlAid) {
1179
+ const state = statusResponse.controlAid.connected ? 'connected' : 'disconnected';
1180
+ console.log(`control: ${statusResponse.controlAid.aid} [${state}]`);
1151
1181
  }
1152
1182
  else {
1153
1183
  console.log('control: not configured');
1154
1184
  }
1155
- if (status.stats) {
1185
+ if (statusResponse.stats) {
1156
1186
  console.log('');
1157
1187
  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`);
1188
+ console.log(` Messages: ${statusResponse.stats.received} received, ${statusResponse.stats.completed} completed`);
1189
+ if (statusResponse.stats.errors > 0)
1190
+ console.log(` Errors: ${statusResponse.stats.errors}`);
1191
+ if (statusResponse.stats.completed > 0)
1192
+ console.log(` Avg response: ${(statusResponse.stats.avgResponseMs / 1000).toFixed(1)}s`);
1163
1193
  }
1164
1194
  }
1165
1195
  else {
1166
1196
  // IPC unreachable but PID exists — fall back to local config if readable.
1167
1197
  const config = readStatusDefaultsConfig(p.defaultsConfig);
1168
1198
  if (config) {
1169
- console.log('🔌 Channels (IPC unreachable):');
1199
+ console.log('🔌 Channels (live status unavailable; configuration is stale):');
1170
1200
  showConfigChannels(config);
1171
1201
  }
1172
1202
  }
@@ -1182,7 +1212,7 @@ export async function cmdStatus() {
1182
1212
  // EvolAgent summary (via IPC, only when running)
1183
1213
  if (pid) {
1184
1214
  try {
1185
- const agentResult = await ipcQuery(p.socket, { type: 'evolagent.list' });
1215
+ const agentResult = agentsResponse;
1186
1216
  if (agentResult?.ok && agentResult.agents?.length > 0) {
1187
1217
  const agents = agentResult.agents;
1188
1218
  if (agents.length > 0) {
@@ -1196,6 +1226,22 @@ export async function cmdStatus() {
1196
1226
  }
1197
1227
  }
1198
1228
  }
1229
+ else {
1230
+ const { EvolAgentRegistry } = await import('../core/evolagent-registry.js');
1231
+ const diskRegistry = new EvolAgentRegistry(p.agentsDir);
1232
+ diskRegistry.loadAll();
1233
+ const agents = diskRegistry.list();
1234
+ if (agents.length > 0) {
1235
+ console.log('');
1236
+ console.log('🤖 EvolAgents (live status unavailable; stale configuration):');
1237
+ for (const a of agents) {
1238
+ const diskStatus = a.status === 'disabled' || a.status === 'error' ? a.status : 'unknown';
1239
+ const channels = summarizeChannelFingerprints(a.channels || []);
1240
+ const shortName = a.name.replace(/\.agentid\.pub$/, '');
1241
+ console.log(` ? ${shortName.padEnd(20)} ${`${diskStatus} (stale)`.padEnd(18)} ${channels}`);
1242
+ }
1243
+ }
1244
+ }
1199
1245
  }
1200
1246
  catch {
1201
1247
  // IPC query for agents failed — skip section
@@ -1789,10 +1835,10 @@ async function cmdWatchLogsFlow() {
1789
1835
  console.log(`❌ Log directory not found: ${p.logs}`);
1790
1836
  process.exit(1);
1791
1837
  }
1792
- const files = fs.readdirSync(p.logs).filter(f => f.endsWith('.log'));
1838
+ const files = fs.readdirSync(p.logs).filter(isWatchLogFile);
1793
1839
  const types = deriveLogTypes(files);
1794
1840
  if (types.length === 0) {
1795
- console.log(`⚠ ${p.logs} 下暂无 .log 文件`);
1841
+ console.log(`⚠ ${p.logs} 下暂无可监听日志文件`);
1796
1842
  return;
1797
1843
  }
1798
1844
  const fileCount = new Map();
@@ -1863,7 +1909,7 @@ function cmdWatch(filterTypes) {
1863
1909
  return c;
1864
1910
  };
1865
1911
  const listLogs = () => {
1866
- const all = fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
1912
+ const all = fs.readdirSync(p.logs).filter(isWatchLogFile).map(f => path.join(p.logs, f));
1867
1913
  return filterLogFiles(all, filterTypes);
1868
1914
  };
1869
1915
  const shortName = shortLogNameLocal;
@@ -1885,7 +1931,7 @@ function cmdWatch(filterTypes) {
1885
1931
  const content = formatWatchContent(line);
1886
1932
  return `${timeStr} ${paddedName} ${content}`;
1887
1933
  };
1888
- console.log(`🔭 Watching ${p.logs}/*.log (ESC to stop)\n`);
1934
+ console.log(`🔭 Watching ${p.logs}/* log files (ESC to stop)\n`);
1889
1935
  // 显示当前实例信息和 AID 状态
1890
1936
  const instStatus = scanInstances();
1891
1937
  const aliveMainEntries = instStatus.mains.filter(m => m.alive);
@@ -2508,9 +2554,9 @@ async function waitForExistingEcweb(p, timeoutMs = 3_000) {
2508
2554
  async function printEcwebStatus(p) {
2509
2555
  try {
2510
2556
  const cfg = loadDaemonConfig();
2511
- if (cfg.ecweb?.enabled === false) {
2557
+ if (ecwebService(cfg.services)?.enabled === false) {
2512
2558
  console.log('');
2513
- console.log('🔭 ECWeb: 已禁用 (daemon.json → ecweb.enabled: false)');
2559
+ console.log('🔭 ECWeb: 已禁用 (daemon.json → services[name=ecweb].enabled: false)');
2514
2560
  return;
2515
2561
  }
2516
2562
  }
@@ -2608,7 +2654,7 @@ export function stopCodexAppServerOrphans() {
2608
2654
  /** 若 ecweb 在运行则杀掉并确认 pid/端口都已释放。 */
2609
2655
  async function stopEcwebIfRunning(p) {
2610
2656
  const alive = findAliveEcweb(p);
2611
- const port = loadDaemonConfig().ecweb?.port ?? 42705;
2657
+ const port = ecwebService(loadDaemonConfig().services)?.port ?? DEFAULT_ECWEB_PORT;
2612
2658
  const pids = new Set([
2613
2659
  ...(alive ? [alive.pid] : []),
2614
2660
  ...platform.findProcessByPort(port),
@@ -2634,78 +2680,6 @@ async function waitForPortRelease(port, timeoutMs) {
2634
2680
  }
2635
2681
  return platform.findProcessByPort(port).length === 0;
2636
2682
  }
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
2683
  /**
2710
2684
  * Start the separately installed `ec-web` process for a configured runtime.
2711
2685
  * Kept public for restart-monitor, which launches the daemon directly and
@@ -2713,7 +2687,8 @@ function ensureServiceProxyConfig(cfg, port) {
2713
2687
  */
2714
2688
  export async function startEcwebIfEnabled(p, options = {}) {
2715
2689
  const cfg = loadDaemonConfig();
2716
- if (!cfg.ecweb?.enabled)
2690
+ const service = ecwebService(cfg.services);
2691
+ if (!service?.enabled)
2717
2692
  return false;
2718
2693
  // The daemon is the lifecycle owner. CLI callers normally only need to
2719
2694
  // ensure ECWeb is available, so adopt a healthy process instead of killing
@@ -2750,7 +2725,7 @@ export async function startEcwebIfEnabled(p, options = {}) {
2750
2725
  console.log(`❌ EC Web 旧进程未清理干净,取消启动: ${error instanceof Error ? error.message : String(error)}`);
2751
2726
  return false;
2752
2727
  }
2753
- const port = cfg.ecweb.port ?? 42705;
2728
+ const port = service.port ?? DEFAULT_ECWEB_PORT;
2754
2729
  const args = ['--home', p.root, '--port', String(port)];
2755
2730
  const launch = resolveEcwebLaunchCommand(args, { installedPkg });
2756
2731
  if (!launch) {
@@ -2880,10 +2855,9 @@ async function cmdWatchWeb() {
2880
2855
  }
2881
2856
  // 2. 启动(后台)并同步配置。默认行为是替换旧实例,确保新配置/新版静态资源生效。
2882
2857
  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);
2858
+ const currentService = ecwebService(cfg.services);
2859
+ const port = currentService?.port ?? DEFAULT_ECWEB_PORT;
2860
+ saveDaemonConfig({ ...cfg, services: setEcwebEnabled(cfg.services, true, port) });
2887
2861
  const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2888
2862
  if (!ok)
2889
2863
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
@@ -3011,8 +2985,9 @@ export async function cmdDiagnose() {
3011
2985
  // 6. 检查 EC Web 端口与实例记录是否一致。
3012
2986
  try {
3013
2987
  const cfg = loadDaemonConfig();
3014
- if (cfg.ecweb?.enabled) {
3015
- const port = cfg.ecweb.port ?? 42705;
2988
+ const service = ecwebService(cfg.services);
2989
+ if (service?.enabled) {
2990
+ const port = service.port ?? DEFAULT_ECWEB_PORT;
3016
2991
  const portPids = platform.findProcessByPort(port);
3017
2992
  const recordedPids = new Set(readEcwebInstanceEntries(p).map(entry => entry.record.pid));
3018
2993
  const unregisteredPids = portPids.filter(pid => !recordedPids.has(pid));
@@ -3083,7 +3058,7 @@ export async function cmdWatchCommand(args) {
3083
3058
  if (requested.length > 0) {
3084
3059
  const p2 = resolvePaths();
3085
3060
  const avail = fs.existsSync(p2.logs)
3086
- ? deriveLogTypes(fs.readdirSync(p2.logs).filter(f => f.endsWith('.log')))
3061
+ ? deriveLogTypes(fs.readdirSync(p2.logs).filter(isWatchLogFile))
3087
3062
  : [];
3088
3063
  const invalid = validateLogTypes(requested, avail);
3089
3064
  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,
@@ -6,6 +6,7 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  export const TASK_RUNTIME_CONTEXT_ENV = 'EVOLCORE_TASK_RUNTIME_CONTEXT';
8
8
  export const SESSION_RUNTIME_DIR_ENV = 'EVOLCORE_SESSION_RUNTIME_DIR';
9
+ export const DAEMON_RUNTIME_EPOCH_ENV = 'EVOLCORE_DAEMON_RUNTIME_EPOCH';
9
10
  export { RUNTIME_LOCK_DIR_ENV };
10
11
  const generatedProcessManagedTempDirs = new Set();
11
12
  let processTempCleanupRegistered = false;
@@ -32,6 +33,13 @@ function normalizeTaskRuntimeContext(value) {
32
33
  peerName: optionalString(value.peerName),
33
34
  peerType: optionalString(value.peerType),
34
35
  peerRole: optionalString(value.peerRole),
36
+ permissionMode: optionalString(value.permissionMode),
37
+ processRole: optionalProcessRole(value.processRole),
38
+ dataScope: optionalDataScope(value.dataScope),
39
+ authorizedBy: optionalString(value.authorizedBy),
40
+ executionSource: value.executionSource === 'fullaccess-command' || value.executionSource === 'trigger'
41
+ ? value.executionSource : undefined,
42
+ daemonRuntimeEpoch: optionalString(value.daemonRuntimeEpoch),
35
43
  threadId: optionalString(value.threadId),
36
44
  sessionRuntimeDir: optionalAbsolutePath(value.sessionRuntimeDir),
37
45
  runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
@@ -39,6 +47,13 @@ function normalizeTaskRuntimeContext(value) {
39
47
  causation: normalizeCausation(value.causation),
40
48
  };
41
49
  }
50
+ function optionalProcessRole(value) {
51
+ return value === 'daemon-owner' || value === 'daemon-service' || value === 'fullaccess-run' || value === 'none'
52
+ ? value : undefined;
53
+ }
54
+ function optionalDataScope(value) {
55
+ return value === 'relation' || value === 'agent' || value === 'daemon' ? value : undefined;
56
+ }
42
57
  /**
43
58
  * Codex's shell carrier can leave one JSON-style escape layer in a message
44
59
  * argument (for example, the two characters `\\` and `n`). Decode only that
@@ -198,6 +213,40 @@ export function isManagedSessionRuntimeDir(directory, managedRoot) {
198
213
  return false;
199
214
  return isPrivateDirectory(root) && isPrivateDirectory(resolved);
200
215
  }
216
+ /**
217
+ * Resolve the session-owned temporary root for a managed child process.
218
+ *
219
+ * A direct runner call without task context is intentionally left alone for
220
+ * backwards-compatible tests and low-level integrations. Once the daemon has
221
+ * injected the task-context marker, however, falling back to the daemon's
222
+ * process-wide TMPDIR would break session isolation. Require the injected
223
+ * value to be present, identical to TMPDIR, and still owned by a trusted
224
+ * managed namespace before any runner-side helper writes a file.
225
+ */
226
+ export function getManagedTaskTempDir(runtimeEnv) {
227
+ if (typeof runtimeEnv?.[TASK_RUNTIME_CONTEXT_ENV] !== 'string')
228
+ return undefined;
229
+ const configured = runtimeEnv[SESSION_RUNTIME_DIR_ENV]?.trim();
230
+ const inherited = runtimeEnv.TMPDIR?.trim();
231
+ const context = parseTaskRuntimeContext(runtimeEnv[TASK_RUNTIME_CONTEXT_ENV]);
232
+ const contextDir = context?.sessionRuntimeDir;
233
+ const processRoot = process.env.TMPDIR?.trim();
234
+ const trusted = configured
235
+ && inherited
236
+ && path.isAbsolute(configured)
237
+ && path.isAbsolute(inherited)
238
+ && path.resolve(configured) === path.resolve(inherited)
239
+ // The process root is shared by all sessions. It may contain a session
240
+ // directory, but must never itself become the task's TMPDIR capability.
241
+ && (!processRoot || !path.isAbsolute(processRoot) || path.resolve(configured) !== path.resolve(processRoot))
242
+ && contextDir
243
+ && path.resolve(contextDir) === path.resolve(configured)
244
+ && (isManagedSessionRuntimeDir(configured) || isRunnerOwnedSessionRuntimeDir(configured));
245
+ if (!trusted) {
246
+ throw new Error('managed session TMPDIR is unavailable or untrusted');
247
+ }
248
+ return path.resolve(configured);
249
+ }
201
250
  /**
202
251
  * Create a private runtime directory below the process-provided TMPDIR.
203
252
  * There is deliberately no os.tmpdir() fallback: managed sessions must not
@@ -290,6 +339,7 @@ export function buildTaskRuntimeEnv(ctx) {
290
339
  const clean = normalizeTaskRuntimeContext(ctx);
291
340
  return {
292
341
  EVOLCORE_SESSION_ID: ctx.sessionId ?? '',
342
+ ...(ctx.daemonRuntimeEpoch ? { [DAEMON_RUNTIME_EPOCH_ENV]: ctx.daemonRuntimeEpoch } : {}),
293
343
  [TASK_RUNTIME_CONTEXT_ENV]: JSON.stringify(clean),
294
344
  // Every managed child receives the session directory as TMPDIR. This keeps
295
345
  // ordinary temporary files and runtime-only fallbacks out of shared agent
@@ -32,7 +32,7 @@ export async function cmdTrigger(args) {
32
32
  return;
33
33
  }
34
34
  if (process.env.EVOLCORE_SESSION_ID
35
- && (hasOption(rest, '--file') || hasOption(rest, '--prompt-file') || hasOption(rest, '--script-path'))) {
35
+ && (hasOption(rest, '--file') || hasOption(rest, '--prompt-file') || hasOption(rest, '--script-path') || hasOption(rest, '--script-file'))) {
36
36
  throw new Error('Managed tasks cannot read trigger definitions, prompts, or scripts from local files; use inline prompt content');
37
37
  }
38
38
  try {
@@ -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)
@@ -123,7 +123,7 @@ Create 参数模式支持:
123
123
 
124
124
  Update 仅支持常用字段:
125
125
  --once | --delay | --at | --cron | --every | --event [--tz]
126
- --prompt <文本> | --prompt-file <路径> --name <名称>
126
+ --prompt <文本> | --prompt-file <路径> --script-file <路径> --name <名称>
127
127
  --model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
128
128
  --max-runs <次数> --max-duration <时长>
129
129
  --concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
@@ -399,19 +399,28 @@ function shellQuoteArg(value) {
399
399
  }
400
400
  async function updateTrigger(args, json) {
401
401
  const agentAid = resolveAgentAid(args);
402
- const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file']);
402
+ const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file', '--script-file']);
403
403
  const usesPromptFile = hasOption(args, '--prompt-file');
404
+ const usesScriptFile = hasOption(args, '--script-file');
405
+ const scriptFilePath = usesScriptFile ? requireFlag(args, '--script-file') : undefined;
404
406
  const { parseTriggerUpdateArgv } = await import('../trigger/parser.js');
405
407
  const parsed = parseTriggerUpdateArgv(triggerId, withPromptFromFile(triggerUpdateArgs(args)), { allowLongPrompt: usesPromptFile });
406
408
  if (!parsed.ok)
407
409
  throw new Error(parsed.error);
410
+ const scriptFileRequested = parsed.value.scriptFile !== undefined;
411
+ const { scriptFile: _scriptFile, ...patchFields } = parsed.value;
412
+ const patch = scriptFileRequested ? { ...patchFields, scriptFile: true } : patchFields;
408
413
  const expectedRevision = flagValue(args, '--if-revision');
414
+ const scriptFileBase64 = scriptFilePath
415
+ ? fs.readFileSync(scriptFilePath).toString('base64')
416
+ : undefined;
409
417
  const res = await request({
410
418
  type: 'trigger.update',
411
419
  agentAid,
412
420
  triggerId,
413
- patch: parsed.value,
421
+ patch,
414
422
  ...(usesPromptFile ? { promptFile: true } : {}),
423
+ ...(scriptFileBase64 !== undefined ? { scriptFileBase64 } : {}),
415
424
  ...(expectedRevision ? { expectedRevision } : {}),
416
425
  }, 30_000);
417
426
  if (json)