diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 1e8ffa9dd8..5450963f90 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,39 @@ ], "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": { + "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" + } + }, + "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": [ diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 5ec61285ca..2193ae7e67 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -117,6 +117,39 @@ ], "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": { + "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" + } + }, + "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": [ diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 540e037563..bc87e0e3a6 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -454,6 +454,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 82f5c3b7dc..9b61d17447 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,39 @@ ], "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": { + "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" + } + }, + "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": [ @@ -10667,13 +10700,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 cd52a492e5..1c04298ea7 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 a73c550f69..58db5e77db 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 beec3a22cc..3450fdcb4a 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 327ecd23b2..8a0a56a733 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 9f14c22084..6999dc08ec 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 6dc02934d3..c4a506033a 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 aa112e7319..6d8485e12e 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 69967e6853..af71b8f3ec 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,39 @@ ], "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": { + "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" + } + }, + "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": [ 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 d934df7f58..293fe7dc31 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 3ba96a852a..4d8a967375 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 5638468a4c..2b832d5bf1 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 de1823e9de..47428c2c14 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 fc6593b186..29d6fbc6d7 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 4385565f96..aba2a13b9d 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 1574c0710e..6b9c1568f0 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 fd75b56a28..fb09a7a14b 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 a1363f97d7..bc15473cc0 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 b21e86fd70..2ea9c64e5b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/AskForApproval.ts +++ b/codex-rs/app-server-protocol/schema/typescript/AskForApproval.ts @@ -6,4 +6,16 @@ * 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": { +/** + * 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, } } | "never"; 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 d3c3e77e39..b5f1bacdfa 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 96c14ef5f1..7bcc3f9ea1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -36,6 +36,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; @@ -161,6 +162,11 @@ pub enum AskForApproval { UnlessTrusted, OnFailure, OnRequest, + Reject { + sandbox_approval: bool, + rules: bool, + mcp_elicitations: bool, + }, Never, } @@ -170,6 +176,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, } } @@ -181,6 +196,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 e3f7456c95..0c363b68ce 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -109,38 +109,15 @@ "description": "App/connector settings loaded from `config.toml`.", "type": "object" }, - "AskForApproval": { - "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", - "oneOf": [ - { - "description": "Under this policy, only \"known safe\" commands—as determined by `is_safe_command()`—that **only read files** are auto‑approved. Everything else will ask the user to approve.", - "enum": [ - "untrusted" - ], - "type": "string" - }, - { - "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", - "enum": [ - "on-failure" - ], - "type": "string" - }, - { - "description": "The model decides when to ask the user for approval.", - "enum": [ - "on-request" - ], - "type": "string" - }, - { - "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", - "enum": [ - "never" - ], - "type": "string" - } - ] + "AskForApprovalToml": { + "description": "Version of [AskForApproval] that is deserializable from config.toml.", + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" }, "AuthCredentialsStoreMode": { "description": "Determine where Codex should store CLI auth credentials.", @@ -183,7 +160,7 @@ "$ref": "#/definitions/AnalyticsConfigToml" }, "approval_policy": { - "$ref": "#/definitions/AskForApproval" + "$ref": "#/definitions/AskForApprovalToml" }, "chatgpt_base_url": { "type": "string" @@ -1280,7 +1257,7 @@ "approval_policy": { "allOf": [ { - "$ref": "#/definitions/AskForApproval" + "$ref": "#/definitions/AskForApprovalToml" } ], "description": "Default approval policy for executing commands." diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6fa5ae3b80..b7ff48ae9e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1263,7 +1263,9 @@ impl Session { }; let services = SessionServices { - mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())), + mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new( + &config.permissions.approval_policy, + ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::default(), analytics_events_client: AnalyticsEventsClient::new( @@ -1380,19 +1382,19 @@ impl Session { required_mcp_servers.sort(); let cancel_token = sess.mcp_startup_cancellation_token().await; - sess.services - .mcp_connection_manager - .write() - .await + let mut mcp_connection_manager = sess.services.mcp_connection_manager.write().await; + mcp_connection_manager .initialize( &mcp_servers, config.mcp_oauth_credentials_store_mode, auth_statuses.clone(), + &session_configuration.approval_policy, tx_event.clone(), cancel_token, sandbox_state, ) .await; + drop(mcp_connection_manager); if !required_mcp_servers.is_empty() { let failures = sess .services @@ -1828,6 +1830,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 { @@ -2997,12 +3004,14 @@ impl Session { }; let cancel_token = self.reset_mcp_startup_cancellation_token().await; - let mut refreshed_manager = McpConnectionManager::default(); + let mut refreshed_manager = + McpConnectionManager::new(&turn_context.config.permissions.approval_policy); refreshed_manager .initialize( &mcp_servers, store_mode, auth_statuses, + &turn_context.config.permissions.approval_policy, self.get_tx_event(), cancel_token, sandbox_state, @@ -7265,7 +7274,9 @@ mod tests { let file_watcher = Arc::new(FileWatcher::noop()); let services = SessionServices { - mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())), + mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new( + &config.permissions.approval_policy, + ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::default(), analytics_events_client: AnalyticsEventsClient::new( @@ -7413,7 +7424,9 @@ mod tests { let file_watcher = Arc::new(FileWatcher::noop()); let services = SessionServices { - mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())), + mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new( + &config.permissions.approval_policy, + ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::default(), analytics_events_client: AnalyticsEventsClient::new( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 9874cc2774..f7047b57cf 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -893,7 +893,7 @@ pub struct ConfigToml { pub model_auto_compact_token_limit: Option, /// Default approval policy for executing commands. - pub approval_policy: Option, + pub approval_policy: Option, #[serde(default)] pub shell_environment_policy: ShellEnvironmentPolicyToml, @@ -1113,7 +1113,7 @@ impl From for UserSavedConfig { .collect(); Self { - approval_policy: config_toml.approval_policy, + approval_policy: config_toml.approval_policy.map(Into::into), sandbox_mode: config_toml.sandbox_mode, sandbox_settings: config_toml.sandbox_workspace_write.map(From::from), forced_chatgpt_workspace_id: config_toml.forced_chatgpt_workspace_id, @@ -1129,6 +1129,34 @@ impl From for UserSavedConfig { } } +/// Version of [AskForApproval] that is deserializable from config.toml. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum AskForApprovalToml { + #[serde(rename = "untrusted")] + UnlessTrusted, + OnFailure, + OnRequest, + Never, +} + +impl AskForApprovalToml { + pub fn to_core(self) -> AskForApproval { + match self { + AskForApprovalToml::UnlessTrusted => AskForApproval::UnlessTrusted, + AskForApprovalToml::OnFailure => AskForApproval::OnFailure, + AskForApprovalToml::OnRequest => AskForApproval::OnRequest, + AskForApprovalToml::Never => AskForApproval::Never, + } + } +} + +impl From for AskForApproval { + fn from(value: AskForApprovalToml) -> Self { + value.to_core() + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ProjectConfig { @@ -1570,8 +1598,8 @@ impl Config { || config_profile.approval_policy.is_some() || cfg.approval_policy.is_some(); let mut approval_policy = approval_policy_override - .or(config_profile.approval_policy) - .or(cfg.approval_policy) + .or(config_profile.approval_policy.map(Into::into)) + .or(cfg.approval_policy.map(Into::into)) .unwrap_or_else(|| { if active_project.is_trusted() { AskForApproval::OnRequest diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/core/src/config/profile.rs index ff343cbf53..fbb1fdb974 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/core/src/config/profile.rs @@ -3,9 +3,9 @@ use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use super::AskForApprovalToml; use crate::config::types::Personality; use crate::config::types::WindowsToml; -use crate::protocol::AskForApproval; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::Verbosity; @@ -21,7 +21,7 @@ pub struct ConfigProfile { /// The key in the `model_providers` map identifying the /// [`ModelProviderInfo`] to use. pub model_provider: Option, - pub approval_policy: Option, + pub approval_policy: Option, pub sandbox_mode: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, @@ -57,7 +57,7 @@ impl From for codex_app_server_protocol::Profile { Self { model: config_profile.model, model_provider: config_profile.model_provider, - approval_policy: config_profile.approval_policy, + approval_policy: config_profile.approval_policy.map(Into::into), model_reasoning_effort: config_profile.model_reasoning_effort, model_reasoning_summary: config_profile.model_reasoning_summary, model_verbosity: config_profile.model_verbosity, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 5a8930231b..a314666eb9 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -91,7 +91,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options( let auth_status_entries = compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; - let mut mcp_connection_manager = McpConnectionManager::default(); + let mut mcp_connection_manager = McpConnectionManager::new(&config.permissions.approval_policy); let (tx_event, rx_event) = unbounded(); drop(rx_event); let cancel_token = CancellationToken::new(); @@ -108,6 +108,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, cancel_token.clone(), sandbox_state, diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 62ebdde43d..19a898049b 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,32 @@ 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. +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::Reject(reject_config) + if prompt_is_rule && reject_config.rejects_rules_approval() => + { + Some(REJECT_RULES_APPROVAL_REASON) + } + AskForApproval::Reject(reject_config) + if !prompt_is_rule && reject_config.rejects_sandbox_approval() => + { + Some(REJECT_SANDBOX_APPROVAL_REASON) + } + _ => None, + } +} + #[derive(Debug, Error)] pub enum ExecPolicyError { #[error("failed to read rules files from {dir}: {source}")] @@ -196,9 +226,13 @@ impl ExecPolicyManager { reason: derive_forbidden_reason(command, &evaluation), }, Decision::Prompt => { - if matches!(approval_policy, AskForApproval::Never) { + let prompt_is_rule = evaluation.matched_rules.iter().any(|rule_match| { + is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt + }); + if let Some(reason) = prompt_is_rejected_by_policy(approval_policy, prompt_is_rule) + { ExecApprovalRequirement::Forbidden { - reason: PROMPT_CONFLICT_REASON.to_string(), + reason: reason.to_string(), } } else { ExecApprovalRequirement::NeedsApproval { @@ -465,6 +499,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 + } + } + }, } } @@ -1197,6 +1245,52 @@ 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 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 1365b5da8b..92484ff4c4 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -191,7 +191,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent let auth_status_entries = compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; - let mut mcp_connection_manager = McpConnectionManager::default(); + let mut mcp_connection_manager = McpConnectionManager::new(&config.permissions.approval_policy); let (tx_event, rx_event) = unbounded(); drop(rx_event); let cancel_token = CancellationToken::new(); @@ -209,6 +209,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, cancel_token.clone(), sandbox_state, diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 589be7aac2..809634cf12 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,20 @@ static CODEX_APPS_TOOLS_CACHE: LazyLock>> type ResponderMap = HashMap<(String, RequestId), oneshot::Sender>; -#[derive(Clone, Default)] +#[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 +216,27 @@ 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| { + matches!(*policy, AskForApproval::Never) + || matches!( + *policy, + AskForApproval::Reject(reject_config) + if reject_config.rejects_mcp_elicitations() + ) + }) { + return Ok(ElicitationResponse { + action: ElicitationAction::Decline, + content: None, + }); + } + let (tx, rx) = oneshot::channel(); { let mut lock = elicitation_requests.lock().await; @@ -346,22 +373,35 @@ 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(approval_policy: &Constrained) -> Self { + Self { + clients: HashMap::new(), + elicitation_requests: ElicitationRequestManager::new(approval_policy.value()), + } + } + pub(crate) fn has_servers(&self) -> bool { !self.clients.is_empty() } + pub fn set_approval_policy(&self, approval_policy: &Constrained) { + if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() { + *policy = approval_policy.value(); + } + } + pub async fn initialize( &mut self, mcp_servers: &HashMap, store_mode: OAuthCredentialsStoreMode, auth_entries: HashMap, + approval_policy: &Constrained, tx_event: Sender, cancel_token: CancellationToken, initial_sandbox_state: SandboxState, @@ -371,7 +411,7 @@ impl McpConnectionManager { } 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(); diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index b0d359b7e1..c7cc122b6d 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,20 @@ 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 | AskForApproval::Reject(_) + ) { if matches!( sandbox_policy, @@ -72,10 +84,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(), diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 04dc10c3e1..e852b93dab 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, @@ -183,6 +194,52 @@ pub(crate) fn default_exec_approval_requirement( } } +#[cfg(test)] +mod tests { + use super::*; + use codex_protocol::protocol::RejectConfig; + use pretty_assertions::assert_eq; + + #[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, + } + ); + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SandboxOverride { NoOverride, @@ -224,7 +281,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>( diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 3a98aa5398..06329e17ad 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -255,6 +255,28 @@ impl DeveloperInstructions { None => APPROVAL_POLICY_ON_REQUEST_RULE.to_string(), } } + AskForApproval::Reject(reject_config) => { + let command_prefixes = format_allow_prefixes(exec_policy.get_allowed_prefixes()); + let on_request_instructions = 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 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." + ) + } }; DeveloperInstructions::new(text) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f9c56c5bc4..221d23c3a4 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -377,11 +377,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, @@ -2831,6 +2861,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) {