Skip to content

Commit e82d19f

Browse files
authored
feat(codex): add auto plugin approvals (#92625)
* feat(codex): add on-request plugin approvals * feat(codex): rename plugin approval policy to auto * fix(codex): update binding schema version callers
1 parent 870ec6d commit e82d19f

20 files changed

Lines changed: 1047 additions & 83 deletions

docs/plugins/codex-native-plugins.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,12 @@ enabled.
200200

201201
OpenClaw sets app-level `destructive_enabled` from the effective global or
202202
per-plugin `allow_destructive_actions` policy and lets Codex enforce
203-
destructive tool metadata from its native app tool annotations. The `_default`
204-
app config is disabled with `open_world_enabled: false`. Enabled plugin apps
205-
are emitted with `open_world_enabled: true`; OpenClaw does not expose a separate
206-
plugin open-world policy knob and does not maintain per-plugin destructive
207-
tool-name deny lists.
203+
destructive tool metadata from its native app tool annotations. `true` and
204+
`"auto"` both set `destructive_enabled: true`; `false` sets it false. The
205+
`_default` app config is disabled with `open_world_enabled: false`. Enabled
206+
plugin apps are emitted with `open_world_enabled: true`; OpenClaw does not
207+
expose a separate plugin open-world policy knob and does not maintain
208+
per-plugin destructive tool-name deny lists.
208209

209210
Tool approval mode is automatic by default for plugin apps so non-destructive
210211
read tools can run without a same-thread approval UI. Destructive tools remain
@@ -221,6 +222,9 @@ plugins, while unsafe schemas and ambiguous ownership still fail closed:
221222
- When policy is `false`, OpenClaw returns a deterministic decline.
222223
- When policy is `true`, OpenClaw auto-accepts only safe schemas it can map to
223224
an approval response, such as a boolean approve field.
225+
- When policy is `"auto"`, OpenClaw exposes destructive plugin actions to
226+
Codex but turns ownership-proven MCP approval elicitations into OpenClaw
227+
plugin approvals before returning the Codex approval response.
224228
- Missing plugin identity, ambiguous ownership, a missing turn id, a wrong turn
225229
id, or an unsafe elicitation schema declines instead of prompting.
226230

@@ -268,8 +272,8 @@ Codex thread bindings keep the app config they started with until OpenClaw
268272
establishes a new harness session or replaces a stale binding.
269273

270274
**Destructive action is declined:** check the global and per-plugin
271-
`allow_destructive_actions` values. Even when policy is true, unsafe elicitation
272-
schemas and ambiguous plugin identity still fail closed.
275+
`allow_destructive_actions` values. Even when policy is true or `"auto"`,
276+
unsafe elicitation schemas and ambiguous plugin identity still fail closed.
273277

274278
## Related
275279

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,31 @@ describe("codex doctor contract", () => {
1313
expect(legacyConfigRules[0]?.match({ codexDynamicToolsLoading: "direct" })).toBe(false);
1414
});
1515

16+
it("reports old approval-routed destructive plugin policy values", () => {
17+
expect(
18+
legacyConfigRules[1]?.match({
19+
allow_destructive_actions: "on-request",
20+
plugins: {},
21+
}),
22+
).toBe(true);
23+
expect(
24+
legacyConfigRules[1]?.match({
25+
allow_destructive_actions: true,
26+
plugins: {
27+
"google-calendar": { allow_destructive_actions: "on-request" },
28+
},
29+
}),
30+
).toBe(true);
31+
expect(
32+
legacyConfigRules[1]?.match({
33+
allow_destructive_actions: "auto",
34+
plugins: {
35+
"google-calendar": { allow_destructive_actions: true },
36+
},
37+
}),
38+
).toBe(false);
39+
});
40+
1641
it("removes the retired dynamic tools profile without dropping other Codex config", () => {
1742
const original = {
1843
plugins: {
@@ -42,4 +67,60 @@ describe("codex doctor contract", () => {
4267
});
4368
expect(original.plugins.entries.codex.config).toHaveProperty("codexDynamicToolsProfile");
4469
});
70+
71+
it("renames old approval-routed destructive plugin policy values", () => {
72+
const original = {
73+
plugins: {
74+
entries: {
75+
codex: {
76+
enabled: true,
77+
config: {
78+
codexDynamicToolsProfile: "openclaw-compat",
79+
codexPlugins: {
80+
enabled: true,
81+
allow_destructive_actions: "on-request",
82+
plugins: {
83+
"google-calendar": {
84+
enabled: true,
85+
allow_destructive_actions: "on-request",
86+
},
87+
slack: {
88+
enabled: true,
89+
allow_destructive_actions: false,
90+
},
91+
},
92+
},
93+
},
94+
},
95+
},
96+
},
97+
};
98+
99+
const result = normalizeCompatibilityConfig({ cfg: original });
100+
101+
expect(result.changes).toEqual([
102+
"Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.",
103+
'Renamed plugins.entries.codex.config.codexPlugins allow_destructive_actions="on-request" values to "auto".',
104+
]);
105+
expect(result.config.plugins?.entries?.codex?.config).toEqual({
106+
codexPlugins: {
107+
enabled: true,
108+
allow_destructive_actions: "auto",
109+
plugins: {
110+
"google-calendar": {
111+
enabled: true,
112+
allow_destructive_actions: "auto",
113+
},
114+
slack: {
115+
enabled: true,
116+
allow_destructive_actions: false,
117+
},
118+
},
119+
},
120+
});
121+
expect(
122+
original.plugins.entries.codex.config.codexPlugins.plugins["google-calendar"]
123+
.allow_destructive_actions,
124+
).toBe("on-request");
125+
});
45126
});

extensions/codex/doctor-contract-api.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ function hasRetiredDynamicToolsProfile(value: unknown): boolean {
2121
return Object.hasOwn(asRecord(value) ?? {}, "codexDynamicToolsProfile");
2222
}
2323

24+
function hasLegacyPluginDestructivePolicy(value: unknown): boolean {
25+
const codexPlugins = asRecord(value);
26+
if (!codexPlugins) {
27+
return false;
28+
}
29+
if (codexPlugins.allow_destructive_actions === "on-request") {
30+
return true;
31+
}
32+
const plugins = asRecord(codexPlugins.plugins);
33+
return Object.values(plugins ?? {}).some(
34+
(plugin) => asRecord(plugin)?.allow_destructive_actions === "on-request",
35+
);
36+
}
37+
2438
/** Legacy Codex config keys that doctor should report or repair. */
2539
export const legacyConfigRules: LegacyConfigRule[] = [
2640
{
@@ -29,6 +43,12 @@ export const legacyConfigRules: LegacyConfigRule[] = [
2943
'plugins.entries.codex.config.codexDynamicToolsProfile is retired; Codex app-server always keeps Codex-native workspace tools native. Run "openclaw doctor --fix".',
3044
match: hasRetiredDynamicToolsProfile,
3145
},
46+
{
47+
path: ["plugins", "entries", "codex", "config", "codexPlugins"],
48+
message:
49+
'plugins.entries.codex.config.codexPlugins.allow_destructive_actions="on-request" was renamed to "auto". Run "openclaw doctor --fix".',
50+
match: hasLegacyPluginDestructivePolicy,
51+
},
3252
];
3353

3454
/**
@@ -40,7 +60,11 @@ export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }):
4060
} {
4161
const rawEntry = asRecord(cfg.plugins?.entries?.codex);
4262
const rawPluginConfig = asRecord(rawEntry?.config);
43-
if (!rawPluginConfig || !hasRetiredDynamicToolsProfile(rawPluginConfig)) {
63+
const rawCodexPlugins = asRecord(rawPluginConfig?.codexPlugins);
64+
const shouldRemoveDynamicToolsProfile =
65+
rawPluginConfig !== null && hasRetiredDynamicToolsProfile(rawPluginConfig);
66+
const shouldRewriteDestructivePolicy = hasLegacyPluginDestructivePolicy(rawCodexPlugins);
67+
if (!rawPluginConfig || (!shouldRemoveDynamicToolsProfile && !shouldRewriteDestructivePolicy)) {
4468
return { config: cfg, changes: [] };
4569
}
4670

@@ -55,12 +79,34 @@ export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }):
5579
return { config: cfg, changes: [] };
5680
}
5781

58-
delete nextPluginConfig.codexDynamicToolsProfile;
82+
const changes: string[] = [];
83+
if (shouldRemoveDynamicToolsProfile) {
84+
delete nextPluginConfig.codexDynamicToolsProfile;
85+
changes.push(
86+
"Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.",
87+
);
88+
}
89+
90+
if (shouldRewriteDestructivePolicy) {
91+
const nextCodexPlugins = asRecord(nextPluginConfig.codexPlugins);
92+
if (nextCodexPlugins?.allow_destructive_actions === "on-request") {
93+
nextCodexPlugins.allow_destructive_actions = "auto";
94+
}
95+
const nextPluginPolicies = asRecord(nextCodexPlugins?.plugins);
96+
for (const plugin of Object.values(nextPluginPolicies ?? {})) {
97+
const nextPlugin = asRecord(plugin);
98+
if (nextPlugin?.allow_destructive_actions === "on-request") {
99+
nextPlugin.allow_destructive_actions = "auto";
100+
}
101+
}
102+
changes.push(
103+
'Renamed plugins.entries.codex.config.codexPlugins allow_destructive_actions="on-request" values to "auto".',
104+
);
105+
}
106+
59107
return {
60108
config: nextConfig,
61-
changes: [
62-
"Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.",
63-
],
109+
changes,
64110
};
65111
}
66112

extensions/codex/openclaw.plugin.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
"default": false
101101
},
102102
"allow_destructive_actions": {
103-
"type": "boolean",
103+
"oneOf": [{ "type": "boolean" }, { "const": "auto" }],
104104
"default": true
105105
},
106106
"plugins": {
@@ -120,7 +120,7 @@
120120
"type": "string"
121121
},
122122
"allow_destructive_actions": {
123-
"type": "boolean"
123+
"oneOf": [{ "type": "boolean" }, { "const": "auto" }]
124124
}
125125
}
126126
}
@@ -290,7 +290,7 @@
290290
},
291291
"codexPlugins.allow_destructive_actions": {
292292
"label": "Allow Destructive Plugin Actions",
293-
"help": "Default policy for plugin app write or destructive action elicitations. Defaults to true.",
293+
"help": "Default policy for plugin app write or destructive action elicitations. Use true to accept safe schemas without prompting, false to decline, or auto to ask through plugin approvals.",
294294
"advanced": true
295295
},
296296
"codexPlugins.plugins": {

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,25 +859,104 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
859859
configured: true,
860860
enabled: true,
861861
allowDestructiveActions: false,
862+
destructiveApprovalMode: "deny",
862863
pluginPolicies: [
863864
{
864865
configKey: "google-calendar",
865866
marketplaceName: "openai-curated",
866867
pluginName: "google-calendar",
867868
enabled: true,
868869
allowDestructiveActions: true,
870+
destructiveApprovalMode: "allow",
869871
},
870872
{
871873
configKey: "slack",
872874
marketplaceName: "openai-curated",
873875
pluginName: "slack",
874876
enabled: false,
875877
allowDestructiveActions: false,
878+
destructiveApprovalMode: "deny",
876879
},
877880
],
878881
});
879882
});
880883

884+
it("parses auto native Codex plugin destructive policy", () => {
885+
const config = readCodexPluginConfig({
886+
codexPlugins: {
887+
enabled: true,
888+
allow_destructive_actions: "auto",
889+
plugins: {
890+
"google-calendar": {
891+
marketplaceName: "openai-curated",
892+
pluginName: "google-calendar",
893+
},
894+
slack: {
895+
marketplaceName: "openai-curated",
896+
pluginName: "slack",
897+
allow_destructive_actions: false,
898+
},
899+
gmail: {
900+
marketplaceName: "openai-curated",
901+
pluginName: "gmail",
902+
allow_destructive_actions: true,
903+
},
904+
},
905+
},
906+
});
907+
908+
expect(config.codexPlugins?.allow_destructive_actions).toBe("auto");
909+
expect(resolveCodexPluginsPolicy(config)).toEqual({
910+
configured: true,
911+
enabled: true,
912+
allowDestructiveActions: true,
913+
destructiveApprovalMode: "auto",
914+
pluginPolicies: [
915+
{
916+
configKey: "gmail",
917+
marketplaceName: "openai-curated",
918+
pluginName: "gmail",
919+
enabled: true,
920+
allowDestructiveActions: true,
921+
destructiveApprovalMode: "allow",
922+
},
923+
{
924+
configKey: "google-calendar",
925+
marketplaceName: "openai-curated",
926+
pluginName: "google-calendar",
927+
enabled: true,
928+
allowDestructiveActions: true,
929+
destructiveApprovalMode: "auto",
930+
},
931+
{
932+
configKey: "slack",
933+
marketplaceName: "openai-curated",
934+
pluginName: "slack",
935+
enabled: true,
936+
allowDestructiveActions: false,
937+
destructiveApprovalMode: "deny",
938+
},
939+
],
940+
});
941+
});
942+
943+
it("rejects unsupported native Codex plugin destructive policy strings", () => {
944+
const config = readCodexPluginConfig({
945+
codexPlugins: {
946+
enabled: true,
947+
allow_destructive_actions: "ask",
948+
plugins: {
949+
slack: {
950+
marketplaceName: "openai-curated",
951+
pluginName: "slack",
952+
},
953+
},
954+
},
955+
});
956+
957+
expect(config.codexPlugins).toBeUndefined();
958+
});
959+
881960
it("defaults native Codex plugin destructive policy to enabled", () => {
882961
const policy = resolveCodexPluginsPolicy({
883962
codexPlugins: {
@@ -895,13 +974,15 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
895974
configured: true,
896975
enabled: true,
897976
allowDestructiveActions: true,
977+
destructiveApprovalMode: "allow",
898978
pluginPolicies: [
899979
{
900980
configKey: "slack",
901981
marketplaceName: "openai-curated",
902982
pluginName: "slack",
903983
enabled: true,
904984
allowDestructiveActions: true,
985+
destructiveApprovalMode: "allow",
905986
},
906987
],
907988
});

0 commit comments

Comments
 (0)