evolcore 0.0.23 → 0.0.24

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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
package/CHANGELOG.md CHANGED
@@ -3,6 +3,56 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.24 (2026-09-14)
7
+
8
+ ### Windows 与 Codex 运行时
9
+
10
+ - 启动、重启前统一检查 Codex managed hook;
11
+ 缺失时自动安装并验证,异常时保持失败关闭。
12
+ - managed hook 按 thread 凭据分流;
13
+ 非托管 Codex 不再受机器级策略误伤。
14
+ - 凭据可跨 daemon 重启恢复;
15
+ 失效或伪造凭据继续保持失败关闭。
16
+ - 修复 Windows ACL、npm 子进程和安全输出解析;
17
+ 兼容 cmd.exe 包装与跨平台可执行文件查找。
18
+ - 修复 Windows Codex 沙箱配置;
19
+ 提升自启动任务的运行权限。
20
+ - 将 Windows Hook 启动探针并入 `ec diagnose --codex-hook`;
21
+ 不再增加 Hook 热路径入口。
22
+ - 恢复 Codex 会话共享策略并保留 Agent 描述;
23
+ 修复单 runner 与直接 backend key 的解析。
24
+ - Codex 首轮会话在 rollout 可恢复后绑定;
25
+ 审批请求、响应和终态统一记录生命周期审计。
26
+
27
+ ### Agent、AID 与生命周期
28
+
29
+ - 新增受权限约束的 AID 查询、展示和管理能力。
30
+ - 新增 Agent 创建、委托创建和创建进度管理;
31
+ 统一创建参数与 Base Agent 探测。
32
+ - 完善 Agent 生命周期、Bootstrap、Owner Welcome;
33
+ 收敛会话交接与主会话切换行为。
34
+ - Claude 会话恢复增加 transcript 就绪校验;
35
+ 失效话题提供 `/renew` 恢复提示。
36
+ - 统一 Agent outbox 的 canonical 路径;
37
+ 旧布局首次读写时执行去重导入。
38
+ - 推理心跳与空闲通知状态隔离;
39
+ 修复话题会话命令的回复路由。
40
+ - `ec stats` 支持托管任务和关系角色范围;
41
+ `ec msg history` 支持管理角色跨对端查询。
42
+ - 自动安装插件,并修复 Evol App 的 Menu 兼容访问。
43
+
44
+ ### AUN、权限与模型
45
+
46
+ - 增强 AUN 消息可靠投递、去重恢复和群消息处理。
47
+ - 统一远程配对、token 鉴权和 Menu 命令权限边界。
48
+ - 主机支持查询 Base Agent 模型列表;
49
+ 统一模型目录与推理强度回退。
50
+ - 合并模型价格与能力参数表;
51
+ 统一维护价格、上下文窗口和模型能力。
52
+ - `max_tokens` 改用模型真实 `context_window`;
53
+ 未知模型继续使用兼容性默认值。
54
+ - 重启与升级流程改为事务化;
55
+ 编译失败时回滚并保持服务运行。
6
56
  ## 0.0.23 (2026-09-08)
7
57
 
8
58
  ### 会话、Trigger 与 Codex 运行时
package/README.md CHANGED
@@ -105,7 +105,9 @@ npm install -g evolcore
105
105
 
106
106
  Linux 全局安装会在 `postinstall` 阶段准备 Codex 受管运行所需的真实目录 `/etc/codex`。如果当前 npm 全局目录属于普通用户且该系统目录尚不存在,请先执行 `sudo install -d -m 0755 /etc/codex`,再重新安装。
107
107
 
108
- Windows 的 npm 安装和升级不会修改机器级 Codex 配置。首次使用 Codex 托管 Hook 需运行上面的 PowerShell 安装器,并批准 Hook 部署步骤的管理员授权;部署完成后,只要 npm 包位置和 Node.js 路径不变,普通 npm 升级无需再次部署。更换 npm prefix 或 Node.js 路径后需重新部署,详见 [Windows Codex 托管 Hook](docs/evolcore-install-script.md#windows-codex-托管-hook)。
108
+ Windows 的 npm 安装和升级不会直接写入机器级 Codex 配置。配置使用 Codex 后,执行 `ec start` 或 `ec restart` 时会自动检测托管 Hook;缺失时自动请求 Windows 管理员授权(UAC),安装并复检成功后继续启动。已安装时直接继续,无需单独执行安装命令,详见 [Windows Codex 托管 Hook](docs/codex-windows-managed-hook.md)。
109
+
110
+ 如果取消 UAC 或安装失败,本次启动或重启会停止;重启检查发生在停止旧服务之前。已有策略异常或安装位置变化需要管理员核对,自动安装不会覆盖这些状态。
109
111
 
110
112
  > **Windows 用户**:首次运行前可能需要执行 `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser`
111
113
 
@@ -3,6 +3,7 @@ param([Parameter(Mandatory = $true)][string]$Request)
3
3
  # Called by the Node installer. Paths and security descriptors are data, never
4
4
  # interpolated into PowerShell source. Use Windows PowerShell's built-in .NET ACL API.
5
5
  $ErrorActionPreference = 'Stop'
6
+ $ProgressPreference = 'SilentlyContinue'
6
7
  # A parent pwsh session can export its incompatible PowerShell 7 modules.
7
8
  # This helper always runs in Windows PowerShell 5.1.
8
9
  $env:PSModulePath = Join-Path $PSHOME 'Modules'
@@ -40,7 +41,8 @@ public static class EvolCoreAclProbe {
40
41
  [DllImport("advapi32.dll", SetLastError=true)]
41
42
  static extern bool GetTokenInformation(IntPtr token, int type, out IntPtr linked, int size, out int returned);
42
43
  [DllImport("advapi32.dll", SetLastError=true)]
43
- static extern bool DuplicateToken(IntPtr token, int level, out IntPtr duplicate);
44
+ static extern bool DuplicateTokenEx(IntPtr token, uint desiredAccess, IntPtr tokenAttributes,
45
+ int impersonationLevel, int tokenType, out IntPtr duplicate);
44
46
  [DllImport("advapi32.dll", SetLastError=true)]
45
47
  static extern bool AccessCheck(byte[] descriptor, IntPtr token, uint desired,
46
48
  ref Mapping mapping, byte[] privileges, ref uint length, out uint granted, out bool allowed);
@@ -60,7 +62,10 @@ public static class EvolCoreAclProbe {
60
62
  token = linked;
61
63
  TokenKind = "linked-token";
62
64
  } else { TokenKind = "current-token"; }
63
- if (!DuplicateToken(token, 2, out duplicate))
65
+ // AccessCheck only needs SecurityIdentification (1). A UAC linked
66
+ // token can be limited to that level, so requesting the higher
67
+ // SecurityImpersonation level fails with ERROR_BAD_IMPERSONATION_LEVEL.
68
+ if (!DuplicateTokenEx(token, 0x0000000A, IntPtr.Zero, 1, 2, out duplicate))
64
69
  throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
65
70
  var mapping = new Mapping { Read=0x120089, Write=0x120116, Execute=0x1200a0, All=0x1f01ff };
66
71
  uint length=1024, granted;
@@ -1,12 +1,67 @@
1
1
  import fs from 'node:fs';
2
2
  import net from 'node:net';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ const startedAt = Date.now();
8
+ const runId = randomUUID();
9
+ let logPath;
10
+ let logFailed = false;
11
+ let context = {};
12
+
13
+ // This file is also copied into the Linux runtime; keep diagnostics self-contained.
14
+ function diagnostic(event, details = {}) {
15
+ if (process.env.EVOLCORE_CODEX_HOOK_LOG === '0' || logFailed) return;
16
+ try {
17
+ if (!logPath) {
18
+ logPath = path.resolve(process.env.EVOLCORE_CODEX_HOOK_LOG
19
+ || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'log', 'evolcore-hook.jsonl'));
20
+ fs.mkdirSync(path.dirname(logPath), { recursive: true, mode: 0o700 });
21
+ // Rotation is best-effort when concurrent tool calls start together.
22
+ try {
23
+ if (fs.statSync(logPath).size >= 2 * 1024 * 1024) fs.renameSync(logPath, `${logPath}.1`);
24
+ } catch {}
25
+ }
26
+ fs.appendFileSync(logPath, JSON.stringify({
27
+ time: new Date().toISOString(), runId, pid: process.pid,
28
+ elapsedMs: Date.now() - startedAt, event, ...context, ...details,
29
+ }) + '\n', { mode: 0o600 });
30
+ } catch (error) {
31
+ logFailed = true;
32
+ process.stderr.write(`[evolcore] Hook diagnostic log unavailable (${error?.code || 'unknown'}): ${logPath || '(unresolved)'}\n`);
33
+ }
34
+ }
35
+
36
+ function errorDetails(error) {
37
+ // JSON parser messages and policy reasons can contain tool input. Omit them.
38
+ return {
39
+ errorName: error?.name,
40
+ errorCode: error?.code,
41
+ syscall: error?.syscall,
42
+ frames: typeof error?.stack === 'string'
43
+ ? error.stack.split('\n').filter(line => /^\s+at /.test(line)).slice(0, 6) : undefined,
44
+ };
45
+ }
46
+
47
+ process.on('uncaughtExceptionMonitor', error => diagnostic('uncaught_exception', errorDetails(error)));
48
+ process.on('exit', code => diagnostic('exit', { exitCode: code }));
49
+ diagnostic('start', {
50
+ nodeVersion: process.version, execPath: process.execPath, entrypoint: process.argv[1],
51
+ argumentCount: Math.max(0, process.argv.length - 2),
52
+ socketArgumentPresent: Boolean(process.argv[2]),
53
+ socketEnvironmentPresent: Boolean(process.env.EVOLCORE_INSTANCE_SOCKET),
54
+ socketArgumentUnexpanded: /^%EVOLCORE_INSTANCE_SOCKET%$/i.test(process.argv[2] || ''),
55
+ socketSource: process.argv[2] ? 'argument' : process.env.EVOLCORE_INSTANCE_SOCKET ? 'environment' : 'missing',
56
+ });
3
57
 
4
58
  // Only bound socket establishment. Once connected, the official Codex hook
5
59
  // timeout owns the lifetime of this process; an application timeout here would
6
60
  // turn /pause into a denial/allow race.
7
61
  const connectTimeoutMs = 1500;
8
62
 
9
- function deny(reason) {
63
+ function deny(reason, category) {
64
+ diagnostic('decision', { decision: 'deny', category });
10
65
  process.stdout.write(JSON.stringify({
11
66
  hookSpecificOutput: {
12
67
  hookEventName: 'PreToolUse',
@@ -21,6 +76,7 @@ function deny(reason) {
21
76
  }
22
77
 
23
78
  function queryDaemon(socketPath, payload) {
79
+ diagnostic('connect_start', { timeoutMs: connectTimeoutMs });
24
80
  return new Promise(resolve => {
25
81
  const socket = net.createConnection(socketPath);
26
82
  let buffer = '';
@@ -35,24 +91,53 @@ function queryDaemon(socketPath, payload) {
35
91
  resolve(value);
36
92
  };
37
93
  timer = setTimeout(() => {
38
- if (!connected) finish(null);
94
+ if (!connected) {
95
+ diagnostic('connect_timeout');
96
+ finish(null);
97
+ }
39
98
  }, connectTimeoutMs);
40
99
  timer.unref?.();
41
- socket.once('error', () => finish(null));
100
+ socket.once('error', error => {
101
+ diagnostic('socket_error', { connected, ...errorDetails(error) });
102
+ finish(null);
103
+ });
104
+ socket.once('close', () => {
105
+ if (!settled) diagnostic('socket_closed_without_response', { connected, responseBytes: Buffer.byteLength(buffer) });
106
+ });
42
107
  socket.once('connect', () => {
43
108
  connected = true;
44
109
  clearTimeout(timer);
110
+ diagnostic('connected');
45
111
  socket.write(JSON.stringify(payload) + '\n');
46
112
  });
47
113
  socket.on('data', chunk => {
48
114
  buffer += String(chunk);
49
115
  const index = buffer.indexOf('\n');
50
116
  if (index < 0) return;
51
- try { finish(JSON.parse(buffer.slice(0, index))); } catch { finish(null); }
117
+ try {
118
+ const result = JSON.parse(buffer.slice(0, index));
119
+ diagnostic('response', {
120
+ ok: result?.ok === true, applicable: result?.applicable === true,
121
+ decision: ['allow', 'deny'].includes(result?.decision) ? result.decision : 'other',
122
+ reasonPresent: typeof result?.reason === 'string',
123
+ });
124
+ finish(result);
125
+ } catch (error) {
126
+ diagnostic('invalid_response', { responseBytes: Buffer.byteLength(buffer), ...errorDetails(error) });
127
+ finish(null);
128
+ }
52
129
  });
53
130
  });
54
131
  }
55
132
 
133
+ // The Windows managed requirements are machine-wide, so unmanaged Codex
134
+ // processes also reach this hook. EvolCore-owned threads carry a per-thread
135
+ // credential (EVOLCORE_CODEX_SESSION_TOKEN, injected through the thread's
136
+ // shell environment policy); unmanaged ones do not. Unmanaged calls are
137
+ // allowed outright and never contact the daemon — EvolCore being stopped must
138
+ // not break a standalone Codex. Credentialed calls always require an explicit
139
+ // daemon allow: a credential without its daemon is an orphaned managed thread
140
+ // and fails closed.
56
141
  async function main() {
57
142
  // Windows managed requirements use one machine-wide command. Resolve the
58
143
  // per-daemon named pipe from the inherited environment so multiple EvolCore
@@ -61,17 +146,45 @@ async function main() {
61
146
  let input;
62
147
  try {
63
148
  input = JSON.parse(fs.readFileSync(0, 'utf8'));
64
- } catch {
65
- deny('Invalid Codex PreToolUse payload.');
149
+ } catch (error) {
150
+ diagnostic('invalid_input', errorDetails(error));
151
+ deny('Invalid Codex PreToolUse payload.', 'invalid_input');
66
152
  return;
67
153
  }
154
+ context = {
155
+ sessionId: typeof input?.session_id === 'string' ? input.session_id.slice(0, 128) : undefined,
156
+ toolName: typeof input?.tool_name === 'string' ? input.tool_name.slice(0, 128) : undefined,
157
+ callId: typeof (input?.tool_use_id ?? input?.toolUseId ?? input?.item_id ?? input?.itemId) === 'string'
158
+ ? (input.tool_use_id ?? input.toolUseId ?? input.item_id ?? input.itemId).slice(0, 128) : undefined,
159
+ };
160
+ diagnostic('input', {
161
+ preToolUse: input?.hook_event_name === 'PreToolUse',
162
+ sessionIdPresent: typeof input?.session_id === 'string' && Boolean(input.session_id),
163
+ toolNamePresent: typeof input?.tool_name === 'string' && Boolean(input.tool_name),
164
+ toolInputPresent: input?.tool_input !== undefined,
165
+ });
68
166
  if (!socketPath
69
167
  || input?.hook_event_name !== 'PreToolUse'
70
168
  || typeof input?.session_id !== 'string'
71
169
  || !input.session_id
72
170
  || typeof input?.tool_name !== 'string'
73
171
  || !input.tool_name) {
74
- deny('Incomplete Codex PreToolUse payload.');
172
+ deny('Incomplete Codex PreToolUse payload.', 'incomplete_input');
173
+ return;
174
+ }
175
+
176
+ const sessionToken = typeof process.env.EVOLCORE_CODEX_SESSION_TOKEN === 'string'
177
+ && process.env.EVOLCORE_CODEX_SESSION_TOKEN
178
+ ? process.env.EVOLCORE_CODEX_SESSION_TOKEN
179
+ : undefined;
180
+
181
+ if (!sessionToken) {
182
+ // Unmanaged process: nothing to enforce, and no reason to pay the daemon
183
+ // round-trip (or its outage) for a call we would allow anyway.
184
+ return;
185
+ }
186
+ if (!socketPath) {
187
+ deny('EvolCore session credential is present but the policy service address is missing.');
75
188
  return;
76
189
  }
77
190
 
@@ -80,16 +193,20 @@ async function main() {
80
193
  threadId: input.session_id,
81
194
  toolName: input.tool_name,
82
195
  toolInput: input.tool_input ?? null,
196
+ sessionToken,
83
197
  ...(typeof (input.tool_use_id ?? input.toolUseId ?? input.item_id ?? input.itemId) === 'string'
84
198
  ? { callId: input.tool_use_id ?? input.toolUseId ?? input.item_id ?? input.itemId }
85
199
  : {}),
86
200
  });
87
201
  if (!result) {
88
- deny('EvolCore policy service is unavailable.');
202
+ deny('EvolCore policy service is unavailable.', 'service_unavailable');
203
+ return;
204
+ }
205
+ if (result.ok === true && result.applicable === true && result.decision === 'allow') {
206
+ diagnostic('decision', { decision: 'allow' });
89
207
  return;
90
208
  }
91
- if (result.ok === true && result.applicable === true && result.decision === 'allow') return;
92
- deny(result.reason || 'EvolCore policy did not explicitly allow this tool call.');
209
+ deny(result.reason || 'EvolCore policy did not explicitly allow this tool call.', 'policy_denied');
93
210
  }
94
211
 
95
212
  void main();
@@ -2,17 +2,95 @@
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { spawn } from 'child_process';
5
+ import { fileURLToPath } from 'url';
5
6
 
6
7
  const MAX_OUTPUT_BYTES = 1024 * 1024;
7
8
  const EC_EXECUTABLES = new Set(['ec']);
8
9
  const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
9
- const HEAD_EXECUTABLE = ['/usr/bin/head', '/bin/head'].find(candidate => {
10
+ const IS_WINDOWS = process.platform === 'win32';
11
+ const HELPER_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ // A rooted POSIX path is not absolute on Windows: Node resolves `/usr/bin/head`
13
+ // onto the current drive as `C:\usr\bin\head`. Keep these fixed candidates as
14
+ // the trusted fast path where they are genuinely absolute, and discover the
15
+ // executable through PATH everywhere else.
16
+ const POSIX_HEAD_CANDIDATES = ['/usr/bin/head', '/bin/head'];
17
+
18
+ function isRegularFile(candidate) {
10
19
  try {
11
20
  return fs.statSync(candidate).isFile();
12
21
  } catch {
13
22
  return false;
14
23
  }
15
- });
24
+ }
25
+
26
+ function pathExtensionCandidates() {
27
+ if (!IS_WINDOWS) return [''];
28
+ const configured = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
29
+ .split(';')
30
+ .map(value => value.trim())
31
+ .filter(Boolean);
32
+ // The extensionless name is tried first so an explicitly named file wins over
33
+ // a same-named shim; npm publishes both `ec` (sh) and `ec.cmd` on Windows.
34
+ return ['', ...configured];
35
+ }
36
+
37
+ /**
38
+ * Resolve one executable to an absolute path without invoking a shell.
39
+ * `spawn` performs no PATH or PATHEXT lookup on Windows, which is why a bare
40
+ * command name has to be materialized here before the child is started.
41
+ */
42
+ function resolveExecutablePath(executable) {
43
+ if (executable.includes('/') || executable.includes('\\')) {
44
+ return isRegularFile(executable) ? path.resolve(executable) : undefined;
45
+ }
46
+ const entries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
47
+ for (const entry of entries) {
48
+ for (const extension of pathExtensionCandidates()) {
49
+ const candidate = path.join(entry, `${executable}${extension}`);
50
+ if (isRegularFile(candidate)) return path.resolve(candidate);
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ function resolveHeadExecutable() {
57
+ if (!IS_WINDOWS) {
58
+ const fixed = POSIX_HEAD_CANDIDATES.find(isRegularFile);
59
+ if (fixed) return fixed;
60
+ }
61
+ return resolveExecutablePath('head');
62
+ }
63
+
64
+ const HEAD_EXECUTABLE = resolveHeadExecutable();
65
+
66
+ /**
67
+ * Resolve the concrete process for a validated payload.
68
+ *
69
+ * For `ec`, prefer this package's own Node entrypoint next to the helper. It is
70
+ * the same installation that published this file, needs no PATH lookup, and
71
+ * avoids the Windows batch shim entirely — `CreateProcess` cannot execute a
72
+ * `.cmd` wrapper, so `spawn('ec')` fails with ENOENT there.
73
+ */
74
+ function resolveChildProcessCommand(payload) {
75
+ if (payload.kind === 'ec') {
76
+ const nodeEntrypoint = path.join(HELPER_DIR, 'ec.js');
77
+ // On Windows CreateProcess cannot execute the npm `.cmd` shim reliably,
78
+ // so prefer this package's Node entrypoint. POSIX hosts can resolve the
79
+ // trusted `ec` executable normally; this also preserves caller-provided
80
+ // PATH shims used by isolated runtimes and tests.
81
+ if (IS_WINDOWS && isRegularFile(nodeEntrypoint)) {
82
+ return { file: process.execPath, args: [nodeEntrypoint, ...payload.argv.slice(1)] };
83
+ }
84
+ }
85
+ const resolved = resolveExecutablePath(payload.argv[0]);
86
+ if (!resolved) fail(`unable to resolve executable: ${payload.argv[0]}`);
87
+ // Quoting rules for batch files differ from CreateProcess argument parsing,
88
+ // so a `.cmd`/`.bat` target could execute something other than this argv.
89
+ if (IS_WINDOWS && /\.(?:cmd|bat)$/i.test(resolved)) {
90
+ fail(`refusing to run a batch wrapper: ${resolved}`);
91
+ }
92
+ return { file: resolved, args: payload.argv.slice(1) };
93
+ }
16
94
 
17
95
  function fail(message) {
18
96
  process.stderr.write(`ec-safe-output: ${message}\n`);
@@ -143,6 +221,9 @@ if (!validateCommand(payload)) fail('command is not permitted');
143
221
  if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
144
222
  if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
145
223
 
224
+ // Resolve the child executable before anything with a side effect: an
225
+ // unresolvable command must not leave an empty create-only output file behind.
226
+ const childCommand = resolveChildProcessCommand(payload);
146
227
  const paths = resolveExecutionPaths(payload);
147
228
  const output = openOutputFile(payload, paths);
148
229
  let bytesWritten = 0;
@@ -182,10 +263,11 @@ if (head) {
182
263
  head.stdin.on('error', () => {});
183
264
  }
184
265
 
185
- const child = spawn(payload.argv[0], payload.argv.slice(1), {
266
+ const child = spawn(childCommand.file, childCommand.args, {
186
267
  cwd: paths.cwd,
187
268
  env: process.env,
188
269
  stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
270
+ windowsHide: IS_WINDOWS,
189
271
  });
190
272
  const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
191
273
  if (head) {
@@ -18,6 +18,10 @@ function argument(name) {
18
18
  return index >= 0 ? process.argv[index + 1] : undefined;
19
19
  }
20
20
 
21
+ function hasArgument(name) {
22
+ return process.argv.includes(name);
23
+ }
24
+
21
25
  function fail(message) {
22
26
  process.stderr.write(`[evolcore] ${message}\n`);
23
27
  process.exit(1);
@@ -91,6 +95,14 @@ function windowsCommandArg(value) {
91
95
  return `"${value.replaceAll('"', '\\"')}"`;
92
96
  }
93
97
 
98
+ function windowsPostinstallNotice() {
99
+ if (process.platform !== 'win32') return;
100
+ process.stderr.write(
101
+ '[evolcore] ec start/restart automatically checks and installs missing Codex managed hooks on Windows.\n'
102
+ + '[evolcore] Accept the Windows administrator (UAC) prompt when installation is needed; no separate setup command is required.\n',
103
+ );
104
+ }
105
+
94
106
  function removeManagedBlock(source) {
95
107
  const original = parseToml(source);
96
108
  if (!source.includes(markerStart) && !source.includes(markerEnd)) return source;
@@ -498,12 +510,11 @@ export function provisionManagedFiles(options, acl = windowsAcl) {
498
510
  }
499
511
 
500
512
  function main() {
501
- // npm may preserve its parent lifecycle variable on Windows when this
502
- // script is spawned from a test runner. A global install without an
503
- // explicit package root is still the postinstall shape; never enter the
504
- // machine provisioning branch on an ordinary Windows lifecycle.
505
- if (lifecycleEvent() === 'postinstall' || (isGlobalInstall() && !argument('--package-root'))) {
513
+ // An explicit package root selects provisioning, even if the elevated
514
+ // child inherited npm lifecycle variables. Ordinary postinstall skips it.
515
+ if (!argument('--package-root') && (lifecycleEvent() === 'postinstall' || isGlobalInstall())) {
506
516
  runLinuxPostinstall();
517
+ windowsPostinstallNotice();
507
518
  return;
508
519
  }
509
520
  if (process.platform === 'linux' && argument('--mountpoint')) {
@@ -522,7 +533,7 @@ function main() {
522
533
  for (const file of [sourceHook, nodePath]) {
523
534
  if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`);
524
535
  }
525
- if (argument('--repair')) {
536
+ if (hasArgument('--repair')) {
526
537
  const result = repairManagedHookInstallation({ requirements, managedDir: targetDirectory, packageRoot, nodePath });
527
538
  process.stdout.write(`${JSON.stringify(result)}\n`);
528
539
  return;
@@ -1,4 +1,4 @@
1
- import { resolveCommandPath } from '../utils/cross-platform.js';
1
+ import { commandExists } from '../utils/cross-platform.js';
2
2
  import { getCodexAppServerAvailability } from './codex-runner.js';
3
3
  export const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
4
4
  export const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
@@ -13,12 +13,12 @@ export function detectAvailableBaseagents(codexAvailability = getCodexAppServerA
13
13
  return true;
14
14
  if (baseagent === 'codex')
15
15
  return codexAvailability.available;
16
- return resolveCommandPath(baseagent) !== null;
16
+ return commandExists(baseagent);
17
17
  });
18
18
  }
19
19
  export function detectBaseagentDetails(codexAvailability = getCodexAppServerAvailability()) {
20
- const claude = resolveCommandPath('claude') !== null;
21
- const gemini = resolveCommandPath('gemini') !== null;
20
+ const claude = commandExists('claude');
21
+ const gemini = commandExists('gemini');
22
22
  return [
23
23
  {
24
24
  id: 'claude',
@@ -12,7 +12,35 @@
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
14
  import os from 'os';
15
- import { commandExists } from '../utils/cross-platform.js';
15
+ import { commandExists, isWindows, resolveProbePath } from '../utils/cross-platform.js';
16
+ import { execFileSync } from 'child_process';
17
+ /** Resolve the installed Codex CLI entrypoint, including npm shims on Windows. */
18
+ export function resolveCodexCliPath() {
19
+ return resolveProbePath('codex');
20
+ }
21
+ export function resolveCodexLaunchCommand(args, options = {}) {
22
+ const windows = options.isWindows ?? isWindows;
23
+ const commandPath = options.commandPath !== undefined ? options.commandPath : resolveCodexCliPath();
24
+ if (!commandPath)
25
+ return null;
26
+ if (windows && /\.(cmd|bat)$/i.test(commandPath)) {
27
+ return { command: options.comSpec ?? process.env.ComSpec ?? 'cmd.exe', args: ['/d', '/s', '/c', commandPath, ...args] };
28
+ }
29
+ return { command: commandPath, args };
30
+ }
31
+ /** Execute a Codex CLI command using the resolved npm shim/path. */
32
+ export function execCodexCliSync(args, options) {
33
+ const commandPath = resolveCodexCliPath();
34
+ if (!commandPath)
35
+ throw new Error('Codex CLI not found');
36
+ const launchCommand = resolveCodexLaunchCommand(args, { commandPath });
37
+ if (!launchCommand)
38
+ throw new Error('Codex CLI not found');
39
+ return execFileSync(launchCommand.command, launchCommand.args, {
40
+ ...(options ?? {}),
41
+ windowsHide: isWindows,
42
+ });
43
+ }
16
44
  const BASEAGENT_ALIASES = {
17
45
  claude: { canonical: 'claude', displayName: 'Claude Code' },
18
46
  cc: { canonical: 'claude', displayName: 'Claude Code' },