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
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import { getArgValue, wantsHelp } from './help.js';
3
+ import { executeMenuClientRequest, MenuClientError } from '../aun/menu-client.js';
4
+ function parseDurationMs(value) {
5
+ if (value === undefined)
6
+ return undefined;
7
+ const match = value.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/i);
8
+ if (!match)
9
+ throw new MenuClientError('INVALID_ARGUMENT', `Invalid timeout: ${value}`);
10
+ const amount = Number(match[1]);
11
+ const unit = match[2].toLowerCase();
12
+ return amount * (unit === 'ms' ? 1 : unit === 's' ? 1_000 : unit === 'm' ? 60_000 : 3_600_000);
13
+ }
14
+ function readRequest(args) {
15
+ const file = getArgValue(args, '--request-file');
16
+ const inline = getArgValue(args, '--request-json');
17
+ if (!!file === !!inline)
18
+ throw new MenuClientError('INVALID_ARGUMENT', '--request-file and --request-json are mutually exclusive');
19
+ let raw;
20
+ try {
21
+ raw = file ? fs.readFileSync(file, 'utf8') : inline;
22
+ }
23
+ catch (error) {
24
+ throw new MenuClientError('INVALID_ARGUMENT', `Unable to read request: ${error?.message ?? String(error)}`);
25
+ }
26
+ try {
27
+ const parsed = JSON.parse(raw);
28
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
29
+ throw new Error('request must be a JSON object');
30
+ return parsed;
31
+ }
32
+ catch (error) {
33
+ throw new MenuClientError('INVALID_ARGUMENT', `Invalid request JSON: ${error?.message ?? String(error)}`);
34
+ }
35
+ }
36
+ function validateOptions(args) {
37
+ const valueOptions = new Set(['--request-file', '--request-json', '--timeout', '--remote-token', '--ecweb-token', '--app', '--aun-path', '--format']);
38
+ const booleanOptions = new Set(['--encrypt', '--no-encrypt']);
39
+ for (let index = 0; index < args.length; index += 1) {
40
+ const option = args[index];
41
+ if (booleanOptions.has(option))
42
+ continue;
43
+ if (!valueOptions.has(option))
44
+ throw new MenuClientError('INVALID_ARGUMENT', `Unknown menu-client option: ${option}`);
45
+ if (index + 1 >= args.length || args[index + 1].startsWith('--')) {
46
+ throw new MenuClientError('INVALID_ARGUMENT', `${option} requires a value`);
47
+ }
48
+ index += 1;
49
+ }
50
+ if (args.includes('--encrypt') && args.includes('--no-encrypt')) {
51
+ throw new MenuClientError('INVALID_ARGUMENT', '--encrypt and --no-encrypt are mutually exclusive');
52
+ }
53
+ const format = getArgValue(args, '--format');
54
+ if (format !== undefined && format !== 'json')
55
+ throw new MenuClientError('INVALID_ARGUMENT', '--format currently supports only json');
56
+ }
57
+ export async function cmdMenuClient(args) {
58
+ if (wantsHelp(args)) {
59
+ console.log(`用法: ec menu-client request <from-aid> <daemon-aid> [选项]
60
+
61
+ --request-file <path> 从 JSON 文件读取一个 menu.* 请求
62
+ --request-json <json> 直接传入一个 menu.* JSON 请求
63
+ --timeout <时长> 默认 15s,最大 120s
64
+ --encrypt 使用 E2EE(默认)
65
+ --no-encrypt 已拒绝:ec-menu-client 必须使用 E2EE
66
+ --remote-token <token> 使用 daemon 远程访问 token 临时授权
67
+ --ecweb-token <token> --remote-token 的兼容别名
68
+ --app <slot> 覆盖默认 evolcore-menu-client 消费槽位
69
+ --aun-path <path> 覆盖本地 AUN keystore 根目录
70
+ --format json 输出标准 menu.response JSON`);
71
+ return;
72
+ }
73
+ if (args[0] !== 'request')
74
+ throw new MenuClientError('INVALID_ARGUMENT', 'menu-client currently supports only the request subcommand');
75
+ const fromAid = args[1]?.trim();
76
+ const targetAid = args[2]?.trim();
77
+ if (!fromAid || !targetAid)
78
+ throw new MenuClientError('INVALID_ARGUMENT', 'request requires <from-aid> and <daemon-aid>');
79
+ const options = args.slice(3);
80
+ validateOptions(options);
81
+ const request = readRequest(options);
82
+ const timeoutMs = parseDurationMs(getArgValue(args, '--timeout'));
83
+ const encrypt = args.includes('--no-encrypt') ? false : true;
84
+ const response = await executeMenuClientRequest({
85
+ fromAid,
86
+ targetAid,
87
+ request,
88
+ timeoutMs,
89
+ encrypt,
90
+ remoteAccessToken: getArgValue(args, '--remote-token') ?? getArgValue(args, '--ecweb-token'),
91
+ slotId: getArgValue(args, '--app'),
92
+ aunPath: getArgValue(args, '--aun-path'),
93
+ });
94
+ console.log(JSON.stringify(response, null, 2));
95
+ }
package/dist/cli/model.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
14
14
  import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, writeScope, clearScope, } from '../core/model/config-scope.js';
15
15
  import { resolveEffectiveFieldWithSource } from '../config/config-manager.js';
16
- import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
16
+ import { getCatalog, getModelInfo, catalogEfforts, resolveCatalogModel, assertCatalogSelectable } from '../core/model/model-catalog.js';
17
17
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole, } from '../core/model/model-permission.js';
18
18
  import { resolvePaths } from '../paths.js';
19
19
  import { ipcQuery } from '../ipc.js';
@@ -23,7 +23,7 @@ import { getFieldPermission } from '../config/roles.js';
23
23
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
24
24
  import { formatModelCheck, runModelCheck } from '../core/model/model-diagnostics.js';
25
25
  import { validateRuntimeStringFieldOverride } from '../core/role/runtime-policy.js';
26
- const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
26
+ const ALL_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'auto'];
27
27
  const SCOPE_LABEL = {
28
28
  global: '全局', agent: 'agent级', role: '角色级', relation: '关系级',
29
29
  };
@@ -298,7 +298,7 @@ async function cmdList(args, formatJson) {
298
298
  const cat = await getCatalog(sel.self, ba);
299
299
  const resolved = resolveModelConfig(effectiveSel, ba);
300
300
  const allowedModelIds = role
301
- ? filterModelsForRole(role, ba, cat.models.map(m => m.id), undefined, sel.self)
301
+ ? filterModelsForRole(role, ba, cat.models.map(m => m.id), model => resolveCatalogModel(cat, model), sel.self)
302
302
  : null;
303
303
  const models = allowedModelIds
304
304
  ? allowedModelIds.map(id => cat.models.find(m => m.id === id) ?? { id })
@@ -308,15 +308,21 @@ async function cmdList(args, formatJson) {
308
308
  effective: { model: resolved.model ?? null, source: resolved.source ?? null },
309
309
  role: role ?? null,
310
310
  catalogSource: cat.source,
311
+ freshness: cat.freshness,
312
+ revision: cat.revision,
313
+ fetchedAt: cat.fetchedAt,
314
+ diagnostics: cat.diagnostics,
311
315
  models,
312
316
  }, () => {
313
317
  const lines = [];
314
318
  lines.push(`当前生效: ${resolved.model ?? '(未设置,回落 SDK 默认)'}` +
315
319
  (resolved.source ? ` (来源: ${SCOPE_LABEL[resolved.source]})` : ''));
316
320
  lines.push('');
317
- const srcTag = cat.source === 'mock' ? ' [mock]'
318
- : cat.source === 'remote' ? ' [remote]'
319
- : '';
321
+ if (resolved.model && !cat.models.some(m => m.id === resolved.model))
322
+ lines.push('当前配置未列入目录');
323
+ lines.push(...cat.diagnostics.map(d => d.message));
324
+ const srcTag = cat.freshness === 'fallback' ? ` [${cat.source}/unverified]`
325
+ : cat.freshness === 'stale' ? ' [stale]' : '';
320
326
  lines.push(`可用模型 (${models.length})${srcTag}:`);
321
327
  for (const e of models) {
322
328
  const live = resolved.model === e.id ? '✓' : ' ';
@@ -430,6 +436,7 @@ async function cmdUse(args, formatJson) {
430
436
  const ba = activeBaseagent(sel.self);
431
437
  // 校验模型在 catalog 中
432
438
  const cat = await getCatalog(sel.self, ba);
439
+ assertCatalogSelectable(cat);
433
440
  if (!cat.models.some(m => m.id === modelId)) {
434
441
  fail(formatJson, 'UNKNOWN_MODEL', `模型不在目录中: ${modelId}(model list 查看可用模型)`);
435
442
  }
@@ -439,14 +446,15 @@ async function cmdUse(args, formatJson) {
439
446
  baseagent: ba,
440
447
  requestedModel: modelId,
441
448
  models: cat.models.map(m => m.id),
449
+ resolveModelId: model => resolveCatalogModel(cat, model),
442
450
  selfAid: sel.self,
443
451
  });
444
452
  if (!decision.ok)
445
453
  fail(formatJson, decision.code || 'MODEL_NOT_ALLOWED', decision.message || `model not allowed: ${modelId}`);
446
454
  }
447
455
  const effort = getArgValue(args, '--effort');
448
- if (effort !== undefined && !ALL_EFFORTS.includes(effort)) {
449
- fail(formatJson, 'INVALID_EFFORT', `无效推理强度: ${effort}(${ALL_EFFORTS.join('/')})`);
456
+ if (effort !== undefined && effort !== 'auto' && !catalogEfforts(cat, modelId, ba).includes(effort)) {
457
+ fail(formatJson, 'INVALID_EFFORT', `当前模型目录未声明支持推理强度: ${effort}`);
450
458
  }
451
459
  const effortVal = effort === 'auto' ? null : (effort ?? undefined);
452
460
  if (isManagedTargetedWriteEnv(args)) {
@@ -492,6 +500,13 @@ async function cmdEffort(args, formatJson) {
492
500
  const scope = determineWriteScope(sel, formatJson);
493
501
  const ba = activeBaseagent(sel.self);
494
502
  const val = level === 'auto' ? null : level;
503
+ if (val) {
504
+ const cat = await getCatalog(sel.self, ba);
505
+ assertCatalogSelectable(cat);
506
+ const model = resolveModelConfig(selectorWithRole(sel, explicitOrDerivedRole(args, sel)), ba).model;
507
+ if (!model || !catalogEfforts(cat, model, ba).includes(val))
508
+ fail(formatJson, 'INVALID_EFFORT', `当前模型目录未声明支持推理强度: ${val}`);
509
+ }
495
510
  if (isManagedTargetedWriteEnv(args)) {
496
511
  const effortKey = 'effort';
497
512
  preflightManagedFieldOverride(sel, explicitOrDerivedRole(args, sel), `baseagents.${ba}.${effortKey}`, val, formatJson);
@@ -253,8 +253,18 @@ export function getManagedTaskTempDir(runtimeEnv) {
253
253
  * silently escape the session-owned temporary workspace.
254
254
  */
255
255
  export function ensureSessionRuntimeDir(sessionId) {
256
- const base = process.env.TMPDIR?.trim();
257
- if (!base || !path.isAbsolute(base) || !sessionId)
256
+ if (!sessionId)
257
+ return undefined;
258
+ let base = process.env.TMPDIR?.trim();
259
+ try {
260
+ if (!base || !path.isAbsolute(base) || !isPrivateDirectory(path.resolve(base))) {
261
+ base = ensureProcessManagedTempDir();
262
+ }
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ if (!base || !path.isAbsolute(base))
258
268
  return undefined;
259
269
  const root = path.resolve(base);
260
270
  const digest = crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
@@ -121,12 +121,20 @@ Create 参数模式支持:
121
121
  通用输出参数:
122
122
  --json | --format json
123
123
 
124
- Update 仅支持常用字段:
124
+ Update 参数:
125
125
  --once | --delay | --at | --cron | --every | --event [--tz]
126
126
  --prompt <文本> | --prompt-file <路径> --script-file <路径> --name <名称>
127
+ --description <文本> (空字符串删除)
127
128
  --model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
128
129
  --max-runs <次数> --max-duration <时长>
129
130
  --concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
131
+ --feedback <origin|target|silent>
132
+ --target-channel <channelKey> --target-channel-id <ID> (必须同时指定)
133
+ --target-chat-type <private|group> --target-session <main|thread> [--target-thread-id <threadId>]
134
+ --trigger-thread <by-run|by-trigger> --baseagent <name> (仅 trigger_session 之间)
135
+ --on-error <fail|retry> --noop-sentinel <文本>
136
+ --retry-attempts <次数> --retry-backoff <毫秒>
137
+ --script-runtime <node|python|bash> --script-args <JSON> --script-timeout <时长>
130
138
 
131
139
  省略 --agent 时使用当前 agent AID;无会话上下文或跨身份操作时需要显式指定。
132
140
  任务会话中的跨身份操作仅允许目标 Agent 上具备 owner/admin 权限的调用者执行 list/show/history;创建、修改、启停、删除和运行仍只允许当前 Agent。`);
@@ -342,8 +342,20 @@ function processIsAlive(pid) {
342
342
  function assertDaemonStopped(home) {
343
343
  const candidates = [];
344
344
  const pidFile = path.join(home, 'daemon.pid');
345
- if (fs.existsSync(pidFile)) {
346
- candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
345
+ try {
346
+ // daemon.pid was retired in favor of the instance registry. It may still
347
+ // exist as a stale legacy file, but a Windows Codex sandbox can also leave
348
+ // a directory at this path when protecting a not-yet-created file. Never
349
+ // feed a directory (or symlink) to readFileSync: that turns harmless
350
+ // legacy residue into an EISDIR startup failure.
351
+ const stat = fs.lstatSync(pidFile);
352
+ if (stat.isFile()) {
353
+ candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
354
+ }
355
+ }
356
+ catch (error) {
357
+ if (!isRecord(error) || error.code !== 'ENOENT')
358
+ throw error;
347
359
  }
348
360
  const instanceDir = path.join(home, 'data', 'instance');
349
361
  if (fs.existsSync(instanceDir)) {
@@ -1,6 +1,7 @@
1
1
  import crypto from 'crypto';
2
2
  import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
3
3
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
4
+ import { logger } from '../../utils/logger.js';
4
5
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
5
6
  export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
6
7
  export class AgentDelegationRegistry {
@@ -43,17 +44,19 @@ export class AgentDelegationRegistry {
43
44
  : undefined;
44
45
  if (approved)
45
46
  return { ok: true, grant: approved };
47
+ const diagnosticCause = grant
48
+ ? grant.sessionId !== sessionId
49
+ ? 'session_mismatch'
50
+ : 'task_replaced'
51
+ : commandHash
52
+ ? 'approved_command_unavailable'
53
+ : 'unknown_token';
54
+ logger.warn(`[AgentDelegation] validate failed: cause=${diagnosticCause} session=${sessionId} grantSession=${grant?.sessionId ?? 'none'} taskId=${grant?.taskId ?? 'none'} hasCommandHash=${!!commandHash} tokenKnown=${!!grant} activeForSession=${this.activeHashBySession.get(sessionId) ?? 'none'} grantTokenHash=${grant?.tokenHash ?? 'none'}`);
46
55
  return {
47
56
  ok: false,
48
57
  code: 'INVALID_DELEGATION',
49
58
  reason: 'The task delegation token is invalid, revoked, or belongs to another session',
50
- diagnosticCause: grant
51
- ? grant.sessionId !== sessionId
52
- ? 'session_mismatch'
53
- : 'task_replaced'
54
- : commandHash
55
- ? 'approved_command_unavailable'
56
- : 'unknown_token',
59
+ diagnosticCause,
57
60
  };
58
61
  }
59
62
  armApprovedCommand(input) {
@@ -185,6 +185,9 @@ export function auditCodexApprovalDecision(input) {
185
185
  taskId: input.taskId,
186
186
  argsSummary: {
187
187
  method: input.method,
188
+ ...(input.summary ? { summary: input.summary } : {}),
189
+ ...(input.threadId ? { threadId: input.threadId } : {}),
190
+ ...(input.cwd ? { cwd: input.cwd } : {}),
188
191
  ...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
189
192
  },
190
193
  });
@@ -218,6 +221,7 @@ export function auditPermissionDecision(input) {
218
221
  dangerous: false,
219
222
  role: input.role ?? 'unknown',
220
223
  taskId: input.taskId,
224
+ argsSummary: input.summary ? { summary: input.summary } : undefined,
221
225
  decision: 'deny',
222
226
  executed: false,
223
227
  executionState: 'blocked',
@@ -38,6 +38,10 @@ const GROUP_MANAGEMENT_OPERATIONS = [
38
38
  ['ec.group.join', 'write-agent', ['agent'], 'Request to join a group'],
39
39
  ['ec.group.leave', 'write-agent', ['agent'], 'Leave a group'],
40
40
  ['ec.group.invite', 'write-agent', ['agent'], 'Invite group members'],
41
+ ['ec.group.invites', 'read', ['agent'], 'List pending targeted group invitations'],
42
+ ['ec.group.accept-invite', 'write-agent', ['agent'], 'Accept a targeted group invitation'],
43
+ ['ec.group.reject-invite', 'write-agent', ['agent'], 'Reject a targeted group invitation'],
44
+ ['ec.group.revoke-invite', 'write-agent', ['agent'], 'Revoke a targeted group invitation'],
41
45
  ['ec.group.kick', 'write-agent', ['agent'], 'Remove a group member'],
42
46
  ['ec.group.role', 'write-agent', ['agent'], 'Change a group member role'],
43
47
  ['ec.group.owner', 'write-agent', ['agent'], 'Transfer group ownership'],
@@ -0,0 +1,135 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { resolvePaths } from '../../paths.js';
5
+ export const REMOTE_ACCESS_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
6
+ export const REMOTE_ACCESS_PAIRING_TTL_MS = 5 * 60 * 1000;
7
+ export function remoteAccessTokenStorePath() {
8
+ return path.join(resolvePaths().instanceDir, 'control-access-tokens.json');
9
+ }
10
+ function loadStore() {
11
+ try {
12
+ const value = JSON.parse(fs.readFileSync(remoteAccessTokenStorePath(), 'utf8'));
13
+ if (Array.isArray(value.tokens))
14
+ return value;
15
+ }
16
+ catch { }
17
+ return { tokens: [] };
18
+ }
19
+ function saveStore(store) {
20
+ const file = remoteAccessTokenStorePath();
21
+ fs.mkdirSync(path.dirname(file), { recursive: true });
22
+ const temporary = `${file}.${process.pid}.tmp`;
23
+ try {
24
+ fs.writeFileSync(temporary, JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
25
+ fs.renameSync(temporary, file);
26
+ try {
27
+ fs.chmodSync(file, 0o600);
28
+ }
29
+ catch { }
30
+ }
31
+ finally {
32
+ if (fs.existsSync(temporary)) {
33
+ try {
34
+ fs.unlinkSync(temporary);
35
+ }
36
+ catch { }
37
+ }
38
+ }
39
+ }
40
+ function pruneExpired(store, now) {
41
+ const before = store.tokens.length;
42
+ store.tokens = store.tokens.filter(record => typeof record?.token === 'string'
43
+ && typeof record.lastActive === 'number'
44
+ && now - record.lastActive < REMOTE_ACCESS_TOKEN_TTL_MS);
45
+ return store.tokens.length !== before;
46
+ }
47
+ function validLabel(value) {
48
+ if (typeof value !== 'string')
49
+ return 'remote';
50
+ const normalized = value.trim().replace(/[\r\n]/g, ' ').slice(0, 256);
51
+ return normalized || 'remote';
52
+ }
53
+ function validOwner(value) {
54
+ if (typeof value !== 'string')
55
+ return undefined;
56
+ const normalized = value.trim();
57
+ if (!normalized || normalized.length > 512 || /[\r\n]/.test(normalized))
58
+ return undefined;
59
+ return normalized;
60
+ }
61
+ /** Validate and slide the expiry window of a daemon-owned remote token. */
62
+ export function validateAndRenewRemoteAccessToken(token, now = Date.now()) {
63
+ if (typeof token !== 'string' || !token || token.length > 512)
64
+ return { valid: false };
65
+ try {
66
+ const store = loadStore();
67
+ const changed = pruneExpired(store, now);
68
+ const record = store.tokens.find(candidate => candidate.token === token);
69
+ if (!record) {
70
+ if (changed)
71
+ saveStore(store);
72
+ return { valid: false };
73
+ }
74
+ record.lastActive = now;
75
+ saveStore(store);
76
+ const authorizedBy = validOwner(record.authorizedBy);
77
+ return { valid: true, ...(authorizedBy ? { authorizedBy } : {}) };
78
+ }
79
+ catch {
80
+ return { valid: false };
81
+ }
82
+ }
83
+ export class RemoteAccessService {
84
+ pairingCode = '';
85
+ pairingExpiresAt = 0;
86
+ pairingAuthorizedBy;
87
+ getPairingCode(authorizedBy, now = Date.now()) {
88
+ if (!this.pairingCode || now >= this.pairingExpiresAt) {
89
+ this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
90
+ this.pairingExpiresAt = now + REMOTE_ACCESS_PAIRING_TTL_MS;
91
+ this.pairingAuthorizedBy = undefined;
92
+ }
93
+ const owner = validOwner(authorizedBy);
94
+ if (!this.pairingAuthorizedBy && owner)
95
+ this.pairingAuthorizedBy = owner;
96
+ return { code: this.pairingCode, expiresAt: this.pairingExpiresAt };
97
+ }
98
+ redeemPairingCode(code, label, authorizedBy, now = Date.now()) {
99
+ if (typeof code !== 'string' || !/^\d{6}$/.test(code)) {
100
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'pairing code must be six digits' };
101
+ }
102
+ if (!this.pairingCode || now >= this.pairingExpiresAt) {
103
+ return { ok: false, code: 'PAIRING_EXPIRED', error: 'pairing code has expired' };
104
+ }
105
+ if (code !== this.pairingCode) {
106
+ return { ok: false, code: 'PAIRING_INVALID', error: 'pairing code is invalid' };
107
+ }
108
+ const createdAt = now;
109
+ const token = crypto.randomBytes(32).toString('hex');
110
+ const store = loadStore();
111
+ pruneExpired(store, now);
112
+ const owner = validOwner(authorizedBy) ?? this.pairingAuthorizedBy;
113
+ store.tokens.push({
114
+ token,
115
+ createdAt,
116
+ lastActive: createdAt,
117
+ label: validLabel(label),
118
+ ...(owner ? { authorizedBy: owner } : {}),
119
+ });
120
+ saveStore(store);
121
+ return { ok: true, token, expiresAt: createdAt + REMOTE_ACCESS_TOKEN_TTL_MS };
122
+ }
123
+ issueLocalToken(label, now = Date.now()) {
124
+ const token = crypto.randomBytes(32).toString('hex');
125
+ const store = loadStore();
126
+ pruneExpired(store, now);
127
+ store.tokens.push({ token, createdAt: now, lastActive: now, label: validLabel(label) });
128
+ saveStore(store);
129
+ return { ok: true, token, expiresAt: now + REMOTE_ACCESS_TOKEN_TTL_MS };
130
+ }
131
+ validateToken(token, now = Date.now()) {
132
+ return validateAndRenewRemoteAccessToken(token, now);
133
+ }
134
+ }
135
+ export const remoteAccessService = new RemoteAccessService();
@@ -114,7 +114,12 @@ export class CommandHandler {
114
114
  triggerSchedulerResolver;
115
115
  triggerSchedulerFallback;
116
116
  triggerSchedulerFallbackAid;
117
+ /** Runtime-only owner for the daemon trigger scheduler. It is intentionally
118
+ * kept outside EvolAgentRegistry because the daemon channel is not an Agent. */
119
+ daemonTriggerOwnerAid;
120
+ daemonTriggerChannel = 'daemon';
117
121
  daemonStatusProvider;
122
+ daemonMonitorProvider;
118
123
  agentDelegationRegistry;
119
124
  handoffRuntime;
120
125
  contactRequestSubmitter;
@@ -219,6 +224,32 @@ export class CommandHandler {
219
224
  setTriggerSchedulerResolver(resolver) {
220
225
  this.triggerSchedulerResolver = resolver;
221
226
  }
227
+ /** Bind the virtual daemon channel to its trigger scheduler owner AID. */
228
+ setDaemonTriggerOwner(aid, channel = 'daemon') {
229
+ const normalizedAid = String(aid || '').trim();
230
+ if (!normalizedAid)
231
+ throw new Error('daemon trigger owner AID is required');
232
+ this.daemonTriggerOwnerAid = normalizedAid;
233
+ this.daemonTriggerChannel = String(channel || 'daemon').trim() || 'daemon';
234
+ }
235
+ /** Used by control-menu routing; daemon owner is not in the Agent registry. */
236
+ getDaemonTriggerOwnerAid(channel = 'daemon') {
237
+ if (channel !== this.daemonTriggerChannel)
238
+ return undefined;
239
+ if (this.daemonTriggerOwnerAid)
240
+ return this.daemonTriggerOwnerAid;
241
+ try {
242
+ const controlAid = String(loadDaemonConfig().aid || '__daemon__').trim() || '__daemon__';
243
+ const existing = this.agentRegistry?.get?.(controlAid);
244
+ // Match index.ts: only a runnable (pre-start `stopped`) Agent causes
245
+ // the daemon scheduler owner to receive the collision suffix.
246
+ const conflicts = !!existing && (existing.status === undefined || existing.status === 'stopped');
247
+ return conflicts ? `${controlAid}#daemon` : controlAid;
248
+ }
249
+ catch {
250
+ return undefined;
251
+ }
252
+ }
222
253
  /** Compatibility entrypoint for older tests/callers that inject a single scheduler. */
223
254
  setTriggerScheduler(scheduler, manager) {
224
255
  this.triggerSchedulerFallback = scheduler;
@@ -233,6 +264,9 @@ export class CommandHandler {
233
264
  setDaemonStatusProvider(provider) {
234
265
  this.daemonStatusProvider = provider;
235
266
  }
267
+ setDaemonMonitorProvider(provider) {
268
+ this.daemonMonitorProvider = provider;
269
+ }
236
270
  /** 返回管理当前通道的 EvolAgent,无则返回 null */
237
271
  getOwningAgent(channel) {
238
272
  if (!this.agentRegistry)
@@ -240,7 +274,9 @@ export class CommandHandler {
240
274
  return this.agentRegistry.resolveByChannel(channel);
241
275
  }
242
276
  getTriggerSchedulerForChannel(channel) {
243
- const agentAid = this.getOwningAgent(channel)?.aid ?? tryParseChannelKey(channel)?.selfAID;
277
+ const agentAid = this.getOwningAgent(channel)?.aid
278
+ ?? this.getDaemonTriggerOwnerAid(channel)
279
+ ?? tryParseChannelKey(channel)?.selfAID;
244
280
  return agentAid ? this.triggerSchedulerResolver?.(agentAid) : this.triggerSchedulerFallback;
245
281
  }
246
282
  getTriggerSchedulerForAgent(agentAid) {
@@ -975,9 +1011,9 @@ export class CommandHandler {
975
1011
  async execMenuForEcweb(payload, auth) {
976
1012
  return await menuExecMenuForEcweb.call(this, payload, auth);
977
1013
  }
978
- /** 控制 AID channel 专用入口:peerId 须 ∈ evolcore.owners,全量权限。 */
979
- async execMenuForControl(payload, peerId) {
980
- return await menuExecMenuForControl.call(this, payload, peerId);
1014
+ /** Control AID Menu entry: daemon-owner or validated remote-access token. */
1015
+ async execMenuForControl(payload, peerId, auth) {
1016
+ return await menuExecMenuForControl.call(this, payload, peerId, auth);
981
1017
  }
982
1018
  /**
983
1019
  * CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
@@ -1344,7 +1380,7 @@ export class CommandHandler {
1344
1380
  /trigger list [--all] — 查看所有触发器
1345
1381
  /trigger create <参数> — 创建触发器(set 为兼容别名)
1346
1382
  /trigger update <名称|ID> <参数> — 修改触发器
1347
- 常用参数:--exec script|trigger-session|target-session(create 必填)、--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
1383
+ 常用参数:--delay/--at/--cron/--every、--prompt、--description、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)、--feedback、--target-channel/--target-channel-id、--trigger-thread、--baseagent、--on-error、--retry-attempts/--retry-backoff、--script-runtime/--script-args/--script-timeout
1348
1384
  /trigger enable <名称|ID> — 启用触发器
1349
1385
  /trigger disable <名称|ID> — 暂停触发器
1350
1386
  /trigger show <名称|ID> — 查看触发器详情