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.
- package/CHANGELOG.md +59 -0
- package/bin/ec.js +12 -1
- package/bin/install-codex-managed-hooks.mjs +28 -1
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +54 -13
- package/dist/agents/codex/app-server-client.js +13 -5
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +128 -42
- package/dist/agents/ecagent-runner.js +14 -18
- package/dist/agents/gemini-runner.js +24 -9
- package/dist/agents/runner-types.js +25 -17
- package/dist/aun/aid/agentmd.js +135 -10
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +53 -1
- package/dist/aun/aid/store.js +17 -13
- package/dist/aun/menu-client.js +74 -0
- package/dist/aun/msg/group.js +1 -1
- package/dist/aun/msg/managed-operation.js +1 -1
- package/dist/aun/msg/p2p.js +6 -1
- package/dist/aun/msg/upload.js +30 -22
- package/dist/aun/storage/client.js +4 -1
- package/dist/aun/storage/index.js +1 -1
- package/dist/channels/aun.js +329 -94
- package/dist/cli/agent-command.js +125 -5
- package/dist/cli/agent-create.js +151 -11
- package/dist/cli/agent.js +119 -6
- package/dist/cli/aun-commands.js +74 -7
- package/dist/cli/config.js +27 -8
- package/dist/cli/contact.js +63 -6
- package/dist/cli/daemon-commands.js +90 -25
- package/dist/cli/diag.js +31 -0
- package/dist/cli/handoff-command.js +35 -3
- package/dist/cli/index.js +7 -1
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +38 -23
- package/dist/cli/restart-monitor.js +57 -10
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +15 -0
- package/dist/config/config-operation-service.js +165 -3
- package/dist/config/contact-operation-service.js +96 -7
- package/dist/config/resolved-config-op.js +66 -8
- package/dist/core/agent-application-service.js +2 -1
- package/dist/core/auth/authorization-audit.js +20 -12
- package/dist/core/auth/operation-authorizer.js +87 -8
- package/dist/core/auth/operation-catalog.js +51 -4
- package/dist/core/auth/remote-access-store.js +40 -1
- package/dist/core/bootstrap-service.js +1 -1
- package/dist/core/command/agent-control.js +54 -25
- package/dist/core/command/cli-intent-parser.js +18 -0
- package/dist/core/command/command-handler.js +152 -20
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/menu-catalog.js +13 -3
- package/dist/core/command/menu-handler.js +88 -31
- package/dist/core/command/menu-protocol.js +6 -0
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +77 -12
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +3 -3
- package/dist/core/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/message/im-renderer.js +72 -21
- package/dist/core/message/message-bridge.js +69 -9
- package/dist/core/message/message-queue.js +26 -0
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +48 -23
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/ec-command-parser.js +51 -12
- package/dist/core/permission/sandbox-runtime.js +36 -14
- package/dist/core/permission/tool-policy.js +47 -7
- package/dist/index.js +195 -31
- package/dist/ipc.js +54 -2
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/price-resolver.js +28 -10
- package/dist/utils/avatar-upload.js +282 -23
- package/dist/utils/cross-platform.js +30 -1
- package/dist/utils/error-utils.js +21 -0
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +3 -1
- package/dist/utils/model-prices.jsonl +3 -3
- package/dist/utils/npm-ops.js +176 -35
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +3 -1
- package/kits/docs/evolcore/agent-create.md +30 -11
- package/kits/docs/evolcore/agent.md +18 -8
- package/kits/docs/evolcore/aid.md +6 -4
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
- package/kits/docs/evolcore/config.md +7 -0
- package/kits/docs/evolcore/contact.md +12 -5
- package/kits/docs/evolcore/menu-client.md +11 -3
- package/kits/docs/evolcore/msg.md +1 -0
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/docs/venues/group.md +12 -0
- package/kits/eck_manifest.json +12 -0
- package/kits/rules/03-identity.md +3 -1
- package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
- package/kits/templates/roles/admin.json +14 -2
- package/kits/templates/roles/member.json +3 -2
- package/kits/templates/roles/visitor.json +4 -3
- package/kits/templates/system-fragments/bootstrap.md +16 -5
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +6 -4
- package/skills/eclink/SKILL.md +0 -84
- 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
|
-
&&
|
|
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');
|
|
@@ -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', '
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|