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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.12 (2026-08-19)
7
+
8
+ ### AUN 通信
9
+
10
+ - 规范结构化提及在消息收发、队列和响应链路中的传递,完善 @all、目标校验和非法载荷处理。
11
+ - 增强入站去重、断线重投和错误回传,避免消息重复处理与错误循环。
12
+
13
+ ### 会话与 Agent
14
+
15
+ - 串行化 Codex 会话的继续、压缩、分叉和回滚操作,完善 app-server 进程管理和安全清理。
16
+ - 优化 proactive 会话、Handoff 和 Bootstrap 的生命周期处理,隔离并发会话上下文。
17
+
18
+ ### 权限与菜单
19
+
20
+ - 收敛菜单执行上下文和托管命令授权,强化 EC shell 与 Codex 命令解析边界。
21
+ - 完善菜单批量查询、关系上下文、联系人状态和工具拦截审计。
22
+
23
+ ### 运行兼容性
24
+
25
+ - 统一跨平台子进程启动和 Windows 窗口处理,改进升级监控与内置 AUN SDK 管理。
26
+ - 统一 Claude、Codex 的技能发现和运行时上下文注入。
27
+
6
28
  ## 0.0.11 (2026-08-11)
7
29
 
8
30
  ### Agent 初始化与引导
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+
4
+ const timeoutMs = 1500;
5
+
6
+ function deny(reason) {
7
+ process.stdout.write(JSON.stringify({
8
+ hookSpecificOutput: {
9
+ hookEventName: 'PreToolUse',
10
+ permissionDecision: 'deny',
11
+ permissionDecisionReason: reason || 'EvolCore policy denied this tool call.',
12
+ },
13
+ }));
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
+ process.stderr.write(`${reason || 'EvolCore policy denied this tool call.'}\n`);
18
+ process.exitCode = 2;
19
+ }
20
+
21
+ function queryDaemon(socketPath, payload) {
22
+ return new Promise(resolve => {
23
+ const socket = net.createConnection(socketPath);
24
+ let buffer = '';
25
+ let settled = false;
26
+ let timer;
27
+ const finish = value => {
28
+ if (settled) return;
29
+ settled = true;
30
+ clearTimeout(timer);
31
+ socket.destroy();
32
+ resolve(value);
33
+ };
34
+ timer = setTimeout(() => finish(null), timeoutMs);
35
+ timer.unref?.();
36
+ socket.once('error', () => finish(null));
37
+ socket.once('connect', () => socket.write(JSON.stringify(payload) + '\n'));
38
+ socket.on('data', chunk => {
39
+ buffer += String(chunk);
40
+ const index = buffer.indexOf('\n');
41
+ if (index < 0) return;
42
+ try { finish(JSON.parse(buffer.slice(0, index))); } catch { finish(null); }
43
+ });
44
+ });
45
+ }
46
+
47
+ async function main() {
48
+ const socketPath = process.argv[2];
49
+ let input;
50
+ try {
51
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
52
+ } catch {
53
+ deny('Invalid Codex PreToolUse payload.');
54
+ return;
55
+ }
56
+ if (!socketPath
57
+ || input?.hook_event_name !== 'PreToolUse'
58
+ || typeof input?.session_id !== 'string'
59
+ || !input.session_id
60
+ || typeof input?.tool_name !== 'string'
61
+ || !input.tool_name) {
62
+ deny('Incomplete Codex PreToolUse payload.');
63
+ return;
64
+ }
65
+
66
+ const result = await queryDaemon(socketPath, {
67
+ type: 'codex-pre-tool-use',
68
+ threadId: input.session_id,
69
+ toolName: typeof input.tool_name === 'string' ? input.tool_name : '',
70
+ toolInput: input.tool_input ?? null,
71
+ });
72
+ if (!result) {
73
+ deny('EvolCore policy service is unavailable.');
74
+ return;
75
+ }
76
+ if (result.ok === true && result.applicable === true && result.decision === 'allow') return;
77
+ deny(result.reason || 'EvolCore policy did not explicitly allow this tool call.');
78
+ }
79
+
80
+ void main();
@@ -14,7 +14,7 @@ import { logger } from '../utils/logger.js';
14
14
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
15
15
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
16
16
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
17
- import { summarizeToolInput } from '../utils/tool-summary.js';
17
+ import { summarizeToolInput, summarizeToolInputForAudit } from '../utils/tool-summary.js';
18
18
  import { encodePath } from '../utils/cross-platform.js';
19
19
  import { getPackageRoot, resolvePaths } from '../paths.js';
20
20
  import { resolveEffective } from '../config/config-manager.js';
@@ -1852,6 +1852,27 @@ export class AgentRunner {
1852
1852
  ...(updatedInput ? { updatedInput } : {}),
1853
1853
  },
1854
1854
  });
1855
+ const recordPreflightDenial = async (toolName, toolInput, preflight, requestId, policyCode) => {
1856
+ const code = policyCode ?? (preflight?.behavior === 'deny' ? preflight.policyCode : undefined);
1857
+ if (!code)
1858
+ return;
1859
+ try {
1860
+ await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
1861
+ code: 'operation_blocked',
1862
+ severity: 'warning',
1863
+ phase: 'execution',
1864
+ occurredAt: Date.now(),
1865
+ toolName,
1866
+ ...(typeof requestId === 'string' && requestId ? { requestId } : {}),
1867
+ policyCode: code,
1868
+ summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
1869
+ effect: 'operation_skipped',
1870
+ });
1871
+ }
1872
+ catch (error) {
1873
+ logger.warn(`[AgentRunner] failed to record preflight denial: session=${sessionId} tool=${toolName} code=${code} error=${error instanceof Error ? error.message : String(error)}`);
1874
+ }
1875
+ };
1855
1876
  // A rewritten helper command is trusted only after this hook produced its
1856
1877
  // random one-shot payload from a validated output wrapper.
1857
1878
  const trustedSafeOutputCommands = new Set();
@@ -1896,6 +1917,7 @@ export class AgentRunner {
1896
1917
  // proactive 模式行为策略(首次工具调用必须是 ec msg send)
1897
1918
  const policyResult = this.permissionContexts.get(sessionId)?.policyHook?.(toolName, toolInput);
1898
1919
  if (policyResult?.block) {
1920
+ await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, policyResult.policyCode ?? 'session_policy_hook');
1899
1921
  return hookDecision('deny', policyResult.reason || '当前会话策略拒绝此工具调用', updatedInput);
1900
1922
  }
1901
1923
  const permCtx = this.permissionContexts.get(sessionId);
@@ -1916,8 +1938,10 @@ export class AgentRunner {
1916
1938
  toolInput = preflight.input;
1917
1939
  if (toolInput !== originalInput)
1918
1940
  updatedInput = toolInput;
1919
- if (preflight.behavior === 'deny')
1941
+ if (preflight.behavior === 'deny') {
1942
+ await recordPreflightDenial(toolName, toolInput, preflight, input.tool_use_id ?? input.toolUseID);
1920
1943
  return hookDecision('deny', preflight.message, updatedInput);
1944
+ }
1921
1945
  if (preflight.behavior === 'allow') {
1922
1946
  const commandAfterPreflight = typeof toolInput.command === 'string' ? toolInput.command : undefined;
1923
1947
  if (commandAfterPreflight && commandAfterPreflight !== commandBeforePreflight) {
@@ -1942,6 +1966,7 @@ export class AgentRunner {
1942
1966
  };
1943
1967
  const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
1944
1968
  if (roResult.behavior === 'deny') {
1969
+ await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, 'readonly_mode');
1945
1970
  return hookDecision('deny', roResult.message, updatedInput);
1946
1971
  }
1947
1972
  return hookDecision('allow', undefined, updatedInput);
@@ -1951,6 +1976,7 @@ export class AgentRunner {
1951
1976
  }
1952
1977
  if (isUnknownClaudeTool(toolName)) {
1953
1978
  if (callPermissionMode === 'auto') {
1979
+ await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, 'auto_unknown_tool');
1954
1980
  return hookDecision('deny', `auto 模式拒绝未知工具 ${toolName}:该工具不受 Claude 本地 sandbox 的完整约束`, updatedInput);
1955
1981
  }
1956
1982
  // request/bypass only receive external tools admitted by the session's
@@ -1960,6 +1986,7 @@ export class AgentRunner {
1960
1986
  const danger = checkDangerousCommand(toolName, toolInput);
1961
1987
  if (danger.isDangerous) {
1962
1988
  if (callPermissionMode === 'auto') {
1989
+ await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, 'auto_dangerous_command');
1963
1990
  return hookDecision('deny', `危险操作已由 auto 模式拒绝:${danger.reason}`, updatedInput);
1964
1991
  }
1965
1992
  if (callPermissionMode === 'request' || callPermissionMode === 'bypass') {
@@ -2030,6 +2057,7 @@ export class AgentRunner {
2030
2057
  });
2031
2058
  input = preflight.input;
2032
2059
  if (preflight.behavior === 'deny') {
2060
+ await recordPreflightDenial(toolName, input, preflight, options.toolUseID);
2033
2061
  return { behavior: 'deny', message: preflight.message, decisionClassification: 'user_reject' };
2034
2062
  }
2035
2063
  if (preflight.behavior === 'allow') {
@@ -2041,6 +2069,7 @@ export class AgentRunner {
2041
2069
  }
2042
2070
  const permissionExpansion = inspectClaudePermissionExpansion(input, options, projectPath);
2043
2071
  if (permissionExpansion.protectedReason) {
2072
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, 'protected_permission_expansion');
2044
2073
  return {
2045
2074
  behavior: 'deny',
2046
2075
  message: permissionExpansion.protectedReason,
@@ -2049,6 +2078,7 @@ export class AgentRunner {
2049
2078
  }
2050
2079
  if (permissionExpansion.requiresApproval) {
2051
2080
  if (callPermissionMode === 'readonly' || callPermissionMode === 'auto') {
2081
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, callPermissionMode === 'readonly' ? 'readonly_permission_expansion' : 'auto_permission_expansion');
2052
2082
  return {
2053
2083
  behavior: 'deny',
2054
2084
  message: `${callPermissionMode} 模式拒绝 Claude SDK 文件系统或未知权限扩权请求`,
@@ -2092,6 +2122,7 @@ export class AgentRunner {
2092
2122
  };
2093
2123
  const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
2094
2124
  if (roResult.behavior === 'deny') {
2125
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, 'readonly_mode');
2095
2126
  return { behavior: 'deny', message: roResult.message, decisionClassification: 'user_reject' };
2096
2127
  }
2097
2128
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
@@ -2101,6 +2132,7 @@ export class AgentRunner {
2101
2132
  }
2102
2133
  if (isUnknownClaudeTool(toolName)) {
2103
2134
  if (callPermissionMode === 'auto') {
2135
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, 'auto_unknown_tool');
2104
2136
  return {
2105
2137
  behavior: 'deny',
2106
2138
  message: `auto 模式拒绝未知工具 ${toolName}:该工具不受 Claude 本地 sandbox 的完整约束`,
@@ -2112,6 +2144,7 @@ export class AgentRunner {
2112
2144
  const danger = checkDangerousCommand(toolName, input);
2113
2145
  if (danger.isDangerous) {
2114
2146
  if (callPermissionMode === 'auto') {
2147
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, 'auto_dangerous_command');
2115
2148
  return { behavior: 'deny', message: `auto 模式拒绝危险操作:${danger.reason}`, decisionClassification: 'user_reject' };
2116
2149
  }
2117
2150
  if (callPermissionMode === 'request' || callPermissionMode === 'bypass') {
@@ -5,6 +5,11 @@ import { buildHClassGuardCommand } from '../core/permission/sandbox-runtime.js';
5
5
  import { resolveCodexLaunchCommand } from '../utils/codex-cli.js';
6
6
  import { createHash } from 'crypto';
7
7
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+ import { getPackageRoot, resolvePaths } from '../paths.js';
12
+ import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from '../utils/codex-app-server-registry.js';
8
13
  const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
9
14
  function tomlProviderKey(value) {
10
15
  if (!value || /[\0\r\n]/.test(value)) {
@@ -41,6 +46,8 @@ export class CodexAppServerClient {
41
46
  initialized = false;
42
47
  initializing = null;
43
48
  stderrBuffer = [];
49
+ managedHookRuntimeDir = null;
50
+ processRegistrationPath = null;
44
51
  constructor(options) {
45
52
  this.options = options;
46
53
  }
@@ -298,25 +305,33 @@ export class CodexAppServerClient {
298
305
  pending.reject(new Error('Codex app-server closed'));
299
306
  }
300
307
  this.pending.clear();
301
- if (!proc)
308
+ if (!proc) {
309
+ this.cleanupManagedHookRuntime();
302
310
  return;
311
+ }
303
312
  // 优雅关闭:先关闭 stdin,然后发送 SIGTERM,等待进程退出
304
313
  proc.stdin.end();
305
314
  proc.kill('SIGTERM');
306
315
  // 等待进程退出,最多等待 5 秒
307
316
  await new Promise((resolve) => {
317
+ let exited = false;
308
318
  const timeout = setTimeout(() => {
309
- if (!proc.killed) {
310
- logger.warn('[CodexAppServer] Process did not exit after SIGTERM, sending SIGKILL');
319
+ if (exited)
320
+ return;
321
+ logger.warn('[CodexAppServer] Process did not exit after SIGTERM, sending SIGKILL');
322
+ try {
311
323
  proc.kill('SIGKILL');
312
324
  }
325
+ catch { }
313
326
  resolve();
314
327
  }, 5000);
315
328
  proc.once('exit', () => {
329
+ exited = true;
316
330
  clearTimeout(timeout);
317
331
  resolve();
318
332
  });
319
333
  });
334
+ this.cleanupManagedHookRuntime();
320
335
  }
321
336
  async ensureStarted() {
322
337
  if (this.initialized)
@@ -352,22 +367,56 @@ export class CodexAppServerClient {
352
367
  const launchCommand = resolveCodexLaunchCommand(args);
353
368
  if (!launchCommand)
354
369
  throw new Error('Codex CLI not found');
355
- const guardedCommand = buildHClassGuardCommand(launchCommand.command, launchCommand.args);
356
- if (process.platform === 'linux' && !guardedCommand) {
357
- throw new Error('Codex 缺少 EvolCore H 类路径隔离运行时,已拒绝启动 app-server');
370
+ try {
371
+ let command = launchCommand.command;
372
+ let commandArgs = launchCommand.args;
373
+ if (process.platform === 'linux') {
374
+ const managedRequirements = this.ensureManagedHookRuntime();
375
+ const guardedCommand = buildHClassGuardCommand(command, commandArgs, undefined, {
376
+ managedCodexRequirements: managedRequirements,
377
+ });
378
+ if (!guardedCommand) {
379
+ throw new Error('Codex 缺少 EvolCore managed-hook 隔离运行时,已拒绝启动 app-server');
380
+ }
381
+ command = guardedCommand.command;
382
+ commandArgs = guardedCommand.args;
383
+ }
384
+ this.proc = spawn(command, commandArgs, {
385
+ cwd: this.options.cwd,
386
+ env,
387
+ stdio: ['pipe', 'pipe', 'pipe'],
388
+ windowsHide: process.platform === 'win32',
389
+ });
390
+ try {
391
+ this.processRegistrationPath = registerCodexAppServerProcess(this.proc.pid);
392
+ }
393
+ catch (error) {
394
+ logger.warn(`[CodexAppServer] failed to register owned process pid=${this.proc.pid ?? '<unknown>'}: ${error}`);
395
+ }
358
396
  }
359
- this.proc = spawn(guardedCommand?.command ?? launchCommand.command, guardedCommand?.args ?? launchCommand.args, {
360
- cwd: this.options.cwd,
361
- env,
362
- stdio: ['pipe', 'pipe', 'pipe'],
397
+ catch (error) {
398
+ this.cleanupManagedHookRuntime();
399
+ throw error;
400
+ }
401
+ const proc = this.proc;
402
+ const processRegistrationPath = this.processRegistrationPath;
403
+ proc.once('error', error => {
404
+ unregisterCodexAppServerProcess(processRegistrationPath);
405
+ if (this.processRegistrationPath === processRegistrationPath) {
406
+ this.processRegistrationPath = null;
407
+ }
408
+ this.failAll(error);
363
409
  });
364
- this.proc.once('error', error => this.failAll(error));
365
- this.proc.once('exit', (code, signal) => {
410
+ proc.once('exit', (code, signal) => {
411
+ unregisterCodexAppServerProcess(processRegistrationPath);
412
+ if (this.processRegistrationPath === processRegistrationPath) {
413
+ this.processRegistrationPath = null;
414
+ }
366
415
  this.failAll(new Error(`Codex app-server exited: code=${code ?? 'null'} signal=${signal ?? 'null'}`));
367
416
  });
368
- const rl = readline.createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
417
+ const rl = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
369
418
  rl.on('line', line => this.handleLine(line));
370
- this.proc.stderr.on('data', chunk => {
419
+ proc.stderr.on('data', chunk => {
371
420
  const text = String(chunk).trim();
372
421
  if (!text)
373
422
  return;
@@ -377,6 +426,62 @@ export class CodexAppServerClient {
377
426
  logger.debug(`[CodexAppServer] ${text}`);
378
427
  });
379
428
  }
429
+ ensureManagedHookRuntime() {
430
+ if (this.managedHookRuntimeDir) {
431
+ return path.join(this.managedHookRuntimeDir, 'requirements.toml');
432
+ }
433
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'evolcore-codex-hooks-'));
434
+ try {
435
+ fs.chmodSync(directory, 0o700);
436
+ const hookPath = path.join(directory, 'pre-tool-use.mjs');
437
+ const sourcePath = path.join(getPackageRoot(), 'bin', 'codex-managed-hook.mjs');
438
+ fs.copyFileSync(sourcePath, hookPath, fs.constants.COPYFILE_EXCL);
439
+ fs.chmodSync(hookPath, 0o500);
440
+ const command = `${this.shellQuote(process.execPath)} ${this.shellQuote(hookPath)} ${this.shellQuote(resolvePaths().instanceSocket)}`;
441
+ const requirementsPath = path.join(directory, 'requirements.toml');
442
+ fs.writeFileSync(requirementsPath, [
443
+ 'allow_managed_hooks_only = true',
444
+ '',
445
+ '[features]',
446
+ 'hooks = true',
447
+ '',
448
+ '[hooks]',
449
+ `managed_dir = ${JSON.stringify(directory)}`,
450
+ '',
451
+ '[[hooks.PreToolUse]]',
452
+ 'matcher = "*"',
453
+ '',
454
+ '[[hooks.PreToolUse.hooks]]',
455
+ 'type = "command"',
456
+ `command = ${JSON.stringify(command)}`,
457
+ 'timeout = 3',
458
+ 'statusMessage = "Checking EvolCore session policy"',
459
+ '',
460
+ ].join('\n'), { mode: 0o400, flag: 'wx' });
461
+ this.managedHookRuntimeDir = directory;
462
+ return requirementsPath;
463
+ }
464
+ catch (error) {
465
+ try {
466
+ fs.rmSync(directory, { recursive: true, force: true });
467
+ }
468
+ catch { }
469
+ throw error;
470
+ }
471
+ }
472
+ shellQuote(value) {
473
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
474
+ }
475
+ cleanupManagedHookRuntime() {
476
+ const directory = this.managedHookRuntimeDir;
477
+ this.managedHookRuntimeDir = null;
478
+ if (!directory || !path.basename(directory).startsWith('evolcore-codex-hooks-'))
479
+ return;
480
+ try {
481
+ fs.rmSync(directory, { recursive: true, force: true });
482
+ }
483
+ catch { }
484
+ }
380
485
  buildProcessEnvironment() {
381
486
  const env = sanitizeShellExecutionEnvironment({
382
487
  ...process.env,
@@ -571,5 +676,6 @@ export class CodexAppServerClient {
571
676
  pending.reject(error);
572
677
  }
573
678
  this.pending.clear();
679
+ this.cleanupManagedHookRuntime();
574
680
  }
575
681
  }