evolcore 0.0.23 → 0.0.25
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 +583 -516
- package/LICENSE +21 -21
- package/README.md +455 -453
- package/bin/codex-managed-acl.ps1 +160 -155
- package/bin/codex-managed-hook.mjs +209 -92
- package/bin/ec-safe-output.js +327 -245
- package/bin/ec.js +29 -29
- package/bin/install-codex-managed-hooks.mjs +555 -544
- package/dist/agents/baseagent-availability.js +4 -4
- package/dist/agents/baseagent.js +29 -1
- package/dist/agents/claude-runner.js +152 -14
- 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 +445 -63
- package/dist/agents/ecagent-runner.js +31 -16
- package/dist/agents/gemini-runner.js +82 -4
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/agentmd.js +56 -4
- package/dist/aun/aid/index.js +1 -0
- package/dist/aun/aid/managed-operation.js +80 -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/aun/storage/client.js +4 -1
- package/dist/aun/storage/index.js +1 -1
- 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 +214 -89
- package/dist/cli/agent-create.js +576 -0
- package/dist/cli/agent.js +129 -676
- package/dist/cli/aun-commands.js +419 -313
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +20 -20
- package/dist/cli/contact.js +10 -10
- package/dist/cli/ctl-command.js +22 -20
- package/dist/cli/daemon-commands.js +163 -32
- package/dist/cli/fs-command.js +67 -67
- package/dist/cli/handoff-command.js +7 -7
- package/dist/cli/index.js +79 -79
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +11 -11
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +29 -29
- package/dist/cli/response.js +30 -30
- package/dist/cli/restart-monitor.js +462 -38
- package/dist/cli/stats.js +161 -51
- package/dist/cli/trigger-command.js +56 -56
- package/dist/config/builtin-roles.js +3 -0
- package/dist/config/config-manager.js +15 -0
- package/dist/config/config-operation-service.js +20 -3
- package/dist/config/resolved-config-op.js +39 -1
- 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 +125 -7
- package/dist/core/auth/operation-catalog.js +26 -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 +60 -26
- package/dist/core/command/cli-intent-parser.js +18 -0
- package/dist/core/command/command-handler.js +173 -36
- package/dist/core/command/menu-catalog.js +4 -1
- package/dist/core/command/menu-handler.js +315 -75
- package/dist/core/command/menu-protocol.js +7 -2
- package/dist/core/command/slash-gate.js +1 -3
- package/dist/core/command/slash-handler.js +166 -57
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/evolagent.js +3 -3
- 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 +85 -27
- 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 +56 -10
- 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 +327 -134
- package/dist/ipc.js +68 -21
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/avatar-upload.js +282 -23
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +11 -4
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +24 -1
- package/dist/utils/welcome.js +22 -22
- package/dist/utils/windows-autostart.js +1 -1
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +66 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +56 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +365 -364
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +71 -69
- package/kits/docs/evolcore/agent-create.md +170 -156
- package/kits/docs/evolcore/agent.md +83 -73
- package/kits/docs/evolcore/aid.md +64 -62
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
- package/kits/docs/evolcore/config.md +195 -195
- package/kits/docs/evolcore/contact.md +68 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +243 -243
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +152 -152
- package/kits/docs/evolcore/menu-client.md +34 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +165 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +50 -50
- package/kits/docs/evolcore/trigger.md +575 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +270 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +25 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +195 -183
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +162 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +39 -37
- package/kits/rules/04-relation.md +75 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +63 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +427 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +201 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +96 -82
- package/kits/templates/roles/member.json +97 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +86 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +38 -29
- package/kits/templates/system-fragments/channel.md +64 -64
- package/kits/templates/system-fragments/commands.md +28 -28
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +98 -94
- package/AGENTS.md +0 -62
- package/dist/utils/codex-cli.js +0 -43
- package/skills/eclink/SKILL.md +0 -84
- package/skills/eclink/agents/openai.yaml +0 -4
|
@@ -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);
|
|
@@ -1188,7 +1298,7 @@ export class CodexRunner {
|
|
|
1188
1298
|
}
|
|
1189
1299
|
return Object.fromEntries([...paths].map(candidate => [candidate, 'write']));
|
|
1190
1300
|
}
|
|
1191
|
-
buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath) {
|
|
1301
|
+
buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1192
1302
|
const profileName = this.permissionProfileName(sessionId);
|
|
1193
1303
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
1194
1304
|
const network = {
|
|
@@ -1214,6 +1324,9 @@ export class CodexRunner {
|
|
|
1214
1324
|
...(bootstrapAgentMdPath ? {
|
|
1215
1325
|
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1216
1326
|
} : {}),
|
|
1327
|
+
...(personalMdPath ? {
|
|
1328
|
+
[path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1329
|
+
} : {}),
|
|
1217
1330
|
...(mode !== 'readonly' && managedTempDir ? {
|
|
1218
1331
|
[managedTempDir]: 'write',
|
|
1219
1332
|
[managedTempDir + '/**']: 'write',
|
|
@@ -1226,12 +1339,12 @@ export class CodexRunner {
|
|
|
1226
1339
|
},
|
|
1227
1340
|
};
|
|
1228
1341
|
}
|
|
1229
|
-
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath) {
|
|
1342
|
+
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1230
1343
|
const decision = resolvePhaseOneExecutionSandbox(mode, this.permissionContexts.get(sessionId)?.role);
|
|
1231
1344
|
return {
|
|
1232
1345
|
decision,
|
|
1233
1346
|
permissionProfileConfig: decision.state === 'active'
|
|
1234
|
-
? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath)
|
|
1347
|
+
? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath, personalMdPath)
|
|
1235
1348
|
: undefined,
|
|
1236
1349
|
sandbox: decision.state === 'off' ? 'danger-full-access' : undefined,
|
|
1237
1350
|
};
|
|
@@ -1315,7 +1428,28 @@ export class CodexRunner {
|
|
|
1315
1428
|
}
|
|
1316
1429
|
await appServer.assertManagedHooksAvailable();
|
|
1317
1430
|
}
|
|
1318
|
-
|
|
1431
|
+
/**
|
|
1432
|
+
* Decide whether the managed hook may proceed with a tool call.
|
|
1433
|
+
*
|
|
1434
|
+
* Credential contract (see CODEX_SESSION_TOKEN_ENV):
|
|
1435
|
+
* - sessionToken present and issued by this runner for this thread → the
|
|
1436
|
+
* call belongs to an EvolCore-managed thread; full policy evaluation.
|
|
1437
|
+
* - sessionToken present but unknown/mismatched → forgery or stale state;
|
|
1438
|
+
* fail closed with applicable=true so no other outcome allows it.
|
|
1439
|
+
* - sessionToken absent → unmanaged process; not this runner's concern
|
|
1440
|
+
* (applicable=false; the IPC layer allows such calls outright).
|
|
1441
|
+
*/
|
|
1442
|
+
async evaluatePreToolUse(threadId, toolName, toolInput, signal, callId, sessionToken) {
|
|
1443
|
+
if (typeof sessionToken === 'string' && sessionToken) {
|
|
1444
|
+
if (!this.validateThreadCredential(threadId, sessionToken)) {
|
|
1445
|
+
return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
applicable: true,
|
|
1448
|
+
decision: 'deny',
|
|
1449
|
+
reason: 'Codex session credential was not recognized',
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1319
1453
|
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
1320
1454
|
const activeThread = this.activeSessions.get(sessionKey) ?? this.provisionalSessions.get(sessionKey);
|
|
1321
1455
|
const context = this.permissionContexts.get(sessionKey);
|
|
@@ -1363,6 +1497,16 @@ export class CodexRunner {
|
|
|
1363
1497
|
}
|
|
1364
1498
|
};
|
|
1365
1499
|
if (!activeThread || !this.isThreadOwnedBySession(sessionKey, threadId)) {
|
|
1500
|
+
if (typeof sessionToken === 'string' && sessionToken) {
|
|
1501
|
+
// A credentialed thread this runner cannot place (e.g. resumed state
|
|
1502
|
+
// lost across a runner reload) must never fall through to allow.
|
|
1503
|
+
return {
|
|
1504
|
+
ok: false,
|
|
1505
|
+
applicable: true,
|
|
1506
|
+
decision: 'deny',
|
|
1507
|
+
reason: 'EvolCore-managed Codex thread is not tracked by an active session',
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1366
1510
|
return { ok: true, applicable: false };
|
|
1367
1511
|
}
|
|
1368
1512
|
const proactiveSession = context?.chatmode === 'proactive';
|
|
@@ -1603,7 +1747,7 @@ export class CodexRunner {
|
|
|
1603
1747
|
}
|
|
1604
1748
|
}
|
|
1605
1749
|
let resumingThread = !!agentSessionId;
|
|
1606
|
-
const callModel = modelOverride?.model || this.model;
|
|
1750
|
+
const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
1607
1751
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
1608
1752
|
? undefined
|
|
1609
1753
|
: modelOverride?.effort ?? this.effort;
|
|
@@ -1617,7 +1761,7 @@ export class CodexRunner {
|
|
|
1617
1761
|
}
|
|
1618
1762
|
this.chatModes.set(sessionId, callMode);
|
|
1619
1763
|
const callApprovalPolicy = this.toApprovalPolicy(callMode);
|
|
1620
|
-
const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath);
|
|
1764
|
+
const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath, modelOverride?.personalMdPath);
|
|
1621
1765
|
const appServer = this.getAppServerClient(callMode === 'fullaccess');
|
|
1622
1766
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1623
1767
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
@@ -1647,7 +1791,7 @@ export class CodexRunner {
|
|
|
1647
1791
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1648
1792
|
approvalsReviewer: 'user',
|
|
1649
1793
|
...(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),
|
|
1794
|
+
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
1795
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1652
1796
|
};
|
|
1653
1797
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
@@ -1690,6 +1834,7 @@ export class CodexRunner {
|
|
|
1690
1834
|
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1691
1835
|
this.unbindExternalToolBoundary(missingThreadId);
|
|
1692
1836
|
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1837
|
+
this.dropThreadCredential(missingThreadId);
|
|
1693
1838
|
agentSessionId = undefined;
|
|
1694
1839
|
resumingThread = false;
|
|
1695
1840
|
knownDelegationCarrier = undefined;
|
|
@@ -1795,6 +1940,7 @@ export class CodexRunner {
|
|
|
1795
1940
|
projectPath,
|
|
1796
1941
|
model: callModel,
|
|
1797
1942
|
committed: resumingThread,
|
|
1943
|
+
persistenceFinalized: resumingThread,
|
|
1798
1944
|
streamedAgentMessageIds: new Set(),
|
|
1799
1945
|
agentMessageDeltaText: new Map(),
|
|
1800
1946
|
completedItemIds: new Set(),
|
|
@@ -1834,8 +1980,9 @@ export class CodexRunner {
|
|
|
1834
1980
|
this.unregisterChildThread(notifThreadId);
|
|
1835
1981
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1836
1982
|
}
|
|
1837
|
-
if (this.isSubagentActivityItem(item))
|
|
1983
|
+
if (this.isSubagentActivityItem(item) || isCodexReasoningNotification(notification.method)) {
|
|
1838
1984
|
queue.push(notification);
|
|
1985
|
+
}
|
|
1839
1986
|
if (notification.method === 'item/completed' && state.turnCompleted) {
|
|
1840
1987
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1841
1988
|
}
|
|
@@ -1882,11 +2029,43 @@ export class CodexRunner {
|
|
|
1882
2029
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1883
2030
|
});
|
|
1884
2031
|
if (!resumingThread) {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
if (committed)
|
|
1888
|
-
|
|
1889
|
-
|
|
2032
|
+
let confirmationInFlight;
|
|
2033
|
+
state.confirmProvisional = (waitMs = 0) => {
|
|
2034
|
+
if (state.committed)
|
|
2035
|
+
return Promise.resolve(true);
|
|
2036
|
+
if (state.persistenceFinalized)
|
|
2037
|
+
return Promise.resolve(false);
|
|
2038
|
+
if (confirmationInFlight)
|
|
2039
|
+
return confirmationInFlight;
|
|
2040
|
+
const providerTurnId = state.turnId;
|
|
2041
|
+
if (!providerTurnId)
|
|
2042
|
+
return Promise.resolve(false);
|
|
2043
|
+
const confirmation = (async () => {
|
|
2044
|
+
const persisted = await this.waitForCodexTurnPersistence(sessionManager, projectPath, threadId, providerTurnId, waitMs);
|
|
2045
|
+
if (!persisted)
|
|
2046
|
+
return false;
|
|
2047
|
+
try {
|
|
2048
|
+
const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
|
|
2049
|
+
if (!committed) {
|
|
2050
|
+
const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
|
|
2051
|
+
staleError.code = 'RUNNER_START_SUPERSEDED';
|
|
2052
|
+
throw staleError;
|
|
2053
|
+
}
|
|
2054
|
+
agentSessionId = threadId;
|
|
2055
|
+
state.committed = true;
|
|
2056
|
+
return true;
|
|
2057
|
+
}
|
|
2058
|
+
catch (error) {
|
|
2059
|
+
state.persistenceFinalized = true;
|
|
2060
|
+
throw error;
|
|
2061
|
+
}
|
|
2062
|
+
})();
|
|
2063
|
+
confirmationInFlight = confirmation;
|
|
2064
|
+
void confirmation.finally(() => {
|
|
2065
|
+
if (confirmationInFlight === confirmation)
|
|
2066
|
+
confirmationInFlight = undefined;
|
|
2067
|
+
}).catch(() => undefined);
|
|
2068
|
+
return confirmation;
|
|
1890
2069
|
};
|
|
1891
2070
|
}
|
|
1892
2071
|
const turnId = turnResponse.turn?.id;
|
|
@@ -1894,6 +2073,12 @@ export class CodexRunner {
|
|
|
1894
2073
|
state.turnId = turnId;
|
|
1895
2074
|
this.activeTurns.set(sessionId, { threadId, turnId });
|
|
1896
2075
|
}
|
|
2076
|
+
if (!resumingThread && turnId) {
|
|
2077
|
+
// Check immediately, but leave a just-created rollout as a candidate.
|
|
2078
|
+
// Subsequent notifications recheck it and the terminal boundary gets
|
|
2079
|
+
// the short settle window below.
|
|
2080
|
+
await state.confirmProvisional?.(0);
|
|
2081
|
+
}
|
|
1897
2082
|
if (turnId && this.consumePendingInterrupt(sessionId)) {
|
|
1898
2083
|
await this.interruptAppServerTurn(threadId, turnId);
|
|
1899
2084
|
controller.abort('User interrupt');
|
|
@@ -2042,11 +2227,16 @@ export class CodexRunner {
|
|
|
2042
2227
|
}
|
|
2043
2228
|
capturedController?.abort('Session closed');
|
|
2044
2229
|
const capturedSubscription = capturedThreadId ? this.threadSubscriptions.get(capturedThreadId) : undefined;
|
|
2230
|
+
const currentActiveThreadId = this.activeSessions.get(sessionId);
|
|
2231
|
+
const currentProvisionalThreadId = this.provisionalSessions.get(sessionId);
|
|
2232
|
+
const capturedThreadIds = new Set([capturedThreadId, capturedProvisionalThreadId].filter((value) => value !== undefined));
|
|
2233
|
+
const threadLifecycleReplaced = (currentActiveThreadId !== undefined && !capturedThreadIds.has(currentActiveThreadId))
|
|
2234
|
+
|| (currentProvisionalThreadId !== undefined && !capturedThreadIds.has(currentProvisionalThreadId));
|
|
2045
2235
|
const lifecycleReplaced = (capturedStream !== undefined && this.activeStreams.get(sessionId) !== capturedStream)
|
|
2046
2236
|
|| (capturedController !== undefined && this.activeAbortControllers.get(sessionId) !== capturedController)
|
|
2047
2237
|
|| (capturedTurn !== undefined && this.activeTurns.get(sessionId) !== capturedTurn)
|
|
2048
2238
|
|| (capturedPermissionContext !== undefined && this.permissionContexts.get(sessionId) !== capturedPermissionContext)
|
|
2049
|
-
||
|
|
2239
|
+
|| threadLifecycleReplaced;
|
|
2050
2240
|
if (capturedSubscription && !lifecycleReplaced
|
|
2051
2241
|
&& this.threadSubscriptions.get(capturedThreadId) === capturedSubscription) {
|
|
2052
2242
|
await this.releaseThreadSubscription(capturedSubscription.client, capturedThreadId, 'session-closed');
|
|
@@ -2083,7 +2273,7 @@ export class CodexRunner {
|
|
|
2083
2273
|
}
|
|
2084
2274
|
}
|
|
2085
2275
|
resolveSessionFile(agentSessionId, _projectPath) {
|
|
2086
|
-
// Codex
|
|
2276
|
+
// Codex sessions are shared with the local Codex Desktop installation.
|
|
2087
2277
|
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
2088
2278
|
if (!fs.existsSync(sessionsDir))
|
|
2089
2279
|
return null;
|
|
@@ -2145,7 +2335,7 @@ export class CodexRunner {
|
|
|
2145
2335
|
if (!this.isThreadNotFoundError(error))
|
|
2146
2336
|
throw error;
|
|
2147
2337
|
logger.info(`[CodexRunner] Compact thread not loaded, resuming before compact: ${agentSessionId}`);
|
|
2148
|
-
const compactModel = modelOverride?.model || this.model;
|
|
2338
|
+
const compactModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
2149
2339
|
const compactEffort = modelOverride?.effortMode === 'model_default'
|
|
2150
2340
|
? undefined
|
|
2151
2341
|
: modelOverride?.effort ?? this.effort;
|
|
@@ -2165,7 +2355,7 @@ export class CodexRunner {
|
|
|
2165
2355
|
approvalPolicy: compactPolicy,
|
|
2166
2356
|
approvalsReviewer: 'user',
|
|
2167
2357
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2168
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
2358
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier, undefined, agentSessionId), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
2169
2359
|
});
|
|
2170
2360
|
this.markThreadSubscribed(appServer, agentSessionId);
|
|
2171
2361
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
@@ -2274,10 +2464,14 @@ export class CodexRunner {
|
|
|
2274
2464
|
}
|
|
2275
2465
|
return false;
|
|
2276
2466
|
}
|
|
2467
|
+
codexHomeDir() {
|
|
2468
|
+
return process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex');
|
|
2469
|
+
}
|
|
2277
2470
|
findCodexSessionFile(threadId) {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2471
|
+
return this.findRolloutFile(path.join(this.codexHomeDir(), 'sessions'), threadId)
|
|
2472
|
+
?? this.findRolloutFile(path.join(this.codexHomeDir(), 'archived_sessions'), threadId);
|
|
2473
|
+
}
|
|
2474
|
+
findRolloutFile(root, threadId) {
|
|
2281
2475
|
const stack = [root];
|
|
2282
2476
|
let newest;
|
|
2283
2477
|
while (stack.length > 0) {
|
|
@@ -2309,6 +2503,101 @@ export class CodexRunner {
|
|
|
2309
2503
|
}
|
|
2310
2504
|
return newest?.path;
|
|
2311
2505
|
}
|
|
2506
|
+
// ── Managed-hook thread credentials ─────────────────────────────────
|
|
2507
|
+
// The Windows managed hook is machine-wide, so unmanaged Codex processes
|
|
2508
|
+
// also reach it. Credentials let the hook tell EvolCore-owned threads
|
|
2509
|
+
// (policy enforced) apart from unmanaged ones (allowed unconditionally).
|
|
2510
|
+
// They follow the thread (not the daemon process) so a daemon restart does
|
|
2511
|
+
// not strand a resumable thread: the token file is re-read on resume, and
|
|
2512
|
+
// it is removed lazily once the rollout file itself disappears.
|
|
2513
|
+
threadCredentialsDir() {
|
|
2514
|
+
const aid = this.resolvedConfig.agentAid;
|
|
2515
|
+
if (!aid)
|
|
2516
|
+
return undefined;
|
|
2517
|
+
return path.join(agentSessionsDir(aid), 'codex-tokens');
|
|
2518
|
+
}
|
|
2519
|
+
threadCredentialPath(threadId) {
|
|
2520
|
+
const dir = this.threadCredentialsDir();
|
|
2521
|
+
return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
|
|
2522
|
+
}
|
|
2523
|
+
/** Issue or re-read this agent's credential for a thread. */
|
|
2524
|
+
ensureThreadCredential(threadId) {
|
|
2525
|
+
const cached = this.threadCredentials.get(threadId);
|
|
2526
|
+
if (cached)
|
|
2527
|
+
return cached;
|
|
2528
|
+
const file = this.threadCredentialPath(threadId);
|
|
2529
|
+
if (!file)
|
|
2530
|
+
return undefined;
|
|
2531
|
+
try {
|
|
2532
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2533
|
+
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2534
|
+
this.threadCredentials.set(threadId, record.token);
|
|
2535
|
+
return record.token;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
catch { /* missing or unreadable: fall through to issuance */ }
|
|
2539
|
+
const token = randomBytes(32).toString('hex');
|
|
2540
|
+
const record = {
|
|
2541
|
+
token,
|
|
2542
|
+
threadId,
|
|
2543
|
+
createdAt: new Date().toISOString(),
|
|
2544
|
+
rolloutPath: this.findCodexSessionFile(threadId),
|
|
2545
|
+
};
|
|
2546
|
+
try {
|
|
2547
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
2548
|
+
fs.writeFileSync(file, JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
2549
|
+
this.threadCredentials.set(threadId, token);
|
|
2550
|
+
return token;
|
|
2551
|
+
}
|
|
2552
|
+
catch (error) {
|
|
2553
|
+
logger.warn(`[CodexRunner] failed to persist thread credential: thread=${threadId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
2554
|
+
return undefined;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Validate a credential presented by the managed hook. Returns the matched
|
|
2559
|
+
* threadId when this runner issued the token; undefined otherwise. Token
|
|
2560
|
+
* records whose rollout file has disappeared are pruned lazily.
|
|
2561
|
+
*/
|
|
2562
|
+
validateThreadCredential(threadId, token) {
|
|
2563
|
+
const expected = this.ensureThreadCredentialLoaded(threadId);
|
|
2564
|
+
if (!expected || expected !== token)
|
|
2565
|
+
return false;
|
|
2566
|
+
const rollout = this.findCodexSessionFile(threadId);
|
|
2567
|
+
if (!rollout) {
|
|
2568
|
+
// The thread's rollout is gone: nothing left to protect, drop the record.
|
|
2569
|
+
this.dropThreadCredential(threadId);
|
|
2570
|
+
return false;
|
|
2571
|
+
}
|
|
2572
|
+
return true;
|
|
2573
|
+
}
|
|
2574
|
+
ensureThreadCredentialLoaded(threadId) {
|
|
2575
|
+
const cached = this.threadCredentials.get(threadId);
|
|
2576
|
+
if (cached)
|
|
2577
|
+
return cached;
|
|
2578
|
+
const file = this.threadCredentialPath(threadId);
|
|
2579
|
+
if (!file)
|
|
2580
|
+
return undefined;
|
|
2581
|
+
try {
|
|
2582
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2583
|
+
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2584
|
+
this.threadCredentials.set(threadId, record.token);
|
|
2585
|
+
return record.token;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
catch { /* absent or unreadable */ }
|
|
2589
|
+
return undefined;
|
|
2590
|
+
}
|
|
2591
|
+
dropThreadCredential(threadId) {
|
|
2592
|
+
this.threadCredentials.delete(threadId);
|
|
2593
|
+
const file = this.threadCredentialPath(threadId);
|
|
2594
|
+
if (!file)
|
|
2595
|
+
return;
|
|
2596
|
+
try {
|
|
2597
|
+
fs.rmSync(file, { force: true });
|
|
2598
|
+
}
|
|
2599
|
+
catch { /* best effort */ }
|
|
2600
|
+
}
|
|
2312
2601
|
async forkSession(agentSessionId, projectPath, title) {
|
|
2313
2602
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
|
|
2314
2603
|
try {
|
|
@@ -2597,6 +2886,31 @@ export class CodexRunner {
|
|
|
2597
2886
|
? 'FileChange'
|
|
2598
2887
|
: 'Bash';
|
|
2599
2888
|
const toolInput = this.buildPermissionInput(request.method, params);
|
|
2889
|
+
const approvalCallId = typeof (params.itemId ?? params.item_id ?? params.callId ?? params.call_id) === 'string'
|
|
2890
|
+
? String(params.itemId ?? params.item_id ?? params.callId ?? params.call_id)
|
|
2891
|
+
: undefined;
|
|
2892
|
+
const approvalRequestId = request.id !== undefined
|
|
2893
|
+
? String(request.id)
|
|
2894
|
+
: approvalCallId ?? `codex-approval-${crypto.randomUUID()}`;
|
|
2895
|
+
this.approvalLifecycleIds.set(request, approvalRequestId);
|
|
2896
|
+
const approvalContext = this.permissionContexts.get(sessionKey);
|
|
2897
|
+
auditPermissionLifecycle({
|
|
2898
|
+
phase: 'request',
|
|
2899
|
+
requestId: approvalRequestId,
|
|
2900
|
+
callId: approvalCallId,
|
|
2901
|
+
correlationId: approvalCallId,
|
|
2902
|
+
sessionId: sessionKey,
|
|
2903
|
+
agentAid: approvalContext?.selfAid,
|
|
2904
|
+
agentName: approvalContext?.agentName,
|
|
2905
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
2906
|
+
toolName,
|
|
2907
|
+
decision: 'pending',
|
|
2908
|
+
decisionSource: 'approval',
|
|
2909
|
+
role: approvalContext?.role,
|
|
2910
|
+
taskId: approvalContext?.taskId,
|
|
2911
|
+
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
2912
|
+
approvalMethod: request.method,
|
|
2913
|
+
});
|
|
2600
2914
|
// A trusted fullaccess run has already been explicitly authorized by the
|
|
2601
2915
|
// daemon owner. Do not route its approvals through ordinary capability,
|
|
2602
2916
|
// workspace, or unattended-trigger policy checks.
|
|
@@ -2834,6 +3148,49 @@ export class CodexRunner {
|
|
|
2834
3148
|
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
2835
3149
|
parseFailure,
|
|
2836
3150
|
});
|
|
3151
|
+
const lifecycleRequestId = request.id !== undefined
|
|
3152
|
+
? String(request.id)
|
|
3153
|
+
: this.approvalLifecycleIds.get(request)
|
|
3154
|
+
?? callId
|
|
3155
|
+
?? `codex-approval-${crypto.randomUUID()}`;
|
|
3156
|
+
auditPermissionLifecycle({
|
|
3157
|
+
phase: 'response',
|
|
3158
|
+
requestId: lifecycleRequestId,
|
|
3159
|
+
callId,
|
|
3160
|
+
correlationId: callId,
|
|
3161
|
+
sessionId: sessionKey,
|
|
3162
|
+
agentAid: context?.selfAid,
|
|
3163
|
+
agentName: context?.agentName,
|
|
3164
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
3165
|
+
toolName,
|
|
3166
|
+
decision,
|
|
3167
|
+
decisionSource,
|
|
3168
|
+
policyCode,
|
|
3169
|
+
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
3170
|
+
role: context?.role,
|
|
3171
|
+
taskId: context?.taskId,
|
|
3172
|
+
summary: auditSummary,
|
|
3173
|
+
approvalMethod: request.method,
|
|
3174
|
+
});
|
|
3175
|
+
auditPermissionLifecycle({
|
|
3176
|
+
phase: 'final',
|
|
3177
|
+
requestId: lifecycleRequestId,
|
|
3178
|
+
callId,
|
|
3179
|
+
correlationId: callId,
|
|
3180
|
+
sessionId: sessionKey,
|
|
3181
|
+
agentAid: context?.selfAid,
|
|
3182
|
+
agentName: context?.agentName,
|
|
3183
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
3184
|
+
toolName,
|
|
3185
|
+
decision,
|
|
3186
|
+
decisionSource,
|
|
3187
|
+
policyCode,
|
|
3188
|
+
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
3189
|
+
role: context?.role,
|
|
3190
|
+
taskId: context?.taskId,
|
|
3191
|
+
summary: auditSummary,
|
|
3192
|
+
approvalMethod: request.method,
|
|
3193
|
+
});
|
|
2837
3194
|
logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
|
|
2838
3195
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2839
3196
|
...(callId ? { callId, correlationId: callId } : {}),
|
|
@@ -3924,26 +4281,37 @@ export class CodexRunner {
|
|
|
3924
4281
|
for await (const notification of notifications) {
|
|
3925
4282
|
if (!this.activeAbortControllers.has(sessionId))
|
|
3926
4283
|
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
|
-
}
|
|
4284
|
+
if (!state.committed && !state.persistenceFinalized) {
|
|
4285
|
+
const terminal = notification.method === 'turn/completed';
|
|
4286
|
+
const committed = await state.confirmProvisional?.(terminal ? 250 : 0) ?? false;
|
|
4287
|
+
if (committed && !sessionIdEmitted) {
|
|
4288
|
+
sessionIdEmitted = true;
|
|
4289
|
+
yield { type: 'session_id', sessionId: state.threadId };
|
|
4290
|
+
}
|
|
4291
|
+
else if (terminal && !committed) {
|
|
4292
|
+
state.persistenceFinalized = true;
|
|
4293
|
+
const persistenceError = new Error(`Codex turn completed before its rollout became recoverable: thread=${state.threadId} turn=${state.turnId ?? '<unknown>'}`);
|
|
4294
|
+
persistenceError.code = 'CODEX_TURN_PERSISTENCE_UNCONFIRMED';
|
|
4295
|
+
throw persistenceError;
|
|
3941
4296
|
}
|
|
3942
4297
|
}
|
|
3943
4298
|
yield* this.mapAppServerNotification(notification, sessionId, state);
|
|
3944
4299
|
}
|
|
3945
4300
|
}
|
|
3946
4301
|
finally {
|
|
4302
|
+
if (!state.committed && !state.persistenceFinalized && state.confirmProvisional) {
|
|
4303
|
+
try {
|
|
4304
|
+
// Do not let an interrupt wait for persistence that did not exist at
|
|
4305
|
+
// cancellation time. A zero-wait check still captures a rollout
|
|
4306
|
+
// that was already durable before the stream was closed.
|
|
4307
|
+
await state.confirmProvisional(0);
|
|
4308
|
+
}
|
|
4309
|
+
catch (error) {
|
|
4310
|
+
logger.debug(`[CodexRunner] provisional thread rejected during final persistence check: `
|
|
4311
|
+
+ `session=${sessionId} thread=${state.threadId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4312
|
+
}
|
|
4313
|
+
state.persistenceFinalized = true;
|
|
4314
|
+
}
|
|
3947
4315
|
unsubscribe();
|
|
3948
4316
|
await this.releaseThreadSubscription(appServer, state.threadId, 'turn-finished');
|
|
3949
4317
|
if (this.activeAbortControllers.get(sessionId) === controller) {
|
|
@@ -3990,6 +4358,15 @@ export class CodexRunner {
|
|
|
3990
4358
|
}
|
|
3991
4359
|
break;
|
|
3992
4360
|
}
|
|
4361
|
+
// Reasoning notifications are provider-internal liveness signals.
|
|
4362
|
+
// Do not expose their content; the ResponseEngine consumes the heartbeat
|
|
4363
|
+
// solely to reset the idle monitor.
|
|
4364
|
+
case 'item/reasoning/textDelta':
|
|
4365
|
+
case 'item/reasoning/summaryTextDelta':
|
|
4366
|
+
case 'item/reasoning/summaryPartAdded': {
|
|
4367
|
+
yield { type: 'heartbeat', source: 'reasoning' };
|
|
4368
|
+
break;
|
|
4369
|
+
}
|
|
3993
4370
|
case 'item/completed': {
|
|
3994
4371
|
const item = params.item;
|
|
3995
4372
|
if (item?.id)
|
|
@@ -4601,12 +4978,17 @@ export class CodexRunner {
|
|
|
4601
4978
|
}
|
|
4602
4979
|
}
|
|
4603
4980
|
managedTempInstruction(sessionId) {
|
|
4604
|
-
|
|
4981
|
+
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
4982
|
+
if (!managedTempDir)
|
|
4605
4983
|
return undefined;
|
|
4606
|
-
return
|
|
4984
|
+
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 \`/tmp/evolcore-codex-*\` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
|
|
4607
4985
|
}
|
|
4608
|
-
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken) {
|
|
4986
|
+
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken, threadId) {
|
|
4609
4987
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
4988
|
+
// The managed-hook credential rides the thread's shell environment so the
|
|
4989
|
+
// PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
|
|
4990
|
+
// It follows the thread (start AND resume) rather than the daemon process.
|
|
4991
|
+
const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
|
|
4610
4992
|
return {
|
|
4611
4993
|
shell_environment_policy: {
|
|
4612
4994
|
set: {
|
|
@@ -4617,6 +4999,7 @@ export class CodexRunner {
|
|
|
4617
4999
|
...(taskDelegationToken || delegationCarrier
|
|
4618
5000
|
? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
|
|
4619
5001
|
: {}),
|
|
5002
|
+
...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
|
|
4620
5003
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
4621
5004
|
},
|
|
4622
5005
|
},
|
|
@@ -4672,6 +5055,7 @@ export class CodexRunner {
|
|
|
4672
5055
|
this.appServerClient?.close(),
|
|
4673
5056
|
this.fullAccessAppServerClient?.close(),
|
|
4674
5057
|
]);
|
|
5058
|
+
this.codexSessionFileAdapter.close();
|
|
4675
5059
|
this.appServerClient = null;
|
|
4676
5060
|
this.fullAccessAppServerClient = null;
|
|
4677
5061
|
}
|
|
@@ -4682,8 +5066,6 @@ export class CodexAgentPlugin {
|
|
|
4682
5066
|
isEnabled(agent) {
|
|
4683
5067
|
if (!agent.config.baseagents?.codex)
|
|
4684
5068
|
return false;
|
|
4685
|
-
if (!isCodexAppServerAvailable())
|
|
4686
|
-
return false;
|
|
4687
5069
|
try {
|
|
4688
5070
|
const override = agent.config.baseagents.codex;
|
|
4689
5071
|
const syntheticConfig = { agents: { codex: override } };
|
|
@@ -4694,8 +5076,8 @@ export class CodexAgentPlugin {
|
|
|
4694
5076
|
return false;
|
|
4695
5077
|
}
|
|
4696
5078
|
}
|
|
4697
|
-
createAgent(agent, callbacks) {
|
|
4698
|
-
const availability =
|
|
5079
|
+
async createAgent(agent, callbacks) {
|
|
5080
|
+
const availability = await getCodexAppServerAvailabilityAsync();
|
|
4699
5081
|
if (!availability.available) {
|
|
4700
5082
|
throw new Error(availability.reason || 'Missing codex CLI with app-server');
|
|
4701
5083
|
}
|