From 425fff7ad6fd3a1a459d5ccf893647b3473ffd71 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 19 Feb 2026 11:41:49 -0800 Subject: [PATCH] feat: add Reject approval policy with granular prompt rejection controls (#12087) ## Why We need a way to auto-reject specific approval prompt categories without switching all approvals off. The goal is to let users independently control: - sandbox escalation approvals, - execpolicy `prompt` rule approvals, - MCP elicitation prompts. ## What changed - Added a new primary approval mode in `protocol/src/protocol.rs`: ```rust pub enum AskForApproval { // ... Reject(RejectConfig), // ... } pub struct RejectConfig { pub sandbox_approval: bool, pub rules: bool, pub mcp_elicitations: bool, } ``` - Wired `RejectConfig` semantics through approval paths in `core`: - `core/src/exec_policy.rs` - rejects rule-driven prompts when `rules = true` - rejects sandbox/escalation prompts when `sandbox_approval = true` - preserves rule priority when both rule and sandbox prompt conditions are present - `core/src/tools/sandboxing.rs` - applies `sandbox_approval` to default exec approval decisions and sandbox-failure retry gating - `core/src/safety.rs` - keeps `Reject { all false }` behavior aligned with `OnRequest` for patch safety - rejects out-of-root patch approvals when `sandbox_approval = true` - `core/src/mcp_connection_manager.rs` - auto-declines MCP elicitations when `mcp_elicitations = true` - Ensured approval policy used by MCP elicitation flow stays in sync with constrained session policy updates. - Updated app-server v2 conversions and generated schema/TypeScript artifacts for the new `Reject` shape. ## Verification Added focused unit coverage for the new behavior in: - `core/src/exec_policy.rs` - `core/src/tools/sandboxing.rs` - `core/src/mcp_connection_manager.rs` - `core/src/safety.rs` - `core/src/tools/runtimes/apply_patch.rs` Key cases covered include rule-vs-sandbox prompt precedence, MCP auto-decline behavior, and patch/sandbox retry behavior under `RejectConfig`. --- .../schema/json/ClientRequest.json | 83 +++++++- .../schema/json/EventMsg.json | 36 ++++ .../schema/json/ServerNotification.json | 36 ++++ .../codex_app_server_protocol.schemas.json | 83 +++++++- .../json/v1/ForkConversationParams.json | 36 ++++ .../json/v1/ForkConversationResponse.json | 36 ++++ .../json/v1/GetUserSavedConfigResponse.json | 36 ++++ .../schema/json/v1/NewConversationParams.json | 36 ++++ .../json/v1/ResumeConversationParams.json | 36 ++++ .../json/v1/ResumeConversationResponse.json | 36 ++++ .../schema/json/v1/SendUserTurnParams.json | 36 ++++ .../v1/SessionConfiguredNotification.json | 36 ++++ .../schema/json/v2/ConfigReadResponse.json | 47 ++++- .../v2/ConfigRequirementsReadResponse.json | 47 ++++- .../schema/json/v2/ThreadForkParams.json | 47 ++++- .../schema/json/v2/ThreadForkResponse.json | 47 ++++- .../schema/json/v2/ThreadResumeParams.json | 47 ++++- .../schema/json/v2/ThreadResumeResponse.json | 47 ++++- .../schema/json/v2/ThreadStartParams.json | 47 ++++- .../schema/json/v2/ThreadStartResponse.json | 47 ++++- .../schema/json/v2/TurnStartParams.json | 47 ++++- .../schema/typescript/AskForApproval.ts | 3 +- .../schema/typescript/RejectConfig.ts | 17 ++ .../schema/typescript/index.ts | 1 + .../schema/typescript/v2/AskForApproval.ts | 2 +- .../app-server-protocol/src/protocol/v2.rs | 20 ++ codex-rs/core/config.schema.json | 35 ++++ codex-rs/core/src/codex.rs | 21 +- codex-rs/core/src/connectors.rs | 1 + codex-rs/core/src/exec_policy.rs | 182 +++++++++++++++++- codex-rs/core/src/mcp/mod.rs | 1 + codex-rs/core/src/mcp_connection_manager.rs | 90 ++++++++- codex-rs/core/src/safety.rs | 103 +++++++++- .../core/src/tools/runtimes/apply_patch.rs | 34 +++- codex-rs/core/src/tools/sandboxing.rs | 63 +++++- codex-rs/protocol/src/models.rs | 35 +++- codex-rs/protocol/src/protocol.rs | 50 +++++ 37 files changed, 1490 insertions(+), 117 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/RejectConfig.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 37ce9c89f..9fc2a9914 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -69,13 +69,46 @@ "type": "object" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "AskForApproval2": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", @@ -101,6 +134,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval2", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -1466,6 +1513,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoveConversationListenerParams": { "properties": { "subscriptionId": { diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 5ca7c850f..a85d02853 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -117,6 +117,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -3769,6 +3783,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoteSkillSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index f3e71b70e..f18531303 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -454,6 +454,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -5169,6 +5183,28 @@ ], "type": "object" }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoteSkillSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 06b0c773f..39b21d0a5 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -221,6 +221,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -6949,6 +6963,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoteSkillSummary": { "properties": { "description": { @@ -10763,13 +10799,46 @@ "type": "object" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json index cd52a492e..b71235277 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationParams.json @@ -25,6 +25,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -114,6 +128,28 @@ }, "type": "object" }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "SandboxMode": { "enum": [ "read-only", diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index c7018d5b7..d7f1e1d83 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -117,6 +117,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -3769,6 +3783,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoteSkillSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json b/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json index beec3a22c..2f6d8b3b5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/GetUserSavedConfigResponse.json @@ -29,6 +29,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -140,6 +154,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "SandboxMode": { "enum": [ "read-only", diff --git a/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json index 327ecd23b..c9c8caaa2 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/NewConversationParams.json @@ -25,6 +25,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -34,6 +48,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "SandboxMode": { "enum": [ "read-only", diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json index 9f14c2208..a45e1e60e 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json @@ -25,6 +25,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -437,6 +451,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "ResponseItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index 2d7cc7e70..9cbd492d9 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -117,6 +117,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -3769,6 +3783,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoteSkillSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json index aa112e731..a213f6077 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json @@ -29,6 +29,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -225,6 +239,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index efd614139..fd66f6060 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -117,6 +117,20 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -3769,6 +3783,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "RemoteSkillSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index d934df7f5..293fe7dc3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -47,13 +47,46 @@ "type": "object" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "Config": { "additionalProperties": true, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 3ba96a852..4d8a96737 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -2,13 +2,46 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "ConfigRequirements": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 5638468a4..2b832d5bf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -2,13 +2,46 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "SandboxMode": { "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index f7db8aa66..2dfc446ac 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -6,13 +6,46 @@ "type": "string" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "ByteRange": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index fc6593b18..29d6fbc6d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -2,13 +2,46 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "ContentItem": { "oneOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 198b91f05..234376632 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -6,13 +6,46 @@ "type": "string" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "ByteRange": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 1574c0710..6b9c1568f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -2,13 +2,46 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "DynamicToolSpec": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 00a619927..1b617a850 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -6,13 +6,46 @@ "type": "string" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "ByteRange": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index a1363f97d..bc15473cc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -6,13 +6,46 @@ "type": "string" }, "AskForApproval": { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] }, "ByteRange": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/AskForApproval.ts b/codex-rs/app-server-protocol/schema/typescript/AskForApproval.ts index b21e86fd7..227eb44e7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/AskForApproval.ts +++ b/codex-rs/app-server-protocol/schema/typescript/AskForApproval.ts @@ -1,9 +1,10 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RejectConfig } from "./RejectConfig"; /** * Determines the conditions under which the user is consulted to approve * running the command proposed by Codex. */ -export type AskForApproval = "untrusted" | "on-failure" | "on-request" | "never"; +export type AskForApproval = "untrusted" | "on-failure" | "on-request" | { "reject": RejectConfig } | "never"; diff --git a/codex-rs/app-server-protocol/schema/typescript/RejectConfig.ts b/codex-rs/app-server-protocol/schema/typescript/RejectConfig.ts new file mode 100644 index 000000000..320f1096b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/RejectConfig.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RejectConfig = { +/** + * Reject approval prompts related to sandbox escalation. + */ +sandbox_approval: boolean, +/** + * Reject prompts triggered by execpolicy `prompt` rules. + */ +rules: boolean, +/** + * Reject MCP elicitation prompts. + */ +mcp_elicitations: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 2a626d363..79d8bdd98 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -153,6 +153,7 @@ export type { ReasoningItemContent } from "./ReasoningItemContent"; export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; export type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent"; export type { ReasoningSummary } from "./ReasoningSummary"; +export type { RejectConfig } from "./RejectConfig"; export type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent"; export type { RemoteSkillSummary } from "./RemoteSkillSummary"; export type { RemoveConversationListenerParams } from "./RemoveConversationListenerParams"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts index d3c3e77e3..b5f1bacdf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AskForApproval = "untrusted" | "on-failure" | "on-request" | "never"; +export type AskForApproval = "untrusted" | "on-failure" | "on-request" | { "reject": { sandbox_approval: boolean, rules: boolean, mcp_elicitations: boolean, } } | "never"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index ecc3ba259..328fdc091 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -37,6 +37,7 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; +use codex_protocol::protocol::RejectConfig as CoreRejectConfig; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; @@ -162,6 +163,11 @@ pub enum AskForApproval { UnlessTrusted, OnFailure, OnRequest, + Reject { + sandbox_approval: bool, + rules: bool, + mcp_elicitations: bool, + }, Never, } @@ -171,6 +177,15 @@ impl AskForApproval { AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted, AskForApproval::OnFailure => CoreAskForApproval::OnFailure, AskForApproval::OnRequest => CoreAskForApproval::OnRequest, + AskForApproval::Reject { + sandbox_approval, + rules, + mcp_elicitations, + } => CoreAskForApproval::Reject(CoreRejectConfig { + sandbox_approval, + rules, + mcp_elicitations, + }), AskForApproval::Never => CoreAskForApproval::Never, } } @@ -182,6 +197,11 @@ impl From for AskForApproval { CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted, CoreAskForApproval::OnFailure => AskForApproval::OnFailure, CoreAskForApproval::OnRequest => AskForApproval::OnRequest, + CoreAskForApproval::Reject(reject_config) => AskForApproval::Reject { + sandbox_approval: reject_config.sandbox_approval, + rules: reject_config.rules, + mcp_elicitations: reject_config.mcp_elicitations, + }, CoreAskForApproval::Never => AskForApproval::Never, } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index af2ba4565..9515ad038 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -139,6 +139,19 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "properties": { + "reject": { + "$ref": "#/definitions/RejectConfig" + } + }, + "required": [ + "reject" + ], + "type": "object" + }, { "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", "enum": [ @@ -1003,6 +1016,28 @@ } ] }, + "RejectConfig": { + "properties": { + "mcp_elicitations": { + "description": "Reject MCP elicitation prompts.", + "type": "boolean" + }, + "rules": { + "description": "Reject prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Reject approval prompts related to sandbox escalation.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "SandboxMode": { "enum": [ "read-only", diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 21aad3c75..d9ba1ca3f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1288,9 +1288,9 @@ impl Session { // before any MCP-related events. It is reasonable to consider // changing this to use Option or OnceCell, though the current // setup is straightforward enough and performs well. - mcp_connection_manager: Arc::new( - RwLock::new(McpConnectionManager::new_uninitialized()), - ), + mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( + &config.permissions.approval_policy, + ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( config.background_terminal_max_timeout, @@ -1417,6 +1417,7 @@ impl Session { &mcp_servers, config.mcp_oauth_credentials_store_mode, auth_statuses.clone(), + &session_configuration.approval_policy, tx_event.clone(), sandbox_state, ) @@ -1872,6 +1873,11 @@ impl Session { sandbox_policy_changed: bool, ) -> Arc { let per_turn_config = Self::build_per_turn_config(&session_configuration); + self.services + .mcp_connection_manager + .read() + .await + .set_approval_policy(&session_configuration.approval_policy); if sandbox_policy_changed { let sandbox_state = SandboxState { @@ -3059,6 +3065,7 @@ impl Session { &mcp_servers, store_mode, auth_statuses, + &turn_context.config.permissions.approval_policy, self.get_tx_event(), sandbox_state, ) @@ -7383,7 +7390,9 @@ mod tests { let file_watcher = Arc::new(FileWatcher::noop()); let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new( - McpConnectionManager::new_mcp_connection_manager_for_tests(), + McpConnectionManager::new_mcp_connection_manager_for_tests( + &config.permissions.approval_policy, + ), )), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( @@ -7537,7 +7546,9 @@ mod tests { let file_watcher = Arc::new(FileWatcher::noop()); let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new( - McpConnectionManager::new_mcp_connection_manager_for_tests(), + McpConnectionManager::new_mcp_connection_manager_for_tests( + &config.permissions.approval_policy, + ), )), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index ebad8cd3b..884e32dd1 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -104,6 +104,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options( &mcp_servers, config.mcp_oauth_credentials_store_mode, auth_status_entries, + &config.permissions.approval_policy, tx_event, sandbox_state, ) diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 62ebdde43..f2cedb3f9 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -32,6 +32,10 @@ use shlex::try_join as shlex_try_join; const PROMPT_CONFLICT_REASON: &str = "approval required by policy, but AskForApproval is set to Never"; +const REJECT_SANDBOX_APPROVAL_REASON: &str = + "approval required by policy, but AskForApproval::Reject.sandbox_approval is set"; +const REJECT_RULES_APPROVAL_REASON: &str = + "approval required by policy rule, but AskForApproval::Reject.rules is set"; const RULES_DIR_NAME: &str = "rules"; const RULE_EXTENSION: &str = "rules"; const DEFAULT_POLICY_FILE: &str = "default.rules"; @@ -91,6 +95,37 @@ fn is_policy_match(rule_match: &RuleMatch) -> bool { } } +/// Returns a rejection reason when `approval_policy` disallows surfacing the +/// current prompt to the user. +/// +/// `prompt_is_rule` distinguishes policy-rule prompts from sandbox/escalation +/// prompts so `Reject.rules` and `Reject.sandbox_approval` are honored +/// independently. When both are present, policy-rule prompts take precedence. +fn prompt_is_rejected_by_policy( + approval_policy: AskForApproval, + prompt_is_rule: bool, +) -> Option<&'static str> { + match approval_policy { + AskForApproval::Never => Some(PROMPT_CONFLICT_REASON), + AskForApproval::OnFailure => None, + AskForApproval::OnRequest => None, + AskForApproval::UnlessTrusted => None, + AskForApproval::Reject(reject_config) => { + if prompt_is_rule { + if reject_config.rejects_rules_approval() { + Some(REJECT_RULES_APPROVAL_REASON) + } else { + None + } + } else if reject_config.rejects_sandbox_approval() { + Some(REJECT_SANDBOX_APPROVAL_REASON) + } else { + None + } + } + } +} + #[derive(Debug, Error)] pub enum ExecPolicyError { #[error("failed to read rules files from {dir}: {source}")] @@ -196,12 +231,14 @@ impl ExecPolicyManager { reason: derive_forbidden_reason(command, &evaluation), }, Decision::Prompt => { - if matches!(approval_policy, AskForApproval::Never) { - ExecApprovalRequirement::Forbidden { - reason: PROMPT_CONFLICT_REASON.to_string(), - } - } else { - ExecApprovalRequirement::NeedsApproval { + let prompt_is_rule = evaluation.matched_rules.iter().any(|rule_match| { + is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt + }); + match prompt_is_rejected_by_policy(approval_policy, prompt_is_rule) { + Some(reason) => ExecApprovalRequirement::Forbidden { + reason: reason.to_string(), + }, + None => ExecApprovalRequirement::NeedsApproval { reason: derive_prompt_reason(command, &evaluation), proposed_execpolicy_amendment: requested_amendment.or_else(|| { if auto_amendment_allowed { @@ -212,7 +249,7 @@ impl ExecPolicyManager { None } }), - } + }, } } Decision::Allow => ExecApprovalRequirement::Skip { @@ -465,6 +502,20 @@ pub fn render_decision_for_unmatched_command( } } } + AskForApproval::Reject(_) => match sandbox_policy { + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { + // Mirror on-request behavior for unmatched commands; prompt-vs-reject is handled + // by `prompt_is_rejected_by_policy`. + Decision::Allow + } + SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => { + if sandbox_permissions.requires_escalated_permissions() { + Decision::Prompt + } else { + Decision::Allow + } + } + }, } } @@ -690,6 +741,7 @@ mod tests { use crate::config_loader::ConfigRequirementsToml; use codex_app_server_protocol::ConfigLayerSource; use codex_protocol::protocol::AskForApproval; + use codex_protocol::protocol::RejectConfig; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -1197,6 +1249,122 @@ prefix_rule( ); } + #[test] + fn unmatched_reject_policy_still_prompts_for_restricted_sandbox_escalation() { + let command = vec!["madeup-cmd".to_string()]; + + assert_eq!( + Decision::Prompt, + render_decision_for_unmatched_command( + AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: false, + }), + &SandboxPolicy::new_read_only_policy(), + &command, + SandboxPermissions::RequireEscalated, + false, + ) + ); + } + + #[tokio::test] + async fn exec_approval_requirement_rejects_unmatched_prompt_when_sandbox_rejection_enabled() { + let command = vec!["madeup-cmd".to_string()]; + + let requirement = ExecPolicyManager::default() + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + command: &command, + approval_policy: AskForApproval::Reject(RejectConfig { + sandbox_approval: true, + rules: false, + mcp_elicitations: false, + }), + sandbox_policy: &SandboxPolicy::new_read_only_policy(), + sandbox_permissions: SandboxPermissions::RequireEscalated, + prefix_rule: None, + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::Forbidden { + reason: REJECT_SANDBOX_APPROVAL_REASON.to_string(), + } + ); + } + + #[tokio::test] + async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision() { + let policy_src = r#"prefix_rule(pattern=["git"], decision="prompt")"#; + let mut parser = PolicyParser::new(); + parser + .parse("test.rules", policy_src) + .expect("parse policy"); + let manager = ExecPolicyManager::new(Arc::new(parser.build())); + let command = vec![ + "bash".to_string(), + "-lc".to_string(), + "git status && madeup-cmd".to_string(), + ]; + + let requirement = manager + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + command: &command, + approval_policy: AskForApproval::Reject(RejectConfig { + sandbox_approval: true, + rules: false, + mcp_elicitations: false, + }), + sandbox_policy: &SandboxPolicy::new_read_only_policy(), + sandbox_permissions: SandboxPermissions::RequireEscalated, + prefix_rule: None, + }) + .await; + + assert!(matches!( + requirement, + ExecApprovalRequirement::NeedsApproval { .. } + )); + } + + #[tokio::test] + async fn mixed_rule_and_sandbox_prompt_rejects_when_rules_rejection_enabled() { + let policy_src = r#"prefix_rule(pattern=["git"], decision="prompt")"#; + let mut parser = PolicyParser::new(); + parser + .parse("test.rules", policy_src) + .expect("parse policy"); + let manager = ExecPolicyManager::new(Arc::new(parser.build())); + let command = vec![ + "bash".to_string(), + "-lc".to_string(), + "git status && madeup-cmd".to_string(), + ]; + + let requirement = manager + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + command: &command, + approval_policy: AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: true, + mcp_elicitations: false, + }), + sandbox_policy: &SandboxPolicy::new_read_only_policy(), + sandbox_permissions: SandboxPermissions::RequireEscalated, + prefix_rule: None, + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::Forbidden { + reason: REJECT_RULES_APPROVAL_REASON.to_string(), + } + ); + } + #[tokio::test] async fn exec_approval_requirement_falls_back_to_heuristics() { let command = vec!["cargo".to_string(), "build".to_string()]; diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index 84b979499..681b1674b 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -205,6 +205,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent &mcp_servers, config.mcp_oauth_credentials_store_mode, auth_status_entries.clone(), + &config.permissions.approval_policy, tx_event, sandbox_state, ) diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 0fe2ac8a8..c872a9732 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -25,9 +25,11 @@ use anyhow::anyhow; use async_channel::Sender; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; +use codex_config::Constrained; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::mcp::CallToolResult; use codex_protocol::mcp::RequestId as ProtocolRequestId; +use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpStartupCompleteEvent; @@ -44,6 +46,7 @@ use futures::future::FutureExt; use futures::future::Shared; use rmcp::model::ClientCapabilities; use rmcp::model::CreateElicitationRequestParams; +use rmcp::model::ElicitationAction; use rmcp::model::ElicitationCapability; use rmcp::model::FormElicitationCapability; use rmcp::model::Implementation; @@ -182,12 +185,30 @@ static CODEX_APPS_TOOLS_CACHE: LazyLock>> type ResponderMap = HashMap<(String, RequestId), oneshot::Sender>; -#[derive(Clone, Default)] +fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool { + match approval_policy { + AskForApproval::Never => true, + AskForApproval::OnFailure => false, + AskForApproval::OnRequest => false, + AskForApproval::UnlessTrusted => false, + AskForApproval::Reject(reject_config) => reject_config.rejects_mcp_elicitations(), + } +} + +#[derive(Clone)] struct ElicitationRequestManager { requests: Arc>, + approval_policy: Arc>, } impl ElicitationRequestManager { + fn new(approval_policy: AskForApproval) -> Self { + Self { + requests: Arc::new(Mutex::new(HashMap::new())), + approval_policy: Arc::new(StdMutex::new(approval_policy)), + } + } + async fn resolve( &self, server_name: String, @@ -205,11 +226,23 @@ impl ElicitationRequestManager { fn make_sender(&self, server_name: String, tx_event: Sender) -> SendElicitation { let elicitation_requests = self.requests.clone(); + let approval_policy = self.approval_policy.clone(); Box::new(move |id, elicitation| { let elicitation_requests = elicitation_requests.clone(); let tx_event = tx_event.clone(); let server_name = server_name.clone(); + let approval_policy = approval_policy.clone(); async move { + if approval_policy + .lock() + .is_ok_and(|policy| elicitation_is_rejected_by_policy(*policy)) + { + return Ok(ElicitationResponse { + action: ElicitationAction::Decline, + content: None, + }); + } + let (tx, rx) = oneshot::channel(); { let mut lock = elicitation_requests.lock().await; @@ -346,41 +379,49 @@ pub struct SandboxState { } /// A thin wrapper around a set of running [`RmcpClient`] instances. -#[derive(Default)] pub(crate) struct McpConnectionManager { clients: HashMap, elicitation_requests: ElicitationRequestManager, } impl McpConnectionManager { - pub(crate) fn new_uninitialized() -> Self { + pub(crate) fn new_uninitialized(approval_policy: &Constrained) -> Self { Self { clients: HashMap::new(), - elicitation_requests: ElicitationRequestManager::default(), + elicitation_requests: ElicitationRequestManager::new(approval_policy.value()), } } #[cfg(test)] - pub(crate) fn new_mcp_connection_manager_for_tests() -> Self { - Self::new_uninitialized() + pub(crate) fn new_mcp_connection_manager_for_tests( + approval_policy: &Constrained, + ) -> Self { + Self::new_uninitialized(approval_policy) } pub(crate) fn has_servers(&self) -> bool { !self.clients.is_empty() } - #[allow(clippy::new_ret_no_self)] + pub fn set_approval_policy(&self, approval_policy: &Constrained) { + if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() { + *policy = approval_policy.value(); + } + } + + #[allow(clippy::new_ret_no_self, clippy::too_many_arguments)] pub async fn new( mcp_servers: &HashMap, store_mode: OAuthCredentialsStoreMode, auth_entries: HashMap, + approval_policy: &Constrained, tx_event: Sender, initial_sandbox_state: SandboxState, ) -> (Self, CancellationToken) { let cancel_token = CancellationToken::new(); let mut clients = HashMap::new(); let mut join_set = JoinSet::new(); - let elicitation_requests = ElicitationRequestManager::default(); + let elicitation_requests = ElicitationRequestManager::new(approval_policy.value()); let mcp_servers = mcp_servers.clone(); for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) { let cancel_token = cancel_token.child_token(); @@ -1331,6 +1372,7 @@ mod mcp_init_error_display_tests {} mod tests { use super::*; use codex_protocol::protocol::McpAuthStatus; + use codex_protocol::protocol::RejectConfig; use rmcp::model::JsonObject; use std::collections::HashSet; use std::sync::Arc; @@ -1370,6 +1412,38 @@ mod tests { result } + #[test] + fn elicitation_reject_policy_defaults_to_prompting() { + assert!(!elicitation_is_rejected_by_policy( + AskForApproval::OnFailure + )); + assert!(!elicitation_is_rejected_by_policy( + AskForApproval::OnRequest + )); + assert!(!elicitation_is_rejected_by_policy( + AskForApproval::UnlessTrusted + )); + assert!(!elicitation_is_rejected_by_policy(AskForApproval::Reject( + RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: false, + } + ))); + } + + #[test] + fn elicitation_reject_policy_respects_never_and_reject_config() { + assert!(elicitation_is_rejected_by_policy(AskForApproval::Never)); + assert!(elicitation_is_rejected_by_policy(AskForApproval::Reject( + RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: true, + } + ))); + } + #[test] fn test_qualify_tools_short_non_duplicated_names() { let tools = vec![ diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index b0d359b7e..350e7dad0 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -38,7 +38,10 @@ pub fn assess_patch_safety( } match policy { - AskForApproval::OnFailure | AskForApproval::Never | AskForApproval::OnRequest => { + AskForApproval::OnFailure + | AskForApproval::Never + | AskForApproval::OnRequest + | AskForApproval::Reject(_) => { // Continue to see if this can be auto-approved. } // TODO(ragona): I'm not sure this is actually correct? I believe in this case @@ -48,11 +51,17 @@ pub fn assess_patch_safety( } } + let rejects_sandbox_approval = matches!(policy, AskForApproval::Never) + || matches!( + policy, + AskForApproval::Reject(reject_config) if reject_config.sandbox_approval + ); + // Even though the patch appears to be constrained to writable paths, it is // possible that paths in the patch are hard links to files outside the // writable roots, so we should still run `apply_patch` in a sandbox in that case. if is_write_patch_constrained_to_writable_paths(action, sandbox_policy, cwd) - || policy == AskForApproval::OnFailure + || matches!(policy, AskForApproval::OnFailure) { if matches!( sandbox_policy, @@ -72,10 +81,20 @@ pub fn assess_patch_safety( sandbox_type, user_explicitly_approved: false, }, - None => SafetyCheck::AskUser, + None => { + if rejects_sandbox_approval { + SafetyCheck::Reject { + reason: + "writing outside of the project; rejected by user approval settings" + .to_string(), + } + } else { + SafetyCheck::AskUser + } + } } } - } else if policy == AskForApproval::Never { + } else if rejects_sandbox_approval { SafetyCheck::Reject { reason: "writing outside of the project; rejected by user approval settings" .to_string(), @@ -174,6 +193,7 @@ fn is_write_patch_constrained_to_writable_paths( #[cfg(test)] mod tests { use super::*; + use codex_protocol::protocol::RejectConfig; use codex_utils_absolute_path::AbsolutePathBuf; use tempfile::TempDir; @@ -253,4 +273,79 @@ mod tests { } ); } + + #[test] + fn reject_with_all_flags_false_matches_on_request_for_out_of_root_patch() { + let tmp = TempDir::new().unwrap(); + let cwd = tmp.path().to_path_buf(); + let parent = cwd.parent().unwrap().to_path_buf(); + let add_outside = + ApplyPatchAction::new_add_for_test(&parent.join("outside.txt"), "".to_string()); + let policy_workspace_only = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: Default::default(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + + assert_eq!( + assess_patch_safety( + &add_outside, + AskForApproval::OnRequest, + &policy_workspace_only, + &cwd, + WindowsSandboxLevel::Disabled, + ), + SafetyCheck::AskUser, + ); + assert_eq!( + assess_patch_safety( + &add_outside, + AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: false, + }), + &policy_workspace_only, + &cwd, + WindowsSandboxLevel::Disabled, + ), + SafetyCheck::AskUser, + ); + } + + #[test] + fn reject_sandbox_approval_rejects_out_of_root_patch() { + let tmp = TempDir::new().unwrap(); + let cwd = tmp.path().to_path_buf(); + let parent = cwd.parent().unwrap().to_path_buf(); + let add_outside = + ApplyPatchAction::new_add_for_test(&parent.join("outside.txt"), "".to_string()); + let policy_workspace_only = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: Default::default(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + + assert_eq!( + assess_patch_safety( + &add_outside, + AskForApproval::Reject(RejectConfig { + sandbox_approval: true, + rules: false, + mcp_elicitations: false, + }), + &policy_workspace_only, + &cwd, + WindowsSandboxLevel::Disabled, + ), + SafetyCheck::Reject { + reason: "writing outside of the project; rejected by user approval settings" + .to_string(), + }, + ); + } } diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index d16f9b836..1cdb30b79 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -127,7 +127,13 @@ impl Approvable for ApplyPatchRuntime { } fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool { - !matches!(policy, AskForApproval::Never) + match policy { + AskForApproval::Never => false, + AskForApproval::Reject(reject_config) => !reject_config.rejects_sandbox_approval(), + AskForApproval::OnFailure => true, + AskForApproval::OnRequest => true, + AskForApproval::UnlessTrusted => true, + } } // apply_patch approvals are decided upstream by assess_patch_safety. @@ -159,3 +165,29 @@ impl ToolRuntime for ApplyPatchRuntime { Ok(out) } } + +#[cfg(test)] +mod tests { + use super::*; + use codex_protocol::protocol::RejectConfig; + + #[test] + fn wants_no_sandbox_approval_reject_respects_sandbox_flag() { + let runtime = ApplyPatchRuntime::new(); + assert!(runtime.wants_no_sandbox_approval(AskForApproval::OnRequest)); + assert!( + !runtime.wants_no_sandbox_approval(AskForApproval::Reject(RejectConfig { + sandbox_approval: true, + rules: false, + mcp_elicitations: false, + })) + ); + assert!( + runtime.wants_no_sandbox_approval(AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: false, + })) + ); + } +} diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 04dc10c3e..89b4bc01e 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -156,6 +156,8 @@ impl ExecApprovalRequirement { /// - Never, OnFailure: do not ask /// - OnRequest: ask unless sandbox policy is DangerFullAccess +/// - Reject: ask unless sandbox policy is DangerFullAccess, but auto-reject +/// when `sandbox_approval` rejection is enabled. /// - UnlessTrusted: always ask pub(crate) fn default_exec_approval_requirement( policy: AskForApproval, @@ -163,14 +165,23 @@ pub(crate) fn default_exec_approval_requirement( ) -> ExecApprovalRequirement { let needs_approval = match policy { AskForApproval::Never | AskForApproval::OnFailure => false, - AskForApproval::OnRequest => !matches!( + AskForApproval::OnRequest | AskForApproval::Reject(_) => !matches!( sandbox_policy, SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } ), AskForApproval::UnlessTrusted => true, }; - if needs_approval { + if needs_approval + && matches!( + policy, + AskForApproval::Reject(reject_config) if reject_config.rejects_sandbox_approval() + ) + { + ExecApprovalRequirement::Forbidden { + reason: "approval policy rejected sandbox approval prompt".to_string(), + } + } else if needs_approval { ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None, @@ -224,7 +235,13 @@ pub(crate) trait Approvable { /// Decide we can request an approval for no-sandbox execution. fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool { - !matches!(policy, AskForApproval::Never | AskForApproval::OnRequest) + match policy { + AskForApproval::OnFailure => true, + AskForApproval::UnlessTrusted => true, + AskForApproval::Never => false, + AskForApproval::OnRequest => false, + AskForApproval::Reject(reject_config) => !reject_config.sandbox_approval, + } } fn start_approval_async<'a>( @@ -313,6 +330,7 @@ impl<'a> SandboxAttempt<'a> { mod tests { use super::*; use codex_protocol::protocol::NetworkAccess; + use codex_protocol::protocol::RejectConfig; use pretty_assertions::assert_eq; #[test] @@ -344,4 +362,43 @@ mod tests { } ); } + + #[test] + fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() { + let policy = AskForApproval::Reject(RejectConfig { + sandbox_approval: true, + rules: false, + mcp_elicitations: false, + }); + + let requirement = + default_exec_approval_requirement(policy, &SandboxPolicy::new_read_only_policy()); + + assert_eq!( + requirement, + ExecApprovalRequirement::Forbidden { + reason: "approval policy rejected sandbox approval prompt".to_string(), + } + ); + } + + #[test] + fn default_exec_approval_requirement_keeps_prompt_when_sandbox_rejection_is_disabled() { + let policy = AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: true, + mcp_elicitations: true, + }); + + let requirement = + default_exec_approval_requirement(policy, &SandboxPolicy::new_read_only_policy()); + + assert_eq!( + requirement, + ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: None, + } + ); + } } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 3a98aa539..65099541c 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -240,20 +240,35 @@ impl DeveloperInstructions { } pub fn from(approval_policy: AskForApproval, exec_policy: &Policy) -> DeveloperInstructions { + let on_request_instructions = || { + let command_prefixes = format_allow_prefixes(exec_policy.get_allowed_prefixes()); + match command_prefixes { + Some(prefixes) => { + format!( + "{APPROVAL_POLICY_ON_REQUEST_RULE}\n## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}" + ) + } + None => APPROVAL_POLICY_ON_REQUEST_RULE.to_string(), + } + }; let text = match approval_policy { AskForApproval::Never => APPROVAL_POLICY_NEVER.to_string(), AskForApproval::UnlessTrusted => APPROVAL_POLICY_UNLESS_TRUSTED.to_string(), AskForApproval::OnFailure => APPROVAL_POLICY_ON_FAILURE.to_string(), - AskForApproval::OnRequest => { - let command_prefixes = format_allow_prefixes(exec_policy.get_allowed_prefixes()); - match command_prefixes { - Some(prefixes) => { - format!( - "{APPROVAL_POLICY_ON_REQUEST_RULE}\n## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}" - ) - } - None => APPROVAL_POLICY_ON_REQUEST_RULE.to_string(), - } + AskForApproval::OnRequest => on_request_instructions(), + AskForApproval::Reject(reject_config) => { + let on_request_instructions = on_request_instructions(); + let sandbox_approval = reject_config.sandbox_approval; + let rules = reject_config.rules; + let mcp_elicitations = reject_config.mcp_elicitations; + format!( + "{on_request_instructions}\n\n\ + Approval policy is `reject`.\n\ + - `sandbox_approval`: {sandbox_approval}\n\ + - `rules`: {rules}\n\ + - `mcp_elicitations`: {mcp_elicitations}\n\ + When a category is `true`, requests in that category are auto-rejected instead of prompting the user." + ) } }; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index b7e4b547f..476f90208 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -378,11 +378,41 @@ pub enum AskForApproval { #[default] OnRequest, + /// Fine-grained rejection controls for approval prompts. + /// + /// When a field is `true`, prompts of that category are automatically + /// rejected instead of shown to the user. + Reject(RejectConfig), + /// Never ask the user to approve commands. Failures are immediately returned /// to the model, and never escalated to the user for approval. Never, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] +pub struct RejectConfig { + /// Reject approval prompts related to sandbox escalation. + pub sandbox_approval: bool, + /// Reject prompts triggered by execpolicy `prompt` rules. + pub rules: bool, + /// Reject MCP elicitation prompts. + pub mcp_elicitations: bool, +} + +impl RejectConfig { + pub const fn rejects_sandbox_approval(self) -> bool { + self.sandbox_approval + } + + pub const fn rejects_rules_approval(self) -> bool { + self.rules + } + + pub const fn rejects_mcp_elicitations(self) -> bool { + self.mcp_elicitations + } +} + /// Represents whether outbound network access is available to the agent. #[derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS, @@ -2818,6 +2848,26 @@ mod tests { assert!(enabled.has_full_network_access()); } + #[test] + fn reject_config_mcp_elicitation_flag_is_field_driven() { + assert!( + RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: true, + } + .rejects_mcp_elicitations() + ); + assert!( + !RejectConfig { + sandbox_approval: false, + rules: false, + mcp_elicitations: false, + } + .rejects_mcp_elicitations() + ); + } + #[test] fn workspace_write_restricted_read_access_includes_effective_writable_roots() { let cwd = if cfg!(windows) {