evolcore 0.0.22 → 0.0.23

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 (75) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +24 -14
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/install-codex-managed-hooks.mjs +373 -95
  6. package/dist/agents/baseagent-availability.js +48 -0
  7. package/dist/agents/baseagent.js +1 -1
  8. package/dist/agents/claude-runner.js +30 -226
  9. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
  10. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  11. package/dist/agents/codex/catalog.js +70 -0
  12. package/dist/agents/codex/diagnostics.js +154 -0
  13. package/dist/agents/codex-runner.js +407 -139
  14. package/dist/agents/ecagent-runner.js +10 -31
  15. package/dist/agents/gemini-runner.js +2 -6
  16. package/dist/aun/aid/store.js +2 -0
  17. package/dist/aun/menu-client.js +272 -0
  18. package/dist/aun/msg/group.js +65 -4
  19. package/dist/aun/msg/index.js +1 -1
  20. package/dist/aun/msg/managed-operation.js +51 -4
  21. package/dist/aun/rpc/client.js +8 -1
  22. package/dist/channels/aun.js +31 -1
  23. package/dist/channels/wechat.js +81 -10
  24. package/dist/cli/aun-commands.js +48 -4
  25. package/dist/cli/command-log.js +21 -2
  26. package/dist/cli/daemon-commands.js +142 -45
  27. package/dist/cli/index.js +20 -0
  28. package/dist/cli/init.js +2 -10
  29. package/dist/cli/menu-client.js +95 -0
  30. package/dist/cli/model.js +23 -8
  31. package/dist/cli/task-context.js +12 -2
  32. package/dist/cli/trigger-command.js +9 -1
  33. package/dist/config/schema-migration.js +14 -2
  34. package/dist/core/auth/agent-delegation.js +10 -7
  35. package/dist/core/auth/authorization-audit.js +4 -0
  36. package/dist/core/auth/operation-catalog.js +4 -0
  37. package/dist/core/auth/remote-access-store.js +135 -0
  38. package/dist/core/command/command-handler.js +41 -5
  39. package/dist/core/command/daemon-diagnostics.js +448 -0
  40. package/dist/core/command/menu-catalog.js +3 -0
  41. package/dist/core/command/menu-handler.js +141 -33
  42. package/dist/core/command/menu-protocol.js +119 -0
  43. package/dist/core/command/slash-gate.js +3 -3
  44. package/dist/core/command/slash-handler.js +42 -22
  45. package/dist/core/message/message-queue.js +20 -15
  46. package/dist/core/message/response-engine.js +74 -73
  47. package/dist/core/model/catalog-service.js +308 -0
  48. package/dist/core/model/model-catalog.js +124 -334
  49. package/dist/core/model/model-diagnostics.js +4 -5
  50. package/dist/core/model/model-fallback.js +9 -16
  51. package/dist/core/model/model-permission.js +1 -1
  52. package/dist/core/permission/approval-gateway.js +16 -1
  53. package/dist/core/permission/protected-paths.js +63 -4
  54. package/dist/core/permission/sandbox-runtime.js +4 -2
  55. package/dist/core/permission/tool-policy.js +244 -25
  56. package/dist/core/runtime-lock.js +13 -8
  57. package/dist/index.js +40 -29
  58. package/dist/ipc.js +123 -81
  59. package/dist/trigger/manager.js +3 -1
  60. package/dist/trigger/parser.js +77 -13
  61. package/dist/trigger/patch.js +104 -1
  62. package/dist/utils/ecweb-supervisor.js +11 -2
  63. package/dist/utils/ecweb-utils.js +8 -26
  64. package/dist/utils/error-utils.js +7 -0
  65. package/dist/utils/npm-ops.js +47 -15
  66. package/kits/docs/evolcore/INDEX.md +1 -0
  67. package/kits/docs/evolcore/agent-create.md +13 -0
  68. package/kits/docs/evolcore/group-collaboration.md +7 -2
  69. package/kits/docs/evolcore/menu-client.md +34 -0
  70. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  71. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  72. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  73. package/kits/templates/roles/visitor.json +1 -0
  74. package/kits/templates/system-fragments/bootstrap.md +8 -1
  75. package/package.json +11 -3
@@ -1,6 +1,7 @@
1
1
  import { normalizeTriggerDefinition, parseDurationMs, TRIGGER_SCHEMA_VERSION } from './validation.js';
2
2
  const PATCH_KEYS = new Set([
3
3
  'name',
4
+ 'description',
4
5
  'scheduleType',
5
6
  'scheduleValue',
6
7
  'timezone',
@@ -12,6 +13,21 @@ const PATCH_KEYS = new Set([
12
13
  'maxDuration',
13
14
  'concurrency',
14
15
  'missedPolicy',
16
+ 'feedback',
17
+ 'targetChannel',
18
+ 'targetChannelId',
19
+ 'targetChatType',
20
+ 'targetSession',
21
+ 'targetThreadId',
22
+ 'baseagent',
23
+ 'triggerThread',
24
+ 'onError',
25
+ 'noopSentinel',
26
+ 'retryAttempts',
27
+ 'retryBackoff',
28
+ 'scriptRuntime',
29
+ 'scriptArgs',
30
+ 'scriptTimeout',
15
31
  ]);
16
32
  export function applyTriggerPatch(definition, input, opts = {}) {
17
33
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
@@ -48,6 +64,12 @@ export function applyTriggerPatch(definition, input, opts = {}) {
48
64
  updated.reliability.concurrency = patch.concurrency;
49
65
  if (patch.missedPolicy !== undefined)
50
66
  updated.reliability.missedPolicy = patch.missedPolicy;
67
+ if (patch.description !== undefined) {
68
+ if (patch.description === null || patch.description === '')
69
+ delete updated.description;
70
+ else
71
+ updated.description = String(patch.description);
72
+ }
51
73
  if (patch.maxRuns !== undefined || patch.maxDuration !== undefined) {
52
74
  const limits = updated.limits ? { ...updated.limits } : {};
53
75
  if (patch.maxRuns !== undefined) {
@@ -64,6 +86,85 @@ export function applyTriggerPatch(definition, input, opts = {}) {
64
86
  }
65
87
  updated.limits = Object.keys(limits).length > 0 ? limits : undefined;
66
88
  }
89
+ // feedback strategy
90
+ if (patch.feedback !== undefined) {
91
+ updated.feedback.strategy = patch.feedback;
92
+ if (patch.feedback !== 'target')
93
+ delete updated.feedback.target;
94
+ }
95
+ // feedback target fields
96
+ const hasTargetPatch = patch.targetChannel !== undefined
97
+ || patch.targetChannelId !== undefined
98
+ || patch.targetChatType !== undefined
99
+ || patch.targetSession !== undefined
100
+ || patch.targetThreadId !== undefined;
101
+ if (hasTargetPatch) {
102
+ if ((patch.targetChannel === undefined) !== (patch.targetChannelId === undefined)) {
103
+ throw new Error('targetChannel 与 targetChannelId 必须同时指定');
104
+ }
105
+ const prev = updated.feedback.target;
106
+ const next = {
107
+ channelKey: patch.targetChannel ?? prev?.channelKey ?? '',
108
+ channelId: patch.targetChannelId ?? prev?.channelId ?? '',
109
+ session: patch.targetSession ?? prev?.session ?? 'main',
110
+ threadId: patch.targetThreadId === null || patch.targetThreadId === ''
111
+ ? undefined
112
+ : (patch.targetThreadId ?? prev?.threadId),
113
+ chatType: patch.targetChatType === null
114
+ ? undefined
115
+ : (patch.targetChatType ?? prev?.chatType),
116
+ };
117
+ if (!next.channelKey || !next.channelId) {
118
+ throw new Error('target 修改需要提供有效的 channelKey 和 channelId');
119
+ }
120
+ updated.feedback.target = next;
121
+ if (updated.feedback.strategy !== 'target')
122
+ updated.feedback.strategy = 'target';
123
+ }
124
+ // execution fields
125
+ if (patch.baseagent !== undefined) {
126
+ if (updated.execution.type !== 'trigger_session') {
127
+ throw new Error('baseagent 仅适用于 trigger_session 类型');
128
+ }
129
+ setOptional(updated.execution, 'baseagent', patch.baseagent);
130
+ }
131
+ if (patch.triggerThread !== undefined)
132
+ setOptional(updated.execution, 'thread', patch.triggerThread);
133
+ if (patch.onError !== undefined)
134
+ updated.execution.onError = patch.onError;
135
+ if (patch.noopSentinel !== undefined) {
136
+ if (patch.noopSentinel === null || patch.noopSentinel === '')
137
+ updated.execution.noopSentinel = '[[NOOP]]';
138
+ else
139
+ updated.execution.noopSentinel = patch.noopSentinel;
140
+ }
141
+ // reliability retry
142
+ if (patch.retryAttempts !== undefined || patch.retryBackoff !== undefined) {
143
+ const retry = { ...updated.reliability.retry };
144
+ if (patch.retryAttempts !== undefined)
145
+ retry.maxAttempts = patch.retryAttempts ?? 0;
146
+ if (patch.retryBackoff !== undefined)
147
+ retry.backoffMs = patch.retryBackoff ?? 30_000;
148
+ updated.reliability = { ...updated.reliability, retry };
149
+ }
150
+ // script fields
151
+ if (patch.scriptRuntime !== undefined || patch.scriptArgs !== undefined || patch.scriptTimeout !== undefined) {
152
+ if (updated.execution.type !== 'script' || !updated.execution.script) {
153
+ throw new Error('scriptRuntime/scriptArgs/scriptTimeout 仅适用于 script 类型 trigger');
154
+ }
155
+ const script = { ...updated.execution.script };
156
+ if (patch.scriptRuntime !== undefined)
157
+ script.runtime = patch.scriptRuntime;
158
+ if (patch.scriptArgs !== undefined) {
159
+ if (patch.scriptArgs === null)
160
+ delete script.args;
161
+ else
162
+ script.args = patch.scriptArgs;
163
+ }
164
+ if (patch.scriptTimeout !== undefined)
165
+ script.timeoutMs = patch.scriptTimeout ?? 30_000;
166
+ updated.execution.script = script;
167
+ }
67
168
  return normalizeTriggerDefinition({
68
169
  ...updated,
69
170
  id: definition.id,
@@ -98,7 +199,9 @@ export function replaceEditableTriggerDefinition(existing, input) {
98
199
  }
99
200
  function assertBaseagentImmutable(existing, updated) {
100
201
  if (existing.execution.baseagent !== updated.execution.baseagent) {
101
- throw new Error('execution.baseagent is immutable; create a new Trigger to use another baseagent');
202
+ if (existing.execution.type !== 'trigger_session' || updated.execution.type !== 'trigger_session') {
203
+ throw new Error('execution.baseagent is only mutable between trigger_session Triggers');
204
+ }
102
205
  }
103
206
  }
104
207
  function applySourcePatch(updated, patch) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
- import { resolvePaths } from '../paths.js';
4
+ import { getPackageRoot, resolvePaths } from '../paths.js';
5
5
  import { WEB_PACKAGE_NAME } from '../product.js';
6
6
  import { resolveGlobalPkg } from './npm-ops.js';
7
7
  import { ecwebVersionRequirementError, resolveEcwebLaunchCommand } from './ecweb-utils.js';
@@ -218,7 +218,16 @@ export function createEcwebSupervisor(options) {
218
218
  detached: true,
219
219
  stdio: ['ignore', childLogFd, childLogFd],
220
220
  windowsHide: platform.isWindows,
221
- env: { ...process.env, EVOLCORE_HOME: root },
221
+ // ECWeb dynamically imports EvolCore's dist modules for config,
222
+ // roles, authorization and session compatibility. Pin that lookup
223
+ // to the same package root as this daemon; otherwise a separately
224
+ // installed ECWeb can resolve its peer dependency and silently load
225
+ // a different (possibly stale) EvolCore copy from node_modules.
226
+ env: {
227
+ ...process.env,
228
+ EVOLCORE_HOME: root,
229
+ EVOLCORE_PACKAGE_ROOT: getPackageRoot(),
230
+ },
222
231
  });
223
232
  }
224
233
  catch (error) {
@@ -4,6 +4,7 @@ import { compareVersions, resolveGlobalPkg } from './npm-ops.js';
4
4
  import { isWindows as platformIsWindows, resolveCommandPath } from './cross-platform.js';
5
5
  import { MIN_ECWEB_VERSION, WEB_CLI_BIN, WEB_PACKAGE_NAME } from '../product.js';
6
6
  import { resolvePaths } from '../paths.js';
7
+ import { ipcQuery } from '../ipc.js';
7
8
  /** EC Web owns the session-log reader, so older builds cannot safely serve a newer daemon. */
8
9
  export function isEcwebVersionSupported(version) {
9
10
  const normalized = version?.trim();
@@ -65,34 +66,15 @@ export function resolveEcwebLaunchCommand(ecwebArgs, opts = {}) {
65
66
  }
66
67
  return { command: commandPath, args: ecwebArgs, source: 'command' };
67
68
  }
68
- /**
69
- * ECWeb 配对码取码 helper(共享给 daemon 和 CLI)。
70
- *
71
- * 配对码是 ecweb 进程自己生成并持有的内部状态。daemon/CLI 通过 ecweb 的
72
- * localhost-only HTTP 接口 GET /api/pair-code 取当前码(远程访问被 ecweb 403 拒绝)。
73
- */
74
- /** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。
75
- * authorizedBy 仅由 daemon 的 AUN 控制通道传入,用于把配对码换出的 token
76
- * 绑定到实际批准的 daemon-owner;普通 CLI 取码不携带该字段。 */
77
- export async function fetchEcwebPairCode(port, authorizedBy) {
69
+ export async function fetchRemotePairingCode(authorizedBy) {
78
70
  try {
79
- const headers = {};
80
- const owner = authorizedBy?.trim();
81
- if (owner)
82
- headers['x-evolcore-pair-authorized-by'] = owner;
83
- try {
84
- const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();
85
- if (controlToken)
86
- headers['x-evolcore-control-token'] = controlToken;
87
- }
88
- catch { }
89
- const resp = await fetch(`http://127.0.0.1:${port}/api/pair-code`, {
90
- headers,
91
- signal: AbortSignal.timeout(2000),
92
- });
93
- if (!resp.ok)
71
+ const response = await ipcQuery(resolvePaths().socket, {
72
+ type: 'remote-auth.pair-code',
73
+ ...(authorizedBy?.trim() ? { authorizedBy: authorizedBy.trim() } : {}),
74
+ }, 2_000);
75
+ if (!response?.ok || typeof response.code !== 'string' || typeof response.expiresAt !== 'number')
94
76
  return null;
95
- return await resp.json();
77
+ return { code: response.code, expiresAt: response.expiresAt };
96
78
  }
97
79
  catch {
98
80
  return null;
@@ -380,6 +380,13 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
380
380
  }
381
381
  // 回退到原有的错误消息匹配逻辑
382
382
  const msg = error?.message || String(error);
383
+ // Codex app-server reports this when another app-server still owns the
384
+ // thread writer. Surface the handoff condition instead of hiding it behind
385
+ // the generic "processing failed" message.
386
+ if (error?.code === 'THREAD_ACTIVE_WRITER' || /already has an active writer/i.test(msg)) {
387
+ const prefix = includeEmoji ? '⚠️ ' : '';
388
+ return `${prefix}Codex 会话正在由另一个执行通道占用,请稍后重试(THREAD_ACTIVE_WRITER)`;
389
+ }
383
390
  // 字典优先 — 命中且有自定义消息则直接返回
384
391
  const rule = matchErrorRule(msg.toLowerCase());
385
392
  if (rule?.message)
@@ -14,7 +14,7 @@ import { getPackageRoot } from '../paths.js';
14
14
  import { isWindows } from './cross-platform.js';
15
15
  const execFileAsync = promisify(execFile);
16
16
  // ── npm install -g (shared) ────────────────────────────────────────────────
17
- export async function npmInstallGlobal(pkg) {
17
+ export async function npmInstallGlobal(pkg, options = {}) {
18
18
  let cmd;
19
19
  let args;
20
20
  // Windows: run via cmd /c to avoid shell:true deprecation warning on Node 22.
@@ -28,14 +28,24 @@ export async function npmInstallGlobal(pkg) {
28
28
  args = ['install', '-g', pkg];
29
29
  }
30
30
  try {
31
- await execFileAsync(cmd, args, { timeout: 180000, windowsHide: isWindows });
31
+ await execFileAsync(cmd, args, {
32
+ timeout: options.timeoutMs ?? 180000,
33
+ windowsHide: isWindows,
34
+ signal: options.signal,
35
+ });
32
36
  }
33
37
  catch (e) {
38
+ if (options.signal?.aborted)
39
+ throw e;
34
40
  if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
35
41
  if (isWindows) {
36
42
  throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
37
43
  }
38
- await execFileAsync('sudo', ['npm', 'install', '-g', pkg], { timeout: 180000, windowsHide: false });
44
+ await execFileAsync('sudo', ['npm', 'install', '-g', pkg], {
45
+ timeout: options.timeoutMs ?? 180000,
46
+ windowsHide: false,
47
+ signal: options.signal,
48
+ });
39
49
  }
40
50
  else {
41
51
  throw e;
@@ -119,15 +129,19 @@ export function resolveGlobalPkg(pkgName) {
119
129
  * 使用 HTTP fetch 直接查 registry API,不依赖 npm CLI。
120
130
  * 超时 10 秒,失败返回 null。
121
131
  */
122
- export async function checkLatestVersion(pkg = 'evolcore') {
132
+ export async function checkLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal) {
133
+ const controller = new AbortController();
134
+ const abort = () => controller.abort();
135
+ if (signal?.aborted)
136
+ abort();
137
+ else
138
+ signal?.addEventListener('abort', abort, { once: true });
139
+ const timer = setTimeout(abort, timeoutMs);
123
140
  try {
124
- const controller = new AbortController();
125
- const timer = setTimeout(() => controller.abort(), 10000);
126
141
  const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
127
142
  signal: controller.signal,
128
143
  headers: { 'Accept': 'application/json' },
129
144
  });
130
- clearTimeout(timer);
131
145
  if (!res.ok)
132
146
  return null;
133
147
  const data = await res.json();
@@ -136,20 +150,26 @@ export async function checkLatestVersion(pkg = 'evolcore') {
136
150
  catch {
137
151
  return null;
138
152
  }
153
+ finally {
154
+ clearTimeout(timer);
155
+ signal?.removeEventListener('abort', abort);
156
+ }
139
157
  }
140
158
  /**
141
159
  * 完整升级流程:检查 → 比较 → 安装(失败重试一次)
142
160
  */
143
- export async function tryUpgrade() {
161
+ export async function tryUpgrade(options = {}) {
144
162
  // 开发模式跳过
145
163
  if (isLinkedInstall()) {
146
164
  return { status: 'skipped' };
147
165
  }
148
166
  const localVer = getLocalVersion();
149
167
  // 查询 registry
150
- const remoteVer = await checkLatestVersion();
168
+ const remoteVer = await checkLatestVersion('evolcore', options.checkTimeoutMs, options.signal);
151
169
  if (!remoteVer) {
152
- return { status: 'skipped', error: 'Failed to check remote version' };
170
+ return options.signal?.aborted
171
+ ? { status: 'failed', from: localVer, error: 'Upgrade cancelled or timed out' }
172
+ : { status: 'skipped', error: 'Failed to check remote version' };
153
173
  }
154
174
  // 版本比较
155
175
  if (compareVersions(localVer, remoteVer) >= 0) {
@@ -159,11 +179,16 @@ export async function tryUpgrade() {
159
179
  let lastError;
160
180
  for (let attempt = 0; attempt < 2; attempt++) {
161
181
  try {
162
- await npmInstallGlobal('evolcore@latest');
182
+ await npmInstallGlobal('evolcore@latest', {
183
+ timeoutMs: options.installTimeoutMs,
184
+ signal: options.signal,
185
+ });
163
186
  return { status: 'upgraded', from: localVer, to: remoteVer };
164
187
  }
165
188
  catch (e) {
166
189
  lastError = e.stderr || e.message || String(e);
190
+ if (options.signal?.aborted)
191
+ break;
167
192
  }
168
193
  }
169
194
  return { status: 'failed', from: localVer, to: remoteVer, error: lastError };
@@ -173,7 +198,7 @@ export async function tryUpgrade() {
173
198
  * 仅在包已安装时检查升级,未安装则跳过(resolveFn 返回 null)。
174
199
  * 用于 EC Web 等真正独立于 EvolCore 的全局包。
175
200
  */
176
- export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName) {
201
+ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName, options = {}) {
177
202
  if (isLinkedInstall()) {
178
203
  return { status: 'skipped' };
179
204
  }
@@ -182,9 +207,11 @@ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName) {
182
207
  return { status: 'skipped' }; // not installed, skip
183
208
  }
184
209
  const localVer = installed.version;
185
- const remoteVer = await checkLatestVersion(pkgName);
210
+ const remoteVer = await checkLatestVersion(pkgName, options.checkTimeoutMs, options.signal);
186
211
  if (!remoteVer) {
187
- return { status: 'skipped', error: 'Failed to check remote version' };
212
+ return options.signal?.aborted
213
+ ? { status: 'failed', from: localVer, error: 'Upgrade cancelled or timed out' }
214
+ : { status: 'skipped', error: 'Failed to check remote version' };
188
215
  }
189
216
  if (compareVersions(localVer, remoteVer) >= 0) {
190
217
  return { status: 'no-update', from: localVer };
@@ -192,11 +219,16 @@ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName) {
192
219
  let lastError;
193
220
  for (let attempt = 0; attempt < 2; attempt++) {
194
221
  try {
195
- await npmInstallGlobal(`${pkgName}@latest`);
222
+ await npmInstallGlobal(`${pkgName}@latest`, {
223
+ timeoutMs: options.installTimeoutMs,
224
+ signal: options.signal,
225
+ });
196
226
  return { status: 'upgraded', from: localVer, to: remoteVer };
197
227
  }
198
228
  catch (e) {
199
229
  lastError = e.stderr || e.message || String(e);
230
+ if (options.signal?.aborted)
231
+ break;
200
232
  }
201
233
  }
202
234
  return { status: 'failed', from: localVer, to: remoteVer, error: lastError };
@@ -37,6 +37,7 @@
37
37
  | `ec agent` | EvolAgent 生命周期 | 创建/启停/热重载/改配置 | 管理员(owner/admin) | `agent.md`;完整创建与 Bootstrap 见 `agent-create.md` |
38
38
  | `ec contact` | Contact Book 与访问控制 | 申请添加、拉黑/解除拉黑、查拉黑 | Agent 当前私聊可申请添加;管理写操作按角色限制 | `contact.md` |
39
39
  | `ec rpc` | 底层 AUN RPC(逃生通道) | 直接调协议方法 | 高级/兜底 | `rpc.md` |
40
+ | `ec menu-client` | 远程 Daemon Menu 请求 | 远程诊断/daemon 日志/监控/原始配置 | 非托管 eclink/运维主机 | `menu-client.md` |
40
41
 
41
42
  ## 会话自管理类(不连 AUN,操作当前会话)
42
43
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  本手册用于创建一个能正常工作的 EvolAgent。创建者负责创建和观察状态;新 Agent 在自己的 Bootstrap 会话中完成首次设定。不要把创建流水线完成误认为 Agent 已激活。
4
4
 
5
+ ### AID 与 Owner 约定
6
+
7
+ - AID 必须是 `{name}.{issuer}`,例如 `helper.agentid.cn`;不要使用四级域名(如 `helper.team.agentid.cn`),也不要把显示名当作 AID。
8
+ - `--owner` 填写需求提出者或实际责任 Owner 的完整 AID,而不是默认填写当前执行者。创建者只是代办创建流程时,仍应保留需求提出者的 Owner 权限。
9
+
5
10
  ## 生命周期与完成标准
6
11
 
7
12
  ```text
@@ -18,6 +23,8 @@ active
18
23
 
19
24
  ## 1. 创建者创建 Agent
20
25
 
26
+ 多个 Agent 必须串行创建和观察。不要并行启动多个 `ec agent new`:证书、热加载和 Bootstrap 会竞争连接与资源,容易造成超时、锁等待或把失败误判为未创建。
27
+
21
28
  在独立终端可以使用交互模式:
22
29
 
23
30
  ```bash
@@ -56,6 +63,8 @@ ec agent show helper.agentid.pub
56
63
 
57
64
  如果是 menu 创建,`accepted=true` 只表示请求已受理。继续轮询 `createProgress.status` 直到 `ready` 或 `failed`。这里的 `createProgress.status=ready` **只表示后台创建流水线结束,不等于 `lifecycle=active`**。
58
65
 
66
+ 创建者应维护一个最小状态账本(可放在当前任务记录中),每个 Agent 至少记录:`aid`、需求 Owner、项目路径、创建请求是否受理、`createProgress.status`、`lifecycle`、本地 `agent.md` 是否存在/字段是否完整、名片是否已发布、最近错误、下一步和最后检查时间。重启或超时恢复时先按账本逐项核对实际状态,再决定续做,不按“上次命令是否返回”推断结果。
67
+
59
68
  ## 2. 触发 Bootstrap
60
69
 
61
70
  当任一已启用 channel 可以联系 Owner 时,系统会向 Owner 发送固定的 Bootstrap 首条消息,并把生命周期从 `created` 切换为 `bootstrapping`。
@@ -136,6 +145,8 @@ ec agent get helper.agentid.pub lifecycle
136
145
  ec agent show helper.agentid.pub
137
146
  ```
138
147
 
148
+ 验证其它 Agent 的生命周期、配置和创建进度统一使用 `ec agent get/show/list`;`ec aid` 只用于身份、证书和名片操作,不要用 `ec aid` 代替 Agent 状态验证。
149
+
139
150
  验收标准:
140
151
 
141
152
  - `lifecycle` 为 `active`。
@@ -144,3 +155,5 @@ ec agent show helper.agentid.pub
144
155
  - 新 Agent 能在新主会话中处理普通任务。
145
156
 
146
157
  如果仍为 `created`,检查 channel 是否已能联系 Owner,必要时由 Owner 主动发送首条消息。如果仍为 `bootstrapping`,回到新 Agent 的 Bootstrap 会话完成字段确认、名片发布和 `ready`,不要由创建者直接绕过引导。
158
+
159
+ 失败重试前必须先执行状态检查:若本地 `agent.md` 已存在且远端名片已发布,只向该 Agent 发送 `ready`/续做指令,让它在自己的 Bootstrap 会话中完成收尾;不要重复创建、重复改名片或重复发布。仅在确认创建记录不存在且名片未发布时,才重新发起创建。
@@ -21,6 +21,7 @@
21
21
 
22
22
  - `<self-aid>` 是当前 Agent 的 AID。托管 Agent 会话必须使用自己的 AID,不能代替其他 Agent 发起管理操作。
23
23
  - 创建群的 Agent 自动成为群 owner。
24
+ - 需求发起者应设置为群 admin(若需求发起者就是创建者,则其已是 owner,无需另设)。
24
25
  - owner/admin 负责邀请成员、设置角色、修改入群规则和发布 `/rules.md`。
25
26
  - 普通 member 可以读取当前群信息、读取规则并参与协作;需要管理群成员或设置时,应请 owner/admin 操作。
26
27
  - `dissolve` 和 `owner` 转让属于 owner 级操作,临时协作结束通常不需要解散群。
@@ -36,13 +37,13 @@
36
37
  - 分工:谁负责协调、执行、复核和最终决策。
37
38
  - 验收:完成条件、截止时间、交付路径和汇报频率。
38
39
 
39
- 使用 Owner 明确提供或已经可靠取得的完整 AID;不要从显示名猜测 AID,也不要邀请尚未确认的对象。如果参与者、可见性或入群方式不明确,先向 Owner 补齐这些信息再创建群。
40
+ 使用 Owner 明确提供或已经可靠取得的完整 AID;AID 必须是 `{name}.{issuer}`,例如 `coordinator.agentid.cn`,不要使用四级域名或从显示名猜测 AID,也不要邀请尚未确认的对象。如果参与者、可见性或入群方式不明确,先向 Owner 补齐这些信息再创建群。
40
41
 
41
42
  如果已经有合适的群,先用 `ec group list` 和 `ec group info` 检查,不要为每个任务重复建群。
42
43
 
43
44
  ## 2. 创建私有协作群
44
45
 
45
- 推荐默认使用私有群和 `invite_only`,先建群再按名单邀请:
46
+ 群名称必填。创建前根据群名称确定一个简短、唯一的 group AID(同样使用 `{name}.{issuer}`,不要四级域名),并给出一句群简介;名称、AID 和简介应能体现协作目标。推荐默认使用私有群和 `invite_only`,先建群再按名单邀请:
46
47
 
47
48
  ```bash
48
49
  ec group create coordinator.agentid.pub "Project Alpha 协作" \
@@ -63,6 +64,8 @@ ec group rules coordinator.agentid.pub <group-id> --format json
63
64
 
64
65
  ## 3. 邀请成员并设置角色
65
66
 
67
+ 先确认所有待协作 Agent 的 `lifecycle` 都是 `active`(使用 `ec agent get/show`,不要用 `ec aid`)。在此之前不要邀请成员,也不要发送或分派任务;Bootstrap 未完成的 Agent 不能稳定接收协作工作。
68
+
66
69
  一次邀请多个成员:
67
70
 
68
71
  ```bash
@@ -100,6 +103,8 @@ ec group rules coordinator.agentid.pub <group-id> \
100
103
 
101
104
  ### 4.2 编写并发布 `/rules.md`
102
105
 
106
+ 团队群必须先发布一份最小 `/rules.md`。至少明确四类职责:协调(拆解任务、同步状态、最终汇总)、侦察(收集资料和事实)、分析(比较方案、识别风险)、策略(做出取舍和升级决策)。可再补充具体成员映射、交付格式和升级路径。
107
+
103
108
  在当前项目中准备一个不超过 4KB 的 UTF-8 Markdown 文件,例如 `project-alpha-rules.md`:
104
109
 
105
110
  ```markdown
@@ -0,0 +1,34 @@
1
+ # ec menu-client — 远程 Daemon Menu 客户端
2
+
3
+ 向另一台主机的 EvolCore Control AID 发送一个标准 `menu.*` 请求,并在同一 AUN 短连接中完成发送、拉取、匹配和 ACK。适合非托管主机上的 eclink/运维工具调用;托管 Agent 会话不应使用它绕过当前 daemon。
4
+
5
+ ## 用法
6
+
7
+ ```bash
8
+ ec menu-client request <from-aid> <daemon-aid> \
9
+ --request-file "$TMPDIR/menu-request.json" \
10
+ --timeout 15s \
11
+ --encrypt \
12
+ --format json
13
+ ```
14
+
15
+ - `--request-file` 与 `--request-json` 二选一,内容是现有 Menu request;缺少 `id` 时自动生成 UUID。
16
+ - 默认使用 E2EE,并发送 `client_name=ec-menu-client` 和本机 EvolCore `client_version`。
17
+ - 默认 slot 是 `evolcore-menu-client`;`--app <slot>` 可覆盖,但会切换消费游标,调用方须自行串行化。
18
+ - 默认授权是远端 `daemon.json.owners` 对发送 AID 的校验。
19
+ - `--remote-token <token>` 使用 daemon 签发的远程访问 token,权限与 daemon-owner 一致。CLI 审计日志不会记录 token 明文。
20
+ - `--ecweb-token <token>` 是 `--remote-token` 的兼容别名。
21
+ - `menu.query` / `menu.options` 等待响应超时时自动重发一次相同 request id;写操作不自动重试。
22
+ - 成功时 stdout 是完整 `menu.response` JSON。远端业务错误仍在响应的 `error` 字段中;本地传输/超时/ACK 错误退出码非 0。
23
+
24
+ ## 常用请求
25
+
26
+ ```json
27
+ {"type":"menu.query","id":"status-1","name":"system","args":{"view":"status"}}
28
+ {"type":"menu.query","id":"monitor-1","name":"system","args":{"view":"monitor"}}
29
+ {"type":"menu.query","id":"logs-1","name":"system","args":{"view":"logs","source":"daemon","limit":200}}
30
+ {"type":"menu.options","id":"config-options-1","name":"config","args":{"option":"rawFile"}}
31
+ {"type":"menu.query","id":"config-1","name":"config","args":{"view":"rawFile","resource":"daemon"}}
32
+ ```
33
+
34
+ 先用 `menu.list` 的 `args.view=catalog`、`context.kind=process` 获取目标 daemon 实际支持的能力和 wire 参数,不要在适配器中另建一套协议字段。
@@ -531,7 +531,15 @@ function planMigration(home) {
531
531
  function assertDaemonStopped(home) {
532
532
  const candidates = [];
533
533
  const pidFile = path.join(home, 'daemon.pid');
534
- if (fs.existsSync(pidFile)) candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
534
+ try {
535
+ // daemon.pid is a retired legacy marker. Ignore directory/symlink residue
536
+ // so a sandbox-created directory cannot turn migration into EISDIR.
537
+ if (fs.lstatSync(pidFile).isFile()) {
538
+ candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
539
+ }
540
+ } catch (error) {
541
+ if (error?.code !== 'ENOENT') throw error;
542
+ }
535
543
  const instanceDir = path.join(home, 'data', 'instance');
536
544
  if (fs.existsSync(instanceDir)) {
537
545
  for (const name of fs.readdirSync(instanceDir)) {
@@ -452,11 +452,17 @@ function changeRecord(home, file, kind, beforeText, after) {
452
452
  function assertDaemonStopped(home) {
453
453
  const candidates = [];
454
454
  const legacyPidFile = path.join(home, 'daemon.pid');
455
- if (fs.existsSync(legacyPidFile)) {
456
- candidates.push({
457
- pid: Number(fs.readFileSync(legacyPidFile, 'utf8').trim()),
458
- source: legacyPidFile,
459
- });
455
+ try {
456
+ // daemon.pid is a retired legacy marker. Ignore directory/symlink residue
457
+ // so a sandbox-created directory cannot turn migration into EISDIR.
458
+ if (fs.lstatSync(legacyPidFile).isFile()) {
459
+ candidates.push({
460
+ pid: Number(fs.readFileSync(legacyPidFile, 'utf8').trim()),
461
+ source: legacyPidFile,
462
+ });
463
+ }
464
+ } catch (error) {
465
+ if (error?.code !== 'ENOENT') throw error;
460
466
  }
461
467
 
462
468
  const instanceDir = path.join(home, 'data', 'instance');
@@ -200,8 +200,14 @@ function planMigration(home) {
200
200
  function assertDaemonStopped(home) {
201
201
  const candidates = [];
202
202
  const pidFile = path.join(home, 'daemon.pid');
203
- if (fs.existsSync(pidFile)) {
204
- candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
203
+ try {
204
+ // daemon.pid is a retired legacy marker. Ignore directory/symlink residue
205
+ // so a sandbox-created directory cannot turn migration into EISDIR.
206
+ if (fs.lstatSync(pidFile).isFile()) {
207
+ candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
208
+ }
209
+ } catch (error) {
210
+ if (error?.code !== 'ENOENT') throw error;
205
211
  }
206
212
  const instanceDir = path.join(home, 'data', 'instance');
207
213
  if (fs.existsSync(instanceDir)) {
@@ -21,6 +21,7 @@
21
21
  "model.list": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
22
22
  "model.current": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
23
23
  "model.info": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
24
+ "model.effort.current": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
24
25
  "model.use": { "allow": false },
25
26
  "model.effort": { "allow": false },
26
27
  "model.reset": { "allow": false },
@@ -2,7 +2,7 @@
2
2
  phase: bootstrapping
3
3
 
4
4
  这是 agent 生命周期的强制门禁。你正在进行首次自我设定;在完成前,唯一目标是和 owner 确认并写入基础名片信息,不处理普通任务。
5
- 整个过程由你根据当前对话自主推进,不存在需要查询或维护的额外 bootstrap 子状态。
5
+ Bootstrap 不另设隐藏状态机;恢复依据是可核对的生命周期和名片状态。每次重启、超时恢复或收到“继续”时,先检查自身 `lifecycle` 以及本地 `agent.md` 是否存在、字段是否完整,再决定续做步骤,不按旧上下文推断尚未完成。
6
6
  系统已经在进入本阶段时向 owner 发送过固定的首条引导消息。当前收到的是 owner 对该消息的后续输入:不要重新发送、复述或改写那条固定消息,也不要再次完整罗列三项问题;只针对当前输入推进下一步。
7
7
 
8
8
  需要确认的字段:
@@ -17,6 +17,13 @@ phase: bootstrapping
17
17
  - `ec agent ready {{selfAid}}`
18
18
  - 除这些 bootstrap 操作及向 owner 回复外,不得调用工具或执行命令。
19
19
 
20
+ 恢复时遵循以下幂等规则:
21
+ - `lifecycle=active`:Bootstrap 已完成,不重复改名片、不重复发布、不重复执行 `ready`,也不处理本轮夹带的普通任务。
22
+ - `lifecycle=created`:尚未进入 Bootstrap,只等待 channel/owner 首条消息;不重复创建 Agent。
23
+ - `lifecycle=bootstrapping` 且本地 `agent.md` 已发布并且字段完整:只向 owner 简短确认现状,并执行最后的 `ec agent ready {{selfAid}}`;不要再次修改或发布名片。
24
+ - `lifecycle=bootstrapping` 但名片未发布或字段不完整:从缺失的确认步骤继续。发布失败时先说明错误,修正后再试,不盲目重试。
25
+ - Bootstrap 期间不执行、不分析、不规划普通任务;普通任务只能留到 `active` 后的新主会话。
26
+
20
27
  每轮处理规则:
21
28
  1. 首次需要候选值时,读取本地 agent.md 的 YAML frontmatter。已有且格式有效的 `name`、`description`、`tags` 是现有候选值:简短回显并询问保持还是修改,不要求重新输入。空值、占位值或格式无效的字段视为缺失;`tags` 必须为 3 到 6 个非空标签。
22
29
  2. 只把 owner 明确提供、修改、选择或确认的 `name`、`description`、`tags` 当作候选信息。普通任务内容、示例值和顺带提及的词语都不是候选信息,不得据此推断字段。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolcore",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,7 +48,7 @@
48
48
  "prepublishOnly": "npm test"
49
49
  },
50
50
  "dependencies": {
51
- "@agentunion/fastaun": "0.5.11",
51
+ "@agentunion/fastaun": "0.5.13",
52
52
  "@anthropic-ai/claude-agent-sdk": "0.3.220",
53
53
  "@modelcontextprotocol/sdk": "^1.29.0",
54
54
  "ajv": "^8.20.0",
@@ -58,6 +58,7 @@
58
58
  "image-type": "^6.0.0",
59
59
  "partial-json": "0.1.7",
60
60
  "qrcode-terminal": "^0.12.0",
61
+ "smol-toml": "1.8.0",
61
62
  "typebox": "1.1.38",
62
63
  "ws": "^8.21.0",
63
64
  "yaml": "2.9.0",
@@ -82,5 +83,12 @@
82
83
  },
83
84
  "engines": {
84
85
  "node": ">=22.16.0"
85
- }
86
+ },
87
+ "directories": {
88
+ "doc": "docs",
89
+ "test": "tests"
90
+ },
91
+ "keywords": [],
92
+ "author": "",
93
+ "license": "ISC"
86
94
  }