Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 17 additions & 10 deletions desktop/src-tauri/src/desktop_host_access.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -148,13 +149,14 @@ impl<R: tauri::Runtime> HostApprovalUi for NativeApproval<R> {
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."
Expand All @@ -163,7 +165,12 @@ impl<R: tauri::Runtime> HostApprovalUi for NativeApproval<R> {
};
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 {
Expand Down
48 changes: 42 additions & 6 deletions desktop/src-tauri/src/host_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,27 @@ 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 },
}

/// 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 {
Expand Down Expand Up @@ -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"));
Expand Down
Loading