evolcore 0.0.21 → 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 +43 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/claude-runner.js +14 -0
- package/dist/agents/codex-app-server-client.js +31 -5
- package/dist/agents/codex-runner.js +926 -121
- package/dist/aun/outbox.js +7 -0
- package/dist/channels/aun.js +209 -35
- package/dist/cli/daemon-commands.js +29 -8
- package/dist/cli/task-context.js +4 -0
- package/dist/cli/trigger-command.js +13 -4
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/core/auth/agent-delegation.js +12 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +66 -6
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +21 -0
- package/dist/core/command/menu-handler.js +9 -0
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/slash-handler.js +41 -18
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +21 -0
- package/dist/core/message/response-engine.js +172 -41
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +11 -10
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +46 -5
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +64 -5
- package/dist/ipc.js +97 -17
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/parser.js +10 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/logger.js +9 -4
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +2 -2
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- 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/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 +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +2 -2
|
@@ -21,7 +21,7 @@ import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from
|
|
|
21
21
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
22
22
|
import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
|
|
23
23
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
24
|
-
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, parseCodexToolCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
|
|
24
|
+
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
|
|
25
25
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
26
26
|
import { resolvePaths, resolveRoot } from '../paths.js';
|
|
27
27
|
import { ensureProcessManagedTempDir, getManagedTaskTempDir } from '../cli/task-context.js';
|
|
@@ -83,6 +83,17 @@ class AsyncEventQueue {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
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
|
+
}
|
|
86
97
|
const managedTempDirsForExitCleanup = new Map();
|
|
87
98
|
let managedTempExitCleanupRegistered = false;
|
|
88
99
|
function isTrustedManagedTempDirectory(directory, parent) {
|
|
@@ -150,6 +161,63 @@ function stableSecurityValue(value) {
|
|
|
150
161
|
function externalToolConfigFingerprint(config) {
|
|
151
162
|
return createHash('sha256').update(stableSecurityValue(config)).digest('hex');
|
|
152
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
|
+
}
|
|
153
221
|
const CODEX_CATALOG_FALLBACK = [
|
|
154
222
|
{ slug: 'gpt-5.5', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
155
223
|
{ slug: 'gpt-5.4', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
@@ -158,6 +226,11 @@ const CODEX_CATALOG_FALLBACK = [
|
|
|
158
226
|
{ slug: 'gpt-5.2', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
159
227
|
];
|
|
160
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
|
+
}
|
|
161
234
|
let codexCatalogCache = null;
|
|
162
235
|
// The permission bridge is version-locked to the audited approval schema and
|
|
163
236
|
// managed PreToolUse hook behavior used by proactive first-tool enforcement.
|
|
@@ -256,15 +329,22 @@ export class CodexRunner {
|
|
|
256
329
|
activeAbortControllers = new Map();
|
|
257
330
|
activeStreams = new Map();
|
|
258
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();
|
|
259
334
|
activeTurns = new Map();
|
|
335
|
+
activeSubagentTurns = new Map();
|
|
260
336
|
threadProjectPaths = new Map();
|
|
337
|
+
/** Child Codex threads inherit the EvolCore session that started them. */
|
|
338
|
+
childThreadSessions = new Map();
|
|
261
339
|
sessionTempDirs = new Map();
|
|
262
340
|
managedTempNonce = randomBytes(16).toString('hex');
|
|
263
341
|
threadExternalToolFingerprints = new Map();
|
|
264
342
|
threadExternalToolBoundaries = new Map();
|
|
265
343
|
trackedApprovalItems = new Map();
|
|
344
|
+
pendingToolDenials = new Map();
|
|
266
345
|
threadDelegationCarriers = new Map();
|
|
267
346
|
pendingInterrupts = new Map();
|
|
347
|
+
steeringTails = new Map();
|
|
268
348
|
threadOperationLocks = new Map();
|
|
269
349
|
activeOperationReleases = new Map();
|
|
270
350
|
appServerClient = null;
|
|
@@ -330,21 +410,27 @@ export class CodexRunner {
|
|
|
330
410
|
createDelegationCarrier() {
|
|
331
411
|
return randomBytes(32).toString('base64url');
|
|
332
412
|
}
|
|
333
|
-
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix') {
|
|
413
|
+
approvedLiteralEvolcoreCommand(command, managedTempDir, dialect = 'posix', expectedDelegationToken, executable) {
|
|
334
414
|
const parseOptions = {
|
|
335
415
|
allowManagedTmpDir: true,
|
|
336
416
|
dialect,
|
|
417
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
337
418
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
419
|
+
...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
|
|
338
420
|
};
|
|
339
421
|
const classification = classifyEvolcoreShellCommand(command, parseOptions);
|
|
340
422
|
if (classification.kind === 'bounded-output')
|
|
341
423
|
return { argv: classification.command.argv };
|
|
342
424
|
// Keep the approval/delegation parser aligned with the preflight parser:
|
|
343
425
|
// the session-managed `$TMPDIR` token is a permitted EC path reference.
|
|
344
|
-
const parsed =
|
|
426
|
+
const parsed = executable
|
|
427
|
+
? parseCodexCarrierCommand({ command, dialect, executable }, parseOptions)
|
|
428
|
+
: dialect === 'powershell'
|
|
429
|
+
? parseCodexCarrierCommand({ command, dialect }, parseOptions)
|
|
430
|
+
: parseLiteralShellCommand(command, parseOptions);
|
|
345
431
|
if (!parsed.ok)
|
|
346
432
|
return { parseFailure: parsed };
|
|
347
|
-
if (parsed.argv[0] !== 'ec')
|
|
433
|
+
if (normalizeEvolcoreCommandArgv(parsed.argv)[0] !== 'ec')
|
|
348
434
|
return {};
|
|
349
435
|
if (containsLiteralManagedTmpDirOutsideSendContent(parsed.argv)) {
|
|
350
436
|
return { parseFailure: {
|
|
@@ -355,14 +441,17 @@ export class CodexRunner {
|
|
|
355
441
|
inputLength: command.length,
|
|
356
442
|
} };
|
|
357
443
|
}
|
|
358
|
-
return {
|
|
444
|
+
return {
|
|
445
|
+
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
446
|
+
...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
|
|
447
|
+
};
|
|
359
448
|
}
|
|
360
|
-
resolveCanonicalEcApproval(toolInput, managedTempDir) {
|
|
449
|
+
resolveCanonicalEcApproval(toolInput, managedTempDir, expectedDelegationToken) {
|
|
361
450
|
const explicitArgv = Array.isArray(toolInput.commandArgv)
|
|
362
451
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
363
452
|
? toolInput.commandArgv
|
|
364
453
|
: undefined;
|
|
365
|
-
if (explicitArgv
|
|
454
|
+
if (explicitArgv && normalizeEvolcoreCommandArgv(explicitArgv)[0] === 'ec') {
|
|
366
455
|
if (explicitArgv.some(value => value.includes('\0'))) {
|
|
367
456
|
return { parseFailure: {
|
|
368
457
|
issue: 'invalid-control-char', offset: 0,
|
|
@@ -372,16 +461,19 @@ export class CodexRunner {
|
|
|
372
461
|
}
|
|
373
462
|
return containsLiteralManagedTmpDirOutsideSendContent(explicitArgv)
|
|
374
463
|
? { parseFailure: { issue: 'unsafe-expansion', offset: 0, tokenIndex: 0, dialect: 'posix', inputLength: 0 } }
|
|
375
|
-
: { argv: explicitArgv };
|
|
464
|
+
: { argv: normalizeEvolcoreCommandArgv(explicitArgv) };
|
|
376
465
|
}
|
|
377
466
|
const explicitCarrier = explicitArgv
|
|
378
467
|
? resolveCodexShellCarrierArgv(explicitArgv)
|
|
379
468
|
: undefined;
|
|
380
469
|
if (explicitCarrier !== undefined) {
|
|
381
|
-
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect);
|
|
470
|
+
return this.approvedLiteralEvolcoreCommand(explicitCarrier.command, managedTempDir, explicitCarrier.dialect, expectedDelegationToken, explicitCarrier.executable);
|
|
382
471
|
}
|
|
383
472
|
if (hasCodexCmdCarrierEcIntent(toolInput)) {
|
|
384
|
-
const parsed = parseCodexToolCommand(toolInput
|
|
473
|
+
const parsed = parseCodexToolCommand(toolInput, {
|
|
474
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
475
|
+
...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
|
|
476
|
+
});
|
|
385
477
|
if (!parsed.ok)
|
|
386
478
|
return { parseFailure: parsed };
|
|
387
479
|
if (parsed.argv[0] === 'ec') {
|
|
@@ -390,7 +482,10 @@ export class CodexRunner {
|
|
|
390
482
|
issue: 'unsafe-expansion', offset: 0, tokenIndex: 0,
|
|
391
483
|
dialect: 'cmd', inputLength: 0,
|
|
392
484
|
} }
|
|
393
|
-
: {
|
|
485
|
+
: {
|
|
486
|
+
argv: normalizeEvolcoreCommandArgv(parsed.argv),
|
|
487
|
+
...(parsed.delegationToken ? { delegationToken: parsed.delegationToken } : {}),
|
|
488
|
+
};
|
|
394
489
|
}
|
|
395
490
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
396
491
|
const classification = classifyEvolcoreShellCommand(command, { dialect: 'cmd' });
|
|
@@ -407,7 +502,9 @@ export class CodexRunner {
|
|
|
407
502
|
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
408
503
|
const canonicalCommand = parseCodexShellCommandArgv(command, {
|
|
409
504
|
allowManagedTmpDir: true,
|
|
505
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
410
506
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
507
|
+
...(expectedDelegationToken !== undefined ? { expectedDelegationToken } : {}),
|
|
411
508
|
});
|
|
412
509
|
if (canonicalCommand?.[0] === 'ec') {
|
|
413
510
|
return containsLiteralManagedTmpDirOutsideSendContent(canonicalCommand)
|
|
@@ -436,11 +533,14 @@ export class CodexRunner {
|
|
|
436
533
|
isManagedEvolcoreCommandIntent(toolInput) {
|
|
437
534
|
if (Array.isArray(toolInput.commandArgv)) {
|
|
438
535
|
const rawArgv = toolInput.commandArgv;
|
|
439
|
-
if (rawArgv[0] === 'ec')
|
|
440
|
-
return true;
|
|
441
536
|
if (!rawArgv.every(value => typeof value === 'string'))
|
|
442
537
|
return false;
|
|
538
|
+
if (normalizeEvolcoreCommandArgv(rawArgv)[0] === 'ec')
|
|
539
|
+
return true;
|
|
443
540
|
const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
|
|
541
|
+
if (shellCarrier?.dialect === 'powershell'
|
|
542
|
+
&& hasPowerShellDelegationAssignment(shellCarrier.command))
|
|
543
|
+
return true;
|
|
444
544
|
return hasCodexCmdCarrierEcIntent(toolInput)
|
|
445
545
|
|| shellCarrier !== undefined
|
|
446
546
|
&& classifyEvolcoreShellCommand(shellCarrier.command, {
|
|
@@ -451,7 +551,7 @@ export class CodexRunner {
|
|
|
451
551
|
return classifyEvolcoreShellCommand(command).kind !== 'none';
|
|
452
552
|
}
|
|
453
553
|
armApprovedDelegationCommand(sessionId, argv) {
|
|
454
|
-
const threadId = this.activeSessions.get(sessionId);
|
|
554
|
+
const threadId = this.activeSessions.get(sessionId) ?? this.provisionalSessions.get(sessionId);
|
|
455
555
|
const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
|
|
456
556
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
457
557
|
const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
|
|
@@ -465,6 +565,21 @@ export class CodexRunner {
|
|
|
465
565
|
return false;
|
|
466
566
|
}
|
|
467
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
|
+
}
|
|
468
583
|
async readThreadDelegationCarrier(appServer, threadId) {
|
|
469
584
|
try {
|
|
470
585
|
const result = await appServer.threadShellCommand(threadId, `node -e "process.stdout.write(process.env.${AGENT_DELEGATION_TOKEN_ENV}||String())"`);
|
|
@@ -497,6 +612,49 @@ export class CodexRunner {
|
|
|
497
612
|
return undefined;
|
|
498
613
|
}
|
|
499
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
|
+
}
|
|
500
658
|
async persistDelegationCarrier(sessionManager, sessionId, threadId, carrier) {
|
|
501
659
|
if (typeof sessionManager?.getSessionById !== 'function')
|
|
502
660
|
return;
|
|
@@ -527,7 +685,15 @@ export class CodexRunner {
|
|
|
527
685
|
this.appServerClient = null;
|
|
528
686
|
this.threadExternalToolFingerprints.clear();
|
|
529
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();
|
|
530
695
|
this.trackedApprovalItems.clear();
|
|
696
|
+
this.pendingToolDenials.clear();
|
|
531
697
|
this.threadDelegationCarriers.clear();
|
|
532
698
|
client?.close().catch(error => {
|
|
533
699
|
logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
|
|
@@ -900,9 +1066,9 @@ export class CodexRunner {
|
|
|
900
1066
|
} : {}),
|
|
901
1067
|
...(mode !== 'readonly' && managedTempDir ? {
|
|
902
1068
|
[managedTempDir]: 'write',
|
|
903
|
-
[
|
|
1069
|
+
[managedTempDir + '/**']: 'write',
|
|
904
1070
|
[path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
|
|
905
|
-
[path.join(path.dirname(managedTempDir), 'evolcore-locks'
|
|
1071
|
+
[path.join(path.dirname(managedTempDir), 'evolcore-locks') + '/**']: 'write',
|
|
906
1072
|
} : {}),
|
|
907
1073
|
},
|
|
908
1074
|
network,
|
|
@@ -949,55 +1115,145 @@ export class CodexRunner {
|
|
|
949
1115
|
}
|
|
950
1116
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
951
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
|
+
}
|
|
952
1160
|
async assertProactiveHookAvailability(sessionId, appServer) {
|
|
953
1161
|
if (this.permissionContexts.get(sessionId)?.chatmode !== 'proactive')
|
|
954
1162
|
return;
|
|
955
|
-
if (process.platform
|
|
956
|
-
await appServer.assertManagedHooksAvailable();
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
if (process.platform !== 'linux') {
|
|
1163
|
+
if (process.platform !== 'linux' && process.platform !== 'win32') {
|
|
960
1164
|
throw new Error('Codex proactive managed PreToolUse enforcement requires Linux or an installed Windows managed hook');
|
|
961
1165
|
}
|
|
1166
|
+
await appServer.assertManagedHooksAvailable();
|
|
962
1167
|
}
|
|
963
|
-
async evaluatePreToolUse(threadId, toolName, toolInput, signal) {
|
|
1168
|
+
async evaluatePreToolUse(threadId, toolName, toolInput, signal, callId) {
|
|
964
1169
|
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
965
|
-
const activeThread = this.activeSessions.get(sessionKey);
|
|
1170
|
+
const activeThread = this.activeSessions.get(sessionKey) ?? this.provisionalSessions.get(sessionKey);
|
|
966
1171
|
const context = this.permissionContexts.get(sessionKey);
|
|
967
|
-
|
|
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)) {
|
|
968
1216
|
return { ok: true, applicable: false };
|
|
969
1217
|
}
|
|
970
1218
|
const proactiveSession = context?.chatmode === 'proactive';
|
|
971
1219
|
if (!context || (proactiveSession && !context.preToolUsePolicyHook)) {
|
|
972
|
-
|
|
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 };
|
|
973
1223
|
}
|
|
974
1224
|
if (context.pauseController) {
|
|
975
1225
|
const pauseResult = await context.pauseController.waitAtToolBoundary(signal);
|
|
976
1226
|
if (pauseResult === 'cancelled') {
|
|
977
|
-
|
|
1227
|
+
const reason = '工具调用已取消';
|
|
1228
|
+
await recordPreToolUseDenial('tool_call_cancelled', reason);
|
|
1229
|
+
return { ok: true, applicable: true, decision: 'deny', reason };
|
|
978
1230
|
}
|
|
979
1231
|
}
|
|
980
1232
|
if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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 };
|
|
984
1238
|
}
|
|
985
1239
|
// A tracked interactive thread still needs an authenticated, explicit
|
|
986
1240
|
// allow so the managed hook can fail closed when no runner matches.
|
|
987
1241
|
if (!context.preToolUsePolicyHook)
|
|
988
1242
|
return { ok: true, applicable: true, decision: 'allow' };
|
|
989
|
-
const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
990
|
-
? toolInput
|
|
991
|
-
: {};
|
|
992
1243
|
try {
|
|
993
1244
|
const result = context.preToolUsePolicyHook(toolName, normalizedInput);
|
|
994
1245
|
if (result?.block) {
|
|
995
|
-
|
|
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 };
|
|
996
1250
|
}
|
|
997
1251
|
return { ok: true, applicable: true, decision: 'allow' };
|
|
998
1252
|
}
|
|
999
1253
|
catch (error) {
|
|
1000
|
-
|
|
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 };
|
|
1001
1257
|
}
|
|
1002
1258
|
}
|
|
1003
1259
|
setPermissionGateway(gw) { this.permissionGateway = gw; }
|
|
@@ -1006,9 +1262,18 @@ export class CodexRunner {
|
|
|
1006
1262
|
this.activeStreams.set(key, stream);
|
|
1007
1263
|
}
|
|
1008
1264
|
cleanupStream(key) {
|
|
1265
|
+
const hadController = this.activeAbortControllers.has(key);
|
|
1266
|
+
const hadTurn = this.activeTurns.has(key);
|
|
1267
|
+
const provisionalThread = this.provisionalSessions.get(key);
|
|
1009
1268
|
this.activeStreams.delete(key);
|
|
1010
1269
|
this.activeAbortControllers.delete(key);
|
|
1011
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
|
+
}
|
|
1012
1277
|
this.chatModes.delete(key);
|
|
1013
1278
|
const release = this.activeOperationReleases.get(key);
|
|
1014
1279
|
if (release) {
|
|
@@ -1064,14 +1329,20 @@ export class CodexRunner {
|
|
|
1064
1329
|
}
|
|
1065
1330
|
}
|
|
1066
1331
|
isActiveThreadOperationAlias(key) {
|
|
1067
|
-
if (key.startsWith('session:'))
|
|
1068
|
-
|
|
1332
|
+
if (key.startsWith('session:')) {
|
|
1333
|
+
const sessionId = key.slice('session:'.length);
|
|
1334
|
+
return this.activeSessions.has(sessionId) || this.provisionalSessions.has(sessionId);
|
|
1335
|
+
}
|
|
1069
1336
|
if (key.startsWith('thread:')) {
|
|
1070
1337
|
const threadId = key.slice('thread:'.length);
|
|
1071
1338
|
for (const activeThreadId of this.activeSessions.values()) {
|
|
1072
1339
|
if (activeThreadId === threadId)
|
|
1073
1340
|
return true;
|
|
1074
1341
|
}
|
|
1342
|
+
for (const provisionalThreadId of this.provisionalSessions.values()) {
|
|
1343
|
+
if (provisionalThreadId === threadId)
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1075
1346
|
return false;
|
|
1076
1347
|
}
|
|
1077
1348
|
return true;
|
|
@@ -1157,7 +1428,18 @@ export class CodexRunner {
|
|
|
1157
1428
|
let agentSessionId = topicBinding
|
|
1158
1429
|
? (topicAgentSessionId || undefined)
|
|
1159
1430
|
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1160
|
-
|
|
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;
|
|
1161
1443
|
const callModel = modelOverride?.model || this.model;
|
|
1162
1444
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
1163
1445
|
? undefined
|
|
@@ -1190,13 +1472,13 @@ export class CodexRunner {
|
|
|
1190
1472
|
if (agentSessionId && !knownDelegationCarrier) {
|
|
1191
1473
|
knownDelegationCarrier = await this.loadPersistedDelegationCarrier(sessionManager, sessionId, agentSessionId);
|
|
1192
1474
|
}
|
|
1193
|
-
|
|
1475
|
+
let delegationCarrierCandidate = knownDelegationCarrier ?? this.createDelegationCarrier();
|
|
1194
1476
|
// Keep newly discovered carrier state local until generation-aware
|
|
1195
1477
|
// activation accepts the backend. A stale run must not publish a carrier
|
|
1196
1478
|
// that a later generation could mistake for its own thread.
|
|
1197
1479
|
let carrierToPublish;
|
|
1198
1480
|
const hasTaskDelegation = typeof runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] === 'string';
|
|
1199
|
-
|
|
1481
|
+
let threadOptions = {
|
|
1200
1482
|
model: callModel,
|
|
1201
1483
|
effort: callEffort,
|
|
1202
1484
|
approvalPolicy: effectiveApprovalPolicy,
|
|
@@ -1207,9 +1489,47 @@ export class CodexRunner {
|
|
|
1207
1489
|
};
|
|
1208
1490
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
1209
1491
|
`role=${executionSandbox.decision.role ?? 'none'} sandbox=${executionSandbox.decision.state}`);
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
+
}
|
|
1213
1533
|
let threadId = threadResponse.thread?.id || agentSessionId;
|
|
1214
1534
|
if (!threadId)
|
|
1215
1535
|
throw new Error('Codex app-server did not return a thread id');
|
|
@@ -1247,28 +1567,39 @@ export class CodexRunner {
|
|
|
1247
1567
|
else if (knownDelegationCarrier) {
|
|
1248
1568
|
carrierToPublish = { threadId, carrier: knownDelegationCarrier };
|
|
1249
1569
|
}
|
|
1250
|
-
agentSessionId = threadId;
|
|
1251
1570
|
const turn = modelOverride?.turn;
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|| bindingResult === 'legacy_updated';
|
|
1261
|
-
if (!bindingAccepted) {
|
|
1262
|
-
const error = new Error(`backend discovery rejected: ${bindingResult}`);
|
|
1263
|
-
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
1264
|
-
throw error;
|
|
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
|
+
}
|
|
1265
1579
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
this.
|
|
1269
|
-
|
|
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}`);
|
|
1270
1602
|
}
|
|
1271
|
-
this.bindThreadOperationKeys(`session:${sessionId}`, `thread:${threadId}`);
|
|
1272
1603
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
1273
1604
|
this.bindExternalToolBoundary(threadId, externalToolConfig);
|
|
1274
1605
|
const controller = new AbortController();
|
|
@@ -1279,21 +1610,55 @@ export class CodexRunner {
|
|
|
1279
1610
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
1280
1611
|
const state = {
|
|
1281
1612
|
threadId,
|
|
1613
|
+
sessionId,
|
|
1614
|
+
projectPath,
|
|
1282
1615
|
model: callModel,
|
|
1616
|
+
committed: resumingThread,
|
|
1283
1617
|
streamedAgentMessageIds: new Set(),
|
|
1284
1618
|
agentMessageDeltaText: new Map(),
|
|
1285
1619
|
completedItemIds: new Set(),
|
|
1286
1620
|
emittedEditCallIds: new Set(),
|
|
1287
1621
|
completedTurnIds: new Set(),
|
|
1288
1622
|
openToolCalls: new Map(),
|
|
1623
|
+
activeSubagentThreads: new Set(),
|
|
1624
|
+
turnCompleted: false,
|
|
1289
1625
|
};
|
|
1290
1626
|
const unsubscribe = appServer.onNotification(notification => {
|
|
1291
1627
|
// 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
|
|
1292
1628
|
// (如 thread/tokenUsage/updated)先于新 turn 到达,不能用它们 latch turnId
|
|
1293
1629
|
const params = notification.params || {};
|
|
1294
1630
|
const notifThreadId = params.threadId ?? params.thread_id;
|
|
1295
|
-
|
|
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)
|
|
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
|
+
}
|
|
1296
1660
|
return;
|
|
1661
|
+
}
|
|
1297
1662
|
if (notification.method === 'turn/started') {
|
|
1298
1663
|
const startedTurnId = this.extractTurnId(notification);
|
|
1299
1664
|
if (startedTurnId && !state.turnId) {
|
|
@@ -1309,13 +1674,20 @@ export class CodexRunner {
|
|
|
1309
1674
|
return;
|
|
1310
1675
|
queue.push(notification);
|
|
1311
1676
|
// 仅在已锁定 turnId 后才允许 turn/completed 结束队列,避免残留的旧 turn/completed 误关
|
|
1312
|
-
if (notification.method === 'turn/completed' && state.turnId)
|
|
1313
|
-
|
|
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
|
+
}
|
|
1314
1684
|
});
|
|
1315
1685
|
if (this.consumePendingInterrupt(sessionId)) {
|
|
1316
1686
|
controller.abort('User interrupt');
|
|
1317
1687
|
this.activeAbortControllers.delete(sessionId);
|
|
1318
1688
|
this.activeStreams.delete(sessionId);
|
|
1689
|
+
if (!resumingThread)
|
|
1690
|
+
this.cleanupProvisionalThread(sessionId, threadId);
|
|
1319
1691
|
logger.info(`[CodexRunner] Applied pending interrupt before turn start: ${sessionId}`);
|
|
1320
1692
|
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, tempFiles);
|
|
1321
1693
|
}
|
|
@@ -1327,6 +1699,14 @@ export class CodexRunner {
|
|
|
1327
1699
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1328
1700
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1329
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
|
+
}
|
|
1330
1710
|
const turnId = turnResponse.turn?.id;
|
|
1331
1711
|
if (turnId && !state.turnId) {
|
|
1332
1712
|
state.turnId = turnId;
|
|
@@ -1341,8 +1721,10 @@ export class CodexRunner {
|
|
|
1341
1721
|
}
|
|
1342
1722
|
const status = turnResponse.turn?.status;
|
|
1343
1723
|
if (status === 'completed' || status === 'failed') {
|
|
1724
|
+
this.primeSubagentStateFromTurn(turnResponse.turn, state);
|
|
1344
1725
|
queue.push({ method: 'turn/completed', params: { threadId, turn: turnResponse.turn } });
|
|
1345
|
-
|
|
1726
|
+
state.turnCompleted = true;
|
|
1727
|
+
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1346
1728
|
}
|
|
1347
1729
|
}
|
|
1348
1730
|
catch (error) {
|
|
@@ -1351,6 +1733,8 @@ export class CodexRunner {
|
|
|
1351
1733
|
this.activeTurns.delete(sessionId);
|
|
1352
1734
|
this.pendingInterrupts.delete(sessionId);
|
|
1353
1735
|
this.cleanupTempFiles(tempFiles);
|
|
1736
|
+
if (!resumingThread)
|
|
1737
|
+
this.cleanupProvisionalThread(sessionId, threadId);
|
|
1354
1738
|
throw error;
|
|
1355
1739
|
}
|
|
1356
1740
|
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, tempFiles);
|
|
@@ -1364,6 +1748,7 @@ export class CodexRunner {
|
|
|
1364
1748
|
const interruptTurn = activeTurn
|
|
1365
1749
|
? this.interruptAppServerTurn(activeTurn.threadId, activeTurn.turnId)
|
|
1366
1750
|
: Promise.resolve();
|
|
1751
|
+
const interruptSubagents = this.interruptSubagentTurns(sessionKey);
|
|
1367
1752
|
if (!activeTurn)
|
|
1368
1753
|
this.rememberPendingInterrupt(sessionKey);
|
|
1369
1754
|
if (controller)
|
|
@@ -1374,7 +1759,17 @@ export class CodexRunner {
|
|
|
1374
1759
|
this.activeTurns.delete(sessionKey);
|
|
1375
1760
|
logger.info(`[CodexRunner] Interrupted session: ${sessionKey}`);
|
|
1376
1761
|
}
|
|
1377
|
-
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
|
+
}));
|
|
1378
1773
|
}
|
|
1379
1774
|
rememberPendingInterrupt(sessionId) {
|
|
1380
1775
|
this.pendingInterrupts.set(sessionId, Date.now());
|
|
@@ -1437,10 +1832,12 @@ export class CodexRunner {
|
|
|
1437
1832
|
}
|
|
1438
1833
|
async closeSession(sessionId) {
|
|
1439
1834
|
const capturedThreadId = this.activeSessions.get(sessionId);
|
|
1835
|
+
const capturedProvisionalThreadId = this.provisionalSessions.get(sessionId);
|
|
1440
1836
|
const capturedPermissionContext = this.permissionContexts.get(sessionId);
|
|
1441
1837
|
const capturedStream = this.activeStreams.get(sessionId);
|
|
1442
1838
|
const capturedController = this.activeAbortControllers.get(sessionId);
|
|
1443
1839
|
const capturedTurn = this.activeTurns.get(sessionId);
|
|
1840
|
+
const capturedSteeringTail = this.steeringTails.get(sessionId);
|
|
1444
1841
|
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
1445
1842
|
// Do not call interrupt(sessionId) here: a new run may have replaced the
|
|
1446
1843
|
// session-scoped maps while an older close was waiting. Interrupt and
|
|
@@ -1448,6 +1845,13 @@ export class CodexRunner {
|
|
|
1448
1845
|
if (capturedTurn) {
|
|
1449
1846
|
await this.interruptAppServerTurn(capturedTurn.threadId, capturedTurn.turnId).catch(() => { });
|
|
1450
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
|
+
}
|
|
1451
1855
|
capturedController?.abort('Session closed');
|
|
1452
1856
|
const lifecycleReplaced = (capturedStream !== undefined && this.activeStreams.get(sessionId) !== capturedStream)
|
|
1453
1857
|
|| (capturedController !== undefined && this.activeAbortControllers.get(sessionId) !== capturedController)
|
|
@@ -1464,6 +1868,10 @@ export class CodexRunner {
|
|
|
1464
1868
|
this.pendingInterrupts.delete(sessionId);
|
|
1465
1869
|
if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
|
|
1466
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);
|
|
1467
1875
|
// Session-level configuration and thread indexes are shared by later
|
|
1468
1876
|
// runs. Only remove them when the captured lifecycle still owns the key.
|
|
1469
1877
|
if (!lifecycleReplaced) {
|
|
@@ -1477,6 +1885,7 @@ export class CodexRunner {
|
|
|
1477
1885
|
this.unbindExternalToolBoundary(capturedThreadId);
|
|
1478
1886
|
}
|
|
1479
1887
|
this.clearTrackedApprovalItemsForThread(capturedThreadId);
|
|
1888
|
+
this.cleanupChildThreadsForSession(sessionId);
|
|
1480
1889
|
this.pruneThreadOperationLocks();
|
|
1481
1890
|
}
|
|
1482
1891
|
}
|
|
@@ -1505,6 +1914,8 @@ export class CodexRunner {
|
|
|
1505
1914
|
// Codex: 清空会话 = 下次 runQuery 不传 resumeId,自动创建新 thread
|
|
1506
1915
|
const threadId = this.activeSessions.get(sessionId) ?? _agentSessionId;
|
|
1507
1916
|
this.activeSessions.delete(sessionId);
|
|
1917
|
+
this.activeTurns.delete(sessionId);
|
|
1918
|
+
this.steeringTails.delete(sessionId);
|
|
1508
1919
|
this.chatModes.delete(sessionId);
|
|
1509
1920
|
this.cleanupSessionTempDir(sessionId);
|
|
1510
1921
|
this.threadProjectPaths.delete(threadId);
|
|
@@ -1774,13 +2185,14 @@ export class CodexRunner {
|
|
|
1774
2185
|
trackApprovalItemNotification(notification) {
|
|
1775
2186
|
const params = (notification.params || {});
|
|
1776
2187
|
const item = params.item && typeof params.item === 'object' ? params.item : undefined;
|
|
1777
|
-
const itemId = item?.id ?? params.itemId;
|
|
2188
|
+
const itemId = item?.id ?? params.itemId ?? params.item_id;
|
|
1778
2189
|
const threadId = this.requestThreadId(params);
|
|
1779
|
-
const
|
|
2190
|
+
const turnId = params.turnId ?? params.turn_id;
|
|
2191
|
+
const key = this.approvalItemKey(threadId, turnId, itemId);
|
|
1780
2192
|
if (notification.method === 'item/started' && key && item && threadId) {
|
|
1781
2193
|
this.trackedApprovalItems.set(key, {
|
|
1782
2194
|
threadId,
|
|
1783
|
-
turnId
|
|
2195
|
+
turnId,
|
|
1784
2196
|
itemId,
|
|
1785
2197
|
item: { ...item },
|
|
1786
2198
|
});
|
|
@@ -1790,7 +2202,7 @@ export class CodexRunner {
|
|
|
1790
2202
|
const tracked = this.trackedApprovalItems.get(key);
|
|
1791
2203
|
this.trackedApprovalItems.set(key, {
|
|
1792
2204
|
threadId,
|
|
1793
|
-
turnId
|
|
2205
|
+
turnId,
|
|
1794
2206
|
itemId,
|
|
1795
2207
|
item: {
|
|
1796
2208
|
...(tracked?.item ?? { id: itemId, type: 'fileChange' }),
|
|
@@ -1814,22 +2226,40 @@ export class CodexRunner {
|
|
|
1814
2226
|
this.trackedApprovalItems.delete(trackedKey);
|
|
1815
2227
|
}
|
|
1816
2228
|
}
|
|
2229
|
+
this.clearPendingToolDenialsForTurn(threadId, completedTurnId);
|
|
1817
2230
|
}
|
|
1818
2231
|
}
|
|
1819
2232
|
findTrackedApprovalItem(params) {
|
|
1820
|
-
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);
|
|
1821
2234
|
return key ? this.trackedApprovalItems.get(key)?.item : undefined;
|
|
1822
2235
|
}
|
|
1823
2236
|
requestThreadId(params) {
|
|
1824
|
-
const
|
|
1825
|
-
|
|
2237
|
+
const threadIdValue = params.threadId ?? params.thread_id;
|
|
2238
|
+
const threadId = typeof threadIdValue === 'string' && threadIdValue.length > 0
|
|
2239
|
+
? threadIdValue
|
|
1826
2240
|
: undefined;
|
|
1827
2241
|
if (threadId)
|
|
1828
2242
|
return threadId;
|
|
1829
|
-
|
|
1830
|
-
|
|
2243
|
+
const conversationIdValue = params.conversationId ?? params.conversation_id;
|
|
2244
|
+
return typeof conversationIdValue === 'string' && conversationIdValue.length > 0
|
|
2245
|
+
? conversationIdValue
|
|
1831
2246
|
: undefined;
|
|
1832
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
|
+
}
|
|
1833
2263
|
clearTrackedApprovalItemsForThread(threadId) {
|
|
1834
2264
|
if (!threadId)
|
|
1835
2265
|
return;
|
|
@@ -1837,6 +2267,73 @@ export class CodexRunner {
|
|
|
1837
2267
|
if (tracked.threadId === threadId)
|
|
1838
2268
|
this.trackedApprovalItems.delete(key);
|
|
1839
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];
|
|
1840
2337
|
}
|
|
1841
2338
|
async handleAppServerRequest(request) {
|
|
1842
2339
|
const params = (request.params || {});
|
|
@@ -1898,41 +2395,46 @@ export class CodexRunner {
|
|
|
1898
2395
|
// workspace, or unattended-trigger policy checks.
|
|
1899
2396
|
if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
|
|
1900
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));
|
|
1901
2407
|
return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1902
2408
|
}
|
|
1903
2409
|
const response = this.toAppServerApprovalResponse(request.method, 'allow', toolInput, false);
|
|
1904
2410
|
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'allow', 'approval', undefined, 'trusted fullaccess execution');
|
|
1905
2411
|
return response;
|
|
1906
2412
|
}
|
|
1907
|
-
//
|
|
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.
|
|
1908
2416
|
const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
|
|
1909
2417
|
if (policyResult?.block) {
|
|
1910
2418
|
logger.info(`[CodexRunner] app-server approval declined by session policy: session=${sessionKey} ` +
|
|
1911
2419
|
`requestId=${request.id ?? '<missing>'} code=${policyResult.policyCode ?? 'session_policy_hook'} ` +
|
|
1912
2420
|
`reason=${policyResult.reason ?? 'policy denied'} executed=false`);
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
1928
|
-
effect: 'operation_skipped',
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
catch (error) {
|
|
1932
|
-
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));
|
|
1933
2435
|
}
|
|
1934
2436
|
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1935
|
-
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy',
|
|
2437
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', policyCode, policyReason);
|
|
1936
2438
|
return response;
|
|
1937
2439
|
}
|
|
1938
2440
|
const summary = this.summarizeAppServerRequest(request.method, params);
|
|
@@ -1941,7 +2443,14 @@ export class CodexRunner {
|
|
|
1941
2443
|
if (!workspacePath) {
|
|
1942
2444
|
logger.warn(`[CodexRunner] approval denied because thread workspace is unknown: method=${request.method} thread=${params.threadId ?? params.conversationId ?? '<missing>'}`);
|
|
1943
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
|
+
});
|
|
1944
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'));
|
|
1945
2454
|
return response;
|
|
1946
2455
|
}
|
|
1947
2456
|
const operationCwd = this.resolvePermissionOperationCwd(params, workspacePath, toolName);
|
|
@@ -1951,8 +2460,9 @@ export class CodexRunner {
|
|
|
1951
2460
|
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
1952
2461
|
|| request.method === 'execCommandApproval';
|
|
1953
2462
|
const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
|
|
2463
|
+
const delegationCarrier = this.delegationCarrierForSession(sessionKey, typeof params.threadId === 'string' ? params.threadId : undefined);
|
|
1954
2464
|
const canonicalEcApproval = isCommandApproval
|
|
1955
|
-
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey))
|
|
2465
|
+
? this.resolveCanonicalEcApproval(toolInput, this.getManagedTempDir(sessionKey), delegationCarrier)
|
|
1956
2466
|
: undefined;
|
|
1957
2467
|
const approvedCommand = canonicalEcApproval?.argv;
|
|
1958
2468
|
const denyInfrastructure = async (policyCode, message) => {
|
|
@@ -1979,19 +2489,32 @@ export class CodexRunner {
|
|
|
1979
2489
|
logger.warn(`[CodexRunner] failed to record infrastructure approval denial: session=${sessionKey} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1980
2490
|
}
|
|
1981
2491
|
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
2492
|
+
this.rememberToolDenial(request, params, {
|
|
2493
|
+
decision: 'deny',
|
|
2494
|
+
decisionSource: 'infrastructure',
|
|
2495
|
+
policyCode,
|
|
2496
|
+
reason: message,
|
|
2497
|
+
});
|
|
1982
2498
|
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode, message, canonicalEcApproval?.parseFailure);
|
|
2499
|
+
setImmediate(() => this.steerPolicyReason(sessionKey, policyCode, message));
|
|
1983
2500
|
return response;
|
|
1984
2501
|
};
|
|
1985
2502
|
if (managedEcIntent && !approvedCommand) {
|
|
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
|
+
}
|
|
1986
2506
|
return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
|
|
1987
2507
|
? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
|
|
1988
2508
|
: 'EC command approval denied because no canonical argv could be derived');
|
|
1989
2509
|
}
|
|
1990
2510
|
try {
|
|
1991
|
-
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;
|
|
1992
2514
|
if (decision !== 'deny' && managedEcIntent && approvedCommand) {
|
|
1993
2515
|
const carrierThreadIds = [
|
|
1994
2516
|
this.activeSessions.get(sessionKey),
|
|
2517
|
+
this.provisionalSessions.get(sessionKey),
|
|
1995
2518
|
typeof params.threadId === 'string' ? params.threadId : undefined,
|
|
1996
2519
|
typeof params.conversationId === 'string' ? params.conversationId : undefined,
|
|
1997
2520
|
];
|
|
@@ -2000,18 +2523,35 @@ export class CodexRunner {
|
|
|
2000
2523
|
// A carrier-bearing thread is managed even if its permission context
|
|
2001
2524
|
// was not wired with an arm callback; it must fail closed here rather
|
|
2002
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
|
+
}
|
|
2003
2531
|
if ((hasDelegationCarrier || canArmDelegation)
|
|
2004
2532
|
&& !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
|
|
2005
2533
|
return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
|
|
2006
2534
|
}
|
|
2007
2535
|
}
|
|
2008
2536
|
const response = this.toAppServerApprovalResponse(request.method, decision, toolInput, unattended);
|
|
2009
|
-
|
|
2010
|
-
? '
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
|
2015
2555
|
: 'approval accepted');
|
|
2016
2556
|
logger.info(`[CodexRunner] app-server approval response id=${request.id} method=${request.method} decision=${decision} response=${JSON.stringify(response)}`);
|
|
2017
2557
|
return response;
|
|
@@ -2024,8 +2564,13 @@ export class CodexRunner {
|
|
|
2024
2564
|
}
|
|
2025
2565
|
logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode, reason, parseFailure) {
|
|
2026
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;
|
|
2027
2570
|
auditCodexApprovalDecision({
|
|
2028
2571
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2572
|
+
callId,
|
|
2573
|
+
correlationId: callId,
|
|
2029
2574
|
method: request.method,
|
|
2030
2575
|
sessionId: sessionKey,
|
|
2031
2576
|
agentAid: context?.selfAid,
|
|
@@ -2041,6 +2586,7 @@ export class CodexRunner {
|
|
|
2041
2586
|
});
|
|
2042
2587
|
logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
|
|
2043
2588
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2589
|
+
...(callId ? { callId, correlationId: callId } : {}),
|
|
2044
2590
|
method: request.method,
|
|
2045
2591
|
sessionId: sessionKey,
|
|
2046
2592
|
toolName,
|
|
@@ -2401,9 +2947,20 @@ export class CodexRunner {
|
|
|
2401
2947
|
if (activeThreadId === candidate)
|
|
2402
2948
|
return sessionKey;
|
|
2403
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;
|
|
2404
2957
|
}
|
|
2405
2958
|
return candidates[0] || 'codex-app-server';
|
|
2406
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
|
+
}
|
|
2407
2964
|
buildPermissionInput(method, params) {
|
|
2408
2965
|
if (method === 'item/permissions/requestApproval') {
|
|
2409
2966
|
return { permissions: params.permissions, cwd: params.cwd, reason: params.reason };
|
|
@@ -2465,6 +3022,12 @@ export class CodexRunner {
|
|
|
2465
3022
|
const threadProjectPath = this.threadProjectPaths.get(threadId);
|
|
2466
3023
|
if (threadProjectPath)
|
|
2467
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);
|
|
2468
3031
|
}
|
|
2469
3032
|
return undefined;
|
|
2470
3033
|
}
|
|
@@ -2595,7 +3158,7 @@ export class CodexRunner {
|
|
|
2595
3158
|
return true;
|
|
2596
3159
|
});
|
|
2597
3160
|
}
|
|
2598
|
-
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) {
|
|
2599
3162
|
const permissionContext = this.permissionContexts.get(sessionKey);
|
|
2600
3163
|
const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
|
|
2601
3164
|
const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
|
|
@@ -2632,6 +3195,8 @@ export class CodexRunner {
|
|
|
2632
3195
|
role: permissionContext?.role,
|
|
2633
3196
|
selfAid: permissionContext?.selfAid,
|
|
2634
3197
|
requestId: typeof requestId === 'string' || typeof requestId === 'number' ? String(requestId) : undefined,
|
|
3198
|
+
callId,
|
|
3199
|
+
correlationId: callId,
|
|
2635
3200
|
taskId: permissionContext?.taskId,
|
|
2636
3201
|
matchedPath,
|
|
2637
3202
|
});
|
|
@@ -2662,6 +3227,7 @@ export class CodexRunner {
|
|
|
2662
3227
|
// distinguishes this explicit empty value from runners without a
|
|
2663
3228
|
// session-temp capability.
|
|
2664
3229
|
managedTempDir: permissionContext?.managedTempDir ?? this.getManagedTempDir(sessionKey) ?? '',
|
|
3230
|
+
delegationCarrier: this.delegationCarrierForSession(sessionKey),
|
|
2665
3231
|
allowProtectedMetadata: false,
|
|
2666
3232
|
selfAid: permissionContext?.selfAid,
|
|
2667
3233
|
channel: permissionContext?.channel,
|
|
@@ -2678,7 +3244,12 @@ export class CodexRunner {
|
|
|
2678
3244
|
if (preflight.policyCode) {
|
|
2679
3245
|
await recordBlockedOperation(preflight.policyCode, toolInput, preflight.message ?? 'tool preflight denied', preflight.matchedPath);
|
|
2680
3246
|
}
|
|
2681
|
-
return
|
|
3247
|
+
return {
|
|
3248
|
+
decision: 'deny',
|
|
3249
|
+
decisionSource: 'policy',
|
|
3250
|
+
policyCode: preflight.policyCode ?? 'tool_preflight_denied',
|
|
3251
|
+
reason: preflight.message ?? 'tool preflight denied',
|
|
3252
|
+
};
|
|
2682
3253
|
}
|
|
2683
3254
|
if (preflight.behavior === 'allow')
|
|
2684
3255
|
return 'allow';
|
|
@@ -2992,12 +3563,124 @@ export class CodexRunner {
|
|
|
2992
3563
|
}
|
|
2993
3564
|
return !state.turnId || !turnId || turnId === state.turnId;
|
|
2994
3565
|
}
|
|
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
|
+
}
|
|
2995
3660
|
async *transformAppServerStream(notifications, sessionId, state, controller, unsubscribe, tempFiles) {
|
|
2996
3661
|
try {
|
|
2997
|
-
|
|
3662
|
+
let sessionIdEmitted = state.committed;
|
|
3663
|
+
if (sessionIdEmitted)
|
|
3664
|
+
yield { type: 'session_id', sessionId: state.threadId };
|
|
2998
3665
|
for await (const notification of notifications) {
|
|
2999
3666
|
if (!this.activeAbortControllers.has(sessionId))
|
|
3000
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
|
+
}
|
|
3001
3684
|
yield* this.mapAppServerNotification(notification, sessionId, state);
|
|
3002
3685
|
}
|
|
3003
3686
|
}
|
|
@@ -3011,6 +3694,11 @@ export class CodexRunner {
|
|
|
3011
3694
|
&& (!state.turnId || activeTurn.turnId === state.turnId)) {
|
|
3012
3695
|
this.activeTurns.delete(sessionId);
|
|
3013
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();
|
|
3014
3702
|
this.cleanupTempFiles(tempFiles);
|
|
3015
3703
|
}
|
|
3016
3704
|
}
|
|
@@ -3078,12 +3766,16 @@ export class CodexRunner {
|
|
|
3078
3766
|
}
|
|
3079
3767
|
case 'turn/completed': {
|
|
3080
3768
|
const turn = params.turn || {};
|
|
3081
|
-
const turnId = turn.id || params.turnId;
|
|
3769
|
+
const turnId = turn.id || params.turnId || state.turnId;
|
|
3082
3770
|
if (turnId && state.completedTurnIds.has(turnId))
|
|
3083
3771
|
break;
|
|
3084
3772
|
if (turnId)
|
|
3085
3773
|
state.completedTurnIds.add(turnId);
|
|
3086
|
-
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);
|
|
3087
3779
|
yield* this.reconcileOpenAppServerToolCalls(state, turnId);
|
|
3088
3780
|
if (turn.status === 'failed' && turn.error?.message) {
|
|
3089
3781
|
if (isRetryableError(new Error(turn.error.message))) {
|
|
@@ -3204,11 +3896,36 @@ export class CodexRunner {
|
|
|
3204
3896
|
};
|
|
3205
3897
|
break;
|
|
3206
3898
|
case 'subAgentActivity':
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
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
|
+
}
|
|
3211
3927
|
break;
|
|
3928
|
+
}
|
|
3212
3929
|
case 'enteredReviewMode':
|
|
3213
3930
|
yield { type: 'task_progress', summary: item.review || 'Review mode entered' };
|
|
3214
3931
|
break;
|
|
@@ -3223,6 +3940,59 @@ export class CodexRunner {
|
|
|
3223
3940
|
*mapAppServerItemCompleted(item, state) {
|
|
3224
3941
|
if (!item)
|
|
3225
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;
|
|
3226
3996
|
switch (item.type) {
|
|
3227
3997
|
case 'agentMessage':
|
|
3228
3998
|
{
|
|
@@ -3239,8 +4009,10 @@ export class CodexRunner {
|
|
|
3239
4009
|
type: 'tool_result',
|
|
3240
4010
|
name: 'Shell',
|
|
3241
4011
|
result: item.aggregatedOutput ?? '',
|
|
3242
|
-
|
|
3243
|
-
|
|
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
|
+
}),
|
|
3244
4016
|
callId: item.id,
|
|
3245
4017
|
durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
|
|
3246
4018
|
};
|
|
@@ -3250,8 +4022,8 @@ export class CodexRunner {
|
|
|
3250
4022
|
type: 'tool_result',
|
|
3251
4023
|
name: `MCP:${item.server}/${item.tool}`,
|
|
3252
4024
|
result: item.result,
|
|
3253
|
-
isError: item.status === 'failed',
|
|
3254
|
-
error: item.error?.message,
|
|
4025
|
+
...(blockedFields ?? { isError: item.status === 'failed' }),
|
|
4026
|
+
...(blockedFields ? {} : { error: item.error?.message }),
|
|
3255
4027
|
...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
|
|
3256
4028
|
callId: item.id,
|
|
3257
4029
|
durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
|
|
@@ -3262,7 +4034,7 @@ export class CodexRunner {
|
|
|
3262
4034
|
type: 'tool_result',
|
|
3263
4035
|
name: item.namespace ? `${item.namespace}:${item.tool}` : item.tool,
|
|
3264
4036
|
result: item.contentItems,
|
|
3265
|
-
isError: item.success === false || item.status === 'failed',
|
|
4037
|
+
...(blockedFields ?? { isError: item.success === false || item.status === 'failed' }),
|
|
3266
4038
|
...(item.error?.code || item.error?.errorCode ? { errorCode: String(item.error.code ?? item.error.errorCode) } : {}),
|
|
3267
4039
|
callId: item.id,
|
|
3268
4040
|
durationMs: typeof item.durationMs === 'number' ? item.durationMs : undefined,
|
|
@@ -3277,12 +4049,12 @@ export class CodexRunner {
|
|
|
3277
4049
|
yield editEvent;
|
|
3278
4050
|
}
|
|
3279
4051
|
}
|
|
3280
|
-
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 };
|
|
3281
4053
|
}
|
|
3282
4054
|
else {
|
|
3283
4055
|
const desc = this.normalizeFileChanges(item.changes).map((change) => this.describeFileChange(change)).join(', ');
|
|
3284
4056
|
yield { type: 'tool_use', name: 'FileChange', input: { description: desc }, callId: item.id };
|
|
3285
|
-
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 };
|
|
3286
4058
|
}
|
|
3287
4059
|
break;
|
|
3288
4060
|
case 'webSearch':
|
|
@@ -3336,6 +4108,34 @@ export class CodexRunner {
|
|
|
3336
4108
|
callId: item.id,
|
|
3337
4109
|
};
|
|
3338
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
|
+
}
|
|
3339
4139
|
}
|
|
3340
4140
|
}
|
|
3341
4141
|
*mapAppServerFileChangePatchUpdated(params, state) {
|
|
@@ -3583,16 +4383,21 @@ export class CodexRunner {
|
|
|
3583
4383
|
for (const [key, controller] of this.activeAbortControllers) {
|
|
3584
4384
|
controller.abort('dispose');
|
|
3585
4385
|
}
|
|
4386
|
+
await Promise.all([...this.activeSubagentTurns.entries()].map(([threadId, active]) => this.interruptAppServerTurn(threadId, active.turnId).catch(() => { })));
|
|
3586
4387
|
this.activeAbortControllers.clear();
|
|
3587
4388
|
this.activeStreams.clear();
|
|
3588
4389
|
this.activeSessions.clear();
|
|
4390
|
+
this.provisionalSessions.clear();
|
|
3589
4391
|
this.activeTurns.clear();
|
|
4392
|
+
this.activeSubagentTurns.clear();
|
|
4393
|
+
this.childThreadSessions.clear();
|
|
3590
4394
|
this.threadProjectPaths.clear();
|
|
3591
4395
|
this.threadExternalToolFingerprints.clear();
|
|
3592
4396
|
this.threadExternalToolBoundaries.clear();
|
|
3593
4397
|
this.trackedApprovalItems.clear();
|
|
3594
4398
|
this.threadDelegationCarriers.clear();
|
|
3595
4399
|
this.pendingInterrupts.clear();
|
|
4400
|
+
this.steeringTails.clear();
|
|
3596
4401
|
this.permissionContexts.clear();
|
|
3597
4402
|
this.chatModes.clear();
|
|
3598
4403
|
this.pruneThreadOperationLocks();
|