evolcore 0.0.13 → 0.0.14

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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/install-codex-managed-hooks.mjs +201 -0
  4. package/dist/agents/claude-runner.js +53 -5
  5. package/dist/agents/codex-app-server-client.js +123 -2
  6. package/dist/agents/codex-runner.js +149 -30
  7. package/dist/agents/ecagent-runner.js +17 -1
  8. package/dist/agents/gemini-runner.js +9 -4
  9. package/dist/aun/msg/managed-operation.js +63 -3
  10. package/dist/channels/aun.js +144 -15
  11. package/dist/channels/daemon.js +2 -0
  12. package/dist/cli/aun-commands.js +1 -1
  13. package/dist/cli/fs-command.js +46 -9
  14. package/dist/cli/task-context.js +172 -0
  15. package/dist/config/builtin-roles.js +2 -0
  16. package/dist/config/config-manager.js +6 -2
  17. package/dist/config/contact-book-store.js +7 -2
  18. package/dist/core/auth/auth-gateway.js +1 -0
  19. package/dist/core/auth/authorization-audit.js +32 -0
  20. package/dist/core/auth/operation-catalog.js +3 -3
  21. package/dist/core/bootstrap-service.js +7 -1
  22. package/dist/core/command/command-handler.js +3 -0
  23. package/dist/core/event-catalog.js +2 -0
  24. package/dist/core/message/im-renderer.js +15 -1
  25. package/dist/core/message/message-bridge.js +5 -2
  26. package/dist/core/message/response-engine.js +138 -10
  27. package/dist/core/permission/ec-command-parser.js +556 -4
  28. package/dist/core/permission/tool-policy.js +17 -29
  29. package/dist/core/runtime-lock.js +101 -0
  30. package/dist/index.js +30 -3
  31. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  32. package/dist/response-system/modes/single-session/index.js +3 -0
  33. package/dist/trigger/history.js +42 -7
  34. package/dist/utils/error-utils.js +7 -0
  35. package/dist/utils/logger.js +37 -4
  36. package/kits/templates/roles/admin.json +2 -0
  37. package/kits/templates/roles/member.json +1 -0
  38. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.14 (2026-08-24)
7
+
8
+ ### Codex 受管运行与安全
9
+
10
+ - 强化 Codex proactive 模式的受管 PreToolUse 校验,Linux 使用隔离运行时,Windows 支持受管 Hook 安装、加载检查和 fail-closed 启动。
11
+ - 隔离会话临时目录并统一运行时锁管理,收紧 Shell、文件访问和受控 EC 命令解析,扩展群文件 ACL 操作及权限校验。
12
+ - 完善工具审批、委派票据和工具结果关联审计,避免未成功发送或未完成授权的操作推进主动模式状态。
13
+ - 为 Claude、Codex、ecagent 的工具预检拒绝补充统一结构化授权审计,记录策略码、请求/任务及会话上下文,便于跨 Runner 追踪拦截原因。
14
+ - 规范 Codex EC 审批命令与委派校验,拒绝无法规范化的命令、复合命令和空字节输入;允许触发器正文安全使用受管临时目录标记。
15
+
16
+ ### AUN 消息与授权
17
+
18
+ - 贯通 AUN 消息路由、会话上下文和授权审计关联信息,补齐请求、工具调用和结果之间的 correlation ID。
19
+ - 统一结构化日志字段,改进异常、策略拦截和拒绝结果的可追踪性。
20
+
6
21
  ## 0.0.13 (2026-08-20)
7
22
 
8
23
  ### AUN 消息与文件
@@ -45,7 +45,10 @@ function queryDaemon(socketPath, payload) {
45
45
  }
46
46
 
47
47
  async function main() {
48
- const socketPath = process.argv[2];
48
+ // Windows managed requirements use one machine-wide command. Resolve the
49
+ // per-daemon named pipe from the inherited environment so multiple EvolCore
50
+ // homes can share the installed hook without rewriting requirements.toml.
51
+ const socketPath = process.argv[2] || process.env.EVOLCORE_INSTANCE_SOCKET;
49
52
  let input;
50
53
  try {
51
54
  input = JSON.parse(fs.readFileSync(0, 'utf8'));
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+
8
+ const markerStart = '# BEGIN EVOLCORE MANAGED CODEX HOOK';
9
+ const markerEnd = '# END EVOLCORE MANAGED CODEX HOOK';
10
+
11
+ function argument(name) {
12
+ const index = process.argv.indexOf(name);
13
+ return index >= 0 ? process.argv[index + 1] : undefined;
14
+ }
15
+
16
+ function fail(message) {
17
+ process.stderr.write(`[evolcore] ${message}\n`);
18
+ process.exit(1);
19
+ }
20
+
21
+ function tomlString(value) {
22
+ return JSON.stringify(value);
23
+ }
24
+
25
+ function windowsCommandArg(value) {
26
+ // Keep filesystem separators intact; tomlString() performs the required
27
+ // TOML escaping after this Windows command-line quoting step.
28
+ return `"${value.replaceAll('"', '\\"')}"`;
29
+ }
30
+
31
+ function removeManagedBlock(source) {
32
+ const expression = new RegExp(`\\n?${markerStart}[\\s\\S]*?${markerEnd}\\n?`, 'g');
33
+ return source.replace(expression, '\n').replace(/\n{3,}/g, '\n\n');
34
+ }
35
+
36
+ function tableBounds(lines, table) {
37
+ const header = table ? `[${table}]` : null;
38
+ const start = header
39
+ ? lines.findIndex(line => line.trim() === header)
40
+ : -1;
41
+ if (table && start < 0) return null;
42
+ const from = table ? start + 1 : 0;
43
+ let to = lines.length;
44
+ for (let index = from; index < lines.length; index += 1) {
45
+ if (/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(lines[index])) {
46
+ to = index;
47
+ break;
48
+ }
49
+ }
50
+ return { start, from, to };
51
+ }
52
+
53
+ function setTomlKey(source, table, key, value) {
54
+ const lines = source.split('\n');
55
+ let bounds = tableBounds(lines, table);
56
+ if (!bounds) {
57
+ if (table) {
58
+ if (lines.length && lines.at(-1) !== '') lines.push('');
59
+ lines.push(`[${table}]`, `${key} = ${value}`);
60
+ } else {
61
+ let insertAt = lines.findIndex(line => /^\s*\[/.test(line));
62
+ if (insertAt < 0) insertAt = lines.length;
63
+ lines.splice(insertAt, 0, `${key} = ${value}`);
64
+ }
65
+ return lines.join('\n');
66
+ }
67
+ const keyExpression = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*=`);
68
+ const existing = lines.slice(bounds.from, bounds.to).findIndex(line => keyExpression.test(line));
69
+ if (existing >= 0) {
70
+ lines[bounds.from + existing] = `${key} = ${value}`;
71
+ } else {
72
+ lines.splice(bounds.from, 0, `${key} = ${value}`);
73
+ }
74
+ return lines.join('\n');
75
+ }
76
+
77
+ function atomicReplace(file, temporary) {
78
+ try {
79
+ fs.renameSync(temporary, file);
80
+ return;
81
+ } catch (error) {
82
+ if (error?.code !== 'EEXIST' && error?.code !== 'EPERM') throw error;
83
+ }
84
+ const backup = `${file}.evolcore-backup-${process.pid}-${Date.now()}`;
85
+ fs.renameSync(file, backup);
86
+ try {
87
+ fs.renameSync(temporary, file);
88
+ } catch (error) {
89
+ try { fs.renameSync(backup, file); } catch {}
90
+ throw error;
91
+ }
92
+ try { fs.rmSync(backup, { force: true }); } catch {}
93
+ }
94
+
95
+ function atomicWrite(file, content) {
96
+ const temporary = `${file}.evolcore-${process.pid}-${Date.now()}.tmp`;
97
+ try {
98
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o440, flag: 'wx' });
99
+ atomicReplace(file, temporary);
100
+ } finally {
101
+ try { fs.rmSync(temporary, { force: true }); } catch {}
102
+ }
103
+ }
104
+
105
+ function restrictWindowsAcl(target) {
106
+ if (process.platform !== 'win32') return;
107
+ // Keep the hook and requirements administrator-owned while allowing the
108
+ // daemon's normal user token to read and execute them.
109
+ execFileSync('icacls.exe', [
110
+ target,
111
+ '/inheritance:r',
112
+ '/grant:r',
113
+ '*S-1-5-18:(OI)(CI)F',
114
+ '*S-1-5-32-544:(OI)(CI)F',
115
+ '*S-1-5-32-545:(OI)(CI)RX',
116
+ ], { stdio: 'ignore' });
117
+ }
118
+
119
+ function managedHookLauncher(packageRoot) {
120
+ // Keep the machine-wide entrypoint stable across npm upgrades. The app
121
+ // npm upgrades replace the package contents at this stable global path, so
122
+ // this file never needs to be rewritten just because the hook changed.
123
+ const trustedPackageRoot = path.resolve(packageRoot);
124
+ return `#!/usr/bin/env node
125
+ import path from 'node:path';
126
+ import { pathToFileURL } from 'node:url';
127
+
128
+ // EVOLCORE_MANAGED_HOOK_LAUNCHER
129
+ const packageRoot = ${JSON.stringify(trustedPackageRoot)};
130
+
131
+ try {
132
+ await import(pathToFileURL(path.join(packageRoot, 'bin', 'codex-managed-hook.mjs')).href);
133
+ } catch (error) {
134
+ process.stderr.write('[evolcore] failed to load the current managed hook: '
135
+ + (error instanceof Error ? error.message : String(error)) + '\\n');
136
+ process.exit(2);
137
+ }
138
+ `;
139
+ }
140
+
141
+ function main() {
142
+ if (process.platform !== 'win32' && !argument('--requirements')) {
143
+ fail('Codex managed hook provisioning is only supported on Windows.');
144
+ }
145
+ const packageRoot = argument('--package-root');
146
+ const nodePath = argument('--node-path') || process.execPath;
147
+ const requirements = argument('--requirements')
148
+ || path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml');
149
+ if (!packageRoot) fail('--package-root is required.');
150
+ const existingRequirements = fs.existsSync(requirements) ? fs.readFileSync(requirements, 'utf8') : '';
151
+ const targetDirectory = argument('--managed-dir')
152
+ || path.join(path.dirname(requirements), 'EvolCore', 'hooks');
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}`);
155
+
156
+ fs.mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
157
+ const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
158
+ const temporaryHook = `${hookPath}.evolcore-${process.pid}-${Date.now()}.tmp`;
159
+ try {
160
+ fs.writeFileSync(temporaryHook, managedHookLauncher(packageRoot), { encoding: 'utf8', mode: 0o500, flag: 'wx' });
161
+ fs.chmodSync(temporaryHook, 0o500);
162
+ atomicReplace(hookPath, temporaryHook);
163
+ } finally {
164
+ try { fs.rmSync(temporaryHook, { force: true }); } catch {}
165
+ }
166
+
167
+ let content = existingRequirements;
168
+ content = removeManagedBlock(content);
169
+ content = setTomlKey(content, null, 'allow_managed_hooks_only', 'true');
170
+ content = setTomlKey(content, 'features', 'hooks', 'true');
171
+ content = setTomlKey(content, 'hooks', 'windows_managed_dir', tomlString(path.resolve(targetDirectory)));
172
+ if (!content.endsWith('\n')) content += '\n';
173
+ const command = `${windowsCommandArg(nodePath)} ${windowsCommandArg(hookPath)}`;
174
+ content += [
175
+ markerStart,
176
+ '[[hooks.PreToolUse]]',
177
+ 'matcher = "*"',
178
+ '',
179
+ '[[hooks.PreToolUse.hooks]]',
180
+ 'type = "command"',
181
+ `command = ${tomlString(command)}`,
182
+ `command_windows = ${tomlString(command)}`,
183
+ 'timeout = 3',
184
+ 'statusMessage = "Checking EvolCore session policy"',
185
+ markerEnd,
186
+ '',
187
+ ].join('\n');
188
+ fs.mkdirSync(path.dirname(requirements), { recursive: true });
189
+ atomicWrite(requirements, content);
190
+ try { fs.chmodSync(requirements, 0o440); } catch {}
191
+ restrictWindowsAcl(targetDirectory);
192
+ restrictWindowsAcl(hookPath);
193
+ restrictWindowsAcl(requirements);
194
+ process.stdout.write(`${JSON.stringify({ requirements, managedDir: path.resolve(targetDirectory), hookPath })}\n`);
195
+ }
196
+
197
+ try {
198
+ main();
199
+ } catch (error) {
200
+ fail(error instanceof Error ? error.message : String(error));
201
+ }
@@ -21,9 +21,10 @@ import { resolveEffective } from '../config/config-manager.js';
21
21
  import { sanitizeSessionTitle } from '../core/session/session-title.js';
22
22
  import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
23
23
  import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
24
- import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, resolveProtectedCandidate, } from '../core/protected-paths.js';
24
+ import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
25
25
  import { buildHClassGuardCommand, createSandboxInitializationError, isSandboxInitializationFailure, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, } from '../core/permission/sandbox-runtime.js';
26
26
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
27
+ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
27
28
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
28
29
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
29
30
  // Built-in tools execute inside the Claude runtime and are covered by the
@@ -216,6 +217,32 @@ function buildClaudeManagedLockdownSettings(capabilityOptions) {
216
217
  };
217
218
  }
218
219
  const CLAUDE_SANDBOX_GLOB_MAGIC = /[*?[\]]/;
220
+ function trustedRuntimeWritePaths(runtimeEnv) {
221
+ const tmp = process.env.TMPDIR?.trim();
222
+ if (!tmp || !path.isAbsolute(tmp))
223
+ return [];
224
+ const root = resolveProtectedCandidate(tmp);
225
+ const candidates = [runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR, runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR]
226
+ .filter((value) => typeof value === 'string' && path.isAbsolute(value));
227
+ const paths = [];
228
+ for (const candidate of candidates) {
229
+ const resolved = resolveProtectedCandidate(candidate);
230
+ if (!isSameOrDescendant(resolved, root))
231
+ continue;
232
+ try {
233
+ const stat = fs.lstatSync(resolved);
234
+ if (!stat.isDirectory() || stat.isSymbolicLink())
235
+ continue;
236
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
237
+ continue;
238
+ }
239
+ catch {
240
+ continue;
241
+ }
242
+ paths.push(resolved, path.join(resolved, '**'));
243
+ }
244
+ return [...new Set(paths)];
245
+ }
219
246
  async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSources, managedSettings) {
220
247
  if (process.platform !== 'linux' || settingSources.length === 0)
221
248
  return;
@@ -1879,8 +1906,25 @@ export class AgentRunner {
1879
1906
  const code = policyCode ?? (preflight?.behavior === 'deny' ? preflight.policyCode : undefined);
1880
1907
  if (!code)
1881
1908
  return;
1909
+ const ctx = this.permissionContexts.get(sessionId);
1910
+ const summary = summarizeToolInputForAudit(toolName, toolInput).slice(0, 512);
1911
+ auditToolPreflightDenial({
1912
+ toolName,
1913
+ policyCode: code,
1914
+ reason: preflight?.behavior === 'deny' ? (preflight.message ?? 'policy denied') : 'policy denied',
1915
+ summary,
1916
+ sessionId,
1917
+ agentAid: ctx?.selfAid,
1918
+ permissionMode: callPermissionMode,
1919
+ channel: ctx?.channel,
1920
+ actorId: ctx?.userId,
1921
+ role: ctx?.role,
1922
+ selfAid: ctx?.selfAid,
1923
+ requestId: typeof requestId === 'string' ? requestId : undefined,
1924
+ taskId: ctx?.taskId,
1925
+ });
1882
1926
  try {
1883
- await this.permissionContexts.get(sessionId)?.recordExecutionAnomaly?.({
1927
+ await ctx?.recordExecutionAnomaly?.({
1884
1928
  code: 'operation_blocked',
1885
1929
  severity: 'warning',
1886
1930
  phase: 'execution',
@@ -1889,9 +1933,9 @@ export class AgentRunner {
1889
1933
  ...(typeof requestId === 'string' && requestId ? { requestId } : {}),
1890
1934
  policyCode: code,
1891
1935
  decisionSource: 'policy',
1892
- agentAid: this.permissionContexts.get(sessionId)?.selfAid,
1936
+ agentAid: ctx?.selfAid,
1893
1937
  permissionMode: callPermissionMode,
1894
- summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
1938
+ summary,
1895
1939
  effect: 'operation_skipped',
1896
1940
  });
1897
1941
  }
@@ -2243,6 +2287,7 @@ export class AgentRunner {
2243
2287
  const sandboxUnixSockets = buildClaudeUnixSocketAllowlist(projectPath, {
2244
2288
  role: runPermissionContext?.role,
2245
2289
  });
2290
+ const runtimeWritePaths = trustedRuntimeWritePaths(runtimeEnv);
2246
2291
  await assertClaudeSettingSourcesHaveLiteralSandboxPaths(projectPath, [...settingSources], managedSettings);
2247
2292
  const sandboxProjectionDurationMs = Date.now() - sandboxProjectionStartedAt;
2248
2293
  if (sandboxProjectionDurationMs > 2_000) {
@@ -2259,7 +2304,10 @@ export class AgentRunner {
2259
2304
  allowAllUnixSockets: false,
2260
2305
  allowLocalBinding: false,
2261
2306
  },
2262
- filesystem: sandboxFilesystem,
2307
+ filesystem: {
2308
+ ...sandboxFilesystem,
2309
+ ...(runtimeWritePaths.length > 0 ? { allowWrite: runtimeWritePaths } : {}),
2310
+ },
2263
2311
  };
2264
2312
  })();
2265
2313
  if (executionSandbox.state === 'off') {
@@ -6,8 +6,8 @@ import { resolveCodexLaunchCommand } from '../utils/codex-cli.js';
6
6
  import { createHash } from 'crypto';
7
7
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
8
8
  import fs from 'fs';
9
- import os from 'os';
10
9
  import path from 'path';
10
+ import { ensureProcessManagedTempDir } from '../cli/task-context.js';
11
11
  import { getPackageRoot, resolvePaths } from '../paths.js';
12
12
  import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from '../utils/codex-app-server-registry.js';
13
13
  const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
@@ -298,6 +298,9 @@ export class CodexAppServerClient {
298
298
  includeLayers: false,
299
299
  });
300
300
  }
301
+ async configRequirementsRead() {
302
+ return this.request('configRequirements/read');
303
+ }
301
304
  async pluginInstalled(cwd) {
302
305
  return this.request('plugin/installed', {
303
306
  ...(cwd ? { cwds: [cwd] } : {}),
@@ -379,7 +382,25 @@ export class CodexAppServerClient {
379
382
  this.initializing = null;
380
383
  }
381
384
  }
385
+ async assertManagedHooksAvailable() {
386
+ await this.ensureStarted();
387
+ if (process.platform !== 'win32')
388
+ return;
389
+ try {
390
+ await this.verifyWindowsManagedHooks();
391
+ }
392
+ catch (error) {
393
+ // Do not leave an unverified app-server alive after a fail-closed check.
394
+ await this.close();
395
+ throw error;
396
+ }
397
+ }
382
398
  startProcess() {
399
+ // Linux managed hooks need a private runtime even when the daemon was
400
+ // launched by a service manager that did not export TMPDIR.
401
+ if (process.platform === 'linux' && !process.env.TMPDIR?.trim()) {
402
+ ensureProcessManagedTempDir();
403
+ }
383
404
  const env = this.buildProcessEnvironment();
384
405
  const args = this.buildProcessArgs();
385
406
  const launchCommand = resolveCodexLaunchCommand(args);
@@ -448,7 +469,8 @@ export class CodexAppServerClient {
448
469
  if (this.managedHookRuntimeDir) {
449
470
  return path.join(this.managedHookRuntimeDir, 'requirements.toml');
450
471
  }
451
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'evolcore-codex-hooks-'));
472
+ const tmpDir = ensureProcessManagedTempDir();
473
+ const directory = fs.mkdtempSync(path.join(tmpDir, 'evolcore-codex-hooks-'));
452
474
  try {
453
475
  fs.chmodSync(directory, 0o700);
454
476
  const hookPath = path.join(directory, 'pre-tool-use.mjs');
@@ -505,7 +527,39 @@ export class CodexAppServerClient {
505
527
  ...process.env,
506
528
  ...this.options.env,
507
529
  });
530
+ // The app-server is shared by multiple EvolCore sessions. Its process
531
+ // environment must therefore use the daemon's trusted private root;
532
+ // per-thread session roots are injected later through thread config.
533
+ const managedTmpDir = ensureProcessManagedTempDir();
534
+ env.TMPDIR = managedTmpDir;
535
+ delete env.EVOLCORE_RUNTIME_LOCK_DIR;
536
+ if (managedTmpDir) {
537
+ const runtimeLockDir = path.join(managedTmpDir, 'evolcore-locks');
538
+ try {
539
+ // The parent is the already-validated process root, so only create
540
+ // and validate the final component. Never follow a pre-existing
541
+ // symlink supplied through the inherited environment.
542
+ try {
543
+ fs.mkdirSync(runtimeLockDir, { mode: 0o700 });
544
+ }
545
+ catch (error) {
546
+ if (error?.code !== 'EEXIST')
547
+ throw error;
548
+ }
549
+ const stat = fs.lstatSync(runtimeLockDir);
550
+ if (!stat.isDirectory() || stat.isSymbolicLink()
551
+ || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
552
+ || (stat.mode & 0o077) !== 0) {
553
+ throw new Error('runtime lock directory is not private');
554
+ }
555
+ env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
556
+ }
557
+ catch {
558
+ logger.warn(`[CodexAppServer] managed runtime lock directory unavailable: ${runtimeLockDir}`);
559
+ }
560
+ }
508
561
  env.CODEX_API_KEY = this.options.apiKey;
562
+ env.EVOLCORE_INSTANCE_SOCKET = resolvePaths().instanceSocket;
509
563
  for (const [name, value] of Object.entries(this.options.headers ?? {})) {
510
564
  env[codexHeaderEnvName(name)] = value;
511
565
  }
@@ -514,6 +568,73 @@ export class CodexAppServerClient {
514
568
  }
515
569
  return env;
516
570
  }
571
+ async verifyWindowsManagedHooks() {
572
+ const response = await this.configRequirementsRead();
573
+ const requirements = response.requirements;
574
+ const hooks = requirements?.hooks;
575
+ const featureRequirements = requirements?.featureRequirements ?? requirements?.feature_requirements;
576
+ const managedDir = hooks?.windowsManagedDir ?? hooks?.windows_managed_dir;
577
+ const preToolUse = hooks?.PreToolUse;
578
+ const expectedHookPath = typeof managedDir === 'string'
579
+ ? path.join(managedDir, 'pre-tool-use.mjs')
580
+ : undefined;
581
+ const currentHookLauncher = !!expectedHookPath && (() => {
582
+ try {
583
+ const source = fs.readFileSync(expectedHookPath, 'utf8');
584
+ const expectedPackageRoot = JSON.stringify(path.resolve(getPackageRoot()));
585
+ return source.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
586
+ && (source.includes(expectedPackageRoot)
587
+ || (process.platform === 'win32' && source.toLowerCase().includes(expectedPackageRoot.toLowerCase())));
588
+ }
589
+ catch {
590
+ return false;
591
+ }
592
+ })();
593
+ const normalizeManagedPath = (value) => path.normalize(value).toLowerCase();
594
+ const commandTargetsHook = (value) => {
595
+ const command = normalizeManagedPath(value).trim();
596
+ const hook = normalizeManagedPath(expectedHookPath ?? '');
597
+ // The managed command is intentionally a single executable invocation.
598
+ // Reject shell operators and trailing arguments so a modified
599
+ // requirements file cannot retain the expected path while running a
600
+ // second command or changing the hook's behavior.
601
+ if (!command || !hook || /[\r\n;&|<>]/.test(command))
602
+ return false;
603
+ return command.replace(/["']+$/, '').endsWith(hook);
604
+ };
605
+ const managedHookLoaded = Array.isArray(preToolUse)
606
+ && preToolUse.some(group => {
607
+ if (!group || typeof group !== 'object' || Array.isArray(group))
608
+ return false;
609
+ if (group.matcher !== '*')
610
+ return false;
611
+ const handlers = group.hooks;
612
+ return Array.isArray(handlers) && handlers.some(handler => {
613
+ if (!handler || typeof handler !== 'object' || Array.isArray(handler))
614
+ return false;
615
+ const entry = handler;
616
+ const command = entry.commandWindows ?? entry.command_windows;
617
+ return entry.type === 'command'
618
+ && entry.async !== true
619
+ && typeof entry.command === 'string'
620
+ && entry.command.length > 0
621
+ && typeof command === 'string'
622
+ && command.length > 0
623
+ && !!expectedHookPath
624
+ && fs.existsSync(expectedHookPath)
625
+ && fs.statSync(expectedHookPath).isFile()
626
+ && currentHookLauncher
627
+ && commandTargetsHook(command);
628
+ });
629
+ });
630
+ const lockdownEnabled = (requirements?.allowManagedHooksOnly ?? requirements?.allow_managed_hooks_only) === true;
631
+ const hooksEnabled = featureRequirements?.hooks === true;
632
+ if (typeof managedDir !== 'string' || !path.isAbsolute(managedDir)
633
+ || !fs.existsSync(managedDir) || !managedHookLoaded || !lockdownEnabled || !hooksEnabled) {
634
+ throw new Error('Codex Windows proactive requires an installed and loaded EvolCore managed PreToolUse hook. '
635
+ + 'Run the elevated Windows installer again to provision %ProgramData%\\OpenAI\\Codex\\requirements.toml.');
636
+ }
637
+ }
517
638
  buildProcessArgs() {
518
639
  const args = ['app-server', '--listen', 'stdio://'];
519
640
  if (this.options.enableRequestUserInput !== false) {