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
@@ -1,6 +1,7 @@
1
1
  import { getRunnerCatalog, peekRunnerCatalog, catalogEfforts, resolveCatalogModel, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
2
  import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages, resolveSettings as sdkResolveSettings, } from '@anthropic-ai/claude-agent-sdk';
3
3
  import { spawn } from 'child_process';
4
+ import { createRequire } from 'module';
4
5
  import { ensureDir } from '../utils/atomic-write.js';
5
6
  import { resolveAnthropicConfig } from './baseagent.js';
6
7
  import { buildModelRequestHeaders } from './request-identity.js';
@@ -30,6 +31,42 @@ import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../cor
30
31
  import { getManagedTaskTempDir, isManagedSessionRuntimeDir } from '../cli/task-context.js';
31
32
  import { hasTrustedFullAccessContext } from './runner-types.js';
32
33
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
34
+ const resolveModule = createRequire(import.meta.url);
35
+ /**
36
+ * Resolve the platform package explicitly instead of relying on the SDK's
37
+ * internal optional-dependency lookup. The latter is sensitive to the module
38
+ * installation/loader context and can report a missing binary even when the
39
+ * package is present in the same node_modules tree.
40
+ */
41
+ export function resolveBundledClaudeExecutable() {
42
+ if (process.platform !== 'win32')
43
+ return undefined;
44
+ const nativePackage = process.arch === 'arm64'
45
+ ? '@anthropic-ai/claude-agent-sdk-win32-arm64'
46
+ : process.arch === 'x64'
47
+ ? '@anthropic-ai/claude-agent-sdk-win32-x64'
48
+ : undefined;
49
+ if (!nativePackage)
50
+ return undefined;
51
+ const binarySpecifier = `${nativePackage}/claude.exe`;
52
+ try {
53
+ const executable = resolveModule.resolve(binarySpecifier);
54
+ if (fs.existsSync(executable))
55
+ return executable;
56
+ }
57
+ catch {
58
+ // Fall through to a direct sibling lookup for package layouts where the
59
+ // optional package does not expose its binary through exports.
60
+ }
61
+ try {
62
+ const sdkEntry = resolveModule.resolve('@anthropic-ai/claude-agent-sdk');
63
+ const executable = path.resolve(path.dirname(sdkEntry), '..', '@anthropic-ai', nativePackage.slice('@anthropic-ai/'.length), 'claude.exe');
64
+ return fs.existsSync(executable) ? executable : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
33
70
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
34
71
  // Built-in tools execute inside the Claude runtime and are covered by the
35
72
  // runner's filesystem/shell policy. MCP and future unknown tools can perform
@@ -109,6 +146,40 @@ function appendQueryParams(baseUrl, queryParams) {
109
146
  return baseUrl;
110
147
  }
111
148
  }
149
+ function claudeSessionFilePath(projectPath, agentSessionId) {
150
+ return path.join(os.homedir(), '.claude', 'projects', encodePath(projectPath), `${agentSessionId}.jsonl`);
151
+ }
152
+ /** A discovered Claude session is resumable only after its transcript header is durable. */
153
+ function isClaudeSessionFileReady(projectPath, agentSessionId) {
154
+ const sessionFile = claudeSessionFilePath(projectPath, agentSessionId);
155
+ if (!fs.existsSync(sessionFile))
156
+ return false;
157
+ try {
158
+ const content = fs.readFileSync(sessionFile, 'utf8');
159
+ for (const line of content.split('\n')) {
160
+ if (!line.trim())
161
+ continue;
162
+ try {
163
+ const data = JSON.parse(line);
164
+ if (data.sessionId && data.version)
165
+ return true;
166
+ }
167
+ catch { }
168
+ }
169
+ }
170
+ catch { }
171
+ return false;
172
+ }
173
+ async function waitForClaudeSessionFile(projectPath, agentSessionId, timeoutMs = 250) {
174
+ const deadline = Date.now() + timeoutMs;
175
+ do {
176
+ if (isClaudeSessionFileReady(projectPath, agentSessionId))
177
+ return true;
178
+ if (Date.now() >= deadline)
179
+ return false;
180
+ await new Promise(resolve => setTimeout(resolve, 10));
181
+ } while (true);
182
+ }
112
183
  function stableClaudePermissionValue(value, depth = 0) {
113
184
  if (depth > 8)
114
185
  return '[truncated]';
@@ -504,9 +575,10 @@ export class AgentRunner {
504
575
  this.agentAid = runtime?.agentAid ?? legacy?.['evolcoreAgentAid'];
505
576
  this.agentConfig = runtime?.agentConfig ?? legacy?.['evolcoreAgentConfig'];
506
577
  this.onSessionIdUpdate = onSessionIdUpdate;
578
+ this.claudeExecutablePath = resolveBundledClaudeExecutable();
507
579
  if (config) {
508
580
  const anthropic = resolveAnthropicConfig(config);
509
- this.claudeExecutablePath = anthropic.pathToClaudeCodeExecutable;
581
+ this.claudeExecutablePath = anthropic.pathToClaudeCodeExecutable || this.claudeExecutablePath;
510
582
  this.customHeaders = buildModelRequestHeaders({
511
583
  baseagent: 'claude',
512
584
  baseUrl: anthropic.baseUrl,
@@ -1257,8 +1329,9 @@ export class AgentRunner {
1257
1329
  * SDK 原始事件 → 标准 AgentEvent 转换
1258
1330
  * 所有 SDK 特有的事件类型引用封装在此方法内
1259
1331
  */
1260
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn) {
1332
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn, projectPath = process.cwd(), topicBinding = false) {
1261
1333
  let lastSessionId;
1334
+ let pendingSessionId;
1262
1335
  let hasTurnActivity = false;
1263
1336
  let ignoredPreTurnResult = false;
1264
1337
  let waitingForBackgroundCompletion = false;
@@ -1278,19 +1351,41 @@ export class AgentRunner {
1278
1351
  let lastAssistantUuid;
1279
1352
  // 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
1280
1353
  const collectedCalls = [];
1354
+ const activatePendingSession = async (waitForFile) => {
1355
+ const candidate = pendingSessionId;
1356
+ if (!candidate)
1357
+ return undefined;
1358
+ if (!isResuming && topicBinding) {
1359
+ const ready = waitForFile
1360
+ ? await waitForClaudeSessionFile(projectPath, candidate)
1361
+ : isClaudeSessionFileReady(projectPath, candidate);
1362
+ if (!ready)
1363
+ return undefined;
1364
+ }
1365
+ if (!await this.acceptDiscoveredSessionId(sessionId, candidate, turn)) {
1366
+ logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${candidate} turn=${turn?.turnId ?? 'legacy'}`);
1367
+ const error = new Error('backend discovery rejected by active turn lease');
1368
+ error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1369
+ throw error;
1370
+ }
1371
+ pendingSessionId = undefined;
1372
+ return candidate;
1373
+ };
1281
1374
  try {
1282
1375
  for await (const event of sdkStream) {
1376
+ const activatedBeforeEvent = await activatePendingSession(false);
1377
+ if (activatedBeforeEvent)
1378
+ yield { type: 'session_id', sessionId: activatedBeforeEvent };
1283
1379
  // 提取 session_id(任意 SDK 事件都可能携带)
1284
1380
  if (event.session_id && event.session_id !== lastSessionId) {
1285
1381
  lastSessionId = event.session_id;
1286
- if (await this.acceptDiscoveredSessionId(sessionId, event.session_id, turn)) {
1287
- yield { type: 'session_id', sessionId: event.session_id };
1382
+ pendingSessionId = event.session_id;
1383
+ const activated = await activatePendingSession(event.type === 'result');
1384
+ if (activated) {
1385
+ yield { type: 'session_id', sessionId: activated };
1288
1386
  }
1289
- else {
1290
- logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${event.session_id} turn=${turn?.turnId ?? 'legacy'}`);
1291
- const error = new Error('backend discovery rejected by active turn lease');
1292
- error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1293
- throw error;
1387
+ else if (!isResuming) {
1388
+ logger.info(`[AgentRunner] Deferring backend activation until Claude session file is durable: sessionId=${sessionId} backend=${event.session_id}`);
1294
1389
  }
1295
1390
  }
1296
1391
  if (event.type === 'user' && event.uuid === inputId) {
@@ -1385,7 +1480,17 @@ export class AgentRunner {
1385
1480
  }
1386
1481
  if (event.type === 'stream_event') {
1387
1482
  const streamEvent = event.event;
1388
- if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1483
+ if (streamEvent?.type === 'content_block_start'
1484
+ && (streamEvent.content_block?.type === 'thinking' || streamEvent.content_block?.type === 'redacted_thinking')) {
1485
+ hasTurnActivity = true;
1486
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1487
+ }
1488
+ else if (streamEvent?.type === 'content_block_delta'
1489
+ && streamEvent.delta?.type === 'thinking_delta') {
1490
+ hasTurnActivity = true;
1491
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1492
+ }
1493
+ else if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1389
1494
  hasTurnActivity = true;
1390
1495
  lastAssistantWasSynthetic = streamEvent.message.model === '<synthetic>';
1391
1496
  lastModelCall = {
@@ -1419,6 +1524,13 @@ export class AgentRunner {
1419
1524
  }
1420
1525
  continue;
1421
1526
  }
1527
+ // During redacted thinking Claude may emit token estimates instead of
1528
+ // readable thinking deltas. They still prove the model is active.
1529
+ if (event.type === 'system' && event.subtype === 'thinking_tokens') {
1530
+ hasTurnActivity = true;
1531
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1532
+ continue;
1533
+ }
1422
1534
  // system: compact_boundary → compact
1423
1535
  if (event.type === 'system' && event.subtype === 'compact_boundary') {
1424
1536
  yield {
@@ -1489,7 +1601,11 @@ export class AgentRunner {
1489
1601
  // 统计本轮 base agent 全部输出字符数(text + tool_use input)
1490
1602
  let turnOutputChars = 0;
1491
1603
  for (const content of event.message.content) {
1492
- if (content.type === 'tool_use') {
1604
+ if (content.type === 'thinking' || content.type === 'redacted_thinking') {
1605
+ hasTurnActivity = true;
1606
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1607
+ }
1608
+ else if (content.type === 'tool_use') {
1493
1609
  const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
1494
1610
  turnOutputChars += inputStr.length;
1495
1611
  }
@@ -1532,6 +1648,12 @@ export class AgentRunner {
1532
1648
  }
1533
1649
  // result → complete(含 permission_denials 提取)
1534
1650
  if (event.type === 'result') {
1651
+ // The SDK may announce session_id on the initial system event and omit
1652
+ // it from the terminal result. Give the transcript a short grace
1653
+ // period here before deciding that a topic backend is unavailable.
1654
+ const activatedAtResult = await activatePendingSession(true);
1655
+ if (activatedAtResult)
1656
+ yield { type: 'session_id', sessionId: activatedAtResult };
1535
1657
  // Most SDK streams surface denials as user-message tool_result blocks.
1536
1658
  // The terminal summary is only a fallback for incomplete streams.
1537
1659
  if (Array.isArray(event.permission_denials)) {
@@ -1661,6 +1783,15 @@ export class AgentRunner {
1661
1783
  modelCalls,
1662
1784
  assistantUuid: lastAssistantUuid,
1663
1785
  };
1786
+ // A topic binding must never outlive the provider transcript that can
1787
+ // resume it. If Claude reports success before its session file becomes
1788
+ // durable, fail this turn while leaving the topic unbound; /renew is
1789
+ // then available for older sessions that already contain a bad bind.
1790
+ if (!event.is_error && topicBinding && pendingSessionId) {
1791
+ const error = new Error(`bound topic backend is unavailable: ${pendingSessionId}`);
1792
+ error.code = 'TOPIC_BACKEND_UNAVAILABLE';
1793
+ throw error;
1794
+ }
1664
1795
  // Fatal results belong to the query transport itself and must terminate
1665
1796
  // even when the current input was not echoed back by the CLI.
1666
1797
  if (event.is_error) {
@@ -2613,7 +2744,7 @@ export class AgentRunner {
2613
2744
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2614
2745
  }
2615
2746
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2616
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn);
2747
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn, projectPath, topicBinding);
2617
2748
  const self = this;
2618
2749
  return (async function* () {
2619
2750
  try {
@@ -2768,7 +2899,10 @@ export class AgentRunner {
2768
2899
  managedSettings: buildClaudeManagedLockdownSettings(),
2769
2900
  ...(catalog && callEffort ? { effort: callEffort } : {}),
2770
2901
  permissionMode: this.toSdkPermissionMode(),
2771
- env: this.getAgentEnv()
2902
+ env: this.getAgentEnv(),
2903
+ ...(this.claudeExecutablePath
2904
+ ? { pathToClaudeCodeExecutable: this.claudeExecutablePath }
2905
+ : {})
2772
2906
  }
2773
2907
  });
2774
2908
  }
@@ -3020,6 +3154,7 @@ export class AgentRunner {
3020
3154
  strictMcpConfig: true,
3021
3155
  settingSources: [],
3022
3156
  managedSettings: buildClaudeManagedLockdownSettings(),
3157
+ ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
3023
3158
  permissionMode: this.toSdkPermissionMode(),
3024
3159
  stderr: (data) => { stderrChunks.push(data); },
3025
3160
  env: this.getAgentEnv(),
@@ -2,7 +2,7 @@ import { spawn } from 'child_process';
2
2
  import readline from 'readline';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../../core/permission/sandbox-runtime.js';
5
- import { resolveCodexLaunchCommand } from '../../utils/codex-cli.js';
5
+ import { resolveCodexLaunchCommand } from '../baseagent.js';
6
6
  import { createHash } from 'crypto';
7
7
  import { sanitizeShellExecutionEnvironment } from '../../core/permission/shell-environment.js';
8
8
  import fs from 'fs';
@@ -65,6 +65,7 @@ export class CodexAppServerClient {
65
65
  initialized = false;
66
66
  initializing = null;
67
67
  stderrBuffer = [];
68
+ stderrSequence = 0;
68
69
  managedHookRuntimeDir = null;
69
70
  processRegistrationPath = null;
70
71
  constructor(options) {
@@ -539,7 +540,7 @@ export class CodexAppServerClient {
539
540
  const text = String(chunk).trim();
540
541
  if (!text)
541
542
  return;
542
- this.stderrBuffer.push(text);
543
+ this.stderrBuffer.push({ sequence: ++this.stderrSequence, text });
543
544
  if (this.stderrBuffer.length > 20)
544
545
  this.stderrBuffer.shift();
545
546
  logger.debug(`[CodexAppServer] ${text}`);
@@ -612,9 +613,8 @@ export class CodexAppServerClient {
612
613
  ...process.env,
613
614
  ...this.options.env,
614
615
  });
615
- // The app-server is shared by multiple EvolCore sessions. Its process
616
- // environment must therefore use the daemon's trusted private root;
617
- // 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.
618
618
  const managedTmpDir = ensureProcessManagedTempDir();
619
619
  env.TMPDIR = managedTmpDir;
620
620
  delete env.EVOLCORE_RUNTIME_LOCK_DIR;
@@ -720,7 +720,8 @@ export class CodexAppServerClient {
720
720
  if (typeof managedDir !== 'string' || !path.isAbsolute(managedDir)
721
721
  || !fs.existsSync(managedDir) || !managedHookLoaded || !lockdownEnabled || !hooksEnabled) {
722
722
  throw new Error('Codex Windows proactive requires an installed and loaded EvolCore managed PreToolUse hook. '
723
- + '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.');
724
725
  }
725
726
  }
726
727
  buildProcessArgs() {
@@ -769,7 +770,13 @@ export class CodexAppServerClient {
769
770
  if (params !== undefined && params !== null)
770
771
  payload.params = params;
771
772
  return new Promise((resolve, reject) => {
772
- 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
+ });
773
780
  proc.stdin.write(`${JSON.stringify(payload)}\n`, error => {
774
781
  if (!error)
775
782
  return;
@@ -843,7 +850,10 @@ export class CodexAppServerClient {
843
850
  // The CLI's one-time bubblewrap configuration warning is process-wide
844
851
  // startup noise, not evidence about this RPC. Do not append it to an
845
852
  // unrelated request failure such as thread/resume.
846
- 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));
847
857
  const stderr = stderrLines.length ? `\n${stderrLines.join('\n')}` : '';
848
858
  const error = new Error(`${pending.method} failed: ${detail}${stderr}`);
849
859
  if (/no rollout found for thread id/i.test(detail)) {
@@ -1,6 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { CodexAppServerClient } from './app-server-client.js';
3
- import { resolveCodexLaunchCommand } from '../../utils/codex-cli.js';
3
+ import { resolveCodexLaunchCommand } from '../baseagent.js';
4
4
  import { parseModelList } from '../../core/model/catalog-service.js';
5
5
  async function cliModels() {
6
6
  const launch = resolveCodexLaunchCommand(['debug', 'models']);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
4
5
  import { parse as parseToml } from 'smol-toml';
5
6
  export function checkCodexProjectPath(value, options = {}) {
6
7
  const configured = typeof value === 'string' ? value.trim() : '';
@@ -51,6 +52,107 @@ export function codexManagedHookPaths(options = {}) {
51
52
  const managedDir = path.join(path.dirname(requirements), 'EvolCore', 'hooks');
52
53
  return { requirements, managedDir, hookPath: path.join(managedDir, 'pre-tool-use.mjs') };
53
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
+ }
54
156
  function regularFileState(file) {
55
157
  try {
56
158
  const stat = fs.lstatSync(file);
@@ -101,6 +203,28 @@ export function inspectCodexManagedHook(options = {}) {
101
203
  if ([requirementState, hookState].includes('unavailable')) {
102
204
  return { status: 'unavailable', requirements, managedDir, hookPath, reason: 'unable to read requirements.toml or launcher' };
103
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
+ }
104
228
  if ([requirementState, hookState].includes('missing')) {
105
229
  return { status: 'missing', requirements, managedDir, hookPath, reason: 'requirements.toml or launcher is missing' };
106
230
  }
@@ -152,3 +276,94 @@ export function inspectCodexManagedHook(options = {}) {
152
276
  ? { status: 'installed', requirements, managedDir, hookPath }
153
277
  : { status: 'unsafe', requirements, managedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
154
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
+ }