evolcore 0.0.26 → 0.0.27
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 +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- 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} +12 -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 +522 -233
- 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 +35 -23
- package/dist/cli/diag.js +2 -2
- 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 +9 -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 +16 -2
- 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 +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- 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 +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- 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 +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- 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 +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- 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 +7 -7
- 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 +34 -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 +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -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/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- 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 +15 -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,11 +1,11 @@
|
|
|
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
10
|
import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
|
|
11
11
|
import { logger } from '../utils/logger.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) {
|
|
@@ -361,7 +307,6 @@ export async function getCodexAppServerAvailabilityAsync() {
|
|
|
361
307
|
: `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
|
|
362
308
|
}
|
|
363
309
|
}
|
|
364
|
-
export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
|
|
365
310
|
export class CodexRunner {
|
|
366
311
|
name = 'codex';
|
|
367
312
|
// turn/start persists a new user turn before runQuery resolves. App-server
|
|
@@ -380,14 +325,12 @@ export class CodexRunner {
|
|
|
380
325
|
threadProjectPaths = new Map();
|
|
381
326
|
/** Child Codex threads inherit the EvolCore session that started them. */
|
|
382
327
|
childThreadSessions = new Map();
|
|
383
|
-
sessionTempDirs = new Map();
|
|
384
|
-
managedTempNonce = randomBytes(16).toString('hex');
|
|
385
328
|
threadExternalToolFingerprints = new Map();
|
|
386
329
|
threadExternalToolBoundaries = new Map();
|
|
387
330
|
trackedApprovalItems = new Map();
|
|
388
331
|
pendingToolDenials = new Map();
|
|
389
|
-
threadDelegationCarriers = new Map();
|
|
390
332
|
pendingInterrupts = new Map();
|
|
333
|
+
unconfirmedInterrupts = new Map();
|
|
391
334
|
steeringTails = new Map();
|
|
392
335
|
threadOperationLocks = new Map();
|
|
393
336
|
activeOperationReleases = new Map();
|
|
@@ -410,6 +353,9 @@ export class CodexRunner {
|
|
|
410
353
|
codexSessionFileAdapter = new CodexSessionFileAdapter();
|
|
411
354
|
/** threadId → managed-hook credential, issued by this runner's agent only. */
|
|
412
355
|
threadCredentials = new Map();
|
|
356
|
+
/** @deprecated Compatibility alias for direct runner integrations. */
|
|
357
|
+
threadDelegationCarriers = this.threadCredentials;
|
|
358
|
+
asyncQuestionItems = new Set();
|
|
413
359
|
constructor(config, callbacks, runtime) {
|
|
414
360
|
// Compatibility for legacy direct callers that still pass the removed
|
|
415
361
|
// internal fields in a synthetic config. AgentLoader uses runtime binding.
|
|
@@ -426,6 +372,7 @@ export class CodexRunner {
|
|
|
426
372
|
configuredHeaders: this.resolvedConfig.headers,
|
|
427
373
|
});
|
|
428
374
|
this.capabilities = {
|
|
375
|
+
taskRecovery: true,
|
|
429
376
|
clear: false,
|
|
430
377
|
compact: true,
|
|
431
378
|
fork: true,
|
|
@@ -642,16 +589,13 @@ export class CodexRunner {
|
|
|
642
589
|
}
|
|
643
590
|
this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
|
|
644
591
|
}
|
|
645
|
-
|
|
646
|
-
return randomBytes(32).toString('base64url');
|
|
647
|
-
}
|
|
648
|
-
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
|
|
592
|
+
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedCredentialToken, executable) {
|
|
649
593
|
const parseOptions = {
|
|
650
594
|
allowManagedTmpDir: true,
|
|
651
595
|
dialect,
|
|
652
596
|
verifyShellExecutable: process.platform === 'win32',
|
|
653
597
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
654
|
-
...(
|
|
598
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
655
599
|
};
|
|
656
600
|
const classification = classifyEvolcoreShellCommand(command, parseOptions);
|
|
657
601
|
if (classification.kind === 'bounded-output')
|
|
@@ -678,10 +622,10 @@ export class CodexRunner {
|
|
|
678
622
|
}
|
|
679
623
|
return {
|
|
680
624
|
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
681
|
-
...(parsed.
|
|
625
|
+
...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
|
|
682
626
|
};
|
|
683
627
|
}
|
|
684
|
-
resolveCanonicalEcApproval(toolInput, managedTempDir,
|
|
628
|
+
resolveCanonicalEcApproval(toolInput, managedTempDir, expectedCredentialToken) {
|
|
685
629
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
686
630
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
687
631
|
? toolInput.commandArgv
|
|
@@ -702,12 +646,12 @@ export class CodexRunner {
|
|
|
702
646
|
? resolveCodexShellCarrierArgv(explicitArgv)
|
|
703
647
|
: undefined;
|
|
704
648
|
if (explicitCarrier !== undefined) {
|
|
705
|
-
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect,
|
|
649
|
+
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedCredentialToken, explicitCarrier.executable);
|
|
706
650
|
}
|
|
707
651
|
if (hasCodexCmdCarrierEcIntent(toolInput)) {
|
|
708
652
|
const parsed = parseCodexToolCommand(toolInput, {
|
|
709
653
|
verifyShellExecutable: process.platform === 'win32',
|
|
710
|
-
...(
|
|
654
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
711
655
|
});
|
|
712
656
|
if (!parsed.ok)
|
|
713
657
|
return { parseFailure: parsed };
|
|
@@ -719,7 +663,7 @@ export class CodexRunner {
|
|
|
719
663
|
} }
|
|
720
664
|
: {
|
|
721
665
|
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
722
|
-
...(parsed.
|
|
666
|
+
...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
|
|
723
667
|
};
|
|
724
668
|
}
|
|
725
669
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
@@ -739,7 +683,7 @@ export class CodexRunner {
|
|
|
739
683
|
allowManagedTmpDir: true,
|
|
740
684
|
verifyShellExecutable: process.platform === 'win32',
|
|
741
685
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
742
|
-
...(
|
|
686
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
743
687
|
});
|
|
744
688
|
if (canonicalCommand?.[0] === 'ec') {
|
|
745
689
|
return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
|
|
@@ -774,7 +718,7 @@ export class CodexRunner {
|
|
|
774
718
|
return true;
|
|
775
719
|
const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
|
|
776
720
|
if (shellCarrier?.dialect === 'powershell'
|
|
777
|
-
&&
|
|
721
|
+
&& hasPowerShellCredentialAssignment(shellCarrier.command))
|
|
778
722
|
return true;
|
|
779
723
|
return hasCodexCmdCarrierEcIntent(toolInput)
|
|
780
724
|
|| shellCarrier !== undefined
|
|
@@ -787,20 +731,20 @@ export class CodexRunner {
|
|
|
787
731
|
}
|
|
788
732
|
armApprovedDelegationCommand(sessionId, argv) {
|
|
789
733
|
const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
790
|
-
const
|
|
734
|
+
const sessionToken = threadId ? this.ensureThreadCredentialLoaded(threadId) : undefined;
|
|
791
735
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
792
736
|
const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
|
|
793
|
-
if (!
|
|
737
|
+
if (!sessionToken || !commandHash || !arm)
|
|
794
738
|
return false;
|
|
795
739
|
try {
|
|
796
|
-
return arm(
|
|
740
|
+
return arm(sessionToken, commandHash);
|
|
797
741
|
}
|
|
798
742
|
catch (error) {
|
|
799
743
|
logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
800
744
|
return false;
|
|
801
745
|
}
|
|
802
746
|
}
|
|
803
|
-
|
|
747
|
+
sessionTokenForSession(sessionId, threadId) {
|
|
804
748
|
const candidates = [
|
|
805
749
|
threadId,
|
|
806
750
|
this.activeSessions.get(sessionId),
|
|
@@ -809,45 +753,13 @@ export class CodexRunner {
|
|
|
809
753
|
for (const candidate of candidates) {
|
|
810
754
|
if (!candidate)
|
|
811
755
|
continue;
|
|
812
|
-
const
|
|
813
|
-
if (
|
|
814
|
-
return
|
|
756
|
+
const token = this.ensureThreadCredentialLoaded(candidate);
|
|
757
|
+
if (token)
|
|
758
|
+
return token;
|
|
815
759
|
}
|
|
816
760
|
return undefined;
|
|
817
761
|
}
|
|
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) {
|
|
762
|
+
async commitProvisionalThread(sessionId, threadId, turn) {
|
|
851
763
|
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
852
764
|
return false;
|
|
853
765
|
const bindingResult = this.onSessionIdUpdate
|
|
@@ -871,10 +783,6 @@ export class CodexRunner {
|
|
|
871
783
|
return false;
|
|
872
784
|
this.provisionalSessions.delete(sessionId);
|
|
873
785
|
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
786
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
879
787
|
return true;
|
|
880
788
|
}
|
|
@@ -907,9 +815,9 @@ export class CodexRunner {
|
|
|
907
815
|
return;
|
|
908
816
|
this.provisionalSessions.delete(sessionId);
|
|
909
817
|
this.threadProjectPaths.delete(threadId);
|
|
910
|
-
this.threadDelegationCarriers.delete(threadId);
|
|
911
818
|
this.unbindExternalToolBoundary(threadId);
|
|
912
819
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
820
|
+
this.dropThreadCredential(threadId);
|
|
913
821
|
// A stale provisional start can finish after a replacement run has
|
|
914
822
|
// already activated another parent thread for this session. Do not tear
|
|
915
823
|
// down the replacement run's child mappings in that race.
|
|
@@ -919,31 +827,6 @@ export class CodexRunner {
|
|
|
919
827
|
}
|
|
920
828
|
this.pruneThreadOperationLocks();
|
|
921
829
|
}
|
|
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
830
|
resetAppServerClient() {
|
|
948
831
|
const clients = [this.appServerClient, this.fullAccessAppServerClient]
|
|
949
832
|
.filter((client) => !!client);
|
|
@@ -952,8 +835,8 @@ export class CodexRunner {
|
|
|
952
835
|
this.threadSubscriptions.clear();
|
|
953
836
|
this.threadExternalToolFingerprints.clear();
|
|
954
837
|
this.threadExternalToolBoundaries.clear();
|
|
955
|
-
for (const childThreadId of this.childThreadSessions.keys())
|
|
956
|
-
this.
|
|
838
|
+
for (const childThreadId of [...this.childThreadSessions.keys()])
|
|
839
|
+
this.unregisterChildThread(childThreadId);
|
|
957
840
|
// Child turn ids belong to the discarded app-server connection. Keeping
|
|
958
841
|
// them would make a later session interrupt attempt target stale threads
|
|
959
842
|
// through the replacement client.
|
|
@@ -961,7 +844,6 @@ export class CodexRunner {
|
|
|
961
844
|
this.childThreadSessions.clear();
|
|
962
845
|
this.trackedApprovalItems.clear();
|
|
963
846
|
this.pendingToolDenials.clear();
|
|
964
|
-
this.threadDelegationCarriers.clear();
|
|
965
847
|
for (const client of clients) {
|
|
966
848
|
client.close().catch(error => {
|
|
967
849
|
logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
|
|
@@ -1308,47 +1190,53 @@ export class CodexRunner {
|
|
|
1308
1190
|
}
|
|
1309
1191
|
buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1310
1192
|
const profileName = this.permissionProfileName(sessionId);
|
|
1311
|
-
const managedTempDir =
|
|
1193
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
1312
1194
|
const network = {
|
|
1313
1195
|
enabled: true,
|
|
1314
1196
|
unix_sockets: { [resolvePaths().instanceSocket]: 'allow' },
|
|
1315
1197
|
};
|
|
1198
|
+
const filesystem = {
|
|
1199
|
+
...buildCodexProtectedFilesystemRules(resolveRoot(), {
|
|
1200
|
+
denyLClassRead: mode === 'readonly',
|
|
1201
|
+
// The app-server itself is launched behind EvolCore's Linux
|
|
1202
|
+
// H-class guard, which already mounts every L-class directory
|
|
1203
|
+
// read-only. Avoid Codex's per-file glob expansion inside that
|
|
1204
|
+
// nested mount namespace; it can fail while creating a missing
|
|
1205
|
+
// target such as triggers/history.jsonl.
|
|
1206
|
+
expandLClassReadDeny: process.platform !== 'linux',
|
|
1207
|
+
}),
|
|
1208
|
+
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
1209
|
+
...(bootstrapAgentMdPath ? {
|
|
1210
|
+
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1211
|
+
} : {}),
|
|
1212
|
+
...(personalMdPath ? {
|
|
1213
|
+
[path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1214
|
+
} : {}),
|
|
1215
|
+
...(mode !== 'readonly' && managedTempDir ? {
|
|
1216
|
+
[managedTempDir]: 'write',
|
|
1217
|
+
[managedTempDir + '/**']: 'write',
|
|
1218
|
+
[path.join(managedTempDir, 'locks')]: 'write',
|
|
1219
|
+
[path.join(managedTempDir, 'locks') + '/**']: 'write',
|
|
1220
|
+
} : {}),
|
|
1221
|
+
};
|
|
1222
|
+
// Codex resolves this complete profile before serializing the Windows
|
|
1223
|
+
// helper payload. Check after all runner-owned additions, not merely the
|
|
1224
|
+
// protected-path base projection, so Git and managed-temp rules cannot
|
|
1225
|
+
// push an otherwise valid profile back over CreateProcess' limit.
|
|
1226
|
+
assertWindowsCodexFilesystemRuleBudget(filesystem);
|
|
1316
1227
|
return {
|
|
1317
1228
|
default_permissions: profileName,
|
|
1318
1229
|
permissions: {
|
|
1319
1230
|
[profileName]: {
|
|
1320
1231
|
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
|
-
},
|
|
1232
|
+
filesystem,
|
|
1345
1233
|
network,
|
|
1346
1234
|
},
|
|
1347
1235
|
},
|
|
1348
1236
|
};
|
|
1349
1237
|
}
|
|
1350
|
-
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1351
|
-
const decision = resolvePhaseOneExecutionSandbox(mode,
|
|
1238
|
+
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
|
|
1239
|
+
const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
|
|
1352
1240
|
return {
|
|
1353
1241
|
decision,
|
|
1354
1242
|
permissionProfileConfig: decision.state === 'active'
|
|
@@ -1386,6 +1274,26 @@ export class CodexRunner {
|
|
|
1386
1274
|
}
|
|
1387
1275
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
1388
1276
|
setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
|
|
1277
|
+
async steerAsyncQuestionAnswer(sessionId, answer) {
|
|
1278
|
+
const active = this.activeTurns.get(sessionId);
|
|
1279
|
+
if (!active)
|
|
1280
|
+
return 'inactive';
|
|
1281
|
+
if (active.threadId !== answer.providerThreadId)
|
|
1282
|
+
throw new Error('Async question backend has changed');
|
|
1283
|
+
const client = this.getAppServerClientForThread(active.threadId);
|
|
1284
|
+
try {
|
|
1285
|
+
await client.turnSteer(active.threadId, active.turnId, [{
|
|
1286
|
+
type: 'text', text: formatAsyncQuestionAnswer(answer), text_elements: [],
|
|
1287
|
+
}]);
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
if (error.code === 'CODEX_NO_ACTIVE_TURN')
|
|
1291
|
+
return 'inactive';
|
|
1292
|
+
// Transport failures are ambiguous: never risk submitting the same answer twice.
|
|
1293
|
+
throw error;
|
|
1294
|
+
}
|
|
1295
|
+
return 'accepted';
|
|
1296
|
+
}
|
|
1389
1297
|
injectUserMessage(sessionId, text) {
|
|
1390
1298
|
// Bind queued reminders to the turn that generated them. A completed
|
|
1391
1299
|
// turn may be replaced before the serialized steer reaches app-server;
|
|
@@ -1722,9 +1630,11 @@ export class CodexRunner {
|
|
|
1722
1630
|
const knownThreadId = topicBinding
|
|
1723
1631
|
? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
|
|
1724
1632
|
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1725
|
-
const operationKey = this.threadOperationKey(knownThreadId, sessionId);
|
|
1633
|
+
const operationKey = this.threadOperationKey(modelOverride?.recovery?.agentSessionId || knownThreadId, sessionId);
|
|
1634
|
+
await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
|
|
1726
1635
|
const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
|
|
1727
1636
|
try {
|
|
1637
|
+
await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
|
|
1728
1638
|
const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
|
|
1729
1639
|
this.activeOperationReleases.set(sessionId, release);
|
|
1730
1640
|
return this.holdThreadOperation(stream, sessionId, release);
|
|
@@ -1754,6 +1664,14 @@ export class CodexRunner {
|
|
|
1754
1664
|
logger.debug(`[CodexRunner] persisted main backend lookup failed; using local cache: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1755
1665
|
}
|
|
1756
1666
|
}
|
|
1667
|
+
const recoverySessionId = modelOverride?.recovery?.agentSessionId;
|
|
1668
|
+
if (modelOverride?.recovery && (!recoverySessionId || agentSessionId !== recoverySessionId)) {
|
|
1669
|
+
throw new Error('Task recovery requires the original Codex thread binding');
|
|
1670
|
+
}
|
|
1671
|
+
if (modelOverride?.asyncQuestionAnswer && agentSessionId !== modelOverride.asyncQuestionAnswer.providerThreadId) {
|
|
1672
|
+
throw new Error('Async question reply requires its original Codex backend');
|
|
1673
|
+
}
|
|
1674
|
+
await this.assertInterruptSettled(sessionId, agentSessionId);
|
|
1757
1675
|
let resumingThread = !!agentSessionId;
|
|
1758
1676
|
const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
1759
1677
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
@@ -1774,6 +1692,10 @@ export class CodexRunner {
|
|
|
1774
1692
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1775
1693
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
1776
1694
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
1695
|
+
const userInputConfig = { features: {
|
|
1696
|
+
default_mode_request_user_input: modelOverride?.enableRequestUserInput
|
|
1697
|
+
?? this.resolvedConfig.enableRequestUserInput ?? true,
|
|
1698
|
+
} };
|
|
1777
1699
|
const externalToolConfig = callMode === 'fullaccess'
|
|
1778
1700
|
? {}
|
|
1779
1701
|
: await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
|
|
@@ -1781,25 +1703,21 @@ export class CodexRunner {
|
|
|
1781
1703
|
await this.assertProactiveHookAvailability(sessionId, appServer);
|
|
1782
1704
|
const managedTempInstruction = this.managedTempInstruction(sessionId);
|
|
1783
1705
|
const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
|
|
1784
|
-
let
|
|
1785
|
-
? this.
|
|
1706
|
+
let threadSessionToken = agentSessionId
|
|
1707
|
+
? this.ensureThreadCredential(agentSessionId)
|
|
1708
|
+
: this.createThreadCredential();
|
|
1709
|
+
if (!threadSessionToken)
|
|
1710
|
+
throw new Error('Codex thread session credential is unavailable');
|
|
1711
|
+
const persistentRuntimeEnv = runtimeEnv
|
|
1712
|
+
? Object.fromEntries(Object.entries(runtimeEnv).filter(([name]) => callMode === 'fullaccess' || name.toUpperCase() !== AGENT_DELEGATION_TOKEN_ENV))
|
|
1786
1713
|
: 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
1714
|
let threadOptions = {
|
|
1797
1715
|
model: callModel,
|
|
1798
1716
|
effort: callEffort,
|
|
1799
1717
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1800
1718
|
approvalsReviewer: 'user',
|
|
1801
1719
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1802
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig,
|
|
1720
|
+
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
1721
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1804
1722
|
};
|
|
1805
1723
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
@@ -1825,12 +1743,12 @@ export class CodexRunner {
|
|
|
1825
1743
|
// A legacy main session can retain a provider ID after its rollout was
|
|
1826
1744
|
// removed externally. It is safe to recover before input submission;
|
|
1827
1745
|
// topic bindings remain authoritative and fail closed.
|
|
1828
|
-
if (topicBinding || !isCodexNoRolloutError(error))
|
|
1746
|
+
if (topicBinding || recoverySessionId || modelOverride?.asyncQuestionAnswer || !isCodexNoRolloutError(error))
|
|
1829
1747
|
throw error;
|
|
1830
1748
|
if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
|
|
1831
1749
|
throw error;
|
|
1832
1750
|
const missingThreadId = agentSessionId;
|
|
1833
|
-
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId
|
|
1751
|
+
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId);
|
|
1834
1752
|
if (cleared !== 'cleared' && cleared !== 'already_clear') {
|
|
1835
1753
|
const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
|
|
1836
1754
|
recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
|
|
@@ -1839,17 +1757,15 @@ export class CodexRunner {
|
|
|
1839
1757
|
if (this.activeSessions.get(sessionId) === missingThreadId)
|
|
1840
1758
|
this.activeSessions.delete(sessionId);
|
|
1841
1759
|
this.threadProjectPaths.delete(missingThreadId);
|
|
1842
|
-
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1843
1760
|
this.unbindExternalToolBoundary(missingThreadId);
|
|
1844
1761
|
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1845
1762
|
this.dropThreadCredential(missingThreadId);
|
|
1846
1763
|
agentSessionId = undefined;
|
|
1847
1764
|
resumingThread = false;
|
|
1848
|
-
|
|
1849
|
-
delegationCarrierCandidate = this.createDelegationCarrier();
|
|
1765
|
+
threadSessionToken = this.createThreadCredential();
|
|
1850
1766
|
threadOptions = {
|
|
1851
1767
|
...threadOptions,
|
|
1852
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig,
|
|
1768
|
+
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
1769
|
};
|
|
1854
1770
|
logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
|
|
1855
1771
|
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
@@ -1862,43 +1778,12 @@ export class CodexRunner {
|
|
|
1862
1778
|
let threadId = threadResponse.thread?.id || agentSessionId;
|
|
1863
1779
|
if (!threadId)
|
|
1864
1780
|
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 };
|
|
1781
|
+
if (recoverySessionId && threadId !== recoverySessionId) {
|
|
1782
|
+
throw new Error('Task recovery cannot replace the original Codex thread');
|
|
1899
1783
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1784
|
+
this.markThreadSubscribed(appServer, threadId);
|
|
1785
|
+
if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
|
|
1786
|
+
throw new Error('Codex thread session credential could not be bound');
|
|
1902
1787
|
}
|
|
1903
1788
|
const turn = modelOverride?.turn;
|
|
1904
1789
|
if (!resumingThread) {
|
|
@@ -1906,9 +1791,6 @@ export class CodexRunner {
|
|
|
1906
1791
|
// has been accepted. In particular, a pending interrupt must not leave
|
|
1907
1792
|
// a durable ID for a thread that never produced a rollout.
|
|
1908
1793
|
this.provisionalSessions.set(sessionId, threadId);
|
|
1909
|
-
if (carrierToPublish) {
|
|
1910
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1911
|
-
}
|
|
1912
1794
|
}
|
|
1913
1795
|
else {
|
|
1914
1796
|
agentSessionId = threadId;
|
|
@@ -1928,10 +1810,6 @@ export class CodexRunner {
|
|
|
1928
1810
|
throw error;
|
|
1929
1811
|
}
|
|
1930
1812
|
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
1813
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
1936
1814
|
}
|
|
1937
1815
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
@@ -1939,7 +1817,11 @@ export class CodexRunner {
|
|
|
1939
1817
|
const controller = new AbortController();
|
|
1940
1818
|
this.activeAbortControllers.set(sessionId, controller);
|
|
1941
1819
|
const tempFiles = [];
|
|
1942
|
-
const
|
|
1820
|
+
const answer = modelOverride?.asyncQuestionAnswer;
|
|
1821
|
+
if (answer && answer.providerThreadId !== threadId) {
|
|
1822
|
+
throw new Error('Async question reply cannot resume a different Codex thread');
|
|
1823
|
+
}
|
|
1824
|
+
const input = this.buildAppServerInput(answer ? formatAsyncQuestionAnswer(answer) : prompt, images, tempFiles, sessionId, runtimeEnv);
|
|
1943
1825
|
const queue = new AsyncEventQueue();
|
|
1944
1826
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
1945
1827
|
const state = {
|
|
@@ -2053,7 +1935,7 @@ export class CodexRunner {
|
|
|
2053
1935
|
if (!persisted)
|
|
2054
1936
|
return false;
|
|
2055
1937
|
try {
|
|
2056
|
-
const committed = await this.commitProvisionalThread(sessionId, threadId,
|
|
1938
|
+
const committed = await this.commitProvisionalThread(sessionId, threadId, turn);
|
|
2057
1939
|
if (!committed) {
|
|
2058
1940
|
const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
|
|
2059
1941
|
staleError.code = 'RUNNER_START_SUPERSEDED';
|
|
@@ -2164,16 +2046,65 @@ export class CodexRunner {
|
|
|
2164
2046
|
}
|
|
2165
2047
|
}
|
|
2166
2048
|
async interruptAppServerTurn(threadId, turnId) {
|
|
2049
|
+
const sessionId = this.findSessionKeyByThread(threadId);
|
|
2050
|
+
const subscribed = this.threadSubscriptions.get(threadId);
|
|
2051
|
+
const client = subscribed
|
|
2052
|
+
? subscribed.client
|
|
2053
|
+
: this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
|
|
2054
|
+
const key = JSON.stringify([threadId, turnId]);
|
|
2055
|
+
const previous = this.unconfirmedInterrupts.get(key);
|
|
2056
|
+
previous?.unsubscribe?.();
|
|
2057
|
+
const record = { sessionId, threadId, turnId, client };
|
|
2058
|
+
this.unconfirmedInterrupts.set(key, record);
|
|
2059
|
+
let confirm;
|
|
2060
|
+
const confirmed = new Promise(resolve => { confirm = resolve; });
|
|
2061
|
+
const clear = () => {
|
|
2062
|
+
if (this.unconfirmedInterrupts.get(key) === record)
|
|
2063
|
+
this.unconfirmedInterrupts.delete(key);
|
|
2064
|
+
record.unsubscribe?.();
|
|
2065
|
+
confirm();
|
|
2066
|
+
};
|
|
2067
|
+
record.unsubscribe = client.onNotification?.(notification => {
|
|
2068
|
+
const params = notification.params ?? {};
|
|
2069
|
+
if ((params.threadId ?? params.thread_id) !== threadId)
|
|
2070
|
+
return;
|
|
2071
|
+
if (notification.method === 'thread/closed'
|
|
2072
|
+
|| (notification.method === 'turn/completed'
|
|
2073
|
+
&& (params.turn?.id ?? params.turnId ?? params.turn_id) === turnId))
|
|
2074
|
+
clear();
|
|
2075
|
+
});
|
|
2167
2076
|
try {
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2077
|
+
await withInterruptDeadline(Promise.race([
|
|
2078
|
+
client.turnInterrupt(threadId, turnId).then(accepted => {
|
|
2079
|
+
if (!accepted)
|
|
2080
|
+
throw new Error('Codex interrupt was not accepted');
|
|
2081
|
+
}),
|
|
2082
|
+
confirmed,
|
|
2083
|
+
]), 'Codex turn/interrupt', CODEX_INTERRUPT_TIMEOUT_MS);
|
|
2174
2084
|
}
|
|
2175
2085
|
catch (error) {
|
|
2176
|
-
logger.
|
|
2086
|
+
logger.warn(`[CodexRunner] Unconfirmed interrupt: thread=${threadId} turn=${turnId} error=${String(error)}`);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
async assertInterruptSettled(sessionId, threadId) {
|
|
2090
|
+
const pending = [...this.unconfirmedInterrupts.entries()].filter(([, record]) => record.sessionId === sessionId || (threadId && record.threadId === threadId));
|
|
2091
|
+
await Promise.all(pending.map(async ([key, record]) => {
|
|
2092
|
+
try {
|
|
2093
|
+
// Local stream closure and unsubscribe do not prove the backend turn
|
|
2094
|
+
// stopped. Check its exact identity before allowing another writer.
|
|
2095
|
+
const response = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt verification', 2_000);
|
|
2096
|
+
const turn = response.thread?.turns?.find(turn => turn.id === record.turnId);
|
|
2097
|
+
if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status))
|
|
2098
|
+
&& !response.thread?.turns?.some(turn => turn.status === 'inProgress')
|
|
2099
|
+
&& this.unconfirmedInterrupts.get(key) === record) {
|
|
2100
|
+
this.unconfirmedInterrupts.delete(key);
|
|
2101
|
+
record.unsubscribe?.();
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
catch { /* Ambiguous execution remains fenced; report it below. */ }
|
|
2105
|
+
}));
|
|
2106
|
+
if ([...this.unconfirmedInterrupts.values()].some(record => record.sessionId === sessionId || (threadId && record.threadId === threadId))) {
|
|
2107
|
+
throw unsettledInterruptError('Codex');
|
|
2177
2108
|
}
|
|
2178
2109
|
}
|
|
2179
2110
|
// ── Session commands ──
|
|
@@ -2185,7 +2116,6 @@ export class CodexRunner {
|
|
|
2185
2116
|
return;
|
|
2186
2117
|
if (previousThreadId && previousThreadId !== agentSessionId) {
|
|
2187
2118
|
this.threadProjectPaths.delete(previousThreadId);
|
|
2188
|
-
this.threadDelegationCarriers.delete(previousThreadId);
|
|
2189
2119
|
this.unbindExternalToolBoundary(previousThreadId);
|
|
2190
2120
|
this.clearTrackedApprovalItemsForThread(previousThreadId);
|
|
2191
2121
|
}
|
|
@@ -2269,10 +2199,8 @@ export class CodexRunner {
|
|
|
2269
2199
|
if (this.activeSessions.get(sessionId) === capturedThreadId)
|
|
2270
2200
|
this.activeSessions.delete(sessionId);
|
|
2271
2201
|
this.chatModes.delete(sessionId);
|
|
2272
|
-
this.cleanupSessionTempDir(sessionId);
|
|
2273
2202
|
if (capturedThreadId) {
|
|
2274
2203
|
this.threadProjectPaths.delete(capturedThreadId);
|
|
2275
|
-
this.threadDelegationCarriers.delete(capturedThreadId);
|
|
2276
2204
|
this.unbindExternalToolBoundary(capturedThreadId);
|
|
2277
2205
|
}
|
|
2278
2206
|
this.clearTrackedApprovalItemsForThread(capturedThreadId);
|
|
@@ -2312,9 +2240,7 @@ export class CodexRunner {
|
|
|
2312
2240
|
this.activeTurns.delete(sessionId);
|
|
2313
2241
|
this.steeringTails.delete(sessionId);
|
|
2314
2242
|
this.chatModes.delete(sessionId);
|
|
2315
|
-
this.cleanupSessionTempDir(sessionId);
|
|
2316
2243
|
this.threadProjectPaths.delete(threadId);
|
|
2317
|
-
this.threadDelegationCarriers.delete(threadId);
|
|
2318
2244
|
this.unbindExternalToolBoundary(threadId);
|
|
2319
2245
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
2320
2246
|
this.pruneThreadOperationLocks();
|
|
@@ -2355,19 +2281,19 @@ export class CodexRunner {
|
|
|
2355
2281
|
: await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
|
|
2356
2282
|
if (compactMode !== 'fullaccess')
|
|
2357
2283
|
await this.assertProactiveHookAvailability(_sessionId, appServer);
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2284
|
+
const compactSessionToken = this.ensureThreadCredential(agentSessionId);
|
|
2285
|
+
if (!compactSessionToken)
|
|
2286
|
+
throw new Error('Codex thread session credential is unavailable');
|
|
2360
2287
|
await appServer.threadResume(agentSessionId, _projectPath, {
|
|
2361
2288
|
model: compactModel,
|
|
2362
2289
|
effort: compactEffort,
|
|
2363
2290
|
approvalPolicy: compactPolicy,
|
|
2364
2291
|
approvalsReviewer: 'user',
|
|
2365
2292
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2366
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId,
|
|
2293
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactSessionToken), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
2367
2294
|
});
|
|
2368
2295
|
this.markThreadSubscribed(appServer, agentSessionId);
|
|
2369
2296
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
2370
|
-
this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
|
|
2371
2297
|
this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
|
|
2372
2298
|
await this.startAndWaitForCompact(appServer, agentSessionId);
|
|
2373
2299
|
return finish({ ok: true, durationMs: Date.now() - startedAt });
|
|
@@ -2528,23 +2454,32 @@ export class CodexRunner {
|
|
|
2528
2454
|
const dir = this.threadCredentialsDir();
|
|
2529
2455
|
return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
|
|
2530
2456
|
}
|
|
2457
|
+
createThreadCredential() {
|
|
2458
|
+
return randomBytes(32).toString('hex');
|
|
2459
|
+
}
|
|
2531
2460
|
/** Issue or re-read this agent's credential for a thread. */
|
|
2532
|
-
ensureThreadCredential(threadId) {
|
|
2461
|
+
ensureThreadCredential(threadId, candidate) {
|
|
2533
2462
|
const cached = this.threadCredentials.get(threadId);
|
|
2534
2463
|
if (cached)
|
|
2535
|
-
return cached;
|
|
2464
|
+
return !candidate || cached === candidate ? cached : undefined;
|
|
2536
2465
|
const file = this.threadCredentialPath(threadId);
|
|
2537
|
-
|
|
2538
|
-
|
|
2466
|
+
// Low-level runner callers may not have an agent binding (for example a
|
|
2467
|
+
// standalone integration test). Keep those calls functional in memory;
|
|
2468
|
+
// daemon-owned runners always have an AID and persist the credential.
|
|
2469
|
+
if (!file) {
|
|
2470
|
+
const token = candidate ?? this.createThreadCredential();
|
|
2471
|
+
this.threadCredentials.set(threadId, token);
|
|
2472
|
+
return token;
|
|
2473
|
+
}
|
|
2539
2474
|
try {
|
|
2540
2475
|
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2541
2476
|
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2542
2477
|
this.threadCredentials.set(threadId, record.token);
|
|
2543
|
-
return record.token;
|
|
2478
|
+
return !candidate || record.token === candidate ? record.token : undefined;
|
|
2544
2479
|
}
|
|
2545
2480
|
}
|
|
2546
2481
|
catch { /* missing or unreadable: fall through to issuance */ }
|
|
2547
|
-
const token =
|
|
2482
|
+
const token = candidate ?? this.createThreadCredential();
|
|
2548
2483
|
const record = {
|
|
2549
2484
|
token,
|
|
2550
2485
|
threadId,
|
|
@@ -2606,49 +2541,55 @@ export class CodexRunner {
|
|
|
2606
2541
|
}
|
|
2607
2542
|
catch { /* best effort */ }
|
|
2608
2543
|
}
|
|
2609
|
-
async forkSession(agentSessionId, projectPath, title) {
|
|
2544
|
+
async forkSession(agentSessionId, projectPath, title, context) {
|
|
2610
2545
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
|
|
2611
2546
|
try {
|
|
2612
|
-
return await this.forkSessionLocked(agentSessionId, projectPath, title);
|
|
2547
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title, undefined, context);
|
|
2613
2548
|
}
|
|
2614
2549
|
finally {
|
|
2615
2550
|
release();
|
|
2616
2551
|
}
|
|
2617
2552
|
}
|
|
2618
|
-
async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId) {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2553
|
+
async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId, context) {
|
|
2554
|
+
// Historical threads may have no live runner binding after idle cleanup or
|
|
2555
|
+
// restart. Cached turn permissions can also outlive a role change. Derive
|
|
2556
|
+
// this management operation solely from its current authenticated request;
|
|
2557
|
+
// callers without that context remain readonly, including old fullaccess
|
|
2558
|
+
// threads. Do not mutate shared turn/session permission maps while forking.
|
|
2559
|
+
const sessionKey = context?.sessionId ?? this.findSessionKeyByThread(agentSessionId);
|
|
2560
|
+
const mode = normalizePermissionMode(context?.permissionMode).mode;
|
|
2561
|
+
const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath, undefined, undefined, context ?? {});
|
|
2622
2562
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
2623
|
-
const appServer = this.
|
|
2624
|
-
?? this.getAppServerClient(mode === 'fullaccess');
|
|
2563
|
+
const appServer = this.getAppServerClient();
|
|
2625
2564
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
|
|
2626
2565
|
if (sessionKey)
|
|
2627
2566
|
await this.assertProactiveHookAvailability(sessionKey, appServer);
|
|
2628
|
-
const
|
|
2567
|
+
const forkSessionToken = this.createThreadCredential();
|
|
2629
2568
|
const response = await appServer.threadFork(agentSessionId, projectPath, title, {
|
|
2630
2569
|
model: this.model,
|
|
2631
2570
|
effort: this.effort,
|
|
2632
2571
|
approvalPolicy: this.toApprovalPolicy(mode),
|
|
2633
2572
|
approvalsReviewer: 'user',
|
|
2634
2573
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2635
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey,
|
|
2574
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, forkSessionToken), this.buildLifecycleLockdownConfig(mode)), mode),
|
|
2636
2575
|
...(lastTurnId ? { lastTurnId } : {}),
|
|
2637
2576
|
});
|
|
2638
2577
|
const forkedThreadId = response.thread?.id;
|
|
2639
2578
|
if (!forkedThreadId)
|
|
2640
2579
|
throw new Error('Codex fork did not return a thread id');
|
|
2580
|
+
if (this.ensureThreadCredential(forkedThreadId, forkSessionToken) !== forkSessionToken) {
|
|
2581
|
+
throw new Error('Codex fork session credential could not be bound');
|
|
2582
|
+
}
|
|
2641
2583
|
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
2642
|
-
this.threadDelegationCarriers.set(forkedThreadId, delegationCarrier);
|
|
2643
2584
|
this.threadProjectPaths.set(forkedThreadId, path.resolve(projectPath));
|
|
2644
2585
|
this.bindExternalToolBoundary(forkedThreadId, externalToolConfig);
|
|
2645
2586
|
await this.releaseThreadSubscription(appServer, forkedThreadId, 'fork-finished');
|
|
2646
2587
|
return forkedThreadId;
|
|
2647
2588
|
}
|
|
2648
|
-
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
|
|
2589
|
+
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title, context) {
|
|
2649
2590
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
|
|
2650
2591
|
try {
|
|
2651
|
-
const appServer = this.
|
|
2592
|
+
const appServer = this.getAppServerClient();
|
|
2652
2593
|
const history = await this.readForkHistory(appServer, agentSessionId);
|
|
2653
2594
|
const messages = this.mapThreadToSessionMessages(history, agentSessionId);
|
|
2654
2595
|
const turns = buildSessionTurnList(messages);
|
|
@@ -2658,7 +2599,7 @@ export class CodexRunner {
|
|
|
2658
2599
|
const targetTurnId = this.findTurnIdForAssistantMessage(history, assistantMessageId);
|
|
2659
2600
|
if (!targetTurnId)
|
|
2660
2601
|
throw new Error('fork point turn not found');
|
|
2661
|
-
return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId);
|
|
2602
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId, context);
|
|
2662
2603
|
}
|
|
2663
2604
|
finally {
|
|
2664
2605
|
release();
|
|
@@ -2730,7 +2671,7 @@ export class CodexRunner {
|
|
|
2730
2671
|
return this.getAppServerClientForThread(agentSessionId).threadMetadataUpdate(agentSessionId, gitInfo);
|
|
2731
2672
|
}
|
|
2732
2673
|
async getSessionMessages(agentSessionId, projectPath) {
|
|
2733
|
-
const response = await this.getAppServerClientForThread(agentSessionId)
|
|
2674
|
+
const response = await this.readForkHistory(this.getAppServerClientForThread(agentSessionId), agentSessionId);
|
|
2734
2675
|
return this.mapThreadToSessionMessages(response, agentSessionId);
|
|
2735
2676
|
}
|
|
2736
2677
|
approvalItemKey(threadId, turnId, itemId) {
|
|
@@ -3070,9 +3011,9 @@ export class CodexRunner {
|
|
|
3070
3011
|
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
3071
3012
|
|| request.method === 'execCommandApproval';
|
|
3072
3013
|
const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
|
|
3073
|
-
const
|
|
3014
|
+
const sessionCredential = this.sessionTokenForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
|
|
3074
3015
|
const canonicalEcApproval = isCommandApproval
|
|
3075
|
-
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey),
|
|
3016
|
+
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), sessionCredential)
|
|
3076
3017
|
: undefined;
|
|
3077
3018
|
const approvedCommand = canonicalEcApproval?.argv;
|
|
3078
3019
|
const denyInfrastructure = async (policyCode, message) => {
|
|
@@ -3122,23 +3063,23 @@ export class CodexRunner {
|
|
|
3122
3063
|
const decision = typeof resolution === 'string' ? resolution : resolution.decision;
|
|
3123
3064
|
const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
|
|
3124
3065
|
if (decision !== 'deny' && managedEcIntent && approvedCommand) {
|
|
3125
|
-
const
|
|
3066
|
+
const credentialThreadIds = [
|
|
3126
3067
|
this.activeSessions.get(sessionKey),
|
|
3127
3068
|
this.provisionalSessions.get(sessionKey),
|
|
3128
3069
|
typeof params.threadId === 'string' ? params.threadId : undefined,
|
|
3129
3070
|
typeof params.conversationId === 'string' ? params.conversationId : undefined,
|
|
3130
3071
|
];
|
|
3131
|
-
const
|
|
3072
|
+
const hasSessionCredential = credentialThreadIds.some(threadId => !!threadId && !!this.ensureThreadCredentialLoaded(threadId));
|
|
3132
3073
|
const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
|
|
3133
|
-
// A
|
|
3074
|
+
// A credentialed thread is managed even if its permission context
|
|
3134
3075
|
// 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
|
|
3076
|
+
// than reaching daemon IPC with an unarmed session credential.
|
|
3077
|
+
if ((hasSessionCredential || canArmDelegation)
|
|
3078
|
+
&& canonicalEcApproval?.credentialToken
|
|
3079
|
+
&& canonicalEcApproval.credentialToken !== sessionCredential) {
|
|
3080
|
+
return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread credential');
|
|
3140
3081
|
}
|
|
3141
|
-
if ((
|
|
3082
|
+
if ((hasSessionCredential || canArmDelegation)
|
|
3142
3083
|
&& !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
|
|
3143
3084
|
return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
|
|
3144
3085
|
}
|
|
@@ -3793,14 +3734,7 @@ export class CodexRunner {
|
|
|
3793
3734
|
if (paths.length === 0)
|
|
3794
3735
|
return true;
|
|
3795
3736
|
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;
|
|
3737
|
+
const canonicalTempDir = this.getManagedTempDir(sessionKey);
|
|
3804
3738
|
return paths.some(candidate => {
|
|
3805
3739
|
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(workspace, candidate);
|
|
3806
3740
|
const canonical = resolveProtectedCandidate(absolute);
|
|
@@ -3895,7 +3829,7 @@ export class CodexRunner {
|
|
|
3895
3829
|
// distinguishes this explicit empty value from runners without a
|
|
3896
3830
|
// session-temp capability.
|
|
3897
3831
|
managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
|
|
3898
|
-
|
|
3832
|
+
sessionCredential: this.sessionTokenForSession(sessionKey),
|
|
3899
3833
|
allowProtectedMetadata: false,
|
|
3900
3834
|
selfAid: permissionContext?.selfAid,
|
|
3901
3835
|
channel: permissionContext?.channel,
|
|
@@ -4252,17 +4186,17 @@ export class CodexRunner {
|
|
|
4252
4186
|
return;
|
|
4253
4187
|
this.childThreadSessions.set(childThreadId, sessionId);
|
|
4254
4188
|
this.threadProjectPaths.set(childThreadId, path.resolve(projectPath));
|
|
4255
|
-
// Child approvals use the parent's capability boundary and
|
|
4256
|
-
//
|
|
4189
|
+
// Child approvals use the parent's capability boundary and session
|
|
4190
|
+
// credential. This is inheritance, not a second permission decision.
|
|
4257
4191
|
const parentBoundary = parentThreadId ? this.threadExternalToolBoundaries.get(parentThreadId) : undefined;
|
|
4258
4192
|
const parentFingerprint = parentThreadId ? this.threadExternalToolFingerprints.get(parentThreadId) : undefined;
|
|
4259
4193
|
if (parentBoundary)
|
|
4260
4194
|
this.threadExternalToolBoundaries.set(childThreadId, parentBoundary);
|
|
4261
4195
|
if (parentFingerprint)
|
|
4262
4196
|
this.threadExternalToolFingerprints.set(childThreadId, parentFingerprint);
|
|
4263
|
-
const
|
|
4264
|
-
if (
|
|
4265
|
-
this.
|
|
4197
|
+
const parentToken = parentThreadId ? this.ensureThreadCredentialLoaded(parentThreadId) : undefined;
|
|
4198
|
+
if (parentToken)
|
|
4199
|
+
this.ensureThreadCredential(childThreadId, parentToken);
|
|
4266
4200
|
}
|
|
4267
4201
|
unregisterChildThread(childThreadId) {
|
|
4268
4202
|
this.childThreadSessions.delete(childThreadId);
|
|
@@ -4270,8 +4204,8 @@ export class CodexRunner {
|
|
|
4270
4204
|
this.threadProjectPaths.delete(childThreadId);
|
|
4271
4205
|
this.threadExternalToolBoundaries.delete(childThreadId);
|
|
4272
4206
|
this.threadExternalToolFingerprints.delete(childThreadId);
|
|
4273
|
-
this.threadDelegationCarriers.delete(childThreadId);
|
|
4274
4207
|
this.clearTrackedApprovalItemsForThread(childThreadId);
|
|
4208
|
+
this.dropThreadCredential(childThreadId);
|
|
4275
4209
|
}
|
|
4276
4210
|
cleanupChildThreadsForSession(sessionId) {
|
|
4277
4211
|
for (const [childThreadId, ownerSessionId] of this.childThreadSessions) {
|
|
@@ -4411,10 +4345,14 @@ export class CodexRunner {
|
|
|
4411
4345
|
}
|
|
4412
4346
|
case 'item/agentMessage/delta': {
|
|
4413
4347
|
const itemId = typeof params.itemId === 'string' ? params.itemId : undefined;
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4348
|
+
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
4349
|
+
if (itemId && delta) {
|
|
4350
|
+
if (!state.streamedAgentMessageIds.has(itemId)) {
|
|
4351
|
+
state.streamedAgentMessageIds.add(itemId);
|
|
4352
|
+
yield { type: 'text_start', contentIndex: 0 };
|
|
4353
|
+
}
|
|
4354
|
+
state.agentMessageDeltaText.set(itemId, (state.agentMessageDeltaText.get(itemId) || '') + delta);
|
|
4355
|
+
yield { type: 'heartbeat', source: 'text' };
|
|
4418
4356
|
}
|
|
4419
4357
|
break;
|
|
4420
4358
|
}
|
|
@@ -4715,10 +4653,35 @@ export class CodexRunner {
|
|
|
4715
4653
|
switch (item.type) {
|
|
4716
4654
|
case 'agentMessage':
|
|
4717
4655
|
{
|
|
4656
|
+
if (item.questions != null) {
|
|
4657
|
+
if (item.id)
|
|
4658
|
+
state.agentMessageDeltaText.delete(item.id);
|
|
4659
|
+
try {
|
|
4660
|
+
const questions = parseAsyncUserQuestions(item.questions);
|
|
4661
|
+
if (!item.id || !state.turnId)
|
|
4662
|
+
throw new Error('Async question is missing its item or turn id');
|
|
4663
|
+
const key = JSON.stringify([state.threadId, state.turnId, item.id]);
|
|
4664
|
+
if (this.asyncQuestionItems.has(key))
|
|
4665
|
+
break;
|
|
4666
|
+
this.asyncQuestionItems.add(key);
|
|
4667
|
+
if (this.asyncQuestionItems.size > 2048)
|
|
4668
|
+
this.asyncQuestionItems.delete(this.asyncQuestionItems.values().next().value);
|
|
4669
|
+
yield { type: 'user_question', request: {
|
|
4670
|
+
providerThreadId: state.threadId, providerTurnId: state.turnId, itemId: item.id, questions,
|
|
4671
|
+
} };
|
|
4672
|
+
}
|
|
4673
|
+
catch (error) {
|
|
4674
|
+
yield { type: 'notice', severity: 'warn', subtype: 'invalid_async_question',
|
|
4675
|
+
message: `Codex 异步问题无法显示:${error instanceof Error ? error.message : String(error)}` };
|
|
4676
|
+
}
|
|
4677
|
+
break;
|
|
4678
|
+
}
|
|
4718
4679
|
const buffered = item.id ? state.agentMessageDeltaText.get(item.id) : undefined;
|
|
4719
4680
|
const text = typeof item.text === 'string' && item.text ? item.text : buffered;
|
|
4720
|
-
if (text)
|
|
4721
|
-
|
|
4681
|
+
if (text) {
|
|
4682
|
+
state.lastAssistantUuid = item.id || state.turnId;
|
|
4683
|
+
yield { type: 'text', text, assistantUuid: state.lastAssistantUuid };
|
|
4684
|
+
}
|
|
4722
4685
|
if (item.id)
|
|
4723
4686
|
state.agentMessageDeltaText.delete(item.id);
|
|
4724
4687
|
}
|
|
@@ -4956,6 +4919,7 @@ export class CodexRunner {
|
|
|
4956
4919
|
return {
|
|
4957
4920
|
type: 'complete',
|
|
4958
4921
|
subtype: status === 'completed' ? 'success' : outputLimit ? 'max_output_tokens' : status,
|
|
4922
|
+
assistantUuid: state.lastAssistantUuid,
|
|
4959
4923
|
isError: status === 'failed',
|
|
4960
4924
|
errors: turn.error?.message ? [turn.error.message] : undefined,
|
|
4961
4925
|
terminalReason,
|
|
@@ -4969,126 +4933,36 @@ export class CodexRunner {
|
|
|
4969
4933
|
contextUsage,
|
|
4970
4934
|
};
|
|
4971
4935
|
}
|
|
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
4936
|
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
|
-
}
|
|
4937
|
+
const directory = ensureSessionRuntimeDir(sessionId);
|
|
4938
|
+
return directory ? resolveProtectedCandidate(directory) : undefined;
|
|
5062
4939
|
}
|
|
5063
4940
|
managedTempInstruction(sessionId) {
|
|
5064
|
-
const managedTempDir =
|
|
4941
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
5065
4942
|
if (!managedTempDir)
|
|
5066
4943
|
return undefined;
|
|
5067
|
-
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp
|
|
4944
|
+
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
|
|
5068
4945
|
}
|
|
5069
|
-
buildEvolcoreShellEnvironmentConfig(sessionId,
|
|
5070
|
-
const managedTempDir =
|
|
4946
|
+
buildEvolcoreShellEnvironmentConfig(sessionId, sessionToken, taskDelegationToken) {
|
|
4947
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
5071
4948
|
// The managed-hook credential rides the thread's shell environment so the
|
|
5072
4949
|
// PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
|
|
5073
4950
|
// It follows the thread (start AND resume) rather than the daemon process.
|
|
5074
|
-
const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
|
|
5075
4951
|
return {
|
|
5076
4952
|
shell_environment_policy: {
|
|
5077
4953
|
set: {
|
|
5078
4954
|
EVOLCORE_SESSION_ID: sessionId,
|
|
5079
4955
|
EVOLCORE_HOME: resolveRoot(),
|
|
5080
4956
|
...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
|
|
5081
|
-
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(
|
|
5082
|
-
...(taskDelegationToken
|
|
5083
|
-
? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
|
|
5084
|
-
: {}),
|
|
4957
|
+
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(managedTempDir, 'locks') } : {}),
|
|
4958
|
+
...(taskDelegationToken ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken } : {}),
|
|
5085
4959
|
...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
|
|
5086
4960
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
5087
4961
|
},
|
|
5088
4962
|
},
|
|
5089
4963
|
};
|
|
5090
4964
|
}
|
|
5091
|
-
sanitizeThreadEnvironmentConfig(config, permissionMode) {
|
|
4965
|
+
sanitizeThreadEnvironmentConfig(config, permissionMode, taskDelegationToken) {
|
|
5092
4966
|
if (!config)
|
|
5093
4967
|
return null;
|
|
5094
4968
|
const policy = this.configRecord(config.shell_environment_policy);
|
|
@@ -5098,11 +4972,40 @@ export class CodexRunner {
|
|
|
5098
4972
|
const safeSet = sanitizeShellExecutionEnvironment(set, {
|
|
5099
4973
|
lockExecutableResolution: permissionMode === 'readonly',
|
|
5100
4974
|
});
|
|
4975
|
+
const sessionToken = typeof safeSet[CODEX_SESSION_TOKEN_ENV] === 'string'
|
|
4976
|
+
? safeSet[CODEX_SESSION_TOKEN_ENV]
|
|
4977
|
+
: undefined;
|
|
4978
|
+
for (const name of Object.keys(safeSet)) {
|
|
4979
|
+
const normalized = name.toUpperCase();
|
|
4980
|
+
if (normalized === AGENT_DELEGATION_TOKEN_ENV || normalized === CODEX_SESSION_TOKEN_ENV) {
|
|
4981
|
+
delete safeSet[name];
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
if (sessionToken)
|
|
4985
|
+
safeSet[CODEX_SESSION_TOKEN_ENV] = sessionToken;
|
|
4986
|
+
const existingExclude = Array.isArray(policy.exclude)
|
|
4987
|
+
? policy.exclude.filter((name) => typeof name === 'string')
|
|
4988
|
+
: [];
|
|
4989
|
+
const carriesTaskDelegation = permissionMode === 'fullaccess'
|
|
4990
|
+
&& !!taskDelegationToken;
|
|
4991
|
+
if (carriesTaskDelegation)
|
|
4992
|
+
safeSet[AGENT_DELEGATION_TOKEN_ENV] = taskDelegationToken;
|
|
4993
|
+
const filteredExclude = existingExclude.filter(name => {
|
|
4994
|
+
const normalized = name.toUpperCase();
|
|
4995
|
+
return normalized !== AGENT_DELEGATION_TOKEN_ENV
|
|
4996
|
+
&& normalized !== CODEX_SESSION_TOKEN_ENV;
|
|
4997
|
+
});
|
|
4998
|
+
const exclude = carriesTaskDelegation
|
|
4999
|
+
? filteredExclude
|
|
5000
|
+
: [...new Set([...filteredExclude, AGENT_DELEGATION_TOKEN_ENV])];
|
|
5101
5001
|
return {
|
|
5102
5002
|
...config,
|
|
5103
5003
|
shell_environment_policy: {
|
|
5104
5004
|
...policy,
|
|
5105
5005
|
set: safeSet,
|
|
5006
|
+
// The task token is invocation-scoped. Persistent non-fullaccess
|
|
5007
|
+
// threads must not inherit a stale value from the app-server process.
|
|
5008
|
+
exclude,
|
|
5106
5009
|
},
|
|
5107
5010
|
};
|
|
5108
5011
|
}
|
|
@@ -5119,21 +5022,23 @@ export class CodexRunner {
|
|
|
5119
5022
|
this.activeSessions.clear();
|
|
5120
5023
|
this.provisionalSessions.clear();
|
|
5121
5024
|
this.activeTurns.clear();
|
|
5025
|
+
for (const childThreadId of [...this.childThreadSessions.keys()])
|
|
5026
|
+
this.unregisterChildThread(childThreadId);
|
|
5122
5027
|
this.activeSubagentTurns.clear();
|
|
5123
5028
|
this.childThreadSessions.clear();
|
|
5124
5029
|
this.threadProjectPaths.clear();
|
|
5125
5030
|
this.threadExternalToolFingerprints.clear();
|
|
5126
5031
|
this.threadExternalToolBoundaries.clear();
|
|
5127
5032
|
this.trackedApprovalItems.clear();
|
|
5128
|
-
this.threadDelegationCarriers.clear();
|
|
5129
5033
|
this.pendingInterrupts.clear();
|
|
5034
|
+
for (const record of this.unconfirmedInterrupts.values())
|
|
5035
|
+
record.unsubscribe?.();
|
|
5036
|
+
this.unconfirmedInterrupts.clear();
|
|
5130
5037
|
this.steeringTails.clear();
|
|
5131
5038
|
this.permissionContexts.clear();
|
|
5132
5039
|
this.chatModes.clear();
|
|
5133
5040
|
this.pruneThreadOperationLocks();
|
|
5134
5041
|
this.threadOperationLocks.clear();
|
|
5135
|
-
for (const sessionId of [...this.sessionTempDirs.keys()])
|
|
5136
|
-
this.cleanupSessionTempDir(sessionId);
|
|
5137
5042
|
await Promise.all([
|
|
5138
5043
|
this.appServerClient?.close(),
|
|
5139
5044
|
this.fullAccessAppServerClient?.close(),
|