diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 7040057f..7973ac69 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -32,6 +32,8 @@ import {sanitizeMcpServerName} from "./McpServerName"; import type { AccountLoginCompletedNotification, AccountUpdatedNotification, + ApprovalsReviewer, + AskForApproval, GetAccountRateLimitsResponse, GetAccountResponse, ListMcpServerStatusResponse, @@ -549,6 +551,9 @@ export class CodexAcpClient { modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, additionalDirectories, + approvalPolicy: response.approvalPolicy, + approvalsReviewer: response.approvalsReviewer, + sandboxPolicy: response.sandbox, } } @@ -600,6 +605,9 @@ export class CodexAcpClient { currentServiceTier: response.serviceTier as ServiceTier ?? null, thread, additionalDirectories, + approvalPolicy: response.approvalPolicy, + approvalsReviewer: response.approvalsReviewer, + sandboxPolicy: response.sandbox, }; } @@ -630,6 +638,9 @@ export class CodexAcpClient { modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, additionalDirectories, + approvalPolicy: response.approvalPolicy, + approvalsReviewer: response.approvalsReviewer, + sandboxPolicy: response.sandbox, }; } @@ -935,7 +946,7 @@ export class CodexAcpClient { async sendPrompt( request: acp.PromptRequest, - agentMode: AgentMode, + turnPermissionSettings: {approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy}, modelId: ModelId, serviceTier: ServiceTier | null, disableSummary: boolean, @@ -953,9 +964,9 @@ export class CodexAcpClient { return await this.codexClient.runTurn({ threadId: request.sessionId, input: input, - approvalPolicy: agentMode.approvalPolicy, - approvalsReviewer: agentMode.approvalsReviewer, - sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories), + approvalPolicy: turnPermissionSettings.approvalPolicy, + approvalsReviewer: turnPermissionSettings.approvalsReviewer, + sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(turnPermissionSettings.sandboxPolicy, additionalDirectories), summary: disableSummary ? "none" : "auto", effort: effort, model: modelId.model, diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 4ff0e1b4..8211d70a 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -26,8 +26,11 @@ import type {InputModality, ReasoningEffort, ServerNotification} from "./app-ser import type { Account, AccountUpdatedNotification, + ApprovalsReviewer, + AskForApproval, Model, ReasoningEffortOption, + SandboxPolicy, Thread, ThreadGoal, ThreadItem, @@ -160,6 +163,9 @@ export interface SessionState { supportedReasoningEfforts: Array, supportedInputModalities: Array, agentMode: AgentMode, + resolvedApprovalPolicy: AskForApproval, + resolvedApprovalsReviewer: ApprovalsReviewer, + resolvedSandboxPolicy: SandboxPolicy, collaborationMode: ModeKind, currentTurnId: string | null; lastTokenUsage: TokenCount | null; @@ -666,6 +672,9 @@ export class CodexAcpServer { supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [], supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"], agentMode: AgentMode.getInitialAgentMode(), + resolvedApprovalPolicy: sessionMetadata.approvalPolicy ?? AgentMode.Agent.approvalPolicy, + resolvedApprovalsReviewer: sessionMetadata.approvalsReviewer ?? AgentMode.Agent.approvalsReviewer, + resolvedSandboxPolicy: sessionMetadata.sandboxPolicy ?? AgentMode.Agent.sandboxPolicy, collaborationMode: sessionMetadata.collaborationMode, currentTurnId: null, lastTokenUsage: null, @@ -1923,6 +1932,9 @@ export class CodexAcpServer { supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [], supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"], agentMode: AgentMode.getInitialAgentMode(), + resolvedApprovalPolicy: sessionMetadata.approvalPolicy ?? AgentMode.Agent.approvalPolicy, + resolvedApprovalsReviewer: sessionMetadata.approvalsReviewer ?? AgentMode.Agent.approvalsReviewer, + resolvedSandboxPolicy: sessionMetadata.sandboxPolicy ?? AgentMode.Agent.sandboxPolicy, collaborationMode: sessionMetadata.collaborationMode, currentTurnId: null, lastTokenUsage: null, @@ -2942,6 +2954,7 @@ export class CodexAcpServer { throw RequestError.invalidRequest("The current model does not support image input"); } const agentMode = sessionState.agentMode; + const turnPermissionSettings = this.createTurnPermissionSettings(sessionState, agentMode); const serviceTier = resolveFastServiceTier( sessionState.fastModeEnabled, sessionState.currentModelSupportsFast, @@ -2951,7 +2964,7 @@ export class CodexAcpServer { const sendPromptPromise = this.runWithProcessCheck( () => this.codexAcpClient.sendPrompt( effectiveParams, - agentMode, + turnPermissionSettings, modelId, serviceTier, disableSummary, @@ -3245,6 +3258,24 @@ export class CodexAcpServer { } } + private createTurnPermissionSettings( + sessionState: SessionState, + agentMode: AgentMode, + ): {approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy} { + if (agentMode.id !== AgentMode.DEFAULT_AGENT_MODE.id) { + return { + approvalPolicy: agentMode.approvalPolicy, + approvalsReviewer: agentMode.approvalsReviewer, + sandboxPolicy: agentMode.sandboxPolicy, + }; + } + return { + approvalPolicy: sessionState.resolvedApprovalPolicy, + approvalsReviewer: sessionState.resolvedApprovalsReviewer, + sandboxPolicy: sessionState.resolvedSandboxPolicy, + }; + } + private cancelledPromptResponse(sessionState: SessionState): acp.PromptResponse { return { stopReason: "cancelled", diff --git a/src/SessionFork.ts b/src/SessionFork.ts index 1a533df7..de38f9cd 100644 --- a/src/SessionFork.ts +++ b/src/SessionFork.ts @@ -51,6 +51,9 @@ export async function forkSession( modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, additionalDirectories, + approvalPolicy: response.approvalPolicy, + approvalsReviewer: response.approvalsReviewer, + sandboxPolicy: response.sandbox, }; } diff --git a/src/SessionMetadata.ts b/src/SessionMetadata.ts index 50562058..4f227713 100644 --- a/src/SessionMetadata.ts +++ b/src/SessionMetadata.ts @@ -1,6 +1,6 @@ import type {ModeKind} from "./app-server/ModeKind"; import type {ServiceTier} from "./app-server/ServiceTier"; -import type {Model, Thread} from "./app-server/v2"; +import type {ApprovalsReviewer, AskForApproval, Model, SandboxPolicy, Thread} from "./app-server/v2"; export type SessionMetadata = { sessionId: string, @@ -10,6 +10,9 @@ export type SessionMetadata = { modelProvider?: string | null, currentServiceTier?: ServiceTier | null, additionalDirectories: string[], + approvalPolicy?: AskForApproval, + approvalsReviewer?: ApprovalsReviewer, + sandboxPolicy?: SandboxPolicy, } export type SessionMetadataWithThread = SessionMetadata & { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index fe51f160..8721c11c 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -9,6 +9,7 @@ import { createTestFixture, createTestModel, createTestSessionState, + setupPromptTestSession, type TestFixture } from "../acp-test-utils"; import type {ServerNotification} from "../../app-server"; @@ -1064,6 +1065,67 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(listSkillsSpy.mock.invocationCallOrder[0]!).toBeLessThan(turnStartSpy.mock.invocationCallOrder[0]!); }); + it('preserves resolved default prompt permissions while adding ACP directories', async () => { + const {mockFixture, turnStartSpy} = setupPromptTestSession({ + sessionId: "session-id", + cwd: "/workspace", + additionalDirectories: ["/workspace/extra"], + agentMode: AgentMode.DEFAULT_AGENT_MODE, + resolvedApprovalPolicy: "on-request", + resolvedApprovalsReviewer: "user", + resolvedSandboxPolicy: { + type: "workspaceWrite", + writableRoots: ["/shared-cache"], + networkAccess: true, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + }); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{type: "text", text: "Hello"}], + }); + + expect(turnStartSpy.mock.calls[0]![0]).toMatchObject({ + approvalPolicy: "on-request", + approvalsReviewer: "user", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: ["/shared-cache", "/workspace/extra"], + networkAccess: true, + }, + }); + }); + + it('uses explicit non-default mode prompt permissions instead of resolved defaults', async () => { + const {mockFixture, turnStartSpy} = setupPromptTestSession({ + sessionId: "session-id", + cwd: "/workspace", + agentMode: AgentMode.AgentFullAccess, + resolvedApprovalPolicy: "on-request", + resolvedApprovalsReviewer: "user", + resolvedSandboxPolicy: { + type: "workspaceWrite", + writableRoots: ["/shared-cache"], + networkAccess: true, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + }); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{type: "text", text: "Hello"}], + }); + + expect(turnStartSpy.mock.calls[0]![0]).toMatchObject({ + approvalPolicy: "never", + approvalsReviewer: "user", + sandboxPolicy: {type: "dangerFullAccess"}, + }); + }); + it('applies ACP additional directories to turn skill discovery and sandbox policy', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent(); diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index f358664c..ee4d3265 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -414,6 +414,9 @@ export function createTestSessionState(overrides?: Partial): Sessi supportedReasoningEfforts: [], supportedInputModalities: ["text", "image"], agentMode: AgentMode.DEFAULT_AGENT_MODE, + resolvedApprovalPolicy: "on-request", + resolvedApprovalsReviewer: "auto_review", + resolvedSandboxPolicy: AgentMode.DEFAULT_AGENT_MODE.sandboxPolicy, collaborationMode: DEFAULT_COLLABORATION_MODE, fastModeEnabled: false, currentModelSupportsFast: false,