evolcore 0.0.33 → 0.0.34

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 (129) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +12 -1
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +82 -13
  6. package/dist/agents/codex/diagnostics.js +37 -11
  7. package/dist/agents/codex-runner.js +218 -44
  8. package/dist/agents/ecagent-runner.js +73 -20
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/identity.js +1 -1
  11. package/dist/channels/aun.js +16 -6
  12. package/dist/channels/daemon.js +6 -3
  13. package/dist/cli/aun-commands.js +3 -2
  14. package/dist/cli/ctl-command.js +3 -1
  15. package/dist/cli/daemon-commands.js +120 -19
  16. package/dist/cli/fs-command.js +2 -1
  17. package/dist/cli/index.js +20 -6
  18. package/dist/cli/integrity-command.js +196 -0
  19. package/dist/cli/memory-command.js +158 -145
  20. package/dist/cli/thread-command.js +5 -3
  21. package/dist/cli/trigger-command.js +6 -1
  22. package/dist/config/config-field-policy.js +4 -0
  23. package/dist/config/config-manager.js +19 -0
  24. package/dist/config/contact-book-store.js +5 -0
  25. package/dist/config/role-service.js +13 -0
  26. package/dist/config/runtime-env.js +16 -0
  27. package/dist/config-store.js +30 -0
  28. package/dist/core/audit/log-integrity.js +3 -1
  29. package/dist/core/auth/agent-delegation.js +108 -35
  30. package/dist/core/auth/delegation-diagnostics.js +414 -0
  31. package/dist/core/auth/operation-authorizer.js +1 -1
  32. package/dist/core/auth/operation-catalog.js +16 -0
  33. package/dist/core/command/agent-control.js +18 -26
  34. package/dist/core/command/command-handler.js +138 -4
  35. package/dist/core/command/daemon-diagnostics.js +1 -0
  36. package/dist/core/command/menu-catalog.js +3 -0
  37. package/dist/core/command/menu-handler.js +22 -7
  38. package/dist/core/command/menu-protocol.js +1 -0
  39. package/dist/core/command/slash-handler.js +2 -0
  40. package/dist/core/daemon-bootstrap.js +4 -7
  41. package/dist/core/file-integrity/read.js +61 -0
  42. package/dist/core/file-integrity/store.js +262 -0
  43. package/dist/core/ipc-registration.js +36 -1
  44. package/dist/core/memory/batch-meta.js +132 -0
  45. package/dist/core/memory/consolidation-context.js +35 -14
  46. package/dist/core/memory/consolidation-ops-records.js +14 -16
  47. package/dist/core/memory/consolidation.js +91 -26
  48. package/dist/core/memory/index-manager.js +19 -28
  49. package/dist/core/memory/memory-admin.js +19 -14
  50. package/dist/core/memory/memory-document.js +53 -9
  51. package/dist/core/memory/memory-writer.js +30 -15
  52. package/dist/core/message/im-renderer.js +20 -4
  53. package/dist/core/message/message-bridge.js +2 -1
  54. package/dist/core/message/response-engine.js +584 -47
  55. package/dist/core/permission/ec-command-parser.js +93 -4
  56. package/dist/core/permission/protected-paths.js +5 -1
  57. package/dist/core/permission/readonly-shell-query.js +43 -5
  58. package/dist/core/permission/session-path-grants.js +334 -0
  59. package/dist/core/permission/tool-canonical.js +226 -0
  60. package/dist/core/permission/tool-policy.js +30 -62
  61. package/dist/core/role/runtime-policy.js +21 -10
  62. package/dist/core/session/session-fs-store.js +123 -8
  63. package/dist/core/session/session-manager.js +75 -5
  64. package/dist/core/skill/index-generator.js +17 -3
  65. package/dist/core/system-session/grants.js +40 -0
  66. package/dist/core/system-session/memory.js +222 -0
  67. package/dist/core/system-session/registry.js +74 -0
  68. package/dist/core/system-session/types.js +14 -0
  69. package/dist/core/thread/thread-service.js +13 -20
  70. package/dist/eck/kit-renderer.js +7 -2
  71. package/dist/eck/manifest-engine.js +24 -4
  72. package/dist/index.js +11 -2
  73. package/dist/ipc.js +51 -40
  74. package/dist/paths.js +70 -0
  75. package/dist/stats/billing.js +20 -8
  76. package/dist/trigger/feedback.js +6 -1
  77. package/dist/trigger/parser.js +8 -1
  78. package/dist/trigger/system-definitions.js +3 -3
  79. package/dist/utils/cross-platform.js +11 -2
  80. package/dist/utils/error-dict.json +18 -4
  81. package/dist/utils/error-utils.js +121 -0
  82. package/dist/utils/instance-registry.js +13 -8
  83. package/dist/utils/log-writer.js +12 -0
  84. package/dist/utils/process-introspect.js +11 -0
  85. package/kits/docs/context-assembly.md +0 -1
  86. package/kits/docs/evolcore/agent-create.md +5 -4
  87. package/kits/docs/evolcore/agent.md +2 -2
  88. package/kits/docs/evolcore/config.md +2 -2
  89. package/kits/docs/evolcore/ctl.md +2 -2
  90. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  91. package/kits/docs/evolcore/fs-architecture.md +4 -5
  92. package/kits/docs/evolcore/fs.md +1 -1
  93. package/kits/docs/evolcore/group-collaboration.md +2 -2
  94. package/kits/docs/evolcore/group.md +10 -10
  95. package/kits/docs/evolcore/memory-architecture.md +4 -5
  96. package/kits/docs/evolcore/memory-consolidation.md +18 -14
  97. package/kits/docs/evolcore/memory-tags.md +3 -1
  98. package/kits/docs/evolcore/memory.md +24 -12
  99. package/kits/docs/evolcore/model.md +16 -0
  100. package/kits/docs/evolcore/msg.md +7 -7
  101. package/kits/docs/evolcore/thread-chain.md +5 -5
  102. package/kits/docs/prompt-loading-architecture.md +0 -1
  103. package/kits/eck_manifest.json +0 -20
  104. package/kits/eck_manifest.memory.json +0 -12
  105. package/kits/rules/01-overview.md +11 -0
  106. package/kits/rules/03-identity.md +6 -3
  107. package/kits/rules/06-channel.md +1 -1
  108. package/kits/rules/07-memory-and-thread.md +12 -15
  109. package/kits/schemas/_meta.json +7 -1
  110. package/kits/schemas/agent-config.schema.13.json +1 -1
  111. package/kits/schemas/daemon.schema.7.json +140 -0
  112. package/kits/skills/ec-group/SKILL.md +4 -4
  113. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  114. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  115. package/kits/templates/memory-consolidation/forget.md +2 -2
  116. package/kits/templates/memory-consolidation/generalize.md +4 -4
  117. package/kits/templates/memory-consolidation/notes.md +11 -11
  118. package/kits/templates/memory-consolidation/profile.md +3 -3
  119. package/kits/templates/memory-consolidation/promote.md +3 -3
  120. package/kits/templates/memory-consolidation/workflow.md +6 -6
  121. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  122. package/kits/templates/system-fragments/bootstrap.md +4 -3
  123. package/kits/templates/system-fragments/channel.md +2 -2
  124. package/kits/templates/system-fragments/memory.md +16 -9
  125. package/kits/templates/system-fragments/reply.md +6 -6
  126. package/kits/templates/system-fragments/session.md +1 -1
  127. package/package.json +4 -3
  128. package/dist/core/memory/memory-session-policy.js +0 -41
  129. package/dist/core/memory/memory-signature.js +0 -257
@@ -1,5 +1,6 @@
1
1
  import { BaseagentRunnerUnavailableError } from '../../agents/runner-types.js';
2
2
  import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
3
+ import { grantSessionPath, listRuntimePathGrants, revokeSessionPath } from '../permission/session-path-grants.js';
3
4
  import { closeDiagnosticSession, diagnosticTokenHash, isDiagnosticSessionActive } from './diag-service.js';
4
5
  import { parseMenuClientArgs } from '../../cli/menu-client.js';
5
6
  import { ensureSessionRuntimeDir } from '../../cli/task-context.js';
@@ -20,6 +21,7 @@ import { agentmdGet } from '../../aun/aid/agentmd.js';
20
21
  import { parseDuration, parseTriggerSet, parseTriggerUpdate } from '../../trigger/parser.js';
21
22
  import { applyTriggerPatch, replaceEditableTriggerDefinition } from '../../trigger/patch.js';
22
23
  import { definitionRevision } from '../../trigger/validation.js';
24
+ import { isDaemonEventSessionTrigger, matchTriggerEvent } from '../../trigger/event-source.js';
23
25
  import { tryParseChannelKey } from '../channel-loader.js';
24
26
  import { displaySessionTitle } from '../session/session-title.js';
25
27
  import { buildTranscriptTurns } from '../session/session-transcript.js';
@@ -45,7 +47,7 @@ import { handleSlashCommand } from './slash-handler.js';
45
47
  import { resolveChatMode } from '../message/peer-mode.js';
46
48
  import { execMenuAction as menuExecMenuAction, execMenuForEcweb as menuExecMenuForEcweb, execMenuForControl as menuExecMenuForControl, execMenuQuery as menuExecMenuQuery, execMenuUpdate as menuExecMenuUpdate, getMenuItems as menuGetMenuItems, getSubMenuItems as menuGetSubMenuItems, buildTopicForkSourcePayload, } from './menu-handler.js';
47
49
  import { getMenuCatalog, execMenuValueBatch, validateMenuValueBatchArgs } from './menu-catalog.js';
48
- import { resolveCurrentSessionLoopTarget } from '../session/session-idle-monitor.js';
50
+ import { resolveCurrentSessionLoopTarget, sessionLoopError } from '../session/session-idle-monitor.js';
49
51
  export { isProcessLevelOwner } from './menu-handler.js';
50
52
  const CLI_EXEC_TIMEOUT_MS = 15_000;
51
53
  const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
@@ -372,6 +374,45 @@ export class CommandHandler {
372
374
  getTriggerSchedulerForAgent(agentAid) {
373
375
  return this.triggerSchedulerResolver?.(agentAid) ?? (this.triggerSchedulerFallbackAid === agentAid ? this.triggerSchedulerFallback : undefined);
374
376
  }
377
+ assertSessionLoopTrigger(session) {
378
+ const daemonAid = this.getDaemonTriggerOwnerAid();
379
+ const schedulers = new Set([
380
+ this.getTriggerSchedulerForAgent(session.selfAID ?? ''),
381
+ daemonAid ? this.getTriggerSchedulerForAgent(daemonAid) : undefined,
382
+ ]);
383
+ const triggers = [...schedulers].flatMap(scheduler => scheduler?.list({ all: true }) ?? [])
384
+ .filter(definition => definition.id === '__session-idle-check'
385
+ || (definition.source.type === 'event' && definition.source.eventPattern === 'session:idle'));
386
+ if (triggers.length === 0) {
387
+ throw sessionLoopError('LOOP_TRIGGER_NOT_FOUND', '未找到 session:idle Trigger,无法开启 loop');
388
+ }
389
+ const enabled = triggers.filter(definition => definition.enabled);
390
+ if (enabled.length === 0) {
391
+ throw sessionLoopError('LOOP_TRIGGER_DISABLED', 'session:idle Trigger 未开启,无法开启 loop');
392
+ }
393
+ const event = {
394
+ type: 'session:idle',
395
+ sessionId: session.id,
396
+ agentAid: session.selfAID,
397
+ channel: session.channelType || session.channel,
398
+ channelName: session.channel,
399
+ channelId: session.channelId,
400
+ chatType: session.chatType,
401
+ ...(session.threadId ? { threadId: session.threadId } : {}),
402
+ peerId: session.metadata?.peerId || session.channelId,
403
+ };
404
+ const valid = enabled.some(definition => definition.source.type === 'event'
405
+ && definition.source.eventPattern === 'session:idle'
406
+ && definition.execution.type === 'target_session'
407
+ && !!definition.execution.prompt?.trim()
408
+ && definition.feedback.strategy === 'event_session'
409
+ && (definition.agentAid === session.selfAID || isDaemonEventSessionTrigger(definition))
410
+ && matchTriggerEvent(definition.source, event).matched);
411
+ if (!valid) {
412
+ throw sessionLoopError('LOOP_TRIGGER_INVALID', 'session:idle Trigger 配置不符合要求:事件过滤条件须允许接收当前会话的闲置事件,'
413
+ + '并使用 target_session 执行、event_session 动态路由,且提示词非空');
414
+ }
415
+ }
375
416
  triggerSourceFromSchedule(scheduleType, scheduleValue) {
376
417
  if (scheduleType === 'once')
377
418
  return { type: 'once' };
@@ -463,6 +504,7 @@ export class CommandHandler {
463
504
  createdAt: definition.createdAt,
464
505
  updatedAt: definition.updatedAt,
465
506
  status: definition.enabled ? 'active' : 'disabled',
507
+ ...(definition.description ? { description: definition.description } : {}),
466
508
  limits: definition.limits,
467
509
  limitState: schedulerDetails?.limitState,
468
510
  subscription: schedulerDetails?.subscription,
@@ -715,6 +757,7 @@ export class CommandHandler {
715
757
  agentAid: schedulerAid,
716
758
  enabled: true,
717
759
  name,
760
+ ...(parsed.description ? { description: parsed.description } : {}),
718
761
  createdAt: now,
719
762
  updatedAt: now,
720
763
  ...(authorizedBy ? { authorizedBy } : {}),
@@ -1822,6 +1865,7 @@ export class CommandHandler {
1822
1865
  // ── Agent Ctl ──
1823
1866
  static CTL_COMMANDS = [
1824
1867
  '/status', '/pwd', '/file', '/send', '/rename', '/name', '/queue', '/restart', '/model', '/effort',
1868
+ '/path',
1825
1869
  ];
1826
1870
  /**
1827
1871
  * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
@@ -2462,7 +2506,7 @@ export class CommandHandler {
2462
2506
  selfAid: grant.selfAid,
2463
2507
  peerKey: grant.peerKey,
2464
2508
  channel: grant.channel,
2465
- role: grant.issuedRole,
2509
+ role: grant.issuedRole ?? 'none',
2466
2510
  taskId: grant.taskId,
2467
2511
  messageId: grant.messageId,
2468
2512
  argvHash: hashArgv(argv),
@@ -2507,7 +2551,7 @@ export class CommandHandler {
2507
2551
  // stay pinned here, and authorizeOperation below remains the authority
2508
2552
  // for every role (member carries currentRelationOnly; visitor is denied).
2509
2553
  const mayReadCrossPeer = hasTrustedFullAccessDelegation(grant)
2510
- || (parsed.command.kind === 'history' && isAdminRole(grant.issuedRole));
2554
+ || (parsed.command.kind === 'history' && isAdminRole(grant.issuedRole ?? 'none'));
2511
2555
  if (!mayReadCrossPeer && parsed.command.target !== conversationId) {
2512
2556
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN private relation can be targeted' };
2513
2557
  }
@@ -2530,7 +2574,7 @@ export class CommandHandler {
2530
2574
  channelId: conversationId,
2531
2575
  chatType: grant.chatType,
2532
2576
  conversationId,
2533
- ...(isSelectorHistory ? { identity: roleToSessionIdentity(grant.issuedRole) } : {}),
2577
+ ...(isSelectorHistory ? { identity: roleToSessionIdentity(grant.issuedRole ?? null) } : {}),
2534
2578
  processOwners: loadDaemonConfig().owners ?? [],
2535
2579
  ...trustedExecutionIdentityInput(grant),
2536
2580
  });
@@ -2983,6 +3027,15 @@ export class CommandHandler {
2983
3027
  const args = cmd.slice('/queue'.length).trim();
2984
3028
  return await this.handleQueueCommand(sessionId, args);
2985
3029
  }
3030
+ // 5.2 /path: 运行时路径授权(E3-a)。授权校验见 handlePathCommand。
3031
+ if (cmd === '/path' || cmd.startsWith('/path ')) {
3032
+ const args = cmd.slice('/path'.length).trim();
3033
+ return await this.handlePathCommand(sessionId, args, {
3034
+ selfAid: grantSelfAid,
3035
+ actorId: grant.actorId,
3036
+ role: ctlSubject.role,
3037
+ });
3038
+ }
2986
3039
  // 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
2987
3040
  try {
2988
3041
  const result = await this._handleInternal(cmd, session.channel, session.channelId, undefined, // 不发送消息
@@ -3006,6 +3059,10 @@ export class CommandHandler {
3006
3059
  source: 'agent-tool',
3007
3060
  args: { ...relationArgs, ...extra },
3008
3061
  });
3062
+ if (name === 'path') {
3063
+ const action = args[0] === 'grant' ? 'grant' : args[0] === 'revoke' ? 'revoke' : 'list';
3064
+ return intent(`ec.ctl.path.${action}`);
3065
+ }
3009
3066
  if (name === 'send')
3010
3067
  return intent('ec.ctl.send');
3011
3068
  if (name === 'file')
@@ -3047,6 +3104,83 @@ export class CommandHandler {
3047
3104
  }
3048
3105
  return null;
3049
3106
  }
3107
+ // ── Path grant command ──
3108
+ /**
3109
+ * `ec ctl path grant|revoke|list` —— 运行时路径授权(E3-a)。
3110
+ *
3111
+ * 这是提权操作:授予一条路径等于扩大该会话能触及的文件范围。约束有三条,
3112
+ * 缺一条这套机制就能被会话自己解除:
3113
+ * 1. 只有 owner / admin 可调用;
3114
+ * 2. 授予只对**当前会话**生效,不写进任何全局配置;
3115
+ * 3. 会话结束时由 releaseSessionPathGrants(sessionId) 清掉。
3116
+ *
3117
+ * 见 docs/memory/session-profile-design.md §4。
3118
+ */
3119
+ async handlePathCommand(sessionId, args, actor) {
3120
+ if (actor.role !== 'owner' && actor.role !== 'admin') {
3121
+ return {
3122
+ ok: false,
3123
+ code: 'ROLE_ACCESS_DENIED',
3124
+ error: `路径授权仅 owner / admin 可操作(当前角色:${actor.role ?? 'none'})`,
3125
+ };
3126
+ }
3127
+ const [action = 'list', ...rest] = args.split(/\s+/).filter(Boolean);
3128
+ const flag = (name) => {
3129
+ const index = rest.indexOf(name);
3130
+ return index >= 0 ? rest[index + 1] : undefined;
3131
+ };
3132
+ if (action === 'list') {
3133
+ const grants = listRuntimePathGrants(sessionId);
3134
+ if (grants.length === 0)
3135
+ return { ok: true, result: '当前会话没有运行时路径授权', data: [] };
3136
+ const data = grants.map(record => ({
3137
+ path: record.path,
3138
+ access: record.access,
3139
+ ...(record.identity ? { identity: record.identity } : {}),
3140
+ ...(record.grantedBy ? { grantedBy: record.grantedBy } : {}),
3141
+ grantedAt: new Date(record.grantedAt).toISOString(),
3142
+ }));
3143
+ return {
3144
+ ok: true,
3145
+ data,
3146
+ result: data.map(item => `${item.access.padEnd(5)} ${item.path}`).join('\n'),
3147
+ };
3148
+ }
3149
+ const rawPath = rest[0];
3150
+ if (!rawPath || rawPath.startsWith('--')) {
3151
+ return { ok: false, code: 'INVALID_ARGUMENT', error: '缺少路径。用法: ec ctl path grant <绝对路径> [--access read|write|none]' };
3152
+ }
3153
+ if (!path.isAbsolute(rawPath)) {
3154
+ return { ok: false, code: 'INVALID_ARGUMENT', error: '路径必须是绝对路径' };
3155
+ }
3156
+ const resolvedPath = path.resolve(rawPath);
3157
+ if (action === 'revoke') {
3158
+ return revokeSessionPath(sessionId, resolvedPath)
3159
+ ? { ok: true, result: `已撤销 ${resolvedPath}` }
3160
+ : { ok: false, code: 'NOT_FOUND', error: `当前会话没有该路径的运行时授权: ${resolvedPath}` };
3161
+ }
3162
+ if (action !== 'grant') {
3163
+ return { ok: false, code: 'INVALID_ARGUMENT', error: `未知子命令: ${action}(支持 grant / revoke / list)` };
3164
+ }
3165
+ const accessValue = flag('--access') ?? 'read';
3166
+ if (accessValue !== 'read' && accessValue !== 'write' && accessValue !== 'none') {
3167
+ return { ok: false, code: 'INVALID_ARGUMENT', error: '--access 必须是 read / write / none' };
3168
+ }
3169
+ const identity = flag('--identity')?.trim();
3170
+ grantSessionPath({
3171
+ sessionId,
3172
+ path: resolvedPath,
3173
+ access: accessValue,
3174
+ ...(identity ? { identity } : {}),
3175
+ ...(actor.actorId ? { grantedBy: actor.actorId } : {}),
3176
+ grantedAt: Date.now(),
3177
+ });
3178
+ logger.info(`[PathGrant] granted session=${sessionId} path=${resolvedPath} access=${accessValue} by=${actor.actorId ?? 'unknown'} self=${actor.selfAid}`);
3179
+ return {
3180
+ ok: true,
3181
+ result: `✓ 已授予 ${accessValue} 于 ${resolvedPath}(仅当前会话,会话结束自动撤销)`,
3182
+ };
3183
+ }
3050
3184
  // ── Queue command ──
3051
3185
  /** 提取命名参数值(如 --cancel msg123 → "msg123") */
3052
3186
  extractArg(args, flag) {
@@ -30,6 +30,7 @@ const LOG_SOURCES = [
30
30
  { id: 'menu-protocol', label: 'Menu Protocol', pattern: /^menu-protocol(?:-.+)?\.log$/ },
31
31
  { id: 'causation', label: 'Causation spans', pattern: /^causation-spans(?:-.+)?\.log$/ },
32
32
  { id: 'command-audit', label: 'Command audit', pattern: /^command-audit(?:-.+)?\.log$/ },
33
+ { id: 'delegation-audit', label: 'Delegation audit', pattern: /^delegation-audit(?:-.+)?\.log$/ },
33
34
  { id: 'role-mutations', label: 'Role mutations', pattern: /^role-mutations(?:-.+)?\.log$/ },
34
35
  ];
35
36
  const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
@@ -178,6 +178,9 @@ export async function resolveMenuCatalogContext(input) {
178
178
  processRole,
179
179
  isDaemonOwner: processRole === 'daemon-owner',
180
180
  fromControlChannel: subject.fromControlChannel,
181
+ ecwebManagement: subject.ecwebManagement,
182
+ agentFullAccessManagement: subject.agentFullAccessManagement,
183
+ canApprovePersistentFullAccess: subject.canApprovePersistentFullAccess,
181
184
  allowAccess: processRole !== 'none' || rebuilt.allowAccess,
182
185
  };
183
186
  const access = authorizeAccess(bound);
@@ -1144,6 +1144,18 @@ function buildMenuAuthSubject(owner, params) {
1144
1144
  fromControlChannel: params.fromControlChannel,
1145
1145
  });
1146
1146
  }
1147
+ function agentFullAccessMenuSubject(subject, source) {
1148
+ return {
1149
+ ...subject,
1150
+ agentFullAccessManagement: source === 'control'
1151
+ ? subject.agentFullAccessManagement === true
1152
+ : source === 'menu'
1153
+ && subject.channelType === 'aun'
1154
+ && !subject.fromControlChannel
1155
+ && subject.canApprovePersistentFullAccess === true
1156
+ && subject.processRole === 'daemon-owner',
1157
+ };
1158
+ }
1147
1159
  async function authorizeMenuIntent(params) {
1148
1160
  const { intent, identity, session, channel, channelId, userId, fromControlChannel } = params;
1149
1161
  if (!intent)
@@ -1719,8 +1731,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1719
1731
  if (!selfAid)
1720
1732
  throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
1721
1733
  const res = this.agentApplicationService
1722
- ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService, subject)
1723
- : await execAgentQuery({ aid: selfAid }, undefined, subject);
1734
+ ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService, agentFullAccessMenuSubject(subject, source))
1735
+ : await execAgentQuery({ aid: selfAid }, undefined, agentFullAccessMenuSubject(subject, source));
1724
1736
  if ('error' in res)
1725
1737
  throw { code: res.code, message: res.error };
1726
1738
  const ag = res.data;
@@ -2286,8 +2298,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2286
2298
  if (fromControlChannel) {
2287
2299
  if (args?.aid) {
2288
2300
  return this.agentApplicationService
2289
- ? await execAgentQuery(args, this.agentApplicationService, subject)
2290
- : await execAgentQuery(args, undefined, subject);
2301
+ ? await execAgentQuery(args, this.agentApplicationService, agentFullAccessMenuSubject(subject, source))
2302
+ : await execAgentQuery(args, undefined, agentFullAccessMenuSubject(subject, source));
2291
2303
  }
2292
2304
  return this.agentApplicationService
2293
2305
  ? await execAgentOptions(args, this.agentApplicationService)
@@ -2297,8 +2309,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2297
2309
  if (!selfAid)
2298
2310
  return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2299
2311
  return this.agentApplicationService
2300
- ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService, subject)
2301
- : await execAgentQuery({ ...(args ?? {}), aid: selfAid }, undefined, subject);
2312
+ ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService, agentFullAccessMenuSubject(subject, source))
2313
+ : await execAgentQuery({ ...(args ?? {}), aid: selfAid }, undefined, agentFullAccessMenuSubject(subject, source));
2302
2314
  }
2303
2315
  // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
2304
2316
  if (cmdBase === '/gateway') {
@@ -2944,6 +2956,8 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2944
2956
  const config = value === true
2945
2957
  ? { enabled: true, idleSeconds: args?.idleSeconds ?? DEFAULT_SESSION_LOOP_SECONDS }
2946
2958
  : { enabled: false };
2959
+ if (config.enabled)
2960
+ this.assertSessionLoopTrigger(current.session);
2947
2961
  return { data: sessionLoopPayload(writeSessionLoopConfig(this.sessionManager, current, config)) };
2948
2962
  }
2949
2963
  catch (error) {
@@ -3705,7 +3719,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3705
3719
  }
3706
3720
  }
3707
3721
  }
3708
- return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService, subject);
3722
+ return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService, agentFullAccessMenuSubject(subject, source));
3709
3723
  }
3710
3724
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
3711
3725
  if (cmdBase === '/trigger') {
@@ -4820,6 +4834,7 @@ async function execMenuForSystemControl(payload, context) {
4820
4834
  : undefined,
4821
4835
  });
4822
4836
  const trustedIdentity = trustedSubject.identity;
4837
+ trustedSubject.agentFullAccessManagement = context.source === 'control' && context.authMethod === 'daemon-owner';
4823
4838
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
4824
4839
  if (payload?.type === 'menu.action'
4825
4840
  && menuCommandToken(cmd) === '/system'
@@ -470,6 +470,7 @@ const STABLE_CODES = new Set([
470
470
  'RESPONSE_TOO_LARGE',
471
471
  'ACTIVITY_NOT_FOUND', 'ACTIVITY_EXPIRED',
472
472
  'TOPIC_SESSION_NOT_FOUND', 'TOPIC_SESSION_LOOKUP_FAILED',
473
+ 'LOOP_TRIGGER_NOT_FOUND', 'LOOP_TRIGGER_DISABLED', 'LOOP_TRIGGER_INVALID',
473
474
  'ROOT_NOT_FOUND', 'ROOT_FORBIDDEN', 'ROOT_EXPIRED', 'ROOT_SCOPE_MISMATCH', 'ROOT_PEER_MISMATCH',
474
475
  'INVALID_ROOT_ID', 'MISSING_ROOT_ID', 'INVALID_ROOT_PATH', 'NOT_A_DIRECTORY', 'FILE_TOO_LARGE',
475
476
  ]);
@@ -603,6 +603,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
603
603
  }
604
604
  try {
605
605
  const latest = await refreshSessionLoopTarget(this.sessionManager, route, target);
606
+ if (next.enabled)
607
+ this.assertSessionLoopTrigger(latest.session);
606
608
  const updated = writeSessionLoopConfig(this.sessionManager, latest, next);
607
609
  const payload = sessionLoopPayload(updated);
608
610
  if (this.shouldSuppressCardTriggerResult(source, channel))
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import { resolvePaths } from '../paths.js';
5
5
  import { AUNChannel } from '../channels/aun.js';
6
6
  import { BindService } from '../utils/aid-bind.js';
7
- import { commandExists } from '../utils/cross-platform.js';
7
+ import { detectBaseagentDetails } from '../agents/baseagent-availability.js';
8
8
  import { logger } from '../utils/logger.js';
9
9
  import { remoteAccessService } from './auth/remote-access-store.js';
10
10
  import { IpcServer } from '../ipc.js';
@@ -13,12 +13,9 @@ import { controlReplyContext } from './message/outbound.js';
13
13
  import { TriggerDefinitionManager } from '../trigger/manager.js';
14
14
  import { seedUpgradeCheckTrigger } from '../trigger/system-definitions.js';
15
15
  export function detectAvailableBaseagents() {
16
- const out = [];
17
- for (const command of ['claude', 'gemini', 'codex']) {
18
- if (commandExists(command))
19
- out.push(command);
20
- }
21
- return out;
16
+ // 与 init/ECWeb 的探测保持同一判据;claude 在这里也走内置二进制,
17
+ // 不再只看 PATH——否则 daemon 会对外声明一个实际跑不起来的 baseagent。
18
+ return detectBaseagentDetails().filter(item => item.detected).map(item => item.id);
22
19
  }
23
20
  export async function runBindBootstrapDaemon(daemonCfg) {
24
21
  logger.warn('[daemon-bootstrap] starting control AID + IPC only');
@@ -0,0 +1,61 @@
1
+ // 读取受台账保护的文件,并按调用方策略决定是否校验。
2
+ //
3
+ // 为什么要有这一层:ECK 文档和技能文件的读取走 FileCache,`on-reload` 策略下
4
+ // 缓存命中时根本不会碰磁盘。篡改发生在首次读盘之后时,只看盘上字节是发现不了的;
5
+ // 所以这里校验的是**将要被使用的那份内容**(缓存里的或刚读到的),
6
+ // 让读到的内容就是校验过的内容(TOCTOU)。
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { kitsDir } from '../../paths.js';
10
+ import { logger } from '../../utils/logger.js';
11
+ import { verifyFile } from './store.js';
12
+ /**
13
+ * 判断一个路径是否属于随包发布的内容(`kits/`)。
14
+ *
15
+ * 只有随包发布的东西才该建立基线:它们在开发仓里由 agent 随手改、打包后应当冻结。
16
+ * `$EVOLCORE_HOME` 下的运行时数据不能套用这套语义——那些文件本来就是给用户和
17
+ * CLI 改的,一旦校验就会把合法修改回滚掉。
18
+ */
19
+ export function isShippedPath(filePath) {
20
+ const kits = path.resolve(kitsDir());
21
+ const target = path.resolve(filePath);
22
+ const rel = path.relative(kits, target);
23
+ return rel !== '' && !path.isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${path.sep}`);
24
+ }
25
+ /**
26
+ * 返回可用于本次注入的文档内容。
27
+ *
28
+ * 返回 null 有三种情况,调用方都应拒绝使用该文档(本函数已记日志):
29
+ * - 文件不存在或读不出来
30
+ * - sha 与台账不符、且快照也损坏,恢复不了
31
+ *
32
+ * 台账无记录时按设计用本次读到的内容自动登记,视为 verified(安装模式首次加载建基线)。
33
+ */
34
+ export function readVerifiedText(filePath, opts) {
35
+ const tag = opts.tag ?? 'file-integrity';
36
+ if (!opts.verify) {
37
+ try {
38
+ return { text: fs.readFileSync(filePath, 'utf-8'), status: 'unverified' };
39
+ }
40
+ catch (err) {
41
+ logger.warn(`[${tag}] read failed path=${filePath} ${err instanceof Error ? err.message : String(err)}`);
42
+ return null;
43
+ }
44
+ }
45
+ const restoreOnFail = opts.restoreOnFail ?? true;
46
+ const result = verifyFile(filePath, { restoreOnFail });
47
+ if (result.status === 'missing') {
48
+ logger.warn(`[${tag}] missing path=${filePath}`);
49
+ return null;
50
+ }
51
+ if (!result.bytes) {
52
+ // 要求恢复却没恢复成功:只有 expected/actual,没有 bytes
53
+ logger.error(`[${tag}] tampered-and-unrecoverable path=${filePath} expected=${result.expected} actual=${result.actual}`);
54
+ return null;
55
+ }
56
+ if (result.status === 'restored') {
57
+ logger.warn(`[${tag}] restored path=${filePath}`);
58
+ return { text: result.bytes.toString('utf-8'), status: 'restored' };
59
+ }
60
+ return { text: result.bytes.toString('utf-8'), status: 'verified' };
61
+ }