evolcore 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/dist/agents/claude-runner.js +35 -2
  4. package/dist/agents/codex-app-server-client.js +120 -14
  5. package/dist/agents/codex-runner.js +273 -35
  6. package/dist/agents/ecagent-runner.js +36 -4
  7. package/dist/agents/gemini-runner.js +1 -0
  8. package/dist/aun/aid/client.js +11 -21
  9. package/dist/aun/aid/managed-operation.js +107 -0
  10. package/dist/aun/msg/group.js +81 -12
  11. package/dist/aun/msg/managed-operation.js +804 -0
  12. package/dist/aun/msg/mention-schema.js +20 -0
  13. package/dist/aun/msg/p2p.js +7 -0
  14. package/dist/channels/aun.js +288 -108
  15. package/dist/channels/daemon.js +13 -0
  16. package/dist/cli/agent-command.js +56 -8
  17. package/dist/cli/agent.js +170 -6
  18. package/dist/cli/aun-commands.js +270 -69
  19. package/dist/cli/bench.js +12 -3
  20. package/dist/cli/daemon-commands.js +31 -31
  21. package/dist/cli/fs-command.js +60 -0
  22. package/dist/cli/handoff-command.js +3 -0
  23. package/dist/cli/index.js +30 -2
  24. package/dist/cli/managed-command-guard.js +41 -0
  25. package/dist/cli/model.js +11 -4
  26. package/dist/cli/queue-command.js +11 -1
  27. package/dist/cli/response.js +71 -0
  28. package/dist/cli/restart-monitor.js +4 -18
  29. package/dist/cli/trigger-command.js +4 -0
  30. package/dist/config/builtin-roles.js +4 -0
  31. package/dist/config/contact-book-store.js +11 -3
  32. package/dist/config/contact-request-service.js +161 -15
  33. package/dist/config/mention-mode.js +8 -24
  34. package/dist/core/auth/agent-delegation.js +0 -1
  35. package/dist/core/auth/operation-authorizer.js +30 -7
  36. package/dist/core/auth/operation-catalog.js +187 -25
  37. package/dist/core/bootstrap-messages.js +18 -0
  38. package/dist/core/bootstrap-service.js +63 -4
  39. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  40. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  41. package/dist/core/capability/skill-discovery.js +55 -0
  42. package/dist/core/channel-loader.js +1 -1
  43. package/dist/core/command/cli-intent-parser.js +9 -0
  44. package/dist/core/command/command-handler.js +467 -6
  45. package/dist/core/command/connect-menu.js +15 -0
  46. package/dist/core/command/group-menu.js +4 -4
  47. package/dist/core/command/menu-catalog.js +366 -0
  48. package/dist/core/command/menu-handler.js +147 -35
  49. package/dist/core/command/menu-protocol.js +71 -0
  50. package/dist/core/command/slash-gate.js +5 -2
  51. package/dist/core/command/slash-handler.js +41 -21
  52. package/dist/core/daemon-file-cache.js +2 -1
  53. package/dist/core/data-migration.js +9 -2
  54. package/dist/core/evolagent.js +4 -2
  55. package/dist/core/handoff/dispatcher.js +5 -2
  56. package/dist/core/handoff/runtime.js +145 -21
  57. package/dist/core/handoff/store.js +24 -0
  58. package/dist/core/handoff/types.js +1 -0
  59. package/dist/core/message/logical-queue-bridge.js +1 -0
  60. package/dist/core/message/mention-schema.js +126 -0
  61. package/dist/core/message/message-bridge.js +236 -78
  62. package/dist/core/message/message-log.js +1 -0
  63. package/dist/core/message/message-queue.js +1 -0
  64. package/dist/core/message/response-engine.js +54 -36
  65. package/dist/core/permission/ec-command-parser.js +156 -10
  66. package/dist/core/permission/sandbox-runtime.js +22 -1
  67. package/dist/core/permission/tool-policy.js +73 -30
  68. package/dist/core/protected-paths.js +7 -2
  69. package/dist/core/session/session-manager.js +21 -2
  70. package/dist/eck/manifest-engine.js +39 -13
  71. package/dist/index.js +384 -98
  72. package/dist/ipc.js +218 -9
  73. package/dist/response-system/coordinator.js +6 -4
  74. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  75. package/dist/response-system/modes/single-session/index.js +14 -1
  76. package/dist/response-system/selector.js +6 -5
  77. package/dist/trigger/anomaly-store.js +4 -0
  78. package/dist/trigger/script-executor.js +1 -0
  79. package/dist/utils/codex-app-server-registry.js +90 -0
  80. package/dist/utils/codex-cli.js +5 -1
  81. package/dist/utils/cross-platform.js +15 -0
  82. package/dist/utils/npm-ops.js +5 -12
  83. package/dist/utils/tool-summary.js +11 -0
  84. package/kits/docs/channels/aun.md +1 -1
  85. package/kits/docs/context-assembly.md +2 -2
  86. package/kits/docs/evolcore/agent.md +11 -6
  87. package/kits/docs/evolcore/aid.md +14 -0
  88. package/kits/docs/prompt-loading-architecture.md +1 -2
  89. package/kits/docs/venues/group.md +1 -1
  90. package/kits/eck_manifest.json +0 -12
  91. package/kits/rules/04-relation.md +1 -1
  92. package/kits/rules/05-venue.md +2 -2
  93. package/kits/schemas/single-session.schema.1.json +3 -3
  94. package/kits/schemas/single-session.schema.2.json +3 -3
  95. package/kits/templates/roles/admin.json +22 -0
  96. package/kits/templates/roles/member.json +27 -0
  97. package/kits/templates/roles/visitor.json +33 -3
  98. package/kits/templates/system-fragments/bootstrap.md +1 -1
  99. package/kits/templates/system-fragments/channel.md +1 -1
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/package.json +2 -2
  102. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -1,19 +1,7 @@
1
- import fs from 'fs';
2
1
  import os from 'os';
3
2
  import path from 'path';
4
3
  import { execCodexCliSync } from '../../../utils/codex-cli.js';
5
- function listDirectories(dir) {
6
- try {
7
- if (!fs.existsSync(dir))
8
- return [];
9
- return fs.readdirSync(dir, { withFileTypes: true })
10
- .filter(entry => entry.isDirectory())
11
- .map(entry => entry.name);
12
- }
13
- catch {
14
- return [];
15
- }
16
- }
4
+ import { discoverSkillDirectories } from '../skill-discovery.js';
17
5
  function discoverSkills(ctx) {
18
6
  const roots = [
19
7
  { dir: path.join(ctx.projectPath, '.codex', 'skills'), source: 'project' },
@@ -23,11 +11,12 @@ function discoverSkills(ctx) {
23
11
  const seen = new Set();
24
12
  const out = [];
25
13
  for (const root of roots) {
26
- for (const id of listDirectories(root.dir)) {
14
+ for (const skill of discoverSkillDirectories(root.dir)) {
15
+ const { id } = skill;
27
16
  if (seen.has(id))
28
17
  continue;
29
18
  seen.add(id);
30
- out.push({ id, label: id, source: root.source });
19
+ out.push({ id, label: id, desc: skill.description, source: root.source });
31
20
  }
32
21
  }
33
22
  return out;
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { parseDocument } from 'yaml';
4
+ function readSkillMetadata(skillDir) {
5
+ const skillFile = path.join(skillDir, 'SKILL.md');
6
+ try {
7
+ if (!fs.statSync(skillFile).isFile())
8
+ return undefined;
9
+ const text = fs.readFileSync(skillFile, 'utf-8');
10
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
11
+ if (!match)
12
+ return undefined;
13
+ const document = parseDocument(match[1]);
14
+ if (document.errors.length > 0)
15
+ return undefined;
16
+ const metadata = document.toJS();
17
+ if (typeof metadata?.name !== 'string' || !metadata.name.trim())
18
+ return undefined;
19
+ if (typeof metadata.description !== 'string' || !metadata.description.trim())
20
+ return undefined;
21
+ return {
22
+ id: metadata.name.trim(),
23
+ description: metadata.description.trim(),
24
+ };
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ /** Return only directories that satisfy the Codex/Claude skill file contract. */
31
+ export function discoverSkillDirectories(root) {
32
+ try {
33
+ if (!fs.existsSync(root))
34
+ return [];
35
+ return fs.readdirSync(root, { withFileTypes: true })
36
+ .filter(entry => !entry.name.startsWith('.'))
37
+ .filter(entry => entry.isDirectory() || entry.isSymbolicLink())
38
+ .map(entry => {
39
+ const dir = path.join(root, entry.name);
40
+ try {
41
+ if (!fs.statSync(dir).isDirectory())
42
+ return undefined;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ const metadata = readSkillMetadata(dir);
48
+ return metadata;
49
+ })
50
+ .filter((entry) => Boolean(entry));
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
@@ -196,7 +196,7 @@ export function buildReloadHooks(deps) {
196
196
  if (!handoffRuntime)
197
197
  return;
198
198
  try {
199
- await handoffRuntime.recover([aid]);
199
+ await handoffRuntime.recover([aid], { discardExpired: false });
200
200
  }
201
201
  finally {
202
202
  handoffRuntime.resumeAgent(aid);
@@ -33,6 +33,15 @@ export function parseCliIntent(inputArgv, source = 'menu.cli', options = {}) {
33
33
  return parseAidCommand(argv, source);
34
34
  if (cmd === 'storage')
35
35
  return parseStorageCommand(argv, source);
36
+ if (cmd === 'version') {
37
+ const parsed = parseArgs(argv.slice(1));
38
+ if (parsed.positionals.length > 0)
39
+ return raw(source, argv, 'Unexpected version positional args');
40
+ const unsupported = Object.keys(parsed.args).filter(key => key !== 'format' && key !== 'json');
41
+ if (unsupported.length > 0)
42
+ return raw(source, argv, `Unsupported version option: --${unsupported[0]}`);
43
+ return recognized('ec.version', 'relation', source, parsed.args, argv);
44
+ }
36
45
  if (cmd === 'status') {
37
46
  return recognized('system.status', 'process', source, parseArgs(argv.slice(1)), argv);
38
47
  }
@@ -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';
@@ -19,14 +20,19 @@ import { resolveConfigCommand } from '../../config/resolved-config-op.js';
19
20
  import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
20
21
  import { hashArgv } from '../auth/authorization-audit.js';
21
22
  import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
23
+ import { executeManagedAidOperation, ManagedAidOperationError, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
24
+ import { executeManagedGroupOperation, isManagedGroupRelationRead, ManagedGroupOperationError, resolveManagedGroupOperation, executeManagedMsgOperation, ManagedMsgOperationError, resolveManagedMsgOperation, executeManagedFsOperation, ManagedFsOperationError, resolveManagedFsOperation, } from '../../aun/msg/managed-operation.js';
22
25
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
26
+ import { isHClassPath, isLClassPath, isSameOrDescendant } from '../protected-paths.js';
23
27
  import { validateModelSelectionForRole } from '../model/model-permission.js';
28
+ import { getModelInfo } from '../model/model-catalog.js';
24
29
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
25
30
  import { parsePeerKey } from '../relation/peer-identity.js';
26
31
  import { isQuickCommand } from './slash-gate.js';
27
32
  import { handleSlashCommand } from './slash-handler.js';
28
33
  import { resolveChatMode } from '../message/peer-mode.js';
29
34
  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';
35
+ import { getMenuCatalog, execMenuValueBatch, validateMenuValueBatchArgs } from './menu-catalog.js';
30
36
  export { isProcessLevelOwner } from './menu-handler.js';
31
37
  const CLI_EXEC_TIMEOUT_MS = 15_000;
32
38
  const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
@@ -93,6 +99,7 @@ export class CommandHandler {
93
99
  triggerSchedulerFallbackAid;
94
100
  daemonStatusProvider;
95
101
  agentDelegationRegistry;
102
+ handoffRuntime;
96
103
  /**
97
104
  * Get the runner for a (channel, baseagent) pair.
98
105
  *
@@ -675,6 +682,9 @@ export class CommandHandler {
675
682
  setMessageQueue(messageQueue) {
676
683
  this.messageQueue = messageQueue;
677
684
  }
685
+ setHandoffRuntime(handoffRuntime) {
686
+ this.handoffRuntime = handoffRuntime;
687
+ }
678
688
  setPermissionGateway(gateway) {
679
689
  this.permissionGateway = gateway;
680
690
  }
@@ -818,6 +828,29 @@ export class CommandHandler {
818
828
  getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
819
829
  return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
820
830
  }
831
+ async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent') {
832
+ return await getMenuCatalog({
833
+ subject,
834
+ channelType: this.resolveChannelType(channel),
835
+ channelScope,
836
+ channelId,
837
+ request,
838
+ });
839
+ }
840
+ async execMenuValueBatch(channel, channelId, subject, args, request, channelScope = 'agent') {
841
+ validateMenuValueBatchArgs(args);
842
+ return await execMenuValueBatch({
843
+ subject,
844
+ channelType: this.resolveChannelType(channel),
845
+ channelScope,
846
+ channelId,
847
+ request,
848
+ items: args?.items,
849
+ schemaVersion: args?.schemaVersion,
850
+ consistency: args?.consistency,
851
+ 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'),
852
+ });
853
+ }
821
854
  /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
822
855
  async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
823
856
  return await menuGetSubMenuItems.call(this, cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel, authSubject, source);
@@ -1569,6 +1602,337 @@ export class CommandHandler {
1569
1602
  ? { ok: true, result }
1570
1603
  : { ok: false, code: result.code, error: result.error, ...(result.data ? { data: result.data } : {}) };
1571
1604
  }
1605
+ /** Agent-managed AID CLI entrypoint. Private key and CA access stay inside the daemon. */
1606
+ async handleAidOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1607
+ if (!this.agentDelegationRegistry) {
1608
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1609
+ }
1610
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1611
+ if (!delegation.ok)
1612
+ return { ok: false, code: delegation.code, error: delegation.reason };
1613
+ const session = await this.sessionManager.getSessionById(sessionId);
1614
+ if (!session)
1615
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1616
+ const parsed = resolveManagedAidOperation(argv);
1617
+ if (!parsed.ok)
1618
+ return { ok: false, code: parsed.code, error: parsed.reason };
1619
+ const grantSelfAid = delegation.grant.selfAid.replace(/^@/, '');
1620
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1621
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1622
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1623
+ }
1624
+ if (parsed.command.aid !== grantSelfAid) {
1625
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can be targeted' };
1626
+ }
1627
+ let conversationId;
1628
+ try {
1629
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
1630
+ }
1631
+ catch {
1632
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1633
+ }
1634
+ const subject = buildAuthSubject({
1635
+ selfAid: delegation.grant.selfAid,
1636
+ actorId: delegation.grant.actorId,
1637
+ channel: delegation.grant.channel,
1638
+ channelType: delegation.grant.channelType,
1639
+ channelId: conversationId,
1640
+ chatType: delegation.grant.chatType,
1641
+ conversationId,
1642
+ processOwners: loadDaemonConfig().owners ?? [],
1643
+ });
1644
+ const managementRole = isManagementRole(subject.role);
1645
+ const decision = authorizeOperation({
1646
+ source: 'agent-tool',
1647
+ subject,
1648
+ intent: {
1649
+ operation: parsed.command.operationId,
1650
+ scope: managementRole ? 'agent' : 'relation',
1651
+ source: 'agent-tool',
1652
+ args: { self: parsed.command.aid, targetId: parsed.command.aid },
1653
+ },
1654
+ auditMetadata: {
1655
+ argvHash: hashArgv(parsed.command.canonicalArgv),
1656
+ taskId: delegation.grant.taskId,
1657
+ messageId: delegation.grant.messageId,
1658
+ },
1659
+ });
1660
+ if (!decision.allow)
1661
+ return { ok: false, code: decision.code, error: decision.reason };
1662
+ try {
1663
+ return { ok: true, result: await executeManagedAidOperation(parsed.command) };
1664
+ }
1665
+ catch (error) {
1666
+ const message = error instanceof Error ? error.message : String(error);
1667
+ return {
1668
+ ok: false,
1669
+ code: error instanceof ManagedAidOperationError ? error.code : 'AID_OPERATION_FAILED',
1670
+ error: message,
1671
+ };
1672
+ }
1673
+ }
1674
+ /** Agent-managed group CLI entrypoint. AUN private material remains daemon-owned. */
1675
+ async handleGroupOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1676
+ if (!this.agentDelegationRegistry) {
1677
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1678
+ }
1679
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1680
+ if (!delegation.ok)
1681
+ return { ok: false, code: delegation.code, error: delegation.reason };
1682
+ const session = await this.sessionManager.getSessionById(sessionId);
1683
+ if (!session)
1684
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1685
+ const parsed = resolveManagedGroupOperation(argv);
1686
+ if (!parsed.ok)
1687
+ return { ok: false, code: parsed.code, error: parsed.reason };
1688
+ const grant = delegation.grant;
1689
+ const grantSelfAid = grant.selfAid.replace(/^@/, '');
1690
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1691
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1692
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1693
+ }
1694
+ if (parsed.command.from !== grantSelfAid) {
1695
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can perform managed group operations' };
1696
+ }
1697
+ let conversationId;
1698
+ try {
1699
+ conversationId = parsePeerKey(grant.peerKey).channelId;
1700
+ }
1701
+ catch {
1702
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1703
+ }
1704
+ const subject = buildAuthSubject({
1705
+ selfAid: grant.selfAid,
1706
+ actorId: grant.actorId,
1707
+ channel: grant.channel,
1708
+ channelType: grant.channelType,
1709
+ channelId: conversationId,
1710
+ chatType: grant.chatType,
1711
+ conversationId,
1712
+ processOwners: loadDaemonConfig().owners ?? [],
1713
+ });
1714
+ const managementRole = isManagementRole(subject.role);
1715
+ if (!managementRole && (grant.channelType !== 'aun' || !isManagedGroupRelationRead(parsed.command))) {
1716
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `Role ${subject.role} cannot manage Agent group membership or settings` };
1717
+ }
1718
+ if ((parsed.command.kind === 'dissolve' || parsed.command.kind === 'owner') && subject.role !== 'owner') {
1719
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `${parsed.command.kind} requires Agent owner permission` };
1720
+ }
1721
+ const decision = authorizeOperation({
1722
+ source: 'agent-tool',
1723
+ subject,
1724
+ intent: {
1725
+ operation: parsed.command.operationId,
1726
+ scope: managementRole ? 'agent' : 'relation',
1727
+ source: 'agent-tool',
1728
+ args: {
1729
+ self: parsed.command.from,
1730
+ ...(parsed.command.groupId ? {
1731
+ targetId: parsed.command.groupId,
1732
+ peer: parsed.command.groupId,
1733
+ peerKey: formatPeerKey(grant.channelType, parsed.command.groupId),
1734
+ } : {}),
1735
+ },
1736
+ },
1737
+ auditMetadata: {
1738
+ argvHash: hashArgv(parsed.command.canonicalArgv),
1739
+ taskId: grant.taskId,
1740
+ messageId: grant.messageId,
1741
+ },
1742
+ });
1743
+ if (!decision.allow)
1744
+ return { ok: false, code: decision.code, error: decision.reason };
1745
+ let rulesContent;
1746
+ if (parsed.command.kind === 'rules.set') {
1747
+ const filePath = String(parsed.command.filePath || '');
1748
+ const projectPath = path.resolve(session.projectPath);
1749
+ const resolvedPath = path.resolve(projectPath, filePath);
1750
+ if (!filePath || !isSameOrDescendant(resolvedPath, projectPath)) {
1751
+ return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file must be inside the current session project' };
1752
+ }
1753
+ if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
1754
+ return { ok: false, code: 'PROTECTED_RULES_PATH', error: 'Rules file cannot be read from a protected EvolCore path' };
1755
+ }
1756
+ try {
1757
+ const realProjectPath = fs.realpathSync(projectPath);
1758
+ const realPath = fs.realpathSync(resolvedPath);
1759
+ if (!isSameOrDescendant(realPath, realProjectPath)) {
1760
+ return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file resolves outside the current session project' };
1761
+ }
1762
+ const stat = fs.statSync(realPath);
1763
+ if (!stat.isFile() || stat.size < 1 || stat.size > 4 * 1024) {
1764
+ return { ok: false, code: 'INVALID_RULES_CONTENT', error: 'Rules file must be a regular file from 1 to 4096 bytes' };
1765
+ }
1766
+ rulesContent = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(realPath));
1767
+ }
1768
+ catch (error) {
1769
+ return {
1770
+ ok: false,
1771
+ code: 'INVALID_RULES_CONTENT',
1772
+ error: error instanceof Error ? error.message : String(error),
1773
+ };
1774
+ }
1775
+ }
1776
+ try {
1777
+ return { ok: true, result: await executeManagedGroupOperation(parsed.command, rulesContent) };
1778
+ }
1779
+ catch (error) {
1780
+ return {
1781
+ ok: false,
1782
+ code: error instanceof ManagedGroupOperationError ? error.code : 'GROUP_OPERATION_FAILED',
1783
+ error: error instanceof Error ? error.message : String(error),
1784
+ };
1785
+ }
1786
+ }
1787
+ /** Agent-managed private msg read entrypoint. AUN credentials and history roots remain daemon-owned. */
1788
+ async handleMsgOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1789
+ if (!this.agentDelegationRegistry) {
1790
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1791
+ }
1792
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1793
+ if (!delegation.ok)
1794
+ return { ok: false, code: delegation.code, error: delegation.reason };
1795
+ const session = await this.sessionManager.getSessionById(sessionId);
1796
+ if (!session)
1797
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1798
+ const parsed = resolveManagedMsgOperation(argv);
1799
+ if (!parsed.ok)
1800
+ return { ok: false, code: parsed.code, error: parsed.reason };
1801
+ const grant = delegation.grant;
1802
+ const grantSelfAid = grant.selfAid.replace(/^@/, '');
1803
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1804
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1805
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1806
+ }
1807
+ if (grant.channelType !== 'aun' || grant.chatType !== 'private') {
1808
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed msg reads require the current AUN private relation' };
1809
+ }
1810
+ let peerAid;
1811
+ try {
1812
+ peerAid = parsePeerKey(grant.peerKey).channelId.replace(/^@/, '');
1813
+ }
1814
+ catch {
1815
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1816
+ }
1817
+ if (parsed.command.from !== grantSelfAid || parsed.command.target !== peerAid) {
1818
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN private relation can be targeted' };
1819
+ }
1820
+ const subject = buildAuthSubject({
1821
+ selfAid: grant.selfAid,
1822
+ actorId: grant.actorId,
1823
+ channel: grant.channel,
1824
+ channelType: grant.channelType,
1825
+ channelId: peerAid,
1826
+ chatType: grant.chatType,
1827
+ conversationId: peerAid,
1828
+ processOwners: loadDaemonConfig().owners ?? [],
1829
+ });
1830
+ const decision = authorizeOperation({
1831
+ source: 'agent-tool',
1832
+ subject,
1833
+ intent: {
1834
+ operation: parsed.command.operationId,
1835
+ scope: 'relation',
1836
+ source: 'agent-tool',
1837
+ args: {
1838
+ self: parsed.command.from,
1839
+ targetId: parsed.command.target,
1840
+ peer: parsed.command.target,
1841
+ peerKey: formatPeerKey('aun', parsed.command.target),
1842
+ },
1843
+ },
1844
+ auditMetadata: {
1845
+ argvHash: hashArgv(parsed.command.canonicalArgv),
1846
+ taskId: grant.taskId,
1847
+ messageId: grant.messageId,
1848
+ },
1849
+ });
1850
+ if (!decision.allow)
1851
+ return { ok: false, code: decision.code, error: decision.reason };
1852
+ try {
1853
+ return { ok: true, result: await executeManagedMsgOperation(parsed.command) };
1854
+ }
1855
+ catch (error) {
1856
+ return {
1857
+ ok: false,
1858
+ code: error instanceof ManagedMsgOperationError ? error.code : 'MSG_OPERATION_FAILED',
1859
+ error: error instanceof Error ? error.message : String(error),
1860
+ };
1861
+ }
1862
+ }
1863
+ /** Agent-managed current-group filesystem read entrypoint. */
1864
+ async handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1865
+ if (!this.agentDelegationRegistry) {
1866
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1867
+ }
1868
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1869
+ if (!delegation.ok)
1870
+ return { ok: false, code: delegation.code, error: delegation.reason };
1871
+ const session = await this.sessionManager.getSessionById(sessionId);
1872
+ if (!session)
1873
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1874
+ const parsed = resolveManagedFsOperation(argv);
1875
+ if (!parsed.ok)
1876
+ return { ok: false, code: parsed.code, error: parsed.reason };
1877
+ const grant = delegation.grant;
1878
+ const actorAid = grant.selfAid.replace(/^@/, '');
1879
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1880
+ if (sessionSelfAid && sessionSelfAid !== actorAid) {
1881
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1882
+ }
1883
+ if (grant.channelType !== 'aun' || grant.chatType !== 'group') {
1884
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed fs reads require the current AUN group relation' };
1885
+ }
1886
+ let currentGroup;
1887
+ try {
1888
+ currentGroup = parsePeerKey(grant.peerKey).channelId;
1889
+ }
1890
+ catch {
1891
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1892
+ }
1893
+ if ((parsed.command.actorAid && parsed.command.actorAid !== actorAid) || parsed.command.groupId !== currentGroup) {
1894
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN group can be targeted' };
1895
+ }
1896
+ parsed.command.actorAid = actorAid;
1897
+ const subject = buildAuthSubject({
1898
+ selfAid: grant.selfAid,
1899
+ actorId: grant.actorId,
1900
+ channel: grant.channel,
1901
+ channelType: grant.channelType,
1902
+ channelId: currentGroup,
1903
+ chatType: grant.chatType,
1904
+ conversationId: currentGroup,
1905
+ processOwners: loadDaemonConfig().owners ?? [],
1906
+ });
1907
+ const decision = authorizeOperation({
1908
+ source: 'agent-tool',
1909
+ subject,
1910
+ intent: {
1911
+ operation: parsed.command.operationId,
1912
+ scope: 'relation',
1913
+ source: 'agent-tool',
1914
+ args: {
1915
+ self: actorAid,
1916
+ targetId: currentGroup,
1917
+ peer: currentGroup,
1918
+ peerKey: formatPeerKey('aun', currentGroup),
1919
+ },
1920
+ },
1921
+ auditMetadata: { argvHash: hashArgv(parsed.command.canonicalArgv), taskId: grant.taskId, messageId: grant.messageId },
1922
+ });
1923
+ if (!decision.allow)
1924
+ return { ok: false, code: decision.code, error: decision.reason };
1925
+ try {
1926
+ return { ok: true, result: await executeManagedFsOperation(parsed.command) };
1927
+ }
1928
+ catch (error) {
1929
+ return {
1930
+ ok: false,
1931
+ code: error instanceof ManagedFsOperationError ? error.code : 'FS_OPERATION_FAILED',
1932
+ error: error instanceof Error ? error.message : String(error),
1933
+ };
1934
+ }
1935
+ }
1572
1936
  setAgentDelegationRegistry(registry) {
1573
1937
  this.agentDelegationRegistry = registry;
1574
1938
  }
@@ -1576,7 +1940,7 @@ export class CommandHandler {
1576
1940
  * Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
1577
1941
  * 复用现有 slash cmd 逻辑,权限继承 session 用户角色
1578
1942
  */
1579
- async handleCtl(cmd, sessionId) {
1943
+ async handleCtl(cmd, sessionId, grant) {
1580
1944
  logger.info(`[ctl] cmd="${cmd}" sessionId=${sessionId}`);
1581
1945
  // 1. 白名单检查
1582
1946
  const inputCmd = cmd.split(' ')[0];
@@ -1588,9 +1952,56 @@ export class CommandHandler {
1588
1952
  if (!session) {
1589
1953
  return { ok: false, error: '无效的 session' };
1590
1954
  }
1955
+ if (!grant || grant.sessionId !== sessionId) {
1956
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'An active task delegation grant is required' };
1957
+ }
1958
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1959
+ const grantSelfAid = grant.selfAid.replace(/^@/, '');
1960
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1961
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1962
+ }
1963
+ let conversationId;
1964
+ try {
1965
+ conversationId = parsePeerKey(grant.peerKey).channelId;
1966
+ }
1967
+ catch {
1968
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1969
+ }
1970
+ const ctlSubject = buildAuthSubject({
1971
+ selfAid: grant.selfAid,
1972
+ actorId: grant.actorId,
1973
+ channel: grant.channel,
1974
+ channelType: grant.channelType,
1975
+ channelId: conversationId,
1976
+ chatType: grant.chatType,
1977
+ conversationId,
1978
+ processOwners: loadDaemonConfig().owners ?? [],
1979
+ });
1980
+ const ctlIntent = this.resolveCtlIntent(cmd, grantSelfAid, grant.peerKey);
1981
+ if (!ctlIntent)
1982
+ return { ok: false, code: 'NOT_ALLOWED', error: `不允许的指令: ${inputCmd}` };
1983
+ const ctlDecision = authorizeOperation({
1984
+ source: 'agent-tool',
1985
+ subject: ctlSubject,
1986
+ intent: ctlIntent,
1987
+ auditMetadata: {
1988
+ argvHash: hashArgv(['ctl', ...cmd.slice(1).split(/\s+/).filter(Boolean)]),
1989
+ taskId: grant.taskId,
1990
+ messageId: grant.messageId,
1991
+ },
1992
+ });
1993
+ if (!ctlDecision.allow)
1994
+ return { ok: false, code: ctlDecision.code, error: ctlDecision.reason };
1995
+ if (cmd.startsWith('/model info ')) {
1996
+ const modelId = cmd.slice('/model info '.length).trim();
1997
+ if (!modelId || /\s/.test(modelId))
1998
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'model info requires one model id' };
1999
+ const info = await getModelInfo(modelId, grantSelfAid, session.baseagent);
2000
+ return { ok: true, result: JSON.stringify(info, null, 2), data: { ok: true, ...info } };
2001
+ }
1591
2002
  // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
1592
2003
  const userId = session.metadata?.peerId;
1593
- const ctlIdentity = this.resolveCtlIdentity(session, userId);
2004
+ const ctlIdentity = ctlSubject.identity;
1594
2005
  // 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
1595
2006
  if (cmd.startsWith('/send ') || cmd === '/send') {
1596
2007
  // 解析 --encrypt 标志和消息文本
@@ -1625,9 +2036,9 @@ export class CommandHandler {
1625
2036
  const parts = sendArgs.split(/\s+/);
1626
2037
  const filePath = parts[parts.length - 1];
1627
2038
  if (filePath) {
1628
- const resolved = path.resolve(session.projectPath, filePath).replace(/\\/g, '/');
1629
- const projectPath = session.projectPath.replace(/\\/g, '/');
1630
- if (!resolved.startsWith(projectPath)) {
2039
+ const resolved = path.resolve(session.projectPath, filePath);
2040
+ const relative = path.relative(path.resolve(session.projectPath), resolved);
2041
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
1631
2042
  return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
1632
2043
  }
1633
2044
  }
@@ -1640,7 +2051,7 @@ export class CommandHandler {
1640
2051
  // 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
1641
2052
  try {
1642
2053
  const result = await this._handleInternal(cmd, session.channel, session.channelId, undefined, // 不发送消息
1643
- userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity);
2054
+ userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity, ctlSubject);
1644
2055
  const text = typeof result === 'string' ? result : (result && 'text' in result ? result.text : '(无输出)');
1645
2056
  if (result && typeof result === 'object' && 'kind' in result && result.kind === 'command.error') {
1646
2057
  return { ok: false, error: text || '执行失败' };
@@ -1651,6 +2062,56 @@ export class CommandHandler {
1651
2062
  return { ok: false, error: err.message };
1652
2063
  }
1653
2064
  }
2065
+ resolveCtlIntent(cmd, selfAid, peerKey) {
2066
+ const [name = '', ...args] = cmd.trim().slice(1).trim().split(/\s+/);
2067
+ const relationArgs = { self: selfAid, peer: peerKey, peerKey };
2068
+ const intent = (operation, extra = {}) => ({
2069
+ operation,
2070
+ scope: 'relation',
2071
+ source: 'agent-tool',
2072
+ args: { ...relationArgs, ...extra },
2073
+ });
2074
+ if (name === 'send')
2075
+ return intent('ec.ctl.send');
2076
+ if (name === 'file')
2077
+ return intent('ec.ctl.file');
2078
+ if (name === 'status')
2079
+ return intent('session.current');
2080
+ if (name === 'pwd')
2081
+ return intent('project.current');
2082
+ if (name === 'name' || name === 'rename')
2083
+ return intent('session.rename');
2084
+ if (name === 'queue') {
2085
+ const action = args.includes('--clear') ? 'clear'
2086
+ : args.includes('--cancel') ? 'cancel'
2087
+ : args.includes('--interrupt') ? 'interrupt'
2088
+ : 'read';
2089
+ return intent(`ec.ctl.queue.${action}`);
2090
+ }
2091
+ if (name === 'model') {
2092
+ if (args.length === 0 || args[0] === 'list')
2093
+ return intent('model.list');
2094
+ if (args[0] === 'current')
2095
+ return intent('model.current');
2096
+ if (args[0] === 'info')
2097
+ return intent('model.info', { model: args[1] });
2098
+ if (args[0] === 'reset')
2099
+ return intent('model.reset');
2100
+ if (args[0] === 'check') {
2101
+ return { operation: 'model.check', scope: 'agent', source: 'agent-tool', args: { self: selfAid } };
2102
+ }
2103
+ return intent('model.use', { model: args[0] });
2104
+ }
2105
+ if (name === 'effort') {
2106
+ return args.length === 0
2107
+ ? intent('model.effort.current')
2108
+ : intent('model.effort', { effort: args[0] });
2109
+ }
2110
+ if (name === 'restart') {
2111
+ return { operation: 'system.restart', scope: 'process', source: 'agent-tool', args: { self: selfAid } };
2112
+ }
2113
+ return null;
2114
+ }
1654
2115
  // ── Queue command ──
1655
2116
  /** 提取命名参数值(如 --cancel msg123 → "msg123") */
1656
2117
  extractArg(args, flag) {