Skip to content

Commit 4936513

Browse files
feat: LLM-25394 support Codex persist approval
1 parent 64d3bc2 commit 4936513

22 files changed

Lines changed: 719 additions & 82 deletions

package-lock.json

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ApprovalOptionId.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
export const ApprovalOptionId = {
22
AllowOnce: "allow_once",
3-
AllowAlways: "allow_always",
3+
AllowForSession: "allow_for_session",
4+
AllowPersist: "allow_persist",
5+
AllowCommandPrefixRule: "allow_command_prefix_rule",
6+
ApplyNetworkPolicyAmendment: "apply_network_policy_amendment",
47
RejectOnce: "reject_once",
8+
Cancel: "cancel",
59
} as const;
610

711
export type ApprovalOptionId = typeof ApprovalOptionId[keyof typeof ApprovalOptionId];

src/CodexAcpClient.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {logger} from "./Logger";
2424
import type {
2525
AccountLoginCompletedNotification,
2626
AccountUpdatedNotification,
27+
ConfigEdit,
2728
GetAccountResponse,
2829
ListMcpServerStatusParams,
2930
ListMcpServerStatusResponse,
@@ -322,6 +323,24 @@ export class CodexAcpClient {
322323
return new Set(Object.keys(mcpServers));
323324
}
324325

326+
// Makes an ACP-provided MCP server durable so Codex core can append
327+
// mcp_servers.<server>.tools.<tool>.approval_mode afterward:
328+
// https://github.com/openai/codex/blob/main/codex-rs/app-server/src/config_api.rs
329+
async persistMcpServer(mcpServer: McpServer): Promise<void> {
330+
const edits: ConfigEdit[] = [{
331+
keyPath: `mcp_servers.${mcpServer.name}`,
332+
value: this.createMcpSeverConfig(mcpServer),
333+
mergeStrategy: "upsert",
334+
}];
335+
336+
await this.codexClient.configBatchWrite({
337+
edits,
338+
filePath: null,
339+
expectedVersion: null,
340+
reloadUserConfig: true,
341+
});
342+
}
343+
325344
getModelProvider(): string | null {
326345
return this.gatewayConfig?.modelProvider ?? this.modelProvider;
327346
}

src/CodexAcpServer.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export interface SessionState {
5656
cwd: string;
5757
fastModeEnabled: boolean;
5858
currentModelSupportsFast: boolean;
59-
sessionMcpServers?: Array<string>;
59+
sessionMcpServers: Map<string, acp.McpServer>;
6060
}
6161

6262
interface PendingMcpStartupSession {
@@ -171,7 +171,6 @@ export class CodexAcpServer implements acp.Agent {
171171

172172
const account = await this.getActiveAccount();
173173
const {sessionId, currentModelId, models} = sessionMetadata;
174-
const sessionMcpServers = this.resolveSessionMcpServers(requestedMcpServers, "sessionId" in request);
175174
const currentModel = this.findCurrentModel(models, currentModelId);
176175
const currentModelSupportsFast = modelSupportsFast(currentModel);
177176
const sessionState: SessionState = {
@@ -189,7 +188,7 @@ export class CodexAcpServer implements acp.Agent {
189188
cwd: request.cwd,
190189
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
191190
currentModelSupportsFast: currentModelSupportsFast,
192-
sessionMcpServers: sessionMcpServers,
191+
sessionMcpServers: this.createSessionMcpServers(requestedMcpServers, "sessionId" in request),
193192
}
194193
this.sessions.set(sessionId, sessionState);
195194

@@ -431,7 +430,6 @@ export class CodexAcpServer implements acp.Agent {
431430

432431
const account = await this.getActiveAccount();
433432
const {sessionId, currentModelId, models, thread} = sessionMetadata;
434-
const sessionMcpServers = this.resolveSessionMcpServers(requestedMcpServers, true);
435433
const currentModel = this.findCurrentModel(models, currentModelId);
436434
const currentModelSupportsFast = modelSupportsFast(currentModel);
437435
const sessionState: SessionState = {
@@ -449,7 +447,7 @@ export class CodexAcpServer implements acp.Agent {
449447
cwd: request.cwd,
450448
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
451449
currentModelSupportsFast: currentModelSupportsFast,
452-
sessionMcpServers: sessionMcpServers,
450+
sessionMcpServers: this.createSessionMcpServers(requestedMcpServers, true),
453451
};
454452
this.sessions.set(sessionId, sessionState);
455453

@@ -703,24 +701,23 @@ export class CodexAcpServer implements acp.Agent {
703701
return sessionState;
704702
}
705703

706-
private resolveSessionMcpServers(
704+
private createSessionMcpServers(
707705
mcpServers: Array<acp.McpServer>,
708706
recoverFromStartup: boolean,
709-
): Array<string> {
707+
): Map<string, acp.McpServer> {
710708
// Explicit MCP servers from the request are the primary source of truth for the session.
711-
const requestedServerNames = getRequestedMcpServerNames(mcpServers);
712-
if (requestedServerNames.length > 0) {
713-
return requestedServerNames;
709+
if (mcpServers.length > 0) {
710+
return new Map(mcpServers.map(server => [server.name, server]));
714711
}
715712
// Fresh sessions without MCP config should not inherit any session MCP state.
716713
if (!recoverFromStartup) {
717-
return [];
714+
return new Map();
718715
}
719716
// Without a thread-scoped startup completion event, loadSession/resumeSession can no longer
720717
// recover omitted session MCP server names. Treat the session set as unknown unless ACP
721718
// explicitly provided mcpServers in the request.
722719
logger.log("Skipping MCP server recovery for load/resume without explicit mcpServers");
723-
return [];
720+
return new Map();
724721
}
725722

726723
private publishMcpStartupStatusAsync(sessionId: string): void {
@@ -781,7 +778,7 @@ export class CodexAcpServer implements acp.Agent {
781778
try {
782779
const eventHandler = new CodexEventHandler(this.connection, sessionState);
783780
const approvalHandler = new CodexApprovalHandler(this.connection, sessionState);
784-
const elicitationHandler = new CodexElicitationHandler(this.connection, sessionState);
781+
const elicitationHandler = new CodexElicitationHandler(this.connection, sessionState, this.codexAcpClient);
785782
await this.codexAcpClient.subscribeToSessionEvents(params.sessionId,
786783
(event) => {
787784
elicitationHandler.handleNotification(event);
@@ -934,7 +931,3 @@ export class CodexAcpServer implements acp.Agent {
934931
}
935932
}
936933
}
937-
938-
function getRequestedMcpServerNames(mcpServers: Array<acp.McpServer>): Array<string> {
939-
return Array.from(new Set(mcpServers.map(server => server.name)));
940-
}

src/CodexAppServerClient.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
AccountLoginCompletedNotification, AccountUpdatedNotification,
1010
ConfigReadParams,
1111
ConfigReadResponse,
12+
ConfigBatchWriteParams,
13+
ConfigWriteResponse,
1214
GetAccountParams,
1315
GetAccountResponse,
1416
ListMcpServerStatusParams,
@@ -221,6 +223,10 @@ export class CodexAppServerClient {
221223
return await this.sendRequest({ method: "config/read", params: params });
222224
}
223225

226+
async configBatchWrite(params: ConfigBatchWriteParams): Promise<ConfigWriteResponse> {
227+
return await this.sendRequest({ method: "config/batchWrite", params });
228+
}
229+
224230
getMcpServerStartupVersion(): number {
225231
return this.mcpServerStartupVersion;
226232
}

0 commit comments

Comments
 (0)