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
|
@@ -5,10 +5,10 @@ import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../
|
|
|
5
5
|
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
6
6
|
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
|
|
7
7
|
import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
|
|
8
|
-
import { resolveOpenaiConfig } from './baseagent.js';
|
|
8
|
+
import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
|
|
9
9
|
import { logger } from '../utils/logger.js';
|
|
10
10
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
11
|
-
import { auditCodexApprovalDecision, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
11
|
+
import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
12
12
|
import { isRetryableError } from '../utils/error-utils.js';
|
|
13
13
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
14
14
|
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
@@ -17,11 +17,12 @@ import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/au
|
|
|
17
17
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
18
18
|
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
|
|
19
19
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
20
|
-
import { resolvePaths, resolveRoot } from '../paths.js';
|
|
20
|
+
import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
|
|
21
21
|
import { ensureProcessManagedTempDir, getManagedTaskTempDir } from '../cli/task-context.js';
|
|
22
22
|
import { buildSessionTurnList } from '../core/session/session-turns.js';
|
|
23
|
+
import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
|
|
23
24
|
import { execFileSync } from 'child_process';
|
|
24
|
-
import {
|
|
25
|
+
import { probeCli, probeCliSync } from '../utils/cross-platform.js';
|
|
25
26
|
import { createHash, randomBytes } from 'crypto';
|
|
26
27
|
import { buildModelRequestHeaders } from './request-identity.js';
|
|
27
28
|
import fs from 'fs';
|
|
@@ -41,6 +42,13 @@ export function formatApprovalLogSummary(summary) {
|
|
|
41
42
|
return singleLine;
|
|
42
43
|
return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
|
|
47
|
+
* Codex/OpenAI API 不认识这些后缀,必须传入标准模型 ID
|
|
48
|
+
*/
|
|
49
|
+
function stripContextWindowSuffix(modelId) {
|
|
50
|
+
return modelId.replace(/\[(?:1m|200k)\]$/i, '');
|
|
51
|
+
}
|
|
44
52
|
// ── Codex 模型目录(动态获取,含 effort) ──
|
|
45
53
|
class AsyncEventQueue {
|
|
46
54
|
queue = [];
|
|
@@ -89,6 +97,11 @@ function errorCodeForToolDenial(decisionSource, policyCode) {
|
|
|
89
97
|
return 'INVALID_ARGUMENT';
|
|
90
98
|
return 'EXECUTION_FAILED';
|
|
91
99
|
}
|
|
100
|
+
function isCodexReasoningNotification(method) {
|
|
101
|
+
return method === 'item/reasoning/textDelta'
|
|
102
|
+
|| method === 'item/reasoning/summaryTextDelta'
|
|
103
|
+
|| method === 'item/reasoning/summaryPartAdded';
|
|
104
|
+
}
|
|
92
105
|
const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
|
|
93
106
|
const managedTempDirsForExitCleanup = new Map();
|
|
94
107
|
let managedTempExitCleanupRegistered = false;
|
|
@@ -235,21 +248,59 @@ export function parseCodexCliVersion(output) {
|
|
|
235
248
|
export function isCodexCliVersionSupported(version) {
|
|
236
249
|
return compareVersions(version, MIN_CODEX_CLI_VERSION) >= 0;
|
|
237
250
|
}
|
|
251
|
+
// Windows shim/process startup can exceed the quick probe deadline under load.
|
|
252
|
+
const CODEX_CLI_PROBE_TIMEOUT_MS = 3000;
|
|
253
|
+
const CODEX_CLI_PROBE_RETRY_TIMEOUT_MS = 15_000;
|
|
254
|
+
function compactCodexProbeText(value, maxLength = 600) {
|
|
255
|
+
if (Buffer.isBuffer(value))
|
|
256
|
+
value = value.toString('utf-8');
|
|
257
|
+
if (typeof value !== 'string')
|
|
258
|
+
return undefined;
|
|
259
|
+
const text = value.replace(/\s+/gu, ' ').trim();
|
|
260
|
+
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
|
|
261
|
+
}
|
|
262
|
+
function logCodexProbeFailure(args, startedAt, error, timeoutMs) {
|
|
263
|
+
const detail = error;
|
|
264
|
+
logger.warn(`[CodexProbe] CLI probe failed: ${JSON.stringify({
|
|
265
|
+
command: ['codex', ...args],
|
|
266
|
+
path: detail?.path ?? null,
|
|
267
|
+
spawnargs: detail?.spawnargs ?? null,
|
|
268
|
+
elapsedMs: Date.now() - startedAt,
|
|
269
|
+
timeoutMs,
|
|
270
|
+
code: detail?.code ?? null,
|
|
271
|
+
status: detail?.status ?? null,
|
|
272
|
+
signal: detail?.signal ?? null,
|
|
273
|
+
timedOut: detail?.code === 'ETIMEDOUT',
|
|
274
|
+
message: compactCodexProbeText(detail?.message ?? String(error)),
|
|
275
|
+
stdout: compactCodexProbeText(detail?.stdout),
|
|
276
|
+
stderr: compactCodexProbeText(detail?.stderr),
|
|
277
|
+
})}`);
|
|
278
|
+
}
|
|
279
|
+
function probeCodexCli(args) {
|
|
280
|
+
return probeCliSync('codex', args, { onFailure: logCodexProbeFailure });
|
|
281
|
+
}
|
|
238
282
|
export function getCodexCliVersion() {
|
|
283
|
+
const startedAt = Date.now();
|
|
239
284
|
try {
|
|
240
|
-
const output =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
285
|
+
const output = probeCodexCli(['--version']);
|
|
286
|
+
const version = parseCodexCliVersion(output);
|
|
287
|
+
if (!version) {
|
|
288
|
+
logCodexProbeFailure(['--version'], startedAt, {
|
|
289
|
+
code: 'INVALID_VERSION_OUTPUT',
|
|
290
|
+
message: 'Codex CLI version output could not be parsed',
|
|
291
|
+
status: 0,
|
|
292
|
+
stdout: output,
|
|
293
|
+
}, CODEX_CLI_PROBE_TIMEOUT_MS);
|
|
294
|
+
}
|
|
295
|
+
return version;
|
|
246
296
|
}
|
|
247
297
|
catch {
|
|
248
298
|
return null;
|
|
249
299
|
}
|
|
250
300
|
}
|
|
251
301
|
export function getCodexAppServerAvailability() {
|
|
252
|
-
|
|
302
|
+
const cliPath = resolveCodexCliPath();
|
|
303
|
+
if (!cliPath) {
|
|
253
304
|
return { available: false, reason: '未检测到 Codex CLI。' };
|
|
254
305
|
}
|
|
255
306
|
const version = getCodexCliVersion();
|
|
@@ -265,21 +316,45 @@ export function getCodexAppServerAvailability() {
|
|
|
265
316
|
};
|
|
266
317
|
}
|
|
267
318
|
try {
|
|
268
|
-
|
|
269
|
-
encoding: 'utf-8',
|
|
270
|
-
timeout: 3000,
|
|
271
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
272
|
-
});
|
|
319
|
+
probeCodexCli(['app-server', '--help']);
|
|
273
320
|
return { available: true, version };
|
|
274
321
|
}
|
|
275
|
-
catch {
|
|
276
|
-
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error?.code === 'ETIMEDOUT') {
|
|
324
|
+
return { available: false, version, reason: `Codex CLI ${version} 的 app-server 探测超时,请稍后重试。` };
|
|
325
|
+
}
|
|
326
|
+
return { available: false, version, reason: `Codex CLI ${version} 的 app-server 探测失败,请检查 CLI 启动日志。` };
|
|
277
327
|
}
|
|
278
328
|
}
|
|
279
329
|
export function isCodexAppServerAvailable() {
|
|
280
330
|
return getCodexAppServerAvailability().available;
|
|
281
331
|
}
|
|
282
|
-
|
|
332
|
+
/** Daemon startup/hot reload use asynchronous probes; sync CLI callers share the cache. */
|
|
333
|
+
export async function getCodexAppServerAvailabilityAsync() {
|
|
334
|
+
const options = {
|
|
335
|
+
timeouts: [CODEX_CLI_PROBE_TIMEOUT_MS, CODEX_CLI_PROBE_RETRY_TIMEOUT_MS],
|
|
336
|
+
onFailure: logCodexProbeFailure,
|
|
337
|
+
};
|
|
338
|
+
let version;
|
|
339
|
+
try {
|
|
340
|
+
version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
|
|
341
|
+
if (!version)
|
|
342
|
+
return { available: false, reason: '无法读取 Codex CLI 版本。' };
|
|
343
|
+
if (!isCodexCliVersionSupported(version)) {
|
|
344
|
+
return { available: false, version, requiresUpgrade: true,
|
|
345
|
+
reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}。请升级 Codex CLI:npm install -g @openai/codex@latest` };
|
|
346
|
+
}
|
|
347
|
+
await probeCli('codex', ['app-server', '--help'], options);
|
|
348
|
+
return { available: true, version };
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const detail = error;
|
|
352
|
+
return { available: false, version, reason: detail.code === 'ETIMEDOUT' || detail.killed
|
|
353
|
+
? 'Codex CLI 探测超时,请稍后重试。'
|
|
354
|
+
: `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
|
|
283
358
|
export class CodexRunner {
|
|
284
359
|
name = 'codex';
|
|
285
360
|
// turn/start persists a new user turn before runQuery resolves. App-server
|
|
@@ -291,7 +366,7 @@ export class CodexRunner {
|
|
|
291
366
|
activeAbortControllers = new Map();
|
|
292
367
|
activeStreams = new Map();
|
|
293
368
|
activeSessions = new Map(); // sessionId → threadId
|
|
294
|
-
/**
|
|
369
|
+
/** New threads held in memory until the exact first turn is visible in the rollout. */
|
|
295
370
|
provisionalSessions = new Map();
|
|
296
371
|
activeTurns = new Map();
|
|
297
372
|
activeSubagentTurns = new Map();
|
|
@@ -320,8 +395,14 @@ export class CodexRunner {
|
|
|
320
395
|
permissionGateway;
|
|
321
396
|
sendPromptFn;
|
|
322
397
|
permissionContexts = new Map();
|
|
398
|
+
/** Fallback authorization lifecycle ids for app-server requests without an RPC id. */
|
|
399
|
+
approvalLifecycleIds = new WeakMap();
|
|
323
400
|
resolvedConfig;
|
|
324
401
|
pendingInterruptTtlMs = 30_000;
|
|
402
|
+
/** Fallback for direct runner callers that do not inject SessionManager. */
|
|
403
|
+
codexSessionFileAdapter = new CodexSessionFileAdapter();
|
|
404
|
+
/** threadId → managed-hook credential, issued by this runner's agent only. */
|
|
405
|
+
threadCredentials = new Map();
|
|
325
406
|
constructor(config, callbacks, runtime) {
|
|
326
407
|
// Compatibility for legacy direct callers that still pass the removed
|
|
327
408
|
// internal fields in a synthetic config. AgentLoader uses runtime binding.
|
|
@@ -350,7 +431,7 @@ export class CodexRunner {
|
|
|
350
431
|
fileRewind: 'git-head',
|
|
351
432
|
fullaccess: true,
|
|
352
433
|
};
|
|
353
|
-
this.model = this.resolvedConfig.model;
|
|
434
|
+
this.model = stripContextWindowSuffix(this.resolvedConfig.model);
|
|
354
435
|
if (this.resolvedConfig.effort)
|
|
355
436
|
this.effort = this.resolvedConfig.effort;
|
|
356
437
|
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
@@ -775,6 +856,11 @@ export class CodexRunner {
|
|
|
775
856
|
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
776
857
|
throw error;
|
|
777
858
|
}
|
|
859
|
+
// The binding callback can yield while a replacement run claims this
|
|
860
|
+
// session. Re-check the provisional owner before publishing any local
|
|
861
|
+
// indexes so a late first turn cannot overwrite the newer thread.
|
|
862
|
+
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
863
|
+
return false;
|
|
778
864
|
this.provisionalSessions.delete(sessionId);
|
|
779
865
|
this.activeSessions.set(sessionId, threadId);
|
|
780
866
|
if (carrierToPublish) {
|
|
@@ -784,6 +870,30 @@ export class CodexRunner {
|
|
|
784
870
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
785
871
|
return true;
|
|
786
872
|
}
|
|
873
|
+
async waitForCodexTurnPersistence(sessionManager, projectPath, threadId, turnId, timeoutMs = 0) {
|
|
874
|
+
const check = () => {
|
|
875
|
+
try {
|
|
876
|
+
if (typeof sessionManager?.checkSessionTurnPersisted === 'function') {
|
|
877
|
+
return sessionManager.checkSessionTurnPersisted(projectPath, threadId, 'codex', turnId, this.resolvedConfig.agentAid) === true;
|
|
878
|
+
}
|
|
879
|
+
return this.codexSessionFileAdapter.checkTurnPersisted(projectPath, threadId, turnId, this.resolvedConfig.agentAid);
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
logger.debug(`[CodexRunner] rollout persistence check failed: session=${threadId} turn=${turnId} `
|
|
883
|
+
+ `error=${error instanceof Error ? error.message : String(error)}`);
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
888
|
+
while (true) {
|
|
889
|
+
if (check())
|
|
890
|
+
return true;
|
|
891
|
+
const remaining = deadline - Date.now();
|
|
892
|
+
if (remaining <= 0)
|
|
893
|
+
return false;
|
|
894
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(10, remaining)));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
787
897
|
cleanupProvisionalThread(sessionId, threadId) {
|
|
788
898
|
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
789
899
|
return;
|
|
@@ -1094,7 +1204,7 @@ export class CodexRunner {
|
|
|
1094
1204
|
}
|
|
1095
1205
|
}
|
|
1096
1206
|
// ── ModelSwitcher ──
|
|
1097
|
-
setModel(model) { this.model = model; this.resetAppServerClient(); }
|
|
1207
|
+
setModel(model) { this.model = stripContextWindowSuffix(model); this.resetAppServerClient(); }
|
|
1098
1208
|
getModel() { return this.model; }
|
|
1099
1209
|
getModelCatalog() {
|
|
1100
1210
|
return getRunnerCatalog('codex', this.resolvedConfig.agentAid, this.resolvedConfig);
|
|
@@ -1315,7 +1425,28 @@ export class CodexRunner {
|
|
|
1315
1425
|
}
|
|
1316
1426
|
await appServer.assertManagedHooksAvailable();
|
|
1317
1427
|
}
|
|
1318
|
-
|
|
1428
|
+
/**
|
|
1429
|
+
* Decide whether the managed hook may proceed with a tool call.
|
|
1430
|
+
*
|
|
1431
|
+
* Credential contract (see CODEX_SESSION_TOKEN_ENV):
|
|
1432
|
+
* - sessionToken present and issued by this runner for this thread → the
|
|
1433
|
+
* call belongs to an EvolCore-managed thread; full policy evaluation.
|
|
1434
|
+
* - sessionToken present but unknown/mismatched → forgery or stale state;
|
|
1435
|
+
* fail closed with applicable=true so no other outcome allows it.
|
|
1436
|
+
* - sessionToken absent → unmanaged process; not this runner's concern
|
|
1437
|
+
* (applicable=false; the IPC layer allows such calls outright).
|
|
1438
|
+
*/
|
|
1439
|
+
async evaluatePreToolUse(threadId, toolName, toolInput, signal, callId, sessionToken) {
|
|
1440
|
+
if (typeof sessionToken === 'string' && sessionToken) {
|
|
1441
|
+
if (!this.validateThreadCredential(threadId, sessionToken)) {
|
|
1442
|
+
return {
|
|
1443
|
+
ok: false,
|
|
1444
|
+
applicable: true,
|
|
1445
|
+
decision: 'deny',
|
|
1446
|
+
reason: 'Codex session credential was not recognized',
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1319
1450
|
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
1320
1451
|
const activeThread = this.activeSessions.get(sessionKey) ?? this.provisionalSessions.get(sessionKey);
|
|
1321
1452
|
const context = this.permissionContexts.get(sessionKey);
|
|
@@ -1363,6 +1494,16 @@ export class CodexRunner {
|
|
|
1363
1494
|
}
|
|
1364
1495
|
};
|
|
1365
1496
|
if (!activeThread || !this.isThreadOwnedBySession(sessionKey, threadId)) {
|
|
1497
|
+
if (typeof sessionToken === 'string' && sessionToken) {
|
|
1498
|
+
// A credentialed thread this runner cannot place (e.g. resumed state
|
|
1499
|
+
// lost across a runner reload) must never fall through to allow.
|
|
1500
|
+
return {
|
|
1501
|
+
ok: false,
|
|
1502
|
+
applicable: true,
|
|
1503
|
+
decision: 'deny',
|
|
1504
|
+
reason: 'EvolCore-managed Codex thread is not tracked by an active session',
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1366
1507
|
return { ok: true, applicable: false };
|
|
1367
1508
|
}
|
|
1368
1509
|
const proactiveSession = context?.chatmode === 'proactive';
|
|
@@ -1603,7 +1744,7 @@ export class CodexRunner {
|
|
|
1603
1744
|
}
|
|
1604
1745
|
}
|
|
1605
1746
|
let resumingThread = !!agentSessionId;
|
|
1606
|
-
const callModel = modelOverride?.model || this.model;
|
|
1747
|
+
const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
1607
1748
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
1608
1749
|
? undefined
|
|
1609
1750
|
: modelOverride?.effort ?? this.effort;
|
|
@@ -1647,7 +1788,7 @@ export class CodexRunner {
|
|
|
1647
1788
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1648
1789
|
approvalsReviewer: 'user',
|
|
1649
1790
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1650
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1791
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined, agentSessionId), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1651
1792
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1652
1793
|
};
|
|
1653
1794
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
@@ -1690,6 +1831,7 @@ export class CodexRunner {
|
|
|
1690
1831
|
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1691
1832
|
this.unbindExternalToolBoundary(missingThreadId);
|
|
1692
1833
|
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1834
|
+
this.dropThreadCredential(missingThreadId);
|
|
1693
1835
|
agentSessionId = undefined;
|
|
1694
1836
|
resumingThread = false;
|
|
1695
1837
|
knownDelegationCarrier = undefined;
|
|
@@ -1795,6 +1937,7 @@ export class CodexRunner {
|
|
|
1795
1937
|
projectPath,
|
|
1796
1938
|
model: callModel,
|
|
1797
1939
|
committed: resumingThread,
|
|
1940
|
+
persistenceFinalized: resumingThread,
|
|
1798
1941
|
streamedAgentMessageIds: new Set(),
|
|
1799
1942
|
agentMessageDeltaText: new Map(),
|
|
1800
1943
|
completedItemIds: new Set(),
|
|
@@ -1834,8 +1977,9 @@ export class CodexRunner {
|
|
|
1834
1977
|
this.unregisterChildThread(notifThreadId);
|
|
1835
1978
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1836
1979
|
}
|
|
1837
|
-
if (this.isSubagentActivityItem(item))
|
|
1980
|
+
if (this.isSubagentActivityItem(item) || isCodexReasoningNotification(notification.method)) {
|
|
1838
1981
|
queue.push(notification);
|
|
1982
|
+
}
|
|
1839
1983
|
if (notification.method === 'item/completed' && state.turnCompleted) {
|
|
1840
1984
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1841
1985
|
}
|
|
@@ -1882,11 +2026,43 @@ export class CodexRunner {
|
|
|
1882
2026
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1883
2027
|
});
|
|
1884
2028
|
if (!resumingThread) {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
if (committed)
|
|
1888
|
-
|
|
1889
|
-
|
|
2029
|
+
let confirmationInFlight;
|
|
2030
|
+
state.confirmProvisional = (waitMs = 0) => {
|
|
2031
|
+
if (state.committed)
|
|
2032
|
+
return Promise.resolve(true);
|
|
2033
|
+
if (state.persistenceFinalized)
|
|
2034
|
+
return Promise.resolve(false);
|
|
2035
|
+
if (confirmationInFlight)
|
|
2036
|
+
return confirmationInFlight;
|
|
2037
|
+
const providerTurnId = state.turnId;
|
|
2038
|
+
if (!providerTurnId)
|
|
2039
|
+
return Promise.resolve(false);
|
|
2040
|
+
const confirmation = (async () => {
|
|
2041
|
+
const persisted = await this.waitForCodexTurnPersistence(sessionManager, projectPath, threadId, providerTurnId, waitMs);
|
|
2042
|
+
if (!persisted)
|
|
2043
|
+
return false;
|
|
2044
|
+
try {
|
|
2045
|
+
const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
|
|
2046
|
+
if (!committed) {
|
|
2047
|
+
const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
|
|
2048
|
+
staleError.code = 'RUNNER_START_SUPERSEDED';
|
|
2049
|
+
throw staleError;
|
|
2050
|
+
}
|
|
2051
|
+
agentSessionId = threadId;
|
|
2052
|
+
state.committed = true;
|
|
2053
|
+
return true;
|
|
2054
|
+
}
|
|
2055
|
+
catch (error) {
|
|
2056
|
+
state.persistenceFinalized = true;
|
|
2057
|
+
throw error;
|
|
2058
|
+
}
|
|
2059
|
+
})();
|
|
2060
|
+
confirmationInFlight = confirmation;
|
|
2061
|
+
void confirmation.finally(() => {
|
|
2062
|
+
if (confirmationInFlight === confirmation)
|
|
2063
|
+
confirmationInFlight = undefined;
|
|
2064
|
+
}).catch(() => undefined);
|
|
2065
|
+
return confirmation;
|
|
1890
2066
|
};
|
|
1891
2067
|
}
|
|
1892
2068
|
const turnId = turnResponse.turn?.id;
|
|
@@ -1894,6 +2070,12 @@ export class CodexRunner {
|
|
|
1894
2070
|
state.turnId = turnId;
|
|
1895
2071
|
this.activeTurns.set(sessionId, { threadId, turnId });
|
|
1896
2072
|
}
|
|
2073
|
+
if (!resumingThread && turnId) {
|
|
2074
|
+
// Check immediately, but leave a just-created rollout as a candidate.
|
|
2075
|
+
// Subsequent notifications recheck it and the terminal boundary gets
|
|
2076
|
+
// the short settle window below.
|
|
2077
|
+
await state.confirmProvisional?.(0);
|
|
2078
|
+
}
|
|
1897
2079
|
if (turnId && this.consumePendingInterrupt(sessionId)) {
|
|
1898
2080
|
await this.interruptAppServerTurn(threadId, turnId);
|
|
1899
2081
|
controller.abort('User interrupt');
|
|
@@ -2042,11 +2224,16 @@ export class CodexRunner {
|
|
|
2042
2224
|
}
|
|
2043
2225
|
capturedController?.abort('Session closed');
|
|
2044
2226
|
const capturedSubscription = capturedThreadId ? this.threadSubscriptions.get(capturedThreadId) : undefined;
|
|
2227
|
+
const currentActiveThreadId = this.activeSessions.get(sessionId);
|
|
2228
|
+
const currentProvisionalThreadId = this.provisionalSessions.get(sessionId);
|
|
2229
|
+
const capturedThreadIds = new Set([capturedThreadId, capturedProvisionalThreadId].filter((value) => value !== undefined));
|
|
2230
|
+
const threadLifecycleReplaced = (currentActiveThreadId !== undefined && !capturedThreadIds.has(currentActiveThreadId))
|
|
2231
|
+
|| (currentProvisionalThreadId !== undefined && !capturedThreadIds.has(currentProvisionalThreadId));
|
|
2045
2232
|
const lifecycleReplaced = (capturedStream !== undefined && this.activeStreams.get(sessionId) !== capturedStream)
|
|
2046
2233
|
|| (capturedController !== undefined && this.activeAbortControllers.get(sessionId) !== capturedController)
|
|
2047
2234
|
|| (capturedTurn !== undefined && this.activeTurns.get(sessionId) !== capturedTurn)
|
|
2048
2235
|
|| (capturedPermissionContext !== undefined && this.permissionContexts.get(sessionId) !== capturedPermissionContext)
|
|
2049
|
-
||
|
|
2236
|
+
|| threadLifecycleReplaced;
|
|
2050
2237
|
if (capturedSubscription && !lifecycleReplaced
|
|
2051
2238
|
&& this.threadSubscriptions.get(capturedThreadId) === capturedSubscription) {
|
|
2052
2239
|
await this.releaseThreadSubscription(capturedSubscription.client, capturedThreadId, 'session-closed');
|
|
@@ -2083,7 +2270,7 @@ export class CodexRunner {
|
|
|
2083
2270
|
}
|
|
2084
2271
|
}
|
|
2085
2272
|
resolveSessionFile(agentSessionId, _projectPath) {
|
|
2086
|
-
// Codex
|
|
2273
|
+
// Codex sessions are shared with the local Codex Desktop installation.
|
|
2087
2274
|
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
2088
2275
|
if (!fs.existsSync(sessionsDir))
|
|
2089
2276
|
return null;
|
|
@@ -2145,7 +2332,7 @@ export class CodexRunner {
|
|
|
2145
2332
|
if (!this.isThreadNotFoundError(error))
|
|
2146
2333
|
throw error;
|
|
2147
2334
|
logger.info(`[CodexRunner] Compact thread not loaded, resuming before compact: ${agentSessionId}`);
|
|
2148
|
-
const compactModel = modelOverride?.model || this.model;
|
|
2335
|
+
const compactModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
2149
2336
|
const compactEffort = modelOverride?.effortMode === 'model_default'
|
|
2150
2337
|
? undefined
|
|
2151
2338
|
: modelOverride?.effort ?? this.effort;
|
|
@@ -2165,7 +2352,7 @@ export class CodexRunner {
|
|
|
2165
2352
|
approvalPolicy: compactPolicy,
|
|
2166
2353
|
approvalsReviewer: 'user',
|
|
2167
2354
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2168
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
2355
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier, undefined, agentSessionId), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
2169
2356
|
});
|
|
2170
2357
|
this.markThreadSubscribed(appServer, agentSessionId);
|
|
2171
2358
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
@@ -2274,10 +2461,14 @@ export class CodexRunner {
|
|
|
2274
2461
|
}
|
|
2275
2462
|
return false;
|
|
2276
2463
|
}
|
|
2464
|
+
codexHomeDir() {
|
|
2465
|
+
return process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex');
|
|
2466
|
+
}
|
|
2277
2467
|
findCodexSessionFile(threadId) {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2468
|
+
return this.findRolloutFile(path.join(this.codexHomeDir(), 'sessions'), threadId)
|
|
2469
|
+
?? this.findRolloutFile(path.join(this.codexHomeDir(), 'archived_sessions'), threadId);
|
|
2470
|
+
}
|
|
2471
|
+
findRolloutFile(root, threadId) {
|
|
2281
2472
|
const stack = [root];
|
|
2282
2473
|
let newest;
|
|
2283
2474
|
while (stack.length > 0) {
|
|
@@ -2309,6 +2500,101 @@ export class CodexRunner {
|
|
|
2309
2500
|
}
|
|
2310
2501
|
return newest?.path;
|
|
2311
2502
|
}
|
|
2503
|
+
// ── Managed-hook thread credentials ─────────────────────────────────
|
|
2504
|
+
// The Windows managed hook is machine-wide, so unmanaged Codex processes
|
|
2505
|
+
// also reach it. Credentials let the hook tell EvolCore-owned threads
|
|
2506
|
+
// (policy enforced) apart from unmanaged ones (allowed unconditionally).
|
|
2507
|
+
// They follow the thread (not the daemon process) so a daemon restart does
|
|
2508
|
+
// not strand a resumable thread: the token file is re-read on resume, and
|
|
2509
|
+
// it is removed lazily once the rollout file itself disappears.
|
|
2510
|
+
threadCredentialsDir() {
|
|
2511
|
+
const aid = this.resolvedConfig.agentAid;
|
|
2512
|
+
if (!aid)
|
|
2513
|
+
return undefined;
|
|
2514
|
+
return path.join(agentSessionsDir(aid), 'codex-tokens');
|
|
2515
|
+
}
|
|
2516
|
+
threadCredentialPath(threadId) {
|
|
2517
|
+
const dir = this.threadCredentialsDir();
|
|
2518
|
+
return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
|
|
2519
|
+
}
|
|
2520
|
+
/** Issue or re-read this agent's credential for a thread. */
|
|
2521
|
+
ensureThreadCredential(threadId) {
|
|
2522
|
+
const cached = this.threadCredentials.get(threadId);
|
|
2523
|
+
if (cached)
|
|
2524
|
+
return cached;
|
|
2525
|
+
const file = this.threadCredentialPath(threadId);
|
|
2526
|
+
if (!file)
|
|
2527
|
+
return undefined;
|
|
2528
|
+
try {
|
|
2529
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2530
|
+
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2531
|
+
this.threadCredentials.set(threadId, record.token);
|
|
2532
|
+
return record.token;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
catch { /* missing or unreadable: fall through to issuance */ }
|
|
2536
|
+
const token = randomBytes(32).toString('hex');
|
|
2537
|
+
const record = {
|
|
2538
|
+
token,
|
|
2539
|
+
threadId,
|
|
2540
|
+
createdAt: new Date().toISOString(),
|
|
2541
|
+
rolloutPath: this.findCodexSessionFile(threadId),
|
|
2542
|
+
};
|
|
2543
|
+
try {
|
|
2544
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
2545
|
+
fs.writeFileSync(file, JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
2546
|
+
this.threadCredentials.set(threadId, token);
|
|
2547
|
+
return token;
|
|
2548
|
+
}
|
|
2549
|
+
catch (error) {
|
|
2550
|
+
logger.warn(`[CodexRunner] failed to persist thread credential: thread=${threadId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
2551
|
+
return undefined;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Validate a credential presented by the managed hook. Returns the matched
|
|
2556
|
+
* threadId when this runner issued the token; undefined otherwise. Token
|
|
2557
|
+
* records whose rollout file has disappeared are pruned lazily.
|
|
2558
|
+
*/
|
|
2559
|
+
validateThreadCredential(threadId, token) {
|
|
2560
|
+
const expected = this.ensureThreadCredentialLoaded(threadId);
|
|
2561
|
+
if (!expected || expected !== token)
|
|
2562
|
+
return false;
|
|
2563
|
+
const rollout = this.findCodexSessionFile(threadId);
|
|
2564
|
+
if (!rollout) {
|
|
2565
|
+
// The thread's rollout is gone: nothing left to protect, drop the record.
|
|
2566
|
+
this.dropThreadCredential(threadId);
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
ensureThreadCredentialLoaded(threadId) {
|
|
2572
|
+
const cached = this.threadCredentials.get(threadId);
|
|
2573
|
+
if (cached)
|
|
2574
|
+
return cached;
|
|
2575
|
+
const file = this.threadCredentialPath(threadId);
|
|
2576
|
+
if (!file)
|
|
2577
|
+
return undefined;
|
|
2578
|
+
try {
|
|
2579
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2580
|
+
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2581
|
+
this.threadCredentials.set(threadId, record.token);
|
|
2582
|
+
return record.token;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
catch { /* absent or unreadable */ }
|
|
2586
|
+
return undefined;
|
|
2587
|
+
}
|
|
2588
|
+
dropThreadCredential(threadId) {
|
|
2589
|
+
this.threadCredentials.delete(threadId);
|
|
2590
|
+
const file = this.threadCredentialPath(threadId);
|
|
2591
|
+
if (!file)
|
|
2592
|
+
return;
|
|
2593
|
+
try {
|
|
2594
|
+
fs.rmSync(file, { force: true });
|
|
2595
|
+
}
|
|
2596
|
+
catch { /* best effort */ }
|
|
2597
|
+
}
|
|
2312
2598
|
async forkSession(agentSessionId, projectPath, title) {
|
|
2313
2599
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
|
|
2314
2600
|
try {
|
|
@@ -2597,6 +2883,31 @@ export class CodexRunner {
|
|
|
2597
2883
|
? 'FileChange'
|
|
2598
2884
|
: 'Bash';
|
|
2599
2885
|
const toolInput = this.buildPermissionInput(request.method, params);
|
|
2886
|
+
const approvalCallId = typeof (params.itemId ?? params.item_id ?? params.callId ?? params.call_id) === 'string'
|
|
2887
|
+
? String(params.itemId ?? params.item_id ?? params.callId ?? params.call_id)
|
|
2888
|
+
: undefined;
|
|
2889
|
+
const approvalRequestId = request.id !== undefined
|
|
2890
|
+
? String(request.id)
|
|
2891
|
+
: approvalCallId ?? `codex-approval-${crypto.randomUUID()}`;
|
|
2892
|
+
this.approvalLifecycleIds.set(request, approvalRequestId);
|
|
2893
|
+
const approvalContext = this.permissionContexts.get(sessionKey);
|
|
2894
|
+
auditPermissionLifecycle({
|
|
2895
|
+
phase: 'request',
|
|
2896
|
+
requestId: approvalRequestId,
|
|
2897
|
+
callId: approvalCallId,
|
|
2898
|
+
correlationId: approvalCallId,
|
|
2899
|
+
sessionId: sessionKey,
|
|
2900
|
+
agentAid: approvalContext?.selfAid,
|
|
2901
|
+
agentName: approvalContext?.agentName,
|
|
2902
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
2903
|
+
toolName,
|
|
2904
|
+
decision: 'pending',
|
|
2905
|
+
decisionSource: 'approval',
|
|
2906
|
+
role: approvalContext?.role,
|
|
2907
|
+
taskId: approvalContext?.taskId,
|
|
2908
|
+
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
2909
|
+
approvalMethod: request.method,
|
|
2910
|
+
});
|
|
2600
2911
|
// A trusted fullaccess run has already been explicitly authorized by the
|
|
2601
2912
|
// daemon owner. Do not route its approvals through ordinary capability,
|
|
2602
2913
|
// workspace, or unattended-trigger policy checks.
|
|
@@ -2834,6 +3145,49 @@ export class CodexRunner {
|
|
|
2834
3145
|
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
2835
3146
|
parseFailure,
|
|
2836
3147
|
});
|
|
3148
|
+
const lifecycleRequestId = request.id !== undefined
|
|
3149
|
+
? String(request.id)
|
|
3150
|
+
: this.approvalLifecycleIds.get(request)
|
|
3151
|
+
?? callId
|
|
3152
|
+
?? `codex-approval-${crypto.randomUUID()}`;
|
|
3153
|
+
auditPermissionLifecycle({
|
|
3154
|
+
phase: 'response',
|
|
3155
|
+
requestId: lifecycleRequestId,
|
|
3156
|
+
callId,
|
|
3157
|
+
correlationId: callId,
|
|
3158
|
+
sessionId: sessionKey,
|
|
3159
|
+
agentAid: context?.selfAid,
|
|
3160
|
+
agentName: context?.agentName,
|
|
3161
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
3162
|
+
toolName,
|
|
3163
|
+
decision,
|
|
3164
|
+
decisionSource,
|
|
3165
|
+
policyCode,
|
|
3166
|
+
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
3167
|
+
role: context?.role,
|
|
3168
|
+
taskId: context?.taskId,
|
|
3169
|
+
summary: auditSummary,
|
|
3170
|
+
approvalMethod: request.method,
|
|
3171
|
+
});
|
|
3172
|
+
auditPermissionLifecycle({
|
|
3173
|
+
phase: 'final',
|
|
3174
|
+
requestId: lifecycleRequestId,
|
|
3175
|
+
callId,
|
|
3176
|
+
correlationId: callId,
|
|
3177
|
+
sessionId: sessionKey,
|
|
3178
|
+
agentAid: context?.selfAid,
|
|
3179
|
+
agentName: context?.agentName,
|
|
3180
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
3181
|
+
toolName,
|
|
3182
|
+
decision,
|
|
3183
|
+
decisionSource,
|
|
3184
|
+
policyCode,
|
|
3185
|
+
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
3186
|
+
role: context?.role,
|
|
3187
|
+
taskId: context?.taskId,
|
|
3188
|
+
summary: auditSummary,
|
|
3189
|
+
approvalMethod: request.method,
|
|
3190
|
+
});
|
|
2837
3191
|
logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
|
|
2838
3192
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2839
3193
|
...(callId ? { callId, correlationId: callId } : {}),
|
|
@@ -3924,26 +4278,37 @@ export class CodexRunner {
|
|
|
3924
4278
|
for await (const notification of notifications) {
|
|
3925
4279
|
if (!this.activeAbortControllers.has(sessionId))
|
|
3926
4280
|
break;
|
|
3927
|
-
if (!state.committed &&
|
|
3928
|
-
const
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
state.
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
yield { type: 'session_id', sessionId: state.threadId };
|
|
3940
|
-
}
|
|
4281
|
+
if (!state.committed && !state.persistenceFinalized) {
|
|
4282
|
+
const terminal = notification.method === 'turn/completed';
|
|
4283
|
+
const committed = await state.confirmProvisional?.(terminal ? 250 : 0) ?? false;
|
|
4284
|
+
if (committed && !sessionIdEmitted) {
|
|
4285
|
+
sessionIdEmitted = true;
|
|
4286
|
+
yield { type: 'session_id', sessionId: state.threadId };
|
|
4287
|
+
}
|
|
4288
|
+
else if (terminal && !committed) {
|
|
4289
|
+
state.persistenceFinalized = true;
|
|
4290
|
+
const persistenceError = new Error(`Codex turn completed before its rollout became recoverable: thread=${state.threadId} turn=${state.turnId ?? '<unknown>'}`);
|
|
4291
|
+
persistenceError.code = 'CODEX_TURN_PERSISTENCE_UNCONFIRMED';
|
|
4292
|
+
throw persistenceError;
|
|
3941
4293
|
}
|
|
3942
4294
|
}
|
|
3943
4295
|
yield* this.mapAppServerNotification(notification, sessionId, state);
|
|
3944
4296
|
}
|
|
3945
4297
|
}
|
|
3946
4298
|
finally {
|
|
4299
|
+
if (!state.committed && !state.persistenceFinalized && state.confirmProvisional) {
|
|
4300
|
+
try {
|
|
4301
|
+
// Do not let an interrupt wait for persistence that did not exist at
|
|
4302
|
+
// cancellation time. A zero-wait check still captures a rollout
|
|
4303
|
+
// that was already durable before the stream was closed.
|
|
4304
|
+
await state.confirmProvisional(0);
|
|
4305
|
+
}
|
|
4306
|
+
catch (error) {
|
|
4307
|
+
logger.debug(`[CodexRunner] provisional thread rejected during final persistence check: `
|
|
4308
|
+
+ `session=${sessionId} thread=${state.threadId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4309
|
+
}
|
|
4310
|
+
state.persistenceFinalized = true;
|
|
4311
|
+
}
|
|
3947
4312
|
unsubscribe();
|
|
3948
4313
|
await this.releaseThreadSubscription(appServer, state.threadId, 'turn-finished');
|
|
3949
4314
|
if (this.activeAbortControllers.get(sessionId) === controller) {
|
|
@@ -3990,6 +4355,15 @@ export class CodexRunner {
|
|
|
3990
4355
|
}
|
|
3991
4356
|
break;
|
|
3992
4357
|
}
|
|
4358
|
+
// Reasoning notifications are provider-internal liveness signals.
|
|
4359
|
+
// Do not expose their content; the ResponseEngine consumes the heartbeat
|
|
4360
|
+
// solely to reset the idle monitor.
|
|
4361
|
+
case 'item/reasoning/textDelta':
|
|
4362
|
+
case 'item/reasoning/summaryTextDelta':
|
|
4363
|
+
case 'item/reasoning/summaryPartAdded': {
|
|
4364
|
+
yield { type: 'heartbeat', source: 'reasoning' };
|
|
4365
|
+
break;
|
|
4366
|
+
}
|
|
3993
4367
|
case 'item/completed': {
|
|
3994
4368
|
const item = params.item;
|
|
3995
4369
|
if (item?.id)
|
|
@@ -4601,12 +4975,17 @@ export class CodexRunner {
|
|
|
4601
4975
|
}
|
|
4602
4976
|
}
|
|
4603
4977
|
managedTempInstruction(sessionId) {
|
|
4604
|
-
|
|
4978
|
+
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
4979
|
+
if (!managedTempDir)
|
|
4605
4980
|
return undefined;
|
|
4606
|
-
return
|
|
4981
|
+
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 \`/tmp/evolcore-codex-*\` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
|
|
4607
4982
|
}
|
|
4608
|
-
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken) {
|
|
4983
|
+
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken, threadId) {
|
|
4609
4984
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
4985
|
+
// The managed-hook credential rides the thread's shell environment so the
|
|
4986
|
+
// PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
|
|
4987
|
+
// It follows the thread (start AND resume) rather than the daemon process.
|
|
4988
|
+
const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
|
|
4610
4989
|
return {
|
|
4611
4990
|
shell_environment_policy: {
|
|
4612
4991
|
set: {
|
|
@@ -4617,6 +4996,7 @@ export class CodexRunner {
|
|
|
4617
4996
|
...(taskDelegationToken || delegationCarrier
|
|
4618
4997
|
? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
|
|
4619
4998
|
: {}),
|
|
4999
|
+
...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
|
|
4620
5000
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
4621
5001
|
},
|
|
4622
5002
|
},
|
|
@@ -4672,6 +5052,7 @@ export class CodexRunner {
|
|
|
4672
5052
|
this.appServerClient?.close(),
|
|
4673
5053
|
this.fullAccessAppServerClient?.close(),
|
|
4674
5054
|
]);
|
|
5055
|
+
this.codexSessionFileAdapter.close();
|
|
4675
5056
|
this.appServerClient = null;
|
|
4676
5057
|
this.fullAccessAppServerClient = null;
|
|
4677
5058
|
}
|
|
@@ -4682,8 +5063,6 @@ export class CodexAgentPlugin {
|
|
|
4682
5063
|
isEnabled(agent) {
|
|
4683
5064
|
if (!agent.config.baseagents?.codex)
|
|
4684
5065
|
return false;
|
|
4685
|
-
if (!isCodexAppServerAvailable())
|
|
4686
|
-
return false;
|
|
4687
5066
|
try {
|
|
4688
5067
|
const override = agent.config.baseagents.codex;
|
|
4689
5068
|
const syntheticConfig = { agents: { codex: override } };
|
|
@@ -4694,8 +5073,8 @@ export class CodexAgentPlugin {
|
|
|
4694
5073
|
return false;
|
|
4695
5074
|
}
|
|
4696
5075
|
}
|
|
4697
|
-
createAgent(agent, callbacks) {
|
|
4698
|
-
const availability =
|
|
5076
|
+
async createAgent(agent, callbacks) {
|
|
5077
|
+
const availability = await getCodexAppServerAvailabilityAsync();
|
|
4699
5078
|
if (!availability.available) {
|
|
4700
5079
|
throw new Error(availability.reason || 'Missing codex CLI with app-server');
|
|
4701
5080
|
}
|