diff --git a/CHANGELOG.md b/CHANGELOG.md index 6863c3f0d..f96c0cf11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### The desktop asks before every command that can edit a folder + +When a Bot runs a command in a folder shared from the desktop app, the person sees a prompt that can +offer "Always allow in this folder". That choice now covers commands that only read the folder. A +command that can change or delete files in it shows the prompt every time, without the "Always +allow" option. Folders already set to "Always allow" work as before for read-only commands. + ### A new deployment starts with two coworkers, not twelve The example package loaded every coworker in `examples/fintech/agents/`, so a fresh deployment opened diff --git a/desktop/src-tauri/src/desktop_host_access.rs b/desktop/src-tauri/src/desktop_host_access.rs index 44a261402..e2cf73d2d 100644 --- a/desktop/src-tauri/src/desktop_host_access.rs +++ b/desktop/src-tauri/src/desktop_host_access.rs @@ -1,7 +1,8 @@ //! Owner approval is collected by native dialogs, never by an app/tool-provided answer. use openbot_desktop_lib::host_access::{ - command_approval, ApprovedFolder, ChooseFolderPrompt, CommandApproval, CommandPrompt, - HostAccessError, HostAccessResult, HostApprovalUi, LocalCommandAllowList, WritePrompt, + command_approval, ApprovedFolder, ChooseFolderPrompt, CommandApproval, CommandPolicy, + CommandPrompt, HostAccessError, HostAccessResult, HostApprovalUi, LocalCommandAllowList, + WritePrompt, }; use tauri::Manager; use tauri_plugin_dialog::{ @@ -148,13 +149,14 @@ impl HostApprovalUi for NativeApproval { reviewable(&request.command)?; let list = self.allow_list()?; let allowed_here = list.allows(&request.bot_id, &request.root); - let offer_always = match command_approval(request.command_policy, allowed_here) { - CommandApproval::Refuse => { - return Err(refused("Commands on this computer are set to never run.")) - } - CommandApproval::Proceed => return Ok(()), - CommandApproval::Ask { offer_always } => offer_always, - }; + let offer_always = + match command_approval(request.command_policy, allowed_here, request.writable) { + CommandApproval::Refuse => { + return Err(refused("Commands on this computer are set to never run.")) + } + CommandApproval::Proceed => return Ok(()), + CommandApproval::Ask { offer_always } => offer_always, + }; let bot = bot_label(request.bot_name.as_deref(), &request.bot_id); let access = if request.writable { "This command may edit or delete files in the approved folder. Commands cannot be undone automatically." @@ -163,7 +165,12 @@ impl HostApprovalUi for NativeApproval { }; let setting = if offer_always { format!( - "Your OpenBot setting: {}. Choose \"{ALWAYS_HERE}\" to stop asking about {bot}'s commands in this folder on this computer.", + "Your OpenBot setting: {}. Choose \"{ALWAYS_HERE}\" to stop asking about {bot}'s read-only commands in this folder on this computer.", + request.command_policy.label() + ) + } else if request.writable && request.command_policy == CommandPolicy::Allow { + format!( + "Your OpenBot setting: {}. Commands that can edit files still ask every time.", request.command_policy.label() ) } else { diff --git a/desktop/src-tauri/src/host_access.rs b/desktop/src-tauri/src/host_access.rs index acf865158..7d47f4cf8 100644 --- a/desktop/src-tauri/src/host_access.rs +++ b/desktop/src-tauri/src/host_access.rs @@ -240,7 +240,8 @@ impl CommandPolicy { pub enum CommandApproval { /// Refused on this machine too, whatever the server let through. Refuse, - /// The person chose "always allow" for this Bot in this folder on this computer. + /// The person chose "always allow" for this Bot in this folder on this computer, and the + /// command leaves the folder read-only. Proceed, /// Show the dialog. `offer_always` adds "Always allow in this folder". Ask { offer_always: bool }, @@ -248,9 +249,18 @@ pub enum CommandApproval { /// The member's server setting gates the local one: a folder the person always allowed here is /// only honoured while their setting (after the team cap) is still "always allow". -pub fn command_approval(policy: CommandPolicy, allowed_here: bool) -> CommandApproval { +/// +/// "Always allow" covers read-only commands. A command that can edit the folder asks every time. +pub fn command_approval( + policy: CommandPolicy, + allowed_here: bool, + writable: bool, +) -> CommandApproval { match policy { CommandPolicy::Never => CommandApproval::Refuse, + CommandPolicy::Allow if writable => CommandApproval::Ask { + offer_always: false, + }, CommandPolicy::Allow if allowed_here => CommandApproval::Proceed, CommandPolicy::Allow => CommandApproval::Ask { offer_always: true }, CommandPolicy::Ask => CommandApproval::Ask { @@ -1765,26 +1775,52 @@ mod command_policy_tests { #[test] fn local_always_allow_only_counts_while_the_server_setting_allows() { assert_eq!( - command_approval(CommandPolicy::Never, true), + command_approval(CommandPolicy::Never, true, false), CommandApproval::Refuse ); assert_eq!( - command_approval(CommandPolicy::Allow, true), + command_approval(CommandPolicy::Allow, true, false), CommandApproval::Proceed ); assert_eq!( - command_approval(CommandPolicy::Allow, false), + command_approval(CommandPolicy::Allow, false, false), CommandApproval::Ask { offer_always: true } ); // An admin cap of "ask" pauses a folder the person always allowed. assert_eq!( - command_approval(CommandPolicy::Ask, true), + command_approval(CommandPolicy::Ask, true, false), CommandApproval::Ask { offer_always: false } ); } + #[test] + fn a_command_that_can_edit_the_folder_asks_even_where_always_allowed() { + assert_eq!( + command_approval(CommandPolicy::Allow, true, true), + CommandApproval::Ask { + offer_always: false + } + ); + assert_eq!( + command_approval(CommandPolicy::Allow, false, true), + CommandApproval::Ask { + offer_always: false + } + ); + assert_eq!( + command_approval(CommandPolicy::Ask, true, true), + CommandApproval::Ask { + offer_always: false + } + ); + assert_eq!( + command_approval(CommandPolicy::Never, true, true), + CommandApproval::Refuse + ); + } + #[test] fn the_local_allow_list_is_per_bot_and_folder_and_survives_reload() { let dir = std::env::temp_dir().join(fresh_id("allow-list"));