evolcore 0.0.17 → 0.0.18

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,38 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.18 (2026-08-27)
7
+
8
+ ### DaemonOwner 与受管 Agent 操作
9
+
10
+ - 新增 `daemon-owner` 进程角色,仅由 `daemon.json.owners` 的控制 AID 解析。
11
+ - 进程操作、跨 Agent Trigger 与内部 Trigger 目录统一要求 DaemonOwner。
12
+ - Agent channel 保持仅能操作自身;拒绝结果附带原因码与下一步指引。
13
+ - 受管任务通过委派 IPC 执行授权的 `ec agent` 查询、非交互创建和生命周期操作。
14
+
15
+ ### 任务暂停与 Bootstrap 门禁
16
+
17
+ - 新增 `/pause`、`/resume`,在下一次工具调用前暂停或恢复当前任务。
18
+ - Codex、Claude 与 ecagent 的工具边界均接入暂停门禁;任务结束自动解除。
19
+ - Agent 仅在 `active`、`bootstrapping` 生命周期运行,不再回退默认 Runner。
20
+ - Bootstrap 隔离普通任务与上下文,仅允许受限的 `agent.md` 确认、发布和 ready。
21
+ - 首次欢迎消息按实际私聊或群聊路由持久化投递。
22
+
23
+ ### AUN 投递与连接可靠性
24
+
25
+ - 群消息恢复支持有界多页补拉,避免尾部同步后遗留大积压。
26
+ - 适配新版 AUN 连接状态事件,隔离旧客户端事件并记录传输/入站时间。
27
+ - 受管沙箱发送 AUN 消息改经配置存储读取超时和默认加密设置。
28
+
29
+ ### 审计、安全与跨平台运行
30
+
31
+ - 菜单响应新增 `processing_ms` 与独立的协议业务流日志。
32
+ - 工具与授权审计新增生命周期键、执行状态和敏感参数脱敏。
33
+ - 统一 Codex 受管 `PreToolUse` Hook,强化 Bubblewrap 挂载与失败熔断诊断。
34
+ - 原子写入保留正式文件并刷新热备,避免受保护状态挂载竞态。
35
+ - `ec stop`、`ec restart` 优先经 IPC 请求守护进程优雅退出并校验 PID。
36
+ - Windows 自启增加计划任务校验、用户级 Run 回退和非 EvolCore 配置保护。
37
+
6
38
  ## 0.0.17 (2026-08-25)
7
39
 
8
40
  ### Claude 与任务边界
@@ -1,7 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import net from 'node:net';
3
3
 
4
- const timeoutMs = 1500;
4
+ // Only bound socket establishment. Once connected, the official Codex hook
5
+ // timeout owns the lifetime of this process; an application timeout here would
6
+ // turn /pause into a denial/allow race.
7
+ const connectTimeoutMs = 1500;
5
8
 
6
9
  function deny(reason) {
7
10
  process.stdout.write(JSON.stringify({
@@ -11,9 +14,8 @@ function deny(reason) {
11
14
  permissionDecisionReason: reason || 'EvolCore policy denied this tool call.',
12
15
  },
13
16
  }));
14
- // Codex 0.147 app-server runs this managed hook but does not reliably honor
15
- // the JSON denial alone for thread/shellCommand. Exit 2 is the documented
16
- // blocking transport; retain the JSON shape for compatible hook consumers.
17
+ // Exit 2 is the documented blocking transport. Keep the structured JSON as
18
+ // well so Codex can surface the policy reason through the PreToolUse result.
17
19
  process.stderr.write(`${reason || 'EvolCore policy denied this tool call.'}\n`);
18
20
  process.exitCode = 2;
19
21
  }
@@ -24,6 +26,7 @@ function queryDaemon(socketPath, payload) {
24
26
  let buffer = '';
25
27
  let settled = false;
26
28
  let timer;
29
+ let connected = false;
27
30
  const finish = value => {
28
31
  if (settled) return;
29
32
  settled = true;
@@ -31,10 +34,16 @@ function queryDaemon(socketPath, payload) {
31
34
  socket.destroy();
32
35
  resolve(value);
33
36
  };
34
- timer = setTimeout(() => finish(null), timeoutMs);
37
+ timer = setTimeout(() => {
38
+ if (!connected) finish(null);
39
+ }, connectTimeoutMs);
35
40
  timer.unref?.();
36
41
  socket.once('error', () => finish(null));
37
- socket.once('connect', () => socket.write(JSON.stringify(payload) + '\n'));
42
+ socket.once('connect', () => {
43
+ connected = true;
44
+ clearTimeout(timer);
45
+ socket.write(JSON.stringify(payload) + '\n');
46
+ });
38
47
  socket.on('data', chunk => {
39
48
  buffer += String(chunk);
40
49
  const index = buffer.indexOf('\n');
@@ -69,7 +78,7 @@ async function main() {
69
78
  const result = await queryDaemon(socketPath, {
70
79
  type: 'codex-pre-tool-use',
71
80
  threadId: input.session_id,
72
- toolName: typeof input.tool_name === 'string' ? input.tool_name : '',
81
+ toolName: input.tool_name,
73
82
  toolInput: input.tool_input ?? null,
74
83
  });
75
84
  if (!result) {
@@ -151,7 +151,7 @@ function main() {
151
151
  const targetDirectory = argument('--managed-dir')
152
152
  || path.join(path.dirname(requirements), 'EvolCore', 'hooks');
153
153
  const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
154
- if (!fs.statSync(sourceHook).isFile()) fail(`Managed hook source was not found: ${sourceHook}`);
154
+ if (!fs.existsSync(sourceHook) || !fs.statSync(sourceHook).isFile()) fail(`Codex PreToolUse hook source was not found: ${sourceHook}`);
155
155
 
156
156
  fs.mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
157
157
  const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
@@ -180,7 +180,9 @@ function main() {
180
180
  'type = "command"',
181
181
  `command = ${tomlString(command)}`,
182
182
  `command_windows = ${tomlString(command)}`,
183
- 'timeout = 3',
183
+ // Intentionally omit timeout: Codex applies its documented 600-second
184
+ // default for synchronous PreToolUse hooks. The application pause gate
185
+ // itself has no timeout; Codex remains the outer infrastructure ceiling.
184
186
  'statusMessage = "Checking EvolCore session policy"',
185
187
  markerEnd,
186
188
  '',
@@ -23,9 +23,9 @@ import { sanitizeSessionTitle } from '../core/session/session-title.js';
23
23
  import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
24
24
  import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
25
25
  import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
26
- import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
26
+ import { buildHClassGuardCommand, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
27
27
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
28
- import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
28
+ import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
29
29
  import { isManagedSessionRuntimeDir } from '../cli/task-context.js';
30
30
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
31
31
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
@@ -626,6 +626,40 @@ export class AgentRunner {
626
626
  /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
627
627
  recentStderr = new Map();
628
628
  static STDERR_BUFFER_MAX = 80;
629
+ async recordSandboxInfrastructureFailure(sessionId, inputId, permissionMode, error, stderr = []) {
630
+ const detail = createSandboxInitializationError(error, stderr);
631
+ const circuit = recordSandboxInitializationFailure(detail, stderr);
632
+ const context = this.permissionContexts.get(sessionId);
633
+ auditToolInfrastructureFailure({
634
+ toolName: 'Bash',
635
+ policyCode: SANDBOX_INITIALIZATION_FAILED,
636
+ reason: detail.message.slice(0, 512),
637
+ sessionId,
638
+ agentAid: context?.selfAid,
639
+ permissionMode,
640
+ role: context?.role,
641
+ callId: inputId,
642
+ taskId: context?.taskId,
643
+ occurrences: circuit.occurrences,
644
+ });
645
+ await context?.recordExecutionAnomaly?.({
646
+ code: 'capability_unavailable',
647
+ severity: 'warning',
648
+ phase: 'execution',
649
+ occurredAt: Date.now(),
650
+ correlationId: inputId,
651
+ requestId: inputId,
652
+ sessionId,
653
+ toolName: 'Bash',
654
+ policyCode: SANDBOX_INITIALIZATION_FAILED,
655
+ decisionSource: 'infrastructure',
656
+ agentAid: context?.selfAid,
657
+ permissionMode: permissionMode,
658
+ summary: detail.message.slice(0, 512),
659
+ effect: 'operation_skipped',
660
+ });
661
+ return { detail, occurrences: circuit.occurrences };
662
+ }
629
663
  trackSandboxSettingsFile(sessionId, filePath) {
630
664
  const files = this.sandboxSettingsFiles.get(sessionId) ?? new Set();
631
665
  files.add(filePath);
@@ -1407,7 +1441,7 @@ export class AgentRunner {
1407
1441
  * SDK 原始事件 → 标准 AgentEvent 转换
1408
1442
  * 所有 SDK 特有的事件类型引用封装在此方法内
1409
1443
  */
1410
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile) {
1444
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode) {
1411
1445
  let lastSessionId;
1412
1446
  let hasTurnActivity = false;
1413
1447
  let ignoredPreTurnResult = false;
@@ -1851,19 +1885,7 @@ export class AgentRunner {
1851
1885
  return;
1852
1886
  }
1853
1887
  if (isSandboxInitializationFailure(err, buf ?? [])) {
1854
- const detail = createSandboxInitializationError(err, buf ?? []);
1855
- await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
1856
- code: 'capability_unavailable',
1857
- severity: 'warning',
1858
- phase: 'execution',
1859
- occurredAt: Date.now(),
1860
- toolName: 'Bash',
1861
- policyCode: SANDBOX_INITIALIZATION_FAILED,
1862
- decisionSource: 'infrastructure',
1863
- agentAid: this.permissionContexts.get(sessionId)?.selfAid,
1864
- summary: detail.message.slice(0, 512),
1865
- effect: 'operation_skipped',
1866
- });
1888
+ const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, inputId, permissionMode, err, buf ?? []);
1867
1889
  yield {
1868
1890
  type: 'complete',
1869
1891
  isError: true,
@@ -1955,6 +1977,57 @@ export class AgentRunner {
1955
1977
  const callPermissionMode = normalizedPermission.mode;
1956
1978
  const runPermissionContext = this.permissionContexts.get(sessionId);
1957
1979
  const executionSandbox = resolvePhaseOneExecutionSandbox(callPermissionMode, runPermissionContext?.role);
1980
+ const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
1981
+ if (executionSandbox.state !== 'off') {
1982
+ const activeSandboxFailure = getActiveSandboxInitializationFailure();
1983
+ if (activeSandboxFailure) {
1984
+ const detail = createSandboxInitializationError(new Error(`sandbox bootstrap circuit open; firstObservedAt=${activeSandboxFailure.openedAt} ` +
1985
+ `occurrences=${activeSandboxFailure.occurrences}; ${activeSandboxFailure.detail}`));
1986
+ const context = this.permissionContexts.get(sessionId);
1987
+ auditToolInfrastructureFailure({
1988
+ toolName: 'Bash',
1989
+ policyCode: SANDBOX_INITIALIZATION_FAILED,
1990
+ reason: detail.message.slice(0, 512),
1991
+ sessionId,
1992
+ agentAid: context?.selfAid,
1993
+ permissionMode: callPermissionMode,
1994
+ role: context?.role,
1995
+ callId: inputId,
1996
+ taskId: context?.taskId,
1997
+ occurrences: activeSandboxFailure.occurrences,
1998
+ });
1999
+ await context?.recordExecutionAnomaly?.({
2000
+ code: 'capability_unavailable',
2001
+ severity: 'warning',
2002
+ phase: 'execution',
2003
+ occurredAt: Date.now(),
2004
+ correlationId: inputId,
2005
+ requestId: inputId,
2006
+ sessionId,
2007
+ toolName: 'Bash',
2008
+ policyCode: SANDBOX_INITIALIZATION_FAILED,
2009
+ decisionSource: 'infrastructure',
2010
+ agentAid: context?.selfAid,
2011
+ permissionMode: callPermissionMode,
2012
+ summary: detail.message.slice(0, 512),
2013
+ effect: 'operation_skipped',
2014
+ });
2015
+ if (shouldLogSandboxCircuitNotice()) {
2016
+ logger.warn(`[ClaudeSandbox] bootstrap circuit open; skipping subprocess spawn ` +
2017
+ `cooldownMs=${SANDBOX_INITIALIZATION_COOLDOWN_MS} occurrences=${activeSandboxFailure.occurrences}`);
2018
+ }
2019
+ return (async function* () {
2020
+ yield {
2021
+ type: 'complete',
2022
+ isError: true,
2023
+ subtype: 'infrastructure',
2024
+ terminalReason: SANDBOX_INITIALIZATION_FAILED,
2025
+ errors: [detail.message],
2026
+ queryFinal: true,
2027
+ };
2028
+ })();
2029
+ }
2030
+ }
1958
2031
  const permissionPrompt = runPermissionContext?.sendPrompt ?? this.sendPromptFn;
1959
2032
  const permissionContext = (signal) => {
1960
2033
  const context = this.permissionContexts.get(sessionId);
@@ -2013,7 +2086,14 @@ export class AgentRunner {
2013
2086
  // random one-shot payload from a validated output wrapper.
2014
2087
  const trustedSafeOutputCommands = new Set();
2015
2088
  // PreToolUse Hook - 黑名单检查 + input 修正(不可绕过,所有模式都走)
2016
- const preToolUseHook = async (input) => {
2089
+ const preToolUseHook = async (input, _toolUseId, hookOptions) => {
2090
+ const pauseController = this.permissionContexts.get(sessionId)?.pauseController;
2091
+ if (pauseController) {
2092
+ const pauseResult = await pauseController.waitAtToolBoundary(hookOptions?.signal);
2093
+ if (pauseResult === 'cancelled') {
2094
+ return hookDecision('deny', '工具调用已取消');
2095
+ }
2096
+ }
2017
2097
  const toolName = input.tool_name;
2018
2098
  const originalInput = (input.tool_input || {});
2019
2099
  let toolInput = originalInput;
@@ -2358,6 +2438,10 @@ export class AgentRunner {
2358
2438
  role: runPermissionContext?.role,
2359
2439
  });
2360
2440
  const runtimeWritePaths = trustedRuntimeWritePaths(runtimeEnv);
2441
+ const bootstrapWritePaths = modelOverride?.bootstrapAgentMdPath
2442
+ ? [path.resolve(modelOverride.bootstrapAgentMdPath)]
2443
+ : [];
2444
+ const allowedWritePaths = [...new Set([...runtimeWritePaths, ...bootstrapWritePaths])];
2361
2445
  await assertClaudeSettingSourcesHaveLiteralSandboxPaths(projectPath, [...settingSources], managedSettings);
2362
2446
  const sandboxProjectionDurationMs = Date.now() - sandboxProjectionStartedAt;
2363
2447
  if (sandboxProjectionDurationMs > 2_000) {
@@ -2376,7 +2460,7 @@ export class AgentRunner {
2376
2460
  },
2377
2461
  filesystem: {
2378
2462
  ...sandboxFilesystem,
2379
- ...(runtimeWritePaths.length > 0 ? { allowWrite: runtimeWritePaths } : {}),
2463
+ ...(allowedWritePaths.length > 0 ? { allowWrite: allowedWritePaths } : {}),
2380
2464
  },
2381
2465
  };
2382
2466
  })();
@@ -2552,7 +2636,6 @@ export class AgentRunner {
2552
2636
  }
2553
2637
  let sdkStream;
2554
2638
  const msgStream = new MessageStream();
2555
- const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
2556
2639
  if (images && images.length > 0) {
2557
2640
  logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2558
2641
  logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
@@ -2565,8 +2648,10 @@ export class AgentRunner {
2565
2648
  const stderr = this.recentStderr.get(sessionId) ?? [];
2566
2649
  if (isClaudeStartupArgumentsTooLong(error))
2567
2650
  throw createClaudeStartupArgumentsTooLongError(error);
2568
- if (isSandboxInitializationFailure(error, stderr))
2569
- throw createSandboxInitializationError(error, stderr);
2651
+ if (isSandboxInitializationFailure(error, stderr)) {
2652
+ const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, inputId, callPermissionMode, error, stderr);
2653
+ throw detail;
2654
+ }
2570
2655
  throw error;
2571
2656
  }
2572
2657
  }
@@ -2581,8 +2666,10 @@ export class AgentRunner {
2581
2666
  const stderr = this.recentStderr.get(sessionId) ?? [];
2582
2667
  if (isClaudeStartupArgumentsTooLong(error))
2583
2668
  throw createClaudeStartupArgumentsTooLongError(error);
2584
- if (isSandboxInitializationFailure(error, stderr))
2585
- throw createSandboxInitializationError(error, stderr);
2669
+ if (isSandboxInitializationFailure(error, stderr)) {
2670
+ const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, inputId, callPermissionMode, error, stderr);
2671
+ throw detail;
2672
+ }
2586
2673
  throw error;
2587
2674
  }
2588
2675
  }
@@ -2595,7 +2682,7 @@ export class AgentRunner {
2595
2682
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2596
2683
  }
2597
2684
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2598
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile);
2685
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode);
2599
2686
  const self = this;
2600
2687
  return (async function* () {
2601
2688
  try {
@@ -2611,6 +2698,7 @@ export class AgentRunner {
2611
2698
  })();
2612
2699
  }
2613
2700
  async interrupt(sessionId) {
2701
+ this.permissionContexts.get(sessionId)?.pauseController?.cancel();
2614
2702
  const streamRegistered = this.activeStreams.has(sessionId);
2615
2703
  const fn = this.interruptFns.get(sessionId);
2616
2704
  const query = this.activeQueries.get(sessionId);
@@ -2764,6 +2852,7 @@ export class AgentRunner {
2764
2852
  }
2765
2853
  }
2766
2854
  async closeSession(sessionId) {
2855
+ this.permissionContexts.get(sessionId)?.pauseController?.cancel();
2767
2856
  const query = this.activeQueries.get(sessionId);
2768
2857
  if (query && typeof query.close === 'function')
2769
2858
  query.close();
@@ -475,6 +475,8 @@ export class CodexAppServerClient {
475
475
  fs.chmodSync(directory, 0o700);
476
476
  const hookPath = path.join(directory, 'pre-tool-use.mjs');
477
477
  const sourcePath = path.join(getPackageRoot(), 'bin', 'codex-managed-hook.mjs');
478
+ if (!fs.existsSync(sourcePath))
479
+ throw new Error('Codex PreToolUse hook source is unavailable');
478
480
  fs.copyFileSync(sourcePath, hookPath, fs.constants.COPYFILE_EXCL);
479
481
  fs.chmodSync(hookPath, 0o500);
480
482
  const command = `${this.shellQuote(process.execPath)} ${this.shellQuote(hookPath)} ${this.shellQuote(resolvePaths().instanceSocket)}`;
@@ -494,7 +496,10 @@ export class CodexAppServerClient {
494
496
  '[[hooks.PreToolUse.hooks]]',
495
497
  'type = "command"',
496
498
  `command = ${JSON.stringify(command)}`,
497
- 'timeout = 3',
499
+ // Intentionally omit timeout: Codex applies its documented 600-second
500
+ // default for synchronous PreToolUse hooks. The application pause
501
+ // gate itself has no timeout; Codex remains the outer infrastructure
502
+ // ceiling.
498
503
  'statusMessage = "Checking EvolCore session policy"',
499
504
  '',
500
505
  ].join('\n'), { mode: 0o400, flag: 'wx' });
@@ -795,7 +795,7 @@ export class CodexRunner {
795
795
  }
796
796
  return Object.fromEntries([...paths].map(candidate => [candidate, 'write']));
797
797
  }
798
- buildPermissionProfileConfig(sessionId, mode, projectPath) {
798
+ buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath) {
799
799
  const profileName = this.permissionProfileName(sessionId);
800
800
  const managedTempDir = this.ensureSessionTempDir(sessionId);
801
801
  const network = {
@@ -812,6 +812,9 @@ export class CodexRunner {
812
812
  denyLClassRead: mode === 'readonly',
813
813
  }),
814
814
  ...this.resolveGitMetadataWriteRules(projectPath, mode),
815
+ ...(bootstrapAgentMdPath ? {
816
+ [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
817
+ } : {}),
815
818
  ...(mode !== 'readonly' && managedTempDir ? {
816
819
  [managedTempDir]: 'write',
817
820
  [path.join(managedTempDir, '**')]: 'write',
@@ -824,12 +827,12 @@ export class CodexRunner {
824
827
  },
825
828
  };
826
829
  }
827
- buildExecutionSandboxOptions(sessionId, mode, projectPath) {
830
+ buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath) {
828
831
  const decision = resolvePhaseOneExecutionSandbox(mode, this.permissionContexts.get(sessionId)?.role);
829
832
  return {
830
833
  decision,
831
834
  permissionProfileConfig: decision.state === 'active'
832
- ? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath)
835
+ ? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath)
833
836
  : undefined,
834
837
  sandbox: decision.state === 'off' ? 'danger-full-access' : undefined,
835
838
  };
@@ -837,7 +840,11 @@ export class CodexRunner {
837
840
  buildLifecycleLockdownConfig() {
838
841
  return {
839
842
  features: {
840
- hooks: false,
843
+ // The app-server is launched with allow_managed_hooks_only=true, so
844
+ // enabling the feature exposes only EvolCore's requirements-managed
845
+ // PreToolUse hook. Disabling it here suppresses that managed hook too
846
+ // and bypasses the pause/proactive policy boundary entirely.
847
+ hooks: true,
841
848
  },
842
849
  };
843
850
  }
@@ -867,7 +874,7 @@ export class CodexRunner {
867
874
  throw new Error('Codex proactive managed PreToolUse enforcement requires Linux or an installed Windows managed hook');
868
875
  }
869
876
  }
870
- async evaluatePreToolUse(threadId, toolName, toolInput) {
877
+ async evaluatePreToolUse(threadId, toolName, toolInput, signal) {
871
878
  const sessionKey = this.findSessionKeyByThread(threadId);
872
879
  const activeThread = this.activeSessions.get(sessionKey);
873
880
  const context = this.permissionContexts.get(sessionKey);
@@ -878,6 +885,12 @@ export class CodexRunner {
878
885
  if (!context || (proactiveSession && !context.preToolUsePolicyHook)) {
879
886
  return { ok: false, applicable: true, decision: 'deny', reason: 'EvolCore proactive session policy context is unavailable' };
880
887
  }
888
+ if (context.pauseController) {
889
+ const pauseResult = await context.pauseController.waitAtToolBoundary(signal);
890
+ if (pauseResult === 'cancelled') {
891
+ return { ok: true, applicable: true, decision: 'deny', reason: '工具调用已取消' };
892
+ }
893
+ }
881
894
  // A tracked interactive thread still needs an authenticated, explicit
882
895
  // allow so the managed hook can fail closed when no runner matches.
883
896
  if (!context.preToolUsePolicyHook)
@@ -1044,7 +1057,7 @@ export class CodexRunner {
1044
1057
  const callMode = normalizedPermission.mode;
1045
1058
  this.chatModes.set(sessionId, callMode);
1046
1059
  const callApprovalPolicy = this.toApprovalPolicy(callMode);
1047
- const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath);
1060
+ const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath);
1048
1061
  const appServer = this.getAppServerClient();
1049
1062
  this.dropExpiredPendingInterrupt(sessionId);
1050
1063
  const effectiveApprovalPolicy = callApprovalPolicy;
@@ -1203,6 +1216,7 @@ export class CodexRunner {
1203
1216
  }
1204
1217
  // ── Interrupt ──
1205
1218
  async interrupt(sessionKey) {
1219
+ this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
1206
1220
  const controller = this.activeAbortControllers.get(sessionKey);
1207
1221
  const activeTurn = this.activeTurns.get(sessionKey);
1208
1222
  const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey);
@@ -1265,6 +1279,7 @@ export class CodexRunner {
1265
1279
  this.onSessionIdUpdate?.(sessionId, agentSessionId);
1266
1280
  }
1267
1281
  async closeSession(sessionId) {
1282
+ this.permissionContexts.get(sessionId)?.pauseController?.cancel();
1268
1283
  const threadId = this.activeSessions.get(sessionId);
1269
1284
  this.activeSessions.delete(sessionId);
1270
1285
  this.activeStreams.delete(sessionId);
@@ -459,11 +459,30 @@ function isWithin(root, candidate) {
459
459
  const relative = path.relative(root, candidate);
460
460
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
461
461
  }
462
- function resolveToolPath(projectPath, candidate, forWrite = false) {
462
+ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath) {
463
463
  const root = fs.realpathSync(projectPath);
464
464
  const resolved = path.resolve(root, candidate || '.');
465
- if (!isWithin(root, resolved))
465
+ const allowedBootstrapFile = bootstrapAgentMdPath
466
+ ? path.resolve(bootstrapAgentMdPath)
467
+ : undefined;
468
+ const isBootstrapAgentMd = allowedBootstrapFile === resolved;
469
+ if (!isWithin(root, resolved) && !isBootstrapAgentMd) {
466
470
  throw new Error(`Path is outside the project: ${candidate}`);
471
+ }
472
+ if (isBootstrapAgentMd) {
473
+ const parent = path.dirname(resolved);
474
+ const canonicalParent = fs.realpathSync(parent);
475
+ if (!isWithin(parent, canonicalParent) || !isWithin(canonicalParent, parent)) {
476
+ throw new Error(`Bootstrap agent.md parent resolves outside its declared path: ${candidate}`);
477
+ }
478
+ if (fs.existsSync(resolved)) {
479
+ const canonicalFile = fs.realpathSync(resolved);
480
+ if (!isWithin(resolved, canonicalFile) || !isWithin(canonicalFile, resolved)) {
481
+ throw new Error(`Bootstrap agent.md resolves outside its declared path: ${candidate}`);
482
+ }
483
+ }
484
+ return resolved;
485
+ }
467
486
  const existing = forWrite ? path.dirname(resolved) : resolved;
468
487
  let cursor = existing;
469
488
  while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
@@ -522,7 +541,7 @@ async function walkFiles(root, signal) {
522
541
  function textResult(text, details = {}) {
523
542
  return { content: [{ type: 'text', text: truncate(text) }], details };
524
543
  }
525
- function createTools(env, projectPath, runtimeEnv) {
544
+ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
526
545
  const read = {
527
546
  name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
528
547
  parameters: Type.Object({
@@ -531,7 +550,7 @@ function createTools(env, projectPath, runtimeEnv) {
531
550
  limit: Type.Optional(Type.Number({ minimum: 1 })),
532
551
  }),
533
552
  async execute(_id, params, signal) {
534
- const file = resolveToolPath(projectPath, params.file_path);
553
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
535
554
  const stat = await fs.promises.stat(file);
536
555
  if (stat.size > MAX_READ_BYTES)
537
556
  throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
@@ -548,7 +567,7 @@ function createTools(env, projectPath, runtimeEnv) {
548
567
  name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
549
568
  parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
550
569
  async execute(_id, params, signal) {
551
- const root = resolveToolPath(projectPath, params.path ?? '.');
570
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
552
571
  const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
553
572
  const files = (await walkFiles(root, signal))
554
573
  .map(file => path.relative(root, file).replaceAll(path.sep, '/'))
@@ -564,7 +583,7 @@ function createTools(env, projectPath, runtimeEnv) {
564
583
  case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
565
584
  }),
566
585
  async execute(_id, params, signal) {
567
- const root = resolveToolPath(projectPath, params.path ?? '.');
586
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
568
587
  const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
569
588
  const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
570
589
  const flags = params.case_insensitive ? 'i' : '';
@@ -597,7 +616,7 @@ function createTools(env, projectPath, runtimeEnv) {
597
616
  name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
598
617
  parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
599
618
  async execute(_id, params, signal) {
600
- const file = resolveToolPath(projectPath, params.file_path, true);
619
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath);
601
620
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
602
621
  await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
603
622
  return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
@@ -610,7 +629,7 @@ function createTools(env, projectPath, runtimeEnv) {
610
629
  replace_all: Type.Optional(Type.Boolean()),
611
630
  }), executionMode: 'sequential',
612
631
  async execute(_id, params, signal) {
613
- const file = resolveToolPath(projectPath, params.file_path);
632
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
614
633
  const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
615
634
  if (!params.old_string)
616
635
  throw new Error('old_string must not be empty');
@@ -809,7 +828,7 @@ export class EcagentRunner {
809
828
  models,
810
829
  model,
811
830
  thinkingLevel: normalizeThinkingLevel(effort),
812
- tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv),
831
+ tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath),
813
832
  systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
814
833
  streamOptions: {
815
834
  timeoutMs: this.config.timeoutMs,
@@ -823,7 +842,15 @@ export class EcagentRunner {
823
842
  runtime.harness = undefined;
824
843
  throw new Error('ecagent run interrupted');
825
844
  }
826
- harness.on('tool_call', event => this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input));
845
+ harness.on('tool_call', async (event, signal) => {
846
+ const pauseController = this.permissionContexts.get(request.sessionId)?.pauseController;
847
+ if (pauseController) {
848
+ const pauseResult = await pauseController.waitAtToolBoundary(signal);
849
+ if (pauseResult === 'cancelled')
850
+ return { block: true, reason: '工具调用已取消' };
851
+ }
852
+ return this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
853
+ });
827
854
  const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
828
855
  queue.push({ type: 'state_changed', state: 'running' });
829
856
  queue.push({ type: 'status', subtype: 'started', message: `ecagent is using ${modelId}` });
@@ -1043,6 +1070,7 @@ export class EcagentRunner {
1043
1070
  return decision.matched && decision.decision === 'deny' ? { block: true, reason: '危险操作未获人工批准' } : {};
1044
1071
  }
1045
1072
  async interrupt(sessionKey) {
1073
+ this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
1046
1074
  this.permissionGateway?.cancelAll(sessionKey, 'interrupted');
1047
1075
  const harness = this.runtimes.get(sessionKey)?.harness;
1048
1076
  if (harness) {
@@ -1058,6 +1086,7 @@ export class EcagentRunner {
1058
1086
  void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
1059
1087
  }
1060
1088
  async closeSession(sessionId) {
1089
+ this.permissionContexts.get(sessionId)?.pauseController?.cancel();
1061
1090
  await this.interrupt(sessionId).catch(() => { });
1062
1091
  this.runtimes.delete(sessionId);
1063
1092
  this.activeStreams.delete(sessionId);