Fix permission gating fallback for Codex MCP

This commit is contained in:
Mohamed Boudra
2025-12-24 18:49:29 +07:00
parent bcd117270e
commit 5b971dba69
2 changed files with 162 additions and 47 deletions
@@ -307,6 +307,8 @@ class CodexMcpAgentSession implements AgentSession {
private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
private pendingPermissions = new Map<string, AgentPermissionRequest>();
private pendingPermissionHandlers = new Map<string, PendingPermission>();
private pendingToolEvents = new Map<string, Array<() => void>>();
private resolvedPermissionRequests = new Set<string>();
private eventQueue: Pushable<AgentStreamEvent | ProviderEvent> | null = null;
private currentAbortController: AbortController | null = null;
private historyPending = false;
@@ -550,6 +552,7 @@ class CodexMcpAgentSession implements AgentSession {
}
this.pendingPermissionHandlers.delete(requestId);
this.pendingPermissions.delete(requestId);
this.resolvedPermissionRequests.add(requestId);
const status = response.behavior === "allow" ? "granted" : "denied";
this.emitEvent({
@@ -580,6 +583,7 @@ class CodexMcpAgentSession implements AgentSession {
? "abort"
: "denied";
pending.resolve({ decision, reason: response.message });
this.flushQueuedToolEvents(requestId, response.behavior === "allow");
}
describePersistence(): AgentPersistenceHandle | null {
@@ -608,6 +612,8 @@ class CodexMcpAgentSession implements AgentSession {
}
this.pendingPermissionHandlers.clear();
this.pendingPermissions.clear();
this.pendingToolEvents.clear();
this.resolvedPermissionRequests.clear();
this.eventQueue?.end();
this.eventQueue = null;
@@ -776,6 +782,97 @@ class CodexMcpAgentSession implements AgentSession {
});
}
private getEffectiveApprovalPolicy(): string {
const modeId = this.currentMode ?? this.config.modeId ?? DEFAULT_CODEX_MODE_ID;
const preset = MODE_PRESETS[modeId] ?? MODE_PRESETS[DEFAULT_CODEX_MODE_ID];
return this.config.approvalPolicy ?? preset.approvalPolicy;
}
private shouldGatePermissions(): boolean {
const approvalPolicy = this.getEffectiveApprovalPolicy();
return approvalPolicy === "on-request" || approvalPolicy === "untrusted";
}
private queuePermissionGatedEvent(
callId: string | undefined,
event: Record<string, unknown>,
emitEvent: () => void
): boolean {
if (!this.shouldGatePermissions()) {
return false;
}
if (!callId) {
return false;
}
const requestId = `permission-${callId}`;
if (this.resolvedPermissionRequests.has(requestId)) {
return false;
}
if (!this.pendingPermissions.has(requestId) && !this.pendingPermissionHandlers.has(requestId)) {
const permission = this.buildPermissionRequest(event);
if (!permission) {
return false;
}
this.pendingPermissions.set(permission.id, permission);
this.pendingPermissionHandlers.set(permission.id, {
request: permission,
resolve: () => {},
reject: () => {},
});
this.emitPermissionRequested(permission);
}
this.queueToolEvent(requestId, emitEvent);
return true;
}
private ensurePermissionRequestFromEvent(event: Record<string, unknown>): void {
const callId = normalizeCallId(event.call_id as string | undefined);
if (!callId) {
return;
}
const requestId = `permission-${callId}`;
if (this.resolvedPermissionRequests.has(requestId)) {
return;
}
if (this.pendingPermissions.has(requestId) || this.pendingPermissionHandlers.has(requestId)) {
return;
}
const permission = this.buildPermissionRequest(event);
if (!permission) {
return;
}
this.pendingPermissions.set(permission.id, permission);
this.pendingPermissionHandlers.set(permission.id, {
request: permission,
resolve: () => {},
reject: () => {},
});
this.emitPermissionRequested(permission);
}
private queueToolEvent(requestId: string, emitEvent: () => void): void {
const queued = this.pendingToolEvents.get(requestId);
if (queued) {
queued.push(emitEvent);
return;
}
this.pendingToolEvents.set(requestId, [emitEvent]);
}
private flushQueuedToolEvents(requestId: string, allow: boolean): void {
const queued = this.pendingToolEvents.get(requestId);
if (!queued) {
return;
}
this.pendingToolEvents.delete(requestId);
if (!allow) {
return;
}
for (const emitEvent of queued) {
emitEvent();
}
}
private recordHistory(item: AgentTimelineItem): void {
if (this.sessionId) {
const history = SESSION_HISTORY.get(this.sessionId) ?? [];
@@ -929,19 +1026,25 @@ class CodexMcpAgentSession implements AgentSession {
const callId = normalizeCallId((event as { call_id?: string }).call_id);
const command = (event as { command?: unknown }).command;
const cwd = (event as { cwd?: string }).cwd;
this.emitEvent({
type: "timeline",
provider: "codex-mcp",
item: createToolCallTimelineItem({
server: "command",
tool: "shell",
status: "running",
callId,
displayName: buildCommandDisplayName(command),
kind: "execute",
input: { command, cwd },
}),
});
const emitEvent = () => {
this.emitEvent({
type: "timeline",
provider: "codex-mcp",
item: createToolCallTimelineItem({
server: "command",
tool: "shell",
status: "running",
callId,
displayName: buildCommandDisplayName(command),
kind: "execute",
input: { command, cwd },
}),
});
};
if (this.queuePermissionGatedEvent(callId, event as Record<string, unknown>, emitEvent)) {
break;
}
emitEvent();
break;
}
case "exec_command_end": {
@@ -965,29 +1068,41 @@ class CodexMcpAgentSession implements AgentSession {
cwd,
}
: undefined;
if (typeof exitCode === "number" && exitCode !== 0) {
this.turnState && (this.turnState.sawError = true);
}
this.emitEvent({
type: "timeline",
provider: "codex-mcp",
item: createToolCallTimelineItem({
server: "command",
tool: "shell",
status: exitCode && exitCode !== 0 ? "failed" : "completed",
callId,
displayName: buildCommandDisplayName(command),
kind: "execute",
input: { command, cwd },
output: structuredOutput,
}),
});
if (typeof exitCode === "number" && exitCode !== 0) {
const emitEvent = () => {
if (typeof exitCode === "number" && exitCode !== 0) {
this.turnState && (this.turnState.sawError = true);
}
this.emitEvent({
type: "timeline",
provider: "codex-mcp",
item: { type: "error", message: `Command failed with exit code ${exitCode}` },
item: createToolCallTimelineItem({
server: "command",
tool: "shell",
status: exitCode && exitCode !== 0 ? "failed" : "completed",
callId,
displayName: buildCommandDisplayName(command),
kind: "execute",
input: { command, cwd },
output: structuredOutput,
}),
});
if (typeof exitCode === "number" && exitCode !== 0) {
this.emitEvent({
type: "timeline",
provider: "codex-mcp",
item: { type: "error", message: `Command failed with exit code ${exitCode}` },
});
}
};
if (this.queuePermissionGatedEvent(callId, event as Record<string, unknown>, emitEvent)) {
break;
}
emitEvent();
break;
}
case "exec_approval_request": {
if (this.shouldGatePermissions()) {
this.ensurePermissionRequestFromEvent(event as Record<string, unknown>);
}
break;
}