evolcore 0.0.11 → 0.0.13
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 +44 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -2
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +243 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/command/evol-menu-version-gate.js +0 -39
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseagentRunnerUnavailableError } from '../../agents/runner-types.js';
|
|
2
2
|
import { renderCommandCardAsText } from '../interaction-router.js';
|
|
3
|
-
import { buildEnvelope, sendInteractionPayload } from '../message/message-utils.js';
|
|
3
|
+
import { buildEnvelope, deliveryTargetFromSession, replyContextFromSession, sendInteractionPayload } from '../message/message-utils.js';
|
|
4
4
|
import { resolvePaths, getPackageRoot } from '../../paths.js';
|
|
5
5
|
import { loadDaemonConfig } from '../../config-store.js';
|
|
6
6
|
import { logger } from '../../utils/logger.js';
|
|
@@ -9,6 +9,7 @@ import { isManagementRole } from '../../config/builtin-roles.js';
|
|
|
9
9
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
10
10
|
import crypto from 'crypto';
|
|
11
11
|
import path from 'path';
|
|
12
|
+
import fs from 'fs';
|
|
12
13
|
import { parseDuration, parseTriggerSet, parseTriggerUpdate } from '../../trigger/parser.js';
|
|
13
14
|
import { applyTriggerPatch, replaceEditableTriggerDefinition } from '../../trigger/patch.js';
|
|
14
15
|
import { definitionRevision } from '../../trigger/validation.js';
|
|
@@ -17,16 +18,22 @@ import { displaySessionTitle } from '../session/session-title.js';
|
|
|
17
18
|
import { buildSessionTurnList } from '../session/session-turns.js';
|
|
18
19
|
import { resolveConfigCommand } from '../../config/resolved-config-op.js';
|
|
19
20
|
import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
|
|
21
|
+
import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
|
|
20
22
|
import { hashArgv } from '../auth/authorization-audit.js';
|
|
21
23
|
import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
|
|
24
|
+
import { executeManagedAidOperation, ManagedAidOperationError, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
|
|
25
|
+
import { executeManagedGroupOperation, isManagedGroupRelationRead, ManagedGroupOperationError, resolveManagedGroupOperation, executeManagedMsgOperation, ManagedMsgOperationError, resolveManagedMsgOperation, executeManagedFsOperation, ManagedFsOperationError, resolveManagedFsOperation, } from '../../aun/msg/managed-operation.js';
|
|
22
26
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
27
|
+
import { isHClassPath, isLClassPath, isSameOrDescendant } from '../protected-paths.js';
|
|
23
28
|
import { validateModelSelectionForRole } from '../model/model-permission.js';
|
|
29
|
+
import { getModelInfo } from '../model/model-catalog.js';
|
|
24
30
|
import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
|
|
25
31
|
import { parsePeerKey } from '../relation/peer-identity.js';
|
|
26
32
|
import { isQuickCommand } from './slash-gate.js';
|
|
27
33
|
import { handleSlashCommand } from './slash-handler.js';
|
|
28
34
|
import { resolveChatMode } from '../message/peer-mode.js';
|
|
29
35
|
import { execMenuAction as menuExecMenuAction, execMenuForEcweb as menuExecMenuForEcweb, execMenuForControl as menuExecMenuForControl, execMenuQuery as menuExecMenuQuery, execMenuUpdate as menuExecMenuUpdate, getMenuItems as menuGetMenuItems, getSubMenuItems as menuGetSubMenuItems, } from './menu-handler.js';
|
|
36
|
+
import { getMenuCatalog, execMenuValueBatch, validateMenuValueBatchArgs } from './menu-catalog.js';
|
|
30
37
|
export { isProcessLevelOwner } from './menu-handler.js';
|
|
31
38
|
const CLI_EXEC_TIMEOUT_MS = 15_000;
|
|
32
39
|
const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
|
|
@@ -93,6 +100,7 @@ export class CommandHandler {
|
|
|
93
100
|
triggerSchedulerFallbackAid;
|
|
94
101
|
daemonStatusProvider;
|
|
95
102
|
agentDelegationRegistry;
|
|
103
|
+
handoffRuntime;
|
|
96
104
|
/**
|
|
97
105
|
* Get the runner for a (channel, baseagent) pair.
|
|
98
106
|
*
|
|
@@ -125,6 +133,15 @@ export class CommandHandler {
|
|
|
125
133
|
.filter(key => key.startsWith(prefix))
|
|
126
134
|
.map(key => key.slice(prefix.length));
|
|
127
135
|
}
|
|
136
|
+
/** Return the runner-owned temporary root for a managed session, if any. */
|
|
137
|
+
getManagedSessionTempDir(session) {
|
|
138
|
+
try {
|
|
139
|
+
return this.getAgent(session.channel, session.baseagent).getManagedTempDir?.(session.id);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
128
145
|
formatBaseagentUnavailable(error) {
|
|
129
146
|
const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
|
|
130
147
|
return `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
|
|
@@ -314,6 +331,7 @@ export class CommandHandler {
|
|
|
314
331
|
channelId: definition.origin.channelId,
|
|
315
332
|
session: definition.origin.session,
|
|
316
333
|
threadId: definition.origin.threadId,
|
|
334
|
+
chatType: definition.origin.delivery?.chatType,
|
|
317
335
|
};
|
|
318
336
|
}
|
|
319
337
|
canAccessTriggerDefinition(definition, peerId, channel, isAdmin) {
|
|
@@ -396,9 +414,6 @@ export class CommandHandler {
|
|
|
396
414
|
}
|
|
397
415
|
const targetChannelType = this.resolveChannelType(feedbackChannel);
|
|
398
416
|
const feedbackChannelId = parsed.targetChannelId ?? peerId;
|
|
399
|
-
if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChannelId.includes('.')) {
|
|
400
|
-
return { error: `AUN 渠道的 --target-channel-id 必须是 AID 格式(如 user.agentid.pub),收到:"${parsed.targetChannelId}"` };
|
|
401
|
-
}
|
|
402
417
|
const schedulerAid = parsed.agentId
|
|
403
418
|
?? this.getOwningAgent(feedbackChannel)?.aid
|
|
404
419
|
?? tryParseChannelKey(feedbackChannel)?.selfAID
|
|
@@ -407,11 +422,22 @@ export class CommandHandler {
|
|
|
407
422
|
if (!schedulerAid || !scheduler) {
|
|
408
423
|
return { error: `目标 agent 不存在或未就绪:${schedulerAid ?? feedbackChannel}` };
|
|
409
424
|
}
|
|
425
|
+
if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChatType) {
|
|
426
|
+
return { error: 'AUN 目标必须显式指定 --target-chat-type private 或 group' };
|
|
427
|
+
}
|
|
428
|
+
const targetDelivery = targetChannelType === 'aun'
|
|
429
|
+
? parsed.targetChatType === 'group'
|
|
430
|
+
? { chatType: 'group', groupId: feedbackChannelId }
|
|
431
|
+
: parsed.targetChatType === 'private'
|
|
432
|
+
? { chatType: 'private' }
|
|
433
|
+
: { chatType: 'private' }
|
|
434
|
+
: undefined;
|
|
410
435
|
const target = {
|
|
411
436
|
channelKey: feedbackChannel,
|
|
412
437
|
channelId: feedbackChannelId,
|
|
413
438
|
session: parsed.targetSession,
|
|
414
439
|
threadId: parsed.targetSession === 'thread' ? parsed.targetThreadId : undefined,
|
|
440
|
+
chatType: targetDelivery?.chatType,
|
|
415
441
|
};
|
|
416
442
|
if (target.session === 'thread') {
|
|
417
443
|
const adapter = this.adapters.get(feedbackChannel);
|
|
@@ -432,6 +458,7 @@ export class CommandHandler {
|
|
|
432
458
|
threadId: !originIsGroup ? threadId : undefined,
|
|
433
459
|
peerId,
|
|
434
460
|
sessionKey: activeSession?.sessionKey,
|
|
461
|
+
delivery: { chatType: 'private' },
|
|
435
462
|
};
|
|
436
463
|
if (!isAdmin && parsed.feedbackStrategy === 'target' && !sameFeedbackTarget(target, origin)) {
|
|
437
464
|
return { error: '无权限:非 admin 只能把 trigger 投递到来源会话' };
|
|
@@ -611,7 +638,7 @@ export class CommandHandler {
|
|
|
611
638
|
}
|
|
612
639
|
/** 从 session 提取渠道预构建的回复上下文 */
|
|
613
640
|
getReplyContext(session) {
|
|
614
|
-
return session
|
|
641
|
+
return replyContextFromSession(session);
|
|
615
642
|
}
|
|
616
643
|
/**
|
|
617
644
|
* 发送 CommandCard 卡片。卡片成功返回 null(调用方直接 return),失败返回降级文本。
|
|
@@ -675,6 +702,9 @@ export class CommandHandler {
|
|
|
675
702
|
setMessageQueue(messageQueue) {
|
|
676
703
|
this.messageQueue = messageQueue;
|
|
677
704
|
}
|
|
705
|
+
setHandoffRuntime(handoffRuntime) {
|
|
706
|
+
this.handoffRuntime = handoffRuntime;
|
|
707
|
+
}
|
|
678
708
|
setPermissionGateway(gateway) {
|
|
679
709
|
this.permissionGateway = gateway;
|
|
680
710
|
}
|
|
@@ -818,6 +848,29 @@ export class CommandHandler {
|
|
|
818
848
|
getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
|
|
819
849
|
return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
|
|
820
850
|
}
|
|
851
|
+
async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent') {
|
|
852
|
+
return await getMenuCatalog({
|
|
853
|
+
subject,
|
|
854
|
+
channelType: this.resolveChannelType(channel),
|
|
855
|
+
channelScope,
|
|
856
|
+
channelId,
|
|
857
|
+
request,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
async execMenuValueBatch(channel, channelId, subject, args, request, channelScope = 'agent') {
|
|
861
|
+
validateMenuValueBatchArgs(args);
|
|
862
|
+
return await execMenuValueBatch({
|
|
863
|
+
subject,
|
|
864
|
+
channelType: this.resolveChannelType(channel),
|
|
865
|
+
channelScope,
|
|
866
|
+
channelId,
|
|
867
|
+
request,
|
|
868
|
+
items: args?.items,
|
|
869
|
+
schemaVersion: args?.schemaVersion,
|
|
870
|
+
consistency: args?.consistency,
|
|
871
|
+
execQuery: async (cmd, resolvedChannelId, queryArgs, context) => this.execMenuQuery(cmd, channel, resolvedChannelId, context.subject.actorId, queryArgs, context.subject.chatType, context.subject.fromControlChannel, context.subject.identity, context.subject, channelScope === 'control' ? 'control' : 'menu'),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
821
874
|
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
822
875
|
async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
|
|
823
876
|
return await menuGetSubMenuItems.call(this, cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel, authSubject, source);
|
|
@@ -1393,7 +1446,7 @@ export class CommandHandler {
|
|
|
1393
1446
|
* - taskId/chatmode:从 processing_state 和 effective chatmode 注入
|
|
1394
1447
|
*/
|
|
1395
1448
|
buildCtlReplyContext(session) {
|
|
1396
|
-
const ctx = {};
|
|
1449
|
+
const ctx = { delivery: deliveryTargetFromSession(session) };
|
|
1397
1450
|
const meta = session.metadata;
|
|
1398
1451
|
if (meta?.replyContext?.threadId)
|
|
1399
1452
|
ctx.threadId = meta.replyContext.threadId;
|
|
@@ -1421,7 +1474,7 @@ export class CommandHandler {
|
|
|
1421
1474
|
if (encrypted != null)
|
|
1422
1475
|
ctx.metadata.encrypted = encrypted;
|
|
1423
1476
|
}
|
|
1424
|
-
return
|
|
1477
|
+
return ctx;
|
|
1425
1478
|
}
|
|
1426
1479
|
resolveEffectiveChatmodeForSession(session) {
|
|
1427
1480
|
const peerType = session.metadata?.peerType;
|
|
@@ -1476,35 +1529,51 @@ export class CommandHandler {
|
|
|
1476
1529
|
conversationId,
|
|
1477
1530
|
processOwners: loadDaemonConfig().owners ?? [],
|
|
1478
1531
|
});
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
subject,
|
|
1487
|
-
intent: {
|
|
1488
|
-
operation: resolved.command.operationId,
|
|
1489
|
-
scope: resolved.command.commandScope,
|
|
1532
|
+
const executeOne = (fieldArgv) => {
|
|
1533
|
+
const resolved = resolveConfigCommand(fieldArgv, {
|
|
1534
|
+
defaultRelation: { self: grant.selfAid, peerKey: grant.peerKey },
|
|
1535
|
+
});
|
|
1536
|
+
if (!resolved.ok)
|
|
1537
|
+
return { ok: false, code: resolved.code, error: resolved.reason };
|
|
1538
|
+
const decision = authorizeOperation({
|
|
1490
1539
|
source: 'agent-tool',
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1540
|
+
subject,
|
|
1541
|
+
intent: {
|
|
1542
|
+
operation: resolved.command.operationId,
|
|
1543
|
+
scope: resolved.command.commandScope,
|
|
1544
|
+
source: 'agent-tool',
|
|
1545
|
+
args: {
|
|
1546
|
+
...(resolved.command.self ? { self: resolved.command.self } : {}),
|
|
1547
|
+
...(resolved.command.peerKey ? { peer: resolved.command.peerKey, peerKey: resolved.command.peerKey } : {}),
|
|
1548
|
+
...('field' in resolved.command && resolved.command.field ? { field: resolved.command.field } : {}),
|
|
1549
|
+
},
|
|
1550
|
+
dangerous: resolved.command.dangerous,
|
|
1495
1551
|
},
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const
|
|
1552
|
+
resolvedConfigCommand: resolved.command,
|
|
1553
|
+
auditMetadata: {
|
|
1554
|
+
argvHash: hashArgv(resolved.command.canonicalArgv),
|
|
1555
|
+
taskId: grant.taskId,
|
|
1556
|
+
messageId: grant.messageId,
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
if (!decision.allow)
|
|
1560
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
1561
|
+
return executeResolvedConfigCommand(resolved.command, { role: subject.role });
|
|
1562
|
+
};
|
|
1563
|
+
const batch = splitConfigBatchGetArgv(argv);
|
|
1564
|
+
if (batch && batch.fields.length > 1) {
|
|
1565
|
+
const results = batch.fields.map(field => {
|
|
1566
|
+
const result = executeOne(['config', 'get', field, ...batch.rest]);
|
|
1567
|
+
if (!result.ok)
|
|
1568
|
+
return { ok: false, field, code: result.code, error: result.error };
|
|
1569
|
+
if (result.subcommand !== 'get') {
|
|
1570
|
+
return { ok: false, field, code: 'UNEXPECTED_RESULT', error: `config get ${field} produced a non-get result` };
|
|
1571
|
+
}
|
|
1572
|
+
return result;
|
|
1573
|
+
});
|
|
1574
|
+
return { ok: true, result: { ok: true, subcommand: 'batch-get', results } };
|
|
1575
|
+
}
|
|
1576
|
+
const result = executeOne(argv);
|
|
1508
1577
|
return result.ok
|
|
1509
1578
|
? { ok: true, result }
|
|
1510
1579
|
: { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
|
|
@@ -1569,6 +1638,415 @@ export class CommandHandler {
|
|
|
1569
1638
|
? { ok: true, result }
|
|
1570
1639
|
: { ok: false, code: result.code, error: result.error, ...(result.data ? { data: result.data } : {}) };
|
|
1571
1640
|
}
|
|
1641
|
+
/** Agent-managed AID CLI entrypoint. Private key and CA access stay inside the daemon. */
|
|
1642
|
+
async handleAidOperation(argv, sessionId, delegationToken, delegationCommandHash) {
|
|
1643
|
+
if (!this.agentDelegationRegistry) {
|
|
1644
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1645
|
+
}
|
|
1646
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
|
|
1647
|
+
if (!delegation.ok)
|
|
1648
|
+
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1649
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1650
|
+
if (!session)
|
|
1651
|
+
return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
|
|
1652
|
+
const parsed = resolveManagedAidOperation(argv);
|
|
1653
|
+
if (!parsed.ok)
|
|
1654
|
+
return { ok: false, code: parsed.code, error: parsed.reason };
|
|
1655
|
+
const grantSelfAid = delegation.grant.selfAid.replace(/^@/, '');
|
|
1656
|
+
const sessionSelfAid = session.selfAID?.replace(/^@/, '');
|
|
1657
|
+
if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
|
|
1658
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
|
|
1659
|
+
}
|
|
1660
|
+
if (parsed.command.aid !== grantSelfAid) {
|
|
1661
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can be targeted' };
|
|
1662
|
+
}
|
|
1663
|
+
let conversationId;
|
|
1664
|
+
try {
|
|
1665
|
+
conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
|
|
1669
|
+
}
|
|
1670
|
+
const subject = buildAuthSubject({
|
|
1671
|
+
selfAid: delegation.grant.selfAid,
|
|
1672
|
+
actorId: delegation.grant.actorId,
|
|
1673
|
+
channel: delegation.grant.channel,
|
|
1674
|
+
channelType: delegation.grant.channelType,
|
|
1675
|
+
channelId: conversationId,
|
|
1676
|
+
chatType: delegation.grant.chatType,
|
|
1677
|
+
conversationId,
|
|
1678
|
+
processOwners: loadDaemonConfig().owners ?? [],
|
|
1679
|
+
});
|
|
1680
|
+
const managementRole = isManagementRole(subject.role);
|
|
1681
|
+
const decision = authorizeOperation({
|
|
1682
|
+
source: 'agent-tool',
|
|
1683
|
+
subject,
|
|
1684
|
+
intent: {
|
|
1685
|
+
operation: parsed.command.operationId,
|
|
1686
|
+
scope: managementRole ? 'agent' : 'relation',
|
|
1687
|
+
source: 'agent-tool',
|
|
1688
|
+
args: { self: parsed.command.aid, targetId: parsed.command.aid },
|
|
1689
|
+
},
|
|
1690
|
+
auditMetadata: {
|
|
1691
|
+
argvHash: hashArgv(parsed.command.canonicalArgv),
|
|
1692
|
+
taskId: delegation.grant.taskId,
|
|
1693
|
+
messageId: delegation.grant.messageId,
|
|
1694
|
+
},
|
|
1695
|
+
});
|
|
1696
|
+
if (!decision.allow)
|
|
1697
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
1698
|
+
try {
|
|
1699
|
+
return { ok: true, result: await executeManagedAidOperation(parsed.command) };
|
|
1700
|
+
}
|
|
1701
|
+
catch (error) {
|
|
1702
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1703
|
+
return {
|
|
1704
|
+
ok: false,
|
|
1705
|
+
code: error instanceof ManagedAidOperationError ? error.code : 'AID_OPERATION_FAILED',
|
|
1706
|
+
error: message,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/** Agent-managed group CLI entrypoint. AUN private material remains daemon-owned. */
|
|
1711
|
+
async handleGroupOperation(argv, sessionId, delegationToken, delegationCommandHash) {
|
|
1712
|
+
if (!this.agentDelegationRegistry) {
|
|
1713
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1714
|
+
}
|
|
1715
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
|
|
1716
|
+
if (!delegation.ok)
|
|
1717
|
+
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1718
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1719
|
+
if (!session)
|
|
1720
|
+
return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
|
|
1721
|
+
const parsed = resolveManagedGroupOperation(argv);
|
|
1722
|
+
if (!parsed.ok)
|
|
1723
|
+
return { ok: false, code: parsed.code, error: parsed.reason };
|
|
1724
|
+
const grant = delegation.grant;
|
|
1725
|
+
const grantSelfAid = grant.selfAid.replace(/^@/, '');
|
|
1726
|
+
const sessionSelfAid = session.selfAID?.replace(/^@/, '');
|
|
1727
|
+
if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
|
|
1728
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
|
|
1729
|
+
}
|
|
1730
|
+
if (parsed.command.from !== grantSelfAid) {
|
|
1731
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can perform managed group operations' };
|
|
1732
|
+
}
|
|
1733
|
+
let conversationId;
|
|
1734
|
+
try {
|
|
1735
|
+
conversationId = parsePeerKey(grant.peerKey).channelId;
|
|
1736
|
+
}
|
|
1737
|
+
catch {
|
|
1738
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
|
|
1739
|
+
}
|
|
1740
|
+
const subject = buildAuthSubject({
|
|
1741
|
+
selfAid: grant.selfAid,
|
|
1742
|
+
actorId: grant.actorId,
|
|
1743
|
+
channel: grant.channel,
|
|
1744
|
+
channelType: grant.channelType,
|
|
1745
|
+
channelId: conversationId,
|
|
1746
|
+
chatType: grant.chatType,
|
|
1747
|
+
conversationId,
|
|
1748
|
+
processOwners: loadDaemonConfig().owners ?? [],
|
|
1749
|
+
});
|
|
1750
|
+
const managementRole = isManagementRole(subject.role);
|
|
1751
|
+
if (!managementRole && (grant.channelType !== 'aun' || !isManagedGroupRelationRead(parsed.command))) {
|
|
1752
|
+
return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `Role ${subject.role} cannot manage Agent group membership or settings` };
|
|
1753
|
+
}
|
|
1754
|
+
if ((parsed.command.kind === 'dissolve' || parsed.command.kind === 'owner') && subject.role !== 'owner') {
|
|
1755
|
+
return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `${parsed.command.kind} requires Agent owner permission` };
|
|
1756
|
+
}
|
|
1757
|
+
const decision = authorizeOperation({
|
|
1758
|
+
source: 'agent-tool',
|
|
1759
|
+
subject,
|
|
1760
|
+
intent: {
|
|
1761
|
+
operation: parsed.command.operationId,
|
|
1762
|
+
scope: managementRole ? 'agent' : 'relation',
|
|
1763
|
+
source: 'agent-tool',
|
|
1764
|
+
args: {
|
|
1765
|
+
self: parsed.command.from,
|
|
1766
|
+
...(parsed.command.groupId ? {
|
|
1767
|
+
targetId: parsed.command.groupId,
|
|
1768
|
+
peer: parsed.command.groupId,
|
|
1769
|
+
peerKey: formatPeerKey(grant.channelType, parsed.command.groupId),
|
|
1770
|
+
} : {}),
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
auditMetadata: {
|
|
1774
|
+
argvHash: hashArgv(parsed.command.canonicalArgv),
|
|
1775
|
+
taskId: grant.taskId,
|
|
1776
|
+
messageId: grant.messageId,
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
if (!decision.allow)
|
|
1780
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
1781
|
+
let rulesContent;
|
|
1782
|
+
if (parsed.command.kind === 'rules.set') {
|
|
1783
|
+
const filePath = String(parsed.command.filePath || '');
|
|
1784
|
+
const projectPath = path.resolve(session.projectPath);
|
|
1785
|
+
const resolvedPath = path.resolve(projectPath, filePath);
|
|
1786
|
+
const managedTempPath = this.getManagedSessionTempDir(session);
|
|
1787
|
+
const allowedPaths = [projectPath, managedTempPath].filter((value) => !!value);
|
|
1788
|
+
if (!filePath || !allowedPaths.some(root => isSameOrDescendant(resolvedPath, root))) {
|
|
1789
|
+
return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file must be inside the current session project or managed TMPDIR' };
|
|
1790
|
+
}
|
|
1791
|
+
if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
|
|
1792
|
+
return { ok: false, code: 'PROTECTED_RULES_PATH', error: 'Rules file cannot be read from a protected EvolCore path' };
|
|
1793
|
+
}
|
|
1794
|
+
let rulesFd;
|
|
1795
|
+
try {
|
|
1796
|
+
const realProjectPath = fs.realpathSync(projectPath);
|
|
1797
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
1798
|
+
let realManagedTempPath;
|
|
1799
|
+
try {
|
|
1800
|
+
realManagedTempPath = managedTempPath ? fs.realpathSync(managedTempPath) : undefined;
|
|
1801
|
+
}
|
|
1802
|
+
catch {
|
|
1803
|
+
// The session may have closed and removed its temporary directory.
|
|
1804
|
+
}
|
|
1805
|
+
const realAllowedPaths = [realProjectPath, realManagedTempPath].filter((value) => !!value);
|
|
1806
|
+
if (!realAllowedPaths.some(root => isSameOrDescendant(realPath, root))) {
|
|
1807
|
+
return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file resolves outside the current session project or managed TMPDIR' };
|
|
1808
|
+
}
|
|
1809
|
+
if (isHClassPath(realPath) || isLClassPath(realPath)) {
|
|
1810
|
+
return { ok: false, code: 'PROTECTED_RULES_PATH', error: 'Rules file cannot be read from a protected EvolCore path' };
|
|
1811
|
+
}
|
|
1812
|
+
rulesFd = fs.openSync(realPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
1813
|
+
const stat = fs.fstatSync(rulesFd);
|
|
1814
|
+
if (!stat.isFile() || stat.size < 1 || stat.size > 4 * 1024) {
|
|
1815
|
+
return { ok: false, code: 'INVALID_RULES_CONTENT', error: 'Rules file must be a regular file from 1 to 4096 bytes' };
|
|
1816
|
+
}
|
|
1817
|
+
if (realManagedTempPath && isSameOrDescendant(realPath, realManagedTempPath) && stat.nlink > 1) {
|
|
1818
|
+
return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file cannot be a hard link' };
|
|
1819
|
+
}
|
|
1820
|
+
rulesContent = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(rulesFd));
|
|
1821
|
+
}
|
|
1822
|
+
catch (error) {
|
|
1823
|
+
return {
|
|
1824
|
+
ok: false,
|
|
1825
|
+
code: 'INVALID_RULES_CONTENT',
|
|
1826
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
finally {
|
|
1830
|
+
if (rulesFd !== undefined)
|
|
1831
|
+
fs.closeSync(rulesFd);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
try {
|
|
1835
|
+
return { ok: true, result: await executeManagedGroupOperation(parsed.command, rulesContent) };
|
|
1836
|
+
}
|
|
1837
|
+
catch (error) {
|
|
1838
|
+
return {
|
|
1839
|
+
ok: false,
|
|
1840
|
+
code: error instanceof ManagedGroupOperationError ? error.code : 'GROUP_OPERATION_FAILED',
|
|
1841
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/** Agent-managed private msg read entrypoint. AUN credentials and history roots remain daemon-owned. */
|
|
1846
|
+
async handleMsgOperation(argv, sessionId, delegationToken, delegationCommandHash) {
|
|
1847
|
+
if (!this.agentDelegationRegistry) {
|
|
1848
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1849
|
+
}
|
|
1850
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
|
|
1851
|
+
if (!delegation.ok)
|
|
1852
|
+
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1853
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1854
|
+
if (!session)
|
|
1855
|
+
return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
|
|
1856
|
+
const parsed = resolveManagedMsgOperation(argv);
|
|
1857
|
+
if (!parsed.ok)
|
|
1858
|
+
return { ok: false, code: parsed.code, error: parsed.reason };
|
|
1859
|
+
const grant = delegation.grant;
|
|
1860
|
+
const grantSelfAid = grant.selfAid.replace(/^@/, '');
|
|
1861
|
+
const sessionSelfAid = session.selfAID?.replace(/^@/, '');
|
|
1862
|
+
if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
|
|
1863
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
|
|
1864
|
+
}
|
|
1865
|
+
if (grant.channelType !== 'aun' || grant.chatType !== 'private') {
|
|
1866
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed msg reads require the current AUN private relation' };
|
|
1867
|
+
}
|
|
1868
|
+
let peerAid;
|
|
1869
|
+
try {
|
|
1870
|
+
peerAid = parsePeerKey(grant.peerKey).channelId.replace(/^@/, '');
|
|
1871
|
+
}
|
|
1872
|
+
catch {
|
|
1873
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
|
|
1874
|
+
}
|
|
1875
|
+
if (parsed.command.from !== grantSelfAid || parsed.command.target !== peerAid) {
|
|
1876
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN private relation can be targeted' };
|
|
1877
|
+
}
|
|
1878
|
+
const subject = buildAuthSubject({
|
|
1879
|
+
selfAid: grant.selfAid,
|
|
1880
|
+
actorId: grant.actorId,
|
|
1881
|
+
channel: grant.channel,
|
|
1882
|
+
channelType: grant.channelType,
|
|
1883
|
+
channelId: peerAid,
|
|
1884
|
+
chatType: grant.chatType,
|
|
1885
|
+
conversationId: peerAid,
|
|
1886
|
+
processOwners: loadDaemonConfig().owners ?? [],
|
|
1887
|
+
});
|
|
1888
|
+
const decision = authorizeOperation({
|
|
1889
|
+
source: 'agent-tool',
|
|
1890
|
+
subject,
|
|
1891
|
+
intent: {
|
|
1892
|
+
operation: parsed.command.operationId,
|
|
1893
|
+
scope: 'relation',
|
|
1894
|
+
source: 'agent-tool',
|
|
1895
|
+
args: {
|
|
1896
|
+
self: parsed.command.from,
|
|
1897
|
+
targetId: parsed.command.target,
|
|
1898
|
+
peer: parsed.command.target,
|
|
1899
|
+
peerKey: formatPeerKey('aun', parsed.command.target),
|
|
1900
|
+
},
|
|
1901
|
+
},
|
|
1902
|
+
auditMetadata: {
|
|
1903
|
+
argvHash: hashArgv(parsed.command.canonicalArgv),
|
|
1904
|
+
taskId: grant.taskId,
|
|
1905
|
+
messageId: grant.messageId,
|
|
1906
|
+
},
|
|
1907
|
+
});
|
|
1908
|
+
if (!decision.allow)
|
|
1909
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
1910
|
+
try {
|
|
1911
|
+
return { ok: true, result: await executeManagedMsgOperation(parsed.command) };
|
|
1912
|
+
}
|
|
1913
|
+
catch (error) {
|
|
1914
|
+
return {
|
|
1915
|
+
ok: false,
|
|
1916
|
+
code: error instanceof ManagedMsgOperationError ? error.code : 'MSG_OPERATION_FAILED',
|
|
1917
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
/** Agent-managed current-group filesystem entrypoint. */
|
|
1922
|
+
async handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash) {
|
|
1923
|
+
if (!this.agentDelegationRegistry) {
|
|
1924
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1925
|
+
}
|
|
1926
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
|
|
1927
|
+
if (!delegation.ok)
|
|
1928
|
+
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1929
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1930
|
+
if (!session)
|
|
1931
|
+
return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
|
|
1932
|
+
const parsed = resolveManagedFsOperation(argv);
|
|
1933
|
+
if (!parsed.ok)
|
|
1934
|
+
return { ok: false, code: parsed.code, error: parsed.reason };
|
|
1935
|
+
const grant = delegation.grant;
|
|
1936
|
+
const actorAid = grant.selfAid.replace(/^@/, '');
|
|
1937
|
+
const sessionSelfAid = session.selfAID?.replace(/^@/, '');
|
|
1938
|
+
if (sessionSelfAid && sessionSelfAid !== actorAid) {
|
|
1939
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
|
|
1940
|
+
}
|
|
1941
|
+
if (grant.channelType !== 'aun' || grant.chatType !== 'group') {
|
|
1942
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed fs operations require the current AUN group relation' };
|
|
1943
|
+
}
|
|
1944
|
+
let currentGroup;
|
|
1945
|
+
try {
|
|
1946
|
+
currentGroup = parsePeerKey(grant.peerKey).channelId;
|
|
1947
|
+
}
|
|
1948
|
+
catch {
|
|
1949
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
|
|
1950
|
+
}
|
|
1951
|
+
if ((parsed.command.actorAid && parsed.command.actorAid !== actorAid) || parsed.command.groupId !== currentGroup) {
|
|
1952
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN group can be targeted' };
|
|
1953
|
+
}
|
|
1954
|
+
parsed.command.actorAid = actorAid;
|
|
1955
|
+
const subject = buildAuthSubject({
|
|
1956
|
+
selfAid: grant.selfAid,
|
|
1957
|
+
actorId: grant.actorId,
|
|
1958
|
+
channel: grant.channel,
|
|
1959
|
+
channelType: grant.channelType,
|
|
1960
|
+
channelId: currentGroup,
|
|
1961
|
+
chatType: grant.chatType,
|
|
1962
|
+
conversationId: currentGroup,
|
|
1963
|
+
processOwners: loadDaemonConfig().owners ?? [],
|
|
1964
|
+
});
|
|
1965
|
+
const intentArgs = {
|
|
1966
|
+
self: actorAid,
|
|
1967
|
+
targetId: currentGroup,
|
|
1968
|
+
peer: currentGroup,
|
|
1969
|
+
peerKey: formatPeerKey('aun', currentGroup),
|
|
1970
|
+
path: parsed.command.remotePath,
|
|
1971
|
+
targetPath: parsed.command.remotePath,
|
|
1972
|
+
...(parsed.command.remoteSourcePath ? { sourcePath: parsed.command.remoteSourcePath } : {}),
|
|
1973
|
+
...(parsed.command.localPath ? { localPath: parsed.command.localPath } : {}),
|
|
1974
|
+
...(parsed.command.recursive !== undefined ? { recursive: parsed.command.recursive } : {}),
|
|
1975
|
+
...(parsed.command.parents !== undefined ? { parents: parsed.command.parents } : {}),
|
|
1976
|
+
...(parsed.command.overwrite !== undefined ? { overwrite: parsed.command.overwrite } : {}),
|
|
1977
|
+
};
|
|
1978
|
+
const decision = authorizeOperation({
|
|
1979
|
+
source: 'agent-tool',
|
|
1980
|
+
subject,
|
|
1981
|
+
intent: {
|
|
1982
|
+
operation: parsed.command.operationId,
|
|
1983
|
+
scope: 'relation',
|
|
1984
|
+
source: 'agent-tool',
|
|
1985
|
+
args: intentArgs,
|
|
1986
|
+
},
|
|
1987
|
+
auditMetadata: {
|
|
1988
|
+
argvHash: hashArgv(parsed.command.canonicalArgv), taskId: grant.taskId, messageId: grant.messageId,
|
|
1989
|
+
sessionId, agentAid: actorAid, toolName: 'ec.fs',
|
|
1990
|
+
},
|
|
1991
|
+
});
|
|
1992
|
+
if (!decision.allow)
|
|
1993
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
1994
|
+
if (parsed.command.kind === 'upload') {
|
|
1995
|
+
const localPath = String(parsed.command.localPath || '').trim();
|
|
1996
|
+
const projectPath = path.resolve(session.projectPath);
|
|
1997
|
+
if (!localPath)
|
|
1998
|
+
return { ok: false, code: 'INVALID_ARGUMENT', error: 'Managed upload source is required' };
|
|
1999
|
+
const resolvedPath = path.resolve(projectPath, localPath);
|
|
2000
|
+
const managedTempPath = this.getManagedSessionTempDir(session);
|
|
2001
|
+
const allowedRoots = [projectPath, managedTempPath].filter((value) => !!value);
|
|
2002
|
+
if (!allowedRoots.some(root => isSameOrDescendant(resolvedPath, root))) {
|
|
2003
|
+
return { ok: false, code: 'INVALID_LOCAL_PATH', error: 'Upload source must be inside the current session project or managed TMPDIR' };
|
|
2004
|
+
}
|
|
2005
|
+
if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
|
|
2006
|
+
return { ok: false, code: 'PROTECTED_LOCAL_PATH', error: 'Upload source cannot be a protected EvolCore path' };
|
|
2007
|
+
}
|
|
2008
|
+
try {
|
|
2009
|
+
const realProjectPath = fs.realpathSync(projectPath);
|
|
2010
|
+
const stat = fs.lstatSync(resolvedPath);
|
|
2011
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
2012
|
+
return { ok: false, code: 'INVALID_LOCAL_FILE', error: 'Upload source must be a regular non-symlink file' };
|
|
2013
|
+
}
|
|
2014
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
2015
|
+
let realManagedTempPath;
|
|
2016
|
+
try {
|
|
2017
|
+
realManagedTempPath = managedTempPath ? fs.realpathSync(managedTempPath) : undefined;
|
|
2018
|
+
}
|
|
2019
|
+
catch {
|
|
2020
|
+
// A removed or invalid session temp root is not an upload root.
|
|
2021
|
+
}
|
|
2022
|
+
const realAllowedRoots = [realProjectPath, realManagedTempPath].filter((value) => !!value);
|
|
2023
|
+
if (!realAllowedRoots.some(root => isSameOrDescendant(realPath, root))
|
|
2024
|
+
|| isHClassPath(realPath) || isLClassPath(realPath)) {
|
|
2025
|
+
return { ok: false, code: 'PROTECTED_LOCAL_PATH', error: 'Upload source resolves outside the allowed project or managed TMPDIR path' };
|
|
2026
|
+
}
|
|
2027
|
+
if (realManagedTempPath && isSameOrDescendant(realPath, realManagedTempPath) && stat.nlink > 1) {
|
|
2028
|
+
return { ok: false, code: 'INVALID_LOCAL_PATH', error: 'Upload source cannot be a hard link inside managed TMPDIR' };
|
|
2029
|
+
}
|
|
2030
|
+
if (stat.size > 16 * 1024 * 1024) {
|
|
2031
|
+
return { ok: false, code: 'LOCAL_FILE_TOO_LARGE', error: 'Managed upload is limited to 16 MiB' };
|
|
2032
|
+
}
|
|
2033
|
+
parsed.command.localPath = realPath;
|
|
2034
|
+
}
|
|
2035
|
+
catch (error) {
|
|
2036
|
+
return { ok: false, code: 'INVALID_LOCAL_FILE', error: error instanceof Error ? error.message : String(error) };
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
try {
|
|
2040
|
+
return { ok: true, result: await executeManagedFsOperation(parsed.command) };
|
|
2041
|
+
}
|
|
2042
|
+
catch (error) {
|
|
2043
|
+
return {
|
|
2044
|
+
ok: false,
|
|
2045
|
+
code: error instanceof ManagedFsOperationError ? error.code : 'FS_OPERATION_FAILED',
|
|
2046
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1572
2050
|
setAgentDelegationRegistry(registry) {
|
|
1573
2051
|
this.agentDelegationRegistry = registry;
|
|
1574
2052
|
}
|
|
@@ -1576,7 +2054,7 @@ export class CommandHandler {
|
|
|
1576
2054
|
* Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
|
|
1577
2055
|
* 复用现有 slash cmd 逻辑,权限继承 session 用户角色
|
|
1578
2056
|
*/
|
|
1579
|
-
async handleCtl(cmd, sessionId) {
|
|
2057
|
+
async handleCtl(cmd, sessionId, grant) {
|
|
1580
2058
|
logger.info(`[ctl] cmd="${cmd}" sessionId=${sessionId}`);
|
|
1581
2059
|
// 1. 白名单检查
|
|
1582
2060
|
const inputCmd = cmd.split(' ')[0];
|
|
@@ -1588,9 +2066,56 @@ export class CommandHandler {
|
|
|
1588
2066
|
if (!session) {
|
|
1589
2067
|
return { ok: false, error: '无效的 session' };
|
|
1590
2068
|
}
|
|
2069
|
+
if (!grant || grant.sessionId !== sessionId) {
|
|
2070
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'An active task delegation grant is required' };
|
|
2071
|
+
}
|
|
2072
|
+
const sessionSelfAid = session.selfAID?.replace(/^@/, '');
|
|
2073
|
+
const grantSelfAid = grant.selfAid.replace(/^@/, '');
|
|
2074
|
+
if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
|
|
2075
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
|
|
2076
|
+
}
|
|
2077
|
+
let conversationId;
|
|
2078
|
+
try {
|
|
2079
|
+
conversationId = parsePeerKey(grant.peerKey).channelId;
|
|
2080
|
+
}
|
|
2081
|
+
catch {
|
|
2082
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
|
|
2083
|
+
}
|
|
2084
|
+
const ctlSubject = buildAuthSubject({
|
|
2085
|
+
selfAid: grant.selfAid,
|
|
2086
|
+
actorId: grant.actorId,
|
|
2087
|
+
channel: grant.channel,
|
|
2088
|
+
channelType: grant.channelType,
|
|
2089
|
+
channelId: conversationId,
|
|
2090
|
+
chatType: grant.chatType,
|
|
2091
|
+
conversationId,
|
|
2092
|
+
processOwners: loadDaemonConfig().owners ?? [],
|
|
2093
|
+
});
|
|
2094
|
+
const ctlIntent = this.resolveCtlIntent(cmd, grantSelfAid, grant.peerKey);
|
|
2095
|
+
if (!ctlIntent)
|
|
2096
|
+
return { ok: false, code: 'NOT_ALLOWED', error: `不允许的指令: ${inputCmd}` };
|
|
2097
|
+
const ctlDecision = authorizeOperation({
|
|
2098
|
+
source: 'agent-tool',
|
|
2099
|
+
subject: ctlSubject,
|
|
2100
|
+
intent: ctlIntent,
|
|
2101
|
+
auditMetadata: {
|
|
2102
|
+
argvHash: hashArgv(['ctl', ...cmd.slice(1).split(/\s+/).filter(Boolean)]),
|
|
2103
|
+
taskId: grant.taskId,
|
|
2104
|
+
messageId: grant.messageId,
|
|
2105
|
+
},
|
|
2106
|
+
});
|
|
2107
|
+
if (!ctlDecision.allow)
|
|
2108
|
+
return { ok: false, code: ctlDecision.code, error: ctlDecision.reason };
|
|
2109
|
+
if (cmd.startsWith('/model info ')) {
|
|
2110
|
+
const modelId = cmd.slice('/model info '.length).trim();
|
|
2111
|
+
if (!modelId || /\s/.test(modelId))
|
|
2112
|
+
return { ok: false, code: 'INVALID_ARGUMENT', error: 'model info requires one model id' };
|
|
2113
|
+
const info = await getModelInfo(modelId, grantSelfAid, session.baseagent);
|
|
2114
|
+
return { ok: true, result: JSON.stringify(info, null, 2), data: { ok: true, ...info } };
|
|
2115
|
+
}
|
|
1591
2116
|
// 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
|
|
1592
2117
|
const userId = session.metadata?.peerId;
|
|
1593
|
-
const ctlIdentity =
|
|
2118
|
+
const ctlIdentity = ctlSubject.identity;
|
|
1594
2119
|
// 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
|
|
1595
2120
|
if (cmd.startsWith('/send ') || cmd === '/send') {
|
|
1596
2121
|
// 解析 --encrypt 标志和消息文本
|
|
@@ -1625,9 +2150,9 @@ export class CommandHandler {
|
|
|
1625
2150
|
const parts = sendArgs.split(/\s+/);
|
|
1626
2151
|
const filePath = parts[parts.length - 1];
|
|
1627
2152
|
if (filePath) {
|
|
1628
|
-
const resolved = path.resolve(session.projectPath, filePath)
|
|
1629
|
-
const
|
|
1630
|
-
if (
|
|
2153
|
+
const resolved = path.resolve(session.projectPath, filePath);
|
|
2154
|
+
const relative = path.relative(path.resolve(session.projectPath), resolved);
|
|
2155
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
1631
2156
|
return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
|
|
1632
2157
|
}
|
|
1633
2158
|
}
|
|
@@ -1640,7 +2165,7 @@ export class CommandHandler {
|
|
|
1640
2165
|
// 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
|
|
1641
2166
|
try {
|
|
1642
2167
|
const result = await this._handleInternal(cmd, session.channel, session.channelId, undefined, // 不发送消息
|
|
1643
|
-
userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity);
|
|
2168
|
+
userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity, ctlSubject);
|
|
1644
2169
|
const text = typeof result === 'string' ? result : (result && 'text' in result ? result.text : '(无输出)');
|
|
1645
2170
|
if (result && typeof result === 'object' && 'kind' in result && result.kind === 'command.error') {
|
|
1646
2171
|
return { ok: false, error: text || '执行失败' };
|
|
@@ -1651,6 +2176,56 @@ export class CommandHandler {
|
|
|
1651
2176
|
return { ok: false, error: err.message };
|
|
1652
2177
|
}
|
|
1653
2178
|
}
|
|
2179
|
+
resolveCtlIntent(cmd, selfAid, peerKey) {
|
|
2180
|
+
const [name = '', ...args] = cmd.trim().slice(1).trim().split(/\s+/);
|
|
2181
|
+
const relationArgs = { self: selfAid, peer: peerKey, peerKey };
|
|
2182
|
+
const intent = (operation, extra = {}) => ({
|
|
2183
|
+
operation,
|
|
2184
|
+
scope: 'relation',
|
|
2185
|
+
source: 'agent-tool',
|
|
2186
|
+
args: { ...relationArgs, ...extra },
|
|
2187
|
+
});
|
|
2188
|
+
if (name === 'send')
|
|
2189
|
+
return intent('ec.ctl.send');
|
|
2190
|
+
if (name === 'file')
|
|
2191
|
+
return intent('ec.ctl.file');
|
|
2192
|
+
if (name === 'status')
|
|
2193
|
+
return intent('session.current');
|
|
2194
|
+
if (name === 'pwd')
|
|
2195
|
+
return intent('project.current');
|
|
2196
|
+
if (name === 'name' || name === 'rename')
|
|
2197
|
+
return intent('session.rename');
|
|
2198
|
+
if (name === 'queue') {
|
|
2199
|
+
const action = args.includes('--clear') ? 'clear'
|
|
2200
|
+
: args.includes('--cancel') ? 'cancel'
|
|
2201
|
+
: args.includes('--interrupt') ? 'interrupt'
|
|
2202
|
+
: 'read';
|
|
2203
|
+
return intent(`ec.ctl.queue.${action}`);
|
|
2204
|
+
}
|
|
2205
|
+
if (name === 'model') {
|
|
2206
|
+
if (args.length === 0 || args[0] === 'list')
|
|
2207
|
+
return intent('model.list');
|
|
2208
|
+
if (args[0] === 'current')
|
|
2209
|
+
return intent('model.current');
|
|
2210
|
+
if (args[0] === 'info')
|
|
2211
|
+
return intent('model.info', { model: args[1] });
|
|
2212
|
+
if (args[0] === 'reset')
|
|
2213
|
+
return intent('model.reset');
|
|
2214
|
+
if (args[0] === 'check') {
|
|
2215
|
+
return { operation: 'model.check', scope: 'agent', source: 'agent-tool', args: { self: selfAid } };
|
|
2216
|
+
}
|
|
2217
|
+
return intent('model.use', { model: args[0] });
|
|
2218
|
+
}
|
|
2219
|
+
if (name === 'effort') {
|
|
2220
|
+
return args.length === 0
|
|
2221
|
+
? intent('model.effort.current')
|
|
2222
|
+
: intent('model.effort', { effort: args[0] });
|
|
2223
|
+
}
|
|
2224
|
+
if (name === 'restart') {
|
|
2225
|
+
return { operation: 'system.restart', scope: 'process', source: 'agent-tool', args: { self: selfAid } };
|
|
2226
|
+
}
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
1654
2229
|
// ── Queue command ──
|
|
1655
2230
|
/** 提取命名参数值(如 --cancel msg123 → "msg123") */
|
|
1656
2231
|
extractArg(args, flag) {
|
|
@@ -1665,6 +2240,14 @@ export class CommandHandler {
|
|
|
1665
2240
|
const showId = args.includes('--showid');
|
|
1666
2241
|
const formatJson = args.includes('--format json');
|
|
1667
2242
|
const full = args.includes('--full');
|
|
2243
|
+
const after = parseQueueTimeArg(this.extractArg(args, '--after'));
|
|
2244
|
+
const before = parseQueueTimeArg(this.extractArg(args, '--before'));
|
|
2245
|
+
if (this.extractArg(args, '--after') && after === undefined)
|
|
2246
|
+
return { ok: false, error: '❌ --after 必须是 epoch 毫秒或 ISO 时间' };
|
|
2247
|
+
if (this.extractArg(args, '--before') && before === undefined)
|
|
2248
|
+
return { ok: false, error: '❌ --before 必须是 epoch 毫秒或 ISO 时间' };
|
|
2249
|
+
if (after !== undefined && before !== undefined && after >= before)
|
|
2250
|
+
return { ok: false, error: '❌ --after 必须早于 --before' };
|
|
1668
2251
|
// 操作分支
|
|
1669
2252
|
if (args.includes('--clear')) {
|
|
1670
2253
|
const count = this.messageQueue.clearBySession(sessionId);
|
|
@@ -1686,13 +2269,19 @@ export class CommandHandler {
|
|
|
1686
2269
|
: { ok: false, error: `❌ 当前无处理中任务` };
|
|
1687
2270
|
}
|
|
1688
2271
|
// 查询
|
|
1689
|
-
const items = this.messageQueue.getQueueItemsBySession(sessionId);
|
|
2272
|
+
const items = this.messageQueue.getQueueItemsBySession(sessionId, { after, before });
|
|
1690
2273
|
if (formatJson) {
|
|
1691
2274
|
return { ok: true, result: JSON.stringify({ items }, null, 2) };
|
|
1692
2275
|
}
|
|
1693
2276
|
return { ok: true, result: renderQueueItemsCtl(items, showId, full) };
|
|
1694
2277
|
}
|
|
1695
2278
|
}
|
|
2279
|
+
function parseQueueTimeArg(value) {
|
|
2280
|
+
if (!value)
|
|
2281
|
+
return undefined;
|
|
2282
|
+
const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
2283
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2284
|
+
}
|
|
1696
2285
|
/**
|
|
1697
2286
|
* ctl 专用渲染:不显示 session 标识列(因为只有一个 session)
|
|
1698
2287
|
*/
|
|
@@ -1715,14 +2304,33 @@ function renderQueueItemsCtl(items, showId, full) {
|
|
|
1715
2304
|
parts.push(` ${name} `);
|
|
1716
2305
|
}
|
|
1717
2306
|
const content = full ? item.preview.replace('...', '') : item.preview;
|
|
1718
|
-
|
|
2307
|
+
const timing = [
|
|
2308
|
+
item.receivedAt != null ? `recv=${new Date(item.receivedAt).toISOString()}` : undefined,
|
|
2309
|
+
item.enqueuedAt != null ? `enq=${new Date(item.enqueuedAt).toISOString()}` : undefined,
|
|
2310
|
+
item.queueWaitMs != null ? `wait=${formatQueueDuration(item.queueWaitMs)}` : undefined,
|
|
2311
|
+
].filter(Boolean).join(' ');
|
|
2312
|
+
parts.push(`${timing ? `${timing} ` : ''}"${content}"`);
|
|
1719
2313
|
lines.push(parts.join(''));
|
|
1720
2314
|
}
|
|
1721
2315
|
return lines.join('\n');
|
|
1722
2316
|
}
|
|
2317
|
+
function formatQueueDuration(ms) {
|
|
2318
|
+
const sec = Math.max(0, Math.round(ms / 1000));
|
|
2319
|
+
return sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
|
|
2320
|
+
}
|
|
1723
2321
|
function sameFeedbackTarget(target, origin) {
|
|
1724
2322
|
if (!origin)
|
|
1725
2323
|
return false;
|
|
2324
|
+
const targetChannelType = tryParseChannelKey(target.channelKey)?.type
|
|
2325
|
+
?? target.channelKey.split('#', 1)[0];
|
|
2326
|
+
const targetChatType = target.chatType
|
|
2327
|
+
?? (targetChannelType === 'aun' ? undefined : 'private');
|
|
2328
|
+
if (!targetChatType)
|
|
2329
|
+
return false;
|
|
2330
|
+
const originDelivery = origin.delivery;
|
|
2331
|
+
const originChatType = originDelivery?.chatType ?? 'private';
|
|
2332
|
+
if (targetChatType !== originChatType)
|
|
2333
|
+
return false;
|
|
1726
2334
|
return target.channelKey === origin.channelKey
|
|
1727
2335
|
&& target.channelId === origin.channelId
|
|
1728
2336
|
&& target.session === origin.session
|