evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -413,6 +413,7 @@ function normalizeExecutionAnomaly(value) {
413
413
  const occurredAt = typeof value.occurredAt === 'number' && Number.isFinite(value.occurredAt)
414
414
  ? value.occurredAt
415
415
  : Date.now();
416
+ const correlationId = nonEmptyString(value.correlationId);
416
417
  const toolName = nonEmptyString(value.toolName);
417
418
  const requestId = nonEmptyString(value.requestId);
418
419
  const policyCode = nonEmptyString(value.policyCode);
@@ -426,6 +427,7 @@ function normalizeExecutionAnomaly(value) {
426
427
  severity: 'warning',
427
428
  phase: 'execution',
428
429
  occurredAt,
430
+ ...(correlationId ? { correlationId } : {}),
429
431
  effect,
430
432
  ...(toolName ? { toolName } : {}),
431
433
  ...(requestId ? { requestId } : {}),
package/dist/cli/agent.js CHANGED
@@ -8,18 +8,14 @@ import { resolveConfigCommand } from '../config/resolved-config-op.js';
8
8
  import { executeResolvedConfigCommand } from '../config/config-operation-service.js';
9
9
  import { ipcQuery } from '../ipc.js';
10
10
  import { CONFIG_SCHEMA_VERSION } from '../types.js';
11
- import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
12
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
13
11
  import { ensureInitialRoleRegistry } from '../config/role-service.js';
14
- import { formatChannelKey, isValidChannelName } from '../core/channel-loader.js';
12
+ import { isValidChannelName } from '../core/channel-loader.js';
15
13
  import { commandExists } from '../utils/cross-platform.js';
16
14
  import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
17
15
  import { resolveEcagentConfig } from '../agents/baseagent.js';
18
16
  import { agentProjectRootFromDefaults, deriveAgentProjectPath } from '../utils/project-path.js';
19
17
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
20
18
  import { readTaskRuntimeContextFromEnv } from './task-context.js';
21
- import { EventBus } from '../core/event-bus.js';
22
- import { SessionManager } from '../core/session/session-manager.js';
23
19
  import { isValidAid } from '../aun/aid/validation.js';
24
20
  function withStaticOwner(config, owner) {
25
21
  const value = owner?.trim();
@@ -1196,33 +1192,9 @@ export async function agentRename(aid, name) {
1196
1192
  return { ok: true, aid, name };
1197
1193
  }
1198
1194
  // ==================== agentReady ====================
1199
- async function ensureOfflinePostBootstrapMainSession(aid, config) {
1200
- const owner = config.owners?.[0];
1201
- if (!owner)
1202
- throw new Error(`Agent "${aid}" has no owner for the post-bootstrap main session`);
1203
- const p = resolvePaths();
1204
- const aunName = config.channels?.find(channel => channel.type === 'aun')?.name || 'main';
1205
- const channel = formatChannelKey({ type: 'aun', selfAID: aid, name: aunName });
1206
- const manager = new SessionManager(p.sessionsDir, new EventBus());
1207
- const active = await manager.getActiveSession(channel, owner);
1208
- const ensured = await manager.ensureNewMainSessionForOperation({
1209
- channel,
1210
- channelId: owner,
1211
- projectPath: active?.projectPath
1212
- || config.projects?.defaultPath
1213
- || path.join(p.root, 'projects', aid.split('.')[0]),
1214
- baseagent: config.active_baseagent || active?.baseagent || 'claude',
1215
- operationId: postBootstrapWelcomeOperationId(aid),
1216
- identityMetadata: {
1217
- ...(active?.metadata ?? {}),
1218
- channelKey: channel,
1219
- peerId: owner,
1220
- },
1221
- });
1222
- return ensured.session.id;
1223
- }
1224
1195
  export async function agentReady(aid) {
1225
- const taskSelfAid = readTaskRuntimeContextFromEnv()?.selfAid;
1196
+ const managedSessionId = process.env.EVOLCORE_SESSION_ID;
1197
+ const taskSelfAid = managedSessionId ? readTaskRuntimeContextFromEnv()?.selfAid : undefined;
1226
1198
  if (taskSelfAid && taskSelfAid.replace(/^@/, '') !== aid.replace(/^@/, '')) {
1227
1199
  return {
1228
1200
  ok: false,
@@ -1231,77 +1203,18 @@ export async function agentReady(aid) {
1231
1203
  };
1232
1204
  }
1233
1205
  const p = resolvePaths();
1234
- const managedSessionId = process.env.EVOLCORE_SESSION_ID;
1235
- if (managedSessionId) {
1236
- const delegationToken = process.env[AGENT_DELEGATION_TOKEN_ENV];
1237
- if (!delegationToken)
1206
+ try {
1207
+ const delegationToken = managedSessionId ? process.env[AGENT_DELEGATION_TOKEN_ENV] : undefined;
1208
+ if (managedSessionId && !delegationToken) {
1238
1209
  return { ok: false, code: 'DELEGATION_REQUIRED', error: `${AGENT_DELEGATION_TOKEN_ENV} is not set for the active task` };
1210
+ }
1239
1211
  const result = await ipcQuery(p.socket, {
1240
1212
  type: 'evolagent.bootstrapComplete',
1241
1213
  aid,
1242
- sessionId: managedSessionId,
1243
- delegationToken,
1214
+ ...(managedSessionId ? { sessionId: managedSessionId, delegationToken } : {}),
1244
1215
  }, 30_000);
1245
1216
  if (!result)
1246
- return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' };
1247
- return result.ok
1248
- ? { ok: true, aid, reloaded: result.reloaded === true }
1249
- : { ok: false, code: result.code, error: result.error || 'Failed to complete bootstrap' };
1250
- }
1251
- const config = loadAgent(aid);
1252
- if (!config)
1253
- return { ok: false, error: `Agent "${aid}" not found` };
1254
- try {
1255
- let controlToken;
1256
- try {
1257
- controlToken = fs.readFileSync(p.controlToken, 'utf8').trim() || undefined;
1258
- }
1259
- catch { }
1260
- const result = await ipcQuery(p.socket, { type: 'evolagent.bootstrapComplete', aid, controlToken }, 30_000);
1261
- if (!result) {
1262
- const offlineConfig = loadAgent(aid) || config;
1263
- const lifecycle = normalizeAgentLifecycle(offlineConfig).lifecycle;
1264
- if (lifecycle !== 'bootstrapping') {
1265
- if (lifecycle === 'active') {
1266
- if (hasPendingPostBootstrapWelcomeOutbox(aid)) {
1267
- try {
1268
- const sessionId = await ensureOfflinePostBootstrapMainSession(aid, offlineConfig);
1269
- if (!bindPostBootstrapWelcomeOutboxSession(aid, sessionId)) {
1270
- throw new Error(`Failed to bind the completion welcome to session ${sessionId}`);
1271
- }
1272
- }
1273
- catch (e) {
1274
- return {
1275
- ok: false,
1276
- code: 'MAIN_SESSION_CREATE_FAILED',
1277
- error: e instanceof Error ? e.message : 'Failed to recover the post-bootstrap main session',
1278
- };
1279
- }
1280
- }
1281
- return { ok: true, aid, reloaded: false };
1282
- }
1283
- return { ok: false, code: 'INVALID_LIFECYCLE', error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready` };
1284
- }
1285
- const owner = offlineConfig.owners?.[0];
1286
- if (!owner)
1287
- return { ok: false, code: 'OWNER_REQUIRED', error: `Agent "${aid}" has no owner for the completion welcome` };
1288
- try {
1289
- preparePostBootstrapWelcomeOutbox(aid, owner);
1290
- const sessionId = await ensureOfflinePostBootstrapMainSession(aid, offlineConfig);
1291
- if (!bindPostBootstrapWelcomeOutboxSession(aid, sessionId)) {
1292
- throw new Error(`Failed to bind the completion welcome to session ${sessionId}`);
1293
- }
1294
- saveAgent(withLifecycleForWrite(offlineConfig, 'active'));
1295
- }
1296
- catch (e) {
1297
- return {
1298
- ok: false,
1299
- code: 'READY_PERSIST_FAILED',
1300
- error: e instanceof Error ? e.message : 'Failed to persist bootstrap completion',
1301
- };
1302
- }
1303
- return { ok: true, aid, reloaded: false };
1304
- }
1217
+ return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon; agent ready requires daemon IPC' };
1305
1218
  if (!result.ok) {
1306
1219
  return { ok: false, code: result.code, error: result.error || 'Failed to complete bootstrap' };
1307
1220
  }
@@ -1698,7 +1698,7 @@ Options:
1698
1698
  else if (r.handoff_id)
1699
1699
  console.log(`✓ 已排队 handoff ${r.handoff_id}(尚未投递)`);
1700
1700
  else
1701
- console.log(`✓ 已发送 message_id=${r.message?.message_id ?? '-'} seq=${r.message?.seq ?? '-'}`);
1701
+ console.log(`✓ 已发送 message_id=${r.message_id ?? r.message?.message_id ?? '-'} seq=${r.message?.seq ?? '-'}`);
1702
1702
  });
1703
1703
  return;
1704
1704
  }
@@ -3,6 +3,16 @@ export function normalizeCliArgv(argv) {
3
3
  return argv.slice(1);
4
4
  return argv;
5
5
  }
6
+ export function splitConfigBatchGetArgv(inputArgv) {
7
+ const argv = normalizeCliArgv(inputArgv);
8
+ if (argv[0] !== 'config' || argv[1] !== 'get')
9
+ return null;
10
+ const fields = [];
11
+ let i = 2;
12
+ for (; i < argv.length && !argv[i].startsWith('-'); i++)
13
+ fields.push(argv[i]);
14
+ return { fields, rest: argv.slice(i) };
15
+ }
6
16
  export function validateCliArgv(value) {
7
17
  if (!Array.isArray(value) || value.length === 0)
8
18
  return { ok: false, reason: 'args.argv must be a non-empty string array' };
@@ -5,7 +5,7 @@ import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand, } from '..
5
5
  import { ipcQuery } from '../ipc.js';
6
6
  import { resolvePaths } from '../paths.js';
7
7
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
8
- import { splitConfigBatchGetArgv } from '../config/config-batch-get.js';
8
+ import { splitConfigBatchGetArgv } from './cli-argv.js';
9
9
  function emit(formatJson, payload, text) {
10
10
  if (formatJson)
11
11
  console.log(JSON.stringify(payload, null, 2));
@@ -47,6 +47,8 @@ function renderConfigExecution(result, formatJson) {
47
47
  switch (result.subcommand) {
48
48
  case 'get':
49
49
  return emit(formatJson, getResultPayload(result), () => getResultLine(result));
50
+ case 'batch-get':
51
+ return renderBatchGet(result.results, formatJson);
50
52
  case 'set':
51
53
  return emit(formatJson, {
52
54
  ok: true,
@@ -186,23 +188,13 @@ function runDirectBatchGet(fields, rest, formatJson) {
186
188
  }
187
189
  async function runManagedBatchGet(fields, rest, formatJson) {
188
190
  const { sessionId, delegationToken } = managedSession(formatJson);
189
- const results = [];
190
- for (const field of fields) {
191
- const response = await managedConfigQuery(['get', field, ...rest], sessionId, delegationToken);
192
- if (!response) {
193
- results.push({ ok: false, field, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' });
194
- }
195
- else if (!response.ok || !response.result || !response.result.ok) {
196
- results.push({ ok: false, field, code: response.code || 'CONFIG_AUTH_FAILED', error: response.error || 'config operation failed' });
197
- }
198
- else if (response.result.subcommand === 'get') {
199
- results.push(response.result);
200
- }
201
- else {
202
- results.push({ ok: false, field, code: 'UNEXPECTED_RESULT', error: `config get ${field} produced a non-get result` });
203
- }
191
+ const response = await managedConfigQuery(['get', ...fields, ...rest], sessionId, delegationToken);
192
+ if (!response)
193
+ fail(formatJson, 'DAEMON_UNAVAILABLE', 'Unable to connect to the evolcore daemon');
194
+ if (!response.ok || !response.result) {
195
+ fail(formatJson, response.code || 'CONFIG_AUTH_FAILED', response.error || 'config operation failed', response.data);
204
196
  }
205
- renderBatchGet(results, formatJson);
197
+ renderConfigExecution(response.result, formatJson);
206
198
  }
207
199
  const HELP = `Usage: ec config <command> [options]
208
200
 
@@ -16,7 +16,7 @@ const CTL_HELP = `用法: ec ctl <command> [args...]
16
16
  消息:
17
17
  send <消息内容> 主动发送文本消息(proactive 模式)
18
18
  file [channel] <path> 发送项目内文件
19
- queue 查看/管理会话消息队列
19
+ queue [--after <time>] [--before <time>] 查看/管理会话消息队列
20
20
 
21
21
  示例:
22
22
  ec ctl status
@@ -389,9 +389,6 @@ export async function cmdStart(opts = {}) {
389
389
  cleanEnv();
390
390
  // 在启动前确保 serviceProxy 配置完整(如果 ecweb 启用但 serviceProxy 未配置)
391
391
  ensureServiceProxyConfigBeforeStart(p);
392
- const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
393
- ? await startEcwebIfEnabled(p)
394
- : false;
395
392
  // 删除旧的 ready signal
396
393
  try {
397
394
  fs.unlinkSync(p.readySignal);
@@ -421,23 +418,11 @@ export async function cmdStart(opts = {}) {
421
418
  console.log(` EVOLCORE_HOME: ${resolveRoot()}`);
422
419
  console.log(` Logs: ${p.logs}/`);
423
420
  console.log(`⏱ ready in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
424
- if (!ecwebStartedBeforeDaemon) {
425
- startEcwebIfEnabled(p)
426
- .then((ecwebStarted) => {
427
- if (shouldPrintNoSelfAgentHints) {
428
- printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
429
- }
430
- })
431
- .catch((err) => {
432
- console.error(`⚠ ECWeb 启动检查失败: ${err instanceof Error ? err.message : String(err)}`);
433
- if (shouldPrintNoSelfAgentHints) {
434
- printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: false, skipped });
435
- }
436
- });
437
- }
438
- else if (shouldPrintNoSelfAgentHints) {
439
- printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: true, skipped });
440
- }
421
+ void waitForExistingEcweb(p).then((ecweb) => {
422
+ if (shouldPrintNoSelfAgentHints) {
423
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: ecweb !== null, skipped });
424
+ }
425
+ });
441
426
  }, 500);
442
427
  let forwardingShutdown = false;
443
428
  const forwardSignal = (signal) => {
@@ -536,8 +521,9 @@ export async function cmdStart(opts = {}) {
536
521
  printCodeStats(getPackageRoot(), p.logs);
537
522
  }
538
523
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
539
- // ECWeb 自动后台启动
540
- const ecwebStarted = ecwebStartedBeforeDaemon || await startEcwebIfEnabled(p);
524
+ // ECWeb lifecycle belongs to the daemon; the CLI only reports whether
525
+ // the supervised instance became healthy.
526
+ const ecwebStarted = await waitForExistingEcweb(p) !== null;
541
527
  if (shouldPrintNoSelfAgentHints) {
542
528
  printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
543
529
  }
@@ -2422,6 +2408,24 @@ async function probeEcwebReady(port) {
2422
2408
  return false;
2423
2409
  }
2424
2410
  }
2411
+ /**
2412
+ * Wait briefly for an already-launched ECWeb to become healthy. `ec start`,
2413
+ * restart-monitor, and the daemon can overlap by a few milliseconds; treating
2414
+ * a healthy instance as an adoption target avoids killing/replacing it.
2415
+ */
2416
+ async function waitForExistingEcweb(p, timeoutMs = 3_000) {
2417
+ const deadline = Date.now() + timeoutMs;
2418
+ do {
2419
+ for (const entry of findAliveEcwebs(p)) {
2420
+ if (await probeEcwebReady(entry.port))
2421
+ return entry;
2422
+ }
2423
+ if (Date.now() >= deadline)
2424
+ break;
2425
+ await sleep(250);
2426
+ } while (true);
2427
+ return null;
2428
+ }
2425
2429
  /**
2426
2430
  * 输出 ECWeb 状态。判定口径:
2427
2431
  * 1. 进程是否存活(instance/ 下的 pid 文件 + isProcessRunning)
@@ -2633,10 +2637,21 @@ function ensureServiceProxyConfig(cfg, port) {
2633
2637
  * Kept public for restart-monitor, which launches the daemon directly and
2634
2638
  * therefore cannot rely on cmdStart's post-ready ECWeb startup path.
2635
2639
  */
2636
- export async function startEcwebIfEnabled(p) {
2640
+ export async function startEcwebIfEnabled(p, options = {}) {
2637
2641
  const cfg = loadDaemonConfig();
2638
2642
  if (!cfg.ecweb?.enabled)
2639
2643
  return false;
2644
+ // The daemon is the lifecycle owner. CLI callers normally only need to
2645
+ // ensure ECWeb is available, so adopt a healthy process instead of killing
2646
+ // it. `ec watch web` passes forceRestart explicitly for an intentional
2647
+ // manual replacement.
2648
+ if (!options.forceRestart) {
2649
+ const existing = await waitForExistingEcweb(p);
2650
+ if (existing) {
2651
+ console.log(`✓ ECWeb 已在运行 (PID: ${existing.pid}) http://localhost:${existing.port}`);
2652
+ return true;
2653
+ }
2654
+ }
2640
2655
  const installedPkg = resolveGlobalPkg(WEB_PACKAGE_NAME);
2641
2656
  if (!installedPkg) {
2642
2657
  console.log(`⚠ EC Web 未安装,跳过启动(运行 npm i -g ${WEB_PACKAGE_NAME}@latest 后可用)`);
@@ -2795,7 +2810,7 @@ async function cmdWatchWeb() {
2795
2810
  saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true, port } });
2796
2811
  // 自动配置 serviceProxy(如果控制 AID 已配置)
2797
2812
  ensureServiceProxyConfigBeforeStart(p);
2798
- const ok = await startEcwebIfEnabled(p);
2813
+ const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2799
2814
  if (!ok)
2800
2815
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
2801
2816
  // 启动成功的访问信息已由 startEcwebIfEnabled 打印,无需 printEcwebAccess 重复输出
@@ -6,6 +6,7 @@ import { agentmdGet, getAidStore, isValidAid, loadClient, SLOT } from '../aun/ai
6
6
  import { ipcQuery } from '../ipc.js';
7
7
  import { resolvePaths } from '../paths.js';
8
8
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
9
+ import { downloadGroupFsBytes } from '../aun/group-fs-download.js';
9
10
  class FsCliError extends Error {
10
11
  code;
11
12
  suggestion;
@@ -33,8 +34,8 @@ Commands:
33
34
  mkdir [-p] <AID>:<path> 创建目录
34
35
  ln -s <target> <AID>:<path> 创建软链(personal storage)
35
36
  chmod [mode] <AID>:<path> 切换公开/私有(personal storage)
36
- setfacl <AID>:<path> -m|-x ... 设置/移除 ACL(personal storage
37
- getfacl <AID>:<path> 查看 ACL(personal storage)
37
+ setfacl <AID>:<path> -m|-x ... 设置/移除 ACL(群路径使用 role:member|role:admin
38
+ getfacl <AID>:<path> 查看 ACL
38
39
  token issue|revoke|ls <path> 管理访问 token(personal storage)
39
40
  find <AID>:<path> [filters] 查找节点
40
41
  df <AID>: 查看容量/配额
@@ -257,6 +258,27 @@ async function runManagedFs(args, formatJson) {
257
258
  for (const item of result.items ?? [])
258
259
  console.log(JSON.stringify(item));
259
260
  return;
261
+ case 'mkdir':
262
+ console.log(`✓ 已创建目录: ${result.path}`);
263
+ return;
264
+ case 'upload':
265
+ console.log(`✓ 已上传: ${result.localPath} -> ${result.path}`);
266
+ return;
267
+ case 'copy':
268
+ console.log(`✓ 已复制: ${result.src} -> ${result.path}`);
269
+ return;
270
+ case 'move':
271
+ console.log(`✓ 已移动: ${result.src} -> ${result.path}`);
272
+ return;
273
+ case 'remove':
274
+ console.log(`✓ 已删除: ${result.path}`);
275
+ return;
276
+ case 'getfacl':
277
+ console.log(JSON.stringify(result.result ?? {}, null, 2));
278
+ return;
279
+ case 'setfacl':
280
+ console.log(`✓ 已${result.removed ? '移除' : '设置'}群 ACL: ${result.path}`);
281
+ return;
260
282
  }
261
283
  }
262
284
  function parseCommonOptions(args) {
@@ -385,8 +407,9 @@ async function fsCat(args, opts) {
385
407
  if (route.backend === 'group') {
386
408
  statInfo = await client.group.fs.stat(remoteRef(target));
387
409
  const size = numberFrom(statInfo, ['size', 'size_bytes', 'bytes']);
388
- const downloaded = await client.group.fs.cp(remoteRef(target), { kind: 'blob' }, { verifyHash: true });
389
- bytes = bytesFromDownload(downloaded);
410
+ const downloaded = await downloadGroupFsBytes(client, remoteRef(target));
411
+ bytes = downloaded.bytes;
412
+ statInfo = { ...statInfo, ...downloaded.ticket };
390
413
  statInfo = { ...statInfo, size: bytes.byteLength };
391
414
  if (size !== undefined && size > opts.maxCatBytes) {
392
415
  binaryHeadOnly = true;
@@ -821,10 +844,16 @@ async function fsSetfacl(args, opts) {
821
844
  }
822
845
  await withClient(opts, async ({ client, store }) => {
823
846
  const route = await resolveRoute(target.aid, store);
824
- if (route.backend === 'group') {
825
- throw new FsCliError('UNSUPPORTED', 'group fs 当前 SDK facade 没有 setfacl 接口', '请在 personal storage 中使用 setfacl。');
826
- }
827
847
  if (modify) {
848
+ if (route.backend === 'group') {
849
+ const [role, rolePerms] = parseGroupAclSpec(modify, true);
850
+ if (expiresAt !== undefined || maxUses !== undefined) {
851
+ throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 不支持 --expires 或 --max-uses');
852
+ }
853
+ const result = await client.group.fs.setAcl(remoteRef(target), { granteeAid: role, perms: rolePerms });
854
+ outputSuccess(opts, { command: 'setfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(`✓ 已设置群 ACL: ${remoteRef(target)}`));
855
+ return;
856
+ }
828
857
  const [grantee, perms] = parseAclSpec(modify, true);
829
858
  const result = await client.storage.setAcl(target.path, {
830
859
  owner: target.aid,
@@ -836,6 +865,12 @@ async function fsSetfacl(args, opts) {
836
865
  outputSuccess(opts, { command: 'setfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(`✓ 已设置 ACL: ${remoteRef(target)}`));
837
866
  return;
838
867
  }
868
+ if (route.backend === 'group') {
869
+ const [role] = parseGroupAclSpec(remove || '', false);
870
+ const result = await client.group.fs.removeAcl(remoteRef(target), { granteeAid: role });
871
+ outputSuccess(opts, { command: 'setfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(`✓ 已移除群 ACL: ${remoteRef(target)}`));
872
+ return;
873
+ }
839
874
  const [grantee] = parseAclSpec(remove || '', false);
840
875
  const result = await client.storage.removeAcl(target.path, {
841
876
  owner: target.aid,
@@ -848,10 +883,9 @@ async function fsGetfacl(args, opts) {
848
883
  const target = parseRemoteRequired(firstPositional(args), { command: 'getfacl' });
849
884
  await withClient(opts, async ({ client, store }) => {
850
885
  const route = await resolveRoute(target.aid, store);
851
- if (route.backend === 'group') {
852
- throw new FsCliError('UNSUPPORTED', 'group fs 当前 SDK facade 没有 getfacl/listAcl 接口', '请在 personal storage 中使用 getfacl。');
853
- }
854
- const result = await client.storage.listAcl(target.path, { owner: target.aid });
886
+ const result = route.backend === 'group'
887
+ ? await client.group.fs.getAcl(remoteRef(target))
888
+ : await client.storage.listAcl(target.path, { owner: target.aid });
855
889
  outputSuccess(opts, { command: 'getfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(JSON.stringify(result, null, 2)));
856
890
  });
857
891
  }
@@ -1288,6 +1322,26 @@ function parseAclSpec(value, requirePerms) {
1288
1322
  }
1289
1323
  return [parts[1]];
1290
1324
  }
1325
+ function parseGroupAclSpec(value, requirePerms) {
1326
+ const parts = value.split(':');
1327
+ if (parts[0] !== 'role' || !parts[1] || (parts.length !== (requirePerms ? 3 : 2))) {
1328
+ throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 条目格式应为 role:member:<perms>、role:admin:<perms> 或 role:<role>');
1329
+ }
1330
+ const role = `role:${parts[1]}`;
1331
+ if (!['role:member', 'role:admin'].includes(role)) {
1332
+ throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 仅支持 role:member 或 role:admin');
1333
+ }
1334
+ if (!requirePerms)
1335
+ return [role];
1336
+ const perms = parts[2];
1337
+ if (!perms || !/^(?:r|rw|rwx)$/.test(perms)) {
1338
+ throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 权限必须是 r、rw 或 rwx');
1339
+ }
1340
+ if (role === 'role:member' && perms !== 'rw') {
1341
+ throw new FsCliError('INVALID_ARGUMENT', 'role:member 只能授予 rw 群空间 ACL');
1342
+ }
1343
+ return [role, perms];
1344
+ }
1291
1345
  function assertLocalFile(localPath) {
1292
1346
  let st;
1293
1347
  try {
@@ -1420,14 +1474,6 @@ function contentTypeFrom(value) {
1420
1474
  const raw = value?.contentType ?? value?.content_type ?? value?.mime ?? value?.mime_type;
1421
1475
  return raw ? String(raw).toLowerCase() : undefined;
1422
1476
  }
1423
- function bytesFromDownload(value) {
1424
- const data = value?.data;
1425
- if (data instanceof Uint8Array)
1426
- return data;
1427
- if (data instanceof ArrayBuffer)
1428
- return new Uint8Array(data);
1429
- throw new FsCliError('BACKEND_ERROR', '下载接口未返回字节内容', undefined, value);
1430
- }
1431
1477
  function looksBinary(bytes, contentType) {
1432
1478
  if (contentType) {
1433
1479
  if (contentType.startsWith('text/'))
package/dist/cli/init.js CHANGED
@@ -7,7 +7,7 @@ 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
9
  import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
10
- import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
10
+ import { getCodexAppServerAvailability } from '../agents/codex-runner.js';
11
11
  import { resolveEcagentConfig } from '../agents/baseagent.js';
12
12
  import { defaultProjectsRoot } from '../utils/project-path.js';
13
13
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
@@ -28,15 +28,44 @@ export function expandLeadingTildePath(value, homeDir = os.homedir()) {
28
28
  }
29
29
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
30
30
  const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
31
- function isBaseagentAvailable(baseagent) {
31
+ function isBaseagentAvailable(baseagent, codexAvailability) {
32
32
  if (baseagent === 'codex')
33
- return isCodexAppServerAvailable();
33
+ return (codexAvailability ?? getCodexAppServerAvailability()).available;
34
34
  if (baseagent === 'ecagent')
35
35
  return true;
36
36
  return commandExists(baseagent);
37
37
  }
38
- function detectAvailable() {
39
- return BASEAGENT_CANDIDATES.filter(isBaseagentAvailable);
38
+ function detectAvailable(codexAvailability = getCodexAppServerAvailability()) {
39
+ return BASEAGENT_CANDIDATES.filter(baseagent => isBaseagentAvailable(baseagent, codexAvailability));
40
+ }
41
+ /** Offer installation only when the probe positively identified an old CLI. */
42
+ export async function maybeUpgradeCodexForInit(availability, dependencies) {
43
+ if (!availability.requiresUpgrade) {
44
+ return { availability, offered: false, accepted: false };
45
+ }
46
+ const accepted = await dependencies.confirm();
47
+ if (!accepted)
48
+ return { availability, offered: true, accepted: false };
49
+ try {
50
+ const install = dependencies.install ?? (async () => {
51
+ const { npmInstallGlobal } = await import('../utils/npm-ops.js');
52
+ await npmInstallGlobal('@openai/codex@latest');
53
+ });
54
+ await install();
55
+ }
56
+ catch (error) {
57
+ return {
58
+ availability,
59
+ offered: true,
60
+ accepted: true,
61
+ error: installErrorMessage(error),
62
+ };
63
+ }
64
+ return {
65
+ availability: (dependencies.probe ?? getCodexAppServerAvailability)(),
66
+ offered: true,
67
+ accepted: true,
68
+ };
40
69
  }
41
70
  function pickDefault(available) {
42
71
  return AUTO_BASEAGENT_CANDIDATES.find(candidate => available.includes(candidate));
@@ -192,7 +221,45 @@ export async function cmdInit(options) {
192
221
  return false;
193
222
  }
194
223
  // ── 2. 探测 baseagent ──
195
- const available = detectAvailable();
224
+ let codexAvailability = getCodexAppServerAvailability();
225
+ if (!options?.nonInteractive && codexAvailability.version) {
226
+ console.log(`检测到 Codex CLI: ${codexAvailability.version}`);
227
+ }
228
+ if (!options?.nonInteractive && codexAvailability.requiresUpgrade) {
229
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
230
+ let result;
231
+ try {
232
+ result = await maybeUpgradeCodexForInit(codexAvailability, {
233
+ confirm: async () => {
234
+ console.log(`⚠ ${codexAvailability.reason}`);
235
+ const answer = (await ask(rl, '是否立即升级 Codex CLI?[Y/n] ')).trim().toLowerCase();
236
+ return answer === '' || answer === 'y' || answer === 'yes';
237
+ },
238
+ });
239
+ }
240
+ finally {
241
+ try {
242
+ rl.close();
243
+ }
244
+ catch { /* ignore */ }
245
+ }
246
+ if (!result.accepted) {
247
+ console.log(' 已跳过 Codex CLI 升级');
248
+ }
249
+ else if (result.error) {
250
+ console.log(` ⚠ Codex CLI 升级失败: ${result.error}`);
251
+ }
252
+ else {
253
+ codexAvailability = result.availability;
254
+ if (codexAvailability.available) {
255
+ console.log(` ✓ Codex CLI 已升级至 ${codexAvailability.version}`);
256
+ }
257
+ else {
258
+ console.log(` ⚠ Codex CLI 升级后仍不可用: ${codexAvailability.reason ?? '未知原因'}`);
259
+ }
260
+ }
261
+ }
262
+ const available = detectAvailable(codexAvailability);
196
263
  if (available.length === 0 || (available.length === 1 && available[0] === 'ecagent' && ecagentUnavailableReason())) {
197
264
  console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
198
265
  console.log(' - claude CLI');
@@ -202,8 +269,10 @@ export async function cmdInit(options) {
202
269
  return false;
203
270
  }
204
271
  console.log('检测到可用的 baseagent:');
205
- for (const b of available)
206
- console.log(` ● ${b}`);
272
+ for (const b of available) {
273
+ const version = b === 'codex' ? codexAvailability.version : undefined;
274
+ console.log(` ● ${b}${version ? ` (CLI ${version})` : ''}`);
275
+ }
207
276
  console.log('');
208
277
  const defaultsPath = p.defaultsConfig;
209
278
  const defaultsExisted = fs.existsSync(defaultsPath);