evolcore 0.0.26 → 0.0.28
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 +36 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +70 -26
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +363 -421
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +13 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +544 -239
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +111 -162
- package/dist/cli/diag.js +3 -3
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +13 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +81 -19
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +105 -40
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +292 -27
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +364 -96
- package/dist/core/command/menu-protocol.js +43 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +61 -37
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +94 -16
- package/dist/core/message/message-bridge.js +140 -13
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +729 -118
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +76 -36
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +8 -8
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +38 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +322 -207
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +3 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/dist/utils/windows-shell-trust.js +37 -0
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +23 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
2
2
|
// Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
|
|
3
3
|
import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
|
|
4
|
-
import { hasTrustedFullAccessContext } from './runner-types.js';
|
|
4
|
+
import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQuestions, unsettledInterruptError, withInterruptDeadline } from './runner-types.js';
|
|
5
5
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
6
6
|
import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
7
7
|
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
8
|
-
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
|
|
8
|
+
import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
|
|
9
9
|
import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
|
|
10
|
-
import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
|
|
10
|
+
import { resolveOpenaiConfig, resolveCodexCliPath, resolveCodexPowerShellShimPath, } from './baseagent.js';
|
|
11
11
|
import { logger } from '../utils/logger.js';
|
|
12
12
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
13
13
|
import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
@@ -15,12 +15,12 @@ import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } f
|
|
|
15
15
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
16
16
|
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
17
17
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
18
|
-
import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
|
|
18
|
+
import { AGENT_DELEGATION_TOKEN_ENV, CODEX_SESSION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
|
|
19
19
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
20
|
-
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent,
|
|
20
|
+
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
|
|
21
21
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
22
22
|
import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
|
|
23
|
-
import {
|
|
23
|
+
import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
|
|
24
24
|
import { buildSessionTurnList } from '../core/session/session-turns.js';
|
|
25
25
|
import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
|
|
26
26
|
import { execFileSync } from 'child_process';
|
|
@@ -38,6 +38,7 @@ const MIME_EXT = {
|
|
|
38
38
|
'image/webp': '.webp',
|
|
39
39
|
};
|
|
40
40
|
const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
|
|
41
|
+
const CODEX_INTERRUPT_TIMEOUT_MS = 5_000;
|
|
41
42
|
function toAgentErrorType(message) {
|
|
42
43
|
switch (classifyError(new Error(message))) {
|
|
43
44
|
case ErrorType.CONTEXT_TOO_LONG: return 'context_too_long';
|
|
@@ -108,60 +109,6 @@ function isCodexReasoningNotification(method) {
|
|
|
108
109
|
|| method === 'item/reasoning/summaryPartAdded';
|
|
109
110
|
}
|
|
110
111
|
const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
|
|
111
|
-
const managedTempDirsForExitCleanup = new Map();
|
|
112
|
-
let managedTempExitCleanupRegistered = false;
|
|
113
|
-
function isTrustedManagedTempDirectory(directory, parent) {
|
|
114
|
-
try {
|
|
115
|
-
const parentPath = path.resolve(parent);
|
|
116
|
-
const directoryPath = path.resolve(directory);
|
|
117
|
-
// The session directory must be a direct child of the private parent.
|
|
118
|
-
// This lexical check is required before canonicalization so a replaced
|
|
119
|
-
// parent symlink cannot turn an old session path into another tree.
|
|
120
|
-
if (path.dirname(directoryPath) !== parentPath)
|
|
121
|
-
return false;
|
|
122
|
-
const trustedDirectory = (candidate) => {
|
|
123
|
-
const stat = fs.lstatSync(candidate);
|
|
124
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
125
|
-
return false;
|
|
126
|
-
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
127
|
-
return false;
|
|
128
|
-
return process.platform === 'win32' || (stat.mode & 0o077) === 0;
|
|
129
|
-
};
|
|
130
|
-
if (!trustedDirectory(parentPath) || !trustedDirectory(directoryPath))
|
|
131
|
-
return false;
|
|
132
|
-
const canonicalParent = resolveProtectedCandidate(parentPath);
|
|
133
|
-
const canonicalDirectory = resolveProtectedCandidate(directoryPath);
|
|
134
|
-
// Keep the canonical and lexical paths identical after the lstat checks;
|
|
135
|
-
// this rejects symlink replacement in either node or an existing ancestor.
|
|
136
|
-
return canonicalParent === parentPath
|
|
137
|
-
&& canonicalDirectory === directoryPath
|
|
138
|
-
&& canonicalDirectory !== canonicalParent
|
|
139
|
-
&& path.dirname(canonicalDirectory) === canonicalParent
|
|
140
|
-
&& isSameOrDescendant(canonicalDirectory, canonicalParent);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function registerManagedTempDirForExitCleanup(directory, parent) {
|
|
147
|
-
managedTempDirsForExitCleanup.set(directory, parent);
|
|
148
|
-
if (managedTempExitCleanupRegistered)
|
|
149
|
-
return;
|
|
150
|
-
managedTempExitCleanupRegistered = true;
|
|
151
|
-
process.once('exit', () => {
|
|
152
|
-
for (const [candidatePath, parentPath] of managedTempDirsForExitCleanup) {
|
|
153
|
-
try {
|
|
154
|
-
if (isTrustedManagedTempDirectory(candidatePath, parentPath)) {
|
|
155
|
-
fs.rmSync(path.resolve(candidatePath), { recursive: true, force: true });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Process exit cleanup is best effort; session cleanup logs failures
|
|
160
|
-
// while the runner is still active.
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
112
|
function stableSecurityValue(value) {
|
|
166
113
|
if (value === undefined)
|
|
167
114
|
return 'undefined';
|
|
@@ -234,7 +181,6 @@ function outputTextForManagedHook(value) {
|
|
|
234
181
|
}
|
|
235
182
|
return String(value);
|
|
236
183
|
}
|
|
237
|
-
const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
238
184
|
const CODEX_NO_ROLLOUT_RE = /no rollout found for thread id/i;
|
|
239
185
|
const CODEX_ACTIVE_WRITER_RE = /already has an active writer/i;
|
|
240
186
|
function isCodexNoRolloutError(error) {
|
|
@@ -266,10 +212,13 @@ function compactCodexProbeText(value, maxLength = 600) {
|
|
|
266
212
|
const text = value.replace(/\s+/gu, ' ').trim();
|
|
267
213
|
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
|
|
268
214
|
}
|
|
269
|
-
function logCodexProbeFailure(args, startedAt, error, timeoutMs) {
|
|
215
|
+
function logCodexProbeFailure(args, startedAt, error, timeoutMs, launch) {
|
|
270
216
|
const detail = error;
|
|
271
217
|
logger.warn(`[CodexProbe] CLI probe failed: ${JSON.stringify({
|
|
272
218
|
command: ['codex', ...args],
|
|
219
|
+
resolvedPath: launch?.resolvedPath ?? null,
|
|
220
|
+
launcherKind: launch?.launcherKind ?? null,
|
|
221
|
+
launcherPath: launch?.launcherPath ?? null,
|
|
273
222
|
path: detail?.path ?? null,
|
|
274
223
|
spawnargs: detail?.spawnargs ?? null,
|
|
275
224
|
elapsedMs: Date.now() - startedAt,
|
|
@@ -283,13 +232,13 @@ function logCodexProbeFailure(args, startedAt, error, timeoutMs) {
|
|
|
283
232
|
stderr: compactCodexProbeText(detail?.stderr),
|
|
284
233
|
})}`);
|
|
285
234
|
}
|
|
286
|
-
function probeCodexCli(args) {
|
|
287
|
-
return probeCliSync('codex', args, { onFailure: logCodexProbeFailure });
|
|
235
|
+
function probeCodexCli(args, executable) {
|
|
236
|
+
return probeCliSync('codex', args, { executable, onFailure: logCodexProbeFailure });
|
|
288
237
|
}
|
|
289
|
-
export function getCodexCliVersion() {
|
|
238
|
+
export function getCodexCliVersion(executable) {
|
|
290
239
|
const startedAt = Date.now();
|
|
291
240
|
try {
|
|
292
|
-
const output = probeCodexCli(['--version']);
|
|
241
|
+
const output = probeCodexCli(['--version'], executable);
|
|
293
242
|
const version = parseCodexCliVersion(output);
|
|
294
243
|
if (!version) {
|
|
295
244
|
logCodexProbeFailure(['--version'], startedAt, {
|
|
@@ -308,9 +257,15 @@ export function getCodexCliVersion() {
|
|
|
308
257
|
export function getCodexAppServerAvailability() {
|
|
309
258
|
const cliPath = resolveCodexCliPath();
|
|
310
259
|
if (!cliPath) {
|
|
311
|
-
|
|
260
|
+
const powershellShim = resolveCodexPowerShellShimPath();
|
|
261
|
+
return {
|
|
262
|
+
available: false,
|
|
263
|
+
reason: powershellShim
|
|
264
|
+
? '检测到 codex.ps1,但当前 Windows 启动路径要求 codex.exe/codex.cmd/codex.bat;请重新安装 @openai/codex 以恢复 npm shim。'
|
|
265
|
+
: '未检测到 Codex CLI。',
|
|
266
|
+
};
|
|
312
267
|
}
|
|
313
|
-
const version = getCodexCliVersion();
|
|
268
|
+
const version = getCodexCliVersion(cliPath);
|
|
314
269
|
if (!version) {
|
|
315
270
|
return { available: false, reason: '无法读取 Codex CLI 版本。' };
|
|
316
271
|
}
|
|
@@ -323,7 +278,7 @@ export function getCodexAppServerAvailability() {
|
|
|
323
278
|
};
|
|
324
279
|
}
|
|
325
280
|
try {
|
|
326
|
-
probeCodexCli(['app-server', '--help']);
|
|
281
|
+
probeCodexCli(['app-server', '--help'], cliPath);
|
|
327
282
|
return { available: true, version };
|
|
328
283
|
}
|
|
329
284
|
catch (error) {
|
|
@@ -338,10 +293,21 @@ export function isCodexAppServerAvailable() {
|
|
|
338
293
|
}
|
|
339
294
|
/** Daemon startup/hot reload use asynchronous probes; sync CLI callers share the cache. */
|
|
340
295
|
export async function getCodexAppServerAvailabilityAsync() {
|
|
296
|
+
const cliPath = resolveCodexCliPath();
|
|
341
297
|
const options = {
|
|
342
298
|
timeouts: [CODEX_CLI_PROBE_TIMEOUT_MS, CODEX_CLI_PROBE_RETRY_TIMEOUT_MS],
|
|
299
|
+
executable: cliPath ?? undefined,
|
|
343
300
|
onFailure: logCodexProbeFailure,
|
|
344
301
|
};
|
|
302
|
+
if (!cliPath) {
|
|
303
|
+
const powershellShim = resolveCodexPowerShellShimPath();
|
|
304
|
+
return {
|
|
305
|
+
available: false,
|
|
306
|
+
reason: powershellShim
|
|
307
|
+
? '检测到 codex.ps1,但当前 Windows 启动路径要求 codex.exe/codex.cmd/codex.bat;请重新安装 @openai/codex 以恢复 npm shim。'
|
|
308
|
+
: '未检测到 Codex CLI。',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
345
311
|
let version;
|
|
346
312
|
try {
|
|
347
313
|
version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
|
|
@@ -361,7 +327,6 @@ export async function getCodexAppServerAvailabilityAsync() {
|
|
|
361
327
|
: `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
|
|
362
328
|
}
|
|
363
329
|
}
|
|
364
|
-
export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
|
|
365
330
|
export class CodexRunner {
|
|
366
331
|
name = 'codex';
|
|
367
332
|
// turn/start persists a new user turn before runQuery resolves. App-server
|
|
@@ -380,14 +345,12 @@ export class CodexRunner {
|
|
|
380
345
|
threadProjectPaths = new Map();
|
|
381
346
|
/** Child Codex threads inherit the EvolCore session that started them. */
|
|
382
347
|
childThreadSessions = new Map();
|
|
383
|
-
sessionTempDirs = new Map();
|
|
384
|
-
managedTempNonce = randomBytes(16).toString('hex');
|
|
385
348
|
threadExternalToolFingerprints = new Map();
|
|
386
349
|
threadExternalToolBoundaries = new Map();
|
|
387
350
|
trackedApprovalItems = new Map();
|
|
388
351
|
pendingToolDenials = new Map();
|
|
389
|
-
threadDelegationCarriers = new Map();
|
|
390
352
|
pendingInterrupts = new Map();
|
|
353
|
+
unconfirmedInterrupts = new Map();
|
|
391
354
|
steeringTails = new Map();
|
|
392
355
|
threadOperationLocks = new Map();
|
|
393
356
|
activeOperationReleases = new Map();
|
|
@@ -405,11 +368,15 @@ export class CodexRunner {
|
|
|
405
368
|
/** Fallback authorization lifecycle ids for app-server requests without an RPC id. */
|
|
406
369
|
approvalLifecycleIds = new WeakMap();
|
|
407
370
|
resolvedConfig;
|
|
371
|
+
codexVersion;
|
|
408
372
|
pendingInterruptTtlMs = 30_000;
|
|
409
373
|
/** Fallback for direct runner callers that do not inject SessionManager. */
|
|
410
374
|
codexSessionFileAdapter = new CodexSessionFileAdapter();
|
|
411
375
|
/** threadId → managed-hook credential, issued by this runner's agent only. */
|
|
412
376
|
threadCredentials = new Map();
|
|
377
|
+
/** @deprecated Compatibility alias for direct runner integrations. */
|
|
378
|
+
threadDelegationCarriers = this.threadCredentials;
|
|
379
|
+
asyncQuestionItems = new Set();
|
|
413
380
|
constructor(config, callbacks, runtime) {
|
|
414
381
|
// Compatibility for legacy direct callers that still pass the removed
|
|
415
382
|
// internal fields in a synthetic config. AgentLoader uses runtime binding.
|
|
@@ -426,6 +393,7 @@ export class CodexRunner {
|
|
|
426
393
|
configuredHeaders: this.resolvedConfig.headers,
|
|
427
394
|
});
|
|
428
395
|
this.capabilities = {
|
|
396
|
+
taskRecovery: true,
|
|
429
397
|
clear: false,
|
|
430
398
|
compact: true,
|
|
431
399
|
fork: true,
|
|
@@ -442,6 +410,7 @@ export class CodexRunner {
|
|
|
442
410
|
if (this.resolvedConfig.effort)
|
|
443
411
|
this.effort = this.resolvedConfig.effort;
|
|
444
412
|
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
413
|
+
this.codexVersion = runtime?.codexVersion;
|
|
445
414
|
}
|
|
446
415
|
getAppServerClient(fullAccess = false) {
|
|
447
416
|
const existing = fullAccess ? this.fullAccessAppServerClient : this.appServerClient;
|
|
@@ -449,6 +418,7 @@ export class CodexRunner {
|
|
|
449
418
|
return existing;
|
|
450
419
|
const client = new CodexAppServerClient({
|
|
451
420
|
apiKey: this.resolvedConfig.apiKey,
|
|
421
|
+
codexVersion: this.codexVersion,
|
|
452
422
|
authType: this.resolvedConfig.authType,
|
|
453
423
|
baseUrl: this.resolvedConfig.baseUrl,
|
|
454
424
|
provider: this.resolvedConfig.provider,
|
|
@@ -642,16 +612,13 @@ export class CodexRunner {
|
|
|
642
612
|
}
|
|
643
613
|
this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
|
|
644
614
|
}
|
|
645
|
-
|
|
646
|
-
return randomBytes(32).toString('base64url');
|
|
647
|
-
}
|
|
648
|
-
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
|
|
615
|
+
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedCredentialToken, executable) {
|
|
649
616
|
const parseOptions = {
|
|
650
617
|
allowManagedTmpDir: true,
|
|
651
618
|
dialect,
|
|
652
619
|
verifyShellExecutable: process.platform === 'win32',
|
|
653
620
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
654
|
-
...(
|
|
621
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
655
622
|
};
|
|
656
623
|
const classification = classifyEvolcoreShellCommand(command, parseOptions);
|
|
657
624
|
if (classification.kind === 'bounded-output')
|
|
@@ -678,10 +645,10 @@ export class CodexRunner {
|
|
|
678
645
|
}
|
|
679
646
|
return {
|
|
680
647
|
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
681
|
-
...(parsed.
|
|
648
|
+
...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
|
|
682
649
|
};
|
|
683
650
|
}
|
|
684
|
-
resolveCanonicalEcApproval(toolInput, managedTempDir,
|
|
651
|
+
resolveCanonicalEcApproval(toolInput, managedTempDir, expectedCredentialToken) {
|
|
685
652
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
686
653
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
687
654
|
? toolInput.commandArgv
|
|
@@ -702,12 +669,12 @@ export class CodexRunner {
|
|
|
702
669
|
? resolveCodexShellCarrierArgv(explicitArgv)
|
|
703
670
|
: undefined;
|
|
704
671
|
if (explicitCarrier !== undefined) {
|
|
705
|
-
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect,
|
|
672
|
+
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedCredentialToken, explicitCarrier.executable);
|
|
706
673
|
}
|
|
707
674
|
if (hasCodexCmdCarrierEcIntent(toolInput)) {
|
|
708
675
|
const parsed = parseCodexToolCommand(toolInput, {
|
|
709
676
|
verifyShellExecutable: process.platform === 'win32',
|
|
710
|
-
...(
|
|
677
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
711
678
|
});
|
|
712
679
|
if (!parsed.ok)
|
|
713
680
|
return { parseFailure: parsed };
|
|
@@ -719,7 +686,7 @@ export class CodexRunner {
|
|
|
719
686
|
} }
|
|
720
687
|
: {
|
|
721
688
|
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
722
|
-
...(parsed.
|
|
689
|
+
...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
|
|
723
690
|
};
|
|
724
691
|
}
|
|
725
692
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
@@ -739,7 +706,7 @@ export class CodexRunner {
|
|
|
739
706
|
allowManagedTmpDir: true,
|
|
740
707
|
verifyShellExecutable: process.platform === 'win32',
|
|
741
708
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
742
|
-
...(
|
|
709
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
743
710
|
});
|
|
744
711
|
if (canonicalCommand?.[0] === 'ec') {
|
|
745
712
|
return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
|
|
@@ -774,7 +741,7 @@ export class CodexRunner {
|
|
|
774
741
|
return true;
|
|
775
742
|
const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
|
|
776
743
|
if (shellCarrier?.dialect === 'powershell'
|
|
777
|
-
&&
|
|
744
|
+
&& hasPowerShellCredentialAssignment(shellCarrier.command))
|
|
778
745
|
return true;
|
|
779
746
|
return hasCodexCmdCarrierEcIntent(toolInput)
|
|
780
747
|
|| shellCarrier !== undefined
|
|
@@ -787,20 +754,20 @@ export class CodexRunner {
|
|
|
787
754
|
}
|
|
788
755
|
armApprovedDelegationCommand(sessionId, argv) {
|
|
789
756
|
const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
790
|
-
const
|
|
757
|
+
const sessionToken = threadId ? this.ensureThreadCredentialLoaded(threadId) : undefined;
|
|
791
758
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
792
759
|
const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
|
|
793
|
-
if (!
|
|
760
|
+
if (!sessionToken || !commandHash || !arm)
|
|
794
761
|
return false;
|
|
795
762
|
try {
|
|
796
|
-
return arm(
|
|
763
|
+
return arm(sessionToken, commandHash);
|
|
797
764
|
}
|
|
798
765
|
catch (error) {
|
|
799
766
|
logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
800
767
|
return false;
|
|
801
768
|
}
|
|
802
769
|
}
|
|
803
|
-
|
|
770
|
+
sessionTokenForSession(sessionId, threadId) {
|
|
804
771
|
const candidates = [
|
|
805
772
|
threadId,
|
|
806
773
|
this.activeSessions.get(sessionId),
|
|
@@ -809,45 +776,13 @@ export class CodexRunner {
|
|
|
809
776
|
for (const candidate of candidates) {
|
|
810
777
|
if (!candidate)
|
|
811
778
|
continue;
|
|
812
|
-
const
|
|
813
|
-
if (
|
|
814
|
-
return
|
|
779
|
+
const token = this.ensureThreadCredentialLoaded(candidate);
|
|
780
|
+
if (token)
|
|
781
|
+
return token;
|
|
815
782
|
}
|
|
816
783
|
return undefined;
|
|
817
784
|
}
|
|
818
|
-
async
|
|
819
|
-
try {
|
|
820
|
-
const result = await appServer.threadShellCommand(threadId, `node -e "process.stdout.write(process.env.${AGENT_DELEGATION_TOKEN_ENV}||String())"`);
|
|
821
|
-
const carrier = result.output.trim();
|
|
822
|
-
if (result.exitCode !== undefined && result.exitCode !== 0)
|
|
823
|
-
return undefined;
|
|
824
|
-
return /^[A-Za-z0-9_-]{32,128}$/.test(carrier) ? carrier : undefined;
|
|
825
|
-
}
|
|
826
|
-
catch (error) {
|
|
827
|
-
logger.warn(`[CodexRunner] failed to recover delegation carrier for loaded thread ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
828
|
-
return undefined;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
async loadPersistedDelegationCarrier(sessionManager, sessionId, threadId) {
|
|
832
|
-
if (typeof sessionManager?.getSessionById !== 'function')
|
|
833
|
-
return undefined;
|
|
834
|
-
try {
|
|
835
|
-
const session = await sessionManager.getSessionById(sessionId);
|
|
836
|
-
const persisted = session?.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
|
|
837
|
-
if (!persisted || typeof persisted !== 'object' || Array.isArray(persisted))
|
|
838
|
-
return undefined;
|
|
839
|
-
if (persisted.threadId !== threadId || typeof persisted.carrier !== 'string')
|
|
840
|
-
return undefined;
|
|
841
|
-
return /^[A-Za-z0-9_-]{32,128}$/.test(persisted.carrier)
|
|
842
|
-
? persisted.carrier
|
|
843
|
-
: undefined;
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
logger.debug(`[CodexRunner] failed to load persisted delegation carrier: session=${sessionId} error=${error}`);
|
|
847
|
-
return undefined;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
async commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn) {
|
|
785
|
+
async commitProvisionalThread(sessionId, threadId, turn) {
|
|
851
786
|
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
852
787
|
return false;
|
|
853
788
|
const bindingResult = this.onSessionIdUpdate
|
|
@@ -871,10 +806,6 @@ export class CodexRunner {
|
|
|
871
806
|
return false;
|
|
872
807
|
this.provisionalSessions.delete(sessionId);
|
|
873
808
|
this.activeSessions.set(sessionId, threadId);
|
|
874
|
-
if (carrierToPublish) {
|
|
875
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
876
|
-
await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
|
|
877
|
-
}
|
|
878
809
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
879
810
|
return true;
|
|
880
811
|
}
|
|
@@ -907,9 +838,9 @@ export class CodexRunner {
|
|
|
907
838
|
return;
|
|
908
839
|
this.provisionalSessions.delete(sessionId);
|
|
909
840
|
this.threadProjectPaths.delete(threadId);
|
|
910
|
-
this.threadDelegationCarriers.delete(threadId);
|
|
911
841
|
this.unbindExternalToolBoundary(threadId);
|
|
912
842
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
843
|
+
this.dropThreadCredential(threadId);
|
|
913
844
|
// A stale provisional start can finish after a replacement run has
|
|
914
845
|
// already activated another parent thread for this session. Do not tear
|
|
915
846
|
// down the replacement run's child mappings in that race.
|
|
@@ -919,31 +850,6 @@ export class CodexRunner {
|
|
|
919
850
|
}
|
|
920
851
|
this.pruneThreadOperationLocks();
|
|
921
852
|
}
|
|
922
|
-
async persistDelegationCarrier(sessionManager, sessionId, threadId, carrier) {
|
|
923
|
-
if (typeof sessionManager?.getSessionById !== 'function')
|
|
924
|
-
return;
|
|
925
|
-
try {
|
|
926
|
-
const session = await sessionManager.getSessionById(sessionId);
|
|
927
|
-
if (!session)
|
|
928
|
-
return;
|
|
929
|
-
const current = session.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
|
|
930
|
-
if (current?.threadId === threadId && current?.carrier === carrier)
|
|
931
|
-
return;
|
|
932
|
-
if (typeof sessionManager.patchSessionMetadata === 'function') {
|
|
933
|
-
await sessionManager.patchSessionMetadata(sessionId, {
|
|
934
|
-
[CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier },
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
else if (typeof sessionManager.updateSession === 'function') {
|
|
938
|
-
await sessionManager.updateSession(sessionId, {
|
|
939
|
-
metadata: { [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier } },
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
catch (error) {
|
|
944
|
-
logger.warn(`[CodexRunner] failed to persist delegation carrier: session=${sessionId} thread=${threadId} error=${error}`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
853
|
resetAppServerClient() {
|
|
948
854
|
const clients = [this.appServerClient, this.fullAccessAppServerClient]
|
|
949
855
|
.filter((client) => !!client);
|
|
@@ -952,8 +858,8 @@ export class CodexRunner {
|
|
|
952
858
|
this.threadSubscriptions.clear();
|
|
953
859
|
this.threadExternalToolFingerprints.clear();
|
|
954
860
|
this.threadExternalToolBoundaries.clear();
|
|
955
|
-
for (const childThreadId of this.childThreadSessions.keys())
|
|
956
|
-
this.
|
|
861
|
+
for (const childThreadId of [...this.childThreadSessions.keys()])
|
|
862
|
+
this.unregisterChildThread(childThreadId);
|
|
957
863
|
// Child turn ids belong to the discarded app-server connection. Keeping
|
|
958
864
|
// them would make a later session interrupt attempt target stale threads
|
|
959
865
|
// through the replacement client.
|
|
@@ -961,7 +867,6 @@ export class CodexRunner {
|
|
|
961
867
|
this.childThreadSessions.clear();
|
|
962
868
|
this.trackedApprovalItems.clear();
|
|
963
869
|
this.pendingToolDenials.clear();
|
|
964
|
-
this.threadDelegationCarriers.clear();
|
|
965
870
|
for (const client of clients) {
|
|
966
871
|
client.close().catch(error => {
|
|
967
872
|
logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
|
|
@@ -1308,47 +1213,53 @@ export class CodexRunner {
|
|
|
1308
1213
|
}
|
|
1309
1214
|
buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1310
1215
|
const profileName = this.permissionProfileName(sessionId);
|
|
1311
|
-
const managedTempDir =
|
|
1216
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
1312
1217
|
const network = {
|
|
1313
1218
|
enabled: true,
|
|
1314
1219
|
unix_sockets: { [resolvePaths().instanceSocket]: 'allow' },
|
|
1315
1220
|
};
|
|
1221
|
+
const filesystem = {
|
|
1222
|
+
...buildCodexProtectedFilesystemRules(resolveRoot(), {
|
|
1223
|
+
denyLClassRead: mode === 'readonly',
|
|
1224
|
+
// The app-server itself is launched behind EvolCore's Linux
|
|
1225
|
+
// H-class guard, which already mounts every L-class directory
|
|
1226
|
+
// read-only. Avoid Codex's per-file glob expansion inside that
|
|
1227
|
+
// nested mount namespace; it can fail while creating a missing
|
|
1228
|
+
// target such as triggers/history.jsonl.
|
|
1229
|
+
expandLClassReadDeny: process.platform !== 'linux',
|
|
1230
|
+
}),
|
|
1231
|
+
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
1232
|
+
...(bootstrapAgentMdPath ? {
|
|
1233
|
+
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1234
|
+
} : {}),
|
|
1235
|
+
...(personalMdPath ? {
|
|
1236
|
+
[path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1237
|
+
} : {}),
|
|
1238
|
+
...(mode !== 'readonly' && managedTempDir ? {
|
|
1239
|
+
[managedTempDir]: 'write',
|
|
1240
|
+
[managedTempDir + '/**']: 'write',
|
|
1241
|
+
[path.join(managedTempDir, 'locks')]: 'write',
|
|
1242
|
+
[path.join(managedTempDir, 'locks') + '/**']: 'write',
|
|
1243
|
+
} : {}),
|
|
1244
|
+
};
|
|
1245
|
+
// Codex resolves this complete profile before serializing the Windows
|
|
1246
|
+
// helper payload. Check after all runner-owned additions, not merely the
|
|
1247
|
+
// protected-path base projection, so Git and managed-temp rules cannot
|
|
1248
|
+
// push an otherwise valid profile back over CreateProcess' limit.
|
|
1249
|
+
assertWindowsCodexFilesystemRuleBudget(filesystem);
|
|
1316
1250
|
return {
|
|
1317
1251
|
default_permissions: profileName,
|
|
1318
1252
|
permissions: {
|
|
1319
1253
|
[profileName]: {
|
|
1320
1254
|
extends: mode === 'readonly' ? ':read-only' : ':workspace',
|
|
1321
|
-
filesystem
|
|
1322
|
-
...buildCodexProtectedFilesystemRules(resolveRoot(), {
|
|
1323
|
-
denyLClassRead: mode === 'readonly',
|
|
1324
|
-
// The app-server itself is launched behind EvolCore's Linux
|
|
1325
|
-
// H-class guard, which already mounts every L-class directory
|
|
1326
|
-
// read-only. Avoid Codex's per-file glob expansion inside that
|
|
1327
|
-
// nested mount namespace; it can fail while creating a missing
|
|
1328
|
-
// target such as triggers/history.jsonl.
|
|
1329
|
-
expandLClassReadDeny: process.platform !== 'linux',
|
|
1330
|
-
}),
|
|
1331
|
-
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
1332
|
-
...(bootstrapAgentMdPath ? {
|
|
1333
|
-
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1334
|
-
} : {}),
|
|
1335
|
-
...(personalMdPath ? {
|
|
1336
|
-
[path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1337
|
-
} : {}),
|
|
1338
|
-
...(mode !== 'readonly' && managedTempDir ? {
|
|
1339
|
-
[managedTempDir]: 'write',
|
|
1340
|
-
[managedTempDir + '/**']: 'write',
|
|
1341
|
-
[path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
|
|
1342
|
-
[path.join(path.dirname(managedTempDir), 'evolcore-locks') + '/**']: 'write',
|
|
1343
|
-
} : {}),
|
|
1344
|
-
},
|
|
1255
|
+
filesystem,
|
|
1345
1256
|
network,
|
|
1346
1257
|
},
|
|
1347
1258
|
},
|
|
1348
1259
|
};
|
|
1349
1260
|
}
|
|
1350
|
-
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1351
|
-
const decision = resolvePhaseOneExecutionSandbox(mode,
|
|
1261
|
+
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
|
|
1262
|
+
const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
|
|
1352
1263
|
return {
|
|
1353
1264
|
decision,
|
|
1354
1265
|
permissionProfileConfig: decision.state === 'active'
|
|
@@ -1386,6 +1297,26 @@ export class CodexRunner {
|
|
|
1386
1297
|
}
|
|
1387
1298
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
1388
1299
|
setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
|
|
1300
|
+
async steerAsyncQuestionAnswer(sessionId, answer) {
|
|
1301
|
+
const active = this.activeTurns.get(sessionId);
|
|
1302
|
+
if (!active)
|
|
1303
|
+
return 'inactive';
|
|
1304
|
+
if (active.threadId !== answer.providerThreadId)
|
|
1305
|
+
throw new Error('Async question backend has changed');
|
|
1306
|
+
const client = this.getAppServerClientForThread(active.threadId);
|
|
1307
|
+
try {
|
|
1308
|
+
await client.turnSteer(active.threadId, active.turnId, [{
|
|
1309
|
+
type: 'text', text: formatAsyncQuestionAnswer(answer), text_elements: [],
|
|
1310
|
+
}]);
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
if (error.code === 'CODEX_NO_ACTIVE_TURN')
|
|
1314
|
+
return 'inactive';
|
|
1315
|
+
// Transport failures are ambiguous: never risk submitting the same answer twice.
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
return 'accepted';
|
|
1319
|
+
}
|
|
1389
1320
|
injectUserMessage(sessionId, text) {
|
|
1390
1321
|
// Bind queued reminders to the turn that generated them. A completed
|
|
1391
1322
|
// turn may be replaced before the serialized steer reaches app-server;
|
|
@@ -1722,9 +1653,11 @@ export class CodexRunner {
|
|
|
1722
1653
|
const knownThreadId = topicBinding
|
|
1723
1654
|
? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
|
|
1724
1655
|
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1725
|
-
const operationKey = this.threadOperationKey(knownThreadId, sessionId);
|
|
1656
|
+
const operationKey = this.threadOperationKey(modelOverride?.recovery?.agentSessionId || knownThreadId, sessionId);
|
|
1657
|
+
await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
|
|
1726
1658
|
const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
|
|
1727
1659
|
try {
|
|
1660
|
+
await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
|
|
1728
1661
|
const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
|
|
1729
1662
|
this.activeOperationReleases.set(sessionId, release);
|
|
1730
1663
|
return this.holdThreadOperation(stream, sessionId, release);
|
|
@@ -1754,6 +1687,14 @@ export class CodexRunner {
|
|
|
1754
1687
|
logger.debug(`[CodexRunner] persisted main backend lookup failed; using local cache: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1755
1688
|
}
|
|
1756
1689
|
}
|
|
1690
|
+
const recoverySessionId = modelOverride?.recovery?.agentSessionId;
|
|
1691
|
+
if (modelOverride?.recovery && (!recoverySessionId || agentSessionId !== recoverySessionId)) {
|
|
1692
|
+
throw new Error('Task recovery requires the original Codex thread binding');
|
|
1693
|
+
}
|
|
1694
|
+
if (modelOverride?.asyncQuestionAnswer && agentSessionId !== modelOverride.asyncQuestionAnswer.providerThreadId) {
|
|
1695
|
+
throw new Error('Async question reply requires its original Codex backend');
|
|
1696
|
+
}
|
|
1697
|
+
await this.assertInterruptSettled(sessionId, agentSessionId);
|
|
1757
1698
|
let resumingThread = !!agentSessionId;
|
|
1758
1699
|
const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
1759
1700
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
@@ -1774,6 +1715,10 @@ export class CodexRunner {
|
|
|
1774
1715
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1775
1716
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
1776
1717
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
1718
|
+
const userInputConfig = { features: {
|
|
1719
|
+
default_mode_request_user_input: modelOverride?.enableRequestUserInput
|
|
1720
|
+
?? this.resolvedConfig.enableRequestUserInput ?? true,
|
|
1721
|
+
} };
|
|
1777
1722
|
const externalToolConfig = callMode === 'fullaccess'
|
|
1778
1723
|
? {}
|
|
1779
1724
|
: await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
|
|
@@ -1781,25 +1726,21 @@ export class CodexRunner {
|
|
|
1781
1726
|
await this.assertProactiveHookAvailability(sessionId, appServer);
|
|
1782
1727
|
const managedTempInstruction = this.managedTempInstruction(sessionId);
|
|
1783
1728
|
const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
|
|
1784
|
-
let
|
|
1785
|
-
? this.
|
|
1729
|
+
let threadSessionToken = agentSessionId
|
|
1730
|
+
? this.ensureThreadCredential(agentSessionId)
|
|
1731
|
+
: this.createThreadCredential();
|
|
1732
|
+
if (!threadSessionToken)
|
|
1733
|
+
throw new Error('Codex thread session credential is unavailable');
|
|
1734
|
+
const persistentRuntimeEnv = runtimeEnv
|
|
1735
|
+
? Object.fromEntries(Object.entries(runtimeEnv).filter(([name]) => callMode === 'fullaccess' || name.toUpperCase() !== AGENT_DELEGATION_TOKEN_ENV))
|
|
1786
1736
|
: undefined;
|
|
1787
|
-
if (agentSessionId && !knownDelegationCarrier) {
|
|
1788
|
-
knownDelegationCarrier = await this.loadPersistedDelegationCarrier(sessionManager, sessionId, agentSessionId);
|
|
1789
|
-
}
|
|
1790
|
-
let delegationCarrierCandidate = knownDelegationCarrier ?? this.createDelegationCarrier();
|
|
1791
|
-
// Keep newly discovered carrier state local until generation-aware
|
|
1792
|
-
// activation accepts the backend. A stale run must not publish a carrier
|
|
1793
|
-
// that a later generation could mistake for its own thread.
|
|
1794
|
-
let carrierToPublish;
|
|
1795
|
-
const hasTaskDelegation = typeof runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] === 'string';
|
|
1796
1737
|
let threadOptions = {
|
|
1797
1738
|
model: callModel,
|
|
1798
1739
|
effort: callEffort,
|
|
1799
1740
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1800
1741
|
approvalsReviewer: 'user',
|
|
1801
1742
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1802
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig,
|
|
1743
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, persistentRuntimeEnv ? { shell_environment_policy: { set: persistentRuntimeEnv } } : undefined, capabilityConfig, userInputConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, threadSessionToken, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined),
|
|
1803
1744
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1804
1745
|
};
|
|
1805
1746
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
@@ -1825,12 +1766,12 @@ export class CodexRunner {
|
|
|
1825
1766
|
// A legacy main session can retain a provider ID after its rollout was
|
|
1826
1767
|
// removed externally. It is safe to recover before input submission;
|
|
1827
1768
|
// topic bindings remain authoritative and fail closed.
|
|
1828
|
-
if (topicBinding || !isCodexNoRolloutError(error))
|
|
1769
|
+
if (topicBinding || recoverySessionId || modelOverride?.asyncQuestionAnswer || !isCodexNoRolloutError(error))
|
|
1829
1770
|
throw error;
|
|
1830
1771
|
if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
|
|
1831
1772
|
throw error;
|
|
1832
1773
|
const missingThreadId = agentSessionId;
|
|
1833
|
-
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId
|
|
1774
|
+
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId);
|
|
1834
1775
|
if (cleared !== 'cleared' && cleared !== 'already_clear') {
|
|
1835
1776
|
const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
|
|
1836
1777
|
recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
|
|
@@ -1839,17 +1780,15 @@ export class CodexRunner {
|
|
|
1839
1780
|
if (this.activeSessions.get(sessionId) === missingThreadId)
|
|
1840
1781
|
this.activeSessions.delete(sessionId);
|
|
1841
1782
|
this.threadProjectPaths.delete(missingThreadId);
|
|
1842
|
-
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1843
1783
|
this.unbindExternalToolBoundary(missingThreadId);
|
|
1844
1784
|
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1845
1785
|
this.dropThreadCredential(missingThreadId);
|
|
1846
1786
|
agentSessionId = undefined;
|
|
1847
1787
|
resumingThread = false;
|
|
1848
|
-
|
|
1849
|
-
delegationCarrierCandidate = this.createDelegationCarrier();
|
|
1788
|
+
threadSessionToken = this.createThreadCredential();
|
|
1850
1789
|
threadOptions = {
|
|
1851
1790
|
...threadOptions,
|
|
1852
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig,
|
|
1791
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, persistentRuntimeEnv ? { shell_environment_policy: { set: persistentRuntimeEnv } } : undefined, capabilityConfig, userInputConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, threadSessionToken, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined),
|
|
1853
1792
|
};
|
|
1854
1793
|
logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
|
|
1855
1794
|
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
@@ -1862,43 +1801,12 @@ export class CodexRunner {
|
|
|
1862
1801
|
let threadId = threadResponse.thread?.id || agentSessionId;
|
|
1863
1802
|
if (!threadId)
|
|
1864
1803
|
throw new Error('Codex app-server did not return a thread id');
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
carrierToPublish = { threadId, carrier: delegationCarrierCandidate };
|
|
1868
|
-
}
|
|
1869
|
-
else if (hasTaskDelegation && !knownDelegationCarrier) {
|
|
1870
|
-
// A BOUND topic cannot silently fork to migrate carrier metadata: its
|
|
1871
|
-
// active-turn activation is allowed to confirm only the authoritative
|
|
1872
|
-
// backend ID, so a fork would deterministically conflict. threadResume
|
|
1873
|
-
// already received the candidate in this turn's environment config;
|
|
1874
|
-
// publish it for the same thread after binding confirmation instead.
|
|
1875
|
-
let recoveredCarrier = topicBinding
|
|
1876
|
-
? delegationCarrierCandidate
|
|
1877
|
-
: await this.readThreadDelegationCarrier(appServer, threadId);
|
|
1878
|
-
if (!recoveredCarrier) {
|
|
1879
|
-
const staleThreadId = threadId;
|
|
1880
|
-
logger.warn(`[CodexRunner] loaded thread has no recoverable delegation carrier; forking once to preserve history: thread=${staleThreadId}`);
|
|
1881
|
-
const forked = await appServer.threadFork(staleThreadId, projectPath, undefined, {
|
|
1882
|
-
model: callModel,
|
|
1883
|
-
effort: callEffort,
|
|
1884
|
-
approvalPolicy: effectiveApprovalPolicy,
|
|
1885
|
-
approvalsReviewer: 'user',
|
|
1886
|
-
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1887
|
-
config: threadOptions.config,
|
|
1888
|
-
});
|
|
1889
|
-
const forkedThreadId = forked.thread?.id;
|
|
1890
|
-
if (!forkedThreadId)
|
|
1891
|
-
throw new Error('Codex delegation-carrier recovery fork did not return a thread id');
|
|
1892
|
-
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
1893
|
-
await this.releaseThreadSubscription(appServer, staleThreadId, 'carrier-recovery-fork-source');
|
|
1894
|
-
await this.releaseThreadSubscription(appServer, forkedThreadId, 'carrier-recovery-fork');
|
|
1895
|
-
threadId = forkedThreadId;
|
|
1896
|
-
recoveredCarrier = delegationCarrierCandidate;
|
|
1897
|
-
}
|
|
1898
|
-
carrierToPublish = { threadId, carrier: recoveredCarrier };
|
|
1804
|
+
if (recoverySessionId && threadId !== recoverySessionId) {
|
|
1805
|
+
throw new Error('Task recovery cannot replace the original Codex thread');
|
|
1899
1806
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1807
|
+
this.markThreadSubscribed(appServer, threadId);
|
|
1808
|
+
if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
|
|
1809
|
+
throw new Error('Codex thread session credential could not be bound');
|
|
1902
1810
|
}
|
|
1903
1811
|
const turn = modelOverride?.turn;
|
|
1904
1812
|
if (!resumingThread) {
|
|
@@ -1906,9 +1814,6 @@ export class CodexRunner {
|
|
|
1906
1814
|
// has been accepted. In particular, a pending interrupt must not leave
|
|
1907
1815
|
// a durable ID for a thread that never produced a rollout.
|
|
1908
1816
|
this.provisionalSessions.set(sessionId, threadId);
|
|
1909
|
-
if (carrierToPublish) {
|
|
1910
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1911
|
-
}
|
|
1912
1817
|
}
|
|
1913
1818
|
else {
|
|
1914
1819
|
agentSessionId = threadId;
|
|
@@ -1928,10 +1833,6 @@ export class CodexRunner {
|
|
|
1928
1833
|
throw error;
|
|
1929
1834
|
}
|
|
1930
1835
|
this.activeSessions.set(sessionId, threadId);
|
|
1931
|
-
if (carrierToPublish) {
|
|
1932
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1933
|
-
await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1934
|
-
}
|
|
1935
1836
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
1936
1837
|
}
|
|
1937
1838
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
@@ -1939,7 +1840,11 @@ export class CodexRunner {
|
|
|
1939
1840
|
const controller = new AbortController();
|
|
1940
1841
|
this.activeAbortControllers.set(sessionId, controller);
|
|
1941
1842
|
const tempFiles = [];
|
|
1942
|
-
const
|
|
1843
|
+
const answer = modelOverride?.asyncQuestionAnswer;
|
|
1844
|
+
if (answer && answer.providerThreadId !== threadId) {
|
|
1845
|
+
throw new Error('Async question reply cannot resume a different Codex thread');
|
|
1846
|
+
}
|
|
1847
|
+
const input = this.buildAppServerInput(answer ? formatAsyncQuestionAnswer(answer) : prompt, images, tempFiles, sessionId, runtimeEnv);
|
|
1943
1848
|
const queue = new AsyncEventQueue();
|
|
1944
1849
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
1945
1850
|
const state = {
|
|
@@ -2053,7 +1958,7 @@ export class CodexRunner {
|
|
|
2053
1958
|
if (!persisted)
|
|
2054
1959
|
return false;
|
|
2055
1960
|
try {
|
|
2056
|
-
const committed = await this.commitProvisionalThread(sessionId, threadId,
|
|
1961
|
+
const committed = await this.commitProvisionalThread(sessionId, threadId, turn);
|
|
2057
1962
|
if (!committed) {
|
|
2058
1963
|
const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
|
|
2059
1964
|
staleError.code = 'RUNNER_START_SUPERSEDED';
|
|
@@ -2164,16 +2069,65 @@ export class CodexRunner {
|
|
|
2164
2069
|
}
|
|
2165
2070
|
}
|
|
2166
2071
|
async interruptAppServerTurn(threadId, turnId) {
|
|
2072
|
+
const sessionId = this.findSessionKeyByThread(threadId);
|
|
2073
|
+
const subscribed = this.threadSubscriptions.get(threadId);
|
|
2074
|
+
const client = subscribed
|
|
2075
|
+
? subscribed.client
|
|
2076
|
+
: this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
|
|
2077
|
+
const key = JSON.stringify([threadId, turnId]);
|
|
2078
|
+
const previous = this.unconfirmedInterrupts.get(key);
|
|
2079
|
+
previous?.unsubscribe?.();
|
|
2080
|
+
const record = { sessionId, threadId, turnId, client };
|
|
2081
|
+
this.unconfirmedInterrupts.set(key, record);
|
|
2082
|
+
let confirm;
|
|
2083
|
+
const confirmed = new Promise(resolve => { confirm = resolve; });
|
|
2084
|
+
const clear = () => {
|
|
2085
|
+
if (this.unconfirmedInterrupts.get(key) === record)
|
|
2086
|
+
this.unconfirmedInterrupts.delete(key);
|
|
2087
|
+
record.unsubscribe?.();
|
|
2088
|
+
confirm();
|
|
2089
|
+
};
|
|
2090
|
+
record.unsubscribe = client.onNotification?.(notification => {
|
|
2091
|
+
const params = notification.params ?? {};
|
|
2092
|
+
if ((params.threadId ?? params.thread_id) !== threadId)
|
|
2093
|
+
return;
|
|
2094
|
+
if (notification.method === 'thread/closed'
|
|
2095
|
+
|| (notification.method === 'turn/completed'
|
|
2096
|
+
&& (params.turn?.id ?? params.turnId ?? params.turn_id) === turnId))
|
|
2097
|
+
clear();
|
|
2098
|
+
});
|
|
2167
2099
|
try {
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2100
|
+
await withInterruptDeadline(Promise.race([
|
|
2101
|
+
client.turnInterrupt(threadId, turnId).then(accepted => {
|
|
2102
|
+
if (!accepted)
|
|
2103
|
+
throw new Error('Codex interrupt was not accepted');
|
|
2104
|
+
}),
|
|
2105
|
+
confirmed,
|
|
2106
|
+
]), 'Codex turn/interrupt', CODEX_INTERRUPT_TIMEOUT_MS);
|
|
2174
2107
|
}
|
|
2175
2108
|
catch (error) {
|
|
2176
|
-
logger.
|
|
2109
|
+
logger.warn(`[CodexRunner] Unconfirmed interrupt: thread=${threadId} turn=${turnId} error=${String(error)}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
async assertInterruptSettled(sessionId, threadId) {
|
|
2113
|
+
const pending = [...this.unconfirmedInterrupts.entries()].filter(([, record]) => record.sessionId === sessionId || (threadId && record.threadId === threadId));
|
|
2114
|
+
await Promise.all(pending.map(async ([key, record]) => {
|
|
2115
|
+
try {
|
|
2116
|
+
// Local stream closure and unsubscribe do not prove the backend turn
|
|
2117
|
+
// stopped. Check its exact identity before allowing another writer.
|
|
2118
|
+
const response = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt verification', 2_000);
|
|
2119
|
+
const turn = response.thread?.turns?.find(turn => turn.id === record.turnId);
|
|
2120
|
+
if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status))
|
|
2121
|
+
&& !response.thread?.turns?.some(turn => turn.status === 'inProgress')
|
|
2122
|
+
&& this.unconfirmedInterrupts.get(key) === record) {
|
|
2123
|
+
this.unconfirmedInterrupts.delete(key);
|
|
2124
|
+
record.unsubscribe?.();
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
catch { /* Ambiguous execution remains fenced; report it below. */ }
|
|
2128
|
+
}));
|
|
2129
|
+
if ([...this.unconfirmedInterrupts.values()].some(record => record.sessionId === sessionId || (threadId && record.threadId === threadId))) {
|
|
2130
|
+
throw unsettledInterruptError('Codex');
|
|
2177
2131
|
}
|
|
2178
2132
|
}
|
|
2179
2133
|
// ── Session commands ──
|
|
@@ -2185,7 +2139,6 @@ export class CodexRunner {
|
|
|
2185
2139
|
return;
|
|
2186
2140
|
if (previousThreadId && previousThreadId !== agentSessionId) {
|
|
2187
2141
|
this.threadProjectPaths.delete(previousThreadId);
|
|
2188
|
-
this.threadDelegationCarriers.delete(previousThreadId);
|
|
2189
2142
|
this.unbindExternalToolBoundary(previousThreadId);
|
|
2190
2143
|
this.clearTrackedApprovalItemsForThread(previousThreadId);
|
|
2191
2144
|
}
|
|
@@ -2269,10 +2222,8 @@ export class CodexRunner {
|
|
|
2269
2222
|
if (this.activeSessions.get(sessionId) === capturedThreadId)
|
|
2270
2223
|
this.activeSessions.delete(sessionId);
|
|
2271
2224
|
this.chatModes.delete(sessionId);
|
|
2272
|
-
this.cleanupSessionTempDir(sessionId);
|
|
2273
2225
|
if (capturedThreadId) {
|
|
2274
2226
|
this.threadProjectPaths.delete(capturedThreadId);
|
|
2275
|
-
this.threadDelegationCarriers.delete(capturedThreadId);
|
|
2276
2227
|
this.unbindExternalToolBoundary(capturedThreadId);
|
|
2277
2228
|
}
|
|
2278
2229
|
this.clearTrackedApprovalItemsForThread(capturedThreadId);
|
|
@@ -2312,9 +2263,7 @@ export class CodexRunner {
|
|
|
2312
2263
|
this.activeTurns.delete(sessionId);
|
|
2313
2264
|
this.steeringTails.delete(sessionId);
|
|
2314
2265
|
this.chatModes.delete(sessionId);
|
|
2315
|
-
this.cleanupSessionTempDir(sessionId);
|
|
2316
2266
|
this.threadProjectPaths.delete(threadId);
|
|
2317
|
-
this.threadDelegationCarriers.delete(threadId);
|
|
2318
2267
|
this.unbindExternalToolBoundary(threadId);
|
|
2319
2268
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
2320
2269
|
this.pruneThreadOperationLocks();
|
|
@@ -2355,19 +2304,19 @@ export class CodexRunner {
|
|
|
2355
2304
|
: await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
|
|
2356
2305
|
if (compactMode !== 'fullaccess')
|
|
2357
2306
|
await this.assertProactiveHookAvailability(_sessionId, appServer);
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2307
|
+
const compactSessionToken = this.ensureThreadCredential(agentSessionId);
|
|
2308
|
+
if (!compactSessionToken)
|
|
2309
|
+
throw new Error('Codex thread session credential is unavailable');
|
|
2360
2310
|
await appServer.threadResume(agentSessionId, _projectPath, {
|
|
2361
2311
|
model: compactModel,
|
|
2362
2312
|
effort: compactEffort,
|
|
2363
2313
|
approvalPolicy: compactPolicy,
|
|
2364
2314
|
approvalsReviewer: 'user',
|
|
2365
2315
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2366
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId,
|
|
2316
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactSessionToken), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
2367
2317
|
});
|
|
2368
2318
|
this.markThreadSubscribed(appServer, agentSessionId);
|
|
2369
2319
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
2370
|
-
this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
|
|
2371
2320
|
this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
|
|
2372
2321
|
await this.startAndWaitForCompact(appServer, agentSessionId);
|
|
2373
2322
|
return finish({ ok: true, durationMs: Date.now() - startedAt });
|
|
@@ -2528,23 +2477,32 @@ export class CodexRunner {
|
|
|
2528
2477
|
const dir = this.threadCredentialsDir();
|
|
2529
2478
|
return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
|
|
2530
2479
|
}
|
|
2480
|
+
createThreadCredential() {
|
|
2481
|
+
return randomBytes(32).toString('hex');
|
|
2482
|
+
}
|
|
2531
2483
|
/** Issue or re-read this agent's credential for a thread. */
|
|
2532
|
-
ensureThreadCredential(threadId) {
|
|
2484
|
+
ensureThreadCredential(threadId, candidate) {
|
|
2533
2485
|
const cached = this.threadCredentials.get(threadId);
|
|
2534
2486
|
if (cached)
|
|
2535
|
-
return cached;
|
|
2487
|
+
return !candidate || cached === candidate ? cached : undefined;
|
|
2536
2488
|
const file = this.threadCredentialPath(threadId);
|
|
2537
|
-
|
|
2538
|
-
|
|
2489
|
+
// Low-level runner callers may not have an agent binding (for example a
|
|
2490
|
+
// standalone integration test). Keep those calls functional in memory;
|
|
2491
|
+
// daemon-owned runners always have an AID and persist the credential.
|
|
2492
|
+
if (!file) {
|
|
2493
|
+
const token = candidate ?? this.createThreadCredential();
|
|
2494
|
+
this.threadCredentials.set(threadId, token);
|
|
2495
|
+
return token;
|
|
2496
|
+
}
|
|
2539
2497
|
try {
|
|
2540
2498
|
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2541
2499
|
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2542
2500
|
this.threadCredentials.set(threadId, record.token);
|
|
2543
|
-
return record.token;
|
|
2501
|
+
return !candidate || record.token === candidate ? record.token : undefined;
|
|
2544
2502
|
}
|
|
2545
2503
|
}
|
|
2546
2504
|
catch { /* missing or unreadable: fall through to issuance */ }
|
|
2547
|
-
const token =
|
|
2505
|
+
const token = candidate ?? this.createThreadCredential();
|
|
2548
2506
|
const record = {
|
|
2549
2507
|
token,
|
|
2550
2508
|
threadId,
|
|
@@ -2606,49 +2564,55 @@ export class CodexRunner {
|
|
|
2606
2564
|
}
|
|
2607
2565
|
catch { /* best effort */ }
|
|
2608
2566
|
}
|
|
2609
|
-
async forkSession(agentSessionId, projectPath, title) {
|
|
2567
|
+
async forkSession(agentSessionId, projectPath, title, context) {
|
|
2610
2568
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
|
|
2611
2569
|
try {
|
|
2612
|
-
return await this.forkSessionLocked(agentSessionId, projectPath, title);
|
|
2570
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title, undefined, context);
|
|
2613
2571
|
}
|
|
2614
2572
|
finally {
|
|
2615
2573
|
release();
|
|
2616
2574
|
}
|
|
2617
2575
|
}
|
|
2618
|
-
async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId) {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2576
|
+
async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId, context) {
|
|
2577
|
+
// Historical threads may have no live runner binding after idle cleanup or
|
|
2578
|
+
// restart. Cached turn permissions can also outlive a role change. Derive
|
|
2579
|
+
// this management operation solely from its current authenticated request;
|
|
2580
|
+
// callers without that context remain readonly, including old fullaccess
|
|
2581
|
+
// threads. Do not mutate shared turn/session permission maps while forking.
|
|
2582
|
+
const sessionKey = context?.sessionId ?? this.findSessionKeyByThread(agentSessionId);
|
|
2583
|
+
const mode = normalizePermissionMode(context?.permissionMode).mode;
|
|
2584
|
+
const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath, undefined, undefined, context ?? {});
|
|
2622
2585
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
2623
|
-
const appServer = this.
|
|
2624
|
-
?? this.getAppServerClient(mode === 'fullaccess');
|
|
2586
|
+
const appServer = this.getAppServerClient();
|
|
2625
2587
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
|
|
2626
2588
|
if (sessionKey)
|
|
2627
2589
|
await this.assertProactiveHookAvailability(sessionKey, appServer);
|
|
2628
|
-
const
|
|
2590
|
+
const forkSessionToken = this.createThreadCredential();
|
|
2629
2591
|
const response = await appServer.threadFork(agentSessionId, projectPath, title, {
|
|
2630
2592
|
model: this.model,
|
|
2631
2593
|
effort: this.effort,
|
|
2632
2594
|
approvalPolicy: this.toApprovalPolicy(mode),
|
|
2633
2595
|
approvalsReviewer: 'user',
|
|
2634
2596
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2635
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey,
|
|
2597
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, forkSessionToken), this.buildLifecycleLockdownConfig(mode)), mode),
|
|
2636
2598
|
...(lastTurnId ? { lastTurnId } : {}),
|
|
2637
2599
|
});
|
|
2638
2600
|
const forkedThreadId = response.thread?.id;
|
|
2639
2601
|
if (!forkedThreadId)
|
|
2640
2602
|
throw new Error('Codex fork did not return a thread id');
|
|
2603
|
+
if (this.ensureThreadCredential(forkedThreadId, forkSessionToken) !== forkSessionToken) {
|
|
2604
|
+
throw new Error('Codex fork session credential could not be bound');
|
|
2605
|
+
}
|
|
2641
2606
|
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
2642
|
-
this.threadDelegationCarriers.set(forkedThreadId, delegationCarrier);
|
|
2643
2607
|
this.threadProjectPaths.set(forkedThreadId, path.resolve(projectPath));
|
|
2644
2608
|
this.bindExternalToolBoundary(forkedThreadId, externalToolConfig);
|
|
2645
2609
|
await this.releaseThreadSubscription(appServer, forkedThreadId, 'fork-finished');
|
|
2646
2610
|
return forkedThreadId;
|
|
2647
2611
|
}
|
|
2648
|
-
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
|
|
2612
|
+
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title, context) {
|
|
2649
2613
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
|
|
2650
2614
|
try {
|
|
2651
|
-
const appServer = this.
|
|
2615
|
+
const appServer = this.getAppServerClient();
|
|
2652
2616
|
const history = await this.readForkHistory(appServer, agentSessionId);
|
|
2653
2617
|
const messages = this.mapThreadToSessionMessages(history, agentSessionId);
|
|
2654
2618
|
const turns = buildSessionTurnList(messages);
|
|
@@ -2658,7 +2622,7 @@ export class CodexRunner {
|
|
|
2658
2622
|
const targetTurnId = this.findTurnIdForAssistantMessage(history, assistantMessageId);
|
|
2659
2623
|
if (!targetTurnId)
|
|
2660
2624
|
throw new Error('fork point turn not found');
|
|
2661
|
-
return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId);
|
|
2625
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId, context);
|
|
2662
2626
|
}
|
|
2663
2627
|
finally {
|
|
2664
2628
|
release();
|
|
@@ -2730,7 +2694,7 @@ export class CodexRunner {
|
|
|
2730
2694
|
return this.getAppServerClientForThread(agentSessionId).threadMetadataUpdate(agentSessionId, gitInfo);
|
|
2731
2695
|
}
|
|
2732
2696
|
async getSessionMessages(agentSessionId, projectPath) {
|
|
2733
|
-
const response = await this.getAppServerClientForThread(agentSessionId)
|
|
2697
|
+
const response = await this.readForkHistory(this.getAppServerClientForThread(agentSessionId), agentSessionId);
|
|
2734
2698
|
return this.mapThreadToSessionMessages(response, agentSessionId);
|
|
2735
2699
|
}
|
|
2736
2700
|
approvalItemKey(threadId, turnId, itemId) {
|
|
@@ -3070,9 +3034,9 @@ export class CodexRunner {
|
|
|
3070
3034
|
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
3071
3035
|
|| request.method === 'execCommandApproval';
|
|
3072
3036
|
const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
|
|
3073
|
-
const
|
|
3037
|
+
const sessionCredential = this.sessionTokenForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
|
|
3074
3038
|
const canonicalEcApproval = isCommandApproval
|
|
3075
|
-
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey),
|
|
3039
|
+
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), sessionCredential)
|
|
3076
3040
|
: undefined;
|
|
3077
3041
|
const approvedCommand = canonicalEcApproval?.argv;
|
|
3078
3042
|
const denyInfrastructure = async (policyCode, message) => {
|
|
@@ -3115,6 +3079,9 @@ export class CodexRunner {
|
|
|
3115
3079
|
}
|
|
3116
3080
|
return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
|
|
3117
3081
|
? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
|
|
3082
|
+
+ (canonicalEcApproval.parseFailure.dialect === 'powershell' && canonicalEcApproval.parseFailure.issue === 'invalid-quote'
|
|
3083
|
+
? '. PowerShell treats smart quotes as delimiters too; escape quotes for PowerShell or use --text-from-file for message text.'
|
|
3084
|
+
: '')
|
|
3118
3085
|
: 'EC command approval denied because no canonical argv could be derived');
|
|
3119
3086
|
}
|
|
3120
3087
|
try {
|
|
@@ -3122,23 +3089,23 @@ export class CodexRunner {
|
|
|
3122
3089
|
const decision = typeof resolution === 'string' ? resolution : resolution.decision;
|
|
3123
3090
|
const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
|
|
3124
3091
|
if (decision !== 'deny' && managedEcIntent && approvedCommand) {
|
|
3125
|
-
const
|
|
3092
|
+
const credentialThreadIds = [
|
|
3126
3093
|
this.activeSessions.get(sessionKey),
|
|
3127
3094
|
this.provisionalSessions.get(sessionKey),
|
|
3128
3095
|
typeof params.threadId === 'string' ? params.threadId : undefined,
|
|
3129
3096
|
typeof params.conversationId === 'string' ? params.conversationId : undefined,
|
|
3130
3097
|
];
|
|
3131
|
-
const
|
|
3098
|
+
const hasSessionCredential = credentialThreadIds.some(threadId => !!threadId && !!this.ensureThreadCredentialLoaded(threadId));
|
|
3132
3099
|
const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
|
|
3133
|
-
// A
|
|
3100
|
+
// A credentialed thread is managed even if its permission context
|
|
3134
3101
|
// was not wired with an arm callback; it must fail closed here rather
|
|
3135
|
-
// than reaching daemon IPC with an unarmed
|
|
3136
|
-
if ((
|
|
3137
|
-
&& canonicalEcApproval?.
|
|
3138
|
-
&& canonicalEcApproval.
|
|
3139
|
-
return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread
|
|
3102
|
+
// than reaching daemon IPC with an unarmed session credential.
|
|
3103
|
+
if ((hasSessionCredential || canArmDelegation)
|
|
3104
|
+
&& canonicalEcApproval?.credentialToken
|
|
3105
|
+
&& canonicalEcApproval.credentialToken !== sessionCredential) {
|
|
3106
|
+
return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread credential');
|
|
3140
3107
|
}
|
|
3141
|
-
if ((
|
|
3108
|
+
if ((hasSessionCredential || canArmDelegation)
|
|
3142
3109
|
&& !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
|
|
3143
3110
|
return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
|
|
3144
3111
|
}
|
|
@@ -3793,14 +3760,7 @@ export class CodexRunner {
|
|
|
3793
3760
|
if (paths.length === 0)
|
|
3794
3761
|
return true;
|
|
3795
3762
|
const workspace = resolveProtectedCandidate(projectPath);
|
|
3796
|
-
const
|
|
3797
|
-
const trustedTempDir = managedTempDir ? this.isTrustedSessionTempDir(managedTempDir) : false;
|
|
3798
|
-
if (managedTempDir && !trustedTempDir) {
|
|
3799
|
-
logger.warn(`[CodexRunner] managed TMPDIR lost its trusted ownership or mode: session=${sessionKey} path=${managedTempDir}`);
|
|
3800
|
-
}
|
|
3801
|
-
const canonicalTempDir = managedTempDir && trustedTempDir
|
|
3802
|
-
? resolveProtectedCandidate(managedTempDir)
|
|
3803
|
-
: undefined;
|
|
3763
|
+
const canonicalTempDir = this.getManagedTempDir(sessionKey);
|
|
3804
3764
|
return paths.some(candidate => {
|
|
3805
3765
|
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(workspace, candidate);
|
|
3806
3766
|
const canonical = resolveProtectedCandidate(absolute);
|
|
@@ -3895,7 +3855,7 @@ export class CodexRunner {
|
|
|
3895
3855
|
// distinguishes this explicit empty value from runners without a
|
|
3896
3856
|
// session-temp capability.
|
|
3897
3857
|
managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
|
|
3898
|
-
|
|
3858
|
+
sessionCredential: this.sessionTokenForSession(sessionKey),
|
|
3899
3859
|
allowProtectedMetadata: false,
|
|
3900
3860
|
selfAid: permissionContext?.selfAid,
|
|
3901
3861
|
channel: permissionContext?.channel,
|
|
@@ -4252,17 +4212,17 @@ export class CodexRunner {
|
|
|
4252
4212
|
return;
|
|
4253
4213
|
this.childThreadSessions.set(childThreadId, sessionId);
|
|
4254
4214
|
this.threadProjectPaths.set(childThreadId, path.resolve(projectPath));
|
|
4255
|
-
// Child approvals use the parent's capability boundary and
|
|
4256
|
-
//
|
|
4215
|
+
// Child approvals use the parent's capability boundary and session
|
|
4216
|
+
// credential. This is inheritance, not a second permission decision.
|
|
4257
4217
|
const parentBoundary = parentThreadId ? this.threadExternalToolBoundaries.get(parentThreadId) : undefined;
|
|
4258
4218
|
const parentFingerprint = parentThreadId ? this.threadExternalToolFingerprints.get(parentThreadId) : undefined;
|
|
4259
4219
|
if (parentBoundary)
|
|
4260
4220
|
this.threadExternalToolBoundaries.set(childThreadId, parentBoundary);
|
|
4261
4221
|
if (parentFingerprint)
|
|
4262
4222
|
this.threadExternalToolFingerprints.set(childThreadId, parentFingerprint);
|
|
4263
|
-
const
|
|
4264
|
-
if (
|
|
4265
|
-
this.
|
|
4223
|
+
const parentToken = parentThreadId ? this.ensureThreadCredentialLoaded(parentThreadId) : undefined;
|
|
4224
|
+
if (parentToken)
|
|
4225
|
+
this.ensureThreadCredential(childThreadId, parentToken);
|
|
4266
4226
|
}
|
|
4267
4227
|
unregisterChildThread(childThreadId) {
|
|
4268
4228
|
this.childThreadSessions.delete(childThreadId);
|
|
@@ -4270,8 +4230,8 @@ export class CodexRunner {
|
|
|
4270
4230
|
this.threadProjectPaths.delete(childThreadId);
|
|
4271
4231
|
this.threadExternalToolBoundaries.delete(childThreadId);
|
|
4272
4232
|
this.threadExternalToolFingerprints.delete(childThreadId);
|
|
4273
|
-
this.threadDelegationCarriers.delete(childThreadId);
|
|
4274
4233
|
this.clearTrackedApprovalItemsForThread(childThreadId);
|
|
4234
|
+
this.dropThreadCredential(childThreadId);
|
|
4275
4235
|
}
|
|
4276
4236
|
cleanupChildThreadsForSession(sessionId) {
|
|
4277
4237
|
for (const [childThreadId, ownerSessionId] of this.childThreadSessions) {
|
|
@@ -4411,10 +4371,14 @@ export class CodexRunner {
|
|
|
4411
4371
|
}
|
|
4412
4372
|
case 'item/agentMessage/delta': {
|
|
4413
4373
|
const itemId = typeof params.itemId === 'string' ? params.itemId : undefined;
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4374
|
+
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
4375
|
+
if (itemId && delta) {
|
|
4376
|
+
if (!state.streamedAgentMessageIds.has(itemId)) {
|
|
4377
|
+
state.streamedAgentMessageIds.add(itemId);
|
|
4378
|
+
yield { type: 'text_start', contentIndex: 0 };
|
|
4379
|
+
}
|
|
4380
|
+
state.agentMessageDeltaText.set(itemId, (state.agentMessageDeltaText.get(itemId) || '') + delta);
|
|
4381
|
+
yield { type: 'heartbeat', source: 'text' };
|
|
4418
4382
|
}
|
|
4419
4383
|
break;
|
|
4420
4384
|
}
|
|
@@ -4715,10 +4679,38 @@ export class CodexRunner {
|
|
|
4715
4679
|
switch (item.type) {
|
|
4716
4680
|
case 'agentMessage':
|
|
4717
4681
|
{
|
|
4682
|
+
if (item.questions != null) {
|
|
4683
|
+
if (item.id)
|
|
4684
|
+
state.agentMessageDeltaText.delete(item.id);
|
|
4685
|
+
try {
|
|
4686
|
+
const questions = parseAsyncUserQuestions(item.questions);
|
|
4687
|
+
if (!item.id || !state.turnId)
|
|
4688
|
+
throw new Error('Async question is missing its item or turn id');
|
|
4689
|
+
const key = JSON.stringify([state.threadId, state.turnId, item.id]);
|
|
4690
|
+
if (this.asyncQuestionItems.has(key))
|
|
4691
|
+
break;
|
|
4692
|
+
this.asyncQuestionItems.add(key);
|
|
4693
|
+
if (this.asyncQuestionItems.size > 2048)
|
|
4694
|
+
this.asyncQuestionItems.delete(this.asyncQuestionItems.values().next().value);
|
|
4695
|
+
yield { type: 'user_question', request: {
|
|
4696
|
+
providerThreadId: state.threadId, providerTurnId: state.turnId, itemId: item.id, questions,
|
|
4697
|
+
} };
|
|
4698
|
+
}
|
|
4699
|
+
catch (error) {
|
|
4700
|
+
yield { type: 'notice', severity: 'warn', subtype: 'invalid_async_question',
|
|
4701
|
+
message: `Codex 异步问题无法显示:${error instanceof Error ? error.message : String(error)}` };
|
|
4702
|
+
}
|
|
4703
|
+
break;
|
|
4704
|
+
}
|
|
4718
4705
|
const buffered = item.id ? state.agentMessageDeltaText.get(item.id) : undefined;
|
|
4719
4706
|
const text = typeof item.text === 'string' && item.text ? item.text : buffered;
|
|
4720
|
-
if (text)
|
|
4721
|
-
|
|
4707
|
+
if (text) {
|
|
4708
|
+
state.lastAssistantUuid = item.id || state.turnId;
|
|
4709
|
+
yield {
|
|
4710
|
+
type: 'text', text, assistantUuid: state.lastAssistantUuid,
|
|
4711
|
+
...(item.phase === 'commentary' || item.phase === 'final_answer' ? { phase: item.phase } : {}),
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4722
4714
|
if (item.id)
|
|
4723
4715
|
state.agentMessageDeltaText.delete(item.id);
|
|
4724
4716
|
}
|
|
@@ -4956,6 +4948,7 @@ export class CodexRunner {
|
|
|
4956
4948
|
return {
|
|
4957
4949
|
type: 'complete',
|
|
4958
4950
|
subtype: status === 'completed' ? 'success' : outputLimit ? 'max_output_tokens' : status,
|
|
4951
|
+
assistantUuid: state.lastAssistantUuid,
|
|
4959
4952
|
isError: status === 'failed',
|
|
4960
4953
|
errors: turn.error?.message ? [turn.error.message] : undefined,
|
|
4961
4954
|
terminalReason,
|
|
@@ -4969,126 +4962,36 @@ export class CodexRunner {
|
|
|
4969
4962
|
contextUsage,
|
|
4970
4963
|
};
|
|
4971
4964
|
}
|
|
4972
|
-
managedTempParent() {
|
|
4973
|
-
const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
|
4974
|
-
const base = ensureProcessManagedTempDir();
|
|
4975
|
-
return path.join(resolveProtectedCandidate(base), `evolcore-codex-${uid}`);
|
|
4976
|
-
}
|
|
4977
|
-
ensurePrivateDirectory(directory) {
|
|
4978
|
-
try {
|
|
4979
|
-
fs.mkdirSync(directory, { mode: 0o700 });
|
|
4980
|
-
}
|
|
4981
|
-
catch (error) {
|
|
4982
|
-
if (error?.code !== 'EEXIST')
|
|
4983
|
-
throw error;
|
|
4984
|
-
}
|
|
4985
|
-
const stat = fs.lstatSync(directory);
|
|
4986
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
4987
|
-
throw new Error(`managed temp path is not a real directory: ${directory}`);
|
|
4988
|
-
}
|
|
4989
|
-
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
4990
|
-
throw new Error(`managed temp path is owned by another uid: ${directory}`);
|
|
4991
|
-
}
|
|
4992
|
-
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
|
|
4993
|
-
fs.chmodSync(directory, 0o700);
|
|
4994
|
-
}
|
|
4995
|
-
ensureSessionTempDir(sessionId) {
|
|
4996
|
-
const existing = this.sessionTempDirs.get(sessionId);
|
|
4997
|
-
if (existing) {
|
|
4998
|
-
try {
|
|
4999
|
-
this.ensurePrivateDirectory(this.managedTempParent());
|
|
5000
|
-
this.ensurePrivateDirectory(existing);
|
|
5001
|
-
return existing;
|
|
5002
|
-
}
|
|
5003
|
-
catch (error) {
|
|
5004
|
-
this.sessionTempDirs.delete(sessionId);
|
|
5005
|
-
logger.warn(`[CodexRunner] existing managed TMPDIR is no longer usable: session=${sessionId} path=${existing} error=${error instanceof Error ? error.message : String(error)}`);
|
|
5006
|
-
return undefined;
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
try {
|
|
5010
|
-
const parent = this.managedTempParent();
|
|
5011
|
-
this.ensurePrivateDirectory(parent);
|
|
5012
|
-
this.ensurePrivateDirectory(path.join(parent, 'evolcore-locks'));
|
|
5013
|
-
// Include a per-runner nonce so a daemon restart never reuses contents
|
|
5014
|
-
// left behind by an unclean shutdown for the same EvolCore session id.
|
|
5015
|
-
const digest = createHash('sha256')
|
|
5016
|
-
.update(this.managedTempNonce)
|
|
5017
|
-
.update('\0')
|
|
5018
|
-
.update(sessionId)
|
|
5019
|
-
.digest('hex')
|
|
5020
|
-
.slice(0, 24);
|
|
5021
|
-
const directory = path.join(parent, digest);
|
|
5022
|
-
this.ensurePrivateDirectory(directory);
|
|
5023
|
-
this.sessionTempDirs.set(sessionId, directory);
|
|
5024
|
-
registerManagedTempDirForExitCleanup(directory, parent);
|
|
5025
|
-
return directory;
|
|
5026
|
-
}
|
|
5027
|
-
catch (error) {
|
|
5028
|
-
logger.warn(`[CodexRunner] managed TMPDIR unavailable for session=${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5029
|
-
return undefined;
|
|
5030
|
-
}
|
|
5031
|
-
}
|
|
5032
|
-
isTrustedSessionTempDir(directory) {
|
|
5033
|
-
return isTrustedManagedTempDirectory(directory, this.managedTempParent());
|
|
5034
|
-
}
|
|
5035
4965
|
getManagedTempDir(sessionId) {
|
|
5036
|
-
const directory =
|
|
5037
|
-
|
|
5038
|
-
return undefined;
|
|
5039
|
-
return resolveProtectedCandidate(directory);
|
|
5040
|
-
}
|
|
5041
|
-
cleanupSessionTempDir(sessionId) {
|
|
5042
|
-
const directory = this.sessionTempDirs.get(sessionId);
|
|
5043
|
-
this.sessionTempDirs.delete(sessionId);
|
|
5044
|
-
if (!directory)
|
|
5045
|
-
return;
|
|
5046
|
-
// Reuse the same ownership, mode, lexical-parent, and canonicalization
|
|
5047
|
-
// checks used at execution time. If the path changed, leave it in place
|
|
5048
|
-
// rather than recursively removing an attacker-selected target.
|
|
5049
|
-
if (!this.isTrustedSessionTempDir(directory)) {
|
|
5050
|
-
managedTempDirsForExitCleanup.delete(directory);
|
|
5051
|
-
logger.warn(`[CodexRunner] refused to clean unmanaged TMPDIR: session=${sessionId} path=${directory}`);
|
|
5052
|
-
return;
|
|
5053
|
-
}
|
|
5054
|
-
const candidate = path.resolve(directory);
|
|
5055
|
-
try {
|
|
5056
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
5057
|
-
managedTempDirsForExitCleanup.delete(directory);
|
|
5058
|
-
}
|
|
5059
|
-
catch (error) {
|
|
5060
|
-
logger.warn(`[CodexRunner] failed to clean managed TMPDIR: session=${sessionId} path=${candidate} error=${error instanceof Error ? error.message : String(error)}`);
|
|
5061
|
-
}
|
|
4966
|
+
const directory = ensureSessionRuntimeDir(sessionId);
|
|
4967
|
+
return directory ? resolveProtectedCandidate(directory) : undefined;
|
|
5062
4968
|
}
|
|
5063
4969
|
managedTempInstruction(sessionId) {
|
|
5064
|
-
const managedTempDir =
|
|
4970
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
5065
4971
|
if (!managedTempDir)
|
|
5066
4972
|
return undefined;
|
|
5067
|
-
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp
|
|
4973
|
+
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
|
|
5068
4974
|
}
|
|
5069
|
-
buildEvolcoreShellEnvironmentConfig(sessionId,
|
|
5070
|
-
const managedTempDir =
|
|
4975
|
+
buildEvolcoreShellEnvironmentConfig(sessionId, sessionToken, taskDelegationToken) {
|
|
4976
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
5071
4977
|
// The managed-hook credential rides the thread's shell environment so the
|
|
5072
4978
|
// PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
|
|
5073
4979
|
// It follows the thread (start AND resume) rather than the daemon process.
|
|
5074
|
-
const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
|
|
5075
4980
|
return {
|
|
5076
4981
|
shell_environment_policy: {
|
|
5077
4982
|
set: {
|
|
5078
4983
|
EVOLCORE_SESSION_ID: sessionId,
|
|
5079
4984
|
EVOLCORE_HOME: resolveRoot(),
|
|
5080
4985
|
...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
|
|
5081
|
-
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(
|
|
5082
|
-
...(taskDelegationToken
|
|
5083
|
-
? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
|
|
5084
|
-
: {}),
|
|
4986
|
+
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(managedTempDir, 'locks') } : {}),
|
|
4987
|
+
...(taskDelegationToken ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken } : {}),
|
|
5085
4988
|
...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
|
|
5086
4989
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
5087
4990
|
},
|
|
5088
4991
|
},
|
|
5089
4992
|
};
|
|
5090
4993
|
}
|
|
5091
|
-
sanitizeThreadEnvironmentConfig(config, permissionMode) {
|
|
4994
|
+
sanitizeThreadEnvironmentConfig(config, permissionMode, taskDelegationToken) {
|
|
5092
4995
|
if (!config)
|
|
5093
4996
|
return null;
|
|
5094
4997
|
const policy = this.configRecord(config.shell_environment_policy);
|
|
@@ -5098,11 +5001,40 @@ export class CodexRunner {
|
|
|
5098
5001
|
const safeSet = sanitizeShellExecutionEnvironment(set, {
|
|
5099
5002
|
lockExecutableResolution: permissionMode === 'readonly',
|
|
5100
5003
|
});
|
|
5004
|
+
const sessionToken = typeof safeSet[CODEX_SESSION_TOKEN_ENV] === 'string'
|
|
5005
|
+
? safeSet[CODEX_SESSION_TOKEN_ENV]
|
|
5006
|
+
: undefined;
|
|
5007
|
+
for (const name of Object.keys(safeSet)) {
|
|
5008
|
+
const normalized = name.toUpperCase();
|
|
5009
|
+
if (normalized === AGENT_DELEGATION_TOKEN_ENV || normalized === CODEX_SESSION_TOKEN_ENV) {
|
|
5010
|
+
delete safeSet[name];
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
if (sessionToken)
|
|
5014
|
+
safeSet[CODEX_SESSION_TOKEN_ENV] = sessionToken;
|
|
5015
|
+
const existingExclude = Array.isArray(policy.exclude)
|
|
5016
|
+
? policy.exclude.filter((name) => typeof name === 'string')
|
|
5017
|
+
: [];
|
|
5018
|
+
const carriesTaskDelegation = permissionMode === 'fullaccess'
|
|
5019
|
+
&& !!taskDelegationToken;
|
|
5020
|
+
if (carriesTaskDelegation)
|
|
5021
|
+
safeSet[AGENT_DELEGATION_TOKEN_ENV] = taskDelegationToken;
|
|
5022
|
+
const filteredExclude = existingExclude.filter(name => {
|
|
5023
|
+
const normalized = name.toUpperCase();
|
|
5024
|
+
return normalized !== AGENT_DELEGATION_TOKEN_ENV
|
|
5025
|
+
&& normalized !== CODEX_SESSION_TOKEN_ENV;
|
|
5026
|
+
});
|
|
5027
|
+
const exclude = carriesTaskDelegation
|
|
5028
|
+
? filteredExclude
|
|
5029
|
+
: [...new Set([...filteredExclude, AGENT_DELEGATION_TOKEN_ENV])];
|
|
5101
5030
|
return {
|
|
5102
5031
|
...config,
|
|
5103
5032
|
shell_environment_policy: {
|
|
5104
5033
|
...policy,
|
|
5105
5034
|
set: safeSet,
|
|
5035
|
+
// The task token is invocation-scoped. Persistent non-fullaccess
|
|
5036
|
+
// threads must not inherit a stale value from the app-server process.
|
|
5037
|
+
exclude,
|
|
5106
5038
|
},
|
|
5107
5039
|
};
|
|
5108
5040
|
}
|
|
@@ -5119,21 +5051,23 @@ export class CodexRunner {
|
|
|
5119
5051
|
this.activeSessions.clear();
|
|
5120
5052
|
this.provisionalSessions.clear();
|
|
5121
5053
|
this.activeTurns.clear();
|
|
5054
|
+
for (const childThreadId of [...this.childThreadSessions.keys()])
|
|
5055
|
+
this.unregisterChildThread(childThreadId);
|
|
5122
5056
|
this.activeSubagentTurns.clear();
|
|
5123
5057
|
this.childThreadSessions.clear();
|
|
5124
5058
|
this.threadProjectPaths.clear();
|
|
5125
5059
|
this.threadExternalToolFingerprints.clear();
|
|
5126
5060
|
this.threadExternalToolBoundaries.clear();
|
|
5127
5061
|
this.trackedApprovalItems.clear();
|
|
5128
|
-
this.threadDelegationCarriers.clear();
|
|
5129
5062
|
this.pendingInterrupts.clear();
|
|
5063
|
+
for (const record of this.unconfirmedInterrupts.values())
|
|
5064
|
+
record.unsubscribe?.();
|
|
5065
|
+
this.unconfirmedInterrupts.clear();
|
|
5130
5066
|
this.steeringTails.clear();
|
|
5131
5067
|
this.permissionContexts.clear();
|
|
5132
5068
|
this.chatModes.clear();
|
|
5133
5069
|
this.pruneThreadOperationLocks();
|
|
5134
5070
|
this.threadOperationLocks.clear();
|
|
5135
|
-
for (const sessionId of [...this.sessionTempDirs.keys()])
|
|
5136
|
-
this.cleanupSessionTempDir(sessionId);
|
|
5137
5071
|
await Promise.all([
|
|
5138
5072
|
this.appServerClient?.close(),
|
|
5139
5073
|
this.fullAccessAppServerClient?.close(),
|
|
@@ -5172,6 +5106,14 @@ export class CodexAgentPlugin {
|
|
|
5172
5106
|
},
|
|
5173
5107
|
},
|
|
5174
5108
|
};
|
|
5175
|
-
return {
|
|
5109
|
+
return {
|
|
5110
|
+
evolagentName: agent.name,
|
|
5111
|
+
baseagent: 'codex',
|
|
5112
|
+
agent: new CodexRunner(merged, callbacks, {
|
|
5113
|
+
agentAid: agent.config.aid,
|
|
5114
|
+
agentConfig: agent.config,
|
|
5115
|
+
codexVersion: availability.version,
|
|
5116
|
+
}),
|
|
5117
|
+
};
|
|
5176
5118
|
}
|
|
5177
5119
|
}
|