evolcore 0.0.22 → 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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  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 +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -1,17 +1,18 @@
1
1
  import { spawn } from 'child_process';
2
2
  import readline from 'readline';
3
- import { logger } from '../utils/logger.js';
4
- import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../core/permission/sandbox-runtime.js';
5
- import { resolveCodexLaunchCommand } from '../utils/codex-cli.js';
3
+ import { logger } from '../../utils/logger.js';
4
+ import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../../core/permission/sandbox-runtime.js';
5
+ import { resolveCodexLaunchCommand } from '../baseagent.js';
6
6
  import { createHash } from 'crypto';
7
- import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
7
+ import { sanitizeShellExecutionEnvironment } from '../../core/permission/shell-environment.js';
8
8
  import fs from 'fs';
9
9
  import path from 'path';
10
- import { ensureProcessManagedTempDir } from '../cli/task-context.js';
11
- import { getPackageRoot, resolvePaths } from '../paths.js';
12
- import { killProcess } from '../utils/cross-platform.js';
13
- import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from '../utils/codex-app-server-registry.js';
10
+ import { ensureProcessManagedTempDir } from '../../cli/task-context.js';
11
+ import { getPackageRoot, resolvePaths } from '../../paths.js';
12
+ import { killProcess } from '../../utils/cross-platform.js';
13
+ import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from './app-server-registry.js';
14
14
  const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
15
+ export const CODEX_THREAD_ACTIVE_WRITER = 'THREAD_ACTIVE_WRITER';
15
16
  function tomlProviderKey(value) {
16
17
  if (!value || /[\0\r\n]/.test(value)) {
17
18
  throw new Error(`Invalid Codex provider name: ${value}`);
@@ -64,6 +65,7 @@ export class CodexAppServerClient {
64
65
  initialized = false;
65
66
  initializing = null;
66
67
  stderrBuffer = [];
68
+ stderrSequence = 0;
67
69
  managedHookRuntimeDir = null;
68
70
  processRegistrationPath = null;
69
71
  constructor(options) {
@@ -115,6 +117,61 @@ export class CodexAppServerClient {
115
117
  ...(options?.selectedCapabilityRoots ? { selectedCapabilityRoots: options.selectedCapabilityRoots } : {}),
116
118
  });
117
119
  }
120
+ async threadArchive(threadId) {
121
+ await this.request('thread/archive', { threadId });
122
+ return true;
123
+ }
124
+ async threadUnarchive(threadId) {
125
+ return this.request('thread/unarchive', { threadId });
126
+ }
127
+ async threadUnsubscribe(threadId) {
128
+ return this.request('thread/unsubscribe', { threadId });
129
+ }
130
+ /**
131
+ * Drop this connection's subscription and wait until the server has released
132
+ * the in-memory thread. This is deliberately scoped to one thread; closing
133
+ * the app-server process would also disrupt unrelated concurrent sessions.
134
+ */
135
+ async threadUnsubscribeAndWait(threadId, timeoutMs = 2_000) {
136
+ let closed = false;
137
+ let resolveClosed;
138
+ const closedPromise = new Promise(resolve => {
139
+ resolveClosed = resolve;
140
+ });
141
+ const removeNotificationHandler = this.onNotification(notification => {
142
+ if (notification.method !== 'thread/closed')
143
+ return;
144
+ const params = (notification.params || {});
145
+ const closedThreadId = params.threadId ?? params.thread_id;
146
+ if (closedThreadId !== threadId || closed)
147
+ return;
148
+ closed = true;
149
+ resolveClosed();
150
+ });
151
+ let timer;
152
+ try {
153
+ const response = await this.threadUnsubscribe(threadId);
154
+ if (response.status !== 'unsubscribed')
155
+ return response;
156
+ const timeoutPromise = new Promise((_, reject) => {
157
+ timer = setTimeout(() => {
158
+ if (closed)
159
+ return;
160
+ const error = new Error(`thread/closed timed out for thread ${threadId}`);
161
+ error.code = 'CODEX_THREAD_CLOSE_TIMEOUT';
162
+ reject(error);
163
+ }, timeoutMs);
164
+ timer.unref?.();
165
+ });
166
+ await Promise.race([closedPromise, timeoutPromise]);
167
+ return response;
168
+ }
169
+ finally {
170
+ if (timer)
171
+ clearTimeout(timer);
172
+ removeNotificationHandler();
173
+ }
174
+ }
118
175
  async turnStart(threadId, input, options) {
119
176
  const sandboxPolicy = this.toTurnSandboxPolicy(options?.sandbox);
120
177
  return this.request('turn/start', {
@@ -295,8 +352,8 @@ export class CodexAppServerClient {
295
352
  await this.request('turn/interrupt', { threadId, turnId });
296
353
  return true;
297
354
  }
298
- async modelList(includeHidden = false) {
299
- return this.request('model/list', { includeHidden });
355
+ async modelList(includeHidden = false, cursor) {
356
+ return this.request('model/list', { includeHidden, ...(cursor ? { cursor } : {}) });
300
357
  }
301
358
  async modelProviderCapabilitiesRead() {
302
359
  return this.request('modelProvider/capabilities/read', {});
@@ -431,6 +488,7 @@ export class CodexAppServerClient {
431
488
  const managedRequirements = this.ensureManagedHookRuntime();
432
489
  const guardedCommand = buildHClassGuardCommand(command, commandArgs, guardRoot, {
433
490
  managedCodexRequirements: managedRequirements,
491
+ protectEvolcoreFilesystem: this.options.protectEvolcoreFilesystem !== false,
434
492
  });
435
493
  if (!guardedCommand) {
436
494
  throw new Error('Codex 缺少 EvolCore managed-hook 隔离运行时,已拒绝启动 app-server');
@@ -482,7 +540,7 @@ export class CodexAppServerClient {
482
540
  const text = String(chunk).trim();
483
541
  if (!text)
484
542
  return;
485
- this.stderrBuffer.push(text);
543
+ this.stderrBuffer.push({ sequence: ++this.stderrSequence, text });
486
544
  if (this.stderrBuffer.length > 20)
487
545
  this.stderrBuffer.shift();
488
546
  logger.debug(`[CodexAppServer] ${text}`);
@@ -555,9 +613,8 @@ export class CodexAppServerClient {
555
613
  ...process.env,
556
614
  ...this.options.env,
557
615
  });
558
- // The app-server is shared by multiple EvolCore sessions. Its process
559
- // environment must therefore use the daemon's trusted private root;
560
- // per-thread session roots are injected later through thread config.
616
+ // Use the normal user CODEX_HOME so EvolCore and Codex Desktop share
617
+ // threads, SQLite state, writer locks, and sandbox state.
561
618
  const managedTmpDir = ensureProcessManagedTempDir();
562
619
  env.TMPDIR = managedTmpDir;
563
620
  delete env.EVOLCORE_RUNTIME_LOCK_DIR;
@@ -576,8 +633,8 @@ export class CodexAppServerClient {
576
633
  }
577
634
  const stat = fs.lstatSync(runtimeLockDir);
578
635
  if (!stat.isDirectory() || stat.isSymbolicLink()
579
- || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
580
- || (stat.mode & 0o077) !== 0) {
636
+ || (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
637
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
581
638
  throw new Error('runtime lock directory is not private');
582
639
  }
583
640
  env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
@@ -663,7 +720,8 @@ export class CodexAppServerClient {
663
720
  if (typeof managedDir !== 'string' || !path.isAbsolute(managedDir)
664
721
  || !fs.existsSync(managedDir) || !managedHookLoaded || !lockdownEnabled || !hooksEnabled) {
665
722
  throw new Error('Codex Windows proactive requires an installed and loaded EvolCore managed PreToolUse hook. '
666
- + 'Run the elevated Windows installer again to provision %ProgramData%\\OpenAI\\Codex\\requirements.toml.');
723
+ + 'ec start/restart automatically checks and installs missing hooks with Windows administrator approval. '
724
+ + 'Restart EvolCore to install missing hooks and reload the Codex policy.');
667
725
  }
668
726
  }
669
727
  buildProcessArgs() {
@@ -677,7 +735,7 @@ export class CodexAppServerClient {
677
735
  const headerEnvNames = Object.fromEntries(Object.keys(this.options.headers ?? {}).map(name => [name, codexHeaderEnvName(name)]));
678
736
  const hasHeaders = Object.keys(headerEnvNames).length > 0;
679
737
  const hasQueryParams = Object.keys(this.options.queryParams ?? {}).length > 0;
680
- if (hasHeaders || hasQueryParams) {
738
+ if (hasHeaders || hasQueryParams || this.options.baseUrl || this.options.provider) {
681
739
  const configuredProvider = this.options.provider?.trim();
682
740
  const useRuntimeProvider = !configuredProvider || BUILT_IN_CODEX_PROVIDER_IDS.has(configuredProvider.toLowerCase());
683
741
  const providerName = !useRuntimeProvider
@@ -712,7 +770,13 @@ export class CodexAppServerClient {
712
770
  if (params !== undefined && params !== null)
713
771
  payload.params = params;
714
772
  return new Promise((resolve, reject) => {
715
- this.pending.set(id, { resolve, reject, method, params });
773
+ this.pending.set(id, {
774
+ resolve,
775
+ reject,
776
+ method,
777
+ params,
778
+ stderrStartSequence: this.stderrSequence,
779
+ });
716
780
  proc.stdin.write(`${JSON.stringify(payload)}\n`, error => {
717
781
  if (!error)
718
782
  return;
@@ -786,12 +850,18 @@ export class CodexAppServerClient {
786
850
  // The CLI's one-time bubblewrap configuration warning is process-wide
787
851
  // startup noise, not evidence about this RPC. Do not append it to an
788
852
  // unrelated request failure such as thread/resume.
789
- const stderrLines = this.stderrBuffer.filter(line => !/could not find bubblewrap on PATH/i.test(line));
853
+ const stderrLines = this.stderrBuffer
854
+ .filter(entry => entry.sequence > pending.stderrStartSequence)
855
+ .map(entry => entry.text)
856
+ .filter(line => !/could not find bubblewrap on PATH/i.test(line));
790
857
  const stderr = stderrLines.length ? `\n${stderrLines.join('\n')}` : '';
791
858
  const error = new Error(`${pending.method} failed: ${detail}${stderr}`);
792
859
  if (/no rollout found for thread id/i.test(detail)) {
793
860
  error.code = 'CODEX_ROLLOUT_NOT_FOUND';
794
861
  }
862
+ else if (/already has an active writer/i.test(detail)) {
863
+ error.code = CODEX_THREAD_ACTIVE_WRITER;
864
+ }
795
865
  pending.reject(error);
796
866
  return;
797
867
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { resolvePaths } from '../paths.js';
4
- import { getProcessStartTime } from './process-introspect.js';
3
+ import { resolvePaths } from '../../paths.js';
4
+ import { getProcessStartTime } from '../../utils/process-introspect.js';
5
5
  const CODEX_APP_SERVER_RECORD_PREFIX = 'codex-app-server-';
6
6
  const CODEX_APP_SERVER_RECORD_SUFFIX = '.json';
7
7
  function recordFilePath(pid) {
@@ -0,0 +1,70 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { CodexAppServerClient } from './app-server-client.js';
3
+ import { resolveCodexLaunchCommand } from '../baseagent.js';
4
+ import { parseModelList } from '../../core/model/catalog-service.js';
5
+ async function cliModels() {
6
+ const launch = resolveCodexLaunchCommand(['debug', 'models']);
7
+ if (!launch)
8
+ return null;
9
+ return new Promise(resolve => {
10
+ execFile(launch.command, launch.args, { timeout: 3000, windowsHide: true, maxBuffer: 16 * 1024 * 1024, encoding: 'utf8' }, (error, stdout) => {
11
+ try {
12
+ if (error)
13
+ return resolve(null);
14
+ const json = JSON.parse(stdout);
15
+ if (!Array.isArray(json?.models))
16
+ return resolve(null);
17
+ resolve(parseModelList(json.models.filter((m) => m.visibility === 'list'))
18
+ .map(m => ({ ...m, verification: 'unverified' })));
19
+ }
20
+ catch {
21
+ resolve(null);
22
+ }
23
+ });
24
+ });
25
+ }
26
+ /** A catalog-only client uses the same transport configuration as inference. */
27
+ export async function codexNativeCatalog(context) {
28
+ if (context.baseagent !== 'codex')
29
+ return null;
30
+ const client = new CodexAppServerClient({
31
+ apiKey: context.apiKey ?? '', baseUrl: context.baseUrl, provider: context.provider,
32
+ headers: context.headers, queryParams: context.queryParams,
33
+ });
34
+ let timer;
35
+ let expired = false;
36
+ try {
37
+ return await Promise.race([
38
+ (async () => {
39
+ const models = [];
40
+ const seen = new Set();
41
+ let cursor;
42
+ do {
43
+ const page = await client.modelList(false, cursor);
44
+ if (expired)
45
+ throw new Error('Model catalog timeout');
46
+ if (!Array.isArray(page.data))
47
+ throw new Error('Invalid native model list');
48
+ models.push(...parseModelList(page.data.filter(m => !m.hidden)));
49
+ cursor = page.nextCursor || undefined;
50
+ if (cursor && (seen.has(cursor) || seen.size >= 100))
51
+ throw new Error('Invalid native model pagination');
52
+ if (cursor)
53
+ seen.add(cursor);
54
+ } while (cursor);
55
+ return [...new Map(models.map(m => [m.id, m])).values()];
56
+ })(),
57
+ new Promise((_, reject) => { timer = setTimeout(() => { expired = true; reject(new Error('Model catalog timeout')); }, 5000); }),
58
+ ]);
59
+ }
60
+ catch {
61
+ // The host CLI catalog has no guarantee of matching this Agent connection.
62
+ // It is useful metadata/suggestions and always remains unverified.
63
+ return await cliModels();
64
+ }
65
+ finally {
66
+ if (timer)
67
+ clearTimeout(timer);
68
+ await client.close();
69
+ }
70
+ }
@@ -0,0 +1,369 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { parse as parseToml } from 'smol-toml';
6
+ export function checkCodexProjectPath(value, options = {}) {
7
+ const configured = typeof value === 'string' ? value.trim() : '';
8
+ if (!configured) {
9
+ return { status: 'invalid', path: configured, warnings: [], reason: 'projects.defaultPath is not configured' };
10
+ }
11
+ const projectPath = path.resolve(configured);
12
+ const warnings = [];
13
+ if ((options.platform ?? process.platform) === 'win32') {
14
+ if (projectPath.length >= 240) {
15
+ warnings.push(`Windows path is long (${projectPath.length} characters); Codex or a child tool may reject it.`);
16
+ }
17
+ if (projectPath.startsWith('\\\\')) {
18
+ warnings.push('Windows UNC/network project path may be unavailable to Codex sandboxed processes.');
19
+ }
20
+ const systemRoot = process.env.SystemRoot ? path.resolve(process.env.SystemRoot).toLowerCase() : '';
21
+ if (systemRoot && projectPath.toLowerCase().startsWith(`${systemRoot}${path.sep}`)) {
22
+ warnings.push('Project path is under the Windows system directory; use a dedicated workspace when possible.');
23
+ }
24
+ }
25
+ const root = path.parse(projectPath).root;
26
+ if (projectPath === root)
27
+ warnings.push('Project path is a filesystem root; use a dedicated project directory when possible.');
28
+ try {
29
+ const stat = fs.statSync(projectPath);
30
+ if (!stat.isDirectory())
31
+ return { status: 'not-directory', path: projectPath, warnings, reason: 'path exists but is not a directory' };
32
+ try {
33
+ fs.accessSync(projectPath, fs.constants.R_OK | fs.constants.W_OK);
34
+ }
35
+ catch (error) {
36
+ return { status: 'inaccessible', path: projectPath, warnings, reason: error instanceof Error ? error.message : String(error) };
37
+ }
38
+ return { status: 'ok', path: projectPath, warnings };
39
+ }
40
+ catch (error) {
41
+ if (error?.code === 'ENOENT') {
42
+ return { status: 'missing', path: projectPath, warnings, reason: 'directory does not exist' };
43
+ }
44
+ return { status: 'inaccessible', path: projectPath, warnings, reason: error instanceof Error ? error.message : String(error) };
45
+ }
46
+ }
47
+ export function codexManagedHookPaths(options = {}) {
48
+ if ((options.platform ?? process.platform) !== 'win32')
49
+ return null;
50
+ const programData = options.programData || process.env.ProgramData || path.join(options.homeDir || os.homedir(), 'ProgramData');
51
+ const requirements = path.resolve(programData, 'OpenAI', 'Codex', 'requirements.toml');
52
+ const managedDir = path.join(path.dirname(requirements), 'EvolCore', 'hooks');
53
+ return { requirements, managedDir, hookPath: path.join(managedDir, 'pre-tool-use.mjs') };
54
+ }
55
+ function decodeProbeOutput(buffer) {
56
+ if (!buffer?.length)
57
+ return '';
58
+ try {
59
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
60
+ }
61
+ catch {
62
+ return new TextDecoder('gb18030').decode(buffer);
63
+ }
64
+ }
65
+ function windowsCommandArg(value) {
66
+ return '"' + value.replaceAll('"', '\\"') + '"';
67
+ }
68
+ function expectedWindowsHookCommand(nodePath, hookPath) {
69
+ return windowsCommandArg(nodePath) + ' ' + windowsCommandArg(hookPath) + ' %EVOLCORE_INSTANCE_SOCKET%';
70
+ }
71
+ /**
72
+ * Run synthetic Windows shell probes for the managed Codex hook.
73
+ *
74
+ * This is intentionally called by ec diagnose, never by the hot hook path.
75
+ * The probes pass {} and therefore stop before contacting a live daemon.
76
+ */
77
+ export function runCodexHookLaunchDiagnostic(options = {}) {
78
+ if ((options.platform ?? process.platform) !== 'win32') {
79
+ return {
80
+ status: 'skipped',
81
+ reason: 'Windows managed-hook launch probes are not applicable on this platform',
82
+ results: [],
83
+ };
84
+ }
85
+ const paths = codexManagedHookPaths(options);
86
+ if (!paths) {
87
+ return {
88
+ status: 'skipped',
89
+ reason: 'Windows managed-hook paths are unavailable on this platform',
90
+ results: [],
91
+ };
92
+ }
93
+ const requirements = path.resolve(options.requirements || paths.requirements);
94
+ const managedDir = path.resolve(options.managedDir || paths.managedDir);
95
+ const hookPath = path.join(managedDir, 'pre-tool-use.mjs');
96
+ const output = path.resolve(options.output
97
+ || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'log', 'evolcore-hook-diagnostic.json'));
98
+ const runtimeLog = output + '.runtime.jsonl';
99
+ const nodePath = path.resolve(options.nodePath || process.execPath);
100
+ const config = parseToml(fs.readFileSync(requirements, 'utf8'));
101
+ const configuredDir = config.hooks?.windows_managed_dir ?? config.hooks?.managed_dir;
102
+ if (typeof configuredDir !== 'string')
103
+ throw new Error('No Windows managed hook directory configured.');
104
+ const expected = expectedWindowsHookCommand(nodePath, hookPath);
105
+ const handlers = Array.isArray(config.hooks?.PreToolUse)
106
+ ? config.hooks.PreToolUse.flatMap((group) => group?.hooks || [])
107
+ : [];
108
+ const handler = handlers.find((item) => (item?.type === 'command'
109
+ && (item.command_windows ?? item.commandWindows) === expected));
110
+ if (!handler || /[\r\n%;&|<>]/.test(nodePath + hookPath)) {
111
+ throw new Error('Managed command differs from the expected EvolCore command for this Node path; no hooks executed.');
112
+ }
113
+ fs.mkdirSync(path.dirname(output), { recursive: true, mode: 0o700 });
114
+ const env = { ...process.env, EVOLCORE_CODEX_HOOK_LOG: runtimeLog };
115
+ delete env.EVOLCORE_INSTANCE_SOCKET;
116
+ const comSpec = options.comSpec || process.env.ComSpec || 'C:/Windows/System32/cmd.exe';
117
+ const probes = [
118
+ { label: 'direct_node', executable: nodePath, args: [hookPath] },
119
+ { label: 'cmd_raw_command', executable: comSpec, args: ['/d', '/s', '/c', expected], windowsVerbatimArguments: true },
120
+ { label: 'cmd_outer_quotes', executable: comSpec, args: ['/d', '/s', '/c', '"' + expected + '"'], windowsVerbatimArguments: true },
121
+ ];
122
+ const results = probes.map(probe => {
123
+ const result = spawnSync(probe.executable, probe.args, {
124
+ input: '{}',
125
+ env,
126
+ windowsHide: true,
127
+ timeout: 5000,
128
+ maxBuffer: 64 * 1024,
129
+ windowsVerbatimArguments: probe.windowsVerbatimArguments,
130
+ });
131
+ return {
132
+ label: probe.label,
133
+ pid: result.pid,
134
+ exitCode: result.status,
135
+ signal: result.signal,
136
+ error: result.error?.message,
137
+ stdout: decodeProbeOutput(result.stdout),
138
+ stderr: decodeProbeOutput(result.stderr),
139
+ stderrBase64: result.stderr?.toString('base64'),
140
+ };
141
+ });
142
+ fs.writeFileSync(output, JSON.stringify({
143
+ time: new Date().toISOString(),
144
+ requirements,
145
+ hookPath,
146
+ command: expected,
147
+ nodeVersion: process.version,
148
+ nodePath,
149
+ socketEnvironmentPresent: Boolean(process.env.EVOLCORE_INSTANCE_SOCKET),
150
+ runtimeLog,
151
+ note: 'Synthetic launch probes; shell variants do not establish how Codex invoked the hook.',
152
+ results,
153
+ }, null, 2) + '\n', { mode: 0o600 });
154
+ return { status: 'ok', output, runtimeLog, results };
155
+ }
156
+ function regularFileState(file) {
157
+ try {
158
+ const stat = fs.lstatSync(file);
159
+ if (!stat.isFile() || stat.isSymbolicLink())
160
+ return 'unsafe';
161
+ return 'ok';
162
+ }
163
+ catch (error) {
164
+ if (error?.code === 'ENOENT')
165
+ return 'missing';
166
+ return 'unavailable';
167
+ }
168
+ }
169
+ function realDirectoryState(directory) {
170
+ try {
171
+ const stat = fs.lstatSync(directory);
172
+ if (!stat.isDirectory() || stat.isSymbolicLink())
173
+ return 'unsafe';
174
+ return 'ok';
175
+ }
176
+ catch (error) {
177
+ if (error?.code === 'ENOENT')
178
+ return 'missing';
179
+ return 'unavailable';
180
+ }
181
+ }
182
+ /** Read-only inspection of the machine-wide Windows Codex managed hook. */
183
+ export function inspectCodexManagedHook(options = {}) {
184
+ const paths = codexManagedHookPaths(options);
185
+ if (!paths)
186
+ return { status: 'skipped', reason: 'Windows managed hooks are not applicable on this platform' };
187
+ const requirements = path.resolve(options.requirements || paths.requirements);
188
+ const managedDir = path.resolve(options.managedDir || paths.managedDir);
189
+ const hookPath = path.join(managedDir, 'pre-tool-use.mjs');
190
+ const configDirState = realDirectoryState(path.dirname(requirements));
191
+ const managedDirState = realDirectoryState(managedDir);
192
+ if ([configDirState, managedDirState].includes('unsafe')) {
193
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'requirements or managed-hook directory is not a real directory' };
194
+ }
195
+ if ([configDirState, managedDirState].includes('unavailable')) {
196
+ return { status: 'unavailable', requirements, managedDir, hookPath, reason: 'unable to inspect requirements or managed-hook directory' };
197
+ }
198
+ const requirementState = regularFileState(requirements);
199
+ const hookState = regularFileState(hookPath);
200
+ if ([requirementState, hookState].includes('unsafe')) {
201
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'requirements.toml or launcher is not a regular file' };
202
+ }
203
+ if ([requirementState, hookState].includes('unavailable')) {
204
+ return { status: 'unavailable', requirements, managedDir, hookPath, reason: 'unable to read requirements.toml or launcher' };
205
+ }
206
+ if (requirementState === 'missing' && hookState === 'ok') {
207
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'canonical launcher exists but requirements.toml is missing' };
208
+ }
209
+ if (requirementState === 'ok' && hookState === 'missing') {
210
+ // Match the installer's repair contract: a partial managed installation
211
+ // is drift; only an untouched user requirements file can be provisioned.
212
+ try {
213
+ const source = fs.readFileSync(requirements, 'utf8');
214
+ const partial = parseToml(source);
215
+ const hooks = partial.hooks;
216
+ if (source.includes('# BEGIN EVOLCORE MANAGED CODEX HOOK')
217
+ || source.includes('# END EVOLCORE MANAGED CODEX HOOK')
218
+ || partial.allow_managed_hooks_only !== undefined
219
+ || hooks?.windows_managed_dir !== undefined
220
+ || hooks?.managed_dir !== undefined) {
221
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: 'requirements contains managed-hook policy but the canonical launcher is missing' };
222
+ }
223
+ }
224
+ catch (error) {
225
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: `requirements.toml cannot be safely repaired: ${error instanceof Error ? error.message : String(error)}` };
226
+ }
227
+ }
228
+ if ([requirementState, hookState].includes('missing')) {
229
+ return { status: 'missing', requirements, managedDir, hookPath, reason: 'requirements.toml or launcher is missing' };
230
+ }
231
+ let config;
232
+ let launcher;
233
+ let source;
234
+ try {
235
+ source = fs.readFileSync(requirements, 'utf8');
236
+ }
237
+ catch (error) {
238
+ return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
239
+ }
240
+ try {
241
+ config = parseToml(source);
242
+ }
243
+ catch (error) {
244
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: `requirements.toml is invalid: ${error instanceof Error ? error.message : String(error)}` };
245
+ }
246
+ try {
247
+ launcher = fs.readFileSync(hookPath, 'utf8');
248
+ }
249
+ catch (error) {
250
+ return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
251
+ }
252
+ const normalized = (value) => path.normalize(String(value || '')).toLowerCase();
253
+ const expectedPackageRoot = path.resolve(options.packageRoot || process.cwd());
254
+ const launcherValid = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
255
+ && launcher.toLowerCase().includes(JSON.stringify(expectedPackageRoot).toLowerCase());
256
+ const configuredDir = config.hooks?.windows_managed_dir ?? config.hooks?.managed_dir;
257
+ const groups = config.hooks?.PreToolUse;
258
+ const canonicalCommand = (value) => {
259
+ if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value))
260
+ return false;
261
+ const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
262
+ return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
263
+ };
264
+ const hookLoaded = Array.isArray(groups) && groups.some((group) => group?.matcher === '*'
265
+ && Array.isArray(group.hooks) && group.hooks.some((handler) => {
266
+ const command = typeof handler?.command === 'string' ? handler.command : '';
267
+ const windowsCommand = typeof handler?.command_windows === 'string' ? handler.command_windows : '';
268
+ return handler?.type === 'command' && handler.async !== true
269
+ && canonicalCommand(command) && canonicalCommand(windowsCommand);
270
+ }));
271
+ const valid = config.allow_managed_hooks_only === true
272
+ && config.features?.hooks === true
273
+ && normalized(configuredDir) === normalized(managedDir)
274
+ && launcherValid && hookLoaded;
275
+ return valid
276
+ ? { status: 'installed', requirements, managedDir, hookPath }
277
+ : { status: 'unsafe', requirements, managedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
278
+ }
279
+ function powershellLiteral(value) {
280
+ return `'${value.replaceAll("'", "''")}'`;
281
+ }
282
+ /** Start-Process joins ArgumentList into a native command line, even for arrays. */
283
+ function windowsArgument(value) {
284
+ return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
285
+ }
286
+ export function buildCodexManagedHookElevationCommand(options) {
287
+ const argumentLine = [options.installer, '--package-root', options.packageRoot,
288
+ '--requirements', options.requirements, '--managed-dir', options.managedDir, '--repair']
289
+ .map(windowsArgument).join(' ');
290
+ return [
291
+ "$ErrorActionPreference = 'Stop'",
292
+ "$ProgressPreference = 'SilentlyContinue'",
293
+ 'try {',
294
+ ` $p = Start-Process -FilePath ${powershellLiteral(options.nodePath)} -ArgumentList ${powershellLiteral(argumentLine)} -WorkingDirectory ${powershellLiteral(options.packageRoot)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru -ErrorAction Stop`,
295
+ ' exit $p.ExitCode',
296
+ '} catch {',
297
+ ' [Console]::Error.WriteLine($_.Exception.Message)',
298
+ ' exit 1',
299
+ '}',
300
+ ].join('\n');
301
+ }
302
+ /**
303
+ * Ensure the machine-wide Windows hook before starting the daemon. Missing
304
+ * files are repaired through one UAC elevation; altered policy remains
305
+ * fail-closed and is never overwritten automatically.
306
+ */
307
+ export function ensureCodexManagedHook(options) {
308
+ const platformName = options.platform ?? process.platform;
309
+ const paths = codexManagedHookPaths({
310
+ platform: platformName,
311
+ programData: options.programData,
312
+ homeDir: options.homeDir,
313
+ });
314
+ if (!paths)
315
+ return { status: 'skipped', reason: 'Windows managed hooks are not applicable on this platform' };
316
+ const inspectOptions = {
317
+ platform: platformName,
318
+ packageRoot: options.packageRoot,
319
+ requirements: options.requirements || paths.requirements,
320
+ managedDir: options.managedDir || paths.managedDir,
321
+ programData: options.programData,
322
+ homeDir: options.homeDir,
323
+ };
324
+ let state = inspectCodexManagedHook(inspectOptions);
325
+ if (state.status === 'installed')
326
+ return { ...state, repaired: false };
327
+ if (state.status !== 'missing')
328
+ return { ...state, repaired: false };
329
+ const installer = path.join(path.resolve(options.packageRoot), 'bin', 'install-codex-managed-hooks.mjs');
330
+ if (!fs.existsSync(installer)) {
331
+ return { ...state, repaired: false, error: `managed-hook installer is missing: ${installer}` };
332
+ }
333
+ const runElevated = options.runElevated || ((script, packageRoot) => {
334
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
335
+ const command = buildCodexManagedHookElevationCommand({
336
+ nodePath: process.execPath, installer: script, packageRoot,
337
+ requirements: path.resolve(inspectOptions.requirements),
338
+ managedDir: path.resolve(inspectOptions.managedDir),
339
+ });
340
+ const result = spawnSync(powershell, [
341
+ // Do not use -NonInteractive here: Start-Process -Verb RunAs must be
342
+ // able to show the UAC consent dialog on the secure desktop.
343
+ '-NoProfile', '-ExecutionPolicy', 'Bypass',
344
+ '-EncodedCommand', Buffer.from(command, 'utf16le').toString('base64'),
345
+ ], { encoding: 'utf8', windowsHide: true });
346
+ if (result.error)
347
+ return { ok: false, detail: result.error.message };
348
+ if (result.status !== 0) {
349
+ const detail = (result.stderr || '').replace(/#<\s*CLIXML[\s\S]*?<\/Objs>\s*/gi, '').trim();
350
+ return { ok: false, detail: detail || `installer exited with code ${result.status}` };
351
+ }
352
+ return { ok: true };
353
+ });
354
+ options.onInstallRequired?.();
355
+ let elevated;
356
+ try {
357
+ elevated = runElevated(installer, path.resolve(options.packageRoot));
358
+ }
359
+ catch (error) {
360
+ elevated = { ok: false, detail: error instanceof Error ? error.message : String(error) };
361
+ }
362
+ if (!elevated.ok) {
363
+ return { ...state, repaired: false, error: elevated.detail || 'elevated managed-hook installation failed' };
364
+ }
365
+ state = inspectCodexManagedHook(inspectOptions);
366
+ return state.status === 'installed'
367
+ ? { ...state, repaired: true }
368
+ : { ...state, repaired: false, error: state.reason || 'managed-hook installation did not pass verification' };
369
+ }