trusted_worktrees.rs

   1//! A module, responsible for managing the trust logic in Zed.
   2//!
   3//! It deals with multiple hosts, distinguished by [`RemoteHostLocation`].
   4//! Each [`crate::Project`] and `HeadlessProject` should call [`init_global`], if wants to establish the trust mechanism.
   5//! This will set up a [`gpui::Global`] with [`TrustedWorktrees`] entity that will persist, restore and allow querying for worktree trust.
   6//! It's also possible to subscribe on [`TrustedWorktreesEvent`] events of this entity to track trust changes dynamically.
   7//!
   8//! The implementation can synchronize trust information with the remote hosts: currently, WSL and SSH.
   9//! Docker and Collab remotes do not employ trust mechanism, as manage that themselves.
  10//!
  11//! Unless `trust_all_worktrees` auto trust is enabled, does not trust anything that was not persisted before.
  12//! When dealing with "restricted" and other related concepts in the API, it means all explicitly restricted, after any of the [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_global`] calls.
  13//!
  14//!
  15//!
  16//!
  17//! Path rust hierarchy.
  18//!
  19//! Zed has multiple layers of trust, based on the requests and [`PathTrust`] enum variants.
  20//! From the least to the most trusted level:
  21//!
  22//! * "single file worktree"
  23//!
  24//! After opening an empty Zed it's possible to open just a file, same as after opening a directory in Zed it's possible to open a file outside of this directory.
  25//! Usual scenario for both cases is opening Zed's settings.json file via `zed: open settings file` command: that starts a language server for a new file open, which originates from a newly created, single file worktree.
  26//!
  27//! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default.
  28//! Each single file worktree requires a separate trust permission, unless a more global level is trusted.
  29//!
  30//! * "workspace"
  31//!
  32//! 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.
  33//!
  34//! Disabling the entire panel is possible with ai-related settings.
  35//! Yet when it's enabled, it's still reasonably safe to use remote AI agents and control their permissions in the Assistant Panel.
  36//!
  37//! Unlike that, MCP servers are similar to language servers and may require fetching, installing and running packages or binaries.
  38//! Given that those servers are not tied to any particular worktree, this level of trust is required to operate any MCP server.
  39//!
  40//! 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.
  41//!
  42//! * "directory worktree"
  43//!
  44//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
  45//! 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.
  46//!
  47//! 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).
  48//!
  49//! * "path override"
  50//!
  51//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
  52//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
  53//!
  54//! 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.
  55
  56use collections::{HashMap, HashSet};
  57use gpui::{
  58    App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,
  59};
  60use remote::RemoteConnectionOptions;
  61use rpc::{AnyProtoClient, proto};
  62use settings::{Settings as _, WorktreeId};
  63use std::{
  64    path::{Path, PathBuf},
  65    sync::Arc,
  66};
  67use util::debug_panic;
  68
  69use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
  70
  71pub fn init(
  72    db_trusted_paths: TrustedPaths,
  73    downstream_client: Option<(AnyProtoClient, u64)>,
  74    upstream_client: Option<(AnyProtoClient, u64)>,
  75    cx: &mut App,
  76) {
  77    if TrustedWorktrees::try_get_global(cx).is_none() {
  78        let trusted_worktrees = cx.new(|_| {
  79            TrustedWorktreesStore::new(
  80                db_trusted_paths,
  81                None,
  82                None,
  83                downstream_client,
  84                upstream_client,
  85            )
  86        });
  87        cx.set_global(TrustedWorktrees(trusted_worktrees))
  88    }
  89}
  90
  91/// An initialization call to set up trust global for a particular project (remote or local).
  92pub fn track_worktree_trust(
  93    worktree_store: Entity<WorktreeStore>,
  94    remote_host: Option<RemoteHostLocation>,
  95    downstream_client: Option<(AnyProtoClient, u64)>,
  96    upstream_client: Option<(AnyProtoClient, u64)>,
  97    cx: &mut App,
  98) {
  99    match TrustedWorktrees::try_get_global(cx) {
 100        Some(trusted_worktrees) => {
 101            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
 102                let sync_upstream = trusted_worktrees.upstream_client.as_ref().map(|(_, id)| id)
 103                    != upstream_client.as_ref().map(|(_, id)| id);
 104                trusted_worktrees.downstream_client = downstream_client;
 105                trusted_worktrees.upstream_client = upstream_client;
 106                trusted_worktrees.add_worktree_store(worktree_store, remote_host, cx);
 107
 108                if sync_upstream {
 109                    if let Some((upstream_client, upstream_project_id)) =
 110                        &trusted_worktrees.upstream_client
 111                    {
 112                        let trusted_paths = trusted_worktrees
 113                            .trusted_paths
 114                            .iter()
 115                            .flat_map(|(_, paths)| {
 116                                paths.iter().map(|trusted_path| trusted_path.to_proto())
 117                            })
 118                            .collect::<Vec<_>>();
 119                        if !trusted_paths.is_empty() {
 120                            upstream_client
 121                                .send(proto::TrustWorktrees {
 122                                    project_id: *upstream_project_id,
 123                                    trusted_paths,
 124                                })
 125                                .ok();
 126                        }
 127                    }
 128                }
 129            });
 130        }
 131        None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
 132    }
 133}
 134
 135/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for the host the [`TrustedWorktrees`] was initialized with.
 136pub fn wait_for_default_workspace_trust(
 137    what_waits: &'static str,
 138    cx: &mut App,
 139) -> Option<Task<()>> {
 140    let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
 141    wait_for_workspace_trust(
 142        trusted_worktrees.read(cx).remote_host.clone(),
 143        what_waits,
 144        cx,
 145    )
 146}
 147
 148/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for a particular host.
 149pub fn wait_for_workspace_trust(
 150    remote_host: Option<impl Into<RemoteHostLocation>>,
 151    what_waits: &'static str,
 152    cx: &mut App,
 153) -> Option<Task<()>> {
 154    let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
 155    let remote_host = remote_host.map(|host| host.into());
 156
 157    let remote_host = if trusted_worktrees.update(cx, |trusted_worktrees, cx| {
 158        trusted_worktrees.can_trust_workspace(remote_host.clone(), cx)
 159    }) {
 160        None
 161    } else {
 162        Some(remote_host)
 163    }?;
 164
 165    Some(cx.spawn(async move |cx| {
 166        log::info!("Waiting for workspace to be trusted before starting {what_waits}");
 167        let (tx, restricted_worktrees_task) = smol::channel::bounded::<()>(1);
 168        let Ok(_subscription) = cx.update(|cx| {
 169            cx.subscribe(&trusted_worktrees, move |_, e, _| {
 170                if let TrustedWorktreesEvent::Trusted(trusted_host, trusted_paths) = e {
 171                    if trusted_host == &remote_host && trusted_paths.contains(&PathTrust::Workspace)
 172                    {
 173                        log::info!("Workspace is trusted for {what_waits}");
 174                        tx.send_blocking(()).ok();
 175                    }
 176                }
 177            })
 178        }) else {
 179            return;
 180        };
 181
 182        restricted_worktrees_task.recv().await.ok();
 183    }))
 184}
 185
 186/// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
 187pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
 188
 189impl Global for TrustedWorktrees {}
 190
 191impl TrustedWorktrees {
 192    pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
 193        cx.try_global::<Self>().map(|this| this.0.clone())
 194    }
 195}
 196
 197/// A collection of worktrees that are considered trusted and not trusted.
 198/// This can be used when checking for this criteria before enabling certain features.
 199///
 200/// Emits an event each time the worktree was checked and found not trusted,
 201/// or a certain worktree had been trusted.
 202pub struct TrustedWorktreesStore {
 203    downstream_client: Option<(AnyProtoClient, u64)>,
 204    upstream_client: Option<(AnyProtoClient, u64)>,
 205    worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
 206    trusted_paths: TrustedPaths,
 207    restricted: HashSet<WorktreeId>,
 208    remote_host: Option<RemoteHostLocation>,
 209    restricted_workspaces: HashSet<Option<RemoteHostLocation>>,
 210}
 211
 212/// An identifier of a host to split the trust questions by.
 213/// Each trusted data change and event is done for a particular host.
 214/// A host may contain more than one worktree or even project open concurrently.
 215#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 216pub struct RemoteHostLocation {
 217    pub user_name: Option<SharedString>,
 218    pub host_identifier: SharedString,
 219}
 220
 221impl From<RemoteConnectionOptions> for RemoteHostLocation {
 222    fn from(options: RemoteConnectionOptions) -> Self {
 223        let (user_name, host_name) = match options {
 224            RemoteConnectionOptions::Ssh(ssh) => (
 225                ssh.username.map(SharedString::new),
 226                SharedString::new(ssh.host.to_string()),
 227            ),
 228            RemoteConnectionOptions::Wsl(wsl) => (
 229                wsl.user.map(SharedString::new),
 230                SharedString::new(wsl.distro_name),
 231            ),
 232            RemoteConnectionOptions::Docker(docker_connection_options) => (
 233                Some(SharedString::new(docker_connection_options.name)),
 234                SharedString::new(docker_connection_options.container_id),
 235            ),
 236        };
 237        RemoteHostLocation {
 238            user_name,
 239            host_identifier: host_name,
 240        }
 241    }
 242}
 243
 244/// A unit of trust consideration inside a particular host:
 245/// either a familiar worktree, or a path that may influence other worktrees' trust.
 246/// See module-level documentation on the trust model.
 247#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 248pub enum PathTrust {
 249    /// General, no worktrees or files open case.
 250    /// E.g. MCP servers can be spawned from a blank Zed instance, but will do `npm i` and other potentially malicious actions.
 251    Workspace,
 252    /// A worktree that is familiar to this workspace.
 253    /// Either a single file or a directory worktree.
 254    Worktree(WorktreeId),
 255    /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),
 256    /// or a parent path coming out of the security modal.
 257    AbsPath(PathBuf),
 258}
 259
 260impl PathTrust {
 261    fn to_proto(&self) -> proto::PathTrust {
 262        match self {
 263            Self::Workspace => proto::PathTrust {
 264                content: Some(proto::path_trust::Content::Workspace(0)),
 265            },
 266            Self::Worktree(worktree_id) => proto::PathTrust {
 267                content: Some(proto::path_trust::Content::WorktreeId(
 268                    worktree_id.to_proto(),
 269                )),
 270            },
 271            Self::AbsPath(path_buf) => proto::PathTrust {
 272                content: Some(proto::path_trust::Content::AbsPath(
 273                    path_buf.to_string_lossy().to_string(),
 274                )),
 275            },
 276        }
 277    }
 278
 279    pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {
 280        Some(match proto.content? {
 281            proto::path_trust::Content::WorktreeId(id) => {
 282                Self::Worktree(WorktreeId::from_proto(id))
 283            }
 284            proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
 285            proto::path_trust::Content::Workspace(_) => Self::Workspace,
 286        })
 287    }
 288}
 289
 290/// A change of trust on a certain host.
 291#[derive(Debug)]
 292pub enum TrustedWorktreesEvent {
 293    Trusted(Option<RemoteHostLocation>, HashSet<PathTrust>),
 294    Restricted(Option<RemoteHostLocation>, HashSet<PathTrust>),
 295}
 296
 297impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
 298
 299pub type TrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>;
 300
 301impl TrustedWorktreesStore {
 302    fn new(
 303        trusted_paths: TrustedPaths,
 304        worktree_store: Option<Entity<WorktreeStore>>,
 305        remote_host: Option<RemoteHostLocation>,
 306        downstream_client: Option<(AnyProtoClient, u64)>,
 307        upstream_client: Option<(AnyProtoClient, u64)>,
 308    ) -> Self {
 309        if let Some((upstream_client, upstream_project_id)) = &upstream_client {
 310            let trusted_paths = trusted_paths
 311                .iter()
 312                .flat_map(|(_, paths)| paths.iter().map(|trusted_path| trusted_path.to_proto()))
 313                .collect::<Vec<_>>();
 314            if !trusted_paths.is_empty() {
 315                upstream_client
 316                    .send(proto::TrustWorktrees {
 317                        project_id: *upstream_project_id,
 318                        trusted_paths,
 319                    })
 320                    .ok();
 321            }
 322        }
 323
 324        let worktree_stores = match worktree_store {
 325            Some(worktree_store) => {
 326                HashMap::from_iter([(worktree_store.downgrade(), remote_host.clone())])
 327            }
 328            None => HashMap::default(),
 329        };
 330
 331        Self {
 332            trusted_paths,
 333            downstream_client,
 334            upstream_client,
 335            remote_host,
 336            restricted_workspaces: HashSet::default(),
 337            restricted: HashSet::default(),
 338            worktree_stores,
 339        }
 340    }
 341
 342    /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.
 343    pub fn has_restricted_worktrees(
 344        &self,
 345        worktree_store: &Entity<WorktreeStore>,
 346        cx: &App,
 347    ) -> bool {
 348        let Some(remote_host) = self.worktree_stores.get(&worktree_store.downgrade()) else {
 349            return false;
 350        };
 351        self.restricted_workspaces.contains(remote_host)
 352            || self.restricted.iter().any(|restricted_worktree| {
 353                worktree_store
 354                    .read(cx)
 355                    .worktree_for_id(*restricted_worktree, cx)
 356                    .is_some()
 357            })
 358    }
 359
 360    /// Adds certain entities on this host to the trusted list.
 361    /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries
 362    /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).
 363    pub fn trust(
 364        &mut self,
 365        mut trusted_paths: HashSet<PathTrust>,
 366        remote_host: Option<RemoteHostLocation>,
 367        cx: &mut Context<Self>,
 368    ) {
 369        let mut new_workspace_trusted = false;
 370        let mut new_trusted_single_file_worktrees = HashSet::default();
 371        let mut new_trusted_other_worktrees = HashSet::default();
 372        let mut new_trusted_abs_paths = HashSet::default();
 373        for trusted_path in trusted_paths.iter().chain(
 374            self.trusted_paths
 375                .remove(&remote_host)
 376                .iter()
 377                .flat_map(|current_trusted| current_trusted.iter()),
 378        ) {
 379            match trusted_path {
 380                PathTrust::Workspace => new_workspace_trusted = true,
 381                PathTrust::Worktree(worktree_id) => {
 382                    self.restricted.remove(worktree_id);
 383                    if let Some((abs_path, is_file, host)) =
 384                        self.find_worktree_data(*worktree_id, cx)
 385                    {
 386                        if host == remote_host {
 387                            if is_file {
 388                                new_trusted_single_file_worktrees.insert(*worktree_id);
 389                            } else {
 390                                new_trusted_other_worktrees.insert((abs_path, *worktree_id));
 391                                new_workspace_trusted = true;
 392                            }
 393                        }
 394                    }
 395                }
 396                PathTrust::AbsPath(path) => {
 397                    new_workspace_trusted = true;
 398                    debug_assert!(
 399                        path.is_absolute(),
 400                        "Cannot trust non-absolute path {path:?}"
 401                    );
 402                    new_trusted_abs_paths.insert(path.clone());
 403                }
 404            }
 405        }
 406
 407        if new_workspace_trusted {
 408            new_trusted_single_file_worktrees.clear();
 409            self.restricted_workspaces.remove(&remote_host);
 410            trusted_paths.insert(PathTrust::Workspace);
 411        }
 412        new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
 413            new_trusted_abs_paths
 414                .iter()
 415                .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))
 416        });
 417        if !new_trusted_other_worktrees.is_empty() {
 418            new_trusted_single_file_worktrees.clear();
 419        }
 420        self.restricted = std::mem::take(&mut self.restricted)
 421            .into_iter()
 422            .filter(|restricted_worktree| {
 423                let Some((restricted_worktree_path, is_file, restricted_host)) =
 424                    self.find_worktree_data(*restricted_worktree, cx)
 425                else {
 426                    return false;
 427                };
 428                if restricted_host != remote_host {
 429                    return true;
 430                }
 431                let retain = (!is_file
 432                    || (!new_workspace_trusted && new_trusted_other_worktrees.is_empty()))
 433                    && new_trusted_abs_paths.iter().all(|new_trusted_path| {
 434                        !restricted_worktree_path.starts_with(new_trusted_path)
 435                    });
 436                if !retain {
 437                    trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 438                }
 439                retain
 440            })
 441            .collect();
 442
 443        {
 444            let trusted_paths = self.trusted_paths.entry(remote_host.clone()).or_default();
 445            trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
 446            trusted_paths.extend(
 447                new_trusted_other_worktrees
 448                    .into_iter()
 449                    .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
 450            );
 451            trusted_paths.extend(
 452                new_trusted_single_file_worktrees
 453                    .into_iter()
 454                    .map(PathTrust::Worktree),
 455            );
 456            if trusted_paths.is_empty() && new_workspace_trusted {
 457                trusted_paths.insert(PathTrust::Workspace);
 458            }
 459        }
 460
 461        cx.emit(TrustedWorktreesEvent::Trusted(
 462            remote_host,
 463            trusted_paths.clone(),
 464        ));
 465
 466        if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 467            let trusted_paths = trusted_paths
 468                .iter()
 469                .map(|trusted_path| trusted_path.to_proto())
 470                .collect::<Vec<_>>();
 471            if !trusted_paths.is_empty() {
 472                upstream_client
 473                    .send(proto::TrustWorktrees {
 474                        project_id: *upstream_project_id,
 475                        trusted_paths,
 476                    })
 477                    .ok();
 478            }
 479        }
 480    }
 481
 482    /// Restricts certain entities on this host.
 483    /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
 484    pub fn restrict(
 485        &mut self,
 486        restricted_paths: HashSet<PathTrust>,
 487        remote_host: Option<RemoteHostLocation>,
 488        cx: &mut Context<Self>,
 489    ) {
 490        for restricted_path in restricted_paths {
 491            match restricted_path {
 492                PathTrust::Workspace => {
 493                    self.restricted_workspaces.insert(remote_host.clone());
 494                    cx.emit(TrustedWorktreesEvent::Restricted(
 495                        remote_host.clone(),
 496                        HashSet::from_iter([PathTrust::Workspace]),
 497                    ));
 498                }
 499                PathTrust::Worktree(worktree_id) => {
 500                    self.restricted.insert(worktree_id);
 501                    cx.emit(TrustedWorktreesEvent::Restricted(
 502                        remote_host.clone(),
 503                        HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 504                    ));
 505                }
 506                PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
 507            }
 508        }
 509    }
 510
 511    /// Erases all trust information.
 512    /// Requires Zed's restart to take proper effect.
 513    pub fn clear_trusted_paths(&mut self) {
 514        self.trusted_paths.clear();
 515    }
 516
 517    /// Checks whether a certain worktree is trusted (or on a larger trust level).
 518    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
 519    ///
 520    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
 521    pub fn can_trust(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) -> bool {
 522        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 523            return true;
 524        }
 525        if self.restricted.contains(&worktree_id) {
 526            return false;
 527        }
 528
 529        let Some((worktree_path, is_file, remote_host)) = self.find_worktree_data(worktree_id, cx)
 530        else {
 531            return false;
 532        };
 533
 534        if self
 535            .trusted_paths
 536            .get(&remote_host)
 537            .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
 538        {
 539            return true;
 540        }
 541
 542        // See module documentation for details on trust level.
 543        if is_file && self.trusted_paths.contains_key(&remote_host) {
 544            return true;
 545        }
 546
 547        let parent_path_trusted =
 548            self.trusted_paths
 549                .get(&remote_host)
 550                .is_some_and(|trusted_paths| {
 551                    trusted_paths.iter().any(|trusted_path| {
 552                        let PathTrust::AbsPath(trusted_path) = trusted_path else {
 553                            return false;
 554                        };
 555                        worktree_path.starts_with(trusted_path)
 556                    })
 557                });
 558        if parent_path_trusted {
 559            return true;
 560        }
 561
 562        self.restricted.insert(worktree_id);
 563        cx.emit(TrustedWorktreesEvent::Restricted(
 564            remote_host,
 565            HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 566        ));
 567        if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
 568            downstream_client
 569                .send(proto::RestrictWorktrees {
 570                    project_id: *downstream_project_id,
 571                    restrict_workspace: false,
 572                    worktree_ids: vec![worktree_id.to_proto()],
 573                })
 574                .ok();
 575        }
 576        if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 577            upstream_client
 578                .send(proto::RestrictWorktrees {
 579                    project_id: *upstream_project_id,
 580                    restrict_workspace: false,
 581                    worktree_ids: vec![worktree_id.to_proto()],
 582                })
 583                .ok();
 584        }
 585        false
 586    }
 587
 588    /// Checks whether a certain worktree is trusted globally (or on a larger trust level).
 589    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if checked for the first time and not trusted.
 590    ///
 591    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
 592    pub fn can_trust_workspace(
 593        &mut self,
 594        remote_host: Option<RemoteHostLocation>,
 595        cx: &mut Context<Self>,
 596    ) -> bool {
 597        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 598            return true;
 599        }
 600        if self.restricted_workspaces.contains(&remote_host) {
 601            return false;
 602        }
 603        if self.trusted_paths.contains_key(&remote_host) {
 604            return true;
 605        }
 606
 607        self.restricted_workspaces.insert(remote_host.clone());
 608        cx.emit(TrustedWorktreesEvent::Restricted(
 609            remote_host.clone(),
 610            HashSet::from_iter([PathTrust::Workspace]),
 611        ));
 612
 613        if remote_host == self.remote_host {
 614            if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
 615                downstream_client
 616                    .send(proto::RestrictWorktrees {
 617                        project_id: *downstream_project_id,
 618                        restrict_workspace: true,
 619                        worktree_ids: Vec::new(),
 620                    })
 621                    .ok();
 622            }
 623            if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 624                upstream_client
 625                    .send(proto::RestrictWorktrees {
 626                        project_id: *upstream_project_id,
 627                        restrict_workspace: true,
 628                        worktree_ids: Vec::new(),
 629                    })
 630                    .ok();
 631            }
 632        }
 633        false
 634    }
 635
 636    /// 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.
 637    pub fn restricted_worktrees(
 638        &self,
 639        worktree_store: &WorktreeStore,
 640        remote_host: Option<RemoteHostLocation>,
 641        cx: &App,
 642    ) -> HashSet<Option<(WorktreeId, Arc<Path>)>> {
 643        let mut single_file_paths = HashSet::default();
 644        let other_paths = self
 645            .restricted
 646            .iter()
 647            .filter_map(|&restricted_worktree_id| {
 648                let worktree = worktree_store.worktree_for_id(restricted_worktree_id, cx)?;
 649                let worktree = worktree.read(cx);
 650                let abs_path = worktree.abs_path();
 651                if worktree.is_single_file() {
 652                    single_file_paths.insert(Some((restricted_worktree_id, abs_path)));
 653                    None
 654                } else {
 655                    Some((restricted_worktree_id, abs_path))
 656                }
 657            })
 658            .map(Some)
 659            .collect::<HashSet<_>>();
 660
 661        if !other_paths.is_empty() {
 662            return other_paths;
 663        } else if self.restricted_workspaces.contains(&remote_host) {
 664            return HashSet::from_iter([None]);
 665        } else {
 666            single_file_paths
 667        }
 668    }
 669
 670    /// Switches the "trust nothing" mode to "automatically trust everything".
 671    /// This does not influence already persisted data, but stops adding new worktrees there.
 672    pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
 673        for (remote_host, mut worktrees) in std::mem::take(&mut self.restricted)
 674            .into_iter()
 675            .flat_map(|restricted_worktree| {
 676                let (_, _, host) = self.find_worktree_data(restricted_worktree, cx)?;
 677                Some((restricted_worktree, host))
 678            })
 679            .fold(HashMap::default(), |mut acc, (worktree_id, remote_host)| {
 680                acc.entry(remote_host)
 681                    .or_insert_with(HashSet::default)
 682                    .insert(PathTrust::Worktree(worktree_id));
 683                acc
 684            })
 685        {
 686            if self.restricted_workspaces.remove(&remote_host) {
 687                worktrees.insert(PathTrust::Workspace);
 688            }
 689            self.trust(worktrees, remote_host, cx);
 690        }
 691
 692        for remote_host in std::mem::take(&mut self.restricted_workspaces) {
 693            self.trust(HashSet::from_iter([PathTrust::Workspace]), remote_host, cx);
 694        }
 695    }
 696
 697    /// Returns a normalized representation of the trusted paths to store in the DB.
 698    pub fn trusted_paths_for_serialization(
 699        &mut self,
 700        cx: &mut Context<Self>,
 701    ) -> (
 702        HashSet<Option<RemoteHostLocation>>,
 703        HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
 704    ) {
 705        let mut new_trusted_workspaces = HashSet::default();
 706        let new_trusted_worktrees = self
 707            .trusted_paths
 708            .clone()
 709            .into_iter()
 710            .map(|(host, paths)| {
 711                let abs_paths = paths
 712                    .into_iter()
 713                    .flat_map(|path| match path {
 714                        PathTrust::Worktree(worktree_id) => self
 715                            .find_worktree_data(worktree_id, cx)
 716                            .map(|(abs_path, ..)| abs_path.to_path_buf()),
 717                        PathTrust::AbsPath(abs_path) => Some(abs_path),
 718                        PathTrust::Workspace => {
 719                            new_trusted_workspaces.insert(host.clone());
 720                            None
 721                        }
 722                    })
 723                    .collect();
 724                (host, abs_paths)
 725            })
 726            .collect();
 727        (new_trusted_workspaces, new_trusted_worktrees)
 728    }
 729
 730    fn find_worktree_data(
 731        &mut self,
 732        worktree_id: WorktreeId,
 733        cx: &mut Context<Self>,
 734    ) -> Option<(Arc<Path>, bool, Option<RemoteHostLocation>)> {
 735        let mut worktree_data = None;
 736        self.worktree_stores.retain(
 737            |worktree_store, remote_host| match worktree_store.upgrade() {
 738                Some(worktree_store) => {
 739                    if worktree_data.is_none() {
 740                        if let Some(worktree) =
 741                            worktree_store.read(cx).worktree_for_id(worktree_id, cx)
 742                        {
 743                            worktree_data = Some((
 744                                worktree.read(cx).abs_path(),
 745                                worktree.read(cx).is_single_file(),
 746                                remote_host.clone(),
 747                            ));
 748                        }
 749                    }
 750                    true
 751                }
 752                None => false,
 753            },
 754        );
 755        worktree_data
 756    }
 757
 758    fn add_worktree_store(
 759        &mut self,
 760        worktree_store: Entity<WorktreeStore>,
 761        remote_host: Option<RemoteHostLocation>,
 762        cx: &mut Context<Self>,
 763    ) {
 764        self.worktree_stores
 765            .insert(worktree_store.downgrade(), remote_host.clone());
 766
 767        if let Some(trusted_paths) = self.trusted_paths.remove(&remote_host) {
 768            self.trusted_paths.insert(
 769                remote_host.clone(),
 770                trusted_paths
 771                    .into_iter()
 772                    .map(|path_trust| match path_trust {
 773                        PathTrust::AbsPath(abs_path) => {
 774                            find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
 775                                .map(PathTrust::Worktree)
 776                                .unwrap_or_else(|| PathTrust::AbsPath(abs_path))
 777                        }
 778                        other => other,
 779                    })
 780                    .collect(),
 781            );
 782        }
 783    }
 784}
 785
 786pub fn find_worktree_in_store(
 787    worktree_store: &WorktreeStore,
 788    abs_path: &Path,
 789    cx: &App,
 790) -> Option<WorktreeId> {
 791    let (worktree, path_in_worktree) = worktree_store.find_worktree(&abs_path, cx)?;
 792    if path_in_worktree.is_empty() {
 793        Some(worktree.read(cx).id())
 794    } else {
 795        None
 796    }
 797}
 798
 799#[cfg(test)]
 800mod tests {
 801    use std::{cell::RefCell, path::PathBuf, rc::Rc};
 802
 803    use collections::HashSet;
 804    use gpui::TestAppContext;
 805    use serde_json::json;
 806    use settings::SettingsStore;
 807    use util::path;
 808
 809    use crate::{FakeFs, Project};
 810
 811    use super::*;
 812
 813    fn init_test(cx: &mut TestAppContext) {
 814        cx.update(|cx| {
 815            if cx.try_global::<SettingsStore>().is_none() {
 816                let settings_store = SettingsStore::test(cx);
 817                cx.set_global(settings_store);
 818            }
 819            if cx.try_global::<TrustedWorktrees>().is_some() {
 820                cx.remove_global::<TrustedWorktrees>();
 821            }
 822        });
 823    }
 824
 825    fn init_trust_global(
 826        worktree_store: Entity<WorktreeStore>,
 827        cx: &mut TestAppContext,
 828    ) -> Entity<TrustedWorktreesStore> {
 829        cx.update(|cx| {
 830            init(HashMap::default(), None, None, cx);
 831            track_worktree_trust(worktree_store, None, None, None, cx);
 832            TrustedWorktrees::try_get_global(cx).expect("global should be set")
 833        })
 834    }
 835
 836    #[gpui::test]
 837    async fn test_single_worktree_trust(cx: &mut TestAppContext) {
 838        init_test(cx);
 839
 840        let fs = FakeFs::new(cx.executor());
 841        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
 842            .await;
 843
 844        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 845        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 846        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 847            store.worktrees().next().unwrap().read(cx).id()
 848        });
 849
 850        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
 851
 852        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 853        cx.update({
 854            let events = events.clone();
 855            |cx| {
 856                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 857                    events.borrow_mut().push(match event {
 858                        TrustedWorktreesEvent::Trusted(host, paths) => {
 859                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 860                        }
 861                        TrustedWorktreesEvent::Restricted(host, paths) => {
 862                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 863                        }
 864                    });
 865                })
 866            }
 867        })
 868        .detach();
 869
 870        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 871        assert!(!can_trust, "worktree should be restricted by default");
 872
 873        {
 874            let events = events.borrow();
 875            assert_eq!(events.len(), 1);
 876            match &events[0] {
 877                TrustedWorktreesEvent::Restricted(host, paths) => {
 878                    assert!(host.is_none());
 879                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 880                }
 881                _ => panic!("expected Restricted event"),
 882            }
 883        }
 884
 885        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
 886            store.has_restricted_worktrees(&worktree_store, cx)
 887        });
 888        assert!(has_restricted, "should have restricted worktrees");
 889
 890        let restricted = worktree_store.read_with(cx, |ws, cx| {
 891            trusted_worktrees
 892                .read(cx)
 893                .restricted_worktrees(ws, None, cx)
 894        });
 895        assert!(
 896            restricted
 897                .iter()
 898                .any(|r| r.as_ref().map(|(id, _)| *id) == Some(worktree_id))
 899        );
 900
 901        events.borrow_mut().clear();
 902
 903        let can_trust_again =
 904            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 905        assert!(!can_trust_again, "worktree should still be restricted");
 906        assert!(
 907            events.borrow().is_empty(),
 908            "no duplicate Restricted event on repeated can_trust"
 909        );
 910
 911        trusted_worktrees.update(cx, |store, cx| {
 912            store.trust(
 913                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 914                None,
 915                cx,
 916            );
 917        });
 918
 919        {
 920            let events = events.borrow();
 921            assert_eq!(events.len(), 1);
 922            match &events[0] {
 923                TrustedWorktreesEvent::Trusted(host, paths) => {
 924                    assert!(host.is_none());
 925                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 926                }
 927                _ => panic!("expected Trusted event"),
 928            }
 929        }
 930
 931        let can_trust_after =
 932            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
 933        assert!(can_trust_after, "worktree should be trusted after trust()");
 934
 935        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
 936            store.has_restricted_worktrees(&worktree_store, cx)
 937        });
 938        assert!(
 939            !has_restricted_after,
 940            "should have no restricted worktrees after trust"
 941        );
 942
 943        let restricted_after = worktree_store.read_with(cx, |ws, cx| {
 944            trusted_worktrees
 945                .read(cx)
 946                .restricted_worktrees(ws, None, cx)
 947        });
 948        assert!(
 949            restricted_after.is_empty(),
 950            "restricted set should be empty"
 951        );
 952    }
 953
 954    #[gpui::test]
 955    async fn test_workspace_trust_no_worktrees(cx: &mut TestAppContext) {
 956        init_test(cx);
 957
 958        let fs = FakeFs::new(cx.executor());
 959        fs.insert_tree(path!("/root"), json!({})).await;
 960
 961        let project = Project::test(fs, Vec::<&Path>::new(), cx).await;
 962        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 963
 964        let trusted_worktrees = init_trust_global(worktree_store, cx);
 965
 966        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 967        cx.update({
 968            let events = events.clone();
 969            |cx| {
 970                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 971                    events.borrow_mut().push(match event {
 972                        TrustedWorktreesEvent::Trusted(host, paths) => {
 973                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 974                        }
 975                        TrustedWorktreesEvent::Restricted(host, paths) => {
 976                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 977                        }
 978                    });
 979                })
 980            }
 981        })
 982        .detach();
 983
 984        let can_trust_workspace =
 985            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
 986        assert!(
 987            !can_trust_workspace,
 988            "workspace should be restricted by default"
 989        );
 990
 991        {
 992            let events = events.borrow();
 993            assert_eq!(events.len(), 1);
 994            match &events[0] {
 995                TrustedWorktreesEvent::Restricted(host, paths) => {
 996                    assert!(host.is_none());
 997                    assert!(paths.contains(&PathTrust::Workspace));
 998                }
 999                _ => panic!("expected Restricted event"),
1000            }
1001        }
1002
1003        events.borrow_mut().clear();
1004
1005        let can_trust_workspace_again =
1006            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1007        assert!(
1008            !can_trust_workspace_again,
1009            "workspace should still be restricted"
1010        );
1011        assert!(
1012            events.borrow().is_empty(),
1013            "no duplicate Restricted event on repeated can_trust_workspace"
1014        );
1015
1016        trusted_worktrees.update(cx, |store, cx| {
1017            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1018        });
1019
1020        {
1021            let events = events.borrow();
1022            assert_eq!(events.len(), 1);
1023            match &events[0] {
1024                TrustedWorktreesEvent::Trusted(host, paths) => {
1025                    assert!(host.is_none());
1026                    assert!(paths.contains(&PathTrust::Workspace));
1027                }
1028                _ => panic!("expected Trusted event"),
1029            }
1030        }
1031
1032        let can_trust_workspace_after =
1033            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1034        assert!(
1035            can_trust_workspace_after,
1036            "workspace should be trusted after trust()"
1037        );
1038    }
1039
1040    #[gpui::test]
1041    async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
1042        init_test(cx);
1043
1044        let fs = FakeFs::new(cx.executor());
1045        fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
1046            .await;
1047
1048        let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
1049        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1050        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1051            let worktree = store.worktrees().next().unwrap();
1052            let worktree = worktree.read(cx);
1053            assert!(worktree.is_single_file(), "expected single-file worktree");
1054            worktree.id()
1055        });
1056
1057        let trusted_worktrees = init_trust_global(worktree_store, cx);
1058
1059        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1060        cx.update({
1061            let events = events.clone();
1062            |cx| {
1063                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1064                    events.borrow_mut().push(match event {
1065                        TrustedWorktreesEvent::Trusted(host, paths) => {
1066                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1067                        }
1068                        TrustedWorktreesEvent::Restricted(host, paths) => {
1069                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1070                        }
1071                    });
1072                })
1073            }
1074        })
1075        .detach();
1076
1077        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1078        assert!(
1079            !can_trust,
1080            "single-file worktree should be restricted by default"
1081        );
1082
1083        {
1084            let events = events.borrow();
1085            assert_eq!(events.len(), 1);
1086            match &events[0] {
1087                TrustedWorktreesEvent::Restricted(host, paths) => {
1088                    assert!(host.is_none());
1089                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
1090                }
1091                _ => panic!("expected Restricted event"),
1092            }
1093        }
1094
1095        events.borrow_mut().clear();
1096
1097        trusted_worktrees.update(cx, |store, cx| {
1098            store.trust(
1099                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1100                None,
1101                cx,
1102            );
1103        });
1104
1105        {
1106            let events = events.borrow();
1107            assert_eq!(events.len(), 1);
1108            match &events[0] {
1109                TrustedWorktreesEvent::Trusted(host, paths) => {
1110                    assert!(host.is_none());
1111                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
1112                }
1113                _ => panic!("expected Trusted event"),
1114            }
1115        }
1116
1117        let can_trust_after =
1118            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1119        assert!(
1120            can_trust_after,
1121            "single-file worktree should be trusted after trust()"
1122        );
1123    }
1124
1125    #[gpui::test]
1126    async fn test_workspace_trust_unlocks_single_file_worktree(cx: &mut TestAppContext) {
1127        init_test(cx);
1128
1129        let fs = FakeFs::new(cx.executor());
1130        fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
1131            .await;
1132
1133        let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
1134        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1135        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1136            let worktree = store.worktrees().next().unwrap();
1137            let worktree = worktree.read(cx);
1138            assert!(worktree.is_single_file(), "expected single-file worktree");
1139            worktree.id()
1140        });
1141
1142        let trusted_worktrees = init_trust_global(worktree_store, cx);
1143
1144        let can_trust_workspace =
1145            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1146        assert!(
1147            !can_trust_workspace,
1148            "workspace should be restricted by default"
1149        );
1150
1151        let can_trust_file =
1152            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1153        assert!(
1154            !can_trust_file,
1155            "single-file worktree should be restricted by default"
1156        );
1157
1158        trusted_worktrees.update(cx, |store, cx| {
1159            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1160        });
1161
1162        let can_trust_workspace_after =
1163            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1164        assert!(
1165            can_trust_workspace_after,
1166            "workspace should be trusted after trust(Workspace)"
1167        );
1168
1169        let can_trust_file_after =
1170            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1171        assert!(
1172            can_trust_file_after,
1173            "single-file worktree should be trusted after workspace trust"
1174        );
1175    }
1176
1177    #[gpui::test]
1178    async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
1179        init_test(cx);
1180
1181        let fs = FakeFs::new(cx.executor());
1182        fs.insert_tree(
1183            path!("/root"),
1184            json!({
1185                "a.rs": "fn a() {}",
1186                "b.rs": "fn b() {}",
1187                "c.rs": "fn c() {}"
1188            }),
1189        )
1190        .await;
1191
1192        let project = Project::test(
1193            fs,
1194            [
1195                path!("/root/a.rs").as_ref(),
1196                path!("/root/b.rs").as_ref(),
1197                path!("/root/c.rs").as_ref(),
1198            ],
1199            cx,
1200        )
1201        .await;
1202        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1203        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1204            store
1205                .worktrees()
1206                .map(|worktree| {
1207                    let worktree = worktree.read(cx);
1208                    assert!(worktree.is_single_file());
1209                    worktree.id()
1210                })
1211                .collect()
1212        });
1213        assert_eq!(worktree_ids.len(), 3);
1214
1215        let trusted_worktrees = init_trust_global(worktree_store, cx);
1216
1217        for &worktree_id in &worktree_ids {
1218            let can_trust =
1219                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1220            assert!(
1221                !can_trust,
1222                "worktree {worktree_id:?} should be restricted initially"
1223            );
1224        }
1225
1226        trusted_worktrees.update(cx, |store, cx| {
1227            store.trust(
1228                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1229                None,
1230                cx,
1231            );
1232        });
1233
1234        let can_trust_0 =
1235            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1236        let can_trust_1 =
1237            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1238        let can_trust_2 =
1239            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[2], cx));
1240
1241        assert!(!can_trust_0, "worktree 0 should still be restricted");
1242        assert!(can_trust_1, "worktree 1 should be trusted");
1243        assert!(!can_trust_2, "worktree 2 should still be restricted");
1244    }
1245
1246    #[gpui::test]
1247    async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
1248        init_test(cx);
1249
1250        let fs = FakeFs::new(cx.executor());
1251        fs.insert_tree(
1252            path!("/projects"),
1253            json!({
1254                "project_a": { "main.rs": "fn main() {}" },
1255                "project_b": { "lib.rs": "pub fn lib() {}" }
1256            }),
1257        )
1258        .await;
1259
1260        let project = Project::test(
1261            fs,
1262            [
1263                path!("/projects/project_a").as_ref(),
1264                path!("/projects/project_b").as_ref(),
1265            ],
1266            cx,
1267        )
1268        .await;
1269        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1270        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1271            store
1272                .worktrees()
1273                .map(|worktree| {
1274                    let worktree = worktree.read(cx);
1275                    assert!(!worktree.is_single_file());
1276                    worktree.id()
1277                })
1278                .collect()
1279        });
1280        assert_eq!(worktree_ids.len(), 2);
1281
1282        let trusted_worktrees = init_trust_global(worktree_store, cx);
1283
1284        let can_trust_a =
1285            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1286        let can_trust_b =
1287            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1288        assert!(!can_trust_a, "project_a should be restricted initially");
1289        assert!(!can_trust_b, "project_b should be restricted initially");
1290
1291        trusted_worktrees.update(cx, |store, cx| {
1292            store.trust(
1293                HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1294                None,
1295                cx,
1296            );
1297        });
1298
1299        let can_trust_a =
1300            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1301        let can_trust_b =
1302            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1303        assert!(can_trust_a, "project_a should be trusted after trust()");
1304        assert!(!can_trust_b, "project_b should still be restricted");
1305
1306        trusted_worktrees.update(cx, |store, cx| {
1307            store.trust(
1308                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1309                None,
1310                cx,
1311            );
1312        });
1313
1314        let can_trust_a =
1315            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1316        let can_trust_b =
1317            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1318        assert!(can_trust_a, "project_a should remain trusted");
1319        assert!(can_trust_b, "project_b should now be trusted");
1320    }
1321
1322    #[gpui::test]
1323    async fn test_directory_worktree_trust_enables_workspace(cx: &mut TestAppContext) {
1324        init_test(cx);
1325
1326        let fs = FakeFs::new(cx.executor());
1327        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1328            .await;
1329
1330        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1331        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1332        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1333            let worktree = store.worktrees().next().unwrap();
1334            assert!(!worktree.read(cx).is_single_file());
1335            worktree.read(cx).id()
1336        });
1337
1338        let trusted_worktrees = init_trust_global(worktree_store, cx);
1339
1340        let can_trust_workspace =
1341            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1342        assert!(
1343            !can_trust_workspace,
1344            "workspace should be restricted initially"
1345        );
1346
1347        trusted_worktrees.update(cx, |store, cx| {
1348            store.trust(
1349                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1350                None,
1351                cx,
1352            );
1353        });
1354
1355        let can_trust_workspace_after =
1356            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1357        assert!(
1358            can_trust_workspace_after,
1359            "workspace should be trusted after trusting directory worktree"
1360        );
1361    }
1362
1363    #[gpui::test]
1364    async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
1365        init_test(cx);
1366
1367        let fs = FakeFs::new(cx.executor());
1368        fs.insert_tree(
1369            path!("/"),
1370            json!({
1371                "project": { "main.rs": "fn main() {}" },
1372                "standalone.rs": "fn standalone() {}"
1373            }),
1374        )
1375        .await;
1376
1377        let project = Project::test(
1378            fs,
1379            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1380            cx,
1381        )
1382        .await;
1383        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1384        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1385            let worktrees: Vec<_> = store.worktrees().collect();
1386            assert_eq!(worktrees.len(), 2);
1387            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1388                (&worktrees[1], &worktrees[0])
1389            } else {
1390                (&worktrees[0], &worktrees[1])
1391            };
1392            assert!(!dir_worktree.read(cx).is_single_file());
1393            assert!(file_worktree.read(cx).is_single_file());
1394            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1395        });
1396
1397        let trusted_worktrees = init_trust_global(worktree_store, cx);
1398
1399        let can_trust_file =
1400            trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
1401        assert!(
1402            !can_trust_file,
1403            "single-file worktree should be restricted initially"
1404        );
1405
1406        trusted_worktrees.update(cx, |store, cx| {
1407            store.trust(
1408                HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
1409                None,
1410                cx,
1411            );
1412        });
1413
1414        let can_trust_dir =
1415            trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
1416        let can_trust_file_after =
1417            trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
1418        assert!(can_trust_dir, "directory worktree should be trusted");
1419        assert!(
1420            can_trust_file_after,
1421            "single-file worktree should be trusted after directory worktree trust"
1422        );
1423    }
1424
1425    #[gpui::test]
1426    async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
1427        init_test(cx);
1428
1429        let fs = FakeFs::new(cx.executor());
1430        fs.insert_tree(
1431            path!("/workspace"),
1432            json!({
1433                "project_a": { "main.rs": "fn main() {}" },
1434                "project_b": { "lib.rs": "pub fn lib() {}" }
1435            }),
1436        )
1437        .await;
1438
1439        let project = Project::test(
1440            fs,
1441            [
1442                path!("/workspace/project_a").as_ref(),
1443                path!("/workspace/project_b").as_ref(),
1444            ],
1445            cx,
1446        )
1447        .await;
1448        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1449        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1450            store
1451                .worktrees()
1452                .map(|worktree| worktree.read(cx).id())
1453                .collect()
1454        });
1455        assert_eq!(worktree_ids.len(), 2);
1456
1457        let trusted_worktrees = init_trust_global(worktree_store, cx);
1458
1459        for &worktree_id in &worktree_ids {
1460            let can_trust =
1461                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1462            assert!(!can_trust, "worktree should be restricted initially");
1463        }
1464
1465        trusted_worktrees.update(cx, |store, cx| {
1466            store.trust(
1467                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/workspace")))]),
1468                None,
1469                cx,
1470            );
1471        });
1472
1473        for &worktree_id in &worktree_ids {
1474            let can_trust =
1475                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1476            assert!(
1477                can_trust,
1478                "worktree should be trusted after parent path trust"
1479            );
1480        }
1481    }
1482
1483    #[gpui::test]
1484    async fn test_auto_trust_all(cx: &mut TestAppContext) {
1485        init_test(cx);
1486
1487        let fs = FakeFs::new(cx.executor());
1488        fs.insert_tree(
1489            path!("/"),
1490            json!({
1491                "project_a": { "main.rs": "fn main() {}" },
1492                "project_b": { "lib.rs": "pub fn lib() {}" },
1493                "single.rs": "fn single() {}"
1494            }),
1495        )
1496        .await;
1497
1498        let project = Project::test(
1499            fs,
1500            [
1501                path!("/project_a").as_ref(),
1502                path!("/project_b").as_ref(),
1503                path!("/single.rs").as_ref(),
1504            ],
1505            cx,
1506        )
1507        .await;
1508        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1509        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1510            store
1511                .worktrees()
1512                .map(|worktree| worktree.read(cx).id())
1513                .collect()
1514        });
1515        assert_eq!(worktree_ids.len(), 3);
1516
1517        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1518
1519        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1520        cx.update({
1521            let events = events.clone();
1522            |cx| {
1523                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1524                    events.borrow_mut().push(match event {
1525                        TrustedWorktreesEvent::Trusted(host, paths) => {
1526                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1527                        }
1528                        TrustedWorktreesEvent::Restricted(host, paths) => {
1529                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1530                        }
1531                    });
1532                })
1533            }
1534        })
1535        .detach();
1536
1537        for &worktree_id in &worktree_ids {
1538            let can_trust =
1539                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1540            assert!(!can_trust, "worktree should be restricted initially");
1541        }
1542        let can_trust_workspace =
1543            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1544        assert!(
1545            !can_trust_workspace,
1546            "workspace should be restricted initially"
1547        );
1548
1549        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1550            store.has_restricted_worktrees(&worktree_store, cx)
1551        });
1552        assert!(has_restricted, "should have restricted worktrees");
1553
1554        events.borrow_mut().clear();
1555
1556        trusted_worktrees.update(cx, |store, cx| {
1557            store.auto_trust_all(cx);
1558        });
1559
1560        for &worktree_id in &worktree_ids {
1561            let can_trust =
1562                trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1563            assert!(
1564                can_trust,
1565                "worktree {worktree_id:?} should be trusted after auto_trust_all"
1566            );
1567        }
1568
1569        let can_trust_workspace =
1570            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1571        assert!(
1572            can_trust_workspace,
1573            "workspace should be trusted after auto_trust_all"
1574        );
1575
1576        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
1577            store.has_restricted_worktrees(&worktree_store, cx)
1578        });
1579        assert!(
1580            !has_restricted_after,
1581            "should have no restricted worktrees after auto_trust_all"
1582        );
1583
1584        let trusted_event_count = events
1585            .borrow()
1586            .iter()
1587            .filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
1588            .count();
1589        assert!(
1590            trusted_event_count > 0,
1591            "should have emitted Trusted events"
1592        );
1593    }
1594
1595    #[gpui::test]
1596    async fn test_wait_for_global_trust_already_trusted(cx: &mut TestAppContext) {
1597        init_test(cx);
1598
1599        let fs = FakeFs::new(cx.executor());
1600        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1601            .await;
1602
1603        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1604        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1605
1606        let trusted_worktrees = init_trust_global(worktree_store, cx);
1607
1608        trusted_worktrees.update(cx, |store, cx| {
1609            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1610        });
1611
1612        let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
1613        assert!(task.is_none(), "should return None when already trusted");
1614    }
1615
1616    #[gpui::test]
1617    async fn test_wait_for_workspace_trust_resolves_on_trust(cx: &mut TestAppContext) {
1618        init_test(cx);
1619
1620        let fs = FakeFs::new(cx.executor());
1621        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1622            .await;
1623
1624        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1625        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1626
1627        let trusted_worktrees = init_trust_global(worktree_store, cx);
1628
1629        let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
1630        assert!(
1631            task.is_some(),
1632            "should return Some(Task) when not yet trusted"
1633        );
1634
1635        let task = task.unwrap();
1636
1637        cx.executor().run_until_parked();
1638
1639        trusted_worktrees.update(cx, |store, cx| {
1640            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1641        });
1642
1643        cx.executor().run_until_parked();
1644        task.await;
1645    }
1646
1647    #[gpui::test]
1648    async fn test_wait_for_default_workspace_trust_resolves_on_directory_worktree_trust(
1649        cx: &mut TestAppContext,
1650    ) {
1651        init_test(cx);
1652
1653        let fs = FakeFs::new(cx.executor());
1654        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1655            .await;
1656
1657        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1658        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1659        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1660            let worktree = store.worktrees().next().unwrap();
1661            assert!(!worktree.read(cx).is_single_file());
1662            worktree.read(cx).id()
1663        });
1664
1665        let trusted_worktrees = init_trust_global(worktree_store, cx);
1666
1667        let task = cx.update(|cx| wait_for_default_workspace_trust("test", cx));
1668        assert!(
1669            task.is_some(),
1670            "should return Some(Task) when not yet trusted"
1671        );
1672
1673        let task = task.unwrap();
1674
1675        cx.executor().run_until_parked();
1676
1677        trusted_worktrees.update(cx, |store, cx| {
1678            store.trust(
1679                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1680                None,
1681                cx,
1682            );
1683        });
1684
1685        cx.executor().run_until_parked();
1686        task.await;
1687    }
1688
1689    #[gpui::test]
1690    async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
1691        init_test(cx);
1692
1693        let fs = FakeFs::new(cx.executor());
1694        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1695            .await;
1696
1697        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1698        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1699        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1700            store.worktrees().next().unwrap().read(cx).id()
1701        });
1702
1703        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1704
1705        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1706        cx.update({
1707            let events = events.clone();
1708            |cx| {
1709                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1710                    events.borrow_mut().push(match event {
1711                        TrustedWorktreesEvent::Trusted(host, paths) => {
1712                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1713                        }
1714                        TrustedWorktreesEvent::Restricted(host, paths) => {
1715                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1716                        }
1717                    });
1718                })
1719            }
1720        })
1721        .detach();
1722
1723        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1724        assert!(!can_trust, "should be restricted initially");
1725        assert_eq!(events.borrow().len(), 1);
1726        events.borrow_mut().clear();
1727
1728        trusted_worktrees.update(cx, |store, cx| {
1729            store.trust(
1730                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1731                None,
1732                cx,
1733            );
1734        });
1735        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1736        assert!(can_trust, "should be trusted after trust()");
1737        assert_eq!(events.borrow().len(), 1);
1738        assert!(matches!(
1739            &events.borrow()[0],
1740            TrustedWorktreesEvent::Trusted(..)
1741        ));
1742        events.borrow_mut().clear();
1743
1744        trusted_worktrees.update(cx, |store, cx| {
1745            store.restrict(
1746                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1747                None,
1748                cx,
1749            );
1750        });
1751        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1752        assert!(!can_trust, "should be restricted after restrict()");
1753        assert_eq!(events.borrow().len(), 1);
1754        assert!(matches!(
1755            &events.borrow()[0],
1756            TrustedWorktreesEvent::Restricted(..)
1757        ));
1758
1759        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1760            store.has_restricted_worktrees(&worktree_store, cx)
1761        });
1762        assert!(has_restricted);
1763        events.borrow_mut().clear();
1764
1765        trusted_worktrees.update(cx, |store, cx| {
1766            store.trust(
1767                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1768                None,
1769                cx,
1770            );
1771        });
1772        let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1773        assert!(can_trust, "should be trusted again after second trust()");
1774        assert_eq!(events.borrow().len(), 1);
1775        assert!(matches!(
1776            &events.borrow()[0],
1777            TrustedWorktreesEvent::Trusted(..)
1778        ));
1779
1780        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1781            store.has_restricted_worktrees(&worktree_store, cx)
1782        });
1783        assert!(!has_restricted);
1784    }
1785
1786    #[gpui::test]
1787    async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
1788        init_test(cx);
1789
1790        let fs = FakeFs::new(cx.executor());
1791        fs.insert_tree(
1792            path!("/"),
1793            json!({
1794                "local_project": { "main.rs": "fn main() {}" },
1795                "remote_project": { "lib.rs": "pub fn lib() {}" }
1796            }),
1797        )
1798        .await;
1799
1800        let project = Project::test(
1801            fs,
1802            [
1803                path!("/local_project").as_ref(),
1804                path!("/remote_project").as_ref(),
1805            ],
1806            cx,
1807        )
1808        .await;
1809        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1810        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1811            store
1812                .worktrees()
1813                .map(|worktree| worktree.read(cx).id())
1814                .collect()
1815        });
1816        assert_eq!(worktree_ids.len(), 2);
1817        let local_worktree = worktree_ids[0];
1818        let _remote_worktree = worktree_ids[1];
1819
1820        let trusted_worktrees = init_trust_global(worktree_store, cx);
1821
1822        let host_a: Option<RemoteHostLocation> = None;
1823        let host_b = Some(RemoteHostLocation {
1824            user_name: Some("user".into()),
1825            host_identifier: "remote-host".into(),
1826        });
1827
1828        let can_trust_local =
1829            trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
1830        assert!(!can_trust_local, "local worktree restricted on host_a");
1831
1832        trusted_worktrees.update(cx, |store, cx| {
1833            store.trust(
1834                HashSet::from_iter([PathTrust::Workspace]),
1835                host_b.clone(),
1836                cx,
1837            );
1838        });
1839
1840        let can_trust_workspace_a = trusted_worktrees.update(cx, |store, cx| {
1841            store.can_trust_workspace(host_a.clone(), cx)
1842        });
1843        assert!(
1844            !can_trust_workspace_a,
1845            "host_a workspace should still be restricted"
1846        );
1847
1848        let can_trust_workspace_b = trusted_worktrees.update(cx, |store, cx| {
1849            store.can_trust_workspace(host_b.clone(), cx)
1850        });
1851        assert!(can_trust_workspace_b, "host_b workspace should be trusted");
1852
1853        trusted_worktrees.update(cx, |store, cx| {
1854            store.trust(
1855                HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
1856                host_a.clone(),
1857                cx,
1858            );
1859        });
1860
1861        let can_trust_local_after =
1862            trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
1863        assert!(
1864            can_trust_local_after,
1865            "local worktree should be trusted on host_a"
1866        );
1867
1868        let can_trust_workspace_a_after = trusted_worktrees.update(cx, |store, cx| {
1869            store.can_trust_workspace(host_a.clone(), cx)
1870        });
1871        assert!(
1872            can_trust_workspace_a_after,
1873            "host_a workspace should be trusted after directory trust"
1874        );
1875    }
1876}