diff --git a/rust/crates/runtime/src/permissions.rs b/rust/crates/runtime/src/permissions.rs index 300206ad7f..30005755c2 100644 --- a/rust/crates/runtime/src/permissions.rs +++ b/rust/crates/runtime/src/permissions.rs @@ -278,7 +278,7 @@ impl PermissionPolicy { if allow_rule.is_some() || current_mode == PermissionMode::Allow - || current_mode >= required_mode + || (current_mode != PermissionMode::Prompt && current_mode >= required_mode) { return PermissionOutcome::Allow; } @@ -587,6 +587,38 @@ mod tests { )); } + #[test] + fn prompt_mode_routes_to_prompter_instead_of_auto_allowing() { + // Regression: `Prompt` sorts above `DangerFullAccess` in the + // `PermissionMode` `Ord` derivation, so the `current_mode >= required_mode` + // ladder check used to auto-allow every tool in `Prompt` mode and never + // invoke the prompter — defeating the entire purpose of the mode. + let policy = PermissionPolicy::new(PermissionMode::Prompt) + .with_tool_requirement("read_file", PermissionMode::ReadOnly); + let mut prompter = RecordingPrompter { + seen: Vec::new(), + allow: true, + }; + + let outcome = policy.authorize("read_file", "{}", Some(&mut prompter)); + + assert_eq!(outcome, PermissionOutcome::Allow); + assert_eq!(prompter.seen.len(), 1); + assert_eq!(prompter.seen[0].tool_name, "read_file"); + assert_eq!(prompter.seen[0].current_mode, PermissionMode::Prompt); + } + + #[test] + fn prompt_mode_denies_when_no_prompter_is_available() { + let policy = PermissionPolicy::new(PermissionMode::Prompt) + .with_tool_requirement("read_file", PermissionMode::ReadOnly); + + assert!(matches!( + policy.authorize("read_file", "{}", None), + PermissionOutcome::Deny { .. } + )); + } + #[test] fn applies_rule_based_denials_and_allows() { let rules = RuntimePermissionRuleConfig::new(