From 064c472fa2b8f56de552b4a32dde9b5318784d7b Mon Sep 17 00:00:00 2001 From: Jasper Hugo Date: Tue, 14 Jul 2026 13:57:16 +0200 Subject: [PATCH] fix(acp): fail closed for unavailable allow-once --- crates/goose/src/acp/common.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/acp/common.rs b/crates/goose/src/acp/common.rs index e359974a60a7..5a3364d90839 100644 --- a/crates/goose/src/acp/common.rs +++ b/crates/goose/src/acp/common.rs @@ -65,8 +65,8 @@ impl From<&RequestPermissionOutcome> for PermissionDecision { } /// Map a permission decision to a response by matching the option kind from the -/// request. Each decision tries its preferred kind first, then falls back to the -/// closest alternative (e.g. AllowAlways falls back to AllowOnce). +/// request. A decision may fall back only when the alternative does not increase +/// the granted permission scope (e.g. AllowAlways falls back to AllowOnce). pub fn map_permission_response( request: &RequestPermissionRequest, decision: PermissionDecision, @@ -78,7 +78,6 @@ pub fn map_permission_response( } PermissionDecision::AllowOnce => { find_option(&request.options, PermissionOptionKind::AllowOnce) - .or_else(|| find_option(&request.options, PermissionOptionKind::AllowAlways)) } PermissionDecision::RejectAlways => { find_option(&request.options, PermissionOptionKind::RejectAlways) @@ -178,6 +177,19 @@ mod tests { } } + #[test] + fn test_allow_once_without_matching_option_is_cancelled() { + let request = make_request(vec![option( + "allow_always", + PermissionOptionKind::AllowAlways, + )]); + let response = map_permission_response(&request, PermissionDecision::AllowOnce); + assert!(matches!( + response.outcome, + RequestPermissionOutcome::Cancelled + )); + } + #[test_case(PermissionDecision::Cancel; "cancelled")] fn test_permission_cancelled(decision: PermissionDecision) { let request = make_request(vec![option("allow_once", PermissionOptionKind::AllowOnce)]);