From 876d831f203b3ee4c87e2dd3ae4daa2d4700e965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 1 Nov 2026 14:52:10 -0600 Subject: [PATCH 1/2] test(tools): expose WebFetch authority misclassification (fixes #433) --- .../crates/tools/tests/web_fetch_authority.rs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src-rust/crates/tools/tests/web_fetch_authority.rs diff --git a/src-rust/crates/tools/tests/web_fetch_authority.rs b/src-rust/crates/tools/tests/web_fetch_authority.rs new file mode 100644 index 00000000..80d4c4dc --- /dev/null +++ b/src-rust/crates/tools/tests/web_fetch_authority.rs @@ -0,0 +1,141 @@ +use claurst_core::config::{Config, PermissionMode, Settings}; +use claurst_core::permissions::{ + AutoPermissionHandler, ManagedAutoPermissionHandler, PermissionAction, + PermissionDecision, PermissionHandler, PermissionManager, PermissionRequest, + PermissionRule, PermissionScope, +}; +use claurst_tools::{PermissionLevel, Tool, ToolContext, WebFetchTool}; +use serde_json::json; +use std::path::PathBuf; +use std::sync::atomic::AtomicUsize; +use std::sync::{Arc, Mutex}; + +fn context(handler: Arc, mode: PermissionMode) -> ToolContext { + ToolContext { + working_dir: PathBuf::from("."), + permission_mode: mode, + permission_handler: handler, + cost_tracker: claurst_core::cost::CostTracker::new(), + session_id: "webfetch-authority-regression".to_string(), + goal_turn: Default::default(), + file_history: Arc::new(parking_lot::Mutex::new( + claurst_core::file_history::FileHistory::new(), + )), + current_turn: Arc::new(AtomicUsize::new(0)), + non_interactive: true, + mcp_manager: None, + config: Config::default(), + managed_agent_config: None, + completion_notifier: None, + pending_permissions: None, + permission_manager: None, + user_question_tx: None, + } +} + +#[derive(Default)] +struct RecordingDenyHandler { + requests: Mutex>, +} + +impl PermissionHandler for RecordingDenyHandler { + fn check_permission(&self, request: &PermissionRequest) -> PermissionDecision { + self.requests.lock().unwrap().push(request.clone()); + PermissionDecision::Deny + } + + fn request_permission(&self, request: &PermissionRequest) -> PermissionDecision { + self.check_permission(request) + } +} + +#[test] +fn webfetch_is_not_advertised_as_an_unrestricted_read() { + assert!( + !matches!( + WebFetchTool.permission_level(), + PermissionLevel::None | PermissionLevel::ReadOnly + ), + "network access must not enter filesystem-read-only tool sets" + ); +} + +#[tokio::test] +async fn webfetch_requests_network_authority_before_io() { + let handler = Arc::new(RecordingDenyHandler::default()); + let ctx = context(handler.clone(), PermissionMode::Default); + let result = WebFetchTool + .execute(json!({"url": "https://example.com/"}), &ctx) + .await; + assert!(result.is_error); + assert!(result.content.contains("Permission denied")); + let requests = handler.requests.lock().unwrap(); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].tool_name, "WebFetch"); + assert!( + !requests[0].is_read_only, + "legacy permission handlers trust this field" + ); +} + +#[tokio::test] +async fn legacy_handlers_do_not_silently_authorize_webfetch() { + for mode in [ + PermissionMode::Default, + PermissionMode::Plan, + PermissionMode::AcceptEdits, + ] { + let handler = Arc::new(AutoPermissionHandler { mode }); + let ctx = context(handler, mode); + // A rejected permission must win before even an unsupported URL is used. + // This fixture performs no external network IO on the unpatched backend. + let result = WebFetchTool + .execute(json!({"url": "file:///webfetch-permission-fixture"}), &ctx) + .await; + assert!(result.is_error); + assert!( + result.content.contains("Permission denied"), + "mode {mode:?} reached the HTTP client: {}", + result.content + ); + } +} + +#[tokio::test] +async fn manager_backed_default_rejects_unapproved_network_access() { + let manager = Arc::new(Mutex::new(PermissionManager::new( + PermissionMode::Default, + &Settings::default(), + ))); + let handler = Arc::new(ManagedAutoPermissionHandler::new(manager)); + let ctx = context(handler, PermissionMode::Default); + let result = WebFetchTool + .execute(json!({"url": "https://example.com/"}), &ctx) + .await; + assert!(result.is_error); + assert!(result.content.contains("Permission denied")); +} + +#[test] +fn canonical_network_policy_keeps_allow_and_deny_rules() { + let mut manager = PermissionManager::new(PermissionMode::Default, &Settings::default()); + assert!(matches!( + manager.evaluate("WebFetch", "fetch fixture", None, None, &[]), + PermissionDecision::Ask { .. } + )); + manager.add_session_allow("WebFetch"); + assert_eq!( + manager.evaluate("WebFetch", "fetch fixture", None, None, &[]), + PermissionDecision::Allow + ); + manager.add_rule(PermissionRule { + tool_name: Some("WebFetch".to_string()), + path_pattern: None, + action: PermissionAction::Deny, + scope: PermissionScope::Session, + }); + assert_eq!( + manager.evaluate("WebFetch", "fetch fixture", None, None, &[]), + PermissionDecision::Deny + ); +} From 52024778907ba094863511ddce621c588ee3098e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 1 Nov 2026 14:58:06 -0600 Subject: [PATCH 2/2] test(tools): correct authority fixture ownership and formatting --- src-rust/crates/tools/tests/web_fetch_authority.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src-rust/crates/tools/tests/web_fetch_authority.rs b/src-rust/crates/tools/tests/web_fetch_authority.rs index 80d4c4dc..8ce238a4 100644 --- a/src-rust/crates/tools/tests/web_fetch_authority.rs +++ b/src-rust/crates/tools/tests/web_fetch_authority.rs @@ -1,8 +1,7 @@ use claurst_core::config::{Config, PermissionMode, Settings}; use claurst_core::permissions::{ - AutoPermissionHandler, ManagedAutoPermissionHandler, PermissionAction, - PermissionDecision, PermissionHandler, PermissionManager, PermissionRequest, - PermissionRule, PermissionScope, + AutoPermissionHandler, ManagedAutoPermissionHandler, PermissionAction, PermissionDecision, + PermissionHandler, PermissionManager, PermissionRequest, PermissionRule, PermissionScope, }; use claurst_tools::{PermissionLevel, Tool, ToolContext, WebFetchTool}; use serde_json::json; @@ -85,7 +84,7 @@ async fn legacy_handlers_do_not_silently_authorize_webfetch() { PermissionMode::Plan, PermissionMode::AcceptEdits, ] { - let handler = Arc::new(AutoPermissionHandler { mode }); + let handler = Arc::new(AutoPermissionHandler { mode: mode.clone() }); let ctx = context(handler, mode); // A rejected permission must win before even an unsupported URL is used. // This fixture performs no external network IO on the unpatched backend. @@ -95,7 +94,8 @@ async fn legacy_handlers_do_not_silently_authorize_webfetch() { assert!(result.is_error); assert!( result.content.contains("Permission denied"), - "mode {mode:?} reached the HTTP client: {}", + "mode {:?} reached the HTTP client: {}", + ctx.permission_mode, result.content ); }