evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Implements the same interface surface as AgentRunner (claude-runner.ts)
|
|
6
6
|
* so MessageProcessor and CommandHandler can work with it transparently.
|
|
7
7
|
*/
|
|
8
|
+
import { hasTrustedFullAccessContext } from './runner-types.js';
|
|
8
9
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
9
10
|
import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
10
|
-
import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
11
|
-
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/protected-paths.js';
|
|
11
|
+
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
12
|
+
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
|
|
12
13
|
import { CodexAppServerClient } from './codex-app-server-client.js';
|
|
13
14
|
import { resolveOpenaiConfig } from './baseagent.js';
|
|
14
15
|
import { logger } from '../utils/logger.js';
|
|
@@ -16,14 +17,14 @@ import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
|
16
17
|
import { auditCodexApprovalDecision, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
17
18
|
import { isRetryableError } from '../utils/error-utils.js';
|
|
18
19
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
19
|
-
import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
20
|
+
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
20
21
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
21
22
|
import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
|
|
22
23
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
23
|
-
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent,
|
|
24
|
+
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
|
|
24
25
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
25
26
|
import { resolvePaths, resolveRoot } from '../paths.js';
|
|
26
|
-
import { ensureProcessManagedTempDir } from '../cli/task-context.js';
|
|
27
|
+
import { ensureProcessManagedTempDir, getManagedTaskTempDir } from '../cli/task-context.js';
|
|
27
28
|
import { buildSessionTurnList } from '../core/session/session-turns.js';
|
|
28
29
|
import { execFileSync } from 'child_process';
|
|
29
30
|
import { execCodexCliSync, resolveCodexCliPath } from '../utils/codex-cli.js';
|
|
@@ -39,6 +40,13 @@ const MIME_EXT = {
|
|
|
39
40
|
'image/gif': '.gif',
|
|
40
41
|
'image/webp': '.webp',
|
|
41
42
|
};
|
|
43
|
+
const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
|
|
44
|
+
export function formatApprovalLogSummary(summary) {
|
|
45
|
+
const singleLine = summary.replace(/\s+/gu, ' ').trim();
|
|
46
|
+
if (singleLine.length <= APPROVAL_LOG_SUMMARY_MAX_LENGTH)
|
|
47
|
+
return singleLine;
|
|
48
|
+
return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
|
|
49
|
+
}
|
|
42
50
|
class AsyncEventQueue {
|
|
43
51
|
queue = [];
|
|
44
52
|
done = false;
|
|
@@ -75,6 +83,17 @@ class AsyncEventQueue {
|
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
}
|
|
86
|
+
function errorCodeForToolDenial(decisionSource, policyCode) {
|
|
87
|
+
if (decisionSource === 'policy')
|
|
88
|
+
return 'POLICY_DENIED';
|
|
89
|
+
if (decisionSource === 'approval')
|
|
90
|
+
return 'USER_DENIED';
|
|
91
|
+
if (policyCode && /delegation|carrier/i.test(policyCode))
|
|
92
|
+
return 'DELEGATION_FAILED';
|
|
93
|
+
if (policyCode && /argument|canonical|quote|shell/i.test(policyCode))
|
|
94
|
+
return 'INVALID_ARGUMENT';
|
|
95
|
+
return 'EXECUTION_FAILED';
|
|
96
|
+
}
|
|
78
97
|
const managedTempDirsForExitCleanup = new Map();
|
|
79
98
|
let managedTempExitCleanupRegistered = false;
|
|
80
99
|
function isTrustedManagedTempDirectory(directory, parent) {
|
|
@@ -142,6 +161,63 @@ function stableSecurityValue(value) {
|
|
|
142
161
|
function externalToolConfigFingerprint(config) {
|
|
143
162
|
return createHash('sha256').update(stableSecurityValue(config)).digest('hex');
|
|
144
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Codex can surface a managed hook rejection as item output instead of an
|
|
166
|
+
* app-server approval request. Preserve the policy boundary in that path too
|
|
167
|
+
* so downstream audit/event consumers never infer a successful execution.
|
|
168
|
+
*/
|
|
169
|
+
function parseManagedPreToolUseDenial(value) {
|
|
170
|
+
let text;
|
|
171
|
+
if (typeof value === 'string')
|
|
172
|
+
text = value;
|
|
173
|
+
else if (value == null)
|
|
174
|
+
return undefined;
|
|
175
|
+
else {
|
|
176
|
+
try {
|
|
177
|
+
text = JSON.stringify(value) ?? '';
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
text = String(value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const marker = /(?:Command|Tool call) blocked by PreToolUse hook\s*:\s*/i.exec(text);
|
|
184
|
+
if (!marker)
|
|
185
|
+
return undefined;
|
|
186
|
+
let reason = text.slice((marker.index ?? 0) + marker[0].length)
|
|
187
|
+
.replace(/\s*\.\s*(?:Command|Tool):[\s\S]*$/i, '')
|
|
188
|
+
.replace(/(?:^|\s)(?:Command|Tool):[\s\S]*$/i, '')
|
|
189
|
+
.trim();
|
|
190
|
+
if (!reason)
|
|
191
|
+
reason = 'EvolCore PreToolUse policy denied this tool call';
|
|
192
|
+
const policyCode = /工具调用已达到\s*\d+\s*次/.test(reason)
|
|
193
|
+
? 'proactive_tool_report_required'
|
|
194
|
+
: /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
|
|
195
|
+
? 'proactive_first_send_required'
|
|
196
|
+
: 'managed_pre_tool_use_denied';
|
|
197
|
+
return { policyCode, reason };
|
|
198
|
+
}
|
|
199
|
+
function outputTextForManagedHook(value) {
|
|
200
|
+
if (typeof value === 'string')
|
|
201
|
+
return value;
|
|
202
|
+
if (value == null)
|
|
203
|
+
return '';
|
|
204
|
+
if (Array.isArray(value))
|
|
205
|
+
return value.map(outputTextForManagedHook).filter(Boolean).join('\n');
|
|
206
|
+
if (typeof value === 'object') {
|
|
207
|
+
const record = value;
|
|
208
|
+
if (typeof record.text === 'string')
|
|
209
|
+
return record.text;
|
|
210
|
+
if (typeof record.message === 'string')
|
|
211
|
+
return record.message;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.stringify(value) ?? '';
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return String(value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return String(value);
|
|
220
|
+
}
|
|
145
221
|
const CODEX_CATALOG_FALLBACK = [
|
|
146
222
|
{ slug: 'gpt-5.5', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
147
223
|
{ slug: 'gpt-5.4', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
@@ -150,6 +226,11 @@ const CODEX_CATALOG_FALLBACK = [
|
|
|
150
226
|
{ slug: 'gpt-5.2', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
151
227
|
];
|
|
152
228
|
const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
229
|
+
const CODEX_NO_ROLLOUT_RE = /no rollout found for thread id/i;
|
|
230
|
+
function isCodexNoRolloutError(error) {
|
|
231
|
+
return error?.code === 'CODEX_ROLLOUT_NOT_FOUND'
|
|
232
|
+
|| CODEX_NO_ROLLOUT_RE.test(error instanceof Error ? error.message : String(error));
|
|
233
|
+
}
|
|
153
234
|
let codexCatalogCache = null;
|
|
154
235
|
// The permission bridge is version-locked to the audited approval schema and
|
|
155
236
|
// managed PreToolUse hook behavior used by proactive first-tool enforcement.
|
|
@@ -248,15 +329,22 @@ export class CodexRunner {
|
|
|
248
329
|
activeAbortControllers = new Map();
|
|
249
330
|
activeStreams = new Map();
|
|
250
331
|
activeSessions = new Map(); // sessionId → threadId
|
|
332
|
+
/** Threads created/resumed for a turn but not yet durably admitted by turn/start. */
|
|
333
|
+
provisionalSessions = new Map();
|
|
251
334
|
activeTurns = new Map();
|
|
335
|
+
activeSubagentTurns = new Map();
|
|
252
336
|
threadProjectPaths = new Map();
|
|
337
|
+
/** Child Codex threads inherit the EvolCore session that started them. */
|
|
338
|
+
childThreadSessions = new Map();
|
|
253
339
|
sessionTempDirs = new Map();
|
|
254
340
|
managedTempNonce = randomBytes(16).toString('hex');
|
|
255
341
|
threadExternalToolFingerprints = new Map();
|
|
256
342
|
threadExternalToolBoundaries = new Map();
|
|
257
343
|
trackedApprovalItems = new Map();
|
|
344
|
+
pendingToolDenials = new Map();
|
|
258
345
|
threadDelegationCarriers = new Map();
|
|
259
346
|
pendingInterrupts = new Map();
|
|
347
|
+
steeringTails = new Map();
|
|
260
348
|
threadOperationLocks = new Map();
|
|
261
349
|
activeOperationReleases = new Map();
|
|
262
350
|
appServerClient = null;
|
|
@@ -267,16 +355,21 @@ export class CodexRunner {
|
|
|
267
355
|
permissionContexts = new Map();
|
|
268
356
|
resolvedConfig;
|
|
269
357
|
pendingInterruptTtlMs = 30_000;
|
|
270
|
-
constructor(config, callbacks) {
|
|
271
|
-
|
|
358
|
+
constructor(config, callbacks, runtime) {
|
|
359
|
+
// Compatibility for legacy direct callers that still pass the removed
|
|
360
|
+
// internal fields in a synthetic config. AgentLoader uses runtime binding.
|
|
361
|
+
const legacy = config.agents?.codex;
|
|
362
|
+
const binding = {
|
|
363
|
+
agentAid: runtime?.agentAid ?? legacy?.['evolcoreAgentAid'],
|
|
364
|
+
agentConfig: runtime?.agentConfig ?? legacy?.['evolcoreAgentConfig'],
|
|
365
|
+
};
|
|
366
|
+
this.resolvedConfig = resolveOpenaiConfig(config, undefined, binding);
|
|
272
367
|
this.resolvedConfig.headers = buildModelRequestHeaders({
|
|
273
368
|
baseagent: 'codex',
|
|
274
369
|
baseUrl: this.resolvedConfig.baseUrl,
|
|
275
|
-
agentAid:
|
|
370
|
+
agentAid: this.resolvedConfig.agentAid,
|
|
276
371
|
configuredHeaders: this.resolvedConfig.headers,
|
|
277
372
|
});
|
|
278
|
-
this.resolvedConfig.evolcoreAgentAid = config.agents?.codex?.evolcoreAgentAid;
|
|
279
|
-
this.resolvedConfig.evolcoreAgentConfig = config.agents?.codex?.evolcoreAgentConfig;
|
|
280
373
|
this.capabilities = {
|
|
281
374
|
clear: false,
|
|
282
375
|
compact: true,
|
|
@@ -288,6 +381,7 @@ export class CodexRunner {
|
|
|
288
381
|
planApproval: false,
|
|
289
382
|
// Current file rewind is intentionally degraded: it restores touched files from Git HEAD.
|
|
290
383
|
fileRewind: 'git-head',
|
|
384
|
+
fullaccess: true,
|
|
291
385
|
};
|
|
292
386
|
this.model = this.resolvedConfig.model;
|
|
293
387
|
if (this.resolvedConfig.effort)
|
|
@@ -316,74 +410,148 @@ export class CodexRunner {
|
|
|
316
410
|
createDelegationCarrier() {
|
|
317
411
|
return randomBytes(32).toString('base64url');
|
|
318
412
|
}
|
|
319
|
-
|
|
413
|
+
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
|
|
320
414
|
const parseOptions = {
|
|
321
415
|
allowManagedTmpDir: true,
|
|
322
416
|
dialect,
|
|
417
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
323
418
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
419
|
+
...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
|
|
324
420
|
};
|
|
325
421
|
const classification = classifyEvolcoreShellCommand(command, parseOptions);
|
|
326
422
|
if (classification.kind === 'bounded-output')
|
|
327
|
-
return classification.command.argv;
|
|
328
|
-
if (classification.kind !== 'literal')
|
|
329
|
-
return undefined;
|
|
423
|
+
return { argv: classification.command.argv };
|
|
330
424
|
// Keep the approval/delegation parser aligned with the preflight parser:
|
|
331
425
|
// the session-managed `$TMPDIR` token is a permitted EC path reference.
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
426
|
+
const parsed = executable
|
|
427
|
+
? parseCodexCarrierCommand({ command, dialect, executable }, parseOptions)
|
|
428
|
+
: dialect === 'powershell'
|
|
429
|
+
? parseCodexCarrierCommand({ command, dialect }, parseOptions)
|
|
430
|
+
: parseLiteralShellCommand(command, parseOptions);
|
|
431
|
+
if (!parsed.ok)
|
|
432
|
+
return { parseFailure: parsed };
|
|
433
|
+
if (normalizeEvolcoreCommandArgv(parsed.argv)[0] !== 'ec')
|
|
434
|
+
return {};
|
|
435
|
+
if (containsLiteralManagedTmpDirOutsideSendContent(parsed.argv)) {
|
|
436
|
+
return { parseFailure: {
|
|
437
|
+
issue: 'unsafe-expansion',
|
|
438
|
+
offset: 0,
|
|
439
|
+
tokenIndex: 0,
|
|
440
|
+
dialect: dialect ?? 'posix',
|
|
441
|
+
inputLength: command.length,
|
|
442
|
+
} };
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
446
|
+
...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
|
|
447
|
+
};
|
|
336
448
|
}
|
|
337
|
-
|
|
449
|
+
resolveCanonicalEcApproval(toolInput, managedTempDir, expectedDelegationToken) {
|
|
338
450
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
339
451
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
340
452
|
? toolInput.commandArgv
|
|
341
453
|
: undefined;
|
|
342
|
-
if (explicitArgv
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
454
|
+
if (explicitArgv && normalizeEvolcoreCommandArgv(explicitArgv)[0] === 'ec') {
|
|
455
|
+
if (explicitArgv.some(value => value.includes('\0'))) {
|
|
456
|
+
return { parseFailure: {
|
|
457
|
+
issue: 'invalid-control-char', offset: 0,
|
|
458
|
+
tokenIndex: explicitArgv.findIndex(value => value.includes('\0')),
|
|
459
|
+
dialect: 'posix', inputLength: 0,
|
|
460
|
+
} };
|
|
461
|
+
}
|
|
462
|
+
return containsLiteralManagedTmpDirOutsideSendContent(explicitArgv)
|
|
463
|
+
? { parseFailure: { issue: 'unsafe-expansion', offset: 0, tokenIndex: 0, dialect: 'posix', inputLength: 0 } }
|
|
464
|
+
: { argv: normalizeEvolcoreCommandArgv(explicitArgv) };
|
|
347
465
|
}
|
|
348
466
|
const explicitCarrier = explicitArgv
|
|
349
467
|
? resolveCodexShellCarrierArgv(explicitArgv)
|
|
350
468
|
: undefined;
|
|
351
469
|
if (explicitCarrier !== undefined) {
|
|
352
|
-
return this.
|
|
470
|
+
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedDelegationToken, explicitCarrier.executable);
|
|
471
|
+
}
|
|
472
|
+
if (hasCodexCmdCarrierEcIntent(toolInput)) {
|
|
473
|
+
const parsed = parseCodexToolCommand(toolInput, {
|
|
474
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
475
|
+
...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
|
|
476
|
+
});
|
|
477
|
+
if (!parsed.ok)
|
|
478
|
+
return { parseFailure: parsed };
|
|
479
|
+
if (parsed.argv[0] === 'ec') {
|
|
480
|
+
return containsLiteralManagedTmpDirOutsideSendContent(parsed.argv)
|
|
481
|
+
? { parseFailure: {
|
|
482
|
+
issue: 'unsafe-expansion', offset: 0, tokenIndex: 0,
|
|
483
|
+
dialect: 'cmd', inputLength: 0,
|
|
484
|
+
} }
|
|
485
|
+
: {
|
|
486
|
+
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
487
|
+
...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
491
|
+
const classification = classifyEvolcoreShellCommand(command, { dialect: 'cmd' });
|
|
492
|
+
return {
|
|
493
|
+
parseFailure: {
|
|
494
|
+
issue: classification.kind === 'composite' ? classification.issue : 'shell-composition',
|
|
495
|
+
offset: 0,
|
|
496
|
+
tokenIndex: 0,
|
|
497
|
+
dialect: 'cmd',
|
|
498
|
+
inputLength: command.length,
|
|
499
|
+
},
|
|
500
|
+
};
|
|
353
501
|
}
|
|
354
502
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
355
|
-
const
|
|
356
|
-
if (directArgv)
|
|
357
|
-
return directArgv;
|
|
358
|
-
const wrappedArgv = parseLiteralShellArgv(command, {
|
|
503
|
+
const canonicalCommand = parseCodexShellCommandArgv(command, {
|
|
359
504
|
allowManagedTmpDir: true,
|
|
505
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
360
506
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
507
|
+
...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
|
|
361
508
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
509
|
+
if (canonicalCommand?.[0] === 'ec') {
|
|
510
|
+
return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
|
|
511
|
+
? { parseFailure: {
|
|
512
|
+
issue: 'unsafe-expansion',
|
|
513
|
+
offset: 0,
|
|
514
|
+
tokenIndex: 0,
|
|
515
|
+
dialect: 'posix',
|
|
516
|
+
inputLength: command.length,
|
|
517
|
+
} }
|
|
518
|
+
: { argv: canonicalCommand };
|
|
519
|
+
}
|
|
520
|
+
const outer = parseLiteralShellCommand(command, {
|
|
521
|
+
allowManagedTmpDir: true,
|
|
522
|
+
...(managedTempDir ? { managedTempDir } : {}),
|
|
523
|
+
});
|
|
524
|
+
if (!outer.ok) {
|
|
525
|
+
const classification = classifyEvolcoreShellCommand(command, {
|
|
526
|
+
allowManagedTmpDir: true,
|
|
527
|
+
...(managedTempDir ? { managedTempDir } : {}),
|
|
528
|
+
});
|
|
529
|
+
return classification.kind === 'composite' ? { parseFailure: outer } : {};
|
|
530
|
+
}
|
|
531
|
+
return {};
|
|
368
532
|
}
|
|
369
533
|
isManagedEvolcoreCommandIntent(toolInput) {
|
|
370
534
|
if (Array.isArray(toolInput.commandArgv)) {
|
|
371
535
|
const rawArgv = toolInput.commandArgv;
|
|
372
|
-
if (rawArgv[0] === 'ec')
|
|
373
|
-
return true;
|
|
374
536
|
if (!rawArgv.every(value => typeof value === 'string'))
|
|
375
537
|
return false;
|
|
538
|
+
if (normalizeEvolcoreCommandArgv(rawArgv)[0] === 'ec')
|
|
539
|
+
return true;
|
|
376
540
|
const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
|
|
377
|
-
|
|
378
|
-
&&
|
|
379
|
-
|
|
380
|
-
|
|
541
|
+
if (shellCarrier?.dialect === 'powershell'
|
|
542
|
+
&& hasPowerShellDelegationAssignment(shellCarrier.command))
|
|
543
|
+
return true;
|
|
544
|
+
return hasCodexCmdCarrierEcIntent(toolInput)
|
|
545
|
+
|| shellCarrier !== undefined
|
|
546
|
+
&& classifyEvolcoreShellCommand(shellCarrier.command, {
|
|
547
|
+
dialect: shellCarrier.dialect,
|
|
548
|
+
}).kind !== 'none';
|
|
381
549
|
}
|
|
382
550
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
383
551
|
return classifyEvolcoreShellCommand(command).kind !== 'none';
|
|
384
552
|
}
|
|
385
553
|
armApprovedDelegationCommand(sessionId, argv) {
|
|
386
|
-
const threadId = this.activeSessions.get(sessionId);
|
|
554
|
+
const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
387
555
|
const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
|
|
388
556
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
389
557
|
const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
|
|
@@ -397,6 +565,21 @@ export class CodexRunner {
|
|
|
397
565
|
return false;
|
|
398
566
|
}
|
|
399
567
|
}
|
|
568
|
+
delegationCarrierForSession(sessionId, threadId) {
|
|
569
|
+
const candidates = [
|
|
570
|
+
threadId,
|
|
571
|
+
this.activeSessions.get(sessionId),
|
|
572
|
+
this.provisionalSessions.get(sessionId),
|
|
573
|
+
];
|
|
574
|
+
for (const candidate of candidates) {
|
|
575
|
+
if (!candidate)
|
|
576
|
+
continue;
|
|
577
|
+
const carrier = this.threadDelegationCarriers.get(candidate);
|
|
578
|
+
if (carrier)
|
|
579
|
+
return carrier;
|
|
580
|
+
}
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
400
583
|
async readThreadDelegationCarrier(appServer, threadId) {
|
|
401
584
|
try {
|
|
402
585
|
const result = await appServer.threadShellCommand(threadId, `node -e "process.stdout.write(process.env.${AGENT_DELEGATION_TOKEN_ENV}||String())"`);
|
|
@@ -429,9 +612,51 @@ export class CodexRunner {
|
|
|
429
612
|
return undefined;
|
|
430
613
|
}
|
|
431
614
|
}
|
|
615
|
+
async commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn) {
|
|
616
|
+
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
617
|
+
return false;
|
|
618
|
+
const bindingResult = this.onSessionIdUpdate
|
|
619
|
+
? ((turn === undefined
|
|
620
|
+
? await this.onSessionIdUpdate(sessionId, threadId)
|
|
621
|
+
: await this.onSessionIdUpdate(sessionId, threadId, { turn }))
|
|
622
|
+
?? 'legacy_updated')
|
|
623
|
+
: 'legacy_updated';
|
|
624
|
+
const bindingAccepted = bindingResult === 'activated'
|
|
625
|
+
|| bindingResult === 'already_active'
|
|
626
|
+
|| bindingResult === 'legacy_updated';
|
|
627
|
+
if (!bindingAccepted) {
|
|
628
|
+
const error = new Error(`backend discovery rejected: ${bindingResult}`);
|
|
629
|
+
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
this.provisionalSessions.delete(sessionId);
|
|
633
|
+
this.activeSessions.set(sessionId, threadId);
|
|
634
|
+
if (carrierToPublish) {
|
|
635
|
+
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
636
|
+
await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
|
|
637
|
+
}
|
|
638
|
+
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
cleanupProvisionalThread(sessionId, threadId) {
|
|
642
|
+
if (this.provisionalSessions.get(sessionId) !== threadId)
|
|
643
|
+
return;
|
|
644
|
+
this.provisionalSessions.delete(sessionId);
|
|
645
|
+
this.threadProjectPaths.delete(threadId);
|
|
646
|
+
this.threadDelegationCarriers.delete(threadId);
|
|
647
|
+
this.unbindExternalToolBoundary(threadId);
|
|
648
|
+
this.clearTrackedApprovalItemsForThread(threadId);
|
|
649
|
+
// A stale provisional start can finish after a replacement run has
|
|
650
|
+
// already activated another parent thread for this session. Do not tear
|
|
651
|
+
// down the replacement run's child mappings in that race.
|
|
652
|
+
const activeParentThread = this.activeSessions.get(sessionId);
|
|
653
|
+
if (!activeParentThread || activeParentThread === threadId) {
|
|
654
|
+
this.cleanupChildThreadsForSession(sessionId);
|
|
655
|
+
}
|
|
656
|
+
this.pruneThreadOperationLocks();
|
|
657
|
+
}
|
|
432
658
|
async persistDelegationCarrier(sessionManager, sessionId, threadId, carrier) {
|
|
433
|
-
if (typeof sessionManager?.getSessionById !== 'function'
|
|
434
|
-
|| typeof sessionManager?.updateSession !== 'function')
|
|
659
|
+
if (typeof sessionManager?.getSessionById !== 'function')
|
|
435
660
|
return;
|
|
436
661
|
try {
|
|
437
662
|
const session = await sessionManager.getSessionById(sessionId);
|
|
@@ -440,12 +665,16 @@ export class CodexRunner {
|
|
|
440
665
|
const current = session.metadata?.[CODEX_DELEGATION_CARRIER_METADATA_KEY];
|
|
441
666
|
if (current?.threadId === threadId && current?.carrier === carrier)
|
|
442
667
|
return;
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
...(session.metadata ?? {}),
|
|
668
|
+
if (typeof sessionManager.patchSessionMetadata === 'function') {
|
|
669
|
+
await sessionManager.patchSessionMetadata(sessionId, {
|
|
446
670
|
[CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier },
|
|
447
|
-
}
|
|
448
|
-
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else if (typeof sessionManager.updateSession === 'function') {
|
|
674
|
+
await sessionManager.updateSession(sessionId, {
|
|
675
|
+
metadata: { [CODEX_DELEGATION_CARRIER_METADATA_KEY]: { threadId, carrier } },
|
|
676
|
+
});
|
|
677
|
+
}
|
|
449
678
|
}
|
|
450
679
|
catch (error) {
|
|
451
680
|
logger.warn(`[CodexRunner] failed to persist delegation carrier: session=${sessionId} thread=${threadId} error=${error}`);
|
|
@@ -456,7 +685,15 @@ export class CodexRunner {
|
|
|
456
685
|
this.appServerClient = null;
|
|
457
686
|
this.threadExternalToolFingerprints.clear();
|
|
458
687
|
this.threadExternalToolBoundaries.clear();
|
|
688
|
+
for (const childThreadId of this.childThreadSessions.keys())
|
|
689
|
+
this.threadProjectPaths.delete(childThreadId);
|
|
690
|
+
// Child turn ids belong to the discarded app-server connection. Keeping
|
|
691
|
+
// them would make a later session interrupt attempt target stale threads
|
|
692
|
+
// through the replacement client.
|
|
693
|
+
this.activeSubagentTurns.clear();
|
|
694
|
+
this.childThreadSessions.clear();
|
|
459
695
|
this.trackedApprovalItems.clear();
|
|
696
|
+
this.pendingToolDenials.clear();
|
|
460
697
|
this.threadDelegationCarriers.clear();
|
|
461
698
|
client?.close().catch(error => {
|
|
462
699
|
logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
|
|
@@ -485,7 +722,7 @@ export class CodexRunner {
|
|
|
485
722
|
return Object.keys(merged).length > 0 ? merged : null;
|
|
486
723
|
}
|
|
487
724
|
async resolveCapabilityThreadConfig(projectPath) {
|
|
488
|
-
const agentConfig = this.resolvedConfig.
|
|
725
|
+
const agentConfig = this.resolvedConfig.agentConfig;
|
|
489
726
|
if (!agentConfig) {
|
|
490
727
|
return {
|
|
491
728
|
skills: { config: [{ name: 'eclink', enabled: false }] },
|
|
@@ -732,6 +969,8 @@ export class CodexRunner {
|
|
|
732
969
|
chatModes = new Map();
|
|
733
970
|
/** 将权限模式映射为 Codex app-server 的 approvalPolicy(纯函数,无副作用,供 per-call 派生用)。 */
|
|
734
971
|
toApprovalPolicy(mode) {
|
|
972
|
+
if (mode === 'fullaccess')
|
|
973
|
+
return 'never';
|
|
735
974
|
// `untrusted` guarantees that commands outside Codex's trusted read-only
|
|
736
975
|
// set reach the EvolCore bridge for EvolCore's per-mode decision.
|
|
737
976
|
void mode;
|
|
@@ -814,6 +1053,12 @@ export class CodexRunner {
|
|
|
814
1053
|
filesystem: {
|
|
815
1054
|
...buildCodexProtectedFilesystemRules(resolveRoot(), {
|
|
816
1055
|
denyLClassRead: mode === 'readonly',
|
|
1056
|
+
// The app-server itself is launched behind EvolCore's Linux
|
|
1057
|
+
// H-class guard, which already mounts every L-class directory
|
|
1058
|
+
// read-only. Avoid Codex's per-file glob expansion inside that
|
|
1059
|
+
// nested mount namespace; it can fail while creating a missing
|
|
1060
|
+
// target such as triggers/history.jsonl.
|
|
1061
|
+
expandLClassReadDeny: process.platform !== 'linux',
|
|
817
1062
|
}),
|
|
818
1063
|
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
819
1064
|
...(bootstrapAgentMdPath ? {
|
|
@@ -821,9 +1066,9 @@ export class CodexRunner {
|
|
|
821
1066
|
} : {}),
|
|
822
1067
|
...(mode !== 'readonly' && managedTempDir ? {
|
|
823
1068
|
[managedTempDir]: 'write',
|
|
824
|
-
[
|
|
1069
|
+
[managedTempDir + '/**']: 'write',
|
|
825
1070
|
[path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
|
|
826
|
-
[path.join(path.dirname(managedTempDir), 'evolcore-locks'
|
|
1071
|
+
[path.join(path.dirname(managedTempDir), 'evolcore-locks') + '/**']: 'write',
|
|
827
1072
|
} : {}),
|
|
828
1073
|
},
|
|
829
1074
|
network,
|
|
@@ -841,14 +1086,17 @@ export class CodexRunner {
|
|
|
841
1086
|
sandbox: decision.state === 'off' ? 'danger-full-access' : undefined,
|
|
842
1087
|
};
|
|
843
1088
|
}
|
|
844
|
-
|
|
1089
|
+
isTrustedFullAccessSession(sessionKey) {
|
|
1090
|
+
return hasTrustedFullAccessContext(this.permissionContexts.get(sessionKey));
|
|
1091
|
+
}
|
|
1092
|
+
buildLifecycleLockdownConfig(permissionMode) {
|
|
845
1093
|
return {
|
|
846
1094
|
features: {
|
|
847
1095
|
// The app-server is launched with allow_managed_hooks_only=true, so
|
|
848
1096
|
// enabling the feature exposes only EvolCore's requirements-managed
|
|
849
1097
|
// PreToolUse hook. Disabling it here suppresses that managed hook too
|
|
850
1098
|
// and bypasses the pause/proactive policy boundary entirely.
|
|
851
|
-
hooks:
|
|
1099
|
+
hooks: permissionMode !== 'fullaccess',
|
|
852
1100
|
},
|
|
853
1101
|
};
|
|
854
1102
|
}
|
|
@@ -867,50 +1115,145 @@ export class CodexRunner {
|
|
|
867
1115
|
}
|
|
868
1116
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
869
1117
|
setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
|
|
1118
|
+
injectUserMessage(sessionId, text) {
|
|
1119
|
+
// Bind queued reminders to the turn that generated them. A completed
|
|
1120
|
+
// turn may be replaced before the serialized steer reaches app-server;
|
|
1121
|
+
// injecting the old reminder into the new turn would corrupt its context.
|
|
1122
|
+
const requestedTurn = this.activeTurns.get(sessionId);
|
|
1123
|
+
if (!requestedTurn) {
|
|
1124
|
+
logger.warn(`[CodexRunner] turn steer skipped because no active turn is registered: session=${sessionId}`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const previous = this.steeringTails.get(sessionId) ?? Promise.resolve();
|
|
1128
|
+
let current;
|
|
1129
|
+
current = previous.catch(() => undefined).then(async () => {
|
|
1130
|
+
const activeTurn = this.activeTurns.get(sessionId);
|
|
1131
|
+
const sameTurn = !!activeTurn
|
|
1132
|
+
&& activeTurn.threadId === requestedTurn.threadId
|
|
1133
|
+
&& activeTurn.turnId === requestedTurn.turnId;
|
|
1134
|
+
if (!sameTurn) {
|
|
1135
|
+
logger.warn(`[CodexRunner] turn steer skipped because the originating turn is no longer active: `
|
|
1136
|
+
+ `session=${sessionId} thread=${requestedTurn.threadId} turn=${requestedTurn.turnId}`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const input = [{ type: 'text', text, text_elements: [] }];
|
|
1140
|
+
const client = this.getAppServerClient();
|
|
1141
|
+
if (typeof client.turnSteer !== 'function') {
|
|
1142
|
+
logger.warn(`[CodexRunner] turn steer unavailable in app-server client: session=${sessionId}`);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
const response = await client.turnSteer(activeTurn.threadId, activeTurn.turnId, input);
|
|
1147
|
+
logger.info(`[CodexRunner] turn steer delivered: session=${sessionId} thread=${activeTurn.threadId} ` +
|
|
1148
|
+
`turn=${response.turnId} chars=${text.length}`);
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
logger.warn(`[CodexRunner] turn steer failed: session=${sessionId} thread=${activeTurn.threadId} ` +
|
|
1152
|
+
`turn=${activeTurn.turnId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1153
|
+
}
|
|
1154
|
+
}).finally(() => {
|
|
1155
|
+
if (this.steeringTails.get(sessionId) === current)
|
|
1156
|
+
this.steeringTails.delete(sessionId);
|
|
1157
|
+
});
|
|
1158
|
+
this.steeringTails.set(sessionId, current);
|
|
1159
|
+
}
|
|
870
1160
|
async assertProactiveHookAvailability(sessionId, appServer) {
|
|
871
1161
|
if (this.permissionContexts.get(sessionId)?.chatmode !== 'proactive')
|
|
872
1162
|
return;
|
|
873
|
-
if (process.platform
|
|
874
|
-
await appServer.assertManagedHooksAvailable();
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
if (process.platform !== 'linux') {
|
|
1163
|
+
if (process.platform !== 'linux' && process.platform !== 'win32') {
|
|
878
1164
|
throw new Error('Codex proactive managed PreToolUse enforcement requires Linux or an installed Windows managed hook');
|
|
879
1165
|
}
|
|
1166
|
+
await appServer.assertManagedHooksAvailable();
|
|
880
1167
|
}
|
|
881
|
-
async evaluatePreToolUse(threadId, toolName, toolInput, signal) {
|
|
1168
|
+
async evaluatePreToolUse(threadId, toolName, toolInput, signal, callId) {
|
|
882
1169
|
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
883
|
-
const activeThread = this.activeSessions.get(sessionKey);
|
|
1170
|
+
const activeThread = this.activeSessions.get(sessionKey) ?? this.provisionalSessions.get(sessionKey);
|
|
884
1171
|
const context = this.permissionContexts.get(sessionKey);
|
|
885
|
-
|
|
1172
|
+
const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
1173
|
+
? toolInput
|
|
1174
|
+
: {};
|
|
1175
|
+
const recordPreToolUseDenial = async (policyCode, reason) => {
|
|
1176
|
+
const summary = summarizeToolInputForAudit(toolName, normalizedInput).slice(0, 512);
|
|
1177
|
+
auditToolPreflightDenial({
|
|
1178
|
+
toolName,
|
|
1179
|
+
policyCode,
|
|
1180
|
+
reason,
|
|
1181
|
+
summary,
|
|
1182
|
+
sessionId: sessionKey,
|
|
1183
|
+
agentAid: context?.selfAid,
|
|
1184
|
+
agentName: context?.agentName,
|
|
1185
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
1186
|
+
channel: context?.channel,
|
|
1187
|
+
actorId: context?.userId,
|
|
1188
|
+
role: context?.role,
|
|
1189
|
+
selfAid: context?.selfAid,
|
|
1190
|
+
callId,
|
|
1191
|
+
correlationId: callId,
|
|
1192
|
+
taskId: context?.taskId,
|
|
1193
|
+
});
|
|
1194
|
+
try {
|
|
1195
|
+
await context?.recordExecutionAnomaly?.({
|
|
1196
|
+
code: 'operation_blocked',
|
|
1197
|
+
severity: 'warning',
|
|
1198
|
+
phase: 'execution',
|
|
1199
|
+
occurredAt: Date.now(),
|
|
1200
|
+
toolName,
|
|
1201
|
+
policyCode,
|
|
1202
|
+
decisionSource: 'policy',
|
|
1203
|
+
agentAid: context?.selfAid,
|
|
1204
|
+
agentName: context?.agentName,
|
|
1205
|
+
sessionId: sessionKey,
|
|
1206
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
1207
|
+
summary,
|
|
1208
|
+
effect: 'operation_skipped',
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
logger.warn(`[CodexRunner] failed to record managed-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
if (!activeThread || !this.isThreadOwnedBySession(sessionKey, threadId)) {
|
|
886
1216
|
return { ok: true, applicable: false };
|
|
887
1217
|
}
|
|
888
1218
|
const proactiveSession = context?.chatmode === 'proactive';
|
|
889
1219
|
if (!context || (proactiveSession && !context.preToolUsePolicyHook)) {
|
|
890
|
-
|
|
1220
|
+
const reason = 'EvolCore proactive session policy context is unavailable';
|
|
1221
|
+
await recordPreToolUseDenial('pre_tool_use_context_unavailable', reason);
|
|
1222
|
+
return { ok: false, applicable: true, decision: 'deny', reason };
|
|
891
1223
|
}
|
|
892
1224
|
if (context.pauseController) {
|
|
893
1225
|
const pauseResult = await context.pauseController.waitAtToolBoundary(signal);
|
|
894
1226
|
if (pauseResult === 'cancelled') {
|
|
895
|
-
|
|
1227
|
+
const reason = '工具调用已取消';
|
|
1228
|
+
await recordPreToolUseDenial('tool_call_cancelled', reason);
|
|
1229
|
+
return { ok: true, applicable: true, decision: 'deny', reason };
|
|
896
1230
|
}
|
|
897
1231
|
}
|
|
1232
|
+
if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
|
|
1233
|
+
if (this.isTrustedFullAccessSession(sessionKey))
|
|
1234
|
+
return { ok: true, applicable: true, decision: 'allow' };
|
|
1235
|
+
const reason = 'Untrusted fullaccess execution context';
|
|
1236
|
+
await recordPreToolUseDenial('fullaccess_context_untrusted', reason);
|
|
1237
|
+
return { ok: false, applicable: true, decision: 'deny', reason };
|
|
1238
|
+
}
|
|
898
1239
|
// A tracked interactive thread still needs an authenticated, explicit
|
|
899
1240
|
// allow so the managed hook can fail closed when no runner matches.
|
|
900
1241
|
if (!context.preToolUsePolicyHook)
|
|
901
1242
|
return { ok: true, applicable: true, decision: 'allow' };
|
|
902
|
-
const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
903
|
-
? toolInput
|
|
904
|
-
: {};
|
|
905
1243
|
try {
|
|
906
1244
|
const result = context.preToolUsePolicyHook(toolName, normalizedInput);
|
|
907
1245
|
if (result?.block) {
|
|
908
|
-
|
|
1246
|
+
const policyCode = result.policyCode ?? 'session_policy_hook';
|
|
1247
|
+
const reason = result.reason ?? 'session policy denied this tool call';
|
|
1248
|
+
await recordPreToolUseDenial(policyCode, reason);
|
|
1249
|
+
return { ok: true, applicable: true, decision: 'deny', reason };
|
|
909
1250
|
}
|
|
910
1251
|
return { ok: true, applicable: true, decision: 'allow' };
|
|
911
1252
|
}
|
|
912
1253
|
catch (error) {
|
|
913
|
-
|
|
1254
|
+
const reason = `policy hook failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1255
|
+
await recordPreToolUseDenial('pre_tool_use_hook_failed', reason);
|
|
1256
|
+
return { ok: false, applicable: true, decision: 'deny', reason };
|
|
914
1257
|
}
|
|
915
1258
|
}
|
|
916
1259
|
setPermissionGateway(gw) { this.permissionGateway = gw; }
|
|
@@ -919,9 +1262,19 @@ export class CodexRunner {
|
|
|
919
1262
|
this.activeStreams.set(key, stream);
|
|
920
1263
|
}
|
|
921
1264
|
cleanupStream(key) {
|
|
1265
|
+
const hadController = this.activeAbortControllers.has(key);
|
|
1266
|
+
const hadTurn = this.activeTurns.has(key);
|
|
1267
|
+
const provisionalThread = this.provisionalSessions.get(key);
|
|
922
1268
|
this.activeStreams.delete(key);
|
|
923
1269
|
this.activeAbortControllers.delete(key);
|
|
924
1270
|
this.pendingInterrupts.delete(key);
|
|
1271
|
+
// A stale runner-start path may dispose the stream without ever entering
|
|
1272
|
+
// transformAppServerStream. Reclaim its provisional thread, but preserve
|
|
1273
|
+
// a replacement run that has already installed a new controller/turn.
|
|
1274
|
+
if (provisionalThread && !hadController && !hadTurn) {
|
|
1275
|
+
this.cleanupProvisionalThread(key, provisionalThread);
|
|
1276
|
+
}
|
|
1277
|
+
this.chatModes.delete(key);
|
|
925
1278
|
const release = this.activeOperationReleases.get(key);
|
|
926
1279
|
if (release) {
|
|
927
1280
|
this.activeOperationReleases.delete(key);
|
|
@@ -976,14 +1329,20 @@ export class CodexRunner {
|
|
|
976
1329
|
}
|
|
977
1330
|
}
|
|
978
1331
|
isActiveThreadOperationAlias(key) {
|
|
979
|
-
if (key.startsWith('session:'))
|
|
980
|
-
|
|
1332
|
+
if (key.startsWith('session:')) {
|
|
1333
|
+
const sessionId = key.slice('session:'.length);
|
|
1334
|
+
return this.activeSessions.has(sessionId) || this.provisionalSessions.has(sessionId);
|
|
1335
|
+
}
|
|
981
1336
|
if (key.startsWith('thread:')) {
|
|
982
1337
|
const threadId = key.slice('thread:'.length);
|
|
983
1338
|
for (const activeThreadId of this.activeSessions.values()) {
|
|
984
1339
|
if (activeThreadId === threadId)
|
|
985
1340
|
return true;
|
|
986
1341
|
}
|
|
1342
|
+
for (const provisionalThreadId of this.provisionalSessions.values()) {
|
|
1343
|
+
if (provisionalThreadId === threadId)
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
987
1346
|
return false;
|
|
988
1347
|
}
|
|
989
1348
|
return true;
|
|
@@ -1032,8 +1391,22 @@ export class CodexRunner {
|
|
|
1032
1391
|
}
|
|
1033
1392
|
}
|
|
1034
1393
|
// ── Core: runQuery ──
|
|
1394
|
+
cachedThreadForTopic(sessionId, agentSessionId) {
|
|
1395
|
+
if (!agentSessionId)
|
|
1396
|
+
return undefined;
|
|
1397
|
+
return this.activeSessions.get(sessionId) === agentSessionId ? agentSessionId : undefined;
|
|
1398
|
+
}
|
|
1035
1399
|
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
1036
|
-
const
|
|
1400
|
+
const topicBinding = modelOverride?.backend?.kind === 'topic';
|
|
1401
|
+
if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
|
|
1402
|
+
const error = new Error('topic backend run requires an active TurnLease and binding callback');
|
|
1403
|
+
error.code = 'TOPIC_TURN_LEASE_REQUIRED';
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
const topicAgentSessionId = topicBinding ? modelOverride?.backend?.agentSessionId ?? null : undefined;
|
|
1407
|
+
const knownThreadId = topicBinding
|
|
1408
|
+
? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
|
|
1409
|
+
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1037
1410
|
const operationKey = this.threadOperationKey(knownThreadId, sessionId);
|
|
1038
1411
|
const release = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start');
|
|
1039
1412
|
try {
|
|
@@ -1047,8 +1420,26 @@ export class CodexRunner {
|
|
|
1047
1420
|
}
|
|
1048
1421
|
}
|
|
1049
1422
|
async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1423
|
+
// A daemon-managed task must never fall back to the app-server's
|
|
1424
|
+
// process-wide TMPDIR if its session namespace disappeared.
|
|
1425
|
+
getManagedTaskTempDir(runtimeEnv);
|
|
1426
|
+
const topicBinding = modelOverride?.backend?.kind === 'topic';
|
|
1427
|
+
const topicAgentSessionId = topicBinding ? modelOverride?.backend?.agentSessionId ?? null : undefined;
|
|
1428
|
+
let agentSessionId = topicBinding
|
|
1429
|
+
? (topicAgentSessionId || undefined)
|
|
1430
|
+
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1431
|
+
if (!topicBinding && !initialAgentSessionId && typeof sessionManager?.getSessionById === 'function') {
|
|
1432
|
+
try {
|
|
1433
|
+
const persisted = await sessionManager.getSessionById(sessionId);
|
|
1434
|
+
agentSessionId = typeof persisted?.agentSessionId === 'string' && persisted.agentSessionId.trim()
|
|
1435
|
+
? persisted.agentSessionId.trim()
|
|
1436
|
+
: undefined;
|
|
1437
|
+
}
|
|
1438
|
+
catch (error) {
|
|
1439
|
+
logger.debug(`[CodexRunner] persisted main backend lookup failed; using local cache: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
let resumingThread = !!agentSessionId;
|
|
1052
1443
|
const callModel = modelOverride?.model || this.model;
|
|
1053
1444
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
1054
1445
|
? undefined
|
|
@@ -1057,8 +1448,10 @@ export class CodexRunner {
|
|
|
1057
1448
|
// 写入 chatModes 供异步审批回调按 sessionKey 读取,并据此派生本次 thread 的 approvalPolicy/sandbox,
|
|
1058
1449
|
// 不依赖共享的 this.approvalPolicy/this.sandboxMode(多会话并发互不污染)。
|
|
1059
1450
|
const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1451
|
+
const callMode = normalizeExecutionPermissionMode(requestedPermissionMode);
|
|
1452
|
+
if (callMode === 'fullaccess' && !hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
|
|
1453
|
+
throw new Error('Codex fullaccess requires a trusted per-call execution authorization');
|
|
1454
|
+
}
|
|
1062
1455
|
this.chatModes.set(sessionId, callMode);
|
|
1063
1456
|
const callApprovalPolicy = this.toApprovalPolicy(callMode);
|
|
1064
1457
|
const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath);
|
|
@@ -1066,8 +1459,11 @@ export class CodexRunner {
|
|
|
1066
1459
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1067
1460
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
1068
1461
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
1069
|
-
const externalToolConfig =
|
|
1070
|
-
|
|
1462
|
+
const externalToolConfig = callMode === 'fullaccess'
|
|
1463
|
+
? {}
|
|
1464
|
+
: await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
|
|
1465
|
+
if (callMode !== 'fullaccess')
|
|
1466
|
+
await this.assertProactiveHookAvailability(sessionId, appServer);
|
|
1071
1467
|
const managedTempInstruction = this.managedTempInstruction(sessionId);
|
|
1072
1468
|
const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
|
|
1073
1469
|
let knownDelegationCarrier = agentSessionId
|
|
@@ -1075,35 +1471,80 @@ export class CodexRunner {
|
|
|
1075
1471
|
: undefined;
|
|
1076
1472
|
if (agentSessionId && !knownDelegationCarrier) {
|
|
1077
1473
|
knownDelegationCarrier = await this.loadPersistedDelegationCarrier(sessionManager, sessionId, agentSessionId);
|
|
1078
|
-
if (knownDelegationCarrier) {
|
|
1079
|
-
this.threadDelegationCarriers.set(agentSessionId, knownDelegationCarrier);
|
|
1080
|
-
}
|
|
1081
1474
|
}
|
|
1082
|
-
|
|
1475
|
+
let delegationCarrierCandidate = knownDelegationCarrier ?? this.createDelegationCarrier();
|
|
1476
|
+
// Keep newly discovered carrier state local until generation-aware
|
|
1477
|
+
// activation accepts the backend. A stale run must not publish a carrier
|
|
1478
|
+
// that a later generation could mistake for its own thread.
|
|
1479
|
+
let carrierToPublish;
|
|
1083
1480
|
const hasTaskDelegation = typeof runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] === 'string';
|
|
1084
|
-
|
|
1481
|
+
let threadOptions = {
|
|
1085
1482
|
model: callModel,
|
|
1086
1483
|
effort: callEffort,
|
|
1087
1484
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1088
1485
|
approvalsReviewer: 'user',
|
|
1089
1486
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1090
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig()), callMode),
|
|
1487
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1091
1488
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1092
1489
|
};
|
|
1093
1490
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
1094
1491
|
`role=${executionSandbox.decision.role ?? 'none'} sandbox=${executionSandbox.decision.state}`);
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1492
|
+
let threadResponse;
|
|
1493
|
+
if (agentSessionId) {
|
|
1494
|
+
try {
|
|
1495
|
+
threadResponse = await appServer.threadResume(agentSessionId, projectPath, threadOptions);
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
// A legacy main session can retain a provider ID after its rollout was
|
|
1499
|
+
// removed externally. It is safe to recover before input submission;
|
|
1500
|
+
// topic bindings remain authoritative and fail closed.
|
|
1501
|
+
if (topicBinding || !isCodexNoRolloutError(error))
|
|
1502
|
+
throw error;
|
|
1503
|
+
if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
|
|
1504
|
+
throw error;
|
|
1505
|
+
const missingThreadId = agentSessionId;
|
|
1506
|
+
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId, [CODEX_DELEGATION_CARRIER_METADATA_KEY]);
|
|
1507
|
+
if (cleared !== 'cleared' && cleared !== 'already_clear') {
|
|
1508
|
+
const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
|
|
1509
|
+
recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
|
|
1510
|
+
throw recoveryError;
|
|
1511
|
+
}
|
|
1512
|
+
if (this.activeSessions.get(sessionId) === missingThreadId)
|
|
1513
|
+
this.activeSessions.delete(sessionId);
|
|
1514
|
+
this.threadProjectPaths.delete(missingThreadId);
|
|
1515
|
+
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1516
|
+
this.unbindExternalToolBoundary(missingThreadId);
|
|
1517
|
+
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1518
|
+
agentSessionId = undefined;
|
|
1519
|
+
resumingThread = false;
|
|
1520
|
+
knownDelegationCarrier = undefined;
|
|
1521
|
+
delegationCarrierCandidate = this.createDelegationCarrier();
|
|
1522
|
+
threadOptions = {
|
|
1523
|
+
...threadOptions,
|
|
1524
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1525
|
+
};
|
|
1526
|
+
logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
|
|
1527
|
+
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
1532
|
+
}
|
|
1098
1533
|
let threadId = threadResponse.thread?.id || agentSessionId;
|
|
1099
1534
|
if (!threadId)
|
|
1100
1535
|
throw new Error('Codex app-server did not return a thread id');
|
|
1101
1536
|
if (!resumingThread) {
|
|
1102
|
-
|
|
1103
|
-
await this.persistDelegationCarrier(sessionManager, sessionId, threadId, delegationCarrierCandidate);
|
|
1537
|
+
carrierToPublish = { threadId, carrier: delegationCarrierCandidate };
|
|
1104
1538
|
}
|
|
1105
1539
|
else if (hasTaskDelegation && !knownDelegationCarrier) {
|
|
1106
|
-
|
|
1540
|
+
// A BOUND topic cannot silently fork to migrate carrier metadata: its
|
|
1541
|
+
// active-turn activation is allowed to confirm only the authoritative
|
|
1542
|
+
// backend ID, so a fork would deterministically conflict. threadResume
|
|
1543
|
+
// already received the candidate in this turn's environment config;
|
|
1544
|
+
// publish it for the same thread after binding confirmation instead.
|
|
1545
|
+
let recoveredCarrier = topicBinding
|
|
1546
|
+
? delegationCarrierCandidate
|
|
1547
|
+
: await this.readThreadDelegationCarrier(appServer, threadId);
|
|
1107
1548
|
if (!recoveredCarrier) {
|
|
1108
1549
|
const staleThreadId = threadId;
|
|
1109
1550
|
logger.warn(`[CodexRunner] loaded thread has no recoverable delegation carrier; forking once to preserve history: thread=${staleThreadId}`);
|
|
@@ -1121,19 +1562,46 @@ export class CodexRunner {
|
|
|
1121
1562
|
threadId = forkedThreadId;
|
|
1122
1563
|
recoveredCarrier = delegationCarrierCandidate;
|
|
1123
1564
|
}
|
|
1124
|
-
|
|
1125
|
-
await this.persistDelegationCarrier(sessionManager, sessionId, threadId, recoveredCarrier);
|
|
1565
|
+
carrierToPublish = { threadId, carrier: recoveredCarrier };
|
|
1126
1566
|
}
|
|
1127
1567
|
else if (knownDelegationCarrier) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1568
|
+
carrierToPublish = { threadId, carrier: knownDelegationCarrier };
|
|
1569
|
+
}
|
|
1570
|
+
const turn = modelOverride?.turn;
|
|
1571
|
+
if (!resumingThread) {
|
|
1572
|
+
// Keep a newly-created backend private to this runner until turn/start
|
|
1573
|
+
// has been accepted. In particular, a pending interrupt must not leave
|
|
1574
|
+
// a durable ID for a thread that never produced a rollout.
|
|
1575
|
+
this.provisionalSessions.set(sessionId, threadId);
|
|
1576
|
+
if (carrierToPublish) {
|
|
1577
|
+
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
agentSessionId = threadId;
|
|
1582
|
+
const bindingResult = this.onSessionIdUpdate
|
|
1583
|
+
? ((turn === undefined
|
|
1584
|
+
? await this.onSessionIdUpdate(sessionId, threadId)
|
|
1585
|
+
: await this.onSessionIdUpdate(sessionId, threadId, { turn }))
|
|
1586
|
+
?? 'legacy_updated')
|
|
1587
|
+
: 'legacy_updated';
|
|
1588
|
+
const bindingAccepted = bindingResult === 'activated'
|
|
1589
|
+
|| bindingResult === 'already_active'
|
|
1590
|
+
|| bindingResult === 'legacy_updated';
|
|
1591
|
+
if (!bindingAccepted) {
|
|
1592
|
+
const error = new Error(`backend discovery rejected: ${bindingResult}`);
|
|
1593
|
+
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
this.activeSessions.set(sessionId, threadId);
|
|
1597
|
+
if (carrierToPublish) {
|
|
1598
|
+
this.threadDelegationCarriers.set(carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1599
|
+
await this.persistDelegationCarrier(sessionManager, sessionId, carrierToPublish.threadId, carrierToPublish.carrier);
|
|
1600
|
+
}
|
|
1601
|
+
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
1130
1602
|
}
|
|
1131
|
-
agentSessionId = threadId;
|
|
1132
|
-
this.activeSessions.set(sessionId, threadId);
|
|
1133
|
-
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
1134
1603
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
1135
1604
|
this.bindExternalToolBoundary(threadId, externalToolConfig);
|
|
1136
|
-
this.onSessionIdUpdate?.(sessionId, threadId);
|
|
1137
1605
|
const controller = new AbortController();
|
|
1138
1606
|
this.activeAbortControllers.set(sessionId, controller);
|
|
1139
1607
|
const tempFiles = [];
|
|
@@ -1142,21 +1610,55 @@ export class CodexRunner {
|
|
|
1142
1610
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
1143
1611
|
const state = {
|
|
1144
1612
|
threadId,
|
|
1613
|
+
sessionId,
|
|
1614
|
+
projectPath,
|
|
1145
1615
|
model: callModel,
|
|
1616
|
+
committed: resumingThread,
|
|
1146
1617
|
streamedAgentMessageIds: new Set(),
|
|
1147
1618
|
agentMessageDeltaText: new Map(),
|
|
1148
1619
|
completedItemIds: new Set(),
|
|
1149
1620
|
emittedEditCallIds: new Set(),
|
|
1150
1621
|
completedTurnIds: new Set(),
|
|
1151
1622
|
openToolCalls: new Map(),
|
|
1623
|
+
activeSubagentThreads: new Set(),
|
|
1624
|
+
turnCompleted: false,
|
|
1152
1625
|
};
|
|
1153
1626
|
const unsubscribe = appServer.onNotification(notification => {
|
|
1154
1627
|
// 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
|
|
1155
1628
|
// (如 thread/tokenUsage/updated)先于新 turn 到达,不能用它们 latch turnId
|
|
1156
1629
|
const params = notification.params || {};
|
|
1157
1630
|
const notifThreadId = params.threadId ?? params.thread_id;
|
|
1158
|
-
|
|
1631
|
+
const item = params.item && typeof params.item === 'object' ? params.item : undefined;
|
|
1632
|
+
const emittedByChild = typeof notifThreadId === 'string'
|
|
1633
|
+
&& this.childThreadSessions.get(notifThreadId) === sessionId;
|
|
1634
|
+
if (notifThreadId !== undefined && notifThreadId !== threadId && !emittedByChild)
|
|
1159
1635
|
return;
|
|
1636
|
+
this.trackSubagentLifecycle(notification, state);
|
|
1637
|
+
if (emittedByChild && notifThreadId !== threadId) {
|
|
1638
|
+
const childTurnId = this.extractTurnId(notification);
|
|
1639
|
+
if (notification.method === 'turn/started' && childTurnId) {
|
|
1640
|
+
this.activeSubagentTurns.set(notifThreadId, {
|
|
1641
|
+
sessionId,
|
|
1642
|
+
parentThreadId: threadId,
|
|
1643
|
+
turnId: childTurnId,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
else if (notification.method === 'turn/completed') {
|
|
1647
|
+
const activeChildTurn = this.activeSubagentTurns.get(notifThreadId);
|
|
1648
|
+
if (!childTurnId || activeChildTurn?.turnId === childTurnId) {
|
|
1649
|
+
this.activeSubagentTurns.delete(notifThreadId);
|
|
1650
|
+
}
|
|
1651
|
+
state.activeSubagentThreads.delete(notifThreadId);
|
|
1652
|
+
this.unregisterChildThread(notifThreadId);
|
|
1653
|
+
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1654
|
+
}
|
|
1655
|
+
if (this.isSubagentActivityItem(item))
|
|
1656
|
+
queue.push(notification);
|
|
1657
|
+
if (notification.method === 'item/completed' && state.turnCompleted) {
|
|
1658
|
+
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1659
|
+
}
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1160
1662
|
if (notification.method === 'turn/started') {
|
|
1161
1663
|
const startedTurnId = this.extractTurnId(notification);
|
|
1162
1664
|
if (startedTurnId && !state.turnId) {
|
|
@@ -1172,15 +1674,22 @@ export class CodexRunner {
|
|
|
1172
1674
|
return;
|
|
1173
1675
|
queue.push(notification);
|
|
1174
1676
|
// 仅在已锁定 turnId 后才允许 turn/completed 结束队列,避免残留的旧 turn/completed 误关
|
|
1175
|
-
if (notification.method === 'turn/completed' && state.turnId)
|
|
1176
|
-
|
|
1677
|
+
if (notification.method === 'turn/completed' && state.turnId) {
|
|
1678
|
+
state.turnCompleted = true;
|
|
1679
|
+
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1680
|
+
}
|
|
1681
|
+
else if (notification.method === 'item/completed' && state.turnCompleted) {
|
|
1682
|
+
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1683
|
+
}
|
|
1177
1684
|
});
|
|
1178
1685
|
if (this.consumePendingInterrupt(sessionId)) {
|
|
1179
1686
|
controller.abort('User interrupt');
|
|
1180
1687
|
this.activeAbortControllers.delete(sessionId);
|
|
1181
1688
|
this.activeStreams.delete(sessionId);
|
|
1689
|
+
if (!resumingThread)
|
|
1690
|
+
this.cleanupProvisionalThread(sessionId, threadId);
|
|
1182
1691
|
logger.info(`[CodexRunner] Applied pending interrupt before turn start: ${sessionId}`);
|
|
1183
|
-
return this.transformAppServerStream(queue, sessionId, state, unsubscribe, tempFiles);
|
|
1692
|
+
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, tempFiles);
|
|
1184
1693
|
}
|
|
1185
1694
|
try {
|
|
1186
1695
|
const turnResponse = await appServer.turnStart(threadId, input, {
|
|
@@ -1190,6 +1699,14 @@ export class CodexRunner {
|
|
|
1190
1699
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1191
1700
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1192
1701
|
});
|
|
1702
|
+
if (!resumingThread) {
|
|
1703
|
+
state.commitProvisional = async () => {
|
|
1704
|
+
const committed = await this.commitProvisionalThread(sessionId, threadId, carrierToPublish, sessionManager, turn);
|
|
1705
|
+
if (committed)
|
|
1706
|
+
agentSessionId = threadId;
|
|
1707
|
+
return committed;
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1193
1710
|
const turnId = turnResponse.turn?.id;
|
|
1194
1711
|
if (turnId && !state.turnId) {
|
|
1195
1712
|
state.turnId = turnId;
|
|
@@ -1204,8 +1721,10 @@ export class CodexRunner {
|
|
|
1204
1721
|
}
|
|
1205
1722
|
const status = turnResponse.turn?.status;
|
|
1206
1723
|
if (status === 'completed' || status === 'failed') {
|
|
1724
|
+
this.primeSubagentStateFromTurn(turnResponse.turn, state);
|
|
1207
1725
|
queue.push({ method: 'turn/completed', params: { threadId, turn: turnResponse.turn } });
|
|
1208
|
-
|
|
1726
|
+
state.turnCompleted = true;
|
|
1727
|
+
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1209
1728
|
}
|
|
1210
1729
|
}
|
|
1211
1730
|
catch (error) {
|
|
@@ -1214,9 +1733,11 @@ export class CodexRunner {
|
|
|
1214
1733
|
this.activeTurns.delete(sessionId);
|
|
1215
1734
|
this.pendingInterrupts.delete(sessionId);
|
|
1216
1735
|
this.cleanupTempFiles(tempFiles);
|
|
1736
|
+
if (!resumingThread)
|
|
1737
|
+
this.cleanupProvisionalThread(sessionId, threadId);
|
|
1217
1738
|
throw error;
|
|
1218
1739
|
}
|
|
1219
|
-
return this.transformAppServerStream(queue, sessionId, state, unsubscribe, tempFiles);
|
|
1740
|
+
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, tempFiles);
|
|
1220
1741
|
}
|
|
1221
1742
|
// ── Interrupt ──
|
|
1222
1743
|
async interrupt(sessionKey) {
|
|
@@ -1227,6 +1748,7 @@ export class CodexRunner {
|
|
|
1227
1748
|
const interruptTurn = activeTurn
|
|
1228
1749
|
? this.interruptAppServerTurn(activeTurn.threadId, activeTurn.turnId)
|
|
1229
1750
|
: Promise.resolve();
|
|
1751
|
+
const interruptSubagents = this.interruptSubagentTurns(sessionKey);
|
|
1230
1752
|
if (!activeTurn)
|
|
1231
1753
|
this.rememberPendingInterrupt(sessionKey);
|
|
1232
1754
|
if (controller)
|
|
@@ -1237,7 +1759,17 @@ export class CodexRunner {
|
|
|
1237
1759
|
this.activeTurns.delete(sessionKey);
|
|
1238
1760
|
logger.info(`[CodexRunner] Interrupted session: ${sessionKey}`);
|
|
1239
1761
|
}
|
|
1240
|
-
await interruptTurn;
|
|
1762
|
+
await Promise.all([interruptTurn, interruptSubagents]);
|
|
1763
|
+
}
|
|
1764
|
+
async interruptSubagentTurns(sessionId, parentThreadId) {
|
|
1765
|
+
const entries = [...this.activeSubagentTurns.entries()]
|
|
1766
|
+
.filter(([, active]) => active.sessionId === sessionId && (!parentThreadId || active.parentThreadId === parentThreadId));
|
|
1767
|
+
await Promise.all(entries.map(async ([childThreadId, active]) => {
|
|
1768
|
+
await this.interruptAppServerTurn(childThreadId, active.turnId).catch(() => { });
|
|
1769
|
+
const current = this.activeSubagentTurns.get(childThreadId);
|
|
1770
|
+
if (current === active)
|
|
1771
|
+
this.activeSubagentTurns.delete(childThreadId);
|
|
1772
|
+
}));
|
|
1241
1773
|
}
|
|
1242
1774
|
rememberPendingInterrupt(sessionId) {
|
|
1243
1775
|
this.pendingInterrupts.set(sessionId, Date.now());
|
|
@@ -1266,40 +1798,96 @@ export class CodexRunner {
|
|
|
1266
1798
|
// ── Session commands ──
|
|
1267
1799
|
updateSessionId(sessionId, agentSessionId) {
|
|
1268
1800
|
const previousThreadId = this.activeSessions.get(sessionId);
|
|
1269
|
-
|
|
1270
|
-
this.
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1801
|
+
const applyLocal = () => {
|
|
1802
|
+
const currentThreadId = this.activeSessions.get(sessionId);
|
|
1803
|
+
if (currentThreadId !== previousThreadId && !(previousThreadId === undefined && currentThreadId === undefined))
|
|
1804
|
+
return;
|
|
1805
|
+
if (previousThreadId && previousThreadId !== agentSessionId) {
|
|
1806
|
+
this.threadProjectPaths.delete(previousThreadId);
|
|
1807
|
+
this.threadDelegationCarriers.delete(previousThreadId);
|
|
1808
|
+
this.unbindExternalToolBoundary(previousThreadId);
|
|
1809
|
+
this.clearTrackedApprovalItemsForThread(previousThreadId);
|
|
1810
|
+
}
|
|
1811
|
+
if (agentSessionId)
|
|
1812
|
+
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${agentSessionId}`);
|
|
1813
|
+
this.pruneThreadOperationLocks();
|
|
1814
|
+
if (agentSessionId)
|
|
1815
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
1816
|
+
else
|
|
1817
|
+
this.activeSessions.delete(sessionId);
|
|
1818
|
+
};
|
|
1819
|
+
if (!this.onSessionIdUpdate) {
|
|
1820
|
+
applyLocal();
|
|
1821
|
+
return;
|
|
1281
1822
|
}
|
|
1282
|
-
this.
|
|
1283
|
-
|
|
1823
|
+
void this.onSessionIdUpdate(sessionId, agentSessionId).then(result => {
|
|
1824
|
+
if (result === 'activated' || result === 'already_active' || result === 'legacy_updated' || !agentSessionId) {
|
|
1825
|
+
applyLocal();
|
|
1826
|
+
}
|
|
1827
|
+
}).catch(error => {
|
|
1828
|
+
logger.warn(`[CodexRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1829
|
+
if (!agentSessionId)
|
|
1830
|
+
applyLocal();
|
|
1831
|
+
});
|
|
1284
1832
|
}
|
|
1285
1833
|
async closeSession(sessionId) {
|
|
1834
|
+
const capturedThreadId = this.activeSessions.get(sessionId);
|
|
1835
|
+
const capturedProvisionalThreadId = this.provisionalSessions.get(sessionId);
|
|
1836
|
+
const capturedPermissionContext = this.permissionContexts.get(sessionId);
|
|
1837
|
+
const capturedStream = this.activeStreams.get(sessionId);
|
|
1838
|
+
const capturedController = this.activeAbortControllers.get(sessionId);
|
|
1839
|
+
const capturedTurn = this.activeTurns.get(sessionId);
|
|
1840
|
+
const capturedSteeringTail = this.steeringTails.get(sessionId);
|
|
1286
1841
|
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
this.
|
|
1299
|
-
|
|
1842
|
+
// Do not call interrupt(sessionId) here: a new run may have replaced the
|
|
1843
|
+
// session-scoped maps while an older close was waiting. Interrupt and
|
|
1844
|
+
// clean only the exact lifecycle captured at entry.
|
|
1845
|
+
if (capturedTurn) {
|
|
1846
|
+
await this.interruptAppServerTurn(capturedTurn.threadId, capturedTurn.turnId).catch(() => { });
|
|
1847
|
+
}
|
|
1848
|
+
const capturedParentThreadId = capturedThreadId ?? capturedProvisionalThreadId;
|
|
1849
|
+
if (capturedParentThreadId) {
|
|
1850
|
+
await this.interruptSubagentTurns(sessionId, capturedParentThreadId);
|
|
1851
|
+
}
|
|
1852
|
+
else if (!this.activeSessions.has(sessionId) && !this.provisionalSessions.has(sessionId)) {
|
|
1853
|
+
await this.interruptSubagentTurns(sessionId);
|
|
1854
|
+
}
|
|
1855
|
+
capturedController?.abort('Session closed');
|
|
1856
|
+
const lifecycleReplaced = (capturedStream !== undefined && this.activeStreams.get(sessionId) !== capturedStream)
|
|
1857
|
+
|| (capturedController !== undefined && this.activeAbortControllers.get(sessionId) !== capturedController)
|
|
1858
|
+
|| (capturedTurn !== undefined && this.activeTurns.get(sessionId) !== capturedTurn)
|
|
1859
|
+
|| (capturedPermissionContext !== undefined && this.permissionContexts.get(sessionId) !== capturedPermissionContext)
|
|
1860
|
+
|| (capturedThreadId !== undefined && this.activeSessions.get(sessionId) !== capturedThreadId);
|
|
1861
|
+
if (capturedStream !== undefined && this.activeStreams.get(sessionId) === capturedStream)
|
|
1862
|
+
this.activeStreams.delete(sessionId);
|
|
1863
|
+
if (capturedController !== undefined && this.activeAbortControllers.get(sessionId) === capturedController)
|
|
1864
|
+
this.activeAbortControllers.delete(sessionId);
|
|
1865
|
+
if (capturedTurn !== undefined && this.activeTurns.get(sessionId) === capturedTurn)
|
|
1866
|
+
this.activeTurns.delete(sessionId);
|
|
1867
|
+
if (this.pendingInterrupts.has(sessionId) && !lifecycleReplaced)
|
|
1868
|
+
this.pendingInterrupts.delete(sessionId);
|
|
1869
|
+
if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
|
|
1870
|
+
this.permissionContexts.delete(sessionId);
|
|
1871
|
+
if (this.steeringTails.get(sessionId) === capturedSteeringTail)
|
|
1872
|
+
this.steeringTails.delete(sessionId);
|
|
1873
|
+
if (capturedProvisionalThreadId)
|
|
1874
|
+
this.cleanupProvisionalThread(sessionId, capturedProvisionalThreadId);
|
|
1875
|
+
// Session-level configuration and thread indexes are shared by later
|
|
1876
|
+
// runs. Only remove them when the captured lifecycle still owns the key.
|
|
1877
|
+
if (!lifecycleReplaced) {
|
|
1878
|
+
if (this.activeSessions.get(sessionId) === capturedThreadId)
|
|
1879
|
+
this.activeSessions.delete(sessionId);
|
|
1880
|
+
this.chatModes.delete(sessionId);
|
|
1881
|
+
this.cleanupSessionTempDir(sessionId);
|
|
1882
|
+
if (capturedThreadId) {
|
|
1883
|
+
this.threadProjectPaths.delete(capturedThreadId);
|
|
1884
|
+
this.threadDelegationCarriers.delete(capturedThreadId);
|
|
1885
|
+
this.unbindExternalToolBoundary(capturedThreadId);
|
|
1886
|
+
}
|
|
1887
|
+
this.clearTrackedApprovalItemsForThread(capturedThreadId);
|
|
1888
|
+
this.cleanupChildThreadsForSession(sessionId);
|
|
1889
|
+
this.pruneThreadOperationLocks();
|
|
1300
1890
|
}
|
|
1301
|
-
this.clearTrackedApprovalItemsForThread(threadId);
|
|
1302
|
-
this.pruneThreadOperationLocks();
|
|
1303
1891
|
}
|
|
1304
1892
|
resolveSessionFile(agentSessionId, _projectPath) {
|
|
1305
1893
|
// Codex session 文件: ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
|
|
@@ -1326,6 +1914,8 @@ export class CodexRunner {
|
|
|
1326
1914
|
// Codex: 清空会话 = 下次 runQuery 不传 resumeId,自动创建新 thread
|
|
1327
1915
|
const threadId = this.activeSessions.get(sessionId) ?? _agentSessionId;
|
|
1328
1916
|
this.activeSessions.delete(sessionId);
|
|
1917
|
+
this.activeTurns.delete(sessionId);
|
|
1918
|
+
this.steeringTails.delete(sessionId);
|
|
1329
1919
|
this.chatModes.delete(sessionId);
|
|
1330
1920
|
this.cleanupSessionTempDir(sessionId);
|
|
1331
1921
|
this.threadProjectPaths.delete(threadId);
|
|
@@ -1333,54 +1923,67 @@ export class CodexRunner {
|
|
|
1333
1923
|
this.unbindExternalToolBoundary(threadId);
|
|
1334
1924
|
this.clearTrackedApprovalItemsForThread(threadId);
|
|
1335
1925
|
this.pruneThreadOperationLocks();
|
|
1336
|
-
this.onSessionIdUpdate?.(sessionId, '');
|
|
1926
|
+
await this.onSessionIdUpdate?.(sessionId, '');
|
|
1337
1927
|
return true;
|
|
1338
1928
|
}
|
|
1339
|
-
async compactSession(_sessionId, agentSessionId, _projectPath) {
|
|
1929
|
+
async compactSession(_sessionId, agentSessionId, _projectPath, modelOverride) {
|
|
1930
|
+
const startedAt = Date.now();
|
|
1931
|
+
const finish = (result) => {
|
|
1932
|
+
logger.info(`[CodexRunner] Compact result: thread=${agentSessionId} ok=${result.ok} code=${result.ok ? 'ok' : result.code} durationMs=${result.durationMs}`);
|
|
1933
|
+
return result;
|
|
1934
|
+
};
|
|
1340
1935
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId, _sessionId), 'compact');
|
|
1341
1936
|
try {
|
|
1342
1937
|
const appServer = this.getAppServerClient();
|
|
1343
1938
|
this.onCompactStart?.(_sessionId);
|
|
1344
1939
|
try {
|
|
1345
|
-
|
|
1940
|
+
await this.startAndWaitForCompact(appServer, agentSessionId);
|
|
1941
|
+
return finish({ ok: true, durationMs: Date.now() - startedAt });
|
|
1346
1942
|
}
|
|
1347
1943
|
catch (error) {
|
|
1348
1944
|
if (!this.isThreadNotFoundError(error))
|
|
1349
1945
|
throw error;
|
|
1350
1946
|
logger.info(`[CodexRunner] Compact thread not loaded, resuming before compact: ${agentSessionId}`);
|
|
1351
|
-
|
|
1352
|
-
const
|
|
1947
|
+
const compactMode = normalizeExecutionPermissionMode(modelOverride?.permissionMode ?? this.chatModes.get(_sessionId) ?? this.currentMode);
|
|
1948
|
+
const compactModel = modelOverride?.model || this.model;
|
|
1949
|
+
const compactEffort = modelOverride?.effortMode === 'model_default'
|
|
1950
|
+
? undefined
|
|
1951
|
+
: modelOverride?.effort ?? this.effort;
|
|
1353
1952
|
const compactPolicy = this.toApprovalPolicy(compactMode);
|
|
1354
1953
|
const executionSandbox = this.buildExecutionSandboxOptions(_sessionId, compactMode, _projectPath);
|
|
1355
1954
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(_projectPath);
|
|
1356
|
-
const externalToolConfig =
|
|
1357
|
-
|
|
1955
|
+
const externalToolConfig = compactMode === 'fullaccess'
|
|
1956
|
+
? {}
|
|
1957
|
+
: await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
|
|
1958
|
+
if (compactMode !== 'fullaccess')
|
|
1959
|
+
await this.assertProactiveHookAvailability(_sessionId, appServer);
|
|
1358
1960
|
const compactDelegationCarrier = this.threadDelegationCarriers.get(agentSessionId)
|
|
1359
1961
|
?? this.createDelegationCarrier();
|
|
1360
1962
|
await appServer.threadResume(agentSessionId, _projectPath, {
|
|
1361
|
-
model:
|
|
1362
|
-
effort:
|
|
1963
|
+
model: compactModel,
|
|
1964
|
+
effort: compactEffort,
|
|
1363
1965
|
approvalPolicy: compactPolicy,
|
|
1364
1966
|
approvalsReviewer: 'user',
|
|
1365
1967
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1366
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig()), compactMode),
|
|
1968
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
1367
1969
|
});
|
|
1368
1970
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
1369
1971
|
this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
|
|
1370
1972
|
this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
|
|
1371
|
-
|
|
1973
|
+
await this.startAndWaitForCompact(appServer, agentSessionId);
|
|
1974
|
+
return finish({ ok: true, durationMs: Date.now() - startedAt });
|
|
1372
1975
|
}
|
|
1373
1976
|
}
|
|
1374
1977
|
catch (error) {
|
|
1375
|
-
|
|
1376
|
-
return false;
|
|
1978
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1979
|
+
return finish({ ok: false, code: 'sdk_error', message, durationMs: Date.now() - startedAt });
|
|
1377
1980
|
}
|
|
1378
1981
|
finally {
|
|
1379
1982
|
release();
|
|
1380
1983
|
}
|
|
1381
1984
|
}
|
|
1382
|
-
async compact(sessionId, agentSessionId, projectPath) {
|
|
1383
|
-
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
1985
|
+
async compact(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
1986
|
+
return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
|
|
1384
1987
|
}
|
|
1385
1988
|
async startAndWaitForCompact(appServer, threadId) {
|
|
1386
1989
|
const completion = this.waitForThreadCompacted(appServer, threadId, Date.now());
|
|
@@ -1527,7 +2130,7 @@ export class CodexRunner {
|
|
|
1527
2130
|
approvalPolicy: this.toApprovalPolicy(mode),
|
|
1528
2131
|
approvalsReviewer: 'user',
|
|
1529
2132
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1530
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig()), mode),
|
|
2133
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig(mode)), mode),
|
|
1531
2134
|
});
|
|
1532
2135
|
const forkedThreadId = response.thread?.id;
|
|
1533
2136
|
if (!forkedThreadId)
|
|
@@ -1582,13 +2185,14 @@ export class CodexRunner {
|
|
|
1582
2185
|
trackApprovalItemNotification(notification) {
|
|
1583
2186
|
const params = (notification.params || {});
|
|
1584
2187
|
const item = params.item && typeof params.item === 'object' ? params.item : undefined;
|
|
1585
|
-
const itemId = item?.id ?? params.itemId;
|
|
2188
|
+
const itemId = item?.id ?? params.itemId ?? params.item_id;
|
|
1586
2189
|
const threadId = this.requestThreadId(params);
|
|
1587
|
-
const
|
|
2190
|
+
const turnId = params.turnId ?? params.turn_id;
|
|
2191
|
+
const key = this.approvalItemKey(threadId, turnId, itemId);
|
|
1588
2192
|
if (notification.method === 'item/started' && key && item && threadId) {
|
|
1589
2193
|
this.trackedApprovalItems.set(key, {
|
|
1590
2194
|
threadId,
|
|
1591
|
-
turnId
|
|
2195
|
+
turnId,
|
|
1592
2196
|
itemId,
|
|
1593
2197
|
item: { ...item },
|
|
1594
2198
|
});
|
|
@@ -1598,7 +2202,7 @@ export class CodexRunner {
|
|
|
1598
2202
|
const tracked = this.trackedApprovalItems.get(key);
|
|
1599
2203
|
this.trackedApprovalItems.set(key, {
|
|
1600
2204
|
threadId,
|
|
1601
|
-
turnId
|
|
2205
|
+
turnId,
|
|
1602
2206
|
itemId,
|
|
1603
2207
|
item: {
|
|
1604
2208
|
...(tracked?.item ?? { id: itemId, type: 'fileChange' }),
|
|
@@ -1622,22 +2226,40 @@ export class CodexRunner {
|
|
|
1622
2226
|
this.trackedApprovalItems.delete(trackedKey);
|
|
1623
2227
|
}
|
|
1624
2228
|
}
|
|
2229
|
+
this.clearPendingToolDenialsForTurn(threadId, completedTurnId);
|
|
1625
2230
|
}
|
|
1626
2231
|
}
|
|
1627
2232
|
findTrackedApprovalItem(params) {
|
|
1628
|
-
const key = this.approvalItemKey(this.requestThreadId(params), params.turnId, params.itemId);
|
|
2233
|
+
const key = this.approvalItemKey(this.requestThreadId(params), params.turnId ?? params.turn_id, params.itemId ?? params.item_id);
|
|
1629
2234
|
return key ? this.trackedApprovalItems.get(key)?.item : undefined;
|
|
1630
2235
|
}
|
|
1631
2236
|
requestThreadId(params) {
|
|
1632
|
-
const
|
|
1633
|
-
|
|
2237
|
+
const threadIdValue = params.threadId ?? params.thread_id;
|
|
2238
|
+
const threadId = typeof threadIdValue === 'string' && threadIdValue.length > 0
|
|
2239
|
+
? threadIdValue
|
|
1634
2240
|
: undefined;
|
|
1635
2241
|
if (threadId)
|
|
1636
2242
|
return threadId;
|
|
1637
|
-
|
|
1638
|
-
|
|
2243
|
+
const conversationIdValue = params.conversationId ?? params.conversation_id;
|
|
2244
|
+
return typeof conversationIdValue === 'string' && conversationIdValue.length > 0
|
|
2245
|
+
? conversationIdValue
|
|
1639
2246
|
: undefined;
|
|
1640
2247
|
}
|
|
2248
|
+
clearPendingToolDenialsForTurn(threadId, turnId) {
|
|
2249
|
+
for (const key of this.pendingToolDenials.keys()) {
|
|
2250
|
+
let parsed;
|
|
2251
|
+
try {
|
|
2252
|
+
parsed = JSON.parse(key);
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
if (!Array.isArray(parsed) || parsed[0] !== threadId)
|
|
2258
|
+
continue;
|
|
2259
|
+
if (!turnId || parsed[1] === turnId)
|
|
2260
|
+
this.pendingToolDenials.delete(key);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
1641
2263
|
clearTrackedApprovalItemsForThread(threadId) {
|
|
1642
2264
|
if (!threadId)
|
|
1643
2265
|
return;
|
|
@@ -1645,6 +2267,73 @@ export class CodexRunner {
|
|
|
1645
2267
|
if (tracked.threadId === threadId)
|
|
1646
2268
|
this.trackedApprovalItems.delete(key);
|
|
1647
2269
|
}
|
|
2270
|
+
for (const key of this.pendingToolDenials.keys()) {
|
|
2271
|
+
let parsed;
|
|
2272
|
+
try {
|
|
2273
|
+
parsed = JSON.parse(key);
|
|
2274
|
+
}
|
|
2275
|
+
catch {
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
if (Array.isArray(parsed) && parsed[0] === threadId)
|
|
2279
|
+
this.pendingToolDenials.delete(key);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
rememberToolDenial(request, params, denial) {
|
|
2283
|
+
const itemId = params.itemId ?? params.item_id ?? params.callId ?? params.call_id;
|
|
2284
|
+
const threadId = this.requestThreadId(params);
|
|
2285
|
+
const rawTurnId = params.turnId ?? params.turn_id;
|
|
2286
|
+
const turnId = typeof rawTurnId === 'string' ? rawTurnId : this.findActiveTurnId(threadId);
|
|
2287
|
+
const key = this.approvalItemKey(threadId, turnId, itemId);
|
|
2288
|
+
if (!key)
|
|
2289
|
+
return;
|
|
2290
|
+
this.pendingToolDenials.set(key, {
|
|
2291
|
+
...denial,
|
|
2292
|
+
...(request.id !== undefined ? { requestId: String(request.id) } : {}),
|
|
2293
|
+
method: request.method,
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
steerPolicyReason(sessionId, policyCode, reason) {
|
|
2297
|
+
const prefix = policyCode ? `策略拒绝(${policyCode})` : '策略拒绝';
|
|
2298
|
+
this.injectUserMessage(sessionId, `⚠️ ${prefix}:${reason}。命令未执行,请按提示重新执行正确的命令。`);
|
|
2299
|
+
}
|
|
2300
|
+
findActiveTurnId(threadId) {
|
|
2301
|
+
if (!threadId)
|
|
2302
|
+
return undefined;
|
|
2303
|
+
for (const active of this.activeTurns.values()) {
|
|
2304
|
+
if (active.threadId === threadId)
|
|
2305
|
+
return active.turnId;
|
|
2306
|
+
}
|
|
2307
|
+
return undefined;
|
|
2308
|
+
}
|
|
2309
|
+
takeToolDenial(state, itemId) {
|
|
2310
|
+
const exactKey = this.approvalItemKey(state.threadId, state.turnId, itemId);
|
|
2311
|
+
if (exactKey) {
|
|
2312
|
+
const exact = this.pendingToolDenials.get(exactKey);
|
|
2313
|
+
if (exact) {
|
|
2314
|
+
this.pendingToolDenials.delete(exactKey);
|
|
2315
|
+
return exact;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
// Legacy approval callbacks may not carry turnId. Consume the unique
|
|
2319
|
+
// matching item for this thread rather than losing the structured reason.
|
|
2320
|
+
const candidates = [];
|
|
2321
|
+
for (const [key, denial] of this.pendingToolDenials) {
|
|
2322
|
+
let parsed;
|
|
2323
|
+
try {
|
|
2324
|
+
parsed = JSON.parse(key);
|
|
2325
|
+
}
|
|
2326
|
+
catch {
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
if (!Array.isArray(parsed) || parsed[0] !== state.threadId || String(parsed[2]) !== String(itemId))
|
|
2330
|
+
continue;
|
|
2331
|
+
candidates.push([key, denial]);
|
|
2332
|
+
}
|
|
2333
|
+
if (candidates.length !== 1)
|
|
2334
|
+
return undefined;
|
|
2335
|
+
this.pendingToolDenials.delete(candidates[0][0]);
|
|
2336
|
+
return candidates[0][1];
|
|
1648
2337
|
}
|
|
1649
2338
|
async handleAppServerRequest(request) {
|
|
1650
2339
|
const params = (request.params || {});
|
|
@@ -1654,7 +2343,12 @@ export class CodexRunner {
|
|
|
1654
2343
|
}
|
|
1655
2344
|
if (request.method === 'item/tool/requestUserInput') {
|
|
1656
2345
|
const trackedItem = this.findTrackedApprovalItem(params);
|
|
2346
|
+
const requestSessionKey = this.findSessionKeyByThread(params.threadId, params.conversationId);
|
|
2347
|
+
const trustedFullAccess = (this.chatModes.get(requestSessionKey) ?? this.currentMode) === 'fullaccess'
|
|
2348
|
+
&& this.isTrustedFullAccessSession(requestSessionKey);
|
|
1657
2349
|
if (trackedItem?.type === 'mcpToolCall') {
|
|
2350
|
+
if (trustedFullAccess)
|
|
2351
|
+
return this.handleMcpToolApproval(params, trackedItem);
|
|
1658
2352
|
const unattendedDenial = await this.denyUnattendedExternalToolApproval(params, `MCP:${String(trackedItem.server || 'unknown')}/${String(trackedItem.tool || 'unknown')}`, request.id);
|
|
1659
2353
|
if (unattendedDenial)
|
|
1660
2354
|
return unattendedDenial;
|
|
@@ -1664,6 +2358,12 @@ export class CodexRunner {
|
|
|
1664
2358
|
return this.handleToolRequestUserInput(params);
|
|
1665
2359
|
}
|
|
1666
2360
|
if (this.looksLikeExternalToolApproval(params.questions)) {
|
|
2361
|
+
// In trusted fullaccess, an untracked approval prompt is still an SDK
|
|
2362
|
+
// permission surface, not ordinary model/user interaction. The
|
|
2363
|
+
// fullaccess branch in handleMcpToolApproval deliberately does not
|
|
2364
|
+
// require capability-bound metadata and selects a one-shot allow.
|
|
2365
|
+
if (trustedFullAccess)
|
|
2366
|
+
return this.handleMcpToolApproval(params, {});
|
|
1667
2367
|
const unattendedDenial = await this.denyUnattendedExternalToolApproval(params, 'MCP:untracked', request.id);
|
|
1668
2368
|
if (unattendedDenial)
|
|
1669
2369
|
return unattendedDenial;
|
|
@@ -1690,30 +2390,51 @@ export class CodexRunner {
|
|
|
1690
2390
|
? 'FileChange'
|
|
1691
2391
|
: 'Bash';
|
|
1692
2392
|
const toolInput = this.buildPermissionInput(request.method, params);
|
|
1693
|
-
//
|
|
2393
|
+
// A trusted fullaccess run has already been explicitly authorized by the
|
|
2394
|
+
// daemon owner. Do not route its approvals through ordinary capability,
|
|
2395
|
+
// workspace, or unattended-trigger policy checks.
|
|
2396
|
+
if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
|
|
2397
|
+
if (!this.isTrustedFullAccessSession(sessionKey)) {
|
|
2398
|
+
const reason = 'Untrusted fullaccess execution context';
|
|
2399
|
+
this.rememberToolDenial(request, params, {
|
|
2400
|
+
decision: 'deny',
|
|
2401
|
+
decisionSource: 'policy',
|
|
2402
|
+
policyCode: 'fullaccess_context_untrusted',
|
|
2403
|
+
reason,
|
|
2404
|
+
});
|
|
2405
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', 'fullaccess_context_untrusted', reason);
|
|
2406
|
+
setImmediate(() => this.steerPolicyReason(sessionKey, 'fullaccess_context_untrusted', reason));
|
|
2407
|
+
return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
2408
|
+
}
|
|
2409
|
+
const response = this.toAppServerApprovalResponse(request.method, 'allow', toolInput, false);
|
|
2410
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'allow', 'approval', undefined, 'trusted fullaccess execution');
|
|
2411
|
+
return response;
|
|
2412
|
+
}
|
|
2413
|
+
// Keep the policy check in the approval path as a fail-closed fallback.
|
|
2414
|
+
// Some app-server tool routes can reach approval without invoking the
|
|
2415
|
+
// command hook; allowing here would bypass the proactive gate.
|
|
1694
2416
|
const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
|
|
1695
2417
|
if (policyResult?.block) {
|
|
1696
2418
|
logger.info(`[CodexRunner] app-server approval declined by session policy: session=${sessionKey} ` +
|
|
1697
2419
|
`requestId=${request.id ?? '<missing>'} code=${policyResult.policyCode ?? 'session_policy_hook'} ` +
|
|
1698
2420
|
`reason=${policyResult.reason ?? 'policy denied'} executed=false`);
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
logger.warn(`[CodexRunner] failed to record policy-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
|
|
2421
|
+
const policyCode = policyResult.policyCode ?? 'session_policy_hook';
|
|
2422
|
+
const policyReason = policyResult.reason ?? 'session policy denied approval';
|
|
2423
|
+
this.rememberToolDenial(request, params, {
|
|
2424
|
+
decision: 'deny',
|
|
2425
|
+
decisionSource: 'policy',
|
|
2426
|
+
policyCode,
|
|
2427
|
+
reason: policyReason,
|
|
2428
|
+
});
|
|
2429
|
+
// ResponseEngine's wrapped policy hook already steers Codex sessions
|
|
2430
|
+
// that have the pure managed-hook policy installed. Standalone callers
|
|
2431
|
+
// may only provide policyHook, so keep this fallback for them without
|
|
2432
|
+
// duplicating the same user message in production.
|
|
2433
|
+
if (!permissionContext?.preToolUsePolicyHook) {
|
|
2434
|
+
setImmediate(() => this.steerPolicyReason(sessionKey, policyCode, policyReason));
|
|
1714
2435
|
}
|
|
1715
2436
|
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1716
|
-
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy',
|
|
2437
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', policyCode, policyReason);
|
|
1717
2438
|
return response;
|
|
1718
2439
|
}
|
|
1719
2440
|
const summary = this.summarizeAppServerRequest(request.method, params);
|
|
@@ -1722,18 +2443,28 @@ export class CodexRunner {
|
|
|
1722
2443
|
if (!workspacePath) {
|
|
1723
2444
|
logger.warn(`[CodexRunner] approval denied because thread workspace is unknown: method=${request.method} thread=${params.threadId ?? params.conversationId ?? '<missing>'}`);
|
|
1724
2445
|
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
2446
|
+
this.rememberToolDenial(request, params, {
|
|
2447
|
+
decision: 'deny',
|
|
2448
|
+
decisionSource: 'infrastructure',
|
|
2449
|
+
policyCode: 'approval_workspace_unknown',
|
|
2450
|
+
reason: 'thread workspace is unknown',
|
|
2451
|
+
});
|
|
1725
2452
|
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', 'approval_workspace_unknown', 'thread workspace is unknown');
|
|
2453
|
+
setImmediate(() => this.steerPolicyReason(sessionKey, 'approval_workspace_unknown', 'thread workspace is unknown'));
|
|
1726
2454
|
return response;
|
|
1727
2455
|
}
|
|
1728
2456
|
const operationCwd = this.resolvePermissionOperationCwd(params, workspacePath, toolName);
|
|
1729
2457
|
const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
1730
|
-
|
|
2458
|
+
const logSummary = formatApprovalLogSummary(summary);
|
|
2459
|
+
logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${logSummary}`);
|
|
1731
2460
|
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
1732
2461
|
|| request.method === 'execCommandApproval';
|
|
1733
2462
|
const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
|
|
1734
|
-
const
|
|
1735
|
-
|
|
2463
|
+
const delegationCarrier = this.delegationCarrierForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
|
|
2464
|
+
const canonicalEcApproval = isCommandApproval
|
|
2465
|
+
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), delegationCarrier)
|
|
1736
2466
|
: undefined;
|
|
2467
|
+
const approvedCommand = canonicalEcApproval?.argv;
|
|
1737
2468
|
const denyInfrastructure = async (policyCode, message) => {
|
|
1738
2469
|
logger.warn(`[CodexRunner] ${message}: session=${sessionKey} requestId=${request.id ?? '<missing>'}`);
|
|
1739
2470
|
try {
|
|
@@ -1747,8 +2478,9 @@ export class CodexRunner {
|
|
|
1747
2478
|
policyCode,
|
|
1748
2479
|
decisionSource: 'infrastructure',
|
|
1749
2480
|
agentAid: permissionContext?.selfAid,
|
|
2481
|
+
agentName: permissionContext?.agentName,
|
|
1750
2482
|
sessionId: sessionKey,
|
|
1751
|
-
permissionMode:
|
|
2483
|
+
permissionMode: normalizeExecutionPermissionMode(sessionMode),
|
|
1752
2484
|
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
1753
2485
|
effect: 'operation_skipped',
|
|
1754
2486
|
});
|
|
@@ -1757,17 +2489,32 @@ export class CodexRunner {
|
|
|
1757
2489
|
logger.warn(`[CodexRunner] failed to record infrastructure approval denial: session=${sessionKey} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1758
2490
|
}
|
|
1759
2491
|
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1760
|
-
this.
|
|
2492
|
+
this.rememberToolDenial(request, params, {
|
|
2493
|
+
decision: 'deny',
|
|
2494
|
+
decisionSource: 'infrastructure',
|
|
2495
|
+
policyCode,
|
|
2496
|
+
reason: message,
|
|
2497
|
+
});
|
|
2498
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode, message, canonicalEcApproval?.parseFailure);
|
|
2499
|
+
setImmediate(() => this.steerPolicyReason(sessionKey, policyCode, message));
|
|
1761
2500
|
return response;
|
|
1762
2501
|
};
|
|
1763
2502
|
if (managedEcIntent && !approvedCommand) {
|
|
1764
|
-
|
|
2503
|
+
if (canonicalEcApproval?.parseFailure?.issue === 'untrusted-shell') {
|
|
2504
|
+
return await denyInfrastructure('ec_shell_untrusted_executable', 'EC command approval denied because the shell executable failed Windows Authenticode trust validation');
|
|
2505
|
+
}
|
|
2506
|
+
return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
|
|
2507
|
+
? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
|
|
2508
|
+
: 'EC command approval denied because no canonical argv could be derived');
|
|
1765
2509
|
}
|
|
1766
2510
|
try {
|
|
1767
|
-
const
|
|
2511
|
+
const resolution = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id, typeof params.itemId === 'string' ? params.itemId : typeof params.item_id === 'string' ? params.item_id : undefined);
|
|
2512
|
+
const decision = typeof resolution === 'string' ? resolution : resolution.decision;
|
|
2513
|
+
const resolutionMeta = typeof resolution === 'string' ? undefined : resolution;
|
|
1768
2514
|
if (decision !== 'deny' && managedEcIntent && approvedCommand) {
|
|
1769
2515
|
const carrierThreadIds = [
|
|
1770
2516
|
this.activeSessions.get(sessionKey),
|
|
2517
|
+
this.provisionalSessions.get(sessionKey),
|
|
1771
2518
|
typeof params.threadId === 'string' ? params.threadId : undefined,
|
|
1772
2519
|
typeof params.conversationId === 'string' ? params.conversationId : undefined,
|
|
1773
2520
|
];
|
|
@@ -1776,18 +2523,35 @@ export class CodexRunner {
|
|
|
1776
2523
|
// A carrier-bearing thread is managed even if its permission context
|
|
1777
2524
|
// was not wired with an arm callback; it must fail closed here rather
|
|
1778
2525
|
// than reaching daemon IPC with an unarmed carrier.
|
|
2526
|
+
if ((hasDelegationCarrier || canArmDelegation)
|
|
2527
|
+
&& canonicalEcApproval?.delegationToken
|
|
2528
|
+
&& canonicalEcApproval.delegationToken !== delegationCarrier) {
|
|
2529
|
+
return await denyInfrastructure('delegation_token_mismatch', 'EC command approval contains a delegation token different from the active thread carrier');
|
|
2530
|
+
}
|
|
1779
2531
|
if ((hasDelegationCarrier || canArmDelegation)
|
|
1780
2532
|
&& !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
|
|
1781
2533
|
return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
|
|
1782
2534
|
}
|
|
1783
2535
|
}
|
|
1784
2536
|
const response = this.toAppServerApprovalResponse(request.method, decision, toolInput, unattended);
|
|
1785
|
-
|
|
1786
|
-
? '
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2537
|
+
const decisionSource = resolutionMeta?.decisionSource
|
|
2538
|
+
?? (sessionMode === 'auto' || sessionMode === 'readonly' ? 'policy' : 'approval');
|
|
2539
|
+
const policyCode = resolutionMeta?.policyCode
|
|
2540
|
+
?? (decisionSource === 'policy' ? 'permission_mode_denied' : undefined);
|
|
2541
|
+
const denialReason = resolutionMeta?.reason || reason || (policyCode
|
|
2542
|
+
? `permission mode ${sessionMode} denied approval`
|
|
2543
|
+
: 'user approval denied');
|
|
2544
|
+
if (decision === 'deny') {
|
|
2545
|
+
this.rememberToolDenial(request, params, {
|
|
2546
|
+
decision: 'deny',
|
|
2547
|
+
decisionSource,
|
|
2548
|
+
...(policyCode ? { policyCode } : {}),
|
|
2549
|
+
reason: denialReason,
|
|
2550
|
+
});
|
|
2551
|
+
setImmediate(() => this.steerPolicyReason(sessionKey, policyCode, denialReason));
|
|
2552
|
+
}
|
|
2553
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, decision === 'deny' ? 'deny' : 'allow', decision === 'deny' ? decisionSource : 'approval', decision === 'deny' ? policyCode : undefined, decision === 'deny'
|
|
2554
|
+
? denialReason
|
|
1791
2555
|
: 'approval accepted');
|
|
1792
2556
|
logger.info(`[CodexRunner] app-server approval response id=${request.id} method=${request.method} decision=${decision} response=${JSON.stringify(response)}`);
|
|
1793
2557
|
return response;
|
|
@@ -1798,29 +2562,38 @@ export class CodexRunner {
|
|
|
1798
2562
|
throw error;
|
|
1799
2563
|
}
|
|
1800
2564
|
}
|
|
1801
|
-
logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode, reason) {
|
|
2565
|
+
logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode, reason, parseFailure) {
|
|
1802
2566
|
const context = this.permissionContexts.get(sessionKey);
|
|
2567
|
+
const params = (request.params || {});
|
|
2568
|
+
const rawCallId = params.itemId ?? params.item_id ?? params.callId ?? params.call_id;
|
|
2569
|
+
const callId = typeof rawCallId === 'string' && rawCallId ? rawCallId : undefined;
|
|
1803
2570
|
auditCodexApprovalDecision({
|
|
1804
2571
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2572
|
+
callId,
|
|
2573
|
+
correlationId: callId,
|
|
1805
2574
|
method: request.method,
|
|
1806
2575
|
sessionId: sessionKey,
|
|
1807
2576
|
agentAid: context?.selfAid,
|
|
2577
|
+
agentName: context?.agentName,
|
|
1808
2578
|
toolName,
|
|
1809
2579
|
decision,
|
|
1810
2580
|
decisionSource,
|
|
1811
2581
|
policyCode,
|
|
1812
|
-
permissionMode:
|
|
2582
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
1813
2583
|
role: context?.role,
|
|
1814
2584
|
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
2585
|
+
parseFailure,
|
|
1815
2586
|
});
|
|
1816
2587
|
logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
|
|
1817
2588
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2589
|
+
...(callId ? { callId, correlationId: callId } : {}),
|
|
1818
2590
|
method: request.method,
|
|
1819
2591
|
sessionId: sessionKey,
|
|
1820
2592
|
toolName,
|
|
1821
2593
|
decision,
|
|
1822
2594
|
decisionSource,
|
|
1823
2595
|
...(policyCode ? { policyCode } : {}),
|
|
2596
|
+
...(parseFailure ? { parseFailure } : {}),
|
|
1824
2597
|
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
1825
2598
|
executed: false,
|
|
1826
2599
|
})}`);
|
|
@@ -1910,6 +2683,12 @@ export class CodexRunner {
|
|
|
1910
2683
|
? { requestId: String(requestId) }
|
|
1911
2684
|
: {}),
|
|
1912
2685
|
policy: context.approvalRouting?.approverPolicy ?? 'requester',
|
|
2686
|
+
policyCode: 'permission_mode_denied',
|
|
2687
|
+
decisionSource: 'policy',
|
|
2688
|
+
agentAid: context.selfAid,
|
|
2689
|
+
agentName: context.agentName,
|
|
2690
|
+
sessionId: sessionKey,
|
|
2691
|
+
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
1913
2692
|
summary: `${toolName} runtime authorization denied by unattended Trigger policy`,
|
|
1914
2693
|
effect: 'operation_skipped',
|
|
1915
2694
|
});
|
|
@@ -1924,6 +2703,27 @@ export class CodexRunner {
|
|
|
1924
2703
|
const questions = Array.isArray(params.questions) ? params.questions : [];
|
|
1925
2704
|
if (questions.length === 0)
|
|
1926
2705
|
throw new Error('Codex MCP 审批未包含问题,已拒绝');
|
|
2706
|
+
const sessionKey = this.findSessionKeyByThread(params.threadId, params.conversationId);
|
|
2707
|
+
if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
|
|
2708
|
+
if (!this.isTrustedFullAccessSession(sessionKey))
|
|
2709
|
+
return this.denyUntrackedExternalToolApproval(params);
|
|
2710
|
+
const answers = {};
|
|
2711
|
+
for (const question of questions) {
|
|
2712
|
+
const questionId = typeof question.id === 'string' ? question.id : `q-${Object.keys(answers).length + 1}`;
|
|
2713
|
+
const labels = Array.isArray(question.options)
|
|
2714
|
+
? question.options.map((option) => typeof option?.label === 'string' ? option.label.trim() : '').filter(Boolean)
|
|
2715
|
+
: [];
|
|
2716
|
+
let answer = '';
|
|
2717
|
+
try {
|
|
2718
|
+
answer = this.selectMcpApprovalAnswer(question, true);
|
|
2719
|
+
}
|
|
2720
|
+
catch {
|
|
2721
|
+
answer = labels[0] ?? 'allow';
|
|
2722
|
+
}
|
|
2723
|
+
answers[questionId] = { answers: [answer] };
|
|
2724
|
+
}
|
|
2725
|
+
return { answers };
|
|
2726
|
+
}
|
|
1927
2727
|
const hasExactIdentity = typeof trackedItem.server === 'string'
|
|
1928
2728
|
&& trackedItem.server.length > 0
|
|
1929
2729
|
&& typeof trackedItem.tool === 'string'
|
|
@@ -1940,7 +2740,6 @@ export class CodexRunner {
|
|
|
1940
2740
|
logger.warn(`[CodexRunner] MCP approval denied because the hardened external-tool config is unbound: thread=${threadId ?? '<missing>'}`);
|
|
1941
2741
|
return this.denyUntrackedExternalToolApproval(params);
|
|
1942
2742
|
}
|
|
1943
|
-
const sessionKey = this.findSessionKeyByThread(params.threadId, params.conversationId);
|
|
1944
2743
|
const server = trackedItem.server;
|
|
1945
2744
|
const tool = trackedItem.tool;
|
|
1946
2745
|
const isAppCall = typeof trackedItem.mcpAppResourceUri === 'string'
|
|
@@ -1964,7 +2763,7 @@ export class CodexRunner {
|
|
|
1964
2763
|
};
|
|
1965
2764
|
const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
|
|
1966
2765
|
const workspacePath = this.resolvePermissionWorkspacePath(params);
|
|
1967
|
-
const mode =
|
|
2766
|
+
const mode = normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode);
|
|
1968
2767
|
// MCP servers are inherited unless capability config explicitly disables
|
|
1969
2768
|
// them. Apps retain their separate request/bypass lifecycle policy.
|
|
1970
2769
|
let allow = !policyResult?.block
|
|
@@ -2084,7 +2883,8 @@ export class CodexRunner {
|
|
|
2084
2883
|
chatmode: context.chatmode,
|
|
2085
2884
|
replyContext: context.replyContext,
|
|
2086
2885
|
});
|
|
2087
|
-
|
|
2886
|
+
const receipt = await sendInteractionPayload(context.adapter, envelope, interaction, undefined, context.replyContext);
|
|
2887
|
+
sent = isInteractionSendAccepted(receipt);
|
|
2088
2888
|
}
|
|
2089
2889
|
if (!sent) {
|
|
2090
2890
|
await sendPrompt(renderActionAsText(interaction));
|
|
@@ -2147,9 +2947,20 @@ export class CodexRunner {
|
|
|
2147
2947
|
if (activeThreadId === candidate)
|
|
2148
2948
|
return sessionKey;
|
|
2149
2949
|
}
|
|
2950
|
+
for (const [sessionKey, provisionalThreadId] of this.provisionalSessions.entries()) {
|
|
2951
|
+
if (provisionalThreadId === candidate)
|
|
2952
|
+
return sessionKey;
|
|
2953
|
+
}
|
|
2954
|
+
const childSessionKey = this.childThreadSessions.get(candidate);
|
|
2955
|
+
if (childSessionKey)
|
|
2956
|
+
return childSessionKey;
|
|
2150
2957
|
}
|
|
2151
2958
|
return candidates[0] || 'codex-app-server';
|
|
2152
2959
|
}
|
|
2960
|
+
isThreadOwnedBySession(sessionId, threadId) {
|
|
2961
|
+
const activeThread = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
2962
|
+
return activeThread === threadId || this.childThreadSessions.get(threadId) === sessionId;
|
|
2963
|
+
}
|
|
2153
2964
|
buildPermissionInput(method, params) {
|
|
2154
2965
|
if (method === 'item/permissions/requestApproval') {
|
|
2155
2966
|
return { permissions: params.permissions, cwd: params.cwd, reason: params.reason };
|
|
@@ -2211,6 +3022,12 @@ export class CodexRunner {
|
|
|
2211
3022
|
const threadProjectPath = this.threadProjectPaths.get(threadId);
|
|
2212
3023
|
if (threadProjectPath)
|
|
2213
3024
|
return threadProjectPath;
|
|
3025
|
+
const parentSession = this.childThreadSessions.get(threadId);
|
|
3026
|
+
const parentThread = parentSession
|
|
3027
|
+
? this.activeSessions.get(parentSession) ?? this.provisionalSessions.get(parentSession)
|
|
3028
|
+
: undefined;
|
|
3029
|
+
if (parentThread)
|
|
3030
|
+
return this.threadProjectPaths.get(parentThread);
|
|
2214
3031
|
}
|
|
2215
3032
|
return undefined;
|
|
2216
3033
|
}
|
|
@@ -2239,7 +3056,7 @@ export class CodexRunner {
|
|
|
2239
3056
|
// Revalidate ownership, mode, and parent containment at the final
|
|
2240
3057
|
// readonly decision. The map entry alone is stale if the directory was
|
|
2241
3058
|
// replaced between preflight and the approval callback.
|
|
2242
|
-
managedTempDir: this.getManagedTempDir(sessionKey),
|
|
3059
|
+
managedTempDir: permCtx?.managedTempDir ?? this.getManagedTempDir(sessionKey),
|
|
2243
3060
|
allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
|
|
2244
3061
|
} : undefined;
|
|
2245
3062
|
if (toolName === 'Bash')
|
|
@@ -2341,7 +3158,7 @@ export class CodexRunner {
|
|
|
2341
3158
|
return true;
|
|
2342
3159
|
});
|
|
2343
3160
|
}
|
|
2344
|
-
async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId) {
|
|
3161
|
+
async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId, callId) {
|
|
2345
3162
|
const permissionContext = this.permissionContexts.get(sessionKey);
|
|
2346
3163
|
const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
|
|
2347
3164
|
const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
|
|
@@ -2355,7 +3172,13 @@ export class CodexRunner {
|
|
|
2355
3172
|
&& this.fileChangeRequiresExpansion(toolInput, workspacePath, sessionKey);
|
|
2356
3173
|
// per-session 权限模式(runQuery 写入);缺省回落实例级 currentMode(兼容无 runQuery 上下文的调用)
|
|
2357
3174
|
const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
2358
|
-
const mode =
|
|
3175
|
+
const mode = normalizeExecutionPermissionMode(rawMode);
|
|
3176
|
+
if (rawMode === 'fullaccess') {
|
|
3177
|
+
if (!this.isTrustedFullAccessSession(sessionKey)) {
|
|
3178
|
+
return 'deny';
|
|
3179
|
+
}
|
|
3180
|
+
return 'allow';
|
|
3181
|
+
}
|
|
2359
3182
|
const recordBlockedOperation = async (policyCode, operationInput = toolInput, denialReason = 'policy denied', matchedPath) => {
|
|
2360
3183
|
const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
|
|
2361
3184
|
auditToolPreflightDenial({
|
|
@@ -2365,12 +3188,15 @@ export class CodexRunner {
|
|
|
2365
3188
|
summary,
|
|
2366
3189
|
sessionId: sessionKey,
|
|
2367
3190
|
agentAid: permissionContext?.selfAid,
|
|
3191
|
+
agentName: permissionContext?.agentName,
|
|
2368
3192
|
permissionMode: mode,
|
|
2369
3193
|
channel: permissionContext?.channel,
|
|
2370
3194
|
actorId: permissionContext?.userId,
|
|
2371
3195
|
role: permissionContext?.role,
|
|
2372
3196
|
selfAid: permissionContext?.selfAid,
|
|
2373
3197
|
requestId: typeof requestId === 'string' || typeof requestId === 'number' ? String(requestId) : undefined,
|
|
3198
|
+
callId,
|
|
3199
|
+
correlationId: callId,
|
|
2374
3200
|
taskId: permissionContext?.taskId,
|
|
2375
3201
|
matchedPath,
|
|
2376
3202
|
});
|
|
@@ -2385,6 +3211,7 @@ export class CodexRunner {
|
|
|
2385
3211
|
policyCode,
|
|
2386
3212
|
decisionSource: 'policy',
|
|
2387
3213
|
agentAid: permissionContext?.selfAid,
|
|
3214
|
+
agentName: permissionContext?.agentName,
|
|
2388
3215
|
permissionMode: mode,
|
|
2389
3216
|
summary,
|
|
2390
3217
|
effect: 'operation_skipped',
|
|
@@ -2399,7 +3226,8 @@ export class CodexRunner {
|
|
|
2399
3226
|
// An unavailable registered root must fail closed; the policy helper
|
|
2400
3227
|
// distinguishes this explicit empty value from runners without a
|
|
2401
3228
|
// session-temp capability.
|
|
2402
|
-
managedTempDir: this.getManagedTempDir(sessionKey) ?? '',
|
|
3229
|
+
managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
|
|
3230
|
+
delegationCarrier: this.delegationCarrierForSession(sessionKey),
|
|
2403
3231
|
allowProtectedMetadata: false,
|
|
2404
3232
|
selfAid: permissionContext?.selfAid,
|
|
2405
3233
|
channel: permissionContext?.channel,
|
|
@@ -2416,7 +3244,12 @@ export class CodexRunner {
|
|
|
2416
3244
|
if (preflight.policyCode) {
|
|
2417
3245
|
await recordBlockedOperation(preflight.policyCode, toolInput, preflight.message ?? 'tool preflight denied', preflight.matchedPath);
|
|
2418
3246
|
}
|
|
2419
|
-
return
|
|
3247
|
+
return {
|
|
3248
|
+
decision: 'deny',
|
|
3249
|
+
decisionSource: 'policy',
|
|
3250
|
+
policyCode: preflight.policyCode ?? 'tool_preflight_denied',
|
|
3251
|
+
reason: preflight.message ?? 'tool preflight denied',
|
|
3252
|
+
};
|
|
2420
3253
|
}
|
|
2421
3254
|
if (preflight.behavior === 'allow')
|
|
2422
3255
|
return 'allow';
|
|
@@ -2686,7 +3519,8 @@ export class CodexRunner {
|
|
|
2686
3519
|
const input = [{ type: 'text', text: prompt, text_elements: [] }];
|
|
2687
3520
|
if (!images?.length)
|
|
2688
3521
|
return input;
|
|
2689
|
-
const tmpDir = runtimeEnv
|
|
3522
|
+
const tmpDir = getManagedTaskTempDir(runtimeEnv)
|
|
3523
|
+
?? runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR
|
|
2690
3524
|
?? (sessionId ? this.getManagedTempDir(sessionId) : undefined)
|
|
2691
3525
|
?? process.env.TMPDIR;
|
|
2692
3526
|
if (!tmpDir || !path.isAbsolute(tmpDir))
|
|
@@ -2729,19 +3563,142 @@ export class CodexRunner {
|
|
|
2729
3563
|
}
|
|
2730
3564
|
return !state.turnId || !turnId || turnId === state.turnId;
|
|
2731
3565
|
}
|
|
2732
|
-
|
|
3566
|
+
isSubagentActivityItem(item) {
|
|
3567
|
+
return typeof item?.type === 'string' && item.type.toLowerCase() === 'subagentactivity';
|
|
3568
|
+
}
|
|
3569
|
+
subagentThreadId(item) {
|
|
3570
|
+
const value = item?.agentThreadId ?? item?.agent_thread_id;
|
|
3571
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
3572
|
+
}
|
|
3573
|
+
subagentPath(item) {
|
|
3574
|
+
const value = item?.agentPath ?? item?.agent_path;
|
|
3575
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
3576
|
+
}
|
|
3577
|
+
subagentType(item) {
|
|
3578
|
+
const value = item?.agentType ?? item?.agent_type;
|
|
3579
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
3580
|
+
}
|
|
3581
|
+
registerChildThread(sessionId, childThreadId, projectPath) {
|
|
3582
|
+
const parentThreadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
3583
|
+
if (childThreadId === parentThreadId)
|
|
3584
|
+
return;
|
|
3585
|
+
this.childThreadSessions.set(childThreadId, sessionId);
|
|
3586
|
+
this.threadProjectPaths.set(childThreadId, path.resolve(projectPath));
|
|
3587
|
+
// Child approvals use the parent's capability boundary and delegation
|
|
3588
|
+
// carrier. This is inheritance, not a second permission decision.
|
|
3589
|
+
const parentBoundary = parentThreadId ? this.threadExternalToolBoundaries.get(parentThreadId) : undefined;
|
|
3590
|
+
const parentFingerprint = parentThreadId ? this.threadExternalToolFingerprints.get(parentThreadId) : undefined;
|
|
3591
|
+
if (parentBoundary)
|
|
3592
|
+
this.threadExternalToolBoundaries.set(childThreadId, parentBoundary);
|
|
3593
|
+
if (parentFingerprint)
|
|
3594
|
+
this.threadExternalToolFingerprints.set(childThreadId, parentFingerprint);
|
|
3595
|
+
const parentCarrier = parentThreadId ? this.threadDelegationCarriers.get(parentThreadId) : undefined;
|
|
3596
|
+
if (parentCarrier)
|
|
3597
|
+
this.threadDelegationCarriers.set(childThreadId, parentCarrier);
|
|
3598
|
+
}
|
|
3599
|
+
unregisterChildThread(childThreadId) {
|
|
3600
|
+
this.childThreadSessions.delete(childThreadId);
|
|
3601
|
+
this.activeSubagentTurns.delete(childThreadId);
|
|
3602
|
+
this.threadProjectPaths.delete(childThreadId);
|
|
3603
|
+
this.threadExternalToolBoundaries.delete(childThreadId);
|
|
3604
|
+
this.threadExternalToolFingerprints.delete(childThreadId);
|
|
3605
|
+
this.threadDelegationCarriers.delete(childThreadId);
|
|
3606
|
+
this.clearTrackedApprovalItemsForThread(childThreadId);
|
|
3607
|
+
}
|
|
3608
|
+
cleanupChildThreadsForSession(sessionId) {
|
|
3609
|
+
for (const [childThreadId, ownerSessionId] of this.childThreadSessions) {
|
|
3610
|
+
if (ownerSessionId === sessionId)
|
|
3611
|
+
this.unregisterChildThread(childThreadId);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
trackSubagentLifecycle(notification, state) {
|
|
3615
|
+
const params = (notification.params || {});
|
|
3616
|
+
const item = params.item && typeof params.item === 'object' ? params.item : undefined;
|
|
3617
|
+
if (!this.isSubagentActivityItem(item))
|
|
3618
|
+
return;
|
|
3619
|
+
const childThreadId = this.subagentThreadId(item);
|
|
3620
|
+
if (!childThreadId)
|
|
3621
|
+
return;
|
|
3622
|
+
const kind = typeof item.kind === 'string' ? item.kind.toLowerCase() : '';
|
|
3623
|
+
if (kind === 'started') {
|
|
3624
|
+
state.activeSubagentThreads.add(childThreadId);
|
|
3625
|
+
this.registerChildThread(state.sessionId, childThreadId, state.projectPath);
|
|
3626
|
+
}
|
|
3627
|
+
if (kind === 'completed' || kind === 'failed' || kind === 'stopped' || kind === 'killed') {
|
|
3628
|
+
state.activeSubagentThreads.delete(childThreadId);
|
|
3629
|
+
this.unregisterChildThread(childThreadId);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
primeSubagentStateFromTurn(turn, state) {
|
|
3633
|
+
for (const item of this.getTurnItems(turn)) {
|
|
3634
|
+
if (!this.isSubagentActivityItem(item))
|
|
3635
|
+
continue;
|
|
3636
|
+
const childThreadId = this.subagentThreadId(item);
|
|
3637
|
+
const kind = typeof item.kind === 'string' ? item.kind.toLowerCase() : '';
|
|
3638
|
+
if (!childThreadId)
|
|
3639
|
+
continue;
|
|
3640
|
+
if (kind === 'started') {
|
|
3641
|
+
state.activeSubagentThreads.add(childThreadId);
|
|
3642
|
+
this.registerChildThread(state.sessionId, childThreadId, state.projectPath);
|
|
3643
|
+
}
|
|
3644
|
+
else if (kind === 'completed' || kind === 'failed' || kind === 'stopped' || kind === 'killed') {
|
|
3645
|
+
state.activeSubagentThreads.delete(childThreadId);
|
|
3646
|
+
this.unregisterChildThread(childThreadId);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
endAppServerQueueWhenSubagentsDone(queue, state) {
|
|
3651
|
+
if (!state.turnCompleted)
|
|
3652
|
+
return;
|
|
3653
|
+
if (state.activeSubagentThreads.size > 0) {
|
|
3654
|
+
logger.info(`[CodexRunner] parent turn completed; waiting for subagent thread(s): `
|
|
3655
|
+
+ `${[...state.activeSubagentThreads].join(',')}`);
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
queue.end();
|
|
3659
|
+
}
|
|
3660
|
+
async *transformAppServerStream(notifications, sessionId, state, controller, unsubscribe, tempFiles) {
|
|
2733
3661
|
try {
|
|
2734
|
-
|
|
3662
|
+
let sessionIdEmitted = state.committed;
|
|
3663
|
+
if (sessionIdEmitted)
|
|
3664
|
+
yield { type: 'session_id', sessionId: state.threadId };
|
|
2735
3665
|
for await (const notification of notifications) {
|
|
2736
3666
|
if (!this.activeAbortControllers.has(sessionId))
|
|
2737
3667
|
break;
|
|
3668
|
+
if (!state.committed && notification.method === 'turn/completed') {
|
|
3669
|
+
const turn = notification.params?.turn ?? {};
|
|
3670
|
+
if (turn.status === 'completed') {
|
|
3671
|
+
const committed = await state.commitProvisional?.();
|
|
3672
|
+
if (!committed) {
|
|
3673
|
+
const staleError = new Error('Codex provisional thread was superseded before commit');
|
|
3674
|
+
staleError.code = 'RUNNER_START_SUPERSEDED';
|
|
3675
|
+
throw staleError;
|
|
3676
|
+
}
|
|
3677
|
+
state.committed = true;
|
|
3678
|
+
if (!sessionIdEmitted) {
|
|
3679
|
+
sessionIdEmitted = true;
|
|
3680
|
+
yield { type: 'session_id', sessionId: state.threadId };
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
2738
3684
|
yield* this.mapAppServerNotification(notification, sessionId, state);
|
|
2739
3685
|
}
|
|
2740
3686
|
}
|
|
2741
3687
|
finally {
|
|
2742
3688
|
unsubscribe();
|
|
2743
|
-
this.activeAbortControllers.
|
|
2744
|
-
|
|
3689
|
+
if (this.activeAbortControllers.get(sessionId) === controller) {
|
|
3690
|
+
this.activeAbortControllers.delete(sessionId);
|
|
3691
|
+
}
|
|
3692
|
+
const activeTurn = this.activeTurns.get(sessionId);
|
|
3693
|
+
if (activeTurn?.threadId === state.threadId
|
|
3694
|
+
&& (!state.turnId || activeTurn.turnId === state.turnId)) {
|
|
3695
|
+
this.activeTurns.delete(sessionId);
|
|
3696
|
+
}
|
|
3697
|
+
if (!state.committed)
|
|
3698
|
+
this.cleanupProvisionalThread(sessionId, state.threadId);
|
|
3699
|
+
for (const childThreadId of state.activeSubagentThreads)
|
|
3700
|
+
this.unregisterChildThread(childThreadId);
|
|
3701
|
+
state.activeSubagentThreads.clear();
|
|
2745
3702
|
this.cleanupTempFiles(tempFiles);
|
|
2746
3703
|
}
|
|
2747
3704
|
}
|
|
@@ -2809,12 +3766,16 @@ export class CodexRunner {
|
|
|
2809
3766
|
}
|
|
2810
3767
|
case 'turn/completed': {
|
|
2811
3768
|
const turn = params.turn || {};
|
|
2812
|
-
const turnId = turn.id || params.turnId;
|
|
3769
|
+
const turnId = turn.id || params.turnId || state.turnId;
|
|
2813
3770
|
if (turnId && state.completedTurnIds.has(turnId))
|
|
2814
3771
|
break;
|
|
2815
3772
|
if (turnId)
|
|
2816
3773
|
state.completedTurnIds.add(turnId);
|
|
2817
|
-
this.activeTurns.
|
|
3774
|
+
const activeTurn = this.activeTurns.get(sessionId);
|
|
3775
|
+
if (activeTurn?.threadId === state.threadId && activeTurn.turnId === turnId) {
|
|
3776
|
+
this.activeTurns.delete(sessionId);
|
|
3777
|
+
}
|
|
3778
|
+
this.clearPendingToolDenialsForTurn(state.threadId, turnId);
|
|
2818
3779
|
yield* this.reconcileOpenAppServerToolCalls(state, turnId);
|
|
2819
3780
|
if (turn.status === 'failed' && turn.error?.message) {
|
|
2820
3781
|
if (isRetryableError(new Error(turn.error.message))) {
|
|
@@ -2935,11 +3896,36 @@ export class CodexRunner {
|
|
|
2935
3896
|
};
|
|
2936
3897
|
break;
|
|
2937
3898
|
case 'subAgentActivity':
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
3899
|
+
case 'SubAgentActivity': {
|
|
3900
|
+
const childThreadId = this.subagentThreadId(item);
|
|
3901
|
+
if (!childThreadId)
|
|
3902
|
+
break;
|
|
3903
|
+
const kind = typeof item.kind === 'string' ? item.kind.toLowerCase() : '';
|
|
3904
|
+
const description = this.subagentPath(item) ?? childThreadId;
|
|
3905
|
+
if (kind === 'started') {
|
|
3906
|
+
yield {
|
|
3907
|
+
type: 'task_started',
|
|
3908
|
+
taskId: childThreadId,
|
|
3909
|
+
taskKind: 'agent',
|
|
3910
|
+
description,
|
|
3911
|
+
subagentType: this.subagentType(item),
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
else if (kind === 'completed' || kind === 'failed' || kind === 'stopped' || kind === 'killed') {
|
|
3915
|
+
yield {
|
|
3916
|
+
type: 'task_notification',
|
|
3917
|
+
taskId: childThreadId,
|
|
3918
|
+
taskKind: 'agent',
|
|
3919
|
+
description,
|
|
3920
|
+
status: kind === 'completed' ? 'completed' : kind,
|
|
3921
|
+
summary: description,
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
else {
|
|
3925
|
+
yield { type: 'task_progress', taskId: childThreadId, taskKind: 'agent', description, summary: `Subagent ${item.kind}: ${description}` };
|
|
3926
|
+
}
|
|
2942
3927
|
break;
|
|
3928
|
+
}
|
|
2943
3929
|
case 'enteredReviewMode':
|
|
2944
3930
|
yield { type: 'task_progress', summary: item.review || 'Review mode entered' };
|
|
2945
3931
|
break;
|
|
@@ -2954,6 +3940,59 @@ export class CodexRunner {
|
|
|
2954
3940
|
*mapAppServerItemCompleted(item, state) {
|
|
2955
3941
|
if (!item)
|
|
2956
3942
|
return;
|
|
3943
|
+
const denial = this.takeToolDenial(state, item.id);
|
|
3944
|
+
const denialFields = denial
|
|
3945
|
+
? {
|
|
3946
|
+
isError: true,
|
|
3947
|
+
error: denial.reason,
|
|
3948
|
+
errorCode: errorCodeForToolDenial(denial.decisionSource, denial.policyCode),
|
|
3949
|
+
decision: 'deny',
|
|
3950
|
+
decisionSource: denial.decisionSource,
|
|
3951
|
+
policyCode: denial.policyCode,
|
|
3952
|
+
reason: denial.reason,
|
|
3953
|
+
requestId: denial.requestId,
|
|
3954
|
+
executed: false,
|
|
3955
|
+
executionState: 'blocked',
|
|
3956
|
+
}
|
|
3957
|
+
: undefined;
|
|
3958
|
+
const itemErrorText = outputTextForManagedHook(typeof item.error === 'string' ? item.error : item.error?.message);
|
|
3959
|
+
const hookErrorText = /(?:Command|Tool call) blocked by PreToolUse hook\s*:/i.test(itemErrorText)
|
|
3960
|
+
? itemErrorText
|
|
3961
|
+
: undefined;
|
|
3962
|
+
const managedHookDenial = (item.status === 'failed' || item.status === 'declined' || hookErrorText)
|
|
3963
|
+
? parseManagedPreToolUseDenial([
|
|
3964
|
+
item.aggregatedOutput,
|
|
3965
|
+
item.output,
|
|
3966
|
+
item.result,
|
|
3967
|
+
hookErrorText,
|
|
3968
|
+
].filter(value => value !== undefined && value !== null).map(outputTextForManagedHook).join('\n'))
|
|
3969
|
+
: undefined;
|
|
3970
|
+
const managedHookFields = !denial && managedHookDenial
|
|
3971
|
+
? {
|
|
3972
|
+
isError: true,
|
|
3973
|
+
error: managedHookDenial.reason,
|
|
3974
|
+
errorCode: 'POLICY_DENIED',
|
|
3975
|
+
decision: 'deny',
|
|
3976
|
+
decisionSource: 'policy',
|
|
3977
|
+
policyCode: managedHookDenial.policyCode,
|
|
3978
|
+
reason: managedHookDenial.reason,
|
|
3979
|
+
executed: false,
|
|
3980
|
+
executionState: 'blocked',
|
|
3981
|
+
}
|
|
3982
|
+
: undefined;
|
|
3983
|
+
const genericDeclineFields = !denial && !managedHookFields && item.status === 'declined'
|
|
3984
|
+
? {
|
|
3985
|
+
isError: true,
|
|
3986
|
+
error: 'Approval declined',
|
|
3987
|
+
errorCode: 'USER_DENIED',
|
|
3988
|
+
decision: 'deny',
|
|
3989
|
+
decisionSource: 'approval',
|
|
3990
|
+
reason: 'Approval declined',
|
|
3991
|
+
executed: false,
|
|
3992
|
+
executionState: 'blocked',
|
|
3993
|
+
}
|
|
3994
|
+
: undefined;
|
|
3995
|
+
const blockedFields = denialFields ?? managedHookFields ?? genericDeclineFields;
|
|
2957
3996
|
switch (item.type) {
|
|
2958
3997
|
case 'agentMessage':
|
|
2959
3998
|
{
|
|
@@ -2970,8 +4009,10 @@ export class CodexRunner {
|
|
|
2970
4009
|
type: 'tool_result',
|
|
2971
4010
|
name: 'Shell',
|
|
2972
4011
|
result: item.aggregatedOutput ?? '',
|
|
2973
|
-
|
|
2974
|
-
|
|
4012
|
+
...(blockedFields ?? {
|
|
4013
|
+
isError: item.exitCode !== null && item.exitCode !== undefined ? item.exitCode !== 0 : item.status === 'failed' || item.status === 'declined',
|
|
4014
|
+
...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
|
|
4015
|
+
}),
|
|
2975
4016
|
callId: item.id,
|
|
2976
4017
|
durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
|
|
2977
4018
|
};
|
|
@@ -2981,8 +4022,8 @@ export class CodexRunner {
|
|
|
2981
4022
|
type: 'tool_result',
|
|
2982
4023
|
name: `MCP:${item.server}/${item.tool}`,
|
|
2983
4024
|
result: item.result,
|
|
2984
|
-
isError: item.status === 'failed',
|
|
2985
|
-
error: item.error?.message,
|
|
4025
|
+
...(blockedFields ?? { isError: item.status === 'failed' }),
|
|
4026
|
+
...(blockedFields ? {} : { error: item.error?.message }),
|
|
2986
4027
|
...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
|
|
2987
4028
|
callId: item.id,
|
|
2988
4029
|
durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
|
|
@@ -2993,7 +4034,7 @@ export class CodexRunner {
|
|
|
2993
4034
|
type: 'tool_result',
|
|
2994
4035
|
name: item.namespace ? `${item.namespace}:${item.tool}` : item.tool,
|
|
2995
4036
|
result: item.contentItems,
|
|
2996
|
-
isError: item.success === false || item.status === 'failed',
|
|
4037
|
+
...(blockedFields ?? { isError: item.success === false || item.status === 'failed' }),
|
|
2997
4038
|
...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
|
|
2998
4039
|
callId: item.id,
|
|
2999
4040
|
durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
|
|
@@ -3008,12 +4049,12 @@ export class CodexRunner {
|
|
|
3008
4049
|
yield editEvent;
|
|
3009
4050
|
}
|
|
3010
4051
|
}
|
|
3011
|
-
yield { type: 'tool_result', name: 'Edit', result: item.changes, isError: item.status === 'failed', callId: item.id };
|
|
4052
|
+
yield { type: 'tool_result', name: 'Edit', result: item.changes, ...(blockedFields ?? { isError: item.status === 'failed' }), callId: item.id };
|
|
3012
4053
|
}
|
|
3013
4054
|
else {
|
|
3014
4055
|
const desc = this.normalizeFileChanges(item.changes).map((change) => this.describeFileChange(change)).join(', ');
|
|
3015
4056
|
yield { type: 'tool_use', name: 'FileChange', input: { description: desc }, callId: item.id };
|
|
3016
|
-
yield { type: 'tool_result', name: 'FileChange', result: item.changes, isError: item.status === 'failed', callId: item.id };
|
|
4057
|
+
yield { type: 'tool_result', name: 'FileChange', result: item.changes, ...(blockedFields ?? { isError: item.status === 'failed' }), callId: item.id };
|
|
3017
4058
|
}
|
|
3018
4059
|
break;
|
|
3019
4060
|
case 'webSearch':
|
|
@@ -3067,6 +4108,34 @@ export class CodexRunner {
|
|
|
3067
4108
|
callId: item.id,
|
|
3068
4109
|
};
|
|
3069
4110
|
break;
|
|
4111
|
+
case 'subAgentActivity':
|
|
4112
|
+
case 'SubAgentActivity': {
|
|
4113
|
+
const childThreadId = this.subagentThreadId(item);
|
|
4114
|
+
if (!childThreadId)
|
|
4115
|
+
break;
|
|
4116
|
+
const kind = typeof item.kind === 'string' ? item.kind.toLowerCase() : '';
|
|
4117
|
+
const description = this.subagentPath(item) ?? childThreadId;
|
|
4118
|
+
if (kind === 'started') {
|
|
4119
|
+
yield {
|
|
4120
|
+
type: 'task_started',
|
|
4121
|
+
taskId: childThreadId,
|
|
4122
|
+
taskKind: 'agent',
|
|
4123
|
+
description,
|
|
4124
|
+
subagentType: this.subagentType(item),
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
else if (kind === 'completed' || kind === 'failed' || kind === 'stopped' || kind === 'killed') {
|
|
4128
|
+
yield {
|
|
4129
|
+
type: 'task_notification',
|
|
4130
|
+
taskId: childThreadId,
|
|
4131
|
+
taskKind: 'agent',
|
|
4132
|
+
description,
|
|
4133
|
+
status: kind === 'completed' ? 'completed' : kind,
|
|
4134
|
+
summary: description,
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
break;
|
|
4138
|
+
}
|
|
3070
4139
|
}
|
|
3071
4140
|
}
|
|
3072
4141
|
*mapAppServerFileChangePatchUpdated(params, state) {
|
|
@@ -3314,16 +4383,21 @@ export class CodexRunner {
|
|
|
3314
4383
|
for (const [key, controller] of this.activeAbortControllers) {
|
|
3315
4384
|
controller.abort('dispose');
|
|
3316
4385
|
}
|
|
4386
|
+
await Promise.all([...this.activeSubagentTurns.entries()].map(([threadId, active]) => this.interruptAppServerTurn(threadId, active.turnId).catch(() => { })));
|
|
3317
4387
|
this.activeAbortControllers.clear();
|
|
3318
4388
|
this.activeStreams.clear();
|
|
3319
4389
|
this.activeSessions.clear();
|
|
4390
|
+
this.provisionalSessions.clear();
|
|
3320
4391
|
this.activeTurns.clear();
|
|
4392
|
+
this.activeSubagentTurns.clear();
|
|
4393
|
+
this.childThreadSessions.clear();
|
|
3321
4394
|
this.threadProjectPaths.clear();
|
|
3322
4395
|
this.threadExternalToolFingerprints.clear();
|
|
3323
4396
|
this.threadExternalToolBoundaries.clear();
|
|
3324
4397
|
this.trackedApprovalItems.clear();
|
|
3325
4398
|
this.threadDelegationCarriers.clear();
|
|
3326
4399
|
this.pendingInterrupts.clear();
|
|
4400
|
+
this.steeringTails.clear();
|
|
3327
4401
|
this.permissionContexts.clear();
|
|
3328
4402
|
this.chatModes.clear();
|
|
3329
4403
|
this.pruneThreadOperationLocks();
|
|
@@ -3362,11 +4436,9 @@ export class CodexAgentPlugin {
|
|
|
3362
4436
|
agents: {
|
|
3363
4437
|
codex: {
|
|
3364
4438
|
...(override || {}),
|
|
3365
|
-
evolcoreAgentAid: agent.config.aid,
|
|
3366
|
-
evolcoreAgentConfig: agent.config,
|
|
3367
4439
|
},
|
|
3368
4440
|
},
|
|
3369
4441
|
};
|
|
3370
|
-
return { evolagentName: agent.name, baseagent: 'codex', agent: new CodexRunner(merged, callbacks) };
|
|
4442
|
+
return { evolagentName: agent.name, baseagent: 'codex', agent: new CodexRunner(merged, callbacks, { agentAid: agent.config.aid, agentConfig: agent.config }) };
|
|
3371
4443
|
}
|
|
3372
4444
|
}
|