fix(codex): honor yolo app-server approval policy

This commit is contained in:
Earl Co
2026-05-24 04:52:19 +02:00
committed by Peter Steinberger
parent 116c600f60
commit 7b30291cc4
8 changed files with 116 additions and 5 deletions
@@ -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
@@ -60,6 +60,7 @@ export async function handleCodexAppServerApprovalRequest(params: {
threadId: string;
turnId: string;
nativeHookRelay?: Pick<NativeHookRelayRegistrationHandle, "allowedEvents" | "relayId">;
autoApprove?: boolean;
signal?: AbortSignal;
}): Promise<JsonValue | undefined> {
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,
+24 -1
View File
@@ -11,6 +11,7 @@ import {
resolveCodexAppServerRuntimeOptions,
resolveCodexComputerUseConfig,
resolveCodexPluginsPolicy,
shouldAutoApproveCodexAppServerApprovals,
} from "./config.js";
type RuntimeOptionsParams = NonNullable<Parameters<typeof resolveCodexAppServerRuntimeOptions>[0]>;
@@ -56,6 +57,27 @@ function expectUiHintLabel(manifest: { uiHints: Record<string, unknown> }, 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",
},
},
}),
@@ -148,6 +148,12 @@ export type CodexPluginConfig = {
};
};
export function shouldAutoApproveCodexAppServerApprovals(
appServer: Pick<CodexAppServerRuntimeOptions, "approvalPolicy" | "sandbox">,
): boolean {
return appServer.approvalPolicy === "never" && appServer.sandbox === "danger-full-access";
}
export const CODEX_APP_SERVER_CONFIG_KEYS = [
"mode",
"transport",
@@ -6978,6 +6978,7 @@ describe("runCodexAppServerAttempt", () => {
},
threadId: "thread-1",
turnId: "turn-1",
autoApprove: true,
});
expect(approvalArgs?.nativeHookRelay).toMatchObject({
relayId,
@@ -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<JsonValue | undefined> {
return handleCodexAppServerApprovalRequest({
@@ -5674,6 +5677,7 @@ function handleApprovalRequest(params: {
threadId: params.threadId,
turnId: params.turnId,
nativeHookRelay: params.nativeHookRelay,
autoApprove: params.autoApprove,
signal: params.signal,
});
}
@@ -676,6 +676,7 @@ describe("runCodexAppServerSideQuestion", () => {
},
threadId: "side-thread",
turnId: "turn-1",
autoApprove: false,
paramsForRun: {
messageChannel: "discord",
messageProvider: "discord-voice",
@@ -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<typeof resolveCodexAppServerRuntimeOptions>["approvalPolicy"];
approvalPolicy: CodexAppServerRuntimeOptions["approvalPolicy"];
}): readonly NativeHookRelayEvent[] {
if (params.configuredEvents?.length) {
return params.configuredEvents;