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
@@ -11,7 +11,7 @@ import { StreamIdleMonitor } from './stream-idle-monitor.js';
11
11
  import { logger } from '../../utils/logger.js';
12
12
  import { isHostChinese } from '../../utils/locale.js';
13
13
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
14
- import { isEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
14
+ import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
15
15
  import { summarizeToolInput } from '../../utils/tool-summary.js';
16
16
  import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
17
17
  import { renderKitSections } from '../../eck/kit-renderer.js';
@@ -33,7 +33,6 @@ import { eligibleFallbackModels } from '../model/model-fallback.js';
33
33
  import { RetryScheduler } from './retry-scheduler.js';
34
34
  import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
35
35
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
36
- import { dispatchToMentionMode } from '../../config/mention-mode.js';
37
36
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
38
37
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
39
38
  import { normalizeUsage } from '../../stats/normalizer.js';
@@ -727,7 +726,19 @@ export class ResponseEngine {
727
726
  const selfAid = runtime?.selfAid || session?.selfAID;
728
727
  if (!selfAid)
729
728
  return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'self agent not found' };
730
- return this.handoffRuntime.returnHandoff({
729
+ return this.returnAuthorizedHandoffResult({
730
+ sessionId: params.sessionId,
731
+ handoffId: params.handoffId,
732
+ content: params.content,
733
+ }, selfAid);
734
+ }
735
+ /** Execute a handoff return after the daemon IPC boundary has authorized the task delegation. */
736
+ async returnAuthorizedHandoffResult(params, selfAid) {
737
+ if (!this.handoffRuntime) {
738
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
739
+ }
740
+ const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
741
+ return await this.handoffRuntime.returnHandoff({
731
742
  selfAid,
732
743
  currentSessionId: params.sessionId,
733
744
  handoffId: params.handoffId,
@@ -1152,6 +1163,14 @@ export class ResponseEngine {
1152
1163
  async processMessage(message) {
1153
1164
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1154
1165
  const totalExecutionMs = this.totalExecutionLimitMs();
1166
+ if (message.handoffDelivery
1167
+ && this.handoffRuntime
1168
+ && this.handoffRuntime.isDiscardedDelivery(message)) {
1169
+ logger.info(`[ResponseEngine] Skip stale handoff delivery before session resolution:`
1170
+ + ` direction=${message.handoffDelivery.direction}`
1171
+ + ` handoff=${message.handoffDelivery.handoffId}`);
1172
+ return;
1173
+ }
1155
1174
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
1156
1175
  // message.channel 现在存实例名(channelName),可直接用于精确路由
1157
1176
  const { session, absoluteProjectPath } = await this.resolveSession(message);
@@ -1720,11 +1739,11 @@ export class ResponseEngine {
1720
1739
  messageId: message.messageId, peerId: message.peerId, content: message.content,
1721
1740
  peerType: message.peerType || session.metadata?.peerType,
1722
1741
  chatType: chatType, isMentioned: message.isMentioned,
1723
- mentionAids: message.mentionAids, source: message.source,
1742
+ mentions: message.mentions, mentionAids: message.mentionAids, source: message.source,
1724
1743
  },
1725
1744
  modeConfig: resolvedMode.context.modeConfig,
1726
1745
  state: modeState,
1727
- isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
1746
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
1728
1747
  logger,
1729
1748
  } : null;
1730
1749
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -2118,11 +2137,21 @@ export class ResponseEngine {
2118
2137
  },
2119
2138
  };
2120
2139
  })();
2140
+ let effectivePermissionMode = 'readonly';
2121
2141
  const recordExecutionAnomaly = triggerRunId
2122
2142
  ? (anomaly) => {
2123
- recordTriggerExecutionAnomaly(triggerRunId, anomaly);
2143
+ recordTriggerExecutionAnomaly(triggerRunId, {
2144
+ ...anomaly,
2145
+ agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
2146
+ sessionId: anomaly.sessionId ?? session.id,
2147
+ permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
2148
+ });
2124
2149
  }
2125
2150
  : undefined;
2151
+ const runModeConfig = resolvedMode?.mode.configureRun && modeProcessCtx
2152
+ ? resolvedMode.mode.configureRun(modeProcessCtx)
2153
+ : undefined;
2154
+ const pureSessionPolicyHook = runModeConfig?.policyHook;
2126
2155
  // 设置权限审批的交互上下文(支持交互卡片)
2127
2156
  agent.setPermissionContext?.(session.id, {
2128
2157
  sendPrompt: permissionPromptForOrigin,
@@ -2144,6 +2173,7 @@ export class ResponseEngine {
2144
2173
  approvalRouting,
2145
2174
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2146
2175
  recordExecutionAnomaly,
2176
+ preToolUsePolicyHook: pureSessionPolicyHook,
2147
2177
  armApprovedDelegationCommand: this.agentDelegationRegistry
2148
2178
  ? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
2149
2179
  carrierToken,
@@ -2233,10 +2263,7 @@ export class ResponseEngine {
2233
2263
  // [迁移点2] policyHook 由 ProactiveMode.configureRun 提供(插件决策);
2234
2264
  // 引擎补充副作用:违规时注入提醒到模型上下文 + 行为探针。
2235
2265
  policyHook: (() => {
2236
- const runConfig = resolvedMode?.mode.configureRun && modeProcessCtx
2237
- ? resolvedMode.mode.configureRun(modeProcessCtx)
2238
- : undefined;
2239
- const pluginHook = runConfig?.policyHook;
2266
+ const pluginHook = pureSessionPolicyHook;
2240
2267
  if (!pluginHook)
2241
2268
  return undefined;
2242
2269
  return (toolName, toolInput) => {
@@ -2250,9 +2277,9 @@ export class ResponseEngine {
2250
2277
  agent.injectUserMessage?.(session.id, errorMsg);
2251
2278
  return result;
2252
2279
  }
2253
- // 首工具检查通过(firstToolDone false 翻成 true 且未拦截)
2254
- const stAfter = modeState.get('proactive');
2255
- if (wasFirstPending && stAfter?.firstToolDone) {
2280
+ // 这里只记录审批结果。firstToolDone 必须等真实 tool_use 事件再推进,
2281
+ // 避免伪造 managed hook IPC 提前解除门禁。
2282
+ if (wasFirstPending) {
2256
2283
  snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: false, toolName } });
2257
2284
  }
2258
2285
  return undefined;
@@ -2290,8 +2317,6 @@ export class ResponseEngine {
2290
2317
  let handoffPromptRendered = false;
2291
2318
  let runtimeEnv;
2292
2319
  try {
2293
- // 动态构建运行时上下文提示
2294
- const contextParts = [];
2295
2320
  // 通道能力
2296
2321
  const supportsFileMarker = currentChannelType !== 'aun' && !isProactive && !!channelInfo.adapter.capabilities?.file;
2297
2322
  const capParts = [];
@@ -2301,14 +2326,11 @@ export class ResponseEngine {
2301
2326
  capParts.push('图片输出');
2302
2327
  if (!isProactive && channelInfo.adapter.capabilities?.file)
2303
2328
  capParts.push('文件发送');
2304
- // Personal layer
2329
+ // Personal state is exposed to the manifest as vars. The manifest owns
2330
+ // loading persona.md and working.md into the system prompt.
2305
2331
  const owningAgent = owningAgentForTask;
2306
2332
  const persona = owningAgent?.getPersona?.() || undefined;
2307
2333
  const working = owningAgent?.getWorkingMemory?.() || undefined;
2308
- if (persona)
2309
- contextParts.push(persona);
2310
- if (working)
2311
- contextParts.push(`[当前关注]\n${working}`);
2312
2334
  // 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
2313
2335
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2314
2336
  const normalizedBaseagent = normalizeBaseagent(agent.name);
@@ -2316,7 +2338,6 @@ export class ResponseEngine {
2316
2338
  // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
2317
2339
  // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
2318
2340
  // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
2319
- let effectivePermissionMode;
2320
2341
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
2321
2342
  try {
2322
2343
  effectivePermissionMode = constrainRuntimePermissionMode({
@@ -2532,9 +2553,10 @@ export class ResponseEngine {
2532
2553
  channel: currentChannelType || null,
2533
2554
  venueUid: undefined,
2534
2555
  // 群 @ 处理模式 / 客户端类型 / 权限模式
2535
- // 优先 agent/relation mentionMode 配置,fallback 到服务器 dispatch_mode 缓存(协议词汇需翻译)。
2556
+ // 优先 agent/relation mentionMode 配置,fallback 到服务器 mention_mode 缓存。
2536
2557
  mentionMode: effectiveAgentConfig?.mentionMode
2537
- ?? dispatchToMentionMode(session.metadata?.dispatchMode ?? message.dispatchMode)
2558
+ ?? session.metadata?.mentionMode
2559
+ ?? message.mentionMode
2538
2560
  ?? undefined,
2539
2561
  clientType: message.clientType || undefined,
2540
2562
  permissionMode: effectivePermissionMode,
@@ -2583,17 +2605,13 @@ export class ResponseEngine {
2583
2605
  const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
2584
2606
  const manifestFile = sessionManifests?.[sessionType] ?? 'eck_manifest.json';
2585
2607
  const kitContext = renderKitSections(kitCtx, manifestFile);
2586
- if (kitContext)
2587
- contextParts.push(kitContext);
2588
- effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
2608
+ effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
2589
2609
  // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
2590
2610
  try {
2591
2611
  const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
2592
2612
  const cbModel = effectiveModel || agentModel || 'unknown';
2593
2613
  const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
2594
2614
  const systemPromptTokens = estTokens(options?.systemPromptAppend);
2595
- const personaTokens = estTokens(persona);
2596
- const workingTokens = estTokens(working);
2597
2615
  const kitTokens = estTokens(kitContext);
2598
2616
  const totalEst = estTokens(effectiveSystemPrompt);
2599
2617
  insertContextBreakdown(resolveRoot(), {
@@ -2603,7 +2621,7 @@ export class ResponseEngine {
2603
2621
  turn_count: 0, // 按 ts 排序得轮次
2604
2622
  model: cbModel,
2605
2623
  max_tokens: cbMaxTokens,
2606
- system_prompt: systemPromptTokens + personaTokens + workingTokens,
2624
+ system_prompt: systemPromptTokens,
2607
2625
  system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
2608
2626
  mcp_tools: 0,
2609
2627
  custom_agents: 0,
@@ -2629,6 +2647,7 @@ export class ResponseEngine {
2629
2647
  encrypted: message.encrypted,
2630
2648
  content: message.content, timestamp: message.timestamp,
2631
2649
  images: message.images,
2650
+ mentions: message.mentions,
2632
2651
  mentionAids: message.mentionAids,
2633
2652
  }];
2634
2653
  const peerItems = (() => {
@@ -3916,12 +3935,10 @@ export class ResponseEngine {
3916
3935
  await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3917
3936
  }
3918
3937
  }
3919
- // 群聊分发模式同步:aun.ts 从服务器信封解析的 dispatchMode 注入到 message,
3920
- // 此处写入 session.metadata,确保 ECK 上下文的 venue fragment 正确渲染 dispatch 变量。
3921
- // 仅当 message.dispatchMode 有值且与 session 记录不一致时更新。
3922
- if (message.chatType === 'group' && message.dispatchMode && session.metadata?.dispatchMode !== message.dispatchMode) {
3923
- logger.info(`[ResponseEngine] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
3924
- session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
3938
+ // 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
3939
+ if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
3940
+ logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
3941
+ session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
3925
3942
  await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3926
3943
  }
3927
3944
  // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
@@ -4293,6 +4310,7 @@ export class ResponseEngine {
4293
4310
  }
4294
4311
  // [迁移点4] 工具汇报提醒:由 ProactiveMode.onToolUse 实现
4295
4312
  if (modeHooks?.mode?.onToolUse) {
4313
+ const proactiveState = modeHooks.state.get('proactive');
4296
4314
  modeHooks.mode.onToolUse({
4297
4315
  session,
4298
4316
  state: modeHooks.state,
@@ -4300,7 +4318,7 @@ export class ResponseEngine {
4300
4318
  toolInput: event.input || {},
4301
4319
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
4302
4320
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4303
- isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
4321
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
4304
4322
  logger,
4305
4323
  });
4306
4324
  }
@@ -1,4 +1,11 @@
1
1
  const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
2
+ // Codex app-server sends command executions through a fixed shell carrier.
3
+ // Treat the carrier as syntax only after parsing it as literal argv; never
4
+ // accept an arbitrary `sh -c` string or unwrap shell composition here.
5
+ const CODEX_SHELL_EXECUTABLES = new Set([
6
+ 'bash', '/bin/bash', '/usr/bin/bash',
7
+ 'sh', '/bin/sh', '/usr/bin/sh',
8
+ ]);
2
9
  const SHELL_UNQUOTED_META = new Set([
3
10
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
4
11
  ]);
@@ -135,6 +142,42 @@ export function parseLiteralShellArgv(command) {
135
142
  const result = parseLiteralShellCommand(command);
136
143
  return result.ok ? result.argv : null;
137
144
  }
145
+ /**
146
+ * Return the command string carried by Codex's literal `/bin/bash -lc ...`
147
+ * wrapper. The wrapper is accepted only in its exact three-argv form and the
148
+ * caller must parse the returned string with the literal shell parser again.
149
+ */
150
+ export function unwrapCodexShellCommandArgv(argv) {
151
+ if (argv.length !== 3 || argv[1] !== '-lc')
152
+ return undefined;
153
+ return CODEX_SHELL_EXECUTABLES.has(argv[0]) ? argv[2] : undefined;
154
+ }
155
+ /**
156
+ * Parse either a bare literal command or the exact shell carrier emitted by
157
+ * Codex app-server. This does not make shell composition safe: the nested
158
+ * command still has to pass `parseLiteralShellCommand`.
159
+ */
160
+ export function parseCodexShellCommandArgv(command) {
161
+ const outer = parseLiteralShellArgv(command);
162
+ if (!outer)
163
+ return null;
164
+ const wrapped = unwrapCodexShellCommandArgv(outer);
165
+ return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
166
+ }
167
+ /**
168
+ * Resolve a Codex command input while preserving the raw `commandArgv` form
169
+ * supplied by app-server. Joining that argv with spaces loses the quoting of
170
+ * the inner `-lc` command and was the source of a false proactive denial.
171
+ */
172
+ export function parseCodexToolCommandArgv(input) {
173
+ if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
174
+ const outer = input.commandArgv;
175
+ const wrapped = unwrapCodexShellCommandArgv(outer);
176
+ return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
177
+ }
178
+ const command = typeof input.command === 'string' ? input.command : '';
179
+ return parseCodexShellCommandArgv(command);
180
+ }
138
181
  function scanBoundedOutputOperators(command) {
139
182
  const operators = [];
140
183
  let quote = null;
@@ -314,9 +357,18 @@ export function parseBoundedOutputShellCommand(command) {
314
357
  export function classifyEvolcoreShellCommand(command) {
315
358
  const literal = parseLiteralShellCommand(command);
316
359
  if (literal.ok) {
317
- return literal.argv[0] === 'ec'
318
- ? { kind: 'literal' }
319
- : { kind: 'none' };
360
+ if (literal.argv[0] === 'ec')
361
+ return { kind: 'literal' };
362
+ const wrapped = unwrapCodexShellCommandArgv(literal.argv);
363
+ if (wrapped !== undefined) {
364
+ const nested = parseLiteralShellCommand(wrapped);
365
+ if (nested.ok && nested.argv[0] === 'ec')
366
+ return { kind: 'literal' };
367
+ if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
368
+ return { kind: 'composite', issue: nested.issue };
369
+ }
370
+ }
371
+ return { kind: 'none' };
320
372
  }
321
373
  const boundedOutput = parseBoundedOutputShellCommand(command);
322
374
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {
@@ -350,8 +402,81 @@ function findSingleFileOption(argv, startIndex) {
350
402
  ? { path: filePath, invalid: false }
351
403
  : { invalid: true };
352
404
  }
353
- export function parseEvolcoreSendCommand(command) {
354
- const argv = parseLiteralShellArgv(command);
405
+ const PROACTIVE_SEND_VALUE_FLAGS = new Set([
406
+ '--format', '--app', '--file', '--text-from-file', '--link', '--payload',
407
+ '--title', '--description', '--text', '--transcript', '--as',
408
+ '--content-type', '--mention', '--aun-path', '--thread', '--return',
409
+ ]);
410
+ const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
411
+ '--encrypt', '--no-encrypt', '--mention-all',
412
+ ]);
413
+ function hasExecutableProactiveSendPayload(argv, scope) {
414
+ if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
415
+ return false;
416
+ const positionals = [];
417
+ const modes = new Map();
418
+ let endOfOptions = false;
419
+ for (let index = 5; index < argv.length; index++) {
420
+ const value = argv[index];
421
+ if (endOfOptions) {
422
+ positionals.push(value);
423
+ continue;
424
+ }
425
+ if (value === '--') {
426
+ endOfOptions = true;
427
+ continue;
428
+ }
429
+ if (PROACTIVE_SEND_BOOLEAN_FLAGS.has(value))
430
+ continue;
431
+ if (!PROACTIVE_SEND_VALUE_FLAGS.has(value)) {
432
+ positionals.push(value);
433
+ continue;
434
+ }
435
+ const operand = argv[++index];
436
+ if (operand === undefined || operand.startsWith('--'))
437
+ return false;
438
+ if (value === '--file' || value === '--text-from-file')
439
+ return false;
440
+ if (value === '--app' || value === '--aun-path')
441
+ return false;
442
+ if (value === '--return' && operand !== 'none' && operand !== 'required')
443
+ return false;
444
+ if (value === '--format' && operand !== 'json')
445
+ return false;
446
+ if (value === '--link') {
447
+ if (scope !== 'msg' || modes.has('link'))
448
+ return false;
449
+ modes.set('link', operand);
450
+ }
451
+ if (value === '--payload') {
452
+ if (modes.has('payload'))
453
+ return false;
454
+ modes.set('payload', operand);
455
+ }
456
+ }
457
+ if (modes.size > 1)
458
+ return false;
459
+ if (modes.has('link')) {
460
+ try {
461
+ const url = new URL(modes.get('link'));
462
+ return url.protocol === 'http:' || url.protocol === 'https:';
463
+ }
464
+ catch {
465
+ return false;
466
+ }
467
+ }
468
+ if (modes.has('payload')) {
469
+ try {
470
+ const payload = JSON.parse(modes.get('payload'));
471
+ return !!payload && typeof payload === 'object' && !Array.isArray(payload);
472
+ }
473
+ catch {
474
+ return false;
475
+ }
476
+ }
477
+ return positionals.join(' ').length > 0;
478
+ }
479
+ function parseEvolcoreSendArgv(argv) {
355
480
  if (!argv || argv[0] !== 'ec')
356
481
  return null;
357
482
  if (argv[1] === 'ctl') {
@@ -380,23 +505,44 @@ export function parseEvolcoreSendCommand(command) {
380
505
  if (subcommand === 'file') {
381
506
  if (argv.length !== 6 || !isDeterministicFilePath(argv[5]))
382
507
  return null;
383
- return { scope, action: 'file', targetId, filePath: argv[5] };
508
+ return { scope, action: 'file', sourceId: argv[3], targetId, filePath: argv[5] };
384
509
  }
385
510
  const fileOption = findSingleFileOption(argv, 5);
386
511
  if (fileOption.invalid)
387
512
  return null;
388
513
  return fileOption.path
389
- ? { scope, action: 'file', targetId, filePath: fileOption.path }
390
- : { scope, action: 'send', targetId };
514
+ ? { scope, action: 'file', sourceId: argv[3], targetId, filePath: fileOption.path }
515
+ : { scope, action: 'send', sourceId: argv[3], targetId };
516
+ }
517
+ export function parseEvolcoreSendCommand(command) {
518
+ const argv = parseCodexShellCommandArgv(command);
519
+ return argv ? parseEvolcoreSendArgv(argv) : null;
391
520
  }
392
521
  export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
393
522
  if (toolName !== 'Bash')
394
523
  return false;
395
- const cmd = typeof input.command === 'string' ? input.command : '';
396
- const parsed = parseEvolcoreSendCommand(cmd);
524
+ const argv = parseCodexToolCommandArgv(input);
525
+ const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
397
526
  if (!parsed)
398
527
  return false;
399
528
  if (parsed.scope === 'ctl')
400
529
  return true;
401
530
  return parsed.targetId === channelId;
402
531
  }
532
+ /** Proactive first-send guard: exact channel kind and current target only. */
533
+ export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
534
+ // Codex hooks use the canonical name `Bash`; the app-server event adapter
535
+ // exposes the same commandExecution items as `Shell` to the renderer.
536
+ if ((toolName !== 'Bash' && toolName !== 'Shell') || !selfAid)
537
+ return false;
538
+ const argv = parseCodexToolCommandArgv(input);
539
+ const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
540
+ if (!parsed || parsed.action !== 'send')
541
+ return false;
542
+ const expectedScope = chatType === 'group' ? 'group' : 'msg';
543
+ return parsed.scope === expectedScope
544
+ && parsed.targetId === channelId
545
+ && parsed.sourceId === selfAid
546
+ && !!argv
547
+ && hasExecutableProactiveSendPayload(argv, expectedScope);
548
+ }
@@ -210,7 +210,7 @@ function appendLClassReadOnlyBinds(args, root) {
210
210
  args.push('--ro-bind', target.path, target.path);
211
211
  }
212
212
  }
213
- export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot()) {
213
+ export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot(), options = {}) {
214
214
  const bubblewrapPath = resolveBubblewrapPath();
215
215
  if (!bubblewrapPath)
216
216
  return undefined;
@@ -224,6 +224,27 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
224
224
  ];
225
225
  appendLClassReadOnlyBinds(args, path.resolve(root));
226
226
  appendHClassMasks(args, path.resolve(root), true);
227
+ if (options.managedCodexRequirements) {
228
+ const requirements = path.resolve(options.managedCodexRequirements);
229
+ if (!fs.statSync(requirements).isFile()) {
230
+ throw new Error(`Codex managed requirements is not a file: ${requirements}`);
231
+ }
232
+ const managedDirectory = path.dirname(requirements);
233
+ // Keep the copied hook immutable even when a session uses bypass mode.
234
+ args.push('--ro-bind', managedDirectory, managedDirectory);
235
+ // A host may not have /etc/codex yet. Overlay /etc inside this namespace so
236
+ // Bubblewrap can create the mountpoint without modifying the host, while
237
+ // retaining the complete host /etc as the read-only lower layer.
238
+ args.push('--overlay-src', '/etc');
239
+ args.push('--tmp-overlay', '/etc');
240
+ args.push('--dir', '/etc/codex');
241
+ // Replace the complete Codex system-config directory inside this app-server
242
+ // namespace so host system hooks cannot join EvolCore's managed hook set.
243
+ args.push('--tmpfs', '/etc/codex');
244
+ args.push('--ro-bind', requirements, '/etc/codex/requirements.toml');
245
+ args.push('--remount-ro', '/etc/codex');
246
+ args.push('--remount-ro', '/etc');
247
+ }
227
248
  args.push('--', executable, ...executableArgs);
228
249
  return { command: bubblewrapPath, args };
229
250
  }