Skip to content

Commit d9b5afa

Browse files
authored
fix(codex): rename destructive approval mode to ask (#98501)
* fix(codex): rename destructive approval mode to ask * fix(codex): keep ask reviewer app-scoped * test: fix current-main CI regressions
1 parent eb417fa commit d9b5afa

20 files changed

Lines changed: 247 additions & 89 deletions

docs/gateway/configuration-reference.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,10 @@ conversation bindings, or any non-Codex harness.
318318
default destructive-action policy for migrated plugin app elicitations.
319319
Use `true` to accept safe Codex approval schemas without prompting, `false`
320320
to decline them, `"auto"` to route Codex-required approvals through OpenClaw
321-
plugin approvals, or `"always"` to ask for every plugin write/destructive
322-
action without durable approval. The `"always"` mode clears durable Codex
323-
per-tool approval overrides for the affected app before starting the thread.
321+
plugin approvals, or `"ask"` to prompt for every plugin write/destructive
322+
action without durable approval. The `"ask"` mode clears durable Codex
323+
per-tool approval overrides for the affected app and selects the human
324+
approvals reviewer for that app before the Codex thread starts.
324325
Default: `true`.
325326
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.enabled`: enables a
326327
migrated plugin entry when global `codexPlugins.enabled` is also true.
@@ -332,7 +333,11 @@ conversation bindings, or any non-Codex harness.
332333
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.allow_destructive_actions`:
333334
per-plugin destructive-action override. When omitted, the global
334335
`allow_destructive_actions` value is used. The per-plugin value accepts the
335-
same `true`, `false`, `"auto"`, or `"always"` policies.
336+
same `true`, `false`, `"auto"`, or `"ask"` policies.
337+
338+
Each admitted plugin app that uses `"ask"` routes that app's approval requests
339+
to the human reviewer. Other apps and non-app thread approvals keep their
340+
configured reviewer, so mixed plugin policies do not inherit `"ask"` behavior.
336341

337342
`codexPlugins.enabled` is the global enablement directive. Explicit plugin
338343
entries written by migration are the durable install and repair eligibility set.

docs/plugins/codex-native-plugins.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ enabled.
201201
OpenClaw sets app-level `destructive_enabled` from the effective global or
202202
per-plugin `allow_destructive_actions` policy and lets Codex enforce
203203
destructive tool metadata from its native app tool annotations. `true`,
204-
`"auto"`, and `"always"` set `destructive_enabled: true`; `false` sets it
204+
`"auto"`, and `"ask"` set `destructive_enabled: true`; `false` sets it
205205
false. The `_default` app config is disabled with `open_world_enabled: false`.
206206
Enabled plugin apps are emitted with `open_world_enabled: true`; OpenClaw does
207207
not expose a separate plugin open-world policy knob and does not maintain
@@ -225,10 +225,14 @@ plugins, while unsafe schemas and ambiguous ownership still fail closed:
225225
- When policy is `"auto"`, OpenClaw exposes destructive plugin actions to
226226
Codex but turns ownership-proven MCP approval elicitations into OpenClaw
227227
plugin approvals before returning the Codex approval response.
228-
- When policy is `"always"`, OpenClaw uses the same Codex write/destructive
228+
- When policy is `"ask"`, OpenClaw uses the same Codex write/destructive
229229
gating as `"auto"`, clears durable Codex per-tool approval overrides for the
230230
app before the thread starts, and only offers one-shot approval or denial so
231231
durable approvals cannot suppress later write-action prompts.
232+
- For each admitted app that uses `"ask"`, OpenClaw selects Codex's human
233+
approvals reviewer for that app so Codex sends its approval elicitations to
234+
OpenClaw. Other apps and non-app thread approvals keep their configured
235+
reviewer and policy.
232236
- Missing plugin identity, ambiguous ownership, a missing turn id, a wrong turn
233237
id, or an unsafe elicitation schema declines instead of prompting.
234238

@@ -277,7 +281,7 @@ establishes a new harness session or replaces a stale binding.
277281

278282
**Destructive action is declined:** check the global and per-plugin
279283
`allow_destructive_actions` values. Even when policy is true, `"auto"`, or
280-
`"always"`, unsafe elicitation schemas and ambiguous plugin identity still fail
284+
`"ask"`, unsafe elicitation schemas and ambiguous plugin identity still fail
281285
closed.
282286

283287
## Related

extensions/codex/doctor-contract-api.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ describe("codex doctor contract", () => {
3636
},
3737
}),
3838
).toBe(false);
39+
expect(
40+
legacyConfigRules[1]?.match({
41+
allow_destructive_actions: "ask",
42+
plugins: {
43+
"google-calendar": { allow_destructive_actions: "ask" },
44+
},
45+
}),
46+
).toBe(false);
3947
expect(
4048
legacyConfigRules[1]?.match({
4149
allow_destructive_actions: "always",

extensions/codex/openclaw.plugin.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"default": false
102102
},
103103
"allow_destructive_actions": {
104-
"oneOf": [{ "type": "boolean" }, { "const": "auto" }, { "const": "always" }],
104+
"oneOf": [{ "type": "boolean" }, { "const": "auto" }, { "const": "ask" }],
105105
"default": true
106106
},
107107
"plugins": {
@@ -121,7 +121,7 @@
121121
"type": "string"
122122
},
123123
"allow_destructive_actions": {
124-
"oneOf": [{ "type": "boolean" }, { "const": "auto" }, { "const": "always" }]
124+
"oneOf": [{ "type": "boolean" }, { "const": "auto" }, { "const": "ask" }]
125125
}
126126
}
127127
}
@@ -343,7 +343,7 @@
343343
},
344344
"codexPlugins.allow_destructive_actions": {
345345
"label": "Allow Destructive Plugin Actions",
346-
"help": "Default policy for plugin app write or destructive action elicitations. Use true to accept safe schemas without prompting, false to decline, auto to ask through plugin approvals when Codex requires approval, or always to ask for every write/destructive action without durable approval.",
346+
"help": "Default policy for plugin app write or destructive action elicitations. Use true to accept safe schemas without prompting, false to decline, auto to ask through plugin approvals when Codex requires approval, or ask to prompt for every write/destructive action without durable approval.",
347347
"advanced": true
348348
},
349349
"codexPlugins.plugins": {

extensions/codex/src/app-server/config.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,11 +1192,12 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
11921192
});
11931193
});
11941194

1195-
it("parses always native Codex plugin destructive policy", () => {
1195+
it("parses ask native Codex plugin destructive policy", () => {
11961196
const config = readCodexPluginConfig({
1197+
appServer: { mode: "guardian" },
11971198
codexPlugins: {
11981199
enabled: true,
1199-
allow_destructive_actions: "always",
1200+
allow_destructive_actions: "ask",
12001201
plugins: {
12011202
"google-calendar": {
12021203
marketplaceName: "openai-curated",
@@ -1211,20 +1212,21 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
12111212
},
12121213
});
12131214

1214-
expect(config.codexPlugins?.allow_destructive_actions).toBe("always");
1215+
expect(config.appServer?.mode).toBe("guardian");
1216+
expect(config.codexPlugins?.allow_destructive_actions).toBe("ask");
12151217
expect(resolveCodexPluginsPolicy(config)).toEqual({
12161218
configured: true,
12171219
enabled: true,
12181220
allowDestructiveActions: true,
1219-
destructiveApprovalMode: "always",
1221+
destructiveApprovalMode: "ask",
12201222
pluginPolicies: [
12211223
{
12221224
configKey: "google-calendar",
12231225
marketplaceName: "openai-curated",
12241226
pluginName: "google-calendar",
12251227
enabled: true,
12261228
allowDestructiveActions: true,
1227-
destructiveApprovalMode: "always",
1229+
destructiveApprovalMode: "ask",
12281230
},
12291231
{
12301232
configKey: "slack",
@@ -1242,7 +1244,7 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
12421244
const config = readCodexPluginConfig({
12431245
codexPlugins: {
12441246
enabled: true,
1245-
allow_destructive_actions: "ask",
1247+
allow_destructive_actions: "always",
12461248
plugins: {
12471249
slack: {
12481250
marketplaceName: "openai-curated",

extensions/codex/src/app-server/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "dange
7474
type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
7575
type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env";
7676
export type CodexDynamicToolsLoading = "searchable" | "direct";
77-
export type CodexPluginDestructivePolicy = boolean | "auto" | "always";
78-
export type CodexPluginDestructiveApprovalMode = "allow" | "deny" | "auto" | "always";
77+
export type CodexPluginDestructivePolicy = boolean | "auto" | "ask";
78+
export type CodexPluginDestructiveApprovalMode = "allow" | "deny" | "auto" | "ask";
7979

8080
export const CODEX_PLUGINS_MARKETPLACE_NAME = "openai-curated";
8181

@@ -314,7 +314,7 @@ const codexDynamicToolsLoadingSchema = z.enum(["searchable", "direct"]);
314314
const codexPluginDestructivePolicySchema = z.union([
315315
z.boolean(),
316316
z.literal("auto"),
317-
z.literal("always"),
317+
z.literal("ask"),
318318
]);
319319
const codexAppServerServiceTierSchema = z
320320
.preprocess(
@@ -499,7 +499,7 @@ function resolveCodexPluginDestructivePolicy(policy: CodexPluginDestructivePolic
499499
allowDestructiveActions: boolean;
500500
destructiveApprovalMode: CodexPluginDestructiveApprovalMode;
501501
} {
502-
if (policy === "auto" || policy === "always") {
502+
if (policy === "auto" || policy === "ask") {
503503
return { allowDestructiveActions: true, destructiveApprovalMode: policy };
504504
}
505505
return {

extensions/codex/src/app-server/elicitation-bridge.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ function buildConnectorPluginApprovalElicitation(overrides: Record<string, unkno
157157
function createPluginAppPolicyContext(
158158
params: {
159159
allowDestructiveActions?: boolean;
160-
destructiveApprovalMode?: "allow" | "deny" | "auto" | "always";
160+
destructiveApprovalMode?: "allow" | "deny" | "auto" | "ask";
161161
apps?: Array<{ appId: string; pluginName: string; mcpServerNames: string[] }>;
162162
} = {},
163163
) {
@@ -1017,7 +1017,7 @@ describe("Codex app-server elicitation bridge", () => {
10171017
});
10181018
});
10191019

1020-
it("does not expose allow-always for always plugin policy", async () => {
1020+
it("does not expose allow-always for ask plugin policy", async () => {
10211021
mockCallGatewayTool
10221022
.mockResolvedValueOnce({ id: "plugin:approval-calendar-always-policy", status: "accepted" })
10231023
.mockResolvedValueOnce({
@@ -1041,7 +1041,7 @@ describe("Codex app-server elicitation bridge", () => {
10411041
turnId: "turn-1",
10421042
pluginAppPolicyContext: createPluginAppPolicyContext({
10431043
allowDestructiveActions: true,
1044-
destructiveApprovalMode: "always",
1044+
destructiveApprovalMode: "ask",
10451045
apps: [
10461046
{
10471047
appId: "connector_google_calendar",
@@ -1062,7 +1062,7 @@ describe("Codex app-server elicitation bridge", () => {
10621062
});
10631063
});
10641064

1065-
it("maps unexpected allow-always decisions to one-shot for always plugin policy", async () => {
1065+
it("maps unexpected allow-always decisions to one-shot for ask plugin policy", async () => {
10661066
mockCallGatewayTool
10671067
.mockResolvedValueOnce({
10681068
id: "plugin:approval-calendar-unexpected-always",
@@ -1089,7 +1089,7 @@ describe("Codex app-server elicitation bridge", () => {
10891089
turnId: "turn-1",
10901090
pluginAppPolicyContext: createPluginAppPolicyContext({
10911091
allowDestructiveActions: true,
1092-
destructiveApprovalMode: "always",
1092+
destructiveApprovalMode: "ask",
10931093
apps: [
10941094
{
10951095
appId: "connector_google_calendar",

extensions/codex/src/app-server/elicitation-bridge.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -332,26 +332,26 @@ async function buildPluginPolicyElicitationResponse(params: {
332332

333333
function resolvePluginDestructiveApprovalMode(
334334
entry: PluginAppPolicyContextEntry,
335-
): "allow" | "deny" | "auto" | "always" {
335+
): "allow" | "deny" | "auto" | "ask" {
336336
return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
337337
}
338338

339339
function allowedPluginPolicyApprovalDecisions(
340-
mode: "allow" | "deny" | "auto" | "always",
340+
mode: "allow" | "deny" | "auto" | "ask",
341341
approvalPrompt: BridgeableApprovalElicitation,
342342
): ExecApprovalDecision[] {
343343
const allowedDecisions = approvalPrompt.allowedDecisions ?? ["allow-once", "deny"];
344-
if (mode !== "always") {
344+
if (mode !== "ask") {
345345
return allowedDecisions;
346346
}
347347
return allowedDecisions.filter((decision) => decision !== "allow-always");
348348
}
349349

350350
function oneShotPluginPolicyApprovalOutcome(
351-
mode: "allow" | "deny" | "auto" | "always",
351+
mode: "allow" | "deny" | "auto" | "ask",
352352
outcome: AppServerApprovalOutcome,
353353
): AppServerApprovalOutcome {
354-
return mode === "always" && outcome === "approved-session" ? "approved-once" : outcome;
354+
return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
355355
}
356356

357357
function readPluginApprovalElicitation(

0 commit comments

Comments
 (0)