fix(codex): preserve plan approval in yolo (#1420)

This commit is contained in:
SSU-WEI HUANG
2026-08-10 10:44:41 +08:00
committed by GitHub
parent b744414241
commit e6e229c068
6 changed files with 53 additions and 27 deletions
+6 -5
View File
@@ -399,13 +399,13 @@ describe('appServerConfig', () => {
expect(params.collaborationMode).toBeUndefined();
});
it('puts collaboration mode in turn params with model settings', () => {
it('keeps yolo access while using Codex built-in plan instructions', () => {
const params = buildTurnStartParams({
threadId: 'thread-1',
message: 'hello',
cwd: '/workspace/project',
mode: {
permissionMode: 'default',
permissionMode: 'yolo',
model: 'o3',
modelReasoningEffort: 'high',
collaborationMode: 'plan'
@@ -417,13 +417,14 @@ describe('appServerConfig', () => {
settings: {
model: 'o3',
reasoning_effort: 'high',
developer_instructions: withCollaborationInstructions(codexSystemPrompt)
developer_instructions: null
}
});
expect(params.sandboxPolicy).toEqual({ type: 'dangerFullAccess' });
expect(params.model).toBeUndefined();
});
it('carries custom developer instructions into collaboration mode settings', () => {
it('does not override Codex built-in plan instructions', () => {
const params = buildTurnStartParams({
threadId: 'thread-1',
message: 'hello',
@@ -436,7 +437,7 @@ describe('appServerConfig', () => {
mode: 'plan',
settings: {
model: 'o3',
developer_instructions: withCollaborationInstructions(`${codexSystemPrompt}\n\nOnly respond in Chinese.`)
developer_instructions: null
}
});
expect(params.collaborationMode?.settings).not.toHaveProperty('reasoning_effort');
+3 -2
View File
@@ -298,13 +298,14 @@ export function buildTurnStartParams(args: {
if (!model) {
throw new Error(`Collaboration mode '${collaborationMode}' requires a resolved model`);
}
const { developerInstructions } = resolveInstructions(args);
params.collaborationMode = {
mode: collaborationMode,
settings: {
model,
...(modelReasoningEffort !== undefined ? { reasoning_effort: modelReasoningEffort } : {}),
developer_instructions: appendCollaborationInstructions(developerInstructions, args.mode?.proactiveMultiAgent)
developer_instructions: collaborationMode === 'plan'
? null
: appendCollaborationInstructions(resolveInstructions(args).developerInstructions, args.mode?.proactiveMultiAgent)
}
};
} else if (model) {
+24 -2
View File
@@ -7,7 +7,10 @@ type FakeAgentState = {
completedRequests: Record<string, unknown>;
};
function createHarness(mode: 'default' | 'read-only' | 'safe-yolo' | 'yolo') {
function createHarness(
mode: 'default' | 'read-only' | 'safe-yolo' | 'yolo',
collaborationMode?: 'default' | 'plan'
) {
let agentState: FakeAgentState = {
requests: {},
completedRequests: {}
@@ -25,7 +28,9 @@ function createHarness(mode: 'default' | 'read-only' | 'safe-yolo' | 'yolo') {
}
} as unknown as ApiSessionClient;
const handler = new CodexPermissionHandler(session, () => mode);
const handler = new CodexPermissionHandler(session, () => mode, {
getCollaborationMode: () => collaborationMode
});
return {
handler,
@@ -69,6 +74,23 @@ describe('CodexPermissionHandler', () => {
});
});
it('keeps plan exit pending in yolo until the user approves it', async () => {
const { handler, rpcHandlers, getAgentState } = createHarness('yolo', 'plan');
const resultPromise = handler.handleToolCall('plan-exit', 'exit_plan_mode', { plan: '1. Implement' });
expect(getAgentState().requests).toMatchObject({
'plan-exit': { tool: 'exit_plan_mode' }
});
await rpcHandlers.get('permission')?.({
id: 'plan-exit',
approved: true,
decision: 'approved'
});
await expect(resultPromise).resolves.toMatchObject({ decision: 'approved' });
});
it('auto-approves safe-yolo requests once', async () => {
const { handler, getAgentState } = createHarness('safe-yolo');
+6 -1
View File
@@ -35,6 +35,7 @@ type UserInputResult = {
type PermissionResult = ToolPermissionResult | UserInputResult;
type CodexPermissionHandlerOptions = {
getCollaborationMode?: () => 'default' | 'plan' | undefined;
onRequest?: (request: { id: string; toolName: string; input: unknown }) => void;
onComplete?: (result: {
id: string;
@@ -110,7 +111,11 @@ export class CodexPermissionHandler extends BasePermissionHandler<PermissionResp
input: unknown
): Promise<ToolPermissionResult> {
const mode = this.getPermissionMode() ?? 'default';
const autoDecision = this.resolveAutoApprovalDecision(mode, toolName, toolCallId);
const requiresPlanApproval = this.options?.getCollaborationMode?.() === 'plan'
&& (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode');
const autoDecision = requiresPlanApproval
? null
: this.resolveAutoApprovalDecision(mode, toolName, toolCallId);
if (autoDecision) {
return Promise.resolve(this.completeAutoApproval(toolCallId, toolName, input, autoDecision));
}