Adds buttons for confirming (and optionally persisting) granular permissions on tool calls: <img width="688" height="302" alt="Screenshot 2026-01-14 at 1 58 40 PM" src="https://github.com/user-attachments/assets/3228cc39-efd5-4a73-989f-ddb28969847f" /> <img width="690" height="282" alt="Screenshot 2026-01-14 at 1 58 31 PM" src="https://github.com/user-attachments/assets/8367a888-5728-4877-a502-1ff20ac929ec" /> ## Features ### Tool Permission Settings - **Per-tool rules** in `agent.tool_permissions.tools.<tool_name>`: - `default_mode`: `"allow"`, `"deny"`, or `"confirm"` (default) - `always_allow`: Array of regex patterns to auto-approve - `always_deny`: Array of regex patterns to block - `always_confirm`: Array of regex patterns requiring confirmation - **Supported tools**: `terminal`, `edit_file`, `delete_path`, `move_path`, `create_directory`, `save_file`, `copy_path`, `fetch`, `web_search` - **MCP tool support**: Third-party tools from context servers with `mcp:<server>:<tool>` naming ### Smart Permission Buttons When a tool requires confirmation, the dialog shows contextual buttons: - **"Always allow \`<tool>\`"** - Sets `default_mode = "allow"` for the tool - **"Always allow \`<pattern>\`"** - Adds a pattern to `always_allow`: - Terminal: Command name (e.g., `cargo`, `npm`, `git`) - File tools: Parent directory path - URL tools: Domain name - **"Allow"** / **"Deny"** - One-time decision ### Pattern Extraction Automatically extracts meaningful patterns from tool inputs: - Terminal commands → `^cargo\s`, `^npm\s`, etc. - File paths → `^/Users/alice/project/src/` - URLs → `^https?://github\.com` ### Invalid Pattern Handling - Malformed regex patterns are detected at settings load time - Tools with invalid patterns show an error and block execution - Clear error messages identify which pattern failed ## Changes ### New Files - `crates/agent/src/tool_permissions.rs` - Permission evaluation logic - `crates/agent/src/pattern_extraction.rs` - Pattern extraction functions ### Modified Files - `crates/agent_settings/src/agent_settings.rs` - `ToolPermissions`, `ToolRules` structs - `crates/settings/src/settings_content/agent.rs` - Settings schema and serialization - `crates/settings/src/settings_file.rs` - Helper methods for modifying settings - `crates/agent/src/thread.rs` - `authorize_with_context()`, `authorize_third_party_tool()` methods - `crates/agent/src/tools/*.rs` - Updated all granular tools to use permission system - `crates/agent/src/tools/context_server_registry.rs` - MCP tool authorization - `crates/agent_ui/src/acp/thread_view.rs` - Permission button UI, removed Keep/Reject buttons ## Example Configuration ```json { "agent": { "tool_permissions": { "tools": { "terminal": { "default_mode": "confirm", "always_allow": ["^cargo\\s", "^npm\\s", "^git\\s"], "always_deny": ["^rm\\s+-rf", "^sudo\\s"] }, "edit_file": { "default_mode": "allow", "always_deny": ["^\\.env", "^/etc/"] }, "mcp:filesystem:write_file": { "default_mode": "confirm" } } } } } ``` Release Notes: - You can now set per-tool permissions when confirming/denying tool use --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com> Co-authored-by: Michael Benfield <mbenfield@zed.dev>
158 lines
5.5 KiB
Rust
158 lines
5.5 KiB
Rust
use crate::{
|
|
AgentTool, ToolCallEventStream, ToolPermissionDecision, decide_permission_from_settings,
|
|
};
|
|
use agent_client_protocol::ToolKind;
|
|
use agent_settings::AgentSettings;
|
|
use anyhow::{Context as _, Result, anyhow};
|
|
use futures::FutureExt as _;
|
|
use gpui::{App, AppContext, Entity, Task};
|
|
use project::Project;
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use settings::Settings;
|
|
use std::sync::Arc;
|
|
use util::markdown::MarkdownInlineCode;
|
|
|
|
/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
|
|
/// Directory contents will be copied recursively.
|
|
///
|
|
/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
|
|
/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal.
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct CopyPathToolInput {
|
|
/// The source path of the file or directory to copy.
|
|
/// If a directory is specified, its contents will be copied recursively.
|
|
///
|
|
/// <example>
|
|
/// If the project has the following files:
|
|
///
|
|
/// - directory1/a/something.txt
|
|
/// - directory2/a/things.txt
|
|
/// - directory3/a/other.txt
|
|
///
|
|
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
|
|
/// </example>
|
|
pub source_path: String,
|
|
/// The destination path where the file or directory should be copied to.
|
|
///
|
|
/// <example>
|
|
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt"
|
|
/// </example>
|
|
pub destination_path: String,
|
|
}
|
|
|
|
pub struct CopyPathTool {
|
|
project: Entity<Project>,
|
|
}
|
|
|
|
impl CopyPathTool {
|
|
pub fn new(project: Entity<Project>) -> Self {
|
|
Self { project }
|
|
}
|
|
}
|
|
|
|
impl AgentTool for CopyPathTool {
|
|
type Input = CopyPathToolInput;
|
|
type Output = String;
|
|
|
|
fn name() -> &'static str {
|
|
"copy_path"
|
|
}
|
|
|
|
fn kind() -> ToolKind {
|
|
ToolKind::Move
|
|
}
|
|
|
|
fn initial_title(
|
|
&self,
|
|
input: Result<Self::Input, serde_json::Value>,
|
|
_cx: &mut App,
|
|
) -> ui::SharedString {
|
|
if let Ok(input) = input {
|
|
let src = MarkdownInlineCode(&input.source_path);
|
|
let dest = MarkdownInlineCode(&input.destination_path);
|
|
format!("Copy {src} to {dest}").into()
|
|
} else {
|
|
"Copy path".into()
|
|
}
|
|
}
|
|
|
|
fn run(
|
|
self: Arc<Self>,
|
|
input: Self::Input,
|
|
event_stream: ToolCallEventStream,
|
|
cx: &mut App,
|
|
) -> Task<Result<Self::Output>> {
|
|
let settings = AgentSettings::get_global(cx);
|
|
|
|
let source_decision =
|
|
decide_permission_from_settings(Self::name(), &input.source_path, settings);
|
|
if let ToolPermissionDecision::Deny(reason) = source_decision {
|
|
return Task::ready(Err(anyhow!("{}", reason)));
|
|
}
|
|
|
|
let dest_decision =
|
|
decide_permission_from_settings(Self::name(), &input.destination_path, settings);
|
|
if let ToolPermissionDecision::Deny(reason) = dest_decision {
|
|
return Task::ready(Err(anyhow!("{}", reason)));
|
|
}
|
|
|
|
let needs_confirmation = matches!(source_decision, ToolPermissionDecision::Confirm)
|
|
|| matches!(dest_decision, ToolPermissionDecision::Confirm);
|
|
|
|
let authorize = if needs_confirmation {
|
|
let src = MarkdownInlineCode(&input.source_path);
|
|
let dest = MarkdownInlineCode(&input.destination_path);
|
|
let context = crate::ToolPermissionContext {
|
|
tool_name: "copy_path".to_string(),
|
|
input_value: input.source_path.clone(),
|
|
};
|
|
Some(event_stream.authorize(format!("Copy {src} to {dest}"), context, cx))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let copy_task = self.project.update(cx, |project, cx| {
|
|
match project
|
|
.find_project_path(&input.source_path, cx)
|
|
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
|
{
|
|
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
|
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
|
|
None => Task::ready(Err(anyhow!(
|
|
"Destination path {} was outside the project.",
|
|
input.destination_path
|
|
))),
|
|
},
|
|
None => Task::ready(Err(anyhow!(
|
|
"Source path {} was not found in the project.",
|
|
input.source_path
|
|
))),
|
|
}
|
|
});
|
|
|
|
cx.background_spawn(async move {
|
|
if let Some(authorize) = authorize {
|
|
authorize.await?;
|
|
}
|
|
|
|
let result = futures::select! {
|
|
result = copy_task.fuse() => result,
|
|
_ = event_stream.cancelled_by_user().fuse() => {
|
|
anyhow::bail!("Copy cancelled by user");
|
|
}
|
|
};
|
|
let _ = result.with_context(|| {
|
|
format!(
|
|
"Copying {} to {}",
|
|
input.source_path, input.destination_path
|
|
)
|
|
})?;
|
|
Ok(format!(
|
|
"Copied {} to {}",
|
|
input.source_path, input.destination_path
|
|
))
|
|
})
|
|
}
|
|
}
|