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>
126 lines
3.9 KiB
Rust
126 lines
3.9 KiB
Rust
use agent_client_protocol::ToolKind;
|
|
use agent_settings::AgentSettings;
|
|
use anyhow::{Context as _, Result, anyhow};
|
|
use futures::FutureExt as _;
|
|
use gpui::{App, Entity, SharedString, Task};
|
|
use project::Project;
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use settings::Settings;
|
|
use std::sync::Arc;
|
|
use util::markdown::MarkdownInlineCode;
|
|
|
|
use crate::{
|
|
AgentTool, ToolCallEventStream, ToolPermissionDecision, decide_permission_from_settings,
|
|
};
|
|
|
|
/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
|
|
///
|
|
/// This tool creates a directory and all necessary parent directories. It should be used whenever you need to create new directories within the project.
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct CreateDirectoryToolInput {
|
|
/// The path of the new directory.
|
|
///
|
|
/// <example>
|
|
/// If the project has the following structure:
|
|
///
|
|
/// - directory1/
|
|
/// - directory2/
|
|
///
|
|
/// You can create a new directory by providing a path of "directory1/new_directory"
|
|
/// </example>
|
|
pub path: String,
|
|
}
|
|
|
|
pub struct CreateDirectoryTool {
|
|
project: Entity<Project>,
|
|
}
|
|
|
|
impl CreateDirectoryTool {
|
|
pub fn new(project: Entity<Project>) -> Self {
|
|
Self { project }
|
|
}
|
|
}
|
|
|
|
impl AgentTool for CreateDirectoryTool {
|
|
type Input = CreateDirectoryToolInput;
|
|
type Output = String;
|
|
|
|
fn name() -> &'static str {
|
|
"create_directory"
|
|
}
|
|
|
|
fn kind() -> ToolKind {
|
|
ToolKind::Read
|
|
}
|
|
|
|
fn initial_title(
|
|
&self,
|
|
input: Result<Self::Input, serde_json::Value>,
|
|
_cx: &mut App,
|
|
) -> SharedString {
|
|
if let Ok(input) = input {
|
|
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
|
|
} else {
|
|
"Create directory".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 decision = decide_permission_from_settings(Self::name(), &input.path, settings);
|
|
|
|
let authorize = match decision {
|
|
ToolPermissionDecision::Allow => None,
|
|
ToolPermissionDecision::Deny(reason) => {
|
|
return Task::ready(Err(anyhow!("{}", reason)));
|
|
}
|
|
ToolPermissionDecision::Confirm => {
|
|
let context = crate::ToolPermissionContext {
|
|
tool_name: "create_directory".to_string(),
|
|
input_value: input.path.clone(),
|
|
};
|
|
Some(event_stream.authorize(
|
|
format!("Create directory {}", MarkdownInlineCode(&input.path)),
|
|
context,
|
|
cx,
|
|
))
|
|
}
|
|
};
|
|
|
|
let project_path = match self.project.read(cx).find_project_path(&input.path, cx) {
|
|
Some(project_path) => project_path,
|
|
None => {
|
|
return Task::ready(Err(anyhow!("Path to create was outside the project")));
|
|
}
|
|
};
|
|
let destination_path: Arc<str> = input.path.as_str().into();
|
|
|
|
let create_entry = self.project.update(cx, |project, cx| {
|
|
project.create_entry(project_path.clone(), true, cx)
|
|
});
|
|
|
|
cx.spawn(async move |_cx| {
|
|
if let Some(authorize) = authorize {
|
|
authorize.await?;
|
|
}
|
|
|
|
futures::select! {
|
|
result = create_entry.fuse() => {
|
|
result.with_context(|| format!("Creating directory {destination_path}"))?;
|
|
}
|
|
_ = event_stream.cancelled_by_user().fuse() => {
|
|
anyhow::bail!("Create directory cancelled by user");
|
|
}
|
|
}
|
|
|
|
Ok(format!("Created directory {destination_path}"))
|
|
})
|
|
}
|
|
}
|