Remove global workspace trust concept (#45129)

Follow-up of https://github.com/zed-industries/zed/pull/44887

Trims the worktree trust mechanism to the actual `worktree`s, so now
"global", workspace-level things like `prettier`, `NodeRuntime`,
`copilot` and global MCP servers are considered as "trusted" a priori.

In the future, a separate mechanism for those will be considered and
added.

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-12-17 18:53:42 +02:00 committed by GitHub
parent f084e20c56
commit ec6702aa73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 77 additions and 799 deletions

View file

@ -7,7 +7,6 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::{ use project::{
ExternalAgentServerName, ExternalAgentServerName,
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME}, agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, wait_for_workspace_trust},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{ use settings::{
@ -264,17 +263,6 @@ impl AgentType {
Self::Custom { .. } => Some(IconName::Sparkle), Self::Custom { .. } => Some(IconName::Sparkle),
} }
} }
fn is_mcp(&self) -> bool {
match self {
Self::NativeAgent => false,
Self::TextThread => false,
Self::Custom { .. } => false,
Self::Gemini => true,
Self::ClaudeCode => true,
Self::Codex => true,
}
}
} }
impl From<ExternalAgent> for AgentType { impl From<ExternalAgent> for AgentType {
@ -455,9 +443,7 @@ pub struct AgentPanel {
pending_serialization: Option<Task<Result<()>>>, pending_serialization: Option<Task<Result<()>>>,
onboarding: Entity<AgentPanelOnboarding>, onboarding: Entity<AgentPanelOnboarding>,
selected_agent: AgentType, selected_agent: AgentType,
new_agent_thread_task: Task<()>,
show_trust_workspace_message: bool, show_trust_workspace_message: bool,
_worktree_trust_subscription: Option<Subscription>,
} }
impl AgentPanel { impl AgentPanel {
@ -681,48 +667,6 @@ impl AgentPanel {
None None
}; };
let mut show_trust_workspace_message = false;
let worktree_trust_subscription =
TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
let has_global_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust_workspace(
project
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from),
cx,
)
});
if has_global_trust {
None
} else {
show_trust_workspace_message = true;
let project = project.clone();
Some(cx.subscribe(
&trusted_worktrees,
move |agent_panel, trusted_worktrees, _, cx| {
let new_show_trust_workspace_message =
!trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust_workspace(
project
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from),
cx,
)
});
if new_show_trust_workspace_message
!= agent_panel.show_trust_workspace_message
{
agent_panel.show_trust_workspace_message =
new_show_trust_workspace_message;
cx.notify();
};
},
))
}
});
let mut panel = Self { let mut panel = Self {
active_view, active_view,
workspace, workspace,
@ -745,14 +689,12 @@ impl AgentPanel {
height: None, height: None,
zoomed: false, zoomed: false,
pending_serialization: None, pending_serialization: None,
new_agent_thread_task: Task::ready(()),
onboarding, onboarding,
acp_history, acp_history,
history_store, history_store,
selected_agent: AgentType::default(), selected_agent: AgentType::default(),
loading: false, loading: false,
show_trust_workspace_message, show_trust_workspace_message: false,
_worktree_trust_subscription: worktree_trust_subscription,
}; };
// Initial sync of agent servers from extensions // Initial sync of agent servers from extensions
@ -945,47 +887,6 @@ impl AgentPanel {
} }
}; };
if ext_agent.is_mcp() {
let wait_task = this.update(cx, |agent_panel, cx| {
agent_panel.project.update(cx, |project, cx| {
wait_for_workspace_trust(
project.remote_connection_options(cx),
"context servers",
cx,
)
})
})?;
if let Some(wait_task) = wait_task {
this.update_in(cx, |agent_panel, window, cx| {
agent_panel.show_trust_workspace_message = true;
cx.notify();
agent_panel.new_agent_thread_task =
cx.spawn_in(window, async move |agent_panel, cx| {
wait_task.await;
let server = ext_agent.server(fs, history);
agent_panel
.update_in(cx, |agent_panel, window, cx| {
agent_panel.show_trust_workspace_message = false;
cx.notify();
agent_panel._external_thread(
server,
resume_thread,
summarize_thread,
workspace,
project,
loading,
ext_agent,
window,
cx,
);
})
.ok();
});
})?;
return Ok(());
}
}
let server = ext_agent.server(fs, history); let server = ext_agent.server(fs, history);
this.update_in(cx, |agent_panel, window, cx| { this.update_in(cx, |agent_panel, window, cx| {
agent_panel._external_thread( agent_panel._external_thread(
@ -1510,36 +1411,6 @@ impl AgentPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let wait_task = if agent.is_mcp() {
self.project.update(cx, |project, cx| {
wait_for_workspace_trust(
project.remote_connection_options(cx),
"context servers",
cx,
)
})
} else {
None
};
if let Some(wait_task) = wait_task {
self.show_trust_workspace_message = true;
cx.notify();
self.new_agent_thread_task = cx.spawn_in(window, async move |agent_panel, cx| {
wait_task.await;
agent_panel
.update_in(cx, |agent_panel, window, cx| {
agent_panel.show_trust_workspace_message = false;
cx.notify();
agent_panel._new_agent_thread(agent, window, cx);
})
.ok();
});
} else {
self._new_agent_thread(agent, window, cx);
}
}
fn _new_agent_thread(&mut self, agent: AgentType, window: &mut Window, cx: &mut Context<Self>) {
match agent { match agent {
AgentType::TextThread => { AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx); window.dispatch_action(NewTextThread.boxed_clone(), cx);

View file

@ -174,16 +174,6 @@ impl ExternalAgent {
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())), Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
} }
} }
pub fn is_mcp(&self) -> bool {
match self {
Self::Gemini => true,
Self::ClaudeCode => true,
Self::Codex => true,
Self::NativeAgent => false,
Self::Custom { .. } => false,
}
}
} }
/// Opens the profile management interface for configuring agent tools and settings. /// Opens the profile management interface for configuring agent tools and settings.

View file

@ -114,7 +114,7 @@ pub fn init(cx: &mut App) -> EpAppState {
tx.send(Some(options)).log_err(); tx.send(Some(options)).log_err();
}) })
.detach(); .detach();
let node_runtime = NodeRuntime::new(client.http_client(), None, rx, None); let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
let extension_host_proxy = ExtensionHostProxy::global(cx); let extension_host_proxy = ExtensionHostProxy::global(cx);

View file

@ -422,7 +422,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
tx.send(Some(options)).log_err(); tx.send(Some(options)).log_err();
}) })
.detach(); .detach();
let node_runtime = NodeRuntime::new(client.http_client(), None, rx, None); let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
let extension_host_proxy = ExtensionHostProxy::global(cx); let extension_host_proxy = ExtensionHostProxy::global(cx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx);

View file

@ -9,8 +9,6 @@ use serde::Deserialize;
use smol::io::BufReader; use smol::io::BufReader;
use smol::{fs, lock::Mutex}; use smol::{fs, lock::Mutex};
use std::fmt::Display; use std::fmt::Display;
use std::future::Future;
use std::pin::Pin;
use std::{ use std::{
env::{self, consts}, env::{self, consts},
ffi::OsString, ffi::OsString,
@ -48,7 +46,6 @@ struct NodeRuntimeState {
last_options: Option<NodeBinaryOptions>, last_options: Option<NodeBinaryOptions>,
options: watch::Receiver<Option<NodeBinaryOptions>>, options: watch::Receiver<Option<NodeBinaryOptions>>,
shell_env_loaded: Shared<oneshot::Receiver<()>>, shell_env_loaded: Shared<oneshot::Receiver<()>>,
trust_task: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
} }
impl NodeRuntime { impl NodeRuntime {
@ -56,11 +53,9 @@ impl NodeRuntime {
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
shell_env_loaded: Option<oneshot::Receiver<()>>, shell_env_loaded: Option<oneshot::Receiver<()>>,
options: watch::Receiver<Option<NodeBinaryOptions>>, options: watch::Receiver<Option<NodeBinaryOptions>>,
trust_task: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
) -> Self { ) -> Self {
NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState {
http, http,
trust_task,
instance: None, instance: None,
last_options: None, last_options: None,
options, options,
@ -75,15 +70,11 @@ impl NodeRuntime {
last_options: None, last_options: None,
options: watch::channel(Some(NodeBinaryOptions::default())).1, options: watch::channel(Some(NodeBinaryOptions::default())).1,
shell_env_loaded: oneshot::channel().1.shared(), shell_env_loaded: oneshot::channel().1.shared(),
trust_task: None,
}))) })))
} }
async fn instance(&self) -> Box<dyn NodeRuntimeTrait> { async fn instance(&self) -> Box<dyn NodeRuntimeTrait> {
let mut state = self.0.lock().await; let mut state = self.0.lock().await;
if let Some(trust_task) = state.trust_task.take() {
trust_task.await;
}
let options = loop { let options = loop {
if let Some(options) = state.options.borrow().as_ref() { if let Some(options) = state.options.borrow().as_ref() {

View file

@ -15,7 +15,6 @@ use util::{ResultExt as _, rel_path::RelPath};
use crate::{ use crate::{
Project, Project,
project_settings::{ContextServerSettings, ProjectSettings}, project_settings::{ContextServerSettings, ProjectSettings},
trusted_worktrees::wait_for_workspace_trust,
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
}; };
@ -333,15 +332,6 @@ impl ContextServerStore {
pub fn start_server(&mut self, server: Arc<ContextServer>, cx: &mut Context<Self>) { pub fn start_server(&mut self, server: Arc<ContextServer>, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let wait_task = this.update(cx, |context_server_store, cx| {
context_server_store.project.update(cx, |project, cx| {
let remote_host = project.remote_connection_options(cx);
wait_for_workspace_trust(remote_host, "context servers", cx)
})
})??;
if let Some(wait_task) = wait_task {
wait_task.await;
}
let this = this.upgrade().context("Context server store dropped")?; let this = this.upgrade().context("Context server store dropped")?;
let settings = this let settings = this
.update(cx, |this, _| { .update(cx, |this, _| {
@ -582,15 +572,6 @@ impl ContextServerStore {
} }
async fn maintain_servers(this: WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> { async fn maintain_servers(this: WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
let wait_task = this.update(cx, |context_server_store, cx| {
context_server_store.project.update(cx, |project, cx| {
let remote_host = project.remote_connection_options(cx);
wait_for_workspace_trust(remote_host, "context servers", cx)
})
})??;
if let Some(wait_task) = wait_task {
wait_task.await;
}
let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| { let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| {
( (
this.context_server_settings.clone(), this.context_server_settings.clone(),

View file

@ -4895,16 +4895,13 @@ impl Project {
.update(|cx| TrustedWorktrees::try_get_global(cx))? .update(|cx| TrustedWorktrees::try_get_global(cx))?
.context("missing trusted worktrees")?; .context("missing trusted worktrees")?;
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| { trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
let mut restricted_paths = envelope let restricted_paths = envelope
.payload .payload
.worktree_ids .worktree_ids
.into_iter() .into_iter()
.map(WorktreeId::from_proto) .map(WorktreeId::from_proto)
.map(PathTrust::Worktree) .map(PathTrust::Worktree)
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if envelope.payload.restrict_workspace {
restricted_paths.insert(PathTrust::Workspace);
}
let remote_host = this let remote_host = this
.read(cx) .read(cx)
.remote_connection_options(cx) .remote_connection_options(cx)

View file

@ -27,36 +27,20 @@
//! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default. //! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default.
//! Each single file worktree requires a separate trust permission, unless a more global level is trusted. //! Each single file worktree requires a separate trust permission, unless a more global level is trusted.
//! //!
//! * "workspace"
//!
//! Even an empty Zed instance with no files or directories open is potentially dangerous: opening an Assistant Panel and creating new external agent thread might require installing and running MCP servers.
//!
//! Disabling the entire panel is possible with ai-related settings.
//! Yet when it's enabled, it's still reasonably safe to use remote AI agents and control their permissions in the Assistant Panel.
//!
//! Unlike that, MCP servers are similar to language servers and may require fetching, installing and running packages or binaries.
//! Given that those servers are not tied to any particular worktree, this level of trust is required to operate any MCP server.
//!
//! Workspace level of trust assumes all single file worktrees are trusted too, for the same host: if we allow global MCP server-related functionality, we can already allow spawning language servers for single file worktrees as well.
//!
//! * "directory worktree" //! * "directory worktree"
//! //!
//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it. //! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
//! Each such worktree requires a separate trust permission, so each separate directory worktree has to be trusted separately, unless a more global level is trusted. //! Each such worktree requires a separate trust permission, so each separate directory worktree has to be trusted separately, unless a more global level is trusted.
//! //!
//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence we also allow workspace level of trust (hence, "single file worktree" level of trust also). //! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence, "single file worktree" level of trust also.
//! //!
//! * "path override" //! * "path override"
//! //!
//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed. //! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees. //! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
//!
//! If we trust multiple projects to install and spawn various language server processes, we can also allow workspace trust requests for MCP servers installation and spawning.
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use gpui::{ use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, WeakEntity};
App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,
};
use remote::RemoteConnectionOptions; use remote::RemoteConnectionOptions;
use rpc::{AnyProtoClient, proto}; use rpc::{AnyProtoClient, proto};
use settings::{Settings as _, WorktreeId}; use settings::{Settings as _, WorktreeId};
@ -132,57 +116,6 @@ pub fn track_worktree_trust(
} }
} }
/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for the host the [`TrustedWorktrees`] was initialized with.
pub fn wait_for_default_workspace_trust(
what_waits: &'static str,
cx: &mut App,
) -> Option<Task<()>> {
let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
wait_for_workspace_trust(
trusted_worktrees.read(cx).remote_host.clone(),
what_waits,
cx,
)
}
/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for a particular host.
pub fn wait_for_workspace_trust(
remote_host: Option<impl Into<RemoteHostLocation>>,
what_waits: &'static str,
cx: &mut App,
) -> Option<Task<()>> {
let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
let remote_host = remote_host.map(|host| host.into());
let remote_host = if trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust_workspace(remote_host.clone(), cx)
}) {
None
} else {
Some(remote_host)
}?;
Some(cx.spawn(async move |cx| {
log::info!("Waiting for workspace to be trusted before starting {what_waits}");
let (tx, restricted_worktrees_task) = smol::channel::bounded::<()>(1);
let Ok(_subscription) = cx.update(|cx| {
cx.subscribe(&trusted_worktrees, move |_, e, _| {
if let TrustedWorktreesEvent::Trusted(trusted_host, trusted_paths) = e {
if trusted_host == &remote_host && trusted_paths.contains(&PathTrust::Workspace)
{
log::info!("Workspace is trusted for {what_waits}");
tx.send_blocking(()).ok();
}
}
})
}) else {
return;
};
restricted_worktrees_task.recv().await.ok();
}))
}
/// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to. /// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>); pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
@ -205,8 +138,6 @@ pub struct TrustedWorktreesStore {
worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>, worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
trusted_paths: TrustedPaths, trusted_paths: TrustedPaths,
restricted: HashSet<WorktreeId>, restricted: HashSet<WorktreeId>,
remote_host: Option<RemoteHostLocation>,
restricted_workspaces: HashSet<Option<RemoteHostLocation>>,
} }
/// An identifier of a host to split the trust questions by. /// An identifier of a host to split the trust questions by.
@ -246,9 +177,6 @@ impl From<RemoteConnectionOptions> for RemoteHostLocation {
/// See module-level documentation on the trust model. /// See module-level documentation on the trust model.
#[derive(Debug, PartialEq, Eq, Clone, Hash)] #[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum PathTrust { pub enum PathTrust {
/// General, no worktrees or files open case.
/// E.g. MCP servers can be spawned from a blank Zed instance, but will do `npm i` and other potentially malicious actions.
Workspace,
/// A worktree that is familiar to this workspace. /// A worktree that is familiar to this workspace.
/// Either a single file or a directory worktree. /// Either a single file or a directory worktree.
Worktree(WorktreeId), Worktree(WorktreeId),
@ -260,9 +188,6 @@ pub enum PathTrust {
impl PathTrust { impl PathTrust {
fn to_proto(&self) -> proto::PathTrust { fn to_proto(&self) -> proto::PathTrust {
match self { match self {
Self::Workspace => proto::PathTrust {
content: Some(proto::path_trust::Content::Workspace(0)),
},
Self::Worktree(worktree_id) => proto::PathTrust { Self::Worktree(worktree_id) => proto::PathTrust {
content: Some(proto::path_trust::Content::WorktreeId( content: Some(proto::path_trust::Content::WorktreeId(
worktree_id.to_proto(), worktree_id.to_proto(),
@ -282,7 +207,6 @@ impl PathTrust {
Self::Worktree(WorktreeId::from_proto(id)) Self::Worktree(WorktreeId::from_proto(id))
} }
proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)), proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
proto::path_trust::Content::Workspace(_) => Self::Workspace,
}) })
} }
} }
@ -322,9 +246,7 @@ impl TrustedWorktreesStore {
} }
let worktree_stores = match worktree_store { let worktree_stores = match worktree_store {
Some(worktree_store) => { Some(worktree_store) => HashMap::from_iter([(worktree_store.downgrade(), remote_host)]),
HashMap::from_iter([(worktree_store.downgrade(), remote_host.clone())])
}
None => HashMap::default(), None => HashMap::default(),
}; };
@ -332,8 +254,6 @@ impl TrustedWorktreesStore {
trusted_paths, trusted_paths,
downstream_client, downstream_client,
upstream_client, upstream_client,
remote_host,
restricted_workspaces: HashSet::default(),
restricted: HashSet::default(), restricted: HashSet::default(),
worktree_stores, worktree_stores,
} }
@ -345,11 +265,9 @@ impl TrustedWorktreesStore {
worktree_store: &Entity<WorktreeStore>, worktree_store: &Entity<WorktreeStore>,
cx: &App, cx: &App,
) -> bool { ) -> bool {
let Some(remote_host) = self.worktree_stores.get(&worktree_store.downgrade()) else { self.worktree_stores
return false; .contains_key(&worktree_store.downgrade())
}; && self.restricted.iter().any(|restricted_worktree| {
self.restricted_workspaces.contains(remote_host)
|| self.restricted.iter().any(|restricted_worktree| {
worktree_store worktree_store
.read(cx) .read(cx)
.worktree_for_id(*restricted_worktree, cx) .worktree_for_id(*restricted_worktree, cx)
@ -366,7 +284,6 @@ impl TrustedWorktreesStore {
remote_host: Option<RemoteHostLocation>, remote_host: Option<RemoteHostLocation>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let mut new_workspace_trusted = false;
let mut new_trusted_single_file_worktrees = HashSet::default(); let mut new_trusted_single_file_worktrees = HashSet::default();
let mut new_trusted_other_worktrees = HashSet::default(); let mut new_trusted_other_worktrees = HashSet::default();
let mut new_trusted_abs_paths = HashSet::default(); let mut new_trusted_abs_paths = HashSet::default();
@ -377,7 +294,6 @@ impl TrustedWorktreesStore {
.flat_map(|current_trusted| current_trusted.iter()), .flat_map(|current_trusted| current_trusted.iter()),
) { ) {
match trusted_path { match trusted_path {
PathTrust::Workspace => new_workspace_trusted = true,
PathTrust::Worktree(worktree_id) => { PathTrust::Worktree(worktree_id) => {
self.restricted.remove(worktree_id); self.restricted.remove(worktree_id);
if let Some((abs_path, is_file, host)) = if let Some((abs_path, is_file, host)) =
@ -388,13 +304,11 @@ impl TrustedWorktreesStore {
new_trusted_single_file_worktrees.insert(*worktree_id); new_trusted_single_file_worktrees.insert(*worktree_id);
} else { } else {
new_trusted_other_worktrees.insert((abs_path, *worktree_id)); new_trusted_other_worktrees.insert((abs_path, *worktree_id));
new_workspace_trusted = true;
} }
} }
} }
} }
PathTrust::AbsPath(path) => { PathTrust::AbsPath(path) => {
new_workspace_trusted = true;
debug_assert!( debug_assert!(
path.is_absolute(), path.is_absolute(),
"Cannot trust non-absolute path {path:?}" "Cannot trust non-absolute path {path:?}"
@ -404,11 +318,6 @@ impl TrustedWorktreesStore {
} }
} }
if new_workspace_trusted {
new_trusted_single_file_worktrees.clear();
self.restricted_workspaces.remove(&remote_host);
trusted_paths.insert(PathTrust::Workspace);
}
new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| { new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
new_trusted_abs_paths new_trusted_abs_paths
.iter() .iter()
@ -428,8 +337,7 @@ impl TrustedWorktreesStore {
if restricted_host != remote_host { if restricted_host != remote_host {
return true; return true;
} }
let retain = (!is_file let retain = (!is_file || new_trusted_other_worktrees.is_empty())
|| (!new_workspace_trusted && new_trusted_other_worktrees.is_empty()))
&& new_trusted_abs_paths.iter().all(|new_trusted_path| { && new_trusted_abs_paths.iter().all(|new_trusted_path| {
!restricted_worktree_path.starts_with(new_trusted_path) !restricted_worktree_path.starts_with(new_trusted_path)
}); });
@ -453,9 +361,6 @@ impl TrustedWorktreesStore {
.into_iter() .into_iter()
.map(PathTrust::Worktree), .map(PathTrust::Worktree),
); );
if trusted_paths.is_empty() && new_workspace_trusted {
trusted_paths.insert(PathTrust::Workspace);
}
} }
cx.emit(TrustedWorktreesEvent::Trusted( cx.emit(TrustedWorktreesEvent::Trusted(
@ -489,13 +394,6 @@ impl TrustedWorktreesStore {
) { ) {
for restricted_path in restricted_paths { for restricted_path in restricted_paths {
match restricted_path { match restricted_path {
PathTrust::Workspace => {
self.restricted_workspaces.insert(remote_host.clone());
cx.emit(TrustedWorktreesEvent::Restricted(
remote_host.clone(),
HashSet::from_iter([PathTrust::Workspace]),
));
}
PathTrust::Worktree(worktree_id) => { PathTrust::Worktree(worktree_id) => {
self.restricted.insert(worktree_id); self.restricted.insert(worktree_id);
cx.emit(TrustedWorktreesEvent::Restricted( cx.emit(TrustedWorktreesEvent::Restricted(
@ -568,7 +466,6 @@ impl TrustedWorktreesStore {
downstream_client downstream_client
.send(proto::RestrictWorktrees { .send(proto::RestrictWorktrees {
project_id: *downstream_project_id, project_id: *downstream_project_id,
restrict_workspace: false,
worktree_ids: vec![worktree_id.to_proto()], worktree_ids: vec![worktree_id.to_proto()],
}) })
.ok(); .ok();
@ -577,7 +474,6 @@ impl TrustedWorktreesStore {
upstream_client upstream_client
.send(proto::RestrictWorktrees { .send(proto::RestrictWorktrees {
project_id: *upstream_project_id, project_id: *upstream_project_id,
restrict_workspace: false,
worktree_ids: vec![worktree_id.to_proto()], worktree_ids: vec![worktree_id.to_proto()],
}) })
.ok(); .ok();
@ -585,61 +481,12 @@ impl TrustedWorktreesStore {
false false
} }
/// Checks whether a certain worktree is trusted globally (or on a larger trust level). /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
/// If not, emits [`TrustedWorktreesEvent::Restricted`] event if checked for the first time and not trusted.
///
/// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
pub fn can_trust_workspace(
&mut self,
remote_host: Option<RemoteHostLocation>,
cx: &mut Context<Self>,
) -> bool {
if ProjectSettings::get_global(cx).session.trust_all_worktrees {
return true;
}
if self.restricted_workspaces.contains(&remote_host) {
return false;
}
if self.trusted_paths.contains_key(&remote_host) {
return true;
}
self.restricted_workspaces.insert(remote_host.clone());
cx.emit(TrustedWorktreesEvent::Restricted(
remote_host.clone(),
HashSet::from_iter([PathTrust::Workspace]),
));
if remote_host == self.remote_host {
if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
downstream_client
.send(proto::RestrictWorktrees {
project_id: *downstream_project_id,
restrict_workspace: true,
worktree_ids: Vec::new(),
})
.ok();
}
if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
upstream_client
.send(proto::RestrictWorktrees {
project_id: *upstream_project_id,
restrict_workspace: true,
worktree_ids: Vec::new(),
})
.ok();
}
}
false
}
/// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_workspace`] method calls) for a particular worktree store on a particular host.
pub fn restricted_worktrees( pub fn restricted_worktrees(
&self, &self,
worktree_store: &WorktreeStore, worktree_store: &WorktreeStore,
remote_host: Option<RemoteHostLocation>,
cx: &App, cx: &App,
) -> HashSet<Option<(WorktreeId, Arc<Path>)>> { ) -> HashSet<(WorktreeId, Arc<Path>)> {
let mut single_file_paths = HashSet::default(); let mut single_file_paths = HashSet::default();
let other_paths = self let other_paths = self
.restricted .restricted
@ -649,19 +496,16 @@ impl TrustedWorktreesStore {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let abs_path = worktree.abs_path(); let abs_path = worktree.abs_path();
if worktree.is_single_file() { if worktree.is_single_file() {
single_file_paths.insert(Some((restricted_worktree_id, abs_path))); single_file_paths.insert((restricted_worktree_id, abs_path));
None None
} else { } else {
Some((restricted_worktree_id, abs_path)) Some((restricted_worktree_id, abs_path))
} }
}) })
.map(Some)
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if !other_paths.is_empty() { if !other_paths.is_empty() {
return other_paths; return other_paths;
} else if self.restricted_workspaces.contains(&remote_host) {
return HashSet::from_iter([None]);
} else { } else {
single_file_paths single_file_paths
} }
@ -670,7 +514,7 @@ impl TrustedWorktreesStore {
/// Switches the "trust nothing" mode to "automatically trust everything". /// Switches the "trust nothing" mode to "automatically trust everything".
/// This does not influence already persisted data, but stops adding new worktrees there. /// This does not influence already persisted data, but stops adding new worktrees there.
pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) { pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
for (remote_host, mut worktrees) in std::mem::take(&mut self.restricted) for (remote_host, worktrees) in std::mem::take(&mut self.restricted)
.into_iter() .into_iter()
.flat_map(|restricted_worktree| { .flat_map(|restricted_worktree| {
let (_, _, host) = self.find_worktree_data(restricted_worktree, cx)?; let (_, _, host) = self.find_worktree_data(restricted_worktree, cx)?;
@ -683,26 +527,15 @@ impl TrustedWorktreesStore {
acc acc
}) })
{ {
if self.restricted_workspaces.remove(&remote_host) {
worktrees.insert(PathTrust::Workspace);
}
self.trust(worktrees, remote_host, cx); self.trust(worktrees, remote_host, cx);
} }
for remote_host in std::mem::take(&mut self.restricted_workspaces) {
self.trust(HashSet::from_iter([PathTrust::Workspace]), remote_host, cx);
}
} }
/// Returns a normalized representation of the trusted paths to store in the DB. /// Returns a normalized representation of the trusted paths to store in the DB.
pub fn trusted_paths_for_serialization( pub fn trusted_paths_for_serialization(
&mut self, &mut self,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> ( ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
HashSet<Option<RemoteHostLocation>>,
HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
) {
let mut new_trusted_workspaces = HashSet::default();
let new_trusted_worktrees = self let new_trusted_worktrees = self
.trusted_paths .trusted_paths
.clone() .clone()
@ -715,16 +548,12 @@ impl TrustedWorktreesStore {
.find_worktree_data(worktree_id, cx) .find_worktree_data(worktree_id, cx)
.map(|(abs_path, ..)| abs_path.to_path_buf()), .map(|(abs_path, ..)| abs_path.to_path_buf()),
PathTrust::AbsPath(abs_path) => Some(abs_path), PathTrust::AbsPath(abs_path) => Some(abs_path),
PathTrust::Workspace => {
new_trusted_workspaces.insert(host.clone());
None
}
}) })
.collect(); .collect();
(host, abs_paths) (host, abs_paths)
}) })
.collect(); .collect();
(new_trusted_workspaces, new_trusted_worktrees) new_trusted_worktrees
} }
fn find_worktree_data( fn find_worktree_data(
@ -888,15 +717,9 @@ mod tests {
assert!(has_restricted, "should have restricted worktrees"); assert!(has_restricted, "should have restricted worktrees");
let restricted = worktree_store.read_with(cx, |ws, cx| { let restricted = worktree_store.read_with(cx, |ws, cx| {
trusted_worktrees trusted_worktrees.read(cx).restricted_worktrees(ws, cx)
.read(cx)
.restricted_worktrees(ws, None, cx)
}); });
assert!( assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
restricted
.iter()
.any(|r| r.as_ref().map(|(id, _)| *id) == Some(worktree_id))
);
events.borrow_mut().clear(); events.borrow_mut().clear();
@ -941,9 +764,7 @@ mod tests {
); );
let restricted_after = worktree_store.read_with(cx, |ws, cx| { let restricted_after = worktree_store.read_with(cx, |ws, cx| {
trusted_worktrees trusted_worktrees.read(cx).restricted_worktrees(ws, cx)
.read(cx)
.restricted_worktrees(ws, None, cx)
}); });
assert!( assert!(
restricted_after.is_empty(), restricted_after.is_empty(),
@ -951,92 +772,6 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_workspace_trust_no_worktrees(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({})).await;
let project = Project::test(fs, Vec::<&Path>::new(), cx).await;
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let trusted_worktrees = init_trust_global(worktree_store, cx);
let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
cx.update({
let events = events.clone();
|cx| {
cx.subscribe(&trusted_worktrees, move |_, event, _| {
events.borrow_mut().push(match event {
TrustedWorktreesEvent::Trusted(host, paths) => {
TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
}
TrustedWorktreesEvent::Restricted(host, paths) => {
TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
}
});
})
}
})
.detach();
let can_trust_workspace =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
!can_trust_workspace,
"workspace should be restricted by default"
);
{
let events = events.borrow();
assert_eq!(events.len(), 1);
match &events[0] {
TrustedWorktreesEvent::Restricted(host, paths) => {
assert!(host.is_none());
assert!(paths.contains(&PathTrust::Workspace));
}
_ => panic!("expected Restricted event"),
}
}
events.borrow_mut().clear();
let can_trust_workspace_again =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
!can_trust_workspace_again,
"workspace should still be restricted"
);
assert!(
events.borrow().is_empty(),
"no duplicate Restricted event on repeated can_trust_workspace"
);
trusted_worktrees.update(cx, |store, cx| {
store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
});
{
let events = events.borrow();
assert_eq!(events.len(), 1);
match &events[0] {
TrustedWorktreesEvent::Trusted(host, paths) => {
assert!(host.is_none());
assert!(paths.contains(&PathTrust::Workspace));
}
_ => panic!("expected Trusted event"),
}
}
let can_trust_workspace_after =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
can_trust_workspace_after,
"workspace should be trusted after trust()"
);
}
#[gpui::test] #[gpui::test]
async fn test_single_file_worktree_trust(cx: &mut TestAppContext) { async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -1122,58 +857,6 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_workspace_trust_unlocks_single_file_worktree(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
.await;
let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let worktree_id = worktree_store.read_with(cx, |store, cx| {
let worktree = store.worktrees().next().unwrap();
let worktree = worktree.read(cx);
assert!(worktree.is_single_file(), "expected single-file worktree");
worktree.id()
});
let trusted_worktrees = init_trust_global(worktree_store, cx);
let can_trust_workspace =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
!can_trust_workspace,
"workspace should be restricted by default"
);
let can_trust_file =
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
assert!(
!can_trust_file,
"single-file worktree should be restricted by default"
);
trusted_worktrees.update(cx, |store, cx| {
store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
});
let can_trust_workspace_after =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
can_trust_workspace_after,
"workspace should be trusted after trust(Workspace)"
);
let can_trust_file_after =
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
assert!(
can_trust_file_after,
"single-file worktree should be trusted after workspace trust"
);
}
#[gpui::test] #[gpui::test]
async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) { async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -1319,47 +1002,6 @@ mod tests {
assert!(can_trust_b, "project_b should now be trusted"); assert!(can_trust_b, "project_b should now be trusted");
} }
#[gpui::test]
async fn test_directory_worktree_trust_enables_workspace(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let worktree_id = worktree_store.read_with(cx, |store, cx| {
let worktree = store.worktrees().next().unwrap();
assert!(!worktree.read(cx).is_single_file());
worktree.read(cx).id()
});
let trusted_worktrees = init_trust_global(worktree_store, cx);
let can_trust_workspace =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
!can_trust_workspace,
"workspace should be restricted initially"
);
trusted_worktrees.update(cx, |store, cx| {
store.trust(
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
None,
cx,
);
});
let can_trust_workspace_after =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
can_trust_workspace_after,
"workspace should be trusted after trusting directory worktree"
);
}
#[gpui::test] #[gpui::test]
async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) { async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -1428,7 +1070,7 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
path!("/workspace"), path!("/root"),
json!({ json!({
"project_a": { "main.rs": "fn main() {}" }, "project_a": { "main.rs": "fn main() {}" },
"project_b": { "lib.rs": "pub fn lib() {}" } "project_b": { "lib.rs": "pub fn lib() {}" }
@ -1439,8 +1081,8 @@ mod tests {
let project = Project::test( let project = Project::test(
fs, fs,
[ [
path!("/workspace/project_a").as_ref(), path!("/root/project_a").as_ref(),
path!("/workspace/project_b").as_ref(), path!("/root/project_b").as_ref(),
], ],
cx, cx,
) )
@ -1464,7 +1106,7 @@ mod tests {
trusted_worktrees.update(cx, |store, cx| { trusted_worktrees.update(cx, |store, cx| {
store.trust( store.trust(
HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/workspace")))]), HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
None, None,
cx, cx,
); );
@ -1539,12 +1181,6 @@ mod tests {
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx)); trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
assert!(!can_trust, "worktree should be restricted initially"); assert!(!can_trust, "worktree should be restricted initially");
} }
let can_trust_workspace =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
!can_trust_workspace,
"workspace should be restricted initially"
);
let has_restricted = trusted_worktrees.read_with(cx, |store, cx| { let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
store.has_restricted_worktrees(&worktree_store, cx) store.has_restricted_worktrees(&worktree_store, cx)
@ -1566,13 +1202,6 @@ mod tests {
); );
} }
let can_trust_workspace =
trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
assert!(
can_trust_workspace,
"workspace should be trusted after auto_trust_all"
);
let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| { let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
store.has_restricted_worktrees(&worktree_store, cx) store.has_restricted_worktrees(&worktree_store, cx)
}); });
@ -1592,100 +1221,6 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_wait_for_global_trust_already_trusted(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let trusted_worktrees = init_trust_global(worktree_store, cx);
trusted_worktrees.update(cx, |store, cx| {
store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
});
let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
assert!(task.is_none(), "should return None when already trusted");
}
#[gpui::test]
async fn test_wait_for_workspace_trust_resolves_on_trust(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let trusted_worktrees = init_trust_global(worktree_store, cx);
let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
assert!(
task.is_some(),
"should return Some(Task) when not yet trusted"
);
let task = task.unwrap();
cx.executor().run_until_parked();
trusted_worktrees.update(cx, |store, cx| {
store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
});
cx.executor().run_until_parked();
task.await;
}
#[gpui::test]
async fn test_wait_for_default_workspace_trust_resolves_on_directory_worktree_trust(
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
let worktree_id = worktree_store.read_with(cx, |store, cx| {
let worktree = store.worktrees().next().unwrap();
assert!(!worktree.read(cx).is_single_file());
worktree.read(cx).id()
});
let trusted_worktrees = init_trust_global(worktree_store, cx);
let task = cx.update(|cx| wait_for_default_workspace_trust("test", cx));
assert!(
task.is_some(),
"should return Some(Task) when not yet trusted"
);
let task = task.unwrap();
cx.executor().run_until_parked();
trusted_worktrees.update(cx, |store, cx| {
store.trust(
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
None,
cx,
);
});
cx.executor().run_until_parked();
task.await;
}
#[gpui::test] #[gpui::test]
async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) { async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -1820,36 +1355,11 @@ mod tests {
let trusted_worktrees = init_trust_global(worktree_store, cx); let trusted_worktrees = init_trust_global(worktree_store, cx);
let host_a: Option<RemoteHostLocation> = None; let host_a: Option<RemoteHostLocation> = None;
let host_b = Some(RemoteHostLocation {
user_name: Some("user".into()),
host_identifier: "remote-host".into(),
});
let can_trust_local = let can_trust_local =
trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx)); trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
assert!(!can_trust_local, "local worktree restricted on host_a"); assert!(!can_trust_local, "local worktree restricted on host_a");
trusted_worktrees.update(cx, |store, cx| {
store.trust(
HashSet::from_iter([PathTrust::Workspace]),
host_b.clone(),
cx,
);
});
let can_trust_workspace_a = trusted_worktrees.update(cx, |store, cx| {
store.can_trust_workspace(host_a.clone(), cx)
});
assert!(
!can_trust_workspace_a,
"host_a workspace should still be restricted"
);
let can_trust_workspace_b = trusted_worktrees.update(cx, |store, cx| {
store.can_trust_workspace(host_b.clone(), cx)
});
assert!(can_trust_workspace_b, "host_b workspace should be trusted");
trusted_worktrees.update(cx, |store, cx| { trusted_worktrees.update(cx, |store, cx| {
store.trust( store.trust(
HashSet::from_iter([PathTrust::Worktree(local_worktree)]), HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
@ -1864,13 +1374,5 @@ mod tests {
can_trust_local_after, can_trust_local_after,
"local worktree should be trusted on host_a" "local worktree should be trusted on host_a"
); );
let can_trust_workspace_a_after = trusted_worktrees.update(cx, |store, cx| {
store.can_trust_workspace(host_a.clone(), cx)
});
assert!(
can_trust_workspace_a_after,
"host_a workspace should be trusted after directory trust"
);
} }
} }

View file

@ -62,7 +62,7 @@ fn main() -> Result<(), anyhow::Error> {
let client = Client::production(cx); let client = Client::production(cx);
let http_client = FakeHttpClient::with_200_response(); let http_client = FakeHttpClient::with_200_response();
let (_, rx) = watch::channel(None); let (_, rx) = watch::channel(None);
let node = NodeRuntime::new(http_client, None, rx, None); let node = NodeRuntime::new(http_client, None, rx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));

View file

@ -166,14 +166,16 @@ message TrustWorktrees {
message PathTrust { message PathTrust {
oneof content { oneof content {
uint64 workspace = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;
string abs_path = 3; string abs_path = 3;
} }
reserved 1;
} }
message RestrictWorktrees { message RestrictWorktrees {
uint64 project_id = 1; uint64 project_id = 1;
bool restrict_workspace = 2;
repeated uint64 worktree_ids = 3; repeated uint64 worktree_ids = 3;
reserved 2;
} }

View file

@ -642,16 +642,13 @@ impl HeadlessProject {
.update(|cx| TrustedWorktrees::try_get_global(cx))? .update(|cx| TrustedWorktrees::try_get_global(cx))?
.context("missing trusted worktrees")?; .context("missing trusted worktrees")?;
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| { trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
let mut restricted_paths = envelope let restricted_paths = envelope
.payload .payload
.worktree_ids .worktree_ids
.into_iter() .into_iter()
.map(WorktreeId::from_proto) .map(WorktreeId::from_proto)
.map(PathTrust::Worktree) .map(PathTrust::Worktree)
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if envelope.payload.restrict_workspace {
restricted_paths.insert(PathTrust::Workspace);
}
trusted_worktrees.restrict(restricted_paths, None, cx); trusted_worktrees.restrict(restricted_paths, None, cx);
})?; })?;
Ok(proto::Ack {}) Ok(proto::Ack {})

View file

@ -36,7 +36,6 @@ use smol::Async;
use smol::channel::{Receiver, Sender}; use smol::channel::{Receiver, Sender};
use smol::io::AsyncReadExt; use smol::io::AsyncReadExt;
use smol::{net::unix::UnixListener, stream::StreamExt as _}; use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::pin::Pin;
use std::{ use std::{
env, env,
ffi::OsStr, ffi::OsStr,
@ -453,13 +452,10 @@ pub fn execute_run(
) )
}; };
let trust_task = trusted_worktrees::wait_for_default_workspace_trust("Node runtime", cx)
.map(|trust_task| Box::pin(trust_task) as Pin<Box<_>>);
let node_runtime = NodeRuntime::new( let node_runtime = NodeRuntime::new(
http_client.clone(), http_client.clone(),
Some(shell_env_loaded_rx), Some(shell_env_loaded_rx),
node_settings_rx, node_settings_rx,
trust_task,
); );
let mut languages = LanguageRegistry::new(cx.background_executor().clone()); let mut languages = LanguageRegistry::new(cx.background_executor().clone());

View file

@ -1919,7 +1919,6 @@ impl WorkspaceDb {
pub(crate) async fn save_trusted_worktrees( pub(crate) async fn save_trusted_worktrees(
&self, &self,
trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>, trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use anyhow::Context as _; use anyhow::Context as _;
use db::sqlez::statement::Statement; use db::sqlez::statement::Statement;
@ -1936,7 +1935,6 @@ impl WorkspaceDb {
.into_iter() .into_iter()
.map(move |abs_path| (Some(abs_path), host.clone())) .map(move |abs_path| (Some(abs_path), host.clone()))
}) })
.chain(trusted_workspaces.into_iter().map(|host| (None, host)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut first_worktree; let mut first_worktree;
let mut last_worktree = 0_usize; let mut last_worktree = 0_usize;
@ -2001,7 +1999,7 @@ VALUES {placeholders};"#
let trusted_worktrees = DB.trusted_worktrees()?; let trusted_worktrees = DB.trusted_worktrees()?;
Ok(trusted_worktrees Ok(trusted_worktrees
.into_iter() .into_iter()
.map(|(abs_path, user_name, host_name)| { .filter_map(|(abs_path, user_name, host_name)| {
let db_host = match (user_name, host_name) { let db_host = match (user_name, host_name) {
(_, None) => None, (_, None) => None,
(None, Some(host_name)) => Some(RemoteHostLocation { (None, Some(host_name)) => Some(RemoteHostLocation {
@ -2014,21 +2012,17 @@ VALUES {placeholders};"#
}), }),
}; };
match abs_path { let abs_path = abs_path?;
Some(abs_path) => { Some(if db_host != host {
if db_host != host { (db_host, PathTrust::AbsPath(abs_path))
(db_host, PathTrust::AbsPath(abs_path)) } else if let Some(worktree_store) = &worktree_store {
} else if let Some(worktree_store) = &worktree_store { find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
find_worktree_in_store(worktree_store.read(cx), &abs_path, cx) .map(PathTrust::Worktree)
.map(PathTrust::Worktree) .map(|trusted_worktree| (host.clone(), trusted_worktree))
.map(|trusted_worktree| (host.clone(), trusted_worktree)) .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
.unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path))) } else {
} else { (db_host, PathTrust::AbsPath(abs_path))
(db_host, PathTrust::AbsPath(abs_path)) })
}
}
None => (db_host, PathTrust::Workspace),
}
}) })
.fold(HashMap::default(), |mut acc, (remote_host, path_trust)| { .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
acc.entry(remote_host) acc.entry(remote_host)

View file

@ -23,7 +23,7 @@ use ui::{
use crate::{DismissDecision, ModalView, ToggleWorktreeSecurity}; use crate::{DismissDecision, ModalView, ToggleWorktreeSecurity};
pub struct SecurityModal { pub struct SecurityModal {
restricted_paths: HashMap<Option<WorktreeId>, RestrictedPath>, restricted_paths: HashMap<WorktreeId, RestrictedPath>,
home_dir: Option<PathBuf>, home_dir: Option<PathBuf>,
trust_parents: bool, trust_parents: bool,
worktree_store: WeakEntity<WorktreeStore>, worktree_store: WeakEntity<WorktreeStore>,
@ -34,7 +34,7 @@ pub struct SecurityModal {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
struct RestrictedPath { struct RestrictedPath {
abs_path: Option<Arc<Path>>, abs_path: Arc<Path>,
is_file: bool, is_file: bool,
host: Option<RemoteHostLocation>, host: Option<RemoteHostLocation>,
} }
@ -103,13 +103,11 @@ impl Render for SecurityModal {
.child(Label::new(header_label)), .child(Label::new(header_label)),
) )
.children(self.restricted_paths.values().map(|restricted_path| { .children(self.restricted_paths.values().map(|restricted_path| {
let abs_path = restricted_path.abs_path.as_ref().and_then(|abs_path| { let abs_path = if restricted_path.is_file {
if restricted_path.is_file { restricted_path.abs_path.parent()
abs_path.parent() } else {
} else { Some(restricted_path.abs_path.as_ref())
Some(abs_path.as_ref()) };
}
});
let label = match abs_path { let label = match abs_path {
Some(abs_path) => match &restricted_path.host { Some(abs_path) => match &restricted_path.host {
@ -254,7 +252,7 @@ impl SecurityModal {
has_restricted_files |= restricted_path.is_file; has_restricted_files |= restricted_path.is_file;
!restricted_path.is_file !restricted_path.is_file
}) })
.filter_map(|restricted_path| restricted_path.abs_path.as_ref()?.parent()) .filter_map(|restricted_path| restricted_path.abs_path.parent())
.collect::<SmallVec<[_; 2]>>(); .collect::<SmallVec<[_; 2]>>();
match available_parents.len() { match available_parents.len() {
0 => { 0 => {
@ -289,19 +287,17 @@ impl SecurityModal {
let mut paths_to_trust = self let mut paths_to_trust = self
.restricted_paths .restricted_paths
.keys() .keys()
.map(|worktree_id| match worktree_id { .copied()
Some(worktree_id) => PathTrust::Worktree(*worktree_id), .map(PathTrust::Worktree)
None => PathTrust::Workspace,
})
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if self.trust_parents { if self.trust_parents {
paths_to_trust.extend(self.restricted_paths.values().filter_map( paths_to_trust.extend(self.restricted_paths.values().filter_map(
|restricted_paths| { |restricted_paths| {
if restricted_paths.is_file { if restricted_paths.is_file {
Some(PathTrust::Workspace) None
} else { } else {
let parent_abs_path = let parent_abs_path =
restricted_paths.abs_path.as_ref()?.parent()?.to_owned(); restricted_paths.abs_path.parent()?.to_owned();
Some(PathTrust::AbsPath(parent_abs_path)) Some(PathTrust::AbsPath(parent_abs_path))
} }
}, },
@ -322,42 +318,22 @@ impl SecurityModal {
pub fn refresh_restricted_paths(&mut self, cx: &mut Context<Self>) { pub fn refresh_restricted_paths(&mut self, cx: &mut Context<Self>) {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
if let Some(worktree_store) = self.worktree_store.upgrade() { if let Some(worktree_store) = self.worktree_store.upgrade() {
let mut new_restricted_worktrees = trusted_worktrees let new_restricted_worktrees = trusted_worktrees
.read(cx) .read(cx)
.restricted_worktrees(worktree_store.read(cx), self.remote_host.clone(), cx) .restricted_worktrees(worktree_store.read(cx), cx)
.into_iter() .into_iter()
.filter_map(|restricted_path| { .filter_map(|(worktree_id, abs_path)| {
let restricted_path = match restricted_path { let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;
Some((worktree_id, abs_path)) => { Some((
let worktree = worktree_id,
worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; RestrictedPath {
( abs_path,
Some(worktree_id), is_file: worktree.read(cx).is_single_file(),
RestrictedPath { host: self.remote_host.clone(),
abs_path: Some(abs_path), },
is_file: worktree.read(cx).is_single_file(), ))
host: self.remote_host.clone(),
},
)
}
None => (
None,
RestrictedPath {
abs_path: None,
is_file: false,
host: self.remote_host.clone(),
},
),
};
Some(restricted_path)
}) })
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
// Do not clutter the UI:
// * trusting regular local worktrees assumes the workspace is trusted either, on the same host.
// * trusting a workspace trusts all single-file worktrees on the same host.
if new_restricted_worktrees.len() > 1 {
new_restricted_worktrees.remove(&None);
}
if self.restricted_paths != new_restricted_worktrees { if self.restricted_paths != new_restricted_worktrees {
self.trust_parents = false; self.trust_parents = false;

View file

@ -1233,21 +1233,18 @@ impl Workspace {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
cx.subscribe(&trusted_worktrees, |workspace, worktrees_store, e, cx| { cx.subscribe(&trusted_worktrees, |workspace, worktrees_store, e, cx| {
if let TrustedWorktreesEvent::Trusted(..) = e { if let TrustedWorktreesEvent::Trusted(..) = e {
let (new_trusted_workspaces, new_trusted_worktrees) = worktrees_store
.update(cx, |worktrees_store, cx| {
worktrees_store.trusted_paths_for_serialization(cx)
});
// Do not persist auto trusted worktrees // Do not persist auto trusted worktrees
if !ProjectSettings::get_global(cx).session.trust_all_worktrees { if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
let new_trusted_worktrees =
worktrees_store.update(cx, |worktrees_store, cx| {
worktrees_store.trusted_paths_for_serialization(cx)
});
let timeout = cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME); let timeout = cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
workspace._schedule_serialize_worktree_trust = workspace._schedule_serialize_worktree_trust =
cx.background_spawn(async move { cx.background_spawn(async move {
timeout.await; timeout.await;
persistence::DB persistence::DB
.save_trusted_worktrees( .save_trusted_worktrees(new_trusted_worktrees)
new_trusted_worktrees,
new_trusted_workspaces,
)
.await .await
.log_err(); .log_err();
}); });

View file

@ -36,7 +36,6 @@ use std::{
env, env,
io::{self, IsTerminal}, io::{self, IsTerminal},
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin,
process, process,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
time::Instant, time::Instant,
@ -484,14 +483,7 @@ pub fn main() {
}) })
.detach(); .detach();
let trust_task = trusted_worktrees::wait_for_default_workspace_trust("Node runtime", cx) let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx);
.map(|trust_task| Box::pin(trust_task) as Pin<Box<_>>);
let node_runtime = NodeRuntime::new(
client.http_client(),
Some(shell_env_loaded_rx),
rx,
trust_task,
);
debug_adapter_extension::init(extension_host_proxy.clone(), cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx);
languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx); languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx);

View file

@ -4,11 +4,11 @@ A worktree in Zed is either a directory or a single file that Zed opens as a sta
Zed opens a worktree every time `zed some/path` is invoked, on drag and dropping a file or directory into Zed, on opening user settings.json, etc. Zed opens a worktree every time `zed some/path` is invoked, on drag and dropping a file or directory into Zed, on opening user settings.json, etc.
Every worktree opened may contain a `.zed/settings.json` file with extra configuration options that may require installing and spawning language servers or MCP servers. Every worktree opened may contain a `.zed/settings.json` file with extra configuration options that may require installing and spawning language servers or MCP servers.
Note that the Zed workspace itself may also perform user-configured MCP server installation and spawning, even if no worktrees are open. In order to provide users the opportunity to make their own choices according to their unique threat model and risk tolerance, all worktrees will be started in Restricted mode, which prevents download and execution of any related items from `.zed/settings.json`. Until configured to trust the worktree(s), Zed will not perform any related untrusted actions and will wait for user confirmation. This gives users a chance to review and understand any pre-configured settings, MCP servers, or language servers associated with a project.
In order to provide users the opportunity to make their own choices according to their unique threat model and risk tolerance, the workspace and all worktrees will be started in Restricted mode, which prevents download and execution of any related items. Until configured to trust the workspace and/or worktrees, Zed will not perform any untrusted actions and will wait for user confirmation. This gives users a chance to review and understand any pre-configured settings, MCP servers, or language servers associated with a project. Note that at this point, Zed trusts the tools it installs itself, hence global entities such as global MCP servers, language servers like prettier and copilot are still in installed and started as usual, independent of worktree trust.
If a worktree is not trusted, Zed will indicate this with an exclamation mark icon in the title bar and a message in the Agent panel. Clicking this icon or using `workspace::ToggleWorktreeSecurity` action will bring up the security modal that allows the user to trust the worktree. If a worktree is not trusted, Zed will indicate this with an exclamation mark icon in the title bar. Clicking this icon or using `workspace::ToggleWorktreeSecurity` action will bring up the security modal that allows the user to trust the worktree.
Trusting any worktree will persist this information between restarts. It's possible to clear all trusted worktrees with `workspace::ClearTrustedWorktrees` command. Trusting any worktree will persist this information between restarts. It's possible to clear all trusted worktrees with `workspace::ClearTrustedWorktrees` command.
This command will restart Zed, to ensure no untrusted settings, language servers or MCP servers persist. This command will restart Zed, to ensure no untrusted settings, language servers or MCP servers persist.
@ -25,7 +25,7 @@ Restricted Mode prevents:
## Configuring broad worktree trust ## Configuring broad worktree trust
By default, Zed won't trust any new worktrees and users will be required to trust each new worktree. Though not recommended, users may elect to trust all worktrees and the current workspace for a given session by configuring the following setting: By default, Zed won't trust any new worktrees and users will be required to trust each new worktree. Though not recommended, users may elect to trust all worktrees by configuring the following setting:
```json [settings] ```json [settings]
"session": { "session": {
@ -47,20 +47,12 @@ A typical scenario where a directory might be open and a single file is subseque
Spawning a language server presents a risk should the language server experience a supply-chain attack; therefore, Zed restricts that by default. Each single file worktree requires a separate trust grant, unless the directory containing it is trusted or all worktrees are trusted. Spawning a language server presents a risk should the language server experience a supply-chain attack; therefore, Zed restricts that by default. Each single file worktree requires a separate trust grant, unless the directory containing it is trusted or all worktrees are trusted.
- "workspace"
Even an empty Zed workspace with no files or directories open presents a risk if new MCP servers are locally configured by the user without review. For instance, opening an Assistant Panel and creating a new external agent thread might require installing and running new user-configured [Model Context Protocol servers](./ai/mcp.md). By default, zed will restrict a new MCP server until the user elects to trust the local workspace. Users may also disable the entire Agent panel if preferred; see [AI Configuration](./ai/configuration.md) for more details.
Workspace trust, permitted by trusting Zed with no worktrees open, allows locally configured resources to be downloaded and executed. Workspace trust is per host and also trusts all single file worktrees from the same host in order to permit all local user-configured MCP and language servers to start.
- "directory worktree" - "directory worktree"
If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it or spawn MCP servers if contained in a project settings file.Therefore, each directory worktree requires a separate trust grant unless a parent directory worktree trust is granted (see below). If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it or spawn MCP servers if contained in a project settings file.Therefore, each directory worktree requires a separate trust grant unless a parent directory worktree trust is granted (see below).
When a directory worktree is trusted, language and MCP servers are permitted to be downloaded and started, hence we also enable workspace trust for the host in question automatically when this occurs. When a directory worktree is trusted, language and MCP servers are permitted to be downloaded and started, hence we also enable single file worktree trust for the host in question automatically when this occurs: this helps when opening single files when using language server features in the trusted directory worktree.
- "parent directory worktree" - "parent directory worktree"
To permit trust decisions for multiple directory worktrees at once, it's possible to trust all subdirectories of a given parent directory worktree opened in Zed by checking the appropriate checkbox. This will grant trust to all its subdirectories, including all current and potential directory worktrees. To permit trust decisions for multiple directory worktrees at once, it's possible to trust all subdirectories of a given parent directory worktree opened in Zed by checking the appropriate checkbox. This will grant trust to all its subdirectories, including all current and potential directory worktrees.
This also automatically enables workspace trust to permit the newly trusted resources to download and start.