mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(codex): honor yolo app-server approval policy
This commit is contained in:
committed by
Peter Steinberger
parent
116c600f60
commit
7b30291cc4
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user