fix(security): preserve denied tool request precedence (#10612)

This commit is contained in:
Jasper
2026-07-31 16:03:03 -05:00
committed by GitHub
parent 2b507b8eb2
commit 83ee4efcf7
2 changed files with 105 additions and 1 deletions
+5 -1
View File
@@ -238,9 +238,13 @@ pub fn apply_inspection_results_to_permissions(
if let Some(request) = all_requests.get(request_id) {
if !permission_result
.needs_approval
.denied
.iter()
.any(|req| req.id == *request_id)
&& !permission_result
.needs_approval
.iter()
.any(|req| req.id == *request_id)
{
permission_result.needs_approval.push(request.clone());
}
@@ -0,0 +1,100 @@
use goose::conversation::message::ToolRequest;
use goose::permission::permission_judge::PermissionCheckResult;
use goose::tool_inspection::{
apply_inspection_results_to_permissions, InspectionAction, InspectionResult,
};
use rmcp::model::CallToolRequestParams;
use rmcp::object;
fn request(id: &str) -> ToolRequest {
ToolRequest {
id: id.to_string(),
tool_call: Ok(CallToolRequestParams::new("test_tool").with_arguments(object!({}))),
metadata: None,
tool_meta: None,
}
}
fn inspection(id: &str, action: InspectionAction) -> InspectionResult {
InspectionResult {
tool_request_id: id.to_string(),
action,
reason: "test decision".to_string(),
confidence: 1.0,
inspector_name: "test_inspector".to_string(),
finding_id: None,
}
}
fn assert_only_denied(result: PermissionCheckResult, id: &str) {
assert!(result.approved.is_empty());
assert!(result.needs_approval.is_empty());
assert_eq!(result.denied, vec![request(id)]);
}
#[test]
fn require_approval_does_not_resurrect_a_denied_request() {
let result = apply_inspection_results_to_permissions(
PermissionCheckResult {
approved: vec![],
needs_approval: vec![],
denied: vec![request("request-1")],
},
&[inspection(
"request-1",
InspectionAction::RequireApproval(Some("security warning".to_string())),
)],
);
assert_only_denied(result, "request-1");
}
#[test]
fn denial_dominates_regardless_of_inspection_result_order() {
for inspection_results in [
vec![
inspection("request-1", InspectionAction::Deny),
inspection(
"request-1",
InspectionAction::RequireApproval(Some("security warning".to_string())),
),
],
vec![
inspection(
"request-1",
InspectionAction::RequireApproval(Some("security warning".to_string())),
),
inspection("request-1", InspectionAction::Deny),
],
] {
let result = apply_inspection_results_to_permissions(
PermissionCheckResult {
approved: vec![request("request-1")],
needs_approval: vec![],
denied: vec![],
},
&inspection_results,
);
assert_only_denied(result, "request-1");
}
}
#[test]
fn require_approval_still_moves_an_approved_request_to_needs_approval() {
let result = apply_inspection_results_to_permissions(
PermissionCheckResult {
approved: vec![request("request-1")],
needs_approval: vec![],
denied: vec![],
},
&[inspection(
"request-1",
InspectionAction::RequireApproval(Some("security warning".to_string())),
)],
);
assert!(result.approved.is_empty());
assert_eq!(result.needs_approval, vec![request("request-1")]);
assert!(result.denied.is_empty());
}