evolcore 0.0.25 → 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 +632 -565
- package/LICENSE +21 -21
- package/README.md +455 -455
- package/bin/codex-managed-acl.ps1 +203 -160
- package/bin/codex-managed-hook.mjs +208 -208
- package/bin/ec-safe-output.js +327 -327
- package/bin/ec.js +40 -29
- package/bin/install-codex-managed-hooks.mjs +582 -555
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +172 -33
- package/dist/agents/codex/app-server-client.js +76 -31
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +434 -446
- package/dist/agents/ecagent-runner.js +174 -34
- package/dist/agents/gemini-runner.js +8 -7
- package/dist/agents/runner-types.js +75 -19
- package/dist/aun/aid/agentmd.js +151 -36
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +4 -4
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +17 -13
- 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} +162 -39
- package/dist/aun/{menu-client.js → menu/client.js} +85 -6
- 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 +85 -19
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +19 -9
- package/dist/aun/msg/upload.js +43 -80
- 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 +842 -318
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +212 -89
- package/dist/cli/agent-create.js +626 -11
- package/dist/cli/agent.js +125 -13
- package/dist/cli/aun-commands.js +487 -337
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +55 -33
- package/dist/cli/contact.js +72 -15
- package/dist/cli/ctl-command.js +24 -24
- package/dist/cli/daemon-commands.js +140 -63
- package/dist/cli/diag.js +31 -0
- package/dist/cli/fs-command.js +70 -70
- package/dist/cli/handoff-command.js +46 -14
- package/dist/cli/index.js +91 -79
- package/dist/cli/init.js +34 -1
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +5 -1
- package/dist/cli/menu-client.js +51 -32
- package/dist/cli/model.js +3 -3
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +31 -31
- package/dist/cli/response.js +32 -32
- package/dist/cli/restart-monitor.js +103 -42
- package/dist/cli/stats.js +51 -51
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +74 -64
- package/dist/config/builtin-roles.js +11 -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 +160 -0
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +97 -8
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +30 -7
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +7 -1
- 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 +53 -49
- package/dist/core/auth/operation-authorizer.js +90 -7
- package/dist/core/auth/operation-catalog.js +145 -4
- package/dist/core/auth/remote-access-store.js +155 -10
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +111 -20
- 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 +159 -65
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +46 -7
- package/dist/core/command/menu-handler.js +392 -100
- package/dist/core/command/menu-protocol.js +45 -6
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +123 -43
- 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/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +146 -34
- package/dist/core/message/message-bridge.js +163 -20
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +99 -41
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +755 -144
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +112 -41
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +108 -17
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +9 -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 +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 +588 -217
- package/dist/ipc.js +193 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/price-resolver.js +28 -10
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- 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/cross-platform.js +30 -1
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +61 -2
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +26 -14
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -23
- package/dist/utils/npm-ops.js +176 -35
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +196 -23
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +60 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +69 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +379 -367
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +76 -71
- package/kits/docs/evolcore/agent-create.md +202 -165
- package/kits/docs/evolcore/agent.md +99 -67
- package/kits/docs/evolcore/aid.md +68 -60
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
- package/kits/docs/evolcore/config.md +241 -195
- package/kits/docs/evolcore/contact.md +75 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +244 -244
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +267 -152
- package/kits/docs/evolcore/menu-client.md +58 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +169 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +53 -50
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +623 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +269 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +42 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +217 -190
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +166 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +36 -36
- package/kits/rules/04-relation.md +76 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +65 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +451 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +215 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +92 -92
- package/kits/templates/roles/member.json +101 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +90 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +53 -34
- package/kits/templates/system-fragments/channel.md +28 -64
- package/kits/templates/system-fragments/commands.md +36 -28
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/reply.md +56 -0
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +104 -98
- 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,24 +1,26 @@
|
|
|
1
1
|
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
2
|
-
|
|
2
|
+
// Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
|
|
3
|
+
import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
|
|
4
|
+
import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQuestions, unsettledInterruptError, withInterruptDeadline } from './runner-types.js';
|
|
3
5
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
4
6
|
import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
5
7
|
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
6
|
-
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
|
|
8
|
+
import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
|
|
7
9
|
import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
|
|
8
10
|
import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
|
|
9
11
|
import { logger } from '../utils/logger.js';
|
|
10
12
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
11
13
|
import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
12
|
-
import { isRetryableError } from '../utils/error-utils.js';
|
|
14
|
+
import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
|
|
13
15
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
14
16
|
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
15
17
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
16
|
-
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';
|
|
17
19
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
18
|
-
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent,
|
|
20
|
+
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
|
|
19
21
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
20
22
|
import { agentSessionsDir, resolvePaths, resolveRoot } from '../paths.js';
|
|
21
|
-
import {
|
|
23
|
+
import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
|
|
22
24
|
import { buildSessionTurnList } from '../core/session/session-turns.js';
|
|
23
25
|
import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
|
|
24
26
|
import { execFileSync } from 'child_process';
|
|
@@ -36,19 +38,23 @@ const MIME_EXT = {
|
|
|
36
38
|
'image/webp': '.webp',
|
|
37
39
|
};
|
|
38
40
|
const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
|
|
41
|
+
const CODEX_INTERRUPT_TIMEOUT_MS = 5_000;
|
|
42
|
+
function toAgentErrorType(message) {
|
|
43
|
+
switch (classifyError(new Error(message))) {
|
|
44
|
+
case ErrorType.CONTEXT_TOO_LONG: return 'context_too_long';
|
|
45
|
+
case ErrorType.AUTH_ERROR: return 'auth';
|
|
46
|
+
case ErrorType.API_ERROR:
|
|
47
|
+
case ErrorType.STREAM_ERROR:
|
|
48
|
+
case ErrorType.SDK_TIMEOUT: return 'network';
|
|
49
|
+
default: return 'unknown';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
39
52
|
export function formatApprovalLogSummary(summary) {
|
|
40
53
|
const singleLine = summary.replace(/\s+/gu, ' ').trim();
|
|
41
54
|
if (singleLine.length <= APPROVAL_LOG_SUMMARY_MAX_LENGTH)
|
|
42
55
|
return singleLine;
|
|
43
56
|
return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
|
|
44
57
|
}
|
|
45
|
-
/**
|
|
46
|
-
* 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
|
|
47
|
-
* Codex/OpenAI API 不认识这些后缀,必须传入标准模型 ID
|
|
48
|
-
*/
|
|
49
|
-
function stripContextWindowSuffix(modelId) {
|
|
50
|
-
return modelId.replace(/\[(?:1m|200k)\]$/i, '');
|
|
51
|
-
}
|
|
52
58
|
// ── Codex 模型目录(动态获取,含 effort) ──
|
|
53
59
|
class AsyncEventQueue {
|
|
54
60
|
queue = [];
|
|
@@ -103,60 +109,6 @@ function isCodexReasoningNotification(method) {
|
|
|
103
109
|
|| method === 'item/reasoning/summaryPartAdded';
|
|
104
110
|
}
|
|
105
111
|
const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
|
|
106
|
-
const managedTempDirsForExitCleanup = new Map();
|
|
107
|
-
let managedTempExitCleanupRegistered = false;
|
|
108
|
-
function isTrustedManagedTempDirectory(directory, parent) {
|
|
109
|
-
try {
|
|
110
|
-
const parentPath = path.resolve(parent);
|
|
111
|
-
const directoryPath = path.resolve(directory);
|
|
112
|
-
// The session directory must be a direct child of the private parent.
|
|
113
|
-
// This lexical check is required before canonicalization so a replaced
|
|
114
|
-
// parent symlink cannot turn an old session path into another tree.
|
|
115
|
-
if (path.dirname(directoryPath) !== parentPath)
|
|
116
|
-
return false;
|
|
117
|
-
const trustedDirectory = (candidate) => {
|
|
118
|
-
const stat = fs.lstatSync(candidate);
|
|
119
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
120
|
-
return false;
|
|
121
|
-
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
122
|
-
return false;
|
|
123
|
-
return process.platform === 'win32' || (stat.mode & 0o077) === 0;
|
|
124
|
-
};
|
|
125
|
-
if (!trustedDirectory(parentPath) || !trustedDirectory(directoryPath))
|
|
126
|
-
return false;
|
|
127
|
-
const canonicalParent = resolveProtectedCandidate(parentPath);
|
|
128
|
-
const canonicalDirectory = resolveProtectedCandidate(directoryPath);
|
|
129
|
-
// Keep the canonical and lexical paths identical after the lstat checks;
|
|
130
|
-
// this rejects symlink replacement in either node or an existing ancestor.
|
|
131
|
-
return canonicalParent === parentPath
|
|
132
|
-
&& canonicalDirectory === directoryPath
|
|
133
|
-
&& canonicalDirectory !== canonicalParent
|
|
134
|
-
&& path.dirname(canonicalDirectory) === canonicalParent
|
|
135
|
-
&& isSameOrDescendant(canonicalDirectory, canonicalParent);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function registerManagedTempDirForExitCleanup(directory, parent) {
|
|
142
|
-
managedTempDirsForExitCleanup.set(directory, parent);
|
|
143
|
-
if (managedTempExitCleanupRegistered)
|
|
144
|
-
return;
|
|
145
|
-
managedTempExitCleanupRegistered = true;
|
|
146
|
-
process.once('exit', () => {
|
|
147
|
-
for (const [candidatePath, parentPath] of managedTempDirsForExitCleanup) {
|
|
148
|
-
try {
|
|
149
|
-
if (isTrustedManagedTempDirectory(candidatePath, parentPath)) {
|
|
150
|
-
fs.rmSync(path.resolve(candidatePath), { recursive: true, force: true });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// Process exit cleanup is best effort; session cleanup logs failures
|
|
155
|
-
// while the runner is still active.
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
112
|
function stableSecurityValue(value) {
|
|
161
113
|
if (value === undefined)
|
|
162
114
|
return 'undefined';
|
|
@@ -198,11 +150,13 @@ function parseManagedPreToolUseDenial(value) {
|
|
|
198
150
|
.trim();
|
|
199
151
|
if (!reason)
|
|
200
152
|
reason = 'EvolCore PreToolUse policy denied this tool call';
|
|
201
|
-
const policyCode =
|
|
202
|
-
? '
|
|
203
|
-
:
|
|
204
|
-
? '
|
|
205
|
-
:
|
|
153
|
+
const policyCode = /当前汇报命令(?:未通过规范解析|不符合当前会话)/.test(reason)
|
|
154
|
+
? 'proactive_send_invalid'
|
|
155
|
+
: /工具调用已达到\s*\d+\s*次/.test(reason)
|
|
156
|
+
? 'proactive_tool_report_required'
|
|
157
|
+
: /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
|
|
158
|
+
? 'proactive_first_send_required'
|
|
159
|
+
: 'managed_pre_tool_use_denied';
|
|
206
160
|
return { policyCode, reason };
|
|
207
161
|
}
|
|
208
162
|
function outputTextForManagedHook(value) {
|
|
@@ -227,7 +181,6 @@ function outputTextForManagedHook(value) {
|
|
|
227
181
|
}
|
|
228
182
|
return String(value);
|
|
229
183
|
}
|
|
230
|
-
const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
231
184
|
const CODEX_NO_ROLLOUT_RE = /no rollout found for thread id/i;
|
|
232
185
|
const CODEX_ACTIVE_WRITER_RE = /already has an active writer/i;
|
|
233
186
|
function isCodexNoRolloutError(error) {
|
|
@@ -354,7 +307,6 @@ export async function getCodexAppServerAvailabilityAsync() {
|
|
|
354
307
|
: `Codex CLI 探测失败:${error instanceof Error ? error.message : String(error)}` };
|
|
355
308
|
}
|
|
356
309
|
}
|
|
357
|
-
export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
|
|
358
310
|
export class CodexRunner {
|
|
359
311
|
name = 'codex';
|
|
360
312
|
// turn/start persists a new user turn before runQuery resolves. App-server
|
|
@@ -373,14 +325,12 @@ export class CodexRunner {
|
|
|
373
325
|
threadProjectPaths = new Map();
|
|
374
326
|
/** Child Codex threads inherit the EvolCore session that started them. */
|
|
375
327
|
childThreadSessions = new Map();
|
|
376
|
-
sessionTempDirs = new Map();
|
|
377
|
-
managedTempNonce = randomBytes(16).toString('hex');
|
|
378
328
|
threadExternalToolFingerprints = new Map();
|
|
379
329
|
threadExternalToolBoundaries = new Map();
|
|
380
330
|
trackedApprovalItems = new Map();
|
|
381
331
|
pendingToolDenials = new Map();
|
|
382
|
-
threadDelegationCarriers = new Map();
|
|
383
332
|
pendingInterrupts = new Map();
|
|
333
|
+
unconfirmedInterrupts = new Map();
|
|
384
334
|
steeringTails = new Map();
|
|
385
335
|
threadOperationLocks = new Map();
|
|
386
336
|
activeOperationReleases = new Map();
|
|
@@ -403,6 +353,9 @@ export class CodexRunner {
|
|
|
403
353
|
codexSessionFileAdapter = new CodexSessionFileAdapter();
|
|
404
354
|
/** threadId → managed-hook credential, issued by this runner's agent only. */
|
|
405
355
|
threadCredentials = new Map();
|
|
356
|
+
/** @deprecated Compatibility alias for direct runner integrations. */
|
|
357
|
+
threadDelegationCarriers = this.threadCredentials;
|
|
358
|
+
asyncQuestionItems = new Set();
|
|
406
359
|
constructor(config, callbacks, runtime) {
|
|
407
360
|
// Compatibility for legacy direct callers that still pass the removed
|
|
408
361
|
// internal fields in a synthetic config. AgentLoader uses runtime binding.
|
|
@@ -419,6 +372,7 @@ export class CodexRunner {
|
|
|
419
372
|
configuredHeaders: this.resolvedConfig.headers,
|
|
420
373
|
});
|
|
421
374
|
this.capabilities = {
|
|
375
|
+
taskRecovery: true,
|
|
422
376
|
clear: false,
|
|
423
377
|
compact: true,
|
|
424
378
|
fork: true,
|
|
@@ -442,6 +396,7 @@ export class CodexRunner {
|
|
|
442
396
|
return existing;
|
|
443
397
|
const client = new CodexAppServerClient({
|
|
444
398
|
apiKey: this.resolvedConfig.apiKey,
|
|
399
|
+
authType: this.resolvedConfig.authType,
|
|
445
400
|
baseUrl: this.resolvedConfig.baseUrl,
|
|
446
401
|
provider: this.resolvedConfig.provider,
|
|
447
402
|
headers: this.resolvedConfig.headers,
|
|
@@ -634,16 +589,13 @@ export class CodexRunner {
|
|
|
634
589
|
}
|
|
635
590
|
this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
|
|
636
591
|
}
|
|
637
|
-
|
|
638
|
-
return randomBytes(32).toString('base64url');
|
|
639
|
-
}
|
|
640
|
-
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
|
|
592
|
+
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedCredentialToken, executable) {
|
|
641
593
|
const parseOptions = {
|
|
642
594
|
allowManagedTmpDir: true,
|
|
643
595
|
dialect,
|
|
644
596
|
verifyShellExecutable: process.platform === 'win32',
|
|
645
597
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
646
|
-
...(
|
|
598
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
647
599
|
};
|
|
648
600
|
const classification = classifyEvolcoreShellCommand(command, parseOptions);
|
|
649
601
|
if (classification.kind === 'bounded-output')
|
|
@@ -670,10 +622,10 @@ export class CodexRunner {
|
|
|
670
622
|
}
|
|
671
623
|
return {
|
|
672
624
|
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
673
|
-
...(parsed.
|
|
625
|
+
...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
|
|
674
626
|
};
|
|
675
627
|
}
|
|
676
|
-
resolveCanonicalEcApproval(toolInput, managedTempDir,
|
|
628
|
+
resolveCanonicalEcApproval(toolInput, managedTempDir, expectedCredentialToken) {
|
|
677
629
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
678
630
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
679
631
|
? toolInput.commandArgv
|
|
@@ -694,12 +646,12 @@ export class CodexRunner {
|
|
|
694
646
|
? resolveCodexShellCarrierArgv(explicitArgv)
|
|
695
647
|
: undefined;
|
|
696
648
|
if (explicitCarrier !== undefined) {
|
|
697
|
-
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect,
|
|
649
|
+
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedCredentialToken, explicitCarrier.executable);
|
|
698
650
|
}
|
|
699
651
|
if (hasCodexCmdCarrierEcIntent(toolInput)) {
|
|
700
652
|
const parsed = parseCodexToolCommand(toolInput, {
|
|
701
653
|
verifyShellExecutable: process.platform === 'win32',
|
|
702
|
-
...(
|
|
654
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
703
655
|
});
|
|
704
656
|
if (!parsed.ok)
|
|
705
657
|
return { parseFailure: parsed };
|
|
@@ -711,7 +663,7 @@ export class CodexRunner {
|
|
|
711
663
|
} }
|
|
712
664
|
: {
|
|
713
665
|
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
714
|
-
...(parsed.
|
|
666
|
+
...(parsed.credentialToken ? { credentialToken: parsed.credentialToken } : {}),
|
|
715
667
|
};
|
|
716
668
|
}
|
|
717
669
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
@@ -731,7 +683,7 @@ export class CodexRunner {
|
|
|
731
683
|
allowManagedTmpDir: true,
|
|
732
684
|
verifyShellExecutable: process.platform === 'win32',
|
|
733
685
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
734
|
-
...(
|
|
686
|
+
...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
|
|
735
687
|
});
|
|
736
688
|
if (canonicalCommand?.[0] === 'ec') {
|
|
737
689
|
return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
|
|
@@ -766,7 +718,7 @@ export class CodexRunner {
|
|
|
766
718
|
return true;
|
|
767
719
|
const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
|
|
768
720
|
if (shellCarrier?.dialect === 'powershell'
|
|
769
|
-
&&
|
|
721
|
+
&& hasPowerShellCredentialAssignment(shellCarrier.command))
|
|
770
722
|
return true;
|
|
771
723
|
return hasCodexCmdCarrierEcIntent(toolInput)
|
|
772
724
|
|| shellCarrier !== undefined
|
|
@@ -779,20 +731,20 @@ export class CodexRunner {
|
|
|
779
731
|
}
|
|
780
732
|
armApprovedDelegationCommand(sessionId, argv) {
|
|
781
733
|
const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
782
|
-
const
|
|
734
|
+
const sessionToken = threadId ? this.ensureThreadCredentialLoaded(threadId) : undefined;
|
|
783
735
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
784
736
|
const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
|
|
785
|
-
if (!
|
|
737
|
+
if (!sessionToken || !commandHash || !arm)
|
|
786
738
|
return false;
|
|
787
739
|
try {
|
|
788
|
-
return arm(
|
|
740
|
+
return arm(sessionToken, commandHash);
|
|
789
741
|
}
|
|
790
742
|
catch (error) {
|
|
791
743
|
logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
792
744
|
return false;
|
|
793
745
|
}
|
|
794
746
|
}
|
|
795
|
-
|
|
747
|
+
sessionTokenForSession(sessionId, threadId) {
|
|
796
748
|
const candidates = [
|
|
797
749
|
threadId,
|
|
798
750
|
this.activeSessions.get(sessionId),
|
|
@@ -801,45 +753,13 @@ export class CodexRunner {
|
|
|
801
753
|
for (const candidate of candidates) {
|
|
802
754
|
if (!candidate)
|
|
803
755
|
continue;
|
|
804
|
-
const
|
|
805
|
-
if (
|
|
806
|
-
return
|
|
756
|
+
const token = this.ensureThreadCredentialLoaded(candidate);
|
|
757
|
+
if (token)
|
|
758
|
+
return token;
|
|
807
759
|
}
|
|
808
760
|
return undefined;
|
|
809
761
|
}
|
|
810
|
-
async
|
|
811
|
-
try {
|
|
812
|
-
const result = await appServer.threadShellCommand(threadId, `node -e "process.stdout.write(process.env.${AGENT_DELEGATION_TOKEN_ENV}||String())"`);
|
|
813
|
-
const carrier = result.output.trim();
|
|
814
|
-
if (result.exitCode !== undefined && result.exitCode !== 0)
|
|
815
|
-
return undefined;
|
|
816
|
-
return /^[A-Za-z0-9_-]{32,128}$/.test(carrier) ? carrier : undefined;
|
|
817
|
-
}
|
|
818
|
-
catch (error) {
|
|
819
|
-
logger.warn(`[CodexRunner] failed to recover delegation carrier for loaded thread ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
820
|
-
return undefined;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
async loadPersistedDelegationCarrier(sessionManager, sessionId, threadId) {
|
|
824
|
-
if (typeof sessionManager?.getSessionById !== 'function')
|
|
825
|
-
return undefined;
|
|
826
|
-
try {
|
|
827
|
-
const session = await sessionManager.getSessionById(sessionId);
|
|
828
|
-
const persisted = session?.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
|
|
829
|
-
if (!persisted || typeof persisted !== 'object' || Array.isArray(persisted))
|
|
830
|
-
return undefined;
|
|
831
|
-
if (persisted.threadId !== threadId || typeof persisted.carrier !== 'string')
|
|
832
|
-
return undefined;
|
|
833
|
-
return /^[A-Za-z0-9_-]{32,128}$/.test(persisted.carrier)
|
|
834
|
-
? persisted.carrier
|
|
835
|
-
: undefined;
|
|
836
|
-
}
|
|
837
|
-
catch (error) {
|
|
838
|
-
logger.debug(`[CodexRunner] failed to load persisted delegation carrier: session=${sessionId} error=${error}`);
|
|
839
|
-
return undefined;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
async commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn) {
|
|
762
|
+
async commitProvisionalThread(sessionId, threadId, turn) {
|
|
843
763
|
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
844
764
|
return false;
|
|
845
765
|
const bindingResult = this.onSessionIdUpdate
|
|
@@ -863,10 +783,6 @@ export class CodexRunner {
|
|
|
863
783
|
return false;
|
|
864
784
|
this.provisionalSessions.delete(sessionId);
|
|
865
785
|
this.activeSessions.set(sessionId, threadId);
|
|
866
|
-
if (carrierToPublish) {
|
|
867
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
868
|
-
await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
|
|
869
|
-
}
|
|
870
786
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
871
787
|
return true;
|
|
872
788
|
}
|
|
@@ -899,9 +815,9 @@ export class CodexRunner {
|
|
|
899
815
|
return;
|
|
900
816
|
this.provisionalSessions.delete(sessionId);
|
|
901
817
|
this.threadProjectPaths.delete(threadId);
|
|
902
|
-
this.threadDelegationCarriers.delete(threadId);
|
|
903
818
|
this.unbindExternalToolBoundary(threadId);
|
|
904
819
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
820
|
+
this.dropThreadCredential(threadId);
|
|
905
821
|
// A stale provisional start can finish after a replacement run has
|
|
906
822
|
// already activated another parent thread for this session. Do not tear
|
|
907
823
|
// down the replacement run's child mappings in that race.
|
|
@@ -911,31 +827,6 @@ export class CodexRunner {
|
|
|
911
827
|
}
|
|
912
828
|
this.pruneThreadOperationLocks();
|
|
913
829
|
}
|
|
914
|
-
async persistDelegationCarrier(sessionManager, sessionId, threadId, carrier) {
|
|
915
|
-
if (typeof sessionManager?.getSessionById !== 'function')
|
|
916
|
-
return;
|
|
917
|
-
try {
|
|
918
|
-
const session = await sessionManager.getSessionById(sessionId);
|
|
919
|
-
if (!session)
|
|
920
|
-
return;
|
|
921
|
-
const current = session.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
|
|
922
|
-
if (current?.threadId === threadId && current?.carrier === carrier)
|
|
923
|
-
return;
|
|
924
|
-
if (typeof sessionManager.patchSessionMetadata === 'function') {
|
|
925
|
-
await sessionManager.patchSessionMetadata(sessionId, {
|
|
926
|
-
[CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier },
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
else if (typeof sessionManager.updateSession === 'function') {
|
|
930
|
-
await sessionManager.updateSession(sessionId, {
|
|
931
|
-
metadata: { [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier } },
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
catch (error) {
|
|
936
|
-
logger.warn(`[CodexRunner] failed to persist delegation carrier: session=${sessionId} thread=${threadId} error=${error}`);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
830
|
resetAppServerClient() {
|
|
940
831
|
const clients = [this.appServerClient, this.fullAccessAppServerClient]
|
|
941
832
|
.filter((client) => !!client);
|
|
@@ -944,8 +835,8 @@ export class CodexRunner {
|
|
|
944
835
|
this.threadSubscriptions.clear();
|
|
945
836
|
this.threadExternalToolFingerprints.clear();
|
|
946
837
|
this.threadExternalToolBoundaries.clear();
|
|
947
|
-
for (const childThreadId of this.childThreadSessions.keys())
|
|
948
|
-
this.
|
|
838
|
+
for (const childThreadId of [...this.childThreadSessions.keys()])
|
|
839
|
+
this.unregisterChildThread(childThreadId);
|
|
949
840
|
// Child turn ids belong to the discarded app-server connection. Keeping
|
|
950
841
|
// them would make a later session interrupt attempt target stale threads
|
|
951
842
|
// through the replacement client.
|
|
@@ -953,7 +844,6 @@ export class CodexRunner {
|
|
|
953
844
|
this.childThreadSessions.clear();
|
|
954
845
|
this.trackedApprovalItems.clear();
|
|
955
846
|
this.pendingToolDenials.clear();
|
|
956
|
-
this.threadDelegationCarriers.clear();
|
|
957
847
|
for (const client of clients) {
|
|
958
848
|
client.close().catch(error => {
|
|
959
849
|
logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
|
|
@@ -1300,47 +1190,53 @@ export class CodexRunner {
|
|
|
1300
1190
|
}
|
|
1301
1191
|
buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1302
1192
|
const profileName = this.permissionProfileName(sessionId);
|
|
1303
|
-
const managedTempDir =
|
|
1193
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
1304
1194
|
const network = {
|
|
1305
1195
|
enabled: true,
|
|
1306
1196
|
unix_sockets: { [resolvePaths().instanceSocket]: 'allow' },
|
|
1307
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);
|
|
1308
1227
|
return {
|
|
1309
1228
|
default_permissions: profileName,
|
|
1310
1229
|
permissions: {
|
|
1311
1230
|
[profileName]: {
|
|
1312
1231
|
extends: mode === 'readonly' ? ':read-only' : ':workspace',
|
|
1313
|
-
filesystem
|
|
1314
|
-
...buildCodexProtectedFilesystemRules(resolveRoot(), {
|
|
1315
|
-
denyLClassRead: mode === 'readonly',
|
|
1316
|
-
// The app-server itself is launched behind EvolCore's Linux
|
|
1317
|
-
// H-class guard, which already mounts every L-class directory
|
|
1318
|
-
// read-only. Avoid Codex's per-file glob expansion inside that
|
|
1319
|
-
// nested mount namespace; it can fail while creating a missing
|
|
1320
|
-
// target such as triggers/history.jsonl.
|
|
1321
|
-
expandLClassReadDeny: process.platform !== 'linux',
|
|
1322
|
-
}),
|
|
1323
|
-
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
1324
|
-
...(bootstrapAgentMdPath ? {
|
|
1325
|
-
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1326
|
-
} : {}),
|
|
1327
|
-
...(personalMdPath ? {
|
|
1328
|
-
[path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1329
|
-
} : {}),
|
|
1330
|
-
...(mode !== 'readonly' && managedTempDir ? {
|
|
1331
|
-
[managedTempDir]: 'write',
|
|
1332
|
-
[managedTempDir + '/**']: 'write',
|
|
1333
|
-
[path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
|
|
1334
|
-
[path.join(path.dirname(managedTempDir), 'evolcore-locks') + '/**']: 'write',
|
|
1335
|
-
} : {}),
|
|
1336
|
-
},
|
|
1232
|
+
filesystem,
|
|
1337
1233
|
network,
|
|
1338
1234
|
},
|
|
1339
1235
|
},
|
|
1340
1236
|
};
|
|
1341
1237
|
}
|
|
1342
|
-
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
|
|
1343
|
-
const decision = resolvePhaseOneExecutionSandbox(mode,
|
|
1238
|
+
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
|
|
1239
|
+
const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
|
|
1344
1240
|
return {
|
|
1345
1241
|
decision,
|
|
1346
1242
|
permissionProfileConfig: decision.state === 'active'
|
|
@@ -1378,6 +1274,26 @@ export class CodexRunner {
|
|
|
1378
1274
|
}
|
|
1379
1275
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
1380
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
|
+
}
|
|
1381
1297
|
injectUserMessage(sessionId, text) {
|
|
1382
1298
|
// Bind queued reminders to the turn that generated them. A completed
|
|
1383
1299
|
// turn may be replaced before the serialized steer reaches app-server;
|
|
@@ -1714,9 +1630,11 @@ export class CodexRunner {
|
|
|
1714
1630
|
const knownThreadId = topicBinding
|
|
1715
1631
|
? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
|
|
1716
1632
|
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1717
|
-
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);
|
|
1718
1635
|
const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
|
|
1719
1636
|
try {
|
|
1637
|
+
await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
|
|
1720
1638
|
const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv);
|
|
1721
1639
|
this.activeOperationReleases.set(sessionId, release);
|
|
1722
1640
|
return this.holdThreadOperation(stream, sessionId, release);
|
|
@@ -1746,6 +1664,14 @@ export class CodexRunner {
|
|
|
1746
1664
|
logger.debug(`[CodexRunner] persisted main backend lookup failed; using local cache: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1747
1665
|
}
|
|
1748
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);
|
|
1749
1675
|
let resumingThread = !!agentSessionId;
|
|
1750
1676
|
const callModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
1751
1677
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
@@ -1766,6 +1692,10 @@ export class CodexRunner {
|
|
|
1766
1692
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1767
1693
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
1768
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
|
+
} };
|
|
1769
1699
|
const externalToolConfig = callMode === 'fullaccess'
|
|
1770
1700
|
? {}
|
|
1771
1701
|
: await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
|
|
@@ -1773,25 +1703,21 @@ export class CodexRunner {
|
|
|
1773
1703
|
await this.assertProactiveHookAvailability(sessionId, appServer);
|
|
1774
1704
|
const managedTempInstruction = this.managedTempInstruction(sessionId);
|
|
1775
1705
|
const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
|
|
1776
|
-
let
|
|
1777
|
-
? 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))
|
|
1778
1713
|
: undefined;
|
|
1779
|
-
if (agentSessionId && !knownDelegationCarrier) {
|
|
1780
|
-
knownDelegationCarrier = await this.loadPersistedDelegationCarrier(sessionManager, sessionId, agentSessionId);
|
|
1781
|
-
}
|
|
1782
|
-
let delegationCarrierCandidate = knownDelegationCarrier ?? this.createDelegationCarrier();
|
|
1783
|
-
// Keep newly discovered carrier state local until generation-aware
|
|
1784
|
-
// activation accepts the backend. A stale run must not publish a carrier
|
|
1785
|
-
// that a later generation could mistake for its own thread.
|
|
1786
|
-
let carrierToPublish;
|
|
1787
|
-
const hasTaskDelegation = typeof runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] === 'string';
|
|
1788
1714
|
let threadOptions = {
|
|
1789
1715
|
model: callModel,
|
|
1790
1716
|
effort: callEffort,
|
|
1791
1717
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1792
1718
|
approvalsReviewer: 'user',
|
|
1793
1719
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1794
|
-
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),
|
|
1795
1721
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1796
1722
|
};
|
|
1797
1723
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
@@ -1817,12 +1743,12 @@ export class CodexRunner {
|
|
|
1817
1743
|
// A legacy main session can retain a provider ID after its rollout was
|
|
1818
1744
|
// removed externally. It is safe to recover before input submission;
|
|
1819
1745
|
// topic bindings remain authoritative and fail closed.
|
|
1820
|
-
if (topicBinding || !isCodexNoRolloutError(error))
|
|
1746
|
+
if (topicBinding || recoverySessionId || modelOverride?.asyncQuestionAnswer || !isCodexNoRolloutError(error))
|
|
1821
1747
|
throw error;
|
|
1822
1748
|
if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
|
|
1823
1749
|
throw error;
|
|
1824
1750
|
const missingThreadId = agentSessionId;
|
|
1825
|
-
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId
|
|
1751
|
+
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId);
|
|
1826
1752
|
if (cleared !== 'cleared' && cleared !== 'already_clear') {
|
|
1827
1753
|
const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
|
|
1828
1754
|
recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
|
|
@@ -1831,17 +1757,15 @@ export class CodexRunner {
|
|
|
1831
1757
|
if (this.activeSessions.get(sessionId) === missingThreadId)
|
|
1832
1758
|
this.activeSessions.delete(sessionId);
|
|
1833
1759
|
this.threadProjectPaths.delete(missingThreadId);
|
|
1834
|
-
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1835
1760
|
this.unbindExternalToolBoundary(missingThreadId);
|
|
1836
1761
|
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1837
1762
|
this.dropThreadCredential(missingThreadId);
|
|
1838
1763
|
agentSessionId = undefined;
|
|
1839
1764
|
resumingThread = false;
|
|
1840
|
-
|
|
1841
|
-
delegationCarrierCandidate = this.createDelegationCarrier();
|
|
1765
|
+
threadSessionToken = this.createThreadCredential();
|
|
1842
1766
|
threadOptions = {
|
|
1843
1767
|
...threadOptions,
|
|
1844
|
-
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),
|
|
1845
1769
|
};
|
|
1846
1770
|
logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
|
|
1847
1771
|
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
@@ -1854,43 +1778,12 @@ export class CodexRunner {
|
|
|
1854
1778
|
let threadId = threadResponse.thread?.id || agentSessionId;
|
|
1855
1779
|
if (!threadId)
|
|
1856
1780
|
throw new Error('Codex app-server did not return a thread id');
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
carrierToPublish = { threadId, carrier: delegationCarrierCandidate };
|
|
1860
|
-
}
|
|
1861
|
-
else if (hasTaskDelegation && !knownDelegationCarrier) {
|
|
1862
|
-
// A BOUND topic cannot silently fork to migrate carrier metadata: its
|
|
1863
|
-
// active-turn activation is allowed to confirm only the authoritative
|
|
1864
|
-
// backend ID, so a fork would deterministically conflict. threadResume
|
|
1865
|
-
// already received the candidate in this turn's environment config;
|
|
1866
|
-
// publish it for the same thread after binding confirmation instead.
|
|
1867
|
-
let recoveredCarrier = topicBinding
|
|
1868
|
-
? delegationCarrierCandidate
|
|
1869
|
-
: await this.readThreadDelegationCarrier(appServer, threadId);
|
|
1870
|
-
if (!recoveredCarrier) {
|
|
1871
|
-
const staleThreadId = threadId;
|
|
1872
|
-
logger.warn(`[CodexRunner] loaded thread has no recoverable delegation carrier; forking once to preserve history: thread=${staleThreadId}`);
|
|
1873
|
-
const forked = await appServer.threadFork(staleThreadId, projectPath, undefined, {
|
|
1874
|
-
model: callModel,
|
|
1875
|
-
effort: callEffort,
|
|
1876
|
-
approvalPolicy: effectiveApprovalPolicy,
|
|
1877
|
-
approvalsReviewer: 'user',
|
|
1878
|
-
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1879
|
-
config: threadOptions.config,
|
|
1880
|
-
});
|
|
1881
|
-
const forkedThreadId = forked.thread?.id;
|
|
1882
|
-
if (!forkedThreadId)
|
|
1883
|
-
throw new Error('Codex delegation-carrier recovery fork did not return a thread id');
|
|
1884
|
-
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
1885
|
-
await this.releaseThreadSubscription(appServer, staleThreadId, 'carrier-recovery-fork-source');
|
|
1886
|
-
await this.releaseThreadSubscription(appServer, forkedThreadId, 'carrier-recovery-fork');
|
|
1887
|
-
threadId = forkedThreadId;
|
|
1888
|
-
recoveredCarrier = delegationCarrierCandidate;
|
|
1889
|
-
}
|
|
1890
|
-
carrierToPublish = { threadId, carrier: recoveredCarrier };
|
|
1781
|
+
if (recoverySessionId && threadId !== recoverySessionId) {
|
|
1782
|
+
throw new Error('Task recovery cannot replace the original Codex thread');
|
|
1891
1783
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
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');
|
|
1894
1787
|
}
|
|
1895
1788
|
const turn = modelOverride?.turn;
|
|
1896
1789
|
if (!resumingThread) {
|
|
@@ -1898,9 +1791,6 @@ export class CodexRunner {
|
|
|
1898
1791
|
// has been accepted. In particular, a pending interrupt must not leave
|
|
1899
1792
|
// a durable ID for a thread that never produced a rollout.
|
|
1900
1793
|
this.provisionalSessions.set(sessionId, threadId);
|
|
1901
|
-
if (carrierToPublish) {
|
|
1902
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1903
|
-
}
|
|
1904
1794
|
}
|
|
1905
1795
|
else {
|
|
1906
1796
|
agentSessionId = threadId;
|
|
@@ -1920,10 +1810,6 @@ export class CodexRunner {
|
|
|
1920
1810
|
throw error;
|
|
1921
1811
|
}
|
|
1922
1812
|
this.activeSessions.set(sessionId, threadId);
|
|
1923
|
-
if (carrierToPublish) {
|
|
1924
|
-
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1925
|
-
await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1926
|
-
}
|
|
1927
1813
|
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
1928
1814
|
}
|
|
1929
1815
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
@@ -1931,7 +1817,11 @@ export class CodexRunner {
|
|
|
1931
1817
|
const controller = new AbortController();
|
|
1932
1818
|
this.activeAbortControllers.set(sessionId, controller);
|
|
1933
1819
|
const tempFiles = [];
|
|
1934
|
-
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);
|
|
1935
1825
|
const queue = new AsyncEventQueue();
|
|
1936
1826
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
1937
1827
|
const state = {
|
|
@@ -2045,7 +1935,7 @@ export class CodexRunner {
|
|
|
2045
1935
|
if (!persisted)
|
|
2046
1936
|
return false;
|
|
2047
1937
|
try {
|
|
2048
|
-
const committed = await this.commitProvisionalThread(sessionId, threadId,
|
|
1938
|
+
const committed = await this.commitProvisionalThread(sessionId, threadId, turn);
|
|
2049
1939
|
if (!committed) {
|
|
2050
1940
|
const staleError = new Error('Codex provisional thread was superseded before persistence confirmation');
|
|
2051
1941
|
staleError.code = 'RUNNER_START_SUPERSEDED';
|
|
@@ -2156,16 +2046,65 @@ export class CodexRunner {
|
|
|
2156
2046
|
}
|
|
2157
2047
|
}
|
|
2158
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
|
+
});
|
|
2159
2076
|
try {
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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);
|
|
2166
2084
|
}
|
|
2167
2085
|
catch (error) {
|
|
2168
|
-
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');
|
|
2169
2108
|
}
|
|
2170
2109
|
}
|
|
2171
2110
|
// ── Session commands ──
|
|
@@ -2177,7 +2116,6 @@ export class CodexRunner {
|
|
|
2177
2116
|
return;
|
|
2178
2117
|
if (previousThreadId && previousThreadId !== agentSessionId) {
|
|
2179
2118
|
this.threadProjectPaths.delete(previousThreadId);
|
|
2180
|
-
this.threadDelegationCarriers.delete(previousThreadId);
|
|
2181
2119
|
this.unbindExternalToolBoundary(previousThreadId);
|
|
2182
2120
|
this.clearTrackedApprovalItemsForThread(previousThreadId);
|
|
2183
2121
|
}
|
|
@@ -2261,10 +2199,8 @@ export class CodexRunner {
|
|
|
2261
2199
|
if (this.activeSessions.get(sessionId) === capturedThreadId)
|
|
2262
2200
|
this.activeSessions.delete(sessionId);
|
|
2263
2201
|
this.chatModes.delete(sessionId);
|
|
2264
|
-
this.cleanupSessionTempDir(sessionId);
|
|
2265
2202
|
if (capturedThreadId) {
|
|
2266
2203
|
this.threadProjectPaths.delete(capturedThreadId);
|
|
2267
|
-
this.threadDelegationCarriers.delete(capturedThreadId);
|
|
2268
2204
|
this.unbindExternalToolBoundary(capturedThreadId);
|
|
2269
2205
|
}
|
|
2270
2206
|
this.clearTrackedApprovalItemsForThread(capturedThreadId);
|
|
@@ -2304,9 +2240,7 @@ export class CodexRunner {
|
|
|
2304
2240
|
this.activeTurns.delete(sessionId);
|
|
2305
2241
|
this.steeringTails.delete(sessionId);
|
|
2306
2242
|
this.chatModes.delete(sessionId);
|
|
2307
|
-
this.cleanupSessionTempDir(sessionId);
|
|
2308
2243
|
this.threadProjectPaths.delete(threadId);
|
|
2309
|
-
this.threadDelegationCarriers.delete(threadId);
|
|
2310
2244
|
this.unbindExternalToolBoundary(threadId);
|
|
2311
2245
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
2312
2246
|
this.pruneThreadOperationLocks();
|
|
@@ -2347,19 +2281,19 @@ export class CodexRunner {
|
|
|
2347
2281
|
: await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
|
|
2348
2282
|
if (compactMode !== 'fullaccess')
|
|
2349
2283
|
await this.assertProactiveHookAvailability(_sessionId, appServer);
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2284
|
+
const compactSessionToken = this.ensureThreadCredential(agentSessionId);
|
|
2285
|
+
if (!compactSessionToken)
|
|
2286
|
+
throw new Error('Codex thread session credential is unavailable');
|
|
2352
2287
|
await appServer.threadResume(agentSessionId, _projectPath, {
|
|
2353
2288
|
model: compactModel,
|
|
2354
2289
|
effort: compactEffort,
|
|
2355
2290
|
approvalPolicy: compactPolicy,
|
|
2356
2291
|
approvalsReviewer: 'user',
|
|
2357
2292
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2358
|
-
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),
|
|
2359
2294
|
});
|
|
2360
2295
|
this.markThreadSubscribed(appServer, agentSessionId);
|
|
2361
2296
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
2362
|
-
this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
|
|
2363
2297
|
this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
|
|
2364
2298
|
await this.startAndWaitForCompact(appServer, agentSessionId);
|
|
2365
2299
|
return finish({ ok: true, durationMs: Date.now() - startedAt });
|
|
@@ -2520,23 +2454,32 @@ export class CodexRunner {
|
|
|
2520
2454
|
const dir = this.threadCredentialsDir();
|
|
2521
2455
|
return dir ? path.join(dir, `${encodeURIComponent(threadId)}.json`) : undefined;
|
|
2522
2456
|
}
|
|
2457
|
+
createThreadCredential() {
|
|
2458
|
+
return randomBytes(32).toString('hex');
|
|
2459
|
+
}
|
|
2523
2460
|
/** Issue or re-read this agent's credential for a thread. */
|
|
2524
|
-
ensureThreadCredential(threadId) {
|
|
2461
|
+
ensureThreadCredential(threadId, candidate) {
|
|
2525
2462
|
const cached = this.threadCredentials.get(threadId);
|
|
2526
2463
|
if (cached)
|
|
2527
|
-
return cached;
|
|
2464
|
+
return !candidate || cached === candidate ? cached : undefined;
|
|
2528
2465
|
const file = this.threadCredentialPath(threadId);
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
+
}
|
|
2531
2474
|
try {
|
|
2532
2475
|
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2533
2476
|
if (record && typeof record.token === 'string' && record.token && record.threadId === threadId) {
|
|
2534
2477
|
this.threadCredentials.set(threadId, record.token);
|
|
2535
|
-
return record.token;
|
|
2478
|
+
return !candidate || record.token === candidate ? record.token : undefined;
|
|
2536
2479
|
}
|
|
2537
2480
|
}
|
|
2538
2481
|
catch { /* missing or unreadable: fall through to issuance */ }
|
|
2539
|
-
const token =
|
|
2482
|
+
const token = candidate ?? this.createThreadCredential();
|
|
2540
2483
|
const record = {
|
|
2541
2484
|
token,
|
|
2542
2485
|
threadId,
|
|
@@ -2598,70 +2541,128 @@ export class CodexRunner {
|
|
|
2598
2541
|
}
|
|
2599
2542
|
catch { /* best effort */ }
|
|
2600
2543
|
}
|
|
2601
|
-
async forkSession(agentSessionId, projectPath, title) {
|
|
2544
|
+
async forkSession(agentSessionId, projectPath, title, context) {
|
|
2602
2545
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'fork');
|
|
2603
2546
|
try {
|
|
2604
|
-
return await this.forkSessionLocked(agentSessionId, projectPath, title);
|
|
2547
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title, undefined, context);
|
|
2605
2548
|
}
|
|
2606
2549
|
finally {
|
|
2607
2550
|
release();
|
|
2608
2551
|
}
|
|
2609
2552
|
}
|
|
2610
|
-
async forkSessionLocked(agentSessionId, projectPath, title) {
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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 ?? {});
|
|
2614
2562
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
2615
|
-
const appServer = this.
|
|
2616
|
-
?? this.getAppServerClient(mode === 'fullaccess');
|
|
2563
|
+
const appServer = this.getAppServerClient();
|
|
2617
2564
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
|
|
2618
2565
|
if (sessionKey)
|
|
2619
2566
|
await this.assertProactiveHookAvailability(sessionKey, appServer);
|
|
2620
|
-
const
|
|
2567
|
+
const forkSessionToken = this.createThreadCredential();
|
|
2621
2568
|
const response = await appServer.threadFork(agentSessionId, projectPath, title, {
|
|
2622
2569
|
model: this.model,
|
|
2623
2570
|
effort: this.effort,
|
|
2624
2571
|
approvalPolicy: this.toApprovalPolicy(mode),
|
|
2625
2572
|
approvalsReviewer: 'user',
|
|
2626
2573
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
2627
|
-
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),
|
|
2575
|
+
...(lastTurnId ? { lastTurnId } : {}),
|
|
2628
2576
|
});
|
|
2629
2577
|
const forkedThreadId = response.thread?.id;
|
|
2630
2578
|
if (!forkedThreadId)
|
|
2631
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
|
+
}
|
|
2632
2583
|
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
2633
|
-
this.threadDelegationCarriers.set(forkedThreadId, delegationCarrier);
|
|
2634
2584
|
this.threadProjectPaths.set(forkedThreadId, path.resolve(projectPath));
|
|
2635
2585
|
this.bindExternalToolBoundary(forkedThreadId, externalToolConfig);
|
|
2636
2586
|
await this.releaseThreadSubscription(appServer, forkedThreadId, 'fork-finished');
|
|
2637
2587
|
return forkedThreadId;
|
|
2638
2588
|
}
|
|
2639
|
-
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
|
|
2589
|
+
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title, context) {
|
|
2640
2590
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
|
|
2641
2591
|
try {
|
|
2642
|
-
const
|
|
2592
|
+
const appServer = this.getAppServerClient();
|
|
2593
|
+
const history = await this.readForkHistory(appServer, agentSessionId);
|
|
2594
|
+
const messages = this.mapThreadToSessionMessages(history, agentSessionId);
|
|
2595
|
+
const turns = buildSessionTurnList(messages);
|
|
2643
2596
|
const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
|
|
2644
2597
|
if (targetIndex < 0)
|
|
2645
2598
|
throw new Error('fork point not found');
|
|
2646
|
-
const
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
try {
|
|
2651
|
-
const rolledBack = await this.rollbackSessionTurnsLocked(forkedThreadId, turnsToDiscard);
|
|
2652
|
-
if (!rolledBack)
|
|
2653
|
-
throw new Error('forked thread rollback failed');
|
|
2654
|
-
}
|
|
2655
|
-
finally {
|
|
2656
|
-
rollbackRelease();
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
return forkedThreadId;
|
|
2599
|
+
const targetTurnId = this.findTurnIdForAssistantMessage(history, assistantMessageId);
|
|
2600
|
+
if (!targetTurnId)
|
|
2601
|
+
throw new Error('fork point turn not found');
|
|
2602
|
+
return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId, context);
|
|
2660
2603
|
}
|
|
2661
2604
|
finally {
|
|
2662
2605
|
release();
|
|
2663
2606
|
}
|
|
2664
2607
|
}
|
|
2608
|
+
findTurnIdForAssistantMessage(response, assistantMessageId) {
|
|
2609
|
+
for (const turn of response.thread?.turns ?? []) {
|
|
2610
|
+
if (typeof turn.id !== 'string' || !turn.id)
|
|
2611
|
+
continue;
|
|
2612
|
+
for (const item of this.getTurnItems(turn)) {
|
|
2613
|
+
if (item.type === 'agentMessage' && item.id === assistantMessageId)
|
|
2614
|
+
return turn.id;
|
|
2615
|
+
}
|
|
2616
|
+
// Older app-server histories may omit item ids; the mapper then uses the
|
|
2617
|
+
// turn id as the assistant message id fallback.
|
|
2618
|
+
if (turn.id === assistantMessageId)
|
|
2619
|
+
return turn.id;
|
|
2620
|
+
}
|
|
2621
|
+
return undefined;
|
|
2622
|
+
}
|
|
2623
|
+
async readForkHistory(appServer, threadId) {
|
|
2624
|
+
let directRead;
|
|
2625
|
+
let directReadError;
|
|
2626
|
+
try {
|
|
2627
|
+
directRead = await appServer.threadRead(threadId, true);
|
|
2628
|
+
if ((directRead.thread?.turns?.length ?? 0) > 0)
|
|
2629
|
+
return directRead;
|
|
2630
|
+
}
|
|
2631
|
+
catch (error) {
|
|
2632
|
+
directReadError = error;
|
|
2633
|
+
}
|
|
2634
|
+
// Paginated threads reject full-history thread/read. Hydrate turns through
|
|
2635
|
+
// the experimental paging RPC, retaining full items so assistant message
|
|
2636
|
+
// IDs can be mapped to their containing turn.
|
|
2637
|
+
const turns = [];
|
|
2638
|
+
let cursor;
|
|
2639
|
+
const seenCursors = new Set();
|
|
2640
|
+
try {
|
|
2641
|
+
do {
|
|
2642
|
+
const page = await appServer.threadTurnsList(threadId, 100, cursor);
|
|
2643
|
+
if (Array.isArray(page.data)) {
|
|
2644
|
+
turns.push(...page.data.filter((turn) => !!turn && typeof turn === 'object'));
|
|
2645
|
+
}
|
|
2646
|
+
const nextCursor = typeof page.nextCursor === 'string' && page.nextCursor ? page.nextCursor : undefined;
|
|
2647
|
+
if (!nextCursor || seenCursors.has(nextCursor))
|
|
2648
|
+
break;
|
|
2649
|
+
seenCursors.add(nextCursor);
|
|
2650
|
+
cursor = nextCursor;
|
|
2651
|
+
} while (true);
|
|
2652
|
+
}
|
|
2653
|
+
catch (error) {
|
|
2654
|
+
if (directReadError)
|
|
2655
|
+
throw directReadError;
|
|
2656
|
+
throw error;
|
|
2657
|
+
}
|
|
2658
|
+
// Pages are returned newest-first when following `nextCursor`; restore
|
|
2659
|
+
// chronological order before mapping turns to session messages.
|
|
2660
|
+
if (turns.length > 0)
|
|
2661
|
+
return { thread: { ...(directRead?.thread ?? {}), id: threadId, turns: turns.reverse() } };
|
|
2662
|
+
if (directRead)
|
|
2663
|
+
return directRead;
|
|
2664
|
+
throw directReadError instanceof Error ? directReadError : new Error('Codex thread history unavailable');
|
|
2665
|
+
}
|
|
2665
2666
|
async setSessionName(agentSessionId, name) {
|
|
2666
2667
|
return this.getAppServerClientForThread(agentSessionId).threadSetName(agentSessionId, name);
|
|
2667
2668
|
}
|
|
@@ -2670,7 +2671,7 @@ export class CodexRunner {
|
|
|
2670
2671
|
return this.getAppServerClientForThread(agentSessionId).threadMetadataUpdate(agentSessionId, gitInfo);
|
|
2671
2672
|
}
|
|
2672
2673
|
async getSessionMessages(agentSessionId, projectPath) {
|
|
2673
|
-
const response = await this.getAppServerClientForThread(agentSessionId)
|
|
2674
|
+
const response = await this.readForkHistory(this.getAppServerClientForThread(agentSessionId), agentSessionId);
|
|
2674
2675
|
return this.mapThreadToSessionMessages(response, agentSessionId);
|
|
2675
2676
|
}
|
|
2676
2677
|
approvalItemKey(threadId, turnId, itemId) {
|
|
@@ -3010,9 +3011,9 @@ export class CodexRunner {
|
|
|
3010
3011
|
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
3011
3012
|
|| request.method === 'execCommandApproval';
|
|
3012
3013
|
const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
|
|
3013
|
-
const
|
|
3014
|
+
const sessionCredential = this.sessionTokenForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
|
|
3014
3015
|
const canonicalEcApproval = isCommandApproval
|
|
3015
|
-
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey),
|
|
3016
|
+
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), sessionCredential)
|
|
3016
3017
|
: undefined;
|
|
3017
3018
|
const approvedCommand = canonicalEcApproval?.argv;
|
|
3018
3019
|
const denyInfrastructure = async (policyCode, message) => {
|
|
@@ -3062,23 +3063,23 @@ export class CodexRunner {
|
|
|
3062
3063
|
const decision = typeof resolution === 'string' ? resolution : resolution.decision;
|
|
3063
3064
|
const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
|
|
3064
3065
|
if (decision !== 'deny' && managedEcIntent && approvedCommand) {
|
|
3065
|
-
const
|
|
3066
|
+
const credentialThreadIds = [
|
|
3066
3067
|
this.activeSessions.get(sessionKey),
|
|
3067
3068
|
this.provisionalSessions.get(sessionKey),
|
|
3068
3069
|
typeof params.threadId === 'string' ? params.threadId : undefined,
|
|
3069
3070
|
typeof params.conversationId === 'string' ? params.conversationId : undefined,
|
|
3070
3071
|
];
|
|
3071
|
-
const
|
|
3072
|
+
const hasSessionCredential = credentialThreadIds.some(threadId => !!threadId && !!this.ensureThreadCredentialLoaded(threadId));
|
|
3072
3073
|
const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
|
|
3073
|
-
// A
|
|
3074
|
+
// A credentialed thread is managed even if its permission context
|
|
3074
3075
|
// was not wired with an arm callback; it must fail closed here rather
|
|
3075
|
-
// than reaching daemon IPC with an unarmed
|
|
3076
|
-
if ((
|
|
3077
|
-
&& canonicalEcApproval?.
|
|
3078
|
-
&& canonicalEcApproval.
|
|
3079
|
-
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');
|
|
3080
3081
|
}
|
|
3081
|
-
if ((
|
|
3082
|
+
if ((hasSessionCredential || canArmDelegation)
|
|
3082
3083
|
&& !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
|
|
3083
3084
|
return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
|
|
3084
3085
|
}
|
|
@@ -3733,14 +3734,7 @@ export class CodexRunner {
|
|
|
3733
3734
|
if (paths.length === 0)
|
|
3734
3735
|
return true;
|
|
3735
3736
|
const workspace = resolveProtectedCandidate(projectPath);
|
|
3736
|
-
const
|
|
3737
|
-
const trustedTempDir = managedTempDir ? this.isTrustedSessionTempDir(managedTempDir) : false;
|
|
3738
|
-
if (managedTempDir && !trustedTempDir) {
|
|
3739
|
-
logger.warn(`[CodexRunner] managed TMPDIR lost its trusted ownership or mode: session=${sessionKey} path=${managedTempDir}`);
|
|
3740
|
-
}
|
|
3741
|
-
const canonicalTempDir = managedTempDir && trustedTempDir
|
|
3742
|
-
? resolveProtectedCandidate(managedTempDir)
|
|
3743
|
-
: undefined;
|
|
3737
|
+
const canonicalTempDir = this.getManagedTempDir(sessionKey);
|
|
3744
3738
|
return paths.some(candidate => {
|
|
3745
3739
|
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(workspace, candidate);
|
|
3746
3740
|
const canonical = resolveProtectedCandidate(absolute);
|
|
@@ -3835,7 +3829,7 @@ export class CodexRunner {
|
|
|
3835
3829
|
// distinguishes this explicit empty value from runners without a
|
|
3836
3830
|
// session-temp capability.
|
|
3837
3831
|
managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
|
|
3838
|
-
|
|
3832
|
+
sessionCredential: this.sessionTokenForSession(sessionKey),
|
|
3839
3833
|
allowProtectedMetadata: false,
|
|
3840
3834
|
selfAid: permissionContext?.selfAid,
|
|
3841
3835
|
channel: permissionContext?.channel,
|
|
@@ -4192,17 +4186,17 @@ export class CodexRunner {
|
|
|
4192
4186
|
return;
|
|
4193
4187
|
this.childThreadSessions.set(childThreadId, sessionId);
|
|
4194
4188
|
this.threadProjectPaths.set(childThreadId, path.resolve(projectPath));
|
|
4195
|
-
// Child approvals use the parent's capability boundary and
|
|
4196
|
-
//
|
|
4189
|
+
// Child approvals use the parent's capability boundary and session
|
|
4190
|
+
// credential. This is inheritance, not a second permission decision.
|
|
4197
4191
|
const parentBoundary = parentThreadId ? this.threadExternalToolBoundaries.get(parentThreadId) : undefined;
|
|
4198
4192
|
const parentFingerprint = parentThreadId ? this.threadExternalToolFingerprints.get(parentThreadId) : undefined;
|
|
4199
4193
|
if (parentBoundary)
|
|
4200
4194
|
this.threadExternalToolBoundaries.set(childThreadId, parentBoundary);
|
|
4201
4195
|
if (parentFingerprint)
|
|
4202
4196
|
this.threadExternalToolFingerprints.set(childThreadId, parentFingerprint);
|
|
4203
|
-
const
|
|
4204
|
-
if (
|
|
4205
|
-
this.
|
|
4197
|
+
const parentToken = parentThreadId ? this.ensureThreadCredentialLoaded(parentThreadId) : undefined;
|
|
4198
|
+
if (parentToken)
|
|
4199
|
+
this.ensureThreadCredential(childThreadId, parentToken);
|
|
4206
4200
|
}
|
|
4207
4201
|
unregisterChildThread(childThreadId) {
|
|
4208
4202
|
this.childThreadSessions.delete(childThreadId);
|
|
@@ -4210,8 +4204,8 @@ export class CodexRunner {
|
|
|
4210
4204
|
this.threadProjectPaths.delete(childThreadId);
|
|
4211
4205
|
this.threadExternalToolBoundaries.delete(childThreadId);
|
|
4212
4206
|
this.threadExternalToolFingerprints.delete(childThreadId);
|
|
4213
|
-
this.threadDelegationCarriers.delete(childThreadId);
|
|
4214
4207
|
this.clearTrackedApprovalItemsForThread(childThreadId);
|
|
4208
|
+
this.dropThreadCredential(childThreadId);
|
|
4215
4209
|
}
|
|
4216
4210
|
cleanupChildThreadsForSession(sessionId) {
|
|
4217
4211
|
for (const [childThreadId, ownerSessionId] of this.childThreadSessions) {
|
|
@@ -4351,10 +4345,14 @@ export class CodexRunner {
|
|
|
4351
4345
|
}
|
|
4352
4346
|
case 'item/agentMessage/delta': {
|
|
4353
4347
|
const itemId = typeof params.itemId === 'string' ? params.itemId : undefined;
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
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' };
|
|
4358
4356
|
}
|
|
4359
4357
|
break;
|
|
4360
4358
|
}
|
|
@@ -4415,24 +4413,46 @@ export class CodexRunner {
|
|
|
4415
4413
|
this.clearPendingToolDenialsForTurn(state.threadId, turnId);
|
|
4416
4414
|
yield* this.reconcileOpenAppServerToolCalls(state, turnId);
|
|
4417
4415
|
if (turn.status === 'failed' && turn.error?.message) {
|
|
4416
|
+
logger.warn(`[CodexRunner] turn failed: ${JSON.stringify({
|
|
4417
|
+
sessionId, threadId: state.threadId, turnId, model: state.model,
|
|
4418
|
+
errorType: classifyError(new Error(turn.error.message)),
|
|
4419
|
+
message: formatApprovalLogSummary(turn.error.message),
|
|
4420
|
+
lastReportedUsage: state.tokenUsage?.last,
|
|
4421
|
+
})}`);
|
|
4418
4422
|
if (isRetryableError(new Error(turn.error.message))) {
|
|
4419
4423
|
throw new Error(turn.error.message);
|
|
4420
4424
|
}
|
|
4421
|
-
yield { type: 'error', error: turn.error.message, errorType:
|
|
4425
|
+
yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
|
|
4422
4426
|
}
|
|
4423
4427
|
yield this.mapAppServerTurnComplete(turn, state);
|
|
4424
4428
|
break;
|
|
4425
4429
|
}
|
|
4426
4430
|
case 'error': {
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4431
|
+
// Current app-server uses error.message; older builds used message.
|
|
4432
|
+
const message = typeof params.error?.message === 'string' ? params.error.message : params.message;
|
|
4433
|
+
if (typeof message !== 'string' || !message.trim()) {
|
|
4434
|
+
logger.debug('[CodexRunner] app-server error notification without a diagnostic message');
|
|
4430
4435
|
break;
|
|
4431
4436
|
}
|
|
4432
|
-
|
|
4433
|
-
|
|
4437
|
+
logger.warn(`[CodexRunner] error notification: ${JSON.stringify({
|
|
4438
|
+
sessionId, threadId: state.threadId, turnId: state.turnId, model: state.model,
|
|
4439
|
+
willRetry: params.willRetry ?? null,
|
|
4440
|
+
errorType: classifyError(new Error(message)), message: formatApprovalLogSummary(message),
|
|
4441
|
+
})}`);
|
|
4442
|
+
// Codex owns retries within this turn. Throwing here closes our subscription
|
|
4443
|
+
// before its recovery can finish; runQuery must not submit the input again.
|
|
4444
|
+
if (params.willRetry === true) {
|
|
4445
|
+
yield {
|
|
4446
|
+
type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
|
|
4447
|
+
message: isOutputTokenLimitError(new Error(message))
|
|
4448
|
+
? '模型响应达到输出上限,Codex 正在当前轮内重试。'
|
|
4449
|
+
: 'Codex 正在当前轮内重试模型请求。',
|
|
4450
|
+
};
|
|
4451
|
+
break;
|
|
4434
4452
|
}
|
|
4435
|
-
|
|
4453
|
+
if (isRetryableError(new Error(message)))
|
|
4454
|
+
throw new Error(message);
|
|
4455
|
+
yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
|
|
4436
4456
|
break;
|
|
4437
4457
|
}
|
|
4438
4458
|
}
|
|
@@ -4633,10 +4653,35 @@ export class CodexRunner {
|
|
|
4633
4653
|
switch (item.type) {
|
|
4634
4654
|
case 'agentMessage':
|
|
4635
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
|
+
}
|
|
4636
4679
|
const buffered = item.id ? state.agentMessageDeltaText.get(item.id) : undefined;
|
|
4637
4680
|
const text = typeof item.text === 'string' && item.text ? item.text : buffered;
|
|
4638
|
-
if (text)
|
|
4639
|
-
|
|
4681
|
+
if (text) {
|
|
4682
|
+
state.lastAssistantUuid = item.id || state.turnId;
|
|
4683
|
+
yield { type: 'text', text, assistantUuid: state.lastAssistantUuid };
|
|
4684
|
+
}
|
|
4640
4685
|
if (item.id)
|
|
4641
4686
|
state.agentMessageDeltaText.delete(item.id);
|
|
4642
4687
|
}
|
|
@@ -4863,16 +4908,18 @@ export class CodexRunner {
|
|
|
4863
4908
|
}
|
|
4864
4909
|
mapAppServerTurnComplete(turn, state) {
|
|
4865
4910
|
const status = turn.status || 'completed';
|
|
4911
|
+
const outputLimit = status === 'failed' && isOutputTokenLimitError(new Error(turn.error?.message ?? ''));
|
|
4866
4912
|
const tokenUsage = this.mapCodexTokenUsage(state.tokenUsage?.last ?? turn.tokenUsage ?? turn.usage);
|
|
4867
4913
|
const contextUsage = this.mapCodexContextUsage(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context);
|
|
4868
4914
|
const terminalReason = status === 'completed'
|
|
4869
4915
|
? undefined
|
|
4870
4916
|
: status === 'interrupted'
|
|
4871
4917
|
? 'aborted_streaming'
|
|
4872
|
-
: status;
|
|
4918
|
+
: outputLimit ? 'max_output_tokens' : status;
|
|
4873
4919
|
return {
|
|
4874
4920
|
type: 'complete',
|
|
4875
|
-
subtype: status === 'completed' ? 'success' : status,
|
|
4921
|
+
subtype: status === 'completed' ? 'success' : outputLimit ? 'max_output_tokens' : status,
|
|
4922
|
+
assistantUuid: state.lastAssistantUuid,
|
|
4876
4923
|
isError: status === 'failed',
|
|
4877
4924
|
errors: turn.error?.message ? [turn.error.message] : undefined,
|
|
4878
4925
|
terminalReason,
|
|
@@ -4886,126 +4933,36 @@ export class CodexRunner {
|
|
|
4886
4933
|
contextUsage,
|
|
4887
4934
|
};
|
|
4888
4935
|
}
|
|
4889
|
-
managedTempParent() {
|
|
4890
|
-
const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
|
4891
|
-
const base = ensureProcessManagedTempDir();
|
|
4892
|
-
return path.join(resolveProtectedCandidate(base), `evolcore-codex-${uid}`);
|
|
4893
|
-
}
|
|
4894
|
-
ensurePrivateDirectory(directory) {
|
|
4895
|
-
try {
|
|
4896
|
-
fs.mkdirSync(directory, { mode: 0o700 });
|
|
4897
|
-
}
|
|
4898
|
-
catch (error) {
|
|
4899
|
-
if (error?.code !== 'EEXIST')
|
|
4900
|
-
throw error;
|
|
4901
|
-
}
|
|
4902
|
-
const stat = fs.lstatSync(directory);
|
|
4903
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
4904
|
-
throw new Error(`managed temp path is not a real directory: ${directory}`);
|
|
4905
|
-
}
|
|
4906
|
-
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
4907
|
-
throw new Error(`managed temp path is owned by another uid: ${directory}`);
|
|
4908
|
-
}
|
|
4909
|
-
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
|
|
4910
|
-
fs.chmodSync(directory, 0o700);
|
|
4911
|
-
}
|
|
4912
|
-
ensureSessionTempDir(sessionId) {
|
|
4913
|
-
const existing = this.sessionTempDirs.get(sessionId);
|
|
4914
|
-
if (existing) {
|
|
4915
|
-
try {
|
|
4916
|
-
this.ensurePrivateDirectory(this.managedTempParent());
|
|
4917
|
-
this.ensurePrivateDirectory(existing);
|
|
4918
|
-
return existing;
|
|
4919
|
-
}
|
|
4920
|
-
catch (error) {
|
|
4921
|
-
this.sessionTempDirs.delete(sessionId);
|
|
4922
|
-
logger.warn(`[CodexRunner] existing managed TMPDIR is no longer usable: session=${sessionId} path=${existing} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4923
|
-
return undefined;
|
|
4924
|
-
}
|
|
4925
|
-
}
|
|
4926
|
-
try {
|
|
4927
|
-
const parent = this.managedTempParent();
|
|
4928
|
-
this.ensurePrivateDirectory(parent);
|
|
4929
|
-
this.ensurePrivateDirectory(path.join(parent, 'evolcore-locks'));
|
|
4930
|
-
// Include a per-runner nonce so a daemon restart never reuses contents
|
|
4931
|
-
// left behind by an unclean shutdown for the same EvolCore session id.
|
|
4932
|
-
const digest = createHash('sha256')
|
|
4933
|
-
.update(this.managedTempNonce)
|
|
4934
|
-
.update('\0')
|
|
4935
|
-
.update(sessionId)
|
|
4936
|
-
.digest('hex')
|
|
4937
|
-
.slice(0, 24);
|
|
4938
|
-
const directory = path.join(parent, digest);
|
|
4939
|
-
this.ensurePrivateDirectory(directory);
|
|
4940
|
-
this.sessionTempDirs.set(sessionId, directory);
|
|
4941
|
-
registerManagedTempDirForExitCleanup(directory, parent);
|
|
4942
|
-
return directory;
|
|
4943
|
-
}
|
|
4944
|
-
catch (error) {
|
|
4945
|
-
logger.warn(`[CodexRunner] managed TMPDIR unavailable for session=${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4946
|
-
return undefined;
|
|
4947
|
-
}
|
|
4948
|
-
}
|
|
4949
|
-
isTrustedSessionTempDir(directory) {
|
|
4950
|
-
return isTrustedManagedTempDirectory(directory, this.managedTempParent());
|
|
4951
|
-
}
|
|
4952
4936
|
getManagedTempDir(sessionId) {
|
|
4953
|
-
const directory =
|
|
4954
|
-
|
|
4955
|
-
return undefined;
|
|
4956
|
-
return resolveProtectedCandidate(directory);
|
|
4957
|
-
}
|
|
4958
|
-
cleanupSessionTempDir(sessionId) {
|
|
4959
|
-
const directory = this.sessionTempDirs.get(sessionId);
|
|
4960
|
-
this.sessionTempDirs.delete(sessionId);
|
|
4961
|
-
if (!directory)
|
|
4962
|
-
return;
|
|
4963
|
-
// Reuse the same ownership, mode, lexical-parent, and canonicalization
|
|
4964
|
-
// checks used at execution time. If the path changed, leave it in place
|
|
4965
|
-
// rather than recursively removing an attacker-selected target.
|
|
4966
|
-
if (!this.isTrustedSessionTempDir(directory)) {
|
|
4967
|
-
managedTempDirsForExitCleanup.delete(directory);
|
|
4968
|
-
logger.warn(`[CodexRunner] refused to clean unmanaged TMPDIR: session=${sessionId} path=${directory}`);
|
|
4969
|
-
return;
|
|
4970
|
-
}
|
|
4971
|
-
const candidate = path.resolve(directory);
|
|
4972
|
-
try {
|
|
4973
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
4974
|
-
managedTempDirsForExitCleanup.delete(directory);
|
|
4975
|
-
}
|
|
4976
|
-
catch (error) {
|
|
4977
|
-
logger.warn(`[CodexRunner] failed to clean managed TMPDIR: session=${sessionId} path=${candidate} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4978
|
-
}
|
|
4937
|
+
const directory = ensureSessionRuntimeDir(sessionId);
|
|
4938
|
+
return directory ? resolveProtectedCandidate(directory) : undefined;
|
|
4979
4939
|
}
|
|
4980
4940
|
managedTempInstruction(sessionId) {
|
|
4981
|
-
const managedTempDir =
|
|
4941
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
4982
4942
|
if (!managedTempDir)
|
|
4983
4943
|
return undefined;
|
|
4984
|
-
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp
|
|
4944
|
+
return `临时文件必须使用环境变量 \`$TMPDIR\`。当前值是 \`${managedTempDir}\`,它是当前 EvolCore 会话唯一允许的临时工作区;不要直接使用 \`/tmp\`,也不要使用其他会话的临时目录。该目录随 EvolCore sessionId 保持稳定;若文件不存在,请先在当前 \`$TMPDIR\` 重新创建,再继续执行后续 \`ec\` 命令。`;
|
|
4985
4945
|
}
|
|
4986
|
-
buildEvolcoreShellEnvironmentConfig(sessionId,
|
|
4987
|
-
const managedTempDir =
|
|
4946
|
+
buildEvolcoreShellEnvironmentConfig(sessionId, sessionToken, taskDelegationToken) {
|
|
4947
|
+
const managedTempDir = ensureSessionRuntimeDir(sessionId);
|
|
4988
4948
|
// The managed-hook credential rides the thread's shell environment so the
|
|
4989
4949
|
// PreToolUse hook can prove the call belongs to an EvolCore-owned thread.
|
|
4990
4950
|
// It follows the thread (start AND resume) rather than the daemon process.
|
|
4991
|
-
const sessionToken = threadId ? this.ensureThreadCredential(threadId) : undefined;
|
|
4992
4951
|
return {
|
|
4993
4952
|
shell_environment_policy: {
|
|
4994
4953
|
set: {
|
|
4995
4954
|
EVOLCORE_SESSION_ID: sessionId,
|
|
4996
4955
|
EVOLCORE_HOME: resolveRoot(),
|
|
4997
4956
|
...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
|
|
4998
|
-
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(
|
|
4999
|
-
...(taskDelegationToken
|
|
5000
|
-
? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
|
|
5001
|
-
: {}),
|
|
4957
|
+
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(managedTempDir, 'locks') } : {}),
|
|
4958
|
+
...(taskDelegationToken ? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken } : {}),
|
|
5002
4959
|
...(sessionToken ? { [CODEX_SESSION_TOKEN_ENV]: sessionToken } : {}),
|
|
5003
4960
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
5004
4961
|
},
|
|
5005
4962
|
},
|
|
5006
4963
|
};
|
|
5007
4964
|
}
|
|
5008
|
-
sanitizeThreadEnvironmentConfig(config, permissionMode) {
|
|
4965
|
+
sanitizeThreadEnvironmentConfig(config, permissionMode, taskDelegationToken) {
|
|
5009
4966
|
if (!config)
|
|
5010
4967
|
return null;
|
|
5011
4968
|
const policy = this.configRecord(config.shell_environment_policy);
|
|
@@ -5015,11 +4972,40 @@ export class CodexRunner {
|
|
|
5015
4972
|
const safeSet = sanitizeShellExecutionEnvironment(set, {
|
|
5016
4973
|
lockExecutableResolution: permissionMode === 'readonly',
|
|
5017
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])];
|
|
5018
5001
|
return {
|
|
5019
5002
|
...config,
|
|
5020
5003
|
shell_environment_policy: {
|
|
5021
5004
|
...policy,
|
|
5022
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,
|
|
5023
5009
|
},
|
|
5024
5010
|
};
|
|
5025
5011
|
}
|
|
@@ -5036,21 +5022,23 @@ export class CodexRunner {
|
|
|
5036
5022
|
this.activeSessions.clear();
|
|
5037
5023
|
this.provisionalSessions.clear();
|
|
5038
5024
|
this.activeTurns.clear();
|
|
5025
|
+
for (const childThreadId of [...this.childThreadSessions.keys()])
|
|
5026
|
+
this.unregisterChildThread(childThreadId);
|
|
5039
5027
|
this.activeSubagentTurns.clear();
|
|
5040
5028
|
this.childThreadSessions.clear();
|
|
5041
5029
|
this.threadProjectPaths.clear();
|
|
5042
5030
|
this.threadExternalToolFingerprints.clear();
|
|
5043
5031
|
this.threadExternalToolBoundaries.clear();
|
|
5044
5032
|
this.trackedApprovalItems.clear();
|
|
5045
|
-
this.threadDelegationCarriers.clear();
|
|
5046
5033
|
this.pendingInterrupts.clear();
|
|
5034
|
+
for (const record of this.unconfirmedInterrupts.values())
|
|
5035
|
+
record.unsubscribe?.();
|
|
5036
|
+
this.unconfirmedInterrupts.clear();
|
|
5047
5037
|
this.steeringTails.clear();
|
|
5048
5038
|
this.permissionContexts.clear();
|
|
5049
5039
|
this.chatModes.clear();
|
|
5050
5040
|
this.pruneThreadOperationLocks();
|
|
5051
5041
|
this.threadOperationLocks.clear();
|
|
5052
|
-
for (const sessionId of [...this.sessionTempDirs.keys()])
|
|
5053
|
-
this.cleanupSessionTempDir(sessionId);
|
|
5054
5042
|
await Promise.all([
|
|
5055
5043
|
this.appServerClient?.close(),
|
|
5056
5044
|
this.fullAccessAppServerClient?.close(),
|