diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 338a7a714..8b2399a2d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -928,7 +928,7 @@ Only the granted subset matters on the wire. Any permissions omitted from `resul Within the same turn, granted permissions are sticky: later shell-like tool calls can automatically reuse the granted subset without reissuing a separate permission request. -If the session approval policy uses `Reject` with `request_permissions: true`, the server does not send `item/permissions/requestApproval` to the client. Instead, the tool is auto-denied and resolves with an empty granted-permissions payload. +If the session approval policy uses `Reject` with `request_permissions: true`, standalone `request_permissions` tool calls are auto-denied and no `item/permissions/requestApproval` prompt is sent. Inline `with_additional_permissions` command requests remain controlled by `sandbox_approval`, and any previously granted permissions remain sticky for later shell-like calls in the same turn. ### Dynamic tool calls (experimental) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 2e29ad7b1..84509cda0 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1344,7 +1344,7 @@ }, "request_permissions": { "default": false, - "description": "Reject approval prompts related to built-in permission requests.", + "description": "Reject `request_permissions` tool requests.", "type": "boolean" }, "rules": { @@ -1352,7 +1352,7 @@ "type": "boolean" }, "sandbox_approval": { - "description": "Reject approval prompts related to sandbox escalation.", + "description": "Reject shell command approval requests, including inline `with_additional_permissions` and `require_escalated` requests.", "type": "boolean" }, "skill_approval": { diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 7e838642a..81ac04d22 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2306,7 +2306,7 @@ async fn request_permissions_emits_event_when_reject_policy_allows_requests() { } #[tokio::test] -async fn request_permissions_returns_empty_grant_when_reject_policy_blocks_requests() { +async fn request_permissions_is_auto_denied_when_reject_policy_blocks_tool_requests() { let (session, mut turn_context, rx) = make_session_and_context_with_rx().await; *session.active_turn.lock().await = Some(ActiveTurn::default()); Arc::get_mut(&mut turn_context) @@ -2323,10 +2323,13 @@ async fn request_permissions_returns_empty_grant_when_reject_policy_blocks_reque )) .expect("test setup should allow updating approval policy"); + let session = Arc::new(session); + let turn_context = Arc::new(turn_context); + let call_id = "call-1".to_string(); let response = session .request_permissions( - &turn_context, - "call-1".to_string(), + turn_context.as_ref(), + call_id, codex_protocol::request_permissions::RequestPermissionsArgs { reason: Some("need network".to_string()), permissions: codex_protocol::models::PermissionProfile { @@ -2349,10 +2352,10 @@ async fn request_permissions_returns_empty_grant_when_reject_policy_blocks_reque ) ); assert!( - tokio::time::timeout(StdDuration::from_millis(50), rx.recv()) + tokio::time::timeout(StdDuration::from_millis(100), rx.recv()) .await .is_err(), - "unexpected request_permissions event emitted", + "request_permissions should not emit an event when reject.request_permissions is set" ); } diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index b0a16fc02..00b3adf43 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -227,6 +227,85 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic ); } +#[tokio::test] +#[cfg(unix)] +async fn shell_handler_allows_sticky_turn_permissions_without_inline_request_permissions_feature() { + let (mut session, turn_context_raw) = make_session_and_context().await; + session + .features + .enable(Feature::RequestPermissionsTool) + .expect("test setup should allow enabling request permissions tool"); + *session.active_turn.lock().await = Some(ActiveTurn::default()); + { + let mut active_turn = session.active_turn.lock().await; + let active_turn = active_turn.as_mut().expect("active turn"); + let mut turn_state = active_turn.turn_state.lock().await; + turn_state.record_granted_permissions(PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + ..Default::default() + }); + } + + let session = Arc::new(session); + let turn_context = Arc::new(turn_context_raw); + + let handler = ShellHandler; + let resp = handler + .handle(ToolInvocation { + session: Arc::clone(&session), + turn: Arc::clone(&turn_context), + tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())), + call_id: "sticky-turn-grant".to_string(), + tool_name: "shell".to_string(), + tool_namespace: None, + payload: ToolPayload::Function { + arguments: serde_json::json!({ + "command": [ + "/bin/sh", + "-c", + "echo hi", + ], + "timeout_ms": 1_000_u64, + "workdir": Some(turn_context.cwd.to_string_lossy().to_string()), + }) + .to_string(), + }, + }) + .await; + + match resp { + Ok(output) => { + let output = expect_text_output(&output); + + #[derive(Deserialize, PartialEq, Eq, Debug)] + struct ResponseExecMetadata { + exit_code: i32, + } + + #[derive(Deserialize)] + struct ResponseExecOutput { + output: String, + metadata: ResponseExecMetadata, + } + + let exec_output: ResponseExecOutput = + serde_json::from_str(&output).expect("valid exec output json"); + + assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 }); + assert!(exec_output.output.contains("hi")); + } + Err(FunctionCallError::RespondToModel(output)) => { + assert!( + !output.contains("additional permissions are disabled"), + "sticky turn permissions should bypass inline validation: {output}" + ); + } + Err(err) => panic!("unexpected error: {err:?}"), + } +} + #[tokio::test] async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { let codex_home = tempdir().expect("create codex home"); diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 5d8aaeba6..030571ebe 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -101,7 +101,7 @@ fn resolve_workdir_base_path( /// Validates feature/policy constraints for `with_additional_permissions` and /// normalizes any path-based permissions. Errors if the request is invalid. pub(crate) fn normalize_and_validate_additional_permissions( - request_permission_enabled: bool, + additional_permissions_allowed: bool, approval_policy: AskForApproval, sandbox_permissions: SandboxPermissions, additional_permissions: Option, @@ -113,11 +113,12 @@ pub(crate) fn normalize_and_validate_additional_permissions( SandboxPermissions::WithAdditionalPermissions ); - if !request_permission_enabled + if !permissions_preapproved + && !additional_permissions_allowed && (uses_additional_permissions || additional_permissions.is_some()) { return Err( - "additional permissions are disabled; enable `features.request_permission` before using `with_additional_permissions`" + "additional permissions are disabled; enable `features.request_permissions` before using `with_additional_permissions`" .to_string(), ); } @@ -164,6 +165,23 @@ pub(super) struct EffectiveAdditionalPermissions { pub permissions_preapproved: bool, } +pub(super) fn implicit_granted_permissions( + sandbox_permissions: SandboxPermissions, + additional_permissions: Option<&PermissionProfile>, + effective_additional_permissions: &EffectiveAdditionalPermissions, +) -> Option { + if !sandbox_permissions.uses_additional_permissions() + && !matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) + && additional_permissions.is_none() + { + effective_additional_permissions + .additional_permissions + .clone() + } else { + None + } +} + pub(super) async fn apply_granted_turn_permissions( session: &Session, sandbox_permissions: SandboxPermissions, @@ -210,3 +228,118 @@ pub(super) async fn apply_granted_turn_permissions( permissions_preapproved, } } + +#[cfg(test)] +mod tests { + use super::EffectiveAdditionalPermissions; + use super::implicit_granted_permissions; + use super::normalize_and_validate_additional_permissions; + use crate::sandboxing::SandboxPermissions; + use codex_protocol::models::FileSystemPermissions; + use codex_protocol::models::NetworkPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::protocol::AskForApproval; + use codex_protocol::protocol::RejectConfig; + use codex_utils_absolute_path::AbsolutePathBuf; + use pretty_assertions::assert_eq; + use tempfile::tempdir; + + fn network_permissions() -> PermissionProfile { + PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + ..Default::default() + } + } + + fn file_system_permissions(path: &std::path::Path) -> PermissionProfile { + PermissionProfile { + file_system: Some(FileSystemPermissions { + read: None, + write: Some(vec![ + AbsolutePathBuf::from_absolute_path(path).expect("absolute path"), + ]), + }), + ..Default::default() + } + } + + #[test] + fn preapproved_permissions_work_when_request_permissions_tool_is_enabled_without_inline_feature() + { + let cwd = tempdir().expect("tempdir"); + + let normalized = normalize_and_validate_additional_permissions( + false, + AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + }), + SandboxPermissions::WithAdditionalPermissions, + Some(network_permissions()), + true, + cwd.path(), + ) + .expect("preapproved permissions should be allowed"); + + assert_eq!(normalized, Some(network_permissions())); + } + + #[test] + fn fresh_additional_permissions_still_require_request_permissions_feature() { + let cwd = tempdir().expect("tempdir"); + + let err = normalize_and_validate_additional_permissions( + false, + AskForApproval::OnRequest, + SandboxPermissions::WithAdditionalPermissions, + Some(network_permissions()), + false, + cwd.path(), + ) + .expect_err("fresh inline permission requests should remain disabled"); + + assert_eq!( + err, + "additional permissions are disabled; enable `features.request_permissions` before using `with_additional_permissions`" + ); + } + + #[test] + fn implicit_sticky_grants_bypass_inline_permission_validation() { + let cwd = tempdir().expect("tempdir"); + let granted_permissions = file_system_permissions(cwd.path()); + let implicit_permissions = implicit_granted_permissions( + SandboxPermissions::UseDefault, + None, + &EffectiveAdditionalPermissions { + sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, + additional_permissions: Some(granted_permissions.clone()), + permissions_preapproved: false, + }, + ); + + assert_eq!(implicit_permissions, Some(granted_permissions)); + } + + #[test] + fn explicit_inline_permissions_do_not_use_implicit_sticky_grant_path() { + let cwd = tempdir().expect("tempdir"); + let requested_permissions = file_system_permissions(cwd.path()); + let implicit_permissions = implicit_granted_permissions( + SandboxPermissions::WithAdditionalPermissions, + Some(&requested_permissions), + &EffectiveAdditionalPermissions { + sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, + additional_permissions: Some(requested_permissions.clone()), + permissions_preapproved: false, + }, + ); + + assert_eq!(implicit_permissions, None); + } +} diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 01d7f1b6e..0318fe90f 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -21,6 +21,7 @@ use crate::tools::events::ToolEmitter; use crate::tools::events::ToolEventCtx; use crate::tools::handlers::apply_granted_turn_permissions; use crate::tools::handlers::apply_patch::intercept_apply_patch; +use crate::tools::handlers::implicit_granted_permissions; use crate::tools::handlers::normalize_and_validate_additional_permissions; use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::resolve_workdir_base_path; @@ -336,19 +337,33 @@ impl ShellHandler { } let request_permission_enabled = session.features().enabled(Feature::RequestPermissions); + let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( session.as_ref(), exec_params.sandbox_permissions, additional_permissions, ) .await; - let normalized_additional_permissions = normalize_and_validate_additional_permissions( - request_permission_enabled, - turn.approval_policy.value(), - effective_additional_permissions.sandbox_permissions, - effective_additional_permissions.additional_permissions, - effective_additional_permissions.permissions_preapproved, - &exec_params.cwd, + let additional_permissions_allowed = request_permission_enabled + || (session.features().enabled(Feature::RequestPermissionsTool) + && effective_additional_permissions.permissions_preapproved); + let normalized_additional_permissions = implicit_granted_permissions( + exec_params.sandbox_permissions, + requested_additional_permissions.as_ref(), + &effective_additional_permissions, + ) + .map_or_else( + || { + normalize_and_validate_additional_permissions( + additional_permissions_allowed, + turn.approval_policy.value(), + effective_additional_permissions.sandbox_permissions, + effective_additional_permissions.additional_permissions, + effective_additional_permissions.permissions_preapproved, + &exec_params.cwd, + ) + }, + |permissions| Ok(Some(permissions)), ) .map_err(FunctionCallError::RespondToModel)?; diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 699dc2011..80c132008 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -12,6 +12,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::apply_granted_turn_permissions; use crate::tools::handlers::apply_patch::intercept_apply_patch; +use crate::tools::handlers::implicit_granted_permissions; use crate::tools::handlers::normalize_and_validate_additional_permissions; use crate::tools::handlers::parse_arguments; use crate::tools::handlers::parse_arguments_with_base_path; @@ -171,12 +172,16 @@ impl ToolHandler for UnifiedExecHandler { let request_permission_enabled = session.features().enabled(Feature::RequestPermissions); + let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( context.session.as_ref(), sandbox_permissions, additional_permissions, ) .await; + let additional_permissions_allowed = request_permission_enabled + || (session.features().enabled(Feature::RequestPermissionsTool) + && effective_additional_permissions.permissions_preapproved); // Sticky turn permissions have already been approved, so they should // continue through the normal exec approval flow for the command. @@ -200,21 +205,30 @@ impl ToolHandler for UnifiedExecHandler { let workdir = workdir.map(|dir| context.turn.resolve_path(Some(dir))); let cwd = workdir.clone().unwrap_or(cwd); - let normalized_additional_permissions = - match normalize_and_validate_additional_permissions( - request_permission_enabled, - context.turn.approval_policy.value(), - effective_additional_permissions.sandbox_permissions, - effective_additional_permissions.additional_permissions, - effective_additional_permissions.permissions_preapproved, - &cwd, - ) { - Ok(normalized) => normalized, - Err(err) => { - manager.release_process_id(process_id).await; - return Err(FunctionCallError::RespondToModel(err)); - } - }; + let normalized_additional_permissions = match implicit_granted_permissions( + sandbox_permissions, + requested_additional_permissions.as_ref(), + &effective_additional_permissions, + ) + .map_or_else( + || { + normalize_and_validate_additional_permissions( + additional_permissions_allowed, + context.turn.approval_policy.value(), + effective_additional_permissions.sandbox_permissions, + effective_additional_permissions.additional_permissions, + effective_additional_permissions.permissions_preapproved, + &cwd, + ) + }, + |permissions| Ok(Some(permissions)), + ) { + Ok(normalized) => normalized, + Err(err) => { + manager.release_process_id(process_id).await; + return Err(FunctionCallError::RespondToModel(err)); + } + }; if let Some(output) = intercept_apply_patch( &command, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index bbe4d12a7..fd2044738 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -10,6 +10,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::Op; +use codex_protocol::protocol::RejectConfig; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_permissions::PermissionGrantScope; @@ -395,6 +396,95 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res Ok(()) } +#[tokio::test(flavor = "current_thread")] +async fn request_permissions_tool_is_auto_denied_when_reject_request_permissions_is_enabled() +-> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = start_mock_server().await; + let approval_policy = AskForApproval::Reject(RejectConfig { + sandbox_approval: false, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + }); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let sandbox_policy_for_config = sandbox_policy.clone(); + + let mut builder = test_codex().with_config(move |config| { + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .features + .enable(Feature::RequestPermissionsTool) + .expect("test config should allow feature update"); + }); + let test = builder.build(&server).await?; + + let requested_dir = test.workspace_path("request-permissions-reject"); + fs::create_dir_all(&requested_dir)?; + let requested_permissions = requested_directory_write_permissions(&requested_dir); + let call_id = "request_permissions_reject_auto_denied"; + let event = request_permissions_tool_event( + call_id, + "Request access through the standalone tool", + &requested_permissions, + )?; + + let _ = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-request-permissions-reject-1"), + event, + ev_completed("resp-request-permissions-reject-1"), + ]), + ) + .await; + let results = mount_sse_once( + &server, + sse(vec![ + ev_assistant_message("msg-request-permissions-reject-1", "done"), + ev_completed("resp-request-permissions-reject-2"), + ]), + ) + .await; + + submit_turn( + &test, + "request permissions under reject.request_permissions", + approval_policy, + sandbox_policy, + ) + .await?; + + let event = wait_for_event(&test.codex, |event| { + matches!( + event, + EventMsg::RequestPermissions(_) | EventMsg::TurnComplete(_) + ) + }) + .await; + assert!( + matches!(event, EventMsg::TurnComplete(_)), + "request_permissions should not emit a prompt when reject.request_permissions is set: {event:?}" + ); + + let call_output = results.single_request().function_call_output(call_id); + let result: RequestPermissionsResponse = + serde_json::from_str(call_output["output"].as_str().unwrap_or_default())?; + assert_eq!( + result, + RequestPermissionsResponse { + permissions: PermissionProfile::default(), + scope: PermissionGrantScope::Turn, + } + ); + + Ok(()) +} + #[tokio::test(flavor = "current_thread")] async fn relative_additional_permissions_resolve_against_tool_workdir() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1254,6 +1344,118 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls() -> Resu Ok(()) } +#[tokio::test(flavor = "current_thread")] +async fn request_permissions_grants_apply_to_later_shell_command_calls_without_inline_permission_feature() +-> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = start_mock_server().await; + let approval_policy = AskForApproval::OnRequest; + let sandbox_policy = workspace_write_excluding_tmp(); + let sandbox_policy_for_config = sandbox_policy.clone(); + + let mut builder = test_codex().with_config(move |config| { + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .features + .enable(Feature::RequestPermissionsTool) + .expect("test config should allow feature update"); + }); + let test = builder.build(&server).await?; + + let outside_dir = tempfile::tempdir()?; + let outside_write = outside_dir + .path() + .join("sticky-shell-feature-independent.txt"); + let command = format!( + "printf {:?} > {:?} && cat {:?}", + "sticky-shell-feature-independent-ok", outside_write, outside_write + ); + let requested_permissions = requested_directory_write_permissions(outside_dir.path()); + let normalized_requested_permissions = + normalized_directory_write_permissions(outside_dir.path())?; + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-sticky-shell-independent-1"), + request_permissions_tool_event( + "permissions-call", + "Allow writing outside the workspace", + &requested_permissions, + )?, + ev_completed("resp-sticky-shell-independent-1"), + ]), + sse(vec![ + ev_response_created("resp-sticky-shell-independent-2"), + shell_command_event("shell-call", &command)?, + ev_completed("resp-sticky-shell-independent-2"), + ]), + sse(vec![ + ev_response_created("resp-sticky-shell-independent-3"), + ev_assistant_message("msg-sticky-shell-independent-1", "done"), + ev_completed("resp-sticky-shell-independent-3"), + ]), + ], + ) + .await; + + submit_turn( + &test, + "write outside the workspace without inline permission feature", + approval_policy, + sandbox_policy, + ) + .await?; + + let granted_permissions = expect_request_permissions_event(&test, "permissions-call").await; + assert_eq!( + granted_permissions, + normalized_requested_permissions.clone() + ); + test.codex + .submit(Op::RequestPermissionsResponse { + id: "permissions-call".to_string(), + response: RequestPermissionsResponse { + permissions: normalized_requested_permissions.clone(), + scope: PermissionGrantScope::Turn, + }, + }) + .await?; + + if let Some(approval) = wait_for_exec_approval_or_completion(&test).await { + test.codex + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::Approved, + }) + .await?; + wait_for_completion(&test).await; + } + + let shell_output = responses + .function_call_output_text("shell-call") + .map(|output| json!({ "output": output })) + .unwrap_or_else(|| panic!("expected shell-call output")); + let result = parse_result(&shell_output); + assert!( + result.exit_code.is_none_or(|exit_code| exit_code == 0), + "expected success output, got exit_code={:?}, stdout={:?}", + result.exit_code, + result.stdout + ); + assert_eq!(result.stdout.trim(), "sticky-shell-feature-independent-ok"); + assert_eq!( + fs::read_to_string(&outside_write)?, + "sticky-shell-feature-independent-ok" + ); + + Ok(()) +} + #[tokio::test(flavor = "current_thread")] async fn partial_request_permissions_grants_do_not_preapprove_new_permissions() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e1efba859..fb90a5ccf 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -529,14 +529,15 @@ pub enum AskForApproval { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] pub struct RejectConfig { - /// Reject approval prompts related to sandbox escalation. + /// Reject shell command approval requests, including inline + /// `with_additional_permissions` and `require_escalated` requests. pub sandbox_approval: bool, /// Reject prompts triggered by execpolicy `prompt` rules. pub rules: bool, /// Reject approval prompts triggered by skill script execution. #[serde(default)] pub skill_approval: bool, - /// Reject approval prompts related to built-in permission requests. + /// Reject `request_permissions` tool requests. #[serde(default)] pub request_permissions: bool, /// Reject MCP elicitation prompts.