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.
- package/CHANGELOG.md +50 -0
- package/README.md +3 -1
- package/bin/codex-managed-acl.ps1 +7 -2
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +17 -6
- package/dist/agents/baseagent-availability.js +4 -4
- package/dist/agents/baseagent.js +29 -1
- package/dist/agents/claude-runner.js +148 -13
- package/dist/agents/codex/app-server-client.js +18 -8
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +215 -0
- package/dist/agents/codex-runner.js +438 -59
- package/dist/agents/ecagent-runner.js +17 -3
- package/dist/agents/gemini-runner.js +60 -2
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +41 -0
- package/dist/aun/msg/managed-operation.js +6 -1
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +4 -5
- package/dist/channels/aun.js +147 -76
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +60 -20
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +148 -17
- package/dist/cli/index.js +2 -2
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +72 -19
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +10 -2
- package/dist/core/auth/remote-access-store.js +3 -2
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +87 -25
- package/dist/core/command/menu-catalog.js +3 -1
- package/dist/core/command/menu-handler.js +274 -70
- package/dist/core/command/menu-protocol.js +6 -2
- package/dist/core/command/slash-gate.js +1 -3
- package/dist/core/command/slash-handler.js +157 -48
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +73 -21
- package/dist/core/model/catalog-service.js +3 -1
- package/dist/core/model/model-catalog.js +10 -0
- package/dist/core/model/model-permission.js +4 -1
- package/dist/core/permission/approval-gateway.js +169 -33
- package/dist/core/permission/tool-policy.js +13 -6
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +174 -28
- package/dist/ipc.js +68 -21
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/error-utils.js +11 -4
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +24 -1
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/templates/roles/admin.json +2 -0
- package/package.json +7 -3
- package/AGENTS.md +0 -62
- 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
|
-
|
|
1287
|
-
|
|
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.
|
|
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 === '
|
|
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 === '
|
|
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 '
|
|
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
|
-
//
|
|
616
|
-
//
|
|
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
|
-
+ '
|
|
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, {
|
|
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
|
|
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 '
|
|
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
|
+
}
|