From 7b30291cc435cafce683baa0deb6b1dc13820177 Mon Sep 17 00:00:00 2001 From: Earl Co Date: Sun, 24 May 2026 04:52:19 +0200 Subject: [PATCH] fix(codex): honor yolo app-server approval policy --- .../src/app-server/approval-bridge.test.ts | 57 +++++++++++++++++++ .../codex/src/app-server/approval-bridge.ts | 13 +++++ .../codex/src/app-server/config.test.ts | 25 +++++++- extensions/codex/src/app-server/config.ts | 6 ++ .../codex/src/app-server/run-attempt.test.ts | 1 + .../codex/src/app-server/run-attempt.ts | 4 ++ .../src/app-server/side-question.test.ts | 1 + .../codex/src/app-server/side-question.ts | 14 +++-- 8 files changed, 116 insertions(+), 5 deletions(-) diff --git a/extensions/codex/src/app-server/approval-bridge.test.ts b/extensions/codex/src/app-server/approval-bridge.test.ts index dacdf75cef4c..457e02c74a80 100644 --- a/extensions/codex/src/app-server/approval-bridge.test.ts +++ b/extensions/codex/src/app-server/approval-bridge.test.ts @@ -110,6 +110,63 @@ describe("Codex app-server approval bridge", () => { })); }); + it("auto-accepts app-server command approvals in yolo mode without opening plugin approvals", async () => { + const params = createParams(); + + const result = await handleCodexAppServerApprovalRequest({ + method: "item/commandExecution/requestApproval", + requestParams: { + threadId: "thread-1", + turnId: "turn-1", + itemId: "cmd-yolo", + command: "/bin/bash -lc 'node -v'", + }, + paramsForRun: params, + threadId: "thread-1", + turnId: "turn-1", + autoApprove: true, + }); + + expect(result).toEqual({ decision: "acceptForSession" }); + expect(mockCallGatewayTool).not.toHaveBeenCalled(); + expect(mockRunBeforeToolCallHook).toHaveBeenCalledWith( + expect.objectContaining({ + toolName: "exec", + approvalMode: "report", + }), + ); + findApprovalEvent(params, { + status: "approved", + message: "Codex app-server approval auto-approved by runtime policy.", + }); + }); + + it("auto-accepts app-server file approvals in yolo mode without opening plugin approvals", async () => { + const params = createParams(); + + const result = await handleCodexAppServerApprovalRequest({ + method: "item/fileChange/requestApproval", + requestParams: { + threadId: "thread-1", + turnId: "turn-1", + itemId: "patch-yolo", + reason: "needs write access", + }, + paramsForRun: params, + threadId: "thread-1", + turnId: "turn-1", + autoApprove: true, + }); + + expect(result).toEqual({ decision: "acceptForSession" }); + expect(mockCallGatewayTool).not.toHaveBeenCalled(); + findApprovalEvent(params, { + status: "approved", + reason: "needs write access", + message: "Codex app-server approval auto-approved by runtime policy.", + }); + }); + it("routes command approvals through plugin approvals and accepts allowed commands", async () => { const params = createParams(); mockCallGatewayTool diff --git a/extensions/codex/src/app-server/approval-bridge.ts b/extensions/codex/src/app-server/approval-bridge.ts index 54c5044f8817..5f2641aaa27e 100644 --- a/extensions/codex/src/app-server/approval-bridge.ts +++ b/extensions/codex/src/app-server/approval-bridge.ts @@ -60,6 +60,7 @@ export async function handleCodexAppServerApprovalRequest(params: { threadId: string; turnId: string; nativeHookRelay?: Pick; + autoApprove?: boolean; signal?: AbortSignal; }): Promise { const requestParams = isJsonObject(params.requestParams) ? params.requestParams : undefined; @@ -97,6 +98,18 @@ export async function handleCodexAppServerApprovalRequest(params: { }); return buildApprovalResponse(params.method, context.requestParams, "denied"); } + if (params.autoApprove === true) { + emitApprovalEvent(params.paramsForRun, { + phase: "resolved", + kind: context.kind, + status: "approved", + title: context.title, + ...context.eventDetails, + ...approvalEventScope(params.method, "approved-session"), + message: "Codex app-server approval auto-approved by runtime policy.", + }); + return buildApprovalResponse(params.method, context.requestParams, "approved-session"); + } const requestResult = await requestPluginApproval({ paramsForRun: params.paramsForRun, title: context.title, diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts index cb7d5e0ce425..158590d66b0f 100644 --- a/extensions/codex/src/app-server/config.test.ts +++ b/extensions/codex/src/app-server/config.test.ts @@ -11,6 +11,7 @@ import { resolveCodexAppServerRuntimeOptions, resolveCodexComputerUseConfig, resolveCodexPluginsPolicy, + shouldAutoApproveCodexAppServerApprovals, } from "./config.js"; type RuntimeOptionsParams = NonNullable[0]>; @@ -56,6 +57,27 @@ function expectUiHintLabel(manifest: { uiHints: Record }, key: } describe("Codex app-server config", () => { + it("only auto-approves app-server approvals for full yolo runtime policy", () => { + expect( + shouldAutoApproveCodexAppServerApprovals({ + approvalPolicy: "never", + sandbox: "danger-full-access", + }), + ).toBe(true); + expect( + shouldAutoApproveCodexAppServerApprovals({ + approvalPolicy: "never", + sandbox: "workspace-write", + }), + ).toBe(false); + expect( + shouldAutoApproveCodexAppServerApprovals({ + approvalPolicy: "on-request", + sandbox: "danger-full-access", + }), + ).toBe(false); + }); + it("parses typed plugin config before falling back to environment knobs", () => { const runtime = resolveRuntimeForTest({ pluginConfig: { @@ -624,7 +646,8 @@ allowed_sandbox_modes = ["read-only", "workspace-write"] resolveRuntimeForTest({ pluginConfig: { appServer: { - command: "node C:\\Users\\me\\.openclaw\\npm\\node_modules\\@openai\\codex\\bin\\codex.js", + command: + "node C:\\Users\\me\\.openclaw\\npm\\node_modules\\@openai\\codex\\bin\\codex.js", }, }, }), diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 45d7f5fd4050..36c533b70ea2 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -148,6 +148,12 @@ export type CodexPluginConfig = { }; }; +export function shouldAutoApproveCodexAppServerApprovals( + appServer: Pick, +): boolean { + return appServer.approvalPolicy === "never" && appServer.sandbox === "danger-full-access"; +} + export const CODEX_APP_SERVER_CONFIG_KEYS = [ "mode", "transport", diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 373dc6ac3bd4..31ca3330e9c4 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -6978,6 +6978,7 @@ describe("runCodexAppServerAttempt", () => { }, threadId: "thread-1", turnId: "turn-1", + autoApprove: true, }); expect(approvalArgs?.nativeHookRelay).toMatchObject({ relayId, diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 8a782d736d5e..17c018e4fb98 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -84,6 +84,7 @@ import { resolveCodexComputerUseConfig, resolveCodexPluginsPolicy, resolveCodexAppServerRuntimeOptions, + shouldAutoApproveCodexAppServerApprovals, withMcpElicitationsApprovalPolicy, type CodexAppServerRuntimeOptions, type CodexPluginConfig, @@ -2478,6 +2479,7 @@ export async function runCodexAppServerAttempt( threadId: thread.threadId, turnId, nativeHookRelay, + autoApprove: shouldAutoApproveCodexAppServerApprovals(appServer), signal: runAbortController.signal, }); } @@ -5665,6 +5667,7 @@ function handleApprovalRequest(params: { threadId: string; turnId: string; nativeHookRelay?: NativeHookRelayRegistrationHandle; + autoApprove?: boolean; signal?: AbortSignal; }): Promise { return handleCodexAppServerApprovalRequest({ @@ -5674,6 +5677,7 @@ function handleApprovalRequest(params: { threadId: params.threadId, turnId: params.turnId, nativeHookRelay: params.nativeHookRelay, + autoApprove: params.autoApprove, signal: params.signal, }); } diff --git a/extensions/codex/src/app-server/side-question.test.ts b/extensions/codex/src/app-server/side-question.test.ts index 622d03134f22..f00a11f27c44 100644 --- a/extensions/codex/src/app-server/side-question.test.ts +++ b/extensions/codex/src/app-server/side-question.test.ts @@ -676,6 +676,7 @@ describe("runCodexAppServerSideQuestion", () => { }, threadId: "side-thread", turnId: "turn-1", + autoApprove: false, paramsForRun: { messageChannel: "discord", messageProvider: "discord-voice", diff --git a/extensions/codex/src/app-server/side-question.ts b/extensions/codex/src/app-server/side-question.ts index e194600b935e..29fc9debb470 100644 --- a/extensions/codex/src/app-server/side-question.ts +++ b/extensions/codex/src/app-server/side-question.ts @@ -19,7 +19,12 @@ import { import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js"; import { refreshCodexAppServerAuthTokens } from "./auth-bridge.js"; import { isCodexAppServerApprovalRequest, type CodexAppServerClient } from "./client.js"; -import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js"; +import { + readCodexPluginConfig, + resolveCodexAppServerRuntimeOptions, + shouldAutoApproveCodexAppServerApprovals, + type CodexAppServerRuntimeOptions, +} from "./config.js"; import { emitDynamicToolErrorDiagnostic, emitDynamicToolStartedDiagnostic, @@ -166,6 +171,8 @@ export async function runCodexAppServerSideQuestion( try { const cwd = binding.cwd || params.workspaceDir || process.cwd(); const sideRunParams = buildSideRunAttemptParams(params, { cwd, authProfileId }); + const approvalPolicy = binding.approvalPolicy ?? appServer.approvalPolicy; + const sandbox = binding.sandbox ?? appServer.sandbox; const { sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, config: params.cfg, @@ -212,6 +219,7 @@ export async function runCodexAppServerSideQuestion( threadId: childThreadId, turnId, nativeHookRelay, + autoApprove: shouldAutoApproveCodexAppServerApprovals({ approvalPolicy, sandbox }), signal: runAbortController.signal, }); } @@ -259,8 +267,6 @@ export async function runCodexAppServerSideQuestion( } }); - const approvalPolicy = binding.approvalPolicy ?? appServer.approvalPolicy; - const sandbox = binding.sandbox ?? appServer.sandbox; const serviceTier = binding.serviceTier ?? appServer.serviceTier; const nativeHookRelayEvents = resolveCodexSideNativeHookRelayEvents({ configuredEvents: options.nativeHookRelay?.events, @@ -401,7 +407,7 @@ export async function runCodexAppServerSideQuestion( function resolveCodexSideNativeHookRelayEvents(params: { configuredEvents?: readonly NativeHookRelayEvent[]; - approvalPolicy: ReturnType["approvalPolicy"]; + approvalPolicy: CodexAppServerRuntimeOptions["approvalPolicy"]; }): readonly NativeHookRelayEvent[] { if (params.configuredEvents?.length) { return params.configuredEvents;