[codex-rs] Add writes app approval mode (#30482)

## Summary

- Adds `writes` to `AppToolApproval` and exposes it through config and
app-server schemas, including
`[apps._default].default_tools_approval_mode`.
- In `writes`, tools with `readOnlyHint = true` skip approval; all other
tools prompt, including non-destructive writes and tools without
annotations.
- Prevents session or persistent approval choices in this mode so later
writes still prompt.

## Why

`auto` only prompts for risk-hinted actions, while `prompt` also
interrupts reads. Apps need a middle mode that gates writes without
prompting for declared read-only actions.

## Validation

- `just write-config-schema`
- `just write-app-server-schema`
- `just fmt`
- `just test -p codex-core mcp_turn_metadata` (4 passed)
- `just test -p codex-core writes_mode` (2 passed)
- `just test -p codex-app-server config_read_includes_apps` (1 passed)
- `just test -p codex-app-server-protocol` (251 passed)
- `just test -p codex-config` (200 passed)
- `just test -p codex-cli` (300 passed)
- `just fix -p codex-core -p codex-config -p codex-app-server-protocol
-p codex-app-server -p codex-cli`
This commit is contained in:
Alex Zamoshchin authored and GitHub committed 2026-07-07 13:13:59 -04:00
1 parent 8841f506c0
commit f6e251c3ac
15 files changed
+198 -23

No files matched your search

+22 -8
View File
@@ -1223,8 +1223,7 @@ async fn maybe_request_mcp_tool_approval(
}
let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());
let approval_required = requires_mcp_tool_approval(annotations);
if !approval_required && approval_mode != AppToolApproval::Prompt {
if !requires_mcp_tool_approval_for_mode(annotations, approval_mode) {
return None;
}
@@ -1927,12 +1926,13 @@ fn normalize_approval_decision_for_mode(
decision: McpToolApprovalDecision,
approval_mode: AppToolApproval,
) -> McpToolApprovalDecision {
if approval_mode == AppToolApproval::Prompt
&& matches!(
decision,
McpToolApprovalDecision::AcceptForSession | McpToolApprovalDecision::AcceptAndRemember
)
{
if matches!(
approval_mode,
AppToolApproval::Prompt | AppToolApproval::Writes
) && matches!(
decision,
McpToolApprovalDecision::AcceptForSession | McpToolApprovalDecision::AcceptAndRemember
) {
McpToolApprovalDecision::Accept
} else {
decision
@@ -2179,6 +2179,20 @@ fn requires_mcp_tool_approval(annotations: Option<&ToolAnnotations>) -> bool {
.unwrap_or(true)
}
fn requires_mcp_tool_approval_for_mode(
annotations: Option<&ToolAnnotations>,
approval_mode: AppToolApproval,
) -> bool {
match approval_mode {
AppToolApproval::Auto => requires_mcp_tool_approval(annotations),
AppToolApproval::Prompt => true,
AppToolApproval::Writes => !annotations
.and_then(|annotations| annotations.read_only_hint)
.unwrap_or(false),
AppToolApproval::Approve => false,
}
}
async fn notify_mcp_tool_call_skip(
sess: &Session,
turn_context: &TurnContext,