From 01571e81efe94d58994bbdf871734ed7dfa6130b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=BE=99?= <76432572+nankingjing@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:23:22 +0800 Subject: [PATCH] fix(runtime): prompt permission mode never invokes the prompter PermissionMode derives Ord from declaration order, placing Prompt (and Allow) above the real privilege ladder (ReadOnly < WorkspaceWrite < DangerFullAccess). In PermissionPolicy::authorize_with_context the ladder check 'current_mode >= required_mode' therefore evaluates true for every tool when the active mode is Prompt (3 >= {0,1,2}), returning Allow and making the subsequent 'if current_mode == PermissionMode::Prompt' branch that routes to prompt_or_deny unreachable. Effect: a session configured with the 'prompt' permission mode silently auto-allows every tool and never calls the prompter (reached via ConversationRuntime::run_turn with an interactive prompter), defeating the mode's entire purpose. The sibling PermissionEnforcer already special-cases Prompt before its own 'active_mode >= required_mode' check, confirming Prompt is not meant to be part of the ordered ladder. Fix: exclude Prompt from the ordered-ladder comparison so Prompt mode falls through to the interactive prompt path. Allow remains handled by its explicit equality check. Adds regression tests covering Prompt mode with and without a prompter. --- rust/crates/runtime/src/permissions.rs | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) 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(