evolcore 0.0.24 → 0.0.26

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 (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { resolvePaths } from '../../paths.js';
5
5
  export const REMOTE_ACCESS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
6
+ export const DIAGNOSTIC_TOKEN_TTL_MS = 60 * 60 * 1000;
6
7
  // 与 owner 绑定二维码(BindService)的默认有效期保持一致:10 分钟。
7
8
  export const REMOTE_ACCESS_PAIRING_TTL_MS = 10 * 60 * 1000;
8
9
  export function remoteAccessTokenStorePath() {
@@ -42,7 +43,8 @@ function pruneExpired(store, now) {
42
43
  const before = store.tokens.length;
43
44
  store.tokens = store.tokens.filter(record => typeof record?.token === 'string'
44
45
  && typeof record.lastActive === 'number'
45
- && now - record.lastActive < REMOTE_ACCESS_TOKEN_TTL_MS);
46
+ && Number.isFinite(record.lastActive)
47
+ && now - record.lastActive < (record.diagnostic ? DIAGNOSTIC_TOKEN_TTL_MS : REMOTE_ACCESS_TOKEN_TTL_MS));
46
48
  return store.tokens.length !== before;
47
49
  }
48
50
  function validLabel(value) {
@@ -85,6 +87,43 @@ export class RemoteAccessService {
85
87
  pairingCode = '';
86
88
  pairingExpiresAt = 0;
87
89
  pairingAuthorizedBy;
90
+ /** Synchronous reservation: concurrent invitations cannot issue two grants. */
91
+ issueDiagnosticToken(controlAid, agentAid, ownerAid, now = Date.now()) {
92
+ const store = loadStore();
93
+ pruneExpired(store, now);
94
+ if (store.tokens.some(record => record.diagnostic?.controlAid === controlAid)) {
95
+ throw Object.assign(new Error('本机已有有效诊断授权,请先执行 /diag stop 或 ec diag stop。'), { code: 'DIAG_ALREADY_ACTIVE' });
96
+ }
97
+ const record = {
98
+ token: crypto.randomBytes(32).toString('hex'), createdAt: now, lastActive: now,
99
+ label: 'diagnostic', authorizedBy: ownerAid,
100
+ diagnostic: { controlAid, agentAid, ownerAid, caseId: crypto.randomUUID() },
101
+ };
102
+ store.tokens.push(record);
103
+ saveStore(store);
104
+ return record;
105
+ }
106
+ getDiagnosticToken(controlAid, now = Date.now()) {
107
+ const store = loadStore();
108
+ if (pruneExpired(store, now))
109
+ saveStore(store);
110
+ return store.tokens.find(record => record.diagnostic?.controlAid === controlAid);
111
+ }
112
+ /** Expected token prevents a failed older send from revoking a newer grant. */
113
+ revokeDiagnosticToken(controlAid, expectedToken) {
114
+ const store = loadStore();
115
+ const pruned = pruneExpired(store, Date.now());
116
+ const index = store.tokens.findIndex(record => record.diagnostic?.controlAid === controlAid
117
+ && (expectedToken === undefined || record.token === expectedToken));
118
+ if (index < 0) {
119
+ if (pruned)
120
+ saveStore(store);
121
+ return undefined;
122
+ }
123
+ const [record] = store.tokens.splice(index, 1);
124
+ saveStore(store);
125
+ return record;
126
+ }
88
127
  getPairingCode(authorizedBy, now = Date.now()) {
89
128
  if (!this.pairingCode || now >= this.pairingExpiresAt) {
90
129
  this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
@@ -369,7 +369,7 @@ export class BootstrapService {
369
369
  const content = existing.trim() || buildInitialAgentMd({
370
370
  aid,
371
371
  name: fallbackName,
372
- type: 'codeagent',
372
+ type: 'agent',
373
373
  });
374
374
  try {
375
375
  await adapter.uploadAgentMd(content);
@@ -6,11 +6,11 @@ import path from 'path';
6
6
  import { ConfigTarget, ensureFile as cfgEnsure, read as cfgRead, write as cfgWrite, } from '../../config/config-manager.js';
7
7
  import { CreateStatusWriter, readCreateStatus } from '../message/create-status.js';
8
8
  import { agentProjectRootFromDefaults, deriveAgentProjectPath } from '../../utils/project-path.js';
9
- import { uploadAvatar } from '../../utils/avatar-upload.js';
9
+ import { uploadAvatar, uploadAvatarUrl } from '../../utils/avatar-upload.js';
10
10
  import { agentmdGet, agentmdPut, updateAgentMdFrontmatterName } from '../../aun/aid/agentmd.js';
11
11
  import { isValidAid } from '../../aun/aid/validation.js';
12
12
  import { isSupportedBaseagent } from '../../agents/baseagent.js';
13
- const SUPPORTED_AGENT_PATCH_FIELDS = new Set(['aid', 'name', 'avatar', 'active_baseagent', 'baseagents', 'projects', 'owners', 'chatmode', 'channels', 'channelOwners']);
13
+ const SUPPORTED_AGENT_PATCH_FIELDS = new Set(['aid', 'name', 'active_baseagent', 'baseagents', 'projects', 'owners', 'chatmode', 'channels', 'channelOwners']);
14
14
  const HIDDEN_VALUE = '[hidden]';
15
15
  const SENSITIVE_CONFIG_KEYS = new Set([
16
16
  'apikey',
@@ -48,6 +48,42 @@ function classifyAgentMdError(error) {
48
48
  return 'INVALID_ARGS';
49
49
  return 'UPLOAD_FAILED';
50
50
  }
51
+ /** Canonical avatar mutation for the `aid.avatar` menu action. */
52
+ export async function execAidAvatar(args) {
53
+ const a = args ?? {};
54
+ const aid = typeof a.aid === 'string' ? a.aid.trim().replace(/^@/, '') : '';
55
+ if (!aid)
56
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
57
+ if (!isValidAid(aid))
58
+ return { error: `无效 aid: ${aid}`, code: 'INVALID_ARGS' };
59
+ const hasData = a.data !== undefined;
60
+ const hasUrl = a.url !== undefined;
61
+ if ((hasData ? 1 : 0) + (hasUrl ? 1 : 0) > 1) {
62
+ return { error: '头像数据源必须在 data 与 url 中二选一', code: 'INVALID_ARGS' };
63
+ }
64
+ if (!hasData && !hasUrl) {
65
+ return { error: '缺少头像数据源:需要 data 或 url', code: 'INVALID_ARGS' };
66
+ }
67
+ const result = hasUrl
68
+ ? await uploadAvatarUrl(aid, a.url, { aunPath: resolveRoot() })
69
+ : await uploadAvatar(aid, a.data, { aunPath: resolveRoot() });
70
+ if (!result.ok) {
71
+ return { error: `头像上传失败: ${result.error}`, code: result.code ?? 'UPLOAD_FAILED' };
72
+ }
73
+ return {
74
+ data: {
75
+ aid,
76
+ avatar: result.publicUrl,
77
+ objectKey: result.objectKey,
78
+ sha256: result.sha256,
79
+ mime: result.mime,
80
+ width: result.width,
81
+ height: result.height,
82
+ aspectRatio: result.aspectRatio,
83
+ saved: true,
84
+ },
85
+ };
86
+ }
51
87
  async function updateAgentMdName(aid, value) {
52
88
  const name = typeof value === 'string' ? value.trim() : '';
53
89
  if (!name)
@@ -299,6 +335,7 @@ async function runCreateInBackground(opts, w, service) {
299
335
  aid: opts.aid, name: opts.name, baseagent: opts.baseagent,
300
336
  project: opts.project, owner: opts.owner,
301
337
  description: opts.description,
338
+ tags: opts.tags,
302
339
  onPhase: (phase, state, detail) => {
303
340
  if (state === 'begin') {
304
341
  curPhase = phase;
@@ -398,6 +435,7 @@ export async function execAgentAction(action, args, peerId, eventBus, service) {
398
435
  const description = typeof a.description === 'string' && a.description.trim()
399
436
  ? a.description.trim()
400
437
  : undefined;
438
+ const tags = Array.isArray(a.tags) ? a.tags : undefined;
401
439
  // D3: accepted=true 必须意味着后续 query 能查到该 AID 的创建状态。
402
440
  let progressWriter;
403
441
  try {
@@ -410,7 +448,7 @@ export async function execAgentAction(action, args, peerId, eventBus, service) {
410
448
  void runCreateInBackground({
411
449
  aid: a.aid, name: a.name, baseagent: a.baseagent,
412
450
  project: a.project, owner,
413
- model, effort, description, chatmode: a.chatmode,
451
+ model, effort, description, tags, chatmode: a.chatmode,
414
452
  eventBus,
415
453
  }, progressWriter, service).catch(e => logger.error(`[agent-control] runCreateInBackground unhandled ${a.aid}: ${e?.message || e}`));
416
454
  return { data: { accepted: true, aid: a.aid } };
@@ -461,14 +499,6 @@ export async function execAgentAction(action, args, peerId, eventBus, service) {
461
499
  if (action === 'update') {
462
500
  const result = await execAgentUpdate(a);
463
501
  if (!('error' in result)) {
464
- if (result.data?.avatar) {
465
- eventBus?.publish({
466
- type: 'agent:avatar_updated',
467
- aid: a.aid,
468
- avatar: result.data.avatar,
469
- timestamp: Date.now(),
470
- });
471
- }
472
502
  eventBus?.publish({
473
503
  type: 'agent:updated',
474
504
  aid: a.aid,
@@ -501,12 +531,6 @@ export async function execAgentUpdate(args) {
501
531
  if (p.aid !== undefined) {
502
532
  return { error: 'aid 不可修改(AUN 身份绑定,如需换 AID 请删除后重建)', code: 'INVALID_ARGS' };
503
533
  }
504
- if (p.avatar !== undefined) {
505
- const otherKeys = Object.keys(p).filter(k => k !== 'avatar');
506
- if (otherKeys.length > 0) {
507
- return { error: 'avatar 不能与其他 patch 字段同时提交,请拆分为两次 update', code: 'INVALID_ARGS' };
508
- }
509
- }
510
534
  if (p.name !== undefined) {
511
535
  const otherKeys = Object.keys(p).filter(k => k !== 'name');
512
536
  if (otherKeys.length > 0) {
@@ -522,13 +546,6 @@ export async function execAgentUpdate(args) {
522
546
  const config = cfgRead(ConfigTarget.Agent, { self: a.aid });
523
547
  if (!config)
524
548
  return { error: `Agent "${a.aid}" not found`, code: 'NOT_FOUND' };
525
- if (p.avatar !== undefined) {
526
- const result = await uploadAvatar(a.aid, p.avatar, { aunPath: resolveRoot() });
527
- if (!result.ok) {
528
- return { error: `头像上传失败: ${result.error}`, code: result.code ?? 'UPLOAD_FAILED' };
529
- }
530
- return { data: { aid: a.aid, avatar: result.publicUrl, saved: true } };
531
- }
532
549
  let touched = false;
533
550
  const data = { aid: a.aid };
534
551
  const hasConfigPatch = p.active_baseagent !== undefined || p.baseagents !== undefined || p.projects !== undefined || p.owners !== undefined || p.chatmode !== undefined || p.channels !== undefined || p.channelOwners !== undefined;
@@ -564,7 +581,19 @@ export async function execAgentUpdate(args) {
564
581
  delete config.projects;
565
582
  }
566
583
  if (p.owners !== undefined) {
567
- config.owners = p.owners;
584
+ if (!Array.isArray(p.owners) || p.owners.length === 0) {
585
+ return { error: 'owners must contain at least one owner', code: 'LAST_OWNER_PROTECTED' };
586
+ }
587
+ const owners = [];
588
+ for (const rawOwner of p.owners) {
589
+ if (typeof rawOwner !== 'string' || !rawOwner.trim() || !isValidAid(rawOwner.trim())) {
590
+ return { error: `Invalid owner AID: ${JSON.stringify(rawOwner)}`, code: 'INVALID_ARGS' };
591
+ }
592
+ const owner = rawOwner.trim();
593
+ if (!owners.includes(owner))
594
+ owners.push(owner);
595
+ }
596
+ config.owners = owners;
568
597
  touched = true;
569
598
  }
570
599
  if (p.chatmode !== undefined) {
@@ -156,6 +156,14 @@ function parseStatsCommand(argv, source) {
156
156
  }
157
157
  function parseAgentCommand(argv, source) {
158
158
  const subcmd = argv[1];
159
+ if (subcmd === 'persona') {
160
+ const tail = argv.at(-2) === '--format' && argv.at(-1) === 'json' ? argv.slice(0, -2) : argv;
161
+ const isShow = tail.length === 3 && tail[2] === 'show';
162
+ const isWrite = tail.length === 4 && (tail[2] === '--text' || tail[2] === '--file');
163
+ return isShow || isWrite
164
+ ? recognized('agent.persona', 'agent', source, {}, argv)
165
+ : raw(source, argv, 'Invalid agent persona command');
166
+ }
159
167
  const parsed = parseArgs(argv.slice(2));
160
168
  const args = parsed.args;
161
169
  if (parsed.positionals.length > 0)
@@ -170,6 +178,16 @@ function parseAgentCommand(argv, source) {
170
178
  }
171
179
  function parseAidCommand(argv, source) {
172
180
  const subcmd = argv[1];
181
+ if (subcmd === 'avatar') {
182
+ const tail = argv.slice(2);
183
+ const aid = tail[0];
184
+ const sourceFlag = tail[1];
185
+ if (!aid || (sourceFlag !== '--file' && sourceFlag !== '--url') || !tail[2] || tail[2].startsWith('-')
186
+ || (tail.length !== 3 && !(tail.length === 5 && tail[3] === '--format' && tail[4] === 'json'))) {
187
+ return raw(source, argv, 'Invalid aid avatar command');
188
+ }
189
+ return recognized('ec.aid.avatar', 'agent', source, { aid, ...(sourceFlag === '--file' ? { file: tail[2] } : { url: tail[2] }) }, argv, true);
190
+ }
173
191
  const parsed = parseArgs(argv.slice(2));
174
192
  const args = parsed.args;
175
193
  if (parsed.positionals.length > 0)
@@ -1,4 +1,5 @@
1
1
  import { BaseagentRunnerUnavailableError } from '../../agents/runner-types.js';
2
+ import { parseMenuClientArgs } from '../../cli/menu-client.js';
2
3
  import { renderCommandCardAsText } from '../interaction-router.js';
3
4
  import { buildEnvelope, deliveryTargetFromSession, isInteractionSendAccepted, replyContextFromSession, sendInteractionPayload } from '../message/message-utils.js';
4
5
  import { resolvePaths, getPackageRoot } from '../../paths.js';
@@ -17,7 +18,7 @@ import { tryParseChannelKey } from '../channel-loader.js';
17
18
  import { displaySessionTitle } from '../session/session-title.js';
18
19
  import { buildSessionTurnList } from '../session/session-turns.js';
19
20
  import { resolveConfigCommand } from '../../config/resolved-config-op.js';
20
- import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
21
+ import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommandAsync } from '../../config/config-operation-service.js';
21
22
  import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
22
23
  import { auditCommandAuthorization, auditFullAccessEvent, hashArgv } from '../auth/authorization-audit.js';
23
24
  import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
@@ -123,6 +124,43 @@ export class CommandHandler {
123
124
  daemonMonitorProvider;
124
125
  agentDelegationRegistry;
125
126
  handoffRuntime;
127
+ diagService;
128
+ setDiagService(service) { this.diagService = service; }
129
+ async handleMenuClientOperation(argv, sessionId, delegationToken, delegationCommandHash, send) {
130
+ const validation = this.agentDelegationRegistry?.validate(delegationToken, sessionId, delegationCommandHash);
131
+ if (!validation?.ok)
132
+ return { ok: false, code: validation?.code ?? 'DELEGATION_REQUIRED', error: validation?.reason ?? 'Task delegation is required' };
133
+ const grant = validation.grant;
134
+ const session = await this.sessionManager.getSessionById(sessionId);
135
+ if (!session || session.selfAID !== grant.selfAid || argv[0] !== 'request' || argv[1] !== grant.selfAid)
136
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Menu requests must use the current Agent identity and session' };
137
+ try {
138
+ const channelId = parsePeerKey(grant.peerKey).channelId;
139
+ const subject = buildAuthSubject({ selfAid: grant.selfAid, actorId: grant.actorId, channel: grant.channel,
140
+ channelType: grant.channelType, channelId, chatType: grant.chatType, conversationId: channelId,
141
+ processOwners: [], ...trustedExecutionIdentityInput(grant) });
142
+ const decision = authorizeOperation({ source: 'agent-tool', subject,
143
+ intent: { operation: 'ec.menu.request', scope: 'relation', source: 'agent-tool', args: { self: grant.selfAid, peer: argv[2], targetId: argv[2] } },
144
+ auditMetadata: { taskId: grant.taskId, sessionId, messageId: grant.messageId } });
145
+ if (!decision.allow)
146
+ return { ok: false, code: decision.code, error: decision.reason };
147
+ const options = parseMenuClientArgs(argv, file => {
148
+ const target = fs.realpathSync(path.resolve(session.projectPath, file));
149
+ const roots = [session.projectPath, this.getManagedSessionTempDir(session)].filter((v) => !!v)
150
+ .map(root => fs.realpathSync(root));
151
+ if (!roots.some(root => isSameOrDescendant(target, root)) || isHClassPath(target) || isLClassPath(target))
152
+ throw new Error('Menu request file must be an unprotected file in the project or session TMPDIR');
153
+ const stat = fs.statSync(target);
154
+ if (!stat.isFile() || stat.nlink > 1 || stat.size > 128 * 1024)
155
+ throw new Error('Menu request file must be a regular file of at most 128 KiB');
156
+ return fs.readFileSync(target, 'utf8');
157
+ });
158
+ return { ok: true, response: await send(options) };
159
+ }
160
+ catch (error) {
161
+ return { ok: false, code: error.code ?? 'MENU_REQUEST_FAILED', error: error.message };
162
+ }
163
+ }
126
164
  contactRequestSubmitter;
127
165
  /**
128
166
  * Get the runner for a (channel, baseagent) pair.
@@ -983,13 +1021,14 @@ export class CommandHandler {
983
1021
  getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
984
1022
  return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
985
1023
  }
986
- async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent') {
1024
+ async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent', paircodeAllowed) {
987
1025
  return await getMenuCatalog({
988
1026
  subject,
989
1027
  channelType: this.resolveChannelType(channel),
990
1028
  channelScope,
991
1029
  channelId,
992
1030
  request,
1031
+ ...(paircodeAllowed === undefined ? {} : { paircodeAllowed }),
993
1032
  });
994
1033
  }
995
1034
  async execMenuValueBatch(channel, channelId, subject, args, request, channelScope = 'agent') {
@@ -1176,9 +1215,9 @@ export class CommandHandler {
1176
1215
  /**
1177
1216
  * 主命令处理入口
1178
1217
  */
1179
- async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
1218
+ async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext, enforceGroupSlashPolicy = false) {
1180
1219
  try {
1181
- const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
1220
+ const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext, enforceGroupSlashPolicy);
1182
1221
  // Lifecycle commands are control-plane operations. Their replies must
1183
1222
  // use notification payloads so other agents in a group do not treat the
1184
1223
  // text as ordinary model input. Normalize guard-path replies here too,
@@ -1210,8 +1249,10 @@ export class CommandHandler {
1210
1249
  throw error;
1211
1250
  }
1212
1251
  }
1213
- async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
1214
- return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
1252
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext,
1253
+ // Menu and delegated CLI operations reuse this implementation without the chat-only policy.
1254
+ applyGroupSlashPolicy = false) {
1255
+ return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext, applyGroupSlashPolicy);
1215
1256
  }
1216
1257
  async handleTriggerCommand(scheduler, content, channel, channelId, peerId, isAdmin, messageId, chatType, threadId, isDaemonOwner = false, canApprovePersistentFullAccess = isDaemonOwner) {
1217
1258
  if (content === '/trigger') {
@@ -1677,8 +1718,9 @@ export class CommandHandler {
1677
1718
  buildCtlReplyContext(session) {
1678
1719
  const ctx = { delivery: deliveryTargetFromSession(session) };
1679
1720
  const meta = session.metadata;
1680
- if (meta?.replyContext?.threadId)
1681
- ctx.threadId = meta.replyContext.threadId;
1721
+ const threadId = session.threadId || meta?.replyContext?.threadId;
1722
+ if (threadId)
1723
+ ctx.threadId = threadId;
1682
1724
  if (meta?.replyContext?.peerId)
1683
1725
  ctx.peerId = meta.replyContext.peerId;
1684
1726
  if (!ctx.peerId && meta?.peerId)
@@ -1759,7 +1801,7 @@ export class CommandHandler {
1759
1801
  processOwners: loadDaemonConfig().owners ?? [],
1760
1802
  ...trustedExecutionIdentityInput(grant),
1761
1803
  });
1762
- const executeOne = (fieldArgv) => {
1804
+ const executeOne = async (fieldArgv) => {
1763
1805
  const resolved = resolveConfigCommand(fieldArgv, {
1764
1806
  defaultRelation: { self: grant.selfAid, peerKey: grant.peerKey },
1765
1807
  });
@@ -1780,6 +1822,7 @@ export class CommandHandler {
1780
1822
  dangerous: resolved.command.dangerous,
1781
1823
  },
1782
1824
  resolvedConfigCommand: resolved.command,
1825
+ allowExplicitRelationTarget: true,
1783
1826
  auditMetadata: {
1784
1827
  argvHash: hashArgv(resolved.command.canonicalArgv),
1785
1828
  taskId: grant.taskId,
@@ -1788,22 +1831,35 @@ export class CommandHandler {
1788
1831
  });
1789
1832
  if (!decision.allow)
1790
1833
  return { ok: false, code: decision.code, error: decision.reason };
1791
- return executeResolvedConfigCommand(resolved.command, { role: subject.role });
1834
+ const roleContext = {
1835
+ self: grant.selfAid,
1836
+ actorRole: subject.role,
1837
+ actorRoleSource: subject.roleSource,
1838
+ actorAllowAccess: subject.allowAccess,
1839
+ actorAid: grant.actorId,
1840
+ isDaemonOwner: subject.isDaemonOwner,
1841
+ fromControlChannel: subject.fromControlChannel,
1842
+ chatType: grant.chatType,
1843
+ };
1844
+ return await executeResolvedConfigCommandAsync(resolved.command, {
1845
+ role: subject.role,
1846
+ roleContext,
1847
+ });
1792
1848
  };
1793
1849
  const batch = splitConfigBatchGetArgv(argv);
1794
1850
  if (batch && batch.fields.length > 1) {
1795
- const results = batch.fields.map(field => {
1796
- const result = executeOne(['config', 'get', field, ...batch.rest]);
1851
+ const results = await Promise.all(batch.fields.map(async (field) => {
1852
+ const result = await executeOne(['config', 'get', field, ...batch.rest]);
1797
1853
  if (!result.ok)
1798
1854
  return { ok: false, field, code: result.code, error: result.error };
1799
1855
  if (result.subcommand !== 'get') {
1800
1856
  return { ok: false, field, code: 'UNEXPECTED_RESULT', error: `config get ${field} produced a non-get result` };
1801
1857
  }
1802
1858
  return result;
1803
- });
1859
+ }));
1804
1860
  return { ok: true, result: { ok: true, subcommand: 'batch-get', results } };
1805
1861
  }
1806
- const result = executeOne(argv);
1862
+ const result = await executeOne(argv);
1807
1863
  return result.ok
1808
1864
  ? { ok: true, result }
1809
1865
  : { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
@@ -1827,7 +1883,8 @@ export class CommandHandler {
1827
1883
  catch {
1828
1884
  return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1829
1885
  }
1830
- const resolved = resolveContactCommand(argv);
1886
+ const resolvedArgv = argv.includes('--self') ? argv : [...argv, '--self', grant.selfAid];
1887
+ const resolved = resolveContactCommand(resolvedArgv);
1831
1888
  if (!resolved.ok)
1832
1889
  return { ok: false, code: resolved.code, error: resolved.reason };
1833
1890
  if (resolved.command.selfAid !== grant.selfAid && !hasTrustedFullAccessDelegation(grant)) {
@@ -1946,10 +2003,10 @@ export class CommandHandler {
1946
2003
  if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1947
2004
  return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1948
2005
  }
1949
- const requiresSelfAid = parsed.command.kind === 'show' || parsed.command.kind === 'agentmd.put';
2006
+ const requiresSelfAid = parsed.command.kind === 'show' || parsed.command.kind === 'agentmd.put' || parsed.command.kind === 'avatar';
1950
2007
  if (requiresSelfAid
1951
2008
  && parsed.command.aid !== grantSelfAid
1952
- && !hasTrustedFullAccessDelegation(delegation.grant)) {
2009
+ && (parsed.command.kind === 'avatar' || !hasTrustedFullAccessDelegation(delegation.grant))) {
1953
2010
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can be targeted' };
1954
2011
  }
1955
2012
  let conversationId;
@@ -1988,12 +2045,87 @@ export class CommandHandler {
1988
2045
  });
1989
2046
  if (!decision.allow)
1990
2047
  return { ok: false, code: decision.code, error: decision.reason };
2048
+ let avatarBuffer;
2049
+ if (parsed.command.kind === 'avatar' && parsed.command.filePath) {
2050
+ const filePath = String(parsed.command.filePath || '').trim();
2051
+ if (typeof session.projectPath !== 'string' || !session.projectPath.trim()) {
2052
+ return { ok: false, code: 'INVALID_AVATAR_PATH', error: 'Avatar file requires a current session project path' };
2053
+ }
2054
+ const projectPath = path.resolve(session.projectPath);
2055
+ const managedTempPath = this.getManagedSessionTempDir(session);
2056
+ const resolvedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(projectPath, filePath);
2057
+ const allowedRoots = [projectPath, managedTempPath].filter((value) => !!value);
2058
+ if (!filePath || !allowedRoots.some(root => isSameOrDescendant(resolvedPath, root))) {
2059
+ return { ok: false, code: 'INVALID_AVATAR_PATH', error: 'Avatar file must be inside the current session project or managed TMPDIR' };
2060
+ }
2061
+ if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
2062
+ return { ok: false, code: 'PROTECTED_AVATAR_PATH', error: 'Avatar file cannot be read from a protected EvolCore path' };
2063
+ }
2064
+ let fd;
2065
+ try {
2066
+ const realProjectPath = fs.realpathSync(projectPath);
2067
+ const realPath = fs.realpathSync(resolvedPath);
2068
+ let realManagedTempPath;
2069
+ try {
2070
+ realManagedTempPath = managedTempPath ? fs.realpathSync(managedTempPath) : undefined;
2071
+ }
2072
+ catch { }
2073
+ const realAllowedRoots = [realProjectPath, realManagedTempPath].filter((value) => !!value);
2074
+ if (!realAllowedRoots.some(root => isSameOrDescendant(realPath, root))) {
2075
+ return { ok: false, code: 'INVALID_AVATAR_PATH', error: 'Avatar file resolves outside the current session project or managed TMPDIR' };
2076
+ }
2077
+ if (isHClassPath(realPath) || isLClassPath(realPath)) {
2078
+ return { ok: false, code: 'PROTECTED_AVATAR_PATH', error: 'Avatar file cannot be read from a protected EvolCore path' };
2079
+ }
2080
+ const stat = fs.lstatSync(resolvedPath);
2081
+ if (stat.isSymbolicLink() || !stat.isFile()) {
2082
+ return { ok: false, code: 'INVALID_AVATAR_FILE', error: 'Avatar source must be a regular non-symlink file' };
2083
+ }
2084
+ if (realManagedTempPath && isSameOrDescendant(realPath, realManagedTempPath) && stat.nlink > 1) {
2085
+ return { ok: false, code: 'INVALID_AVATAR_PATH', error: 'Avatar source cannot be a hard link inside managed TMPDIR' };
2086
+ }
2087
+ if (stat.size < 1 || stat.size > 500 * 1024) {
2088
+ return { ok: false, code: 'INVALID_AVATAR_FILE', error: 'Avatar source must be between 1 byte and 500KB' };
2089
+ }
2090
+ // Open the user-supplied node with no-follow semantics after checking
2091
+ // its canonical ancestors; the bytes passed to the uploader come from
2092
+ // this descriptor rather than from a second path lookup.
2093
+ fd = fs.openSync(resolvedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2094
+ const finalStat = fs.fstatSync(fd);
2095
+ if (!finalStat.isFile() || finalStat.size < 1 || finalStat.size > 500 * 1024 || finalStat.nlink !== stat.nlink) {
2096
+ return { ok: false, code: 'INVALID_AVATAR_FILE', error: 'Avatar source changed during validation' };
2097
+ }
2098
+ avatarBuffer = fs.readFileSync(fd);
2099
+ }
2100
+ catch (error) {
2101
+ return { ok: false, code: 'INVALID_AVATAR_FILE', error: error instanceof Error ? error.message : String(error) };
2102
+ }
2103
+ finally {
2104
+ if (fd !== undefined)
2105
+ fs.closeSync(fd);
2106
+ }
2107
+ }
1991
2108
  try {
2109
+ const result = await executeManagedAidOperation(parsed.command, {
2110
+ showView: managedAidShowViewForRole(subject.role),
2111
+ ...(avatarBuffer ? { avatarBuffer } : {}),
2112
+ });
2113
+ if (result.kind === 'avatar' && result.avatar) {
2114
+ this.eventBus.publish({
2115
+ type: 'agent:avatar_updated',
2116
+ aid: result.aid,
2117
+ avatar: result.avatar,
2118
+ ...(result.sha256 ? { sha256: result.sha256 } : {}),
2119
+ ...(result.mime ? { mime: result.mime } : {}),
2120
+ ...(result.width ? { width: result.width } : {}),
2121
+ ...(result.height ? { height: result.height } : {}),
2122
+ ...(result.aspectRatio ? { aspectRatio: result.aspectRatio } : {}),
2123
+ timestamp: Date.now(),
2124
+ });
2125
+ }
1992
2126
  return {
1993
2127
  ok: true,
1994
- result: await executeManagedAidOperation(parsed.command, {
1995
- showView: managedAidShowViewForRole(subject.role),
1996
- }),
2128
+ result,
1997
2129
  };
1998
2130
  }
1999
2131
  catch (error) {
@@ -0,0 +1,118 @@
1
+ import { isValidAid } from '../../aun/aid/validation.js';
2
+ import { remoteAccessService } from '../auth/remote-access-store.js';
3
+ export const DIAGNOSTIC_AGENT_AID = 'diag.agentid.cn';
4
+ /** IPC verifies local control authority; managed calls must independently prove their actor. */
5
+ export async function executeDiagIpc(service, registry, owners, cmd) {
6
+ if (cmd.sessionId !== undefined) {
7
+ if (typeof cmd.sessionId !== 'string' || !cmd.sessionId)
8
+ return { ok: false, code: 'INVALID_SESSION', message: '无效托管会话' };
9
+ const auth = registry.validate(cmd.delegationToken, cmd.sessionId, cmd.delegationCommandHash);
10
+ if (!auth.ok)
11
+ return { ok: false, code: auth.code, message: auth.reason };
12
+ if (auth.grant.channelType !== 'aun' || !owners.includes(auth.grant.actorId))
13
+ return { ok: false, code: 'DAEMON_OWNER_REQUIRED', message: '托管诊断发起需要本机 AUN DaemonOwner' };
14
+ }
15
+ try {
16
+ if (!Array.isArray(cmd.argv) || cmd.argv.some((v) => typeof v !== 'string'))
17
+ throw new Error('Invalid diag argv');
18
+ return await service.execute(parseDiagArgs(cmd.argv));
19
+ }
20
+ catch (error) {
21
+ return { ok: false, code: error.code ?? 'INVALID_ARGUMENT', message: error.message };
22
+ }
23
+ }
24
+ export function parseDiagArgs(args) {
25
+ const words = [];
26
+ let ownerAid;
27
+ let literal = false;
28
+ for (let i = 0; i < args.length; i++) {
29
+ const word = args[i];
30
+ if (!literal && word === '--') {
31
+ literal = true;
32
+ continue;
33
+ }
34
+ if (!literal && word === '--owner') {
35
+ if (ownerAid || !args[i + 1] || !isValidAid(args[i + 1]))
36
+ throw new Error('--owner 需要一个有效 AID');
37
+ ownerAid = args[++i];
38
+ }
39
+ else if (!literal && word.startsWith('--'))
40
+ throw new Error(`不支持的参数:${word}`);
41
+ else
42
+ words.push(word);
43
+ }
44
+ const problem = words.join(' ').trim();
45
+ if (problem.toLowerCase() === 'stop') {
46
+ if (ownerAid)
47
+ throw new Error('diag stop 不需要指定对象');
48
+ return { action: 'stop' };
49
+ }
50
+ if (!problem || problem.length > 16_000)
51
+ throw new Error('请提供 1 到 16000 字符的问题描述');
52
+ return { action: 'start', problem, ...(ownerAid ? { ownerAid } : {}) };
53
+ }
54
+ export class DiagService {
55
+ context;
56
+ send;
57
+ tokens;
58
+ constructor(context, send, tokens = remoteAccessService) {
59
+ this.context = context;
60
+ this.send = send;
61
+ this.tokens = tokens;
62
+ }
63
+ /** Local CLI authority is checked by IPC; slash identity is authenticated AUN peerId. */
64
+ async execute(command, actorAid) {
65
+ const { controlAid, owners } = this.context();
66
+ if (!controlAid || !isValidAid(controlAid))
67
+ return { ok: false, code: 'CONTROL_AID_REQUIRED', message: '本机尚未配置 Control AID' };
68
+ if (actorAid !== undefined && !owners.includes(actorAid))
69
+ return { ok: false, code: 'DAEMON_OWNER_REQUIRED', message: '只有本机 DaemonOwner 可以发起或停止诊断' };
70
+ if (command.action === 'stop') {
71
+ const previous = this.tokens.revokeDiagnosticToken(controlAid);
72
+ if (!previous)
73
+ return { ok: true, message: '当前没有诊断授权' };
74
+ const diag = previous.diagnostic;
75
+ try {
76
+ await this.send(diag.agentAid, `诊断授权已取消。目标 daemon:${controlAid};case:${diag.caseId};沟通 Owner:${diag.ownerAid}。请停止这次诊断的后续访问,并告知 Owner。`);
77
+ return { ok: true, message: '诊断授权已撤销,已通知诊断 Agent' };
78
+ }
79
+ catch {
80
+ return { ok: true, code: 'DIAG_STOP_NOTICE_FAILED', message: '诊断授权已撤销;取消通知发送失败,诊断 Agent 后续访问将被拒绝' };
81
+ }
82
+ }
83
+ const ownerAid = actorAid ?? command.ownerAid ?? owners[0];
84
+ if (!ownerAid || !isValidAid(ownerAid) || !owners.includes(ownerAid))
85
+ return { ok: false, code: 'DAEMON_OWNER_REQUIRED', message: '请配置本机 owners,或指定其中一个有效 Owner AID' };
86
+ if (!command.problem?.trim() || command.problem.length > 16_000)
87
+ return { ok: false, code: 'INVALID_ARGUMENT', message: '请提供 1 到 16000 字符的问题描述' };
88
+ let record;
89
+ try {
90
+ record = this.tokens.issueDiagnosticToken(controlAid, DIAGNOSTIC_AGENT_AID, ownerAid);
91
+ }
92
+ catch (error) {
93
+ return { ok: false, code: error.code ?? 'DIAG_AUTH_FAILED', message: error.message };
94
+ }
95
+ const caseId = record.diagnostic.caseId;
96
+ const invitation = [
97
+ '请协助诊断以下主机,并主动联系指定 Owner 展示进展、追问和报告。',
98
+ `目标 daemon Control AID:${controlAid}`, `沟通 Owner AID:${ownerAid}`, `case:${caseId}`,
99
+ `远程访问 Token:${record.token}`,
100
+ '请通过标准 Menu 调用诊断;Token 放入 E2EE 受保护头。权限复用该主机既有 Control Menu,包含 restart。有效访问滑动续期,空闲 60 分钟失效。',
101
+ '先给 Owner 发开始消息,使用通用 handoff 保留目标、Token、问题和发现;无需等待 Owner 回复即可开始。授权失效后停止访问。',
102
+ `问题描述:\n${command.problem.trim()}`,
103
+ ].join('\n');
104
+ try {
105
+ await this.send(DIAGNOSTIC_AGENT_AID, invitation);
106
+ }
107
+ catch (error) {
108
+ this.tokens.revokeDiagnosticToken(controlAid, record.token);
109
+ const reason = error instanceof Error ? error.message : String(error);
110
+ return { ok: false, code: 'DIAG_INVITATION_FAILED', message: `未能确认诊断邀请发送成功:${reason}。本次授权已撤销,请重试` };
111
+ }
112
+ // A stop may race with delivery; never report an already revoked grant as active.
113
+ if (this.tokens.getDiagnosticToken(controlAid)?.token !== record.token)
114
+ return { ok: false, code: 'DIAG_CANCELLED', message: '邀请发送期间授权已撤销,本次诊断已取消' };
115
+ return { ok: true, caseId, ownerAid, agentAid: DIAGNOSTIC_AGENT_AID,
116
+ message: `诊断邀请已发出,远程诊断 Agent 将联系 ${ownerAid},请在与 ${DIAGNOSTIC_AGENT_AID} 的会话中查看处理过程。` };
117
+ }
118
+ }