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//! * "directory worktree"
  31//!
  32//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
  33//! 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.
  34//!
  35//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence, "single file worktree" level of trust also.
  36//!
  37//! * "path override"
  38//!
  39//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
  40//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
  41
  42use client::ProjectId;
  43use collections::{HashMap, HashSet};
  44use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, WeakEntity};
  45use remote::RemoteConnectionOptions;
  46use rpc::{AnyProtoClient, proto};
  47use settings::{Settings as _, WorktreeId};
  48use std::{
  49    path::{Path, PathBuf},
  50    sync::Arc,
  51};
  52use util::debug_panic;
  53
  54use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
  55
  56pub fn init(
  57    db_trusted_paths: TrustedPaths,
  58    downstream_client: Option<(AnyProtoClient, ProjectId)>,
  59    upstream_client: Option<(AnyProtoClient, ProjectId)>,
  60    cx: &mut App,
  61) {
  62    if TrustedWorktrees::try_get_global(cx).is_none() {
  63        let trusted_worktrees = cx.new(|_| {
  64            TrustedWorktreesStore::new(
  65                db_trusted_paths,
  66                None,
  67                None,
  68                downstream_client,
  69                upstream_client,
  70            )
  71        });
  72        cx.set_global(TrustedWorktrees(trusted_worktrees))
  73    }
  74}
  75
  76/// An initialization call to set up trust global for a particular project (remote or local).
  77pub fn track_worktree_trust(
  78    worktree_store: Entity<WorktreeStore>,
  79    remote_host: Option<RemoteHostLocation>,
  80    downstream_client: Option<(AnyProtoClient, ProjectId)>,
  81    upstream_client: Option<(AnyProtoClient, ProjectId)>,
  82    cx: &mut App,
  83) {
  84    match TrustedWorktrees::try_get_global(cx) {
  85        Some(trusted_worktrees) => {
  86            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
  87                let sync_upstream = trusted_worktrees.upstream_client.as_ref().map(|(_, id)| id)
  88                    != upstream_client.as_ref().map(|(_, id)| id);
  89                trusted_worktrees.downstream_client = downstream_client;
  90                trusted_worktrees.upstream_client = upstream_client;
  91                trusted_worktrees.add_worktree_store(worktree_store, remote_host, cx);
  92
  93                if sync_upstream {
  94                    if let Some((upstream_client, upstream_project_id)) =
  95                        &trusted_worktrees.upstream_client
  96                    {
  97                        let trusted_paths = trusted_worktrees
  98                            .trusted_paths
  99                            .iter()
 100                            .flat_map(|(_, paths)| {
 101                                paths.iter().map(|trusted_path| trusted_path.to_proto())
 102                            })
 103                            .collect::<Vec<_>>();
 104                        if !trusted_paths.is_empty() {
 105                            upstream_client
 106                                .send(proto::TrustWorktrees {
 107                                    project_id: upstream_project_id.0,
 108                                    trusted_paths,
 109                                })
 110                                .ok();
 111                        }
 112                    }
 113                }
 114            });
 115        }
 116        None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
 117    }
 118}
 119
 120/// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
 121pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
 122
 123impl Global for TrustedWorktrees {}
 124
 125impl TrustedWorktrees {
 126    pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
 127        cx.try_global::<Self>().map(|this| this.0.clone())
 128    }
 129}
 130
 131/// A collection of worktrees that are considered trusted and not trusted.
 132/// This can be used when checking for this criteria before enabling certain features.
 133///
 134/// Emits an event each time the worktree was checked and found not trusted,
 135/// or a certain worktree had been trusted.
 136pub struct TrustedWorktreesStore {
 137    downstream_client: Option<(AnyProtoClient, ProjectId)>,
 138    upstream_client: Option<(AnyProtoClient, ProjectId)>,
 139    worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
 140    trusted_paths: TrustedPaths,
 141    restricted: HashMap<Option<RemoteHostLocation>, HashSet<WorktreeId>>,
 142}
 143
 144/// An identifier of a host to split the trust questions by.
 145/// Each trusted data change and event is done for a particular host.
 146/// A host may contain more than one worktree or even project open concurrently.
 147#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 148pub struct RemoteHostLocation {
 149    pub user_name: Option<SharedString>,
 150    pub host_identifier: SharedString,
 151    pub project_id: ProjectId,
 152}
 153
 154impl From<(u64, RemoteConnectionOptions)> for RemoteHostLocation {
 155    fn from((project_id, options): (u64, RemoteConnectionOptions)) -> Self {
 156        let (user_name, host_name) = match options {
 157            RemoteConnectionOptions::Ssh(ssh) => (
 158                ssh.username.map(SharedString::new),
 159                SharedString::new(ssh.host.to_string()),
 160            ),
 161            RemoteConnectionOptions::Wsl(wsl) => (
 162                wsl.user.map(SharedString::new),
 163                SharedString::new(wsl.distro_name),
 164            ),
 165            RemoteConnectionOptions::Docker(docker_connection_options) => (
 166                Some(SharedString::new(docker_connection_options.name)),
 167                SharedString::new(docker_connection_options.container_id),
 168            ),
 169        };
 170        Self {
 171            user_name,
 172            host_identifier: host_name,
 173            project_id: ProjectId(project_id),
 174        }
 175    }
 176}
 177
 178/// A unit of trust consideration inside a particular host:
 179/// either a familiar worktree, or a path that may influence other worktrees' trust.
 180/// See module-level documentation on the trust model.
 181#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 182pub enum PathTrust {
 183    /// A worktree that is familiar to this workspace.
 184    /// Either a single file or a directory worktree.
 185    Worktree(WorktreeId),
 186    /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),
 187    /// or a parent path coming out of the security modal.
 188    AbsPath(PathBuf),
 189}
 190
 191impl PathTrust {
 192    fn to_proto(&self) -> proto::PathTrust {
 193        match self {
 194            Self::Worktree(worktree_id) => proto::PathTrust {
 195                content: Some(proto::path_trust::Content::WorktreeId(
 196                    worktree_id.to_proto(),
 197                )),
 198            },
 199            Self::AbsPath(path_buf) => proto::PathTrust {
 200                content: Some(proto::path_trust::Content::AbsPath(
 201                    path_buf.to_string_lossy().to_string(),
 202                )),
 203            },
 204        }
 205    }
 206
 207    pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {
 208        Some(match proto.content? {
 209            proto::path_trust::Content::WorktreeId(id) => {
 210                Self::Worktree(WorktreeId::from_proto(id))
 211            }
 212            proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
 213        })
 214    }
 215}
 216
 217/// A change of trust on a certain host.
 218#[derive(Debug)]
 219pub enum TrustedWorktreesEvent {
 220    Trusted(Option<RemoteHostLocation>, HashSet<PathTrust>),
 221    Restricted(Option<RemoteHostLocation>, HashSet<PathTrust>),
 222}
 223
 224impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
 225
 226pub type TrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>;
 227
 228impl TrustedWorktreesStore {
 229    fn new(
 230        trusted_paths: TrustedPaths,
 231        worktree_store: Option<Entity<WorktreeStore>>,
 232        remote_host: Option<RemoteHostLocation>,
 233        downstream_client: Option<(AnyProtoClient, ProjectId)>,
 234        upstream_client: Option<(AnyProtoClient, ProjectId)>,
 235    ) -> Self {
 236        if let Some((upstream_client, upstream_project_id)) = &upstream_client {
 237            let trusted_paths = trusted_paths
 238                .iter()
 239                .flat_map(|(_, paths)| paths.iter().map(|trusted_path| trusted_path.to_proto()))
 240                .collect::<Vec<_>>();
 241            if !trusted_paths.is_empty() {
 242                upstream_client
 243                    .send(proto::TrustWorktrees {
 244                        project_id: upstream_project_id.0,
 245                        trusted_paths,
 246                    })
 247                    .ok();
 248            }
 249        }
 250
 251        let worktree_stores = match worktree_store {
 252            Some(worktree_store) => HashMap::from_iter([(worktree_store.downgrade(), remote_host)]),
 253            None => HashMap::default(),
 254        };
 255
 256        Self {
 257            trusted_paths,
 258            downstream_client,
 259            upstream_client,
 260            worktree_stores,
 261            restricted: HashMap::default(),
 262        }
 263    }
 264
 265    /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.
 266    pub fn has_restricted_worktrees(
 267        &self,
 268        worktree_store: &Entity<WorktreeStore>,
 269        cx: &App,
 270    ) -> bool {
 271        let Some(remote_host) = self.worktree_stores.get(&worktree_store.downgrade()) else {
 272            return false;
 273        };
 274        self.restricted
 275            .get(remote_host)
 276            .is_some_and(|restricted_worktrees| {
 277                restricted_worktrees.iter().any(|restricted_worktree| {
 278                    worktree_store
 279                        .read(cx)
 280                        .worktree_for_id(*restricted_worktree, cx)
 281                        .is_some()
 282                })
 283            })
 284    }
 285
 286    /// Adds certain entities on this host to the trusted list.
 287    /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries
 288    /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).
 289    pub fn trust(
 290        &mut self,
 291        mut trusted_paths: HashSet<PathTrust>,
 292        remote_host: Option<RemoteHostLocation>,
 293        cx: &mut Context<Self>,
 294    ) {
 295        let mut new_trusted_single_file_worktrees = HashSet::default();
 296        let mut new_trusted_other_worktrees = HashSet::default();
 297        let mut new_trusted_abs_paths = HashSet::default();
 298        for trusted_path in trusted_paths.iter().chain(
 299            self.trusted_paths
 300                .remove(&remote_host)
 301                .iter()
 302                .flat_map(|current_trusted| current_trusted.iter()),
 303        ) {
 304            match trusted_path {
 305                PathTrust::Worktree(worktree_id) => {
 306                    if let Some(restricted_worktrees) = self.restricted.get_mut(&remote_host) {
 307                        restricted_worktrees.remove(worktree_id);
 308                    };
 309                    if let Some((abs_path, is_file, host)) =
 310                        self.find_worktree_data(*worktree_id, cx)
 311                    {
 312                        if host == remote_host {
 313                            if is_file {
 314                                new_trusted_single_file_worktrees.insert(*worktree_id);
 315                            } else {
 316                                new_trusted_other_worktrees.insert((abs_path, *worktree_id));
 317                            }
 318                        }
 319                    }
 320                }
 321                PathTrust::AbsPath(path) => {
 322                    debug_assert!(
 323                        path.is_absolute(),
 324                        "Cannot trust non-absolute path {path:?}"
 325                    );
 326                    new_trusted_abs_paths.insert(path.clone());
 327                }
 328            }
 329        }
 330
 331        new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
 332            new_trusted_abs_paths
 333                .iter()
 334                .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))
 335        });
 336        if !new_trusted_other_worktrees.is_empty() {
 337            new_trusted_single_file_worktrees.clear();
 338        }
 339
 340        if let Some(restricted_worktrees) = self.restricted.remove(&remote_host) {
 341            let new_restricted_worktrees = restricted_worktrees
 342                .into_iter()
 343                .filter(|restricted_worktree| {
 344                    let Some((restricted_worktree_path, is_file, restricted_host)) =
 345                        self.find_worktree_data(*restricted_worktree, cx)
 346                    else {
 347                        return false;
 348                    };
 349                    if restricted_host != remote_host {
 350                        return true;
 351                    }
 352
 353                    // When trusting an abs path on the host, we transitively trust all single file worktrees on this host too.
 354                    if is_file && !new_trusted_abs_paths.is_empty() {
 355                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 356                        return false;
 357                    }
 358
 359                    let retain = (!is_file || new_trusted_other_worktrees.is_empty())
 360                        && new_trusted_abs_paths.iter().all(|new_trusted_path| {
 361                            !restricted_worktree_path.starts_with(new_trusted_path)
 362                        });
 363                    if !retain {
 364                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 365                    }
 366                    retain
 367                })
 368                .collect();
 369            self.restricted
 370                .insert(remote_host.clone(), new_restricted_worktrees);
 371        }
 372
 373        {
 374            let trusted_paths = self.trusted_paths.entry(remote_host.clone()).or_default();
 375            trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
 376            trusted_paths.extend(
 377                new_trusted_other_worktrees
 378                    .into_iter()
 379                    .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
 380            );
 381            trusted_paths.extend(
 382                new_trusted_single_file_worktrees
 383                    .into_iter()
 384                    .map(PathTrust::Worktree),
 385            );
 386        }
 387
 388        cx.emit(TrustedWorktreesEvent::Trusted(
 389            remote_host,
 390            trusted_paths.clone(),
 391        ));
 392
 393        if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 394            let trusted_paths = trusted_paths
 395                .iter()
 396                .map(|trusted_path| trusted_path.to_proto())
 397                .collect::<Vec<_>>();
 398            if !trusted_paths.is_empty() {
 399                upstream_client
 400                    .send(proto::TrustWorktrees {
 401                        project_id: upstream_project_id.0,
 402                        trusted_paths,
 403                    })
 404                    .ok();
 405            }
 406        }
 407    }
 408
 409    /// Restricts certain entities on this host.
 410    /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
 411    pub fn restrict(
 412        &mut self,
 413        restricted_paths: HashSet<PathTrust>,
 414        remote_host: Option<RemoteHostLocation>,
 415        cx: &mut Context<Self>,
 416    ) {
 417        for restricted_path in restricted_paths {
 418            match restricted_path {
 419                PathTrust::Worktree(worktree_id) => {
 420                    self.restricted
 421                        .entry(remote_host.clone())
 422                        .or_default()
 423                        .insert(worktree_id);
 424                    cx.emit(TrustedWorktreesEvent::Restricted(
 425                        remote_host.clone(),
 426                        HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 427                    ));
 428                }
 429                PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
 430            }
 431        }
 432    }
 433
 434    /// Erases all trust information.
 435    /// Requires Zed's restart to take proper effect.
 436    pub fn clear_trusted_paths(&mut self) {
 437        self.trusted_paths.clear();
 438    }
 439
 440    /// Checks whether a certain worktree is trusted (or on a larger trust level).
 441    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
 442    ///
 443    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
 444    pub fn can_trust_local_worktree(
 445        &mut self,
 446        worktree_id: WorktreeId,
 447        cx: &mut Context<Self>,
 448    ) -> bool {
 449        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 450            return true;
 451        }
 452
 453        let Some((worktree_path, is_file, remote_host)) = self.find_worktree_data(worktree_id, cx)
 454        else {
 455            return false;
 456        };
 457        if self
 458            .restricted
 459            .get(&remote_host)
 460            .is_some_and(|restricted_worktrees| restricted_worktrees.contains(&worktree_id))
 461        {
 462            return false;
 463        }
 464
 465        if self
 466            .trusted_paths
 467            .get(&remote_host)
 468            .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
 469        {
 470            return true;
 471        }
 472
 473        // See module documentation for details on trust level.
 474        if is_file && self.trusted_paths.contains_key(&remote_host) {
 475            return true;
 476        }
 477
 478        let parent_path_trusted =
 479            self.trusted_paths
 480                .get(&remote_host)
 481                .is_some_and(|trusted_paths| {
 482                    trusted_paths.iter().any(|trusted_path| {
 483                        let PathTrust::AbsPath(trusted_path) = trusted_path else {
 484                            return false;
 485                        };
 486                        worktree_path.starts_with(trusted_path)
 487                    })
 488                });
 489        if parent_path_trusted {
 490            return true;
 491        }
 492
 493        self.restricted
 494            .entry(remote_host.clone())
 495            .or_default()
 496            .insert(worktree_id);
 497        cx.emit(TrustedWorktreesEvent::Restricted(
 498            remote_host,
 499            HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 500        ));
 501        if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
 502            downstream_client
 503                .send(proto::RestrictWorktrees {
 504                    project_id: downstream_project_id.0,
 505                    worktree_ids: vec![worktree_id.to_proto()],
 506                })
 507                .ok();
 508        }
 509        if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
 510            upstream_client
 511                .send(proto::RestrictWorktrees {
 512                    project_id: upstream_project_id.0,
 513                    worktree_ids: vec![worktree_id.to_proto()],
 514                })
 515                .ok();
 516        }
 517        false
 518    }
 519
 520    /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
 521    pub fn restricted_worktrees(
 522        &self,
 523        worktree_store: &Entity<WorktreeStore>,
 524        cx: &App,
 525    ) -> HashSet<(WorktreeId, Arc<Path>)> {
 526        let Some(remote_host) = self.worktree_stores.get(&worktree_store.downgrade()) else {
 527            return HashSet::default();
 528        };
 529        let mut single_file_paths = HashSet::default();
 530
 531        let other_paths = self
 532            .restricted
 533            .get(&remote_host)
 534            .into_iter()
 535            .flatten()
 536            .filter_map(|&restricted_worktree_id| {
 537                let worktree = worktree_store
 538                    .read(cx)
 539                    .worktree_for_id(restricted_worktree_id, cx)?;
 540                let worktree = worktree.read(cx);
 541                let abs_path = worktree.abs_path();
 542                if worktree.is_single_file() {
 543                    single_file_paths.insert((restricted_worktree_id, abs_path));
 544                    None
 545                } else {
 546                    Some((restricted_worktree_id, abs_path))
 547                }
 548            })
 549            .collect::<HashSet<_>>();
 550
 551        if !other_paths.is_empty() {
 552            return other_paths;
 553        } else {
 554            single_file_paths
 555        }
 556    }
 557
 558    /// Switches the "trust nothing" mode to "automatically trust everything".
 559    /// This does not influence already persisted data, but stops adding new worktrees there.
 560    pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
 561        for (remote_host, worktrees) in std::mem::take(&mut self.restricted).into_iter().fold(
 562            HashMap::default(),
 563            |mut acc, (remote_host, worktrees)| {
 564                acc.entry(remote_host)
 565                    .or_insert_with(HashSet::default)
 566                    .extend(worktrees.into_iter().map(PathTrust::Worktree));
 567                acc
 568            },
 569        ) {
 570            self.trust(worktrees, remote_host, cx);
 571        }
 572    }
 573
 574    /// Returns a normalized representation of the trusted paths to store in the DB.
 575    pub fn trusted_paths_for_serialization(
 576        &mut self,
 577        cx: &mut Context<Self>,
 578    ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
 579        let new_trusted_worktrees = self
 580            .trusted_paths
 581            .clone()
 582            .into_iter()
 583            .map(|(host, paths)| {
 584                let abs_paths = paths
 585                    .into_iter()
 586                    .flat_map(|path| match path {
 587                        PathTrust::Worktree(worktree_id) => self
 588                            .find_worktree_data(worktree_id, cx)
 589                            .map(|(abs_path, ..)| abs_path.to_path_buf()),
 590                        PathTrust::AbsPath(abs_path) => Some(abs_path),
 591                    })
 592                    .collect();
 593                (host, abs_paths)
 594            })
 595            .collect();
 596        new_trusted_worktrees
 597    }
 598
 599    // TODO kb would not work due to duplicated IDs
 600    fn find_worktree_data(
 601        &mut self,
 602        worktree_id: WorktreeId,
 603        cx: &mut Context<Self>,
 604    ) -> Option<(Arc<Path>, bool, Option<RemoteHostLocation>)> {
 605        let mut worktree_data = None;
 606        self.worktree_stores.retain(
 607            |worktree_store, remote_host| match worktree_store.upgrade() {
 608                Some(worktree_store) => {
 609                    if worktree_data.is_none() {
 610                        if let Some(worktree) =
 611                            worktree_store.read(cx).worktree_for_id(worktree_id, cx)
 612                        {
 613                            worktree_data = Some((
 614                                worktree.read(cx).abs_path(),
 615                                worktree.read(cx).is_single_file(),
 616                                remote_host.clone(),
 617                            ));
 618                        }
 619                    }
 620                    true
 621                }
 622                None => false,
 623            },
 624        );
 625        worktree_data
 626    }
 627
 628    fn add_worktree_store(
 629        &mut self,
 630        worktree_store: Entity<WorktreeStore>,
 631        remote_host: Option<RemoteHostLocation>,
 632        cx: &mut Context<Self>,
 633    ) {
 634        self.worktree_stores
 635            .insert(worktree_store.downgrade(), remote_host.clone());
 636
 637        if let Some(trusted_paths) = self.trusted_paths.remove(&remote_host) {
 638            self.trusted_paths.insert(
 639                remote_host.clone(),
 640                trusted_paths
 641                    .into_iter()
 642                    .map(|path_trust| match path_trust {
 643                        PathTrust::AbsPath(abs_path) => {
 644                            find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
 645                                .map(PathTrust::Worktree)
 646                                .unwrap_or_else(|| PathTrust::AbsPath(abs_path))
 647                        }
 648                        other => other,
 649                    })
 650                    .collect(),
 651            );
 652        }
 653    }
 654}
 655
 656pub fn find_worktree_in_store(
 657    worktree_store: &WorktreeStore,
 658    abs_path: &Path,
 659    cx: &App,
 660) -> Option<WorktreeId> {
 661    let (worktree, path_in_worktree) = worktree_store.find_worktree(&abs_path, cx)?;
 662    if path_in_worktree.is_empty() {
 663        Some(worktree.read(cx).id())
 664    } else {
 665        None
 666    }
 667}
 668
 669#[cfg(test)]
 670mod tests {
 671    use std::{cell::RefCell, path::PathBuf, rc::Rc};
 672
 673    use collections::HashSet;
 674    use gpui::TestAppContext;
 675    use serde_json::json;
 676    use settings::SettingsStore;
 677    use util::path;
 678
 679    use crate::{FakeFs, Project};
 680
 681    use super::*;
 682
 683    fn init_test(cx: &mut TestAppContext) {
 684        cx.update(|cx| {
 685            if cx.try_global::<SettingsStore>().is_none() {
 686                let settings_store = SettingsStore::test(cx);
 687                cx.set_global(settings_store);
 688            }
 689            if cx.try_global::<TrustedWorktrees>().is_some() {
 690                cx.remove_global::<TrustedWorktrees>();
 691            }
 692        });
 693    }
 694
 695    fn init_trust_global(
 696        worktree_store: Entity<WorktreeStore>,
 697        cx: &mut TestAppContext,
 698    ) -> Entity<TrustedWorktreesStore> {
 699        cx.update(|cx| {
 700            init(HashMap::default(), None, None, cx);
 701            track_worktree_trust(worktree_store, None, None, None, cx);
 702            TrustedWorktrees::try_get_global(cx).expect("global should be set")
 703        })
 704    }
 705
 706    #[gpui::test]
 707    async fn test_single_worktree_trust(cx: &mut TestAppContext) {
 708        init_test(cx);
 709
 710        let fs = FakeFs::new(cx.executor());
 711        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
 712            .await;
 713
 714        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 715        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 716        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 717            store.worktrees().next().unwrap().read(cx).id()
 718        });
 719
 720        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
 721
 722        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 723        cx.update({
 724            let events = events.clone();
 725            |cx| {
 726                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 727                    events.borrow_mut().push(match event {
 728                        TrustedWorktreesEvent::Trusted(host, paths) => {
 729                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 730                        }
 731                        TrustedWorktreesEvent::Restricted(host, paths) => {
 732                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 733                        }
 734                    });
 735                })
 736            }
 737        })
 738        .detach();
 739
 740        let can_trust = trusted_worktrees.update(cx, |store, cx| {
 741            store.can_trust_local_worktree(worktree_id, cx)
 742        });
 743        assert!(!can_trust, "worktree should be restricted by default");
 744
 745        {
 746            let events = events.borrow();
 747            assert_eq!(events.len(), 1);
 748            match &events[0] {
 749                TrustedWorktreesEvent::Restricted(host, paths) => {
 750                    assert!(host.is_none());
 751                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 752                }
 753                _ => panic!("expected Restricted event"),
 754            }
 755        }
 756
 757        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
 758            store.has_restricted_worktrees(&worktree_store, cx)
 759        });
 760        assert!(has_restricted, "should have restricted worktrees");
 761
 762        let restricted = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
 763            trusted_worktrees.restricted_worktrees(&worktree_store, cx)
 764        });
 765        assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
 766
 767        events.borrow_mut().clear();
 768
 769        let can_trust_again = trusted_worktrees.update(cx, |store, cx| {
 770            store.can_trust_local_worktree(worktree_id, cx)
 771        });
 772        assert!(!can_trust_again, "worktree should still be restricted");
 773        assert!(
 774            events.borrow().is_empty(),
 775            "no duplicate Restricted event on repeated can_trust"
 776        );
 777
 778        trusted_worktrees.update(cx, |store, cx| {
 779            store.trust(
 780                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 781                None,
 782                cx,
 783            );
 784        });
 785
 786        {
 787            let events = events.borrow();
 788            assert_eq!(events.len(), 1);
 789            match &events[0] {
 790                TrustedWorktreesEvent::Trusted(host, paths) => {
 791                    assert!(host.is_none());
 792                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 793                }
 794                _ => panic!("expected Trusted event"),
 795            }
 796        }
 797
 798        let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
 799            store.can_trust_local_worktree(worktree_id, cx)
 800        });
 801        assert!(can_trust_after, "worktree should be trusted after trust()");
 802
 803        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
 804            store.has_restricted_worktrees(&worktree_store, cx)
 805        });
 806        assert!(
 807            !has_restricted_after,
 808            "should have no restricted worktrees after trust"
 809        );
 810
 811        let restricted_after = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
 812            trusted_worktrees.restricted_worktrees(&worktree_store, cx)
 813        });
 814        assert!(
 815            restricted_after.is_empty(),
 816            "restricted set should be empty"
 817        );
 818    }
 819
 820    #[gpui::test]
 821    async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
 822        init_test(cx);
 823
 824        let fs = FakeFs::new(cx.executor());
 825        fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
 826            .await;
 827
 828        let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
 829        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 830        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 831            let worktree = store.worktrees().next().unwrap();
 832            let worktree = worktree.read(cx);
 833            assert!(worktree.is_single_file(), "expected single-file worktree");
 834            worktree.id()
 835        });
 836
 837        let trusted_worktrees = init_trust_global(worktree_store, cx);
 838
 839        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 840        cx.update({
 841            let events = events.clone();
 842            |cx| {
 843                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 844                    events.borrow_mut().push(match event {
 845                        TrustedWorktreesEvent::Trusted(host, paths) => {
 846                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 847                        }
 848                        TrustedWorktreesEvent::Restricted(host, paths) => {
 849                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 850                        }
 851                    });
 852                })
 853            }
 854        })
 855        .detach();
 856
 857        let can_trust = trusted_worktrees.update(cx, |store, cx| {
 858            store.can_trust_local_worktree(worktree_id, cx)
 859        });
 860        assert!(
 861            !can_trust,
 862            "single-file worktree should be restricted by default"
 863        );
 864
 865        {
 866            let events = events.borrow();
 867            assert_eq!(events.len(), 1);
 868            match &events[0] {
 869                TrustedWorktreesEvent::Restricted(host, paths) => {
 870                    assert!(host.is_none());
 871                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 872                }
 873                _ => panic!("expected Restricted event"),
 874            }
 875        }
 876
 877        events.borrow_mut().clear();
 878
 879        trusted_worktrees.update(cx, |store, cx| {
 880            store.trust(
 881                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 882                None,
 883                cx,
 884            );
 885        });
 886
 887        {
 888            let events = events.borrow();
 889            assert_eq!(events.len(), 1);
 890            match &events[0] {
 891                TrustedWorktreesEvent::Trusted(host, paths) => {
 892                    assert!(host.is_none());
 893                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 894                }
 895                _ => panic!("expected Trusted event"),
 896            }
 897        }
 898
 899        let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
 900            store.can_trust_local_worktree(worktree_id, cx)
 901        });
 902        assert!(
 903            can_trust_after,
 904            "single-file worktree should be trusted after trust()"
 905        );
 906    }
 907
 908    #[gpui::test]
 909    async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
 910        init_test(cx);
 911
 912        let fs = FakeFs::new(cx.executor());
 913        fs.insert_tree(
 914            path!("/root"),
 915            json!({
 916                "a.rs": "fn a() {}",
 917                "b.rs": "fn b() {}",
 918                "c.rs": "fn c() {}"
 919            }),
 920        )
 921        .await;
 922
 923        let project = Project::test(
 924            fs,
 925            [
 926                path!("/root/a.rs").as_ref(),
 927                path!("/root/b.rs").as_ref(),
 928                path!("/root/c.rs").as_ref(),
 929            ],
 930            cx,
 931        )
 932        .await;
 933        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 934        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
 935            store
 936                .worktrees()
 937                .map(|worktree| {
 938                    let worktree = worktree.read(cx);
 939                    assert!(worktree.is_single_file());
 940                    worktree.id()
 941                })
 942                .collect()
 943        });
 944        assert_eq!(worktree_ids.len(), 3);
 945
 946        let trusted_worktrees = init_trust_global(worktree_store, cx);
 947
 948        for &worktree_id in &worktree_ids {
 949            let can_trust = trusted_worktrees.update(cx, |store, cx| {
 950                store.can_trust_local_worktree(worktree_id, cx)
 951            });
 952            assert!(
 953                !can_trust,
 954                "worktree {worktree_id:?} should be restricted initially"
 955            );
 956        }
 957
 958        trusted_worktrees.update(cx, |store, cx| {
 959            store.trust(
 960                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
 961                None,
 962                cx,
 963            );
 964        });
 965
 966        let can_trust_0 = trusted_worktrees.update(cx, |store, cx| {
 967            store.can_trust_local_worktree(worktree_ids[0], cx)
 968        });
 969        let can_trust_1 = trusted_worktrees.update(cx, |store, cx| {
 970            store.can_trust_local_worktree(worktree_ids[1], cx)
 971        });
 972        let can_trust_2 = trusted_worktrees.update(cx, |store, cx| {
 973            store.can_trust_local_worktree(worktree_ids[2], cx)
 974        });
 975
 976        assert!(!can_trust_0, "worktree 0 should still be restricted");
 977        assert!(can_trust_1, "worktree 1 should be trusted");
 978        assert!(!can_trust_2, "worktree 2 should still be restricted");
 979    }
 980
 981    #[gpui::test]
 982    async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
 983        init_test(cx);
 984
 985        let fs = FakeFs::new(cx.executor());
 986        fs.insert_tree(
 987            path!("/projects"),
 988            json!({
 989                "project_a": { "main.rs": "fn main() {}" },
 990                "project_b": { "lib.rs": "pub fn lib() {}" }
 991            }),
 992        )
 993        .await;
 994
 995        let project = Project::test(
 996            fs,
 997            [
 998                path!("/projects/project_a").as_ref(),
 999                path!("/projects/project_b").as_ref(),
1000            ],
1001            cx,
1002        )
1003        .await;
1004        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1005        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1006            store
1007                .worktrees()
1008                .map(|worktree| {
1009                    let worktree = worktree.read(cx);
1010                    assert!(!worktree.is_single_file());
1011                    worktree.id()
1012                })
1013                .collect()
1014        });
1015        assert_eq!(worktree_ids.len(), 2);
1016
1017        let trusted_worktrees = init_trust_global(worktree_store, cx);
1018
1019        let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1020            store.can_trust_local_worktree(worktree_ids[0], cx)
1021        });
1022        let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1023            store.can_trust_local_worktree(worktree_ids[1], cx)
1024        });
1025        assert!(!can_trust_a, "project_a should be restricted initially");
1026        assert!(!can_trust_b, "project_b should be restricted initially");
1027
1028        trusted_worktrees.update(cx, |store, cx| {
1029            store.trust(
1030                HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1031                None,
1032                cx,
1033            );
1034        });
1035
1036        let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1037            store.can_trust_local_worktree(worktree_ids[0], cx)
1038        });
1039        let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1040            store.can_trust_local_worktree(worktree_ids[1], cx)
1041        });
1042        assert!(can_trust_a, "project_a should be trusted after trust()");
1043        assert!(!can_trust_b, "project_b should still be restricted");
1044
1045        trusted_worktrees.update(cx, |store, cx| {
1046            store.trust(
1047                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1048                None,
1049                cx,
1050            );
1051        });
1052
1053        let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1054            store.can_trust_local_worktree(worktree_ids[0], cx)
1055        });
1056        let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1057            store.can_trust_local_worktree(worktree_ids[1], cx)
1058        });
1059        assert!(can_trust_a, "project_a should remain trusted");
1060        assert!(can_trust_b, "project_b should now be trusted");
1061    }
1062
1063    #[gpui::test]
1064    async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
1065        init_test(cx);
1066
1067        let fs = FakeFs::new(cx.executor());
1068        fs.insert_tree(
1069            path!("/"),
1070            json!({
1071                "project": { "main.rs": "fn main() {}" },
1072                "standalone.rs": "fn standalone() {}"
1073            }),
1074        )
1075        .await;
1076
1077        let project = Project::test(
1078            fs,
1079            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1080            cx,
1081        )
1082        .await;
1083        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1084        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1085            let worktrees: Vec<_> = store.worktrees().collect();
1086            assert_eq!(worktrees.len(), 2);
1087            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1088                (&worktrees[1], &worktrees[0])
1089            } else {
1090                (&worktrees[0], &worktrees[1])
1091            };
1092            assert!(!dir_worktree.read(cx).is_single_file());
1093            assert!(file_worktree.read(cx).is_single_file());
1094            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1095        });
1096
1097        let trusted_worktrees = init_trust_global(worktree_store, cx);
1098
1099        let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
1100            store.can_trust_local_worktree(file_worktree_id, cx)
1101        });
1102        assert!(
1103            !can_trust_file,
1104            "single-file worktree should be restricted initially"
1105        );
1106
1107        let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
1108            store.can_trust_local_worktree(dir_worktree_id, cx)
1109        });
1110        assert!(
1111            !can_trust_directory,
1112            "directory worktree should be restricted initially"
1113        );
1114
1115        trusted_worktrees.update(cx, |store, cx| {
1116            store.trust(
1117                HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
1118                None,
1119                cx,
1120            );
1121        });
1122
1123        let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
1124            store.can_trust_local_worktree(dir_worktree_id, cx)
1125        });
1126        let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
1127            store.can_trust_local_worktree(file_worktree_id, cx)
1128        });
1129        assert!(can_trust_dir, "directory worktree should be trusted");
1130        assert!(
1131            can_trust_file_after,
1132            "single-file worktree should be trusted after directory worktree trust"
1133        );
1134    }
1135
1136    #[gpui::test]
1137    async fn test_parent_path_trust_enables_single_file(cx: &mut TestAppContext) {
1138        init_test(cx);
1139
1140        let fs = FakeFs::new(cx.executor());
1141        fs.insert_tree(
1142            path!("/"),
1143            json!({
1144                "project": { "main.rs": "fn main() {}" },
1145                "standalone.rs": "fn standalone() {}"
1146            }),
1147        )
1148        .await;
1149
1150        let project = Project::test(
1151            fs,
1152            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1153            cx,
1154        )
1155        .await;
1156        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1157        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1158            let worktrees: Vec<_> = store.worktrees().collect();
1159            assert_eq!(worktrees.len(), 2);
1160            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1161                (&worktrees[1], &worktrees[0])
1162            } else {
1163                (&worktrees[0], &worktrees[1])
1164            };
1165            assert!(!dir_worktree.read(cx).is_single_file());
1166            assert!(file_worktree.read(cx).is_single_file());
1167            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1168        });
1169
1170        let trusted_worktrees = init_trust_global(worktree_store, cx);
1171
1172        let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
1173            store.can_trust_local_worktree(file_worktree_id, cx)
1174        });
1175        assert!(
1176            !can_trust_file,
1177            "single-file worktree should be restricted initially"
1178        );
1179
1180        let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
1181            store.can_trust_local_worktree(dir_worktree_id, cx)
1182        });
1183        assert!(
1184            !can_trust_directory,
1185            "directory worktree should be restricted initially"
1186        );
1187
1188        trusted_worktrees.update(cx, |store, cx| {
1189            store.trust(
1190                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/project")))]),
1191                None,
1192                cx,
1193            );
1194        });
1195
1196        let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
1197            store.can_trust_local_worktree(dir_worktree_id, cx)
1198        });
1199        let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
1200            store.can_trust_local_worktree(file_worktree_id, cx)
1201        });
1202        assert!(
1203            can_trust_dir,
1204            "directory worktree should be trusted after its parent is trusted"
1205        );
1206        assert!(
1207            can_trust_file_after,
1208            "single-file worktree should be trusted after directory worktree trust via its parent directory trust"
1209        );
1210    }
1211
1212    #[gpui::test]
1213    async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
1214        init_test(cx);
1215
1216        let fs = FakeFs::new(cx.executor());
1217        fs.insert_tree(
1218            path!("/root"),
1219            json!({
1220                "project_a": { "main.rs": "fn main() {}" },
1221                "project_b": { "lib.rs": "pub fn lib() {}" }
1222            }),
1223        )
1224        .await;
1225
1226        let project = Project::test(
1227            fs,
1228            [
1229                path!("/root/project_a").as_ref(),
1230                path!("/root/project_b").as_ref(),
1231            ],
1232            cx,
1233        )
1234        .await;
1235        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1236        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1237            store
1238                .worktrees()
1239                .map(|worktree| worktree.read(cx).id())
1240                .collect()
1241        });
1242        assert_eq!(worktree_ids.len(), 2);
1243
1244        let trusted_worktrees = init_trust_global(worktree_store, cx);
1245
1246        for &worktree_id in &worktree_ids {
1247            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1248                store.can_trust_local_worktree(worktree_id, cx)
1249            });
1250            assert!(!can_trust, "worktree should be restricted initially");
1251        }
1252
1253        trusted_worktrees.update(cx, |store, cx| {
1254            store.trust(
1255                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
1256                None,
1257                cx,
1258            );
1259        });
1260
1261        for &worktree_id in &worktree_ids {
1262            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1263                store.can_trust_local_worktree(worktree_id, cx)
1264            });
1265            assert!(
1266                can_trust,
1267                "worktree should be trusted after parent path trust"
1268            );
1269        }
1270    }
1271
1272    #[gpui::test]
1273    async fn test_auto_trust_all(cx: &mut TestAppContext) {
1274        init_test(cx);
1275
1276        let fs = FakeFs::new(cx.executor());
1277        fs.insert_tree(
1278            path!("/"),
1279            json!({
1280                "project_a": { "main.rs": "fn main() {}" },
1281                "project_b": { "lib.rs": "pub fn lib() {}" },
1282                "single.rs": "fn single() {}"
1283            }),
1284        )
1285        .await;
1286
1287        let project = Project::test(
1288            fs,
1289            [
1290                path!("/project_a").as_ref(),
1291                path!("/project_b").as_ref(),
1292                path!("/single.rs").as_ref(),
1293            ],
1294            cx,
1295        )
1296        .await;
1297        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1298        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1299            store
1300                .worktrees()
1301                .map(|worktree| worktree.read(cx).id())
1302                .collect()
1303        });
1304        assert_eq!(worktree_ids.len(), 3);
1305
1306        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1307
1308        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1309        cx.update({
1310            let events = events.clone();
1311            |cx| {
1312                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1313                    events.borrow_mut().push(match event {
1314                        TrustedWorktreesEvent::Trusted(host, paths) => {
1315                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1316                        }
1317                        TrustedWorktreesEvent::Restricted(host, paths) => {
1318                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1319                        }
1320                    });
1321                })
1322            }
1323        })
1324        .detach();
1325
1326        for &worktree_id in &worktree_ids {
1327            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1328                store.can_trust_local_worktree(worktree_id, cx)
1329            });
1330            assert!(!can_trust, "worktree should be restricted initially");
1331        }
1332
1333        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1334            store.has_restricted_worktrees(&worktree_store, cx)
1335        });
1336        assert!(has_restricted, "should have restricted worktrees");
1337
1338        events.borrow_mut().clear();
1339
1340        trusted_worktrees.update(cx, |store, cx| {
1341            store.auto_trust_all(cx);
1342        });
1343
1344        for &worktree_id in &worktree_ids {
1345            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1346                store.can_trust_local_worktree(worktree_id, cx)
1347            });
1348            assert!(
1349                can_trust,
1350                "worktree {worktree_id:?} should be trusted after auto_trust_all"
1351            );
1352        }
1353
1354        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
1355            store.has_restricted_worktrees(&worktree_store, cx)
1356        });
1357        assert!(
1358            !has_restricted_after,
1359            "should have no restricted worktrees after auto_trust_all"
1360        );
1361
1362        let trusted_event_count = events
1363            .borrow()
1364            .iter()
1365            .filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
1366            .count();
1367        assert!(
1368            trusted_event_count > 0,
1369            "should have emitted Trusted events"
1370        );
1371    }
1372
1373    #[gpui::test]
1374    async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
1375        init_test(cx);
1376
1377        let fs = FakeFs::new(cx.executor());
1378        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1379            .await;
1380
1381        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1382        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1383        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1384            store.worktrees().next().unwrap().read(cx).id()
1385        });
1386
1387        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1388
1389        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1390        cx.update({
1391            let events = events.clone();
1392            |cx| {
1393                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1394                    events.borrow_mut().push(match event {
1395                        TrustedWorktreesEvent::Trusted(host, paths) => {
1396                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1397                        }
1398                        TrustedWorktreesEvent::Restricted(host, paths) => {
1399                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1400                        }
1401                    });
1402                })
1403            }
1404        })
1405        .detach();
1406
1407        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1408            store.can_trust_local_worktree(worktree_id, cx)
1409        });
1410        assert!(!can_trust, "should be restricted initially");
1411        assert_eq!(events.borrow().len(), 1);
1412        events.borrow_mut().clear();
1413
1414        trusted_worktrees.update(cx, |store, cx| {
1415            store.trust(
1416                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1417                None,
1418                cx,
1419            );
1420        });
1421        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1422            store.can_trust_local_worktree(worktree_id, cx)
1423        });
1424        assert!(can_trust, "should be trusted after trust()");
1425        assert_eq!(events.borrow().len(), 1);
1426        assert!(matches!(
1427            &events.borrow()[0],
1428            TrustedWorktreesEvent::Trusted(..)
1429        ));
1430        events.borrow_mut().clear();
1431
1432        trusted_worktrees.update(cx, |store, cx| {
1433            store.restrict(
1434                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1435                None,
1436                cx,
1437            );
1438        });
1439        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1440            store.can_trust_local_worktree(worktree_id, cx)
1441        });
1442        assert!(!can_trust, "should be restricted after restrict()");
1443        assert_eq!(events.borrow().len(), 1);
1444        assert!(matches!(
1445            &events.borrow()[0],
1446            TrustedWorktreesEvent::Restricted(..)
1447        ));
1448
1449        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1450            store.has_restricted_worktrees(&worktree_store, cx)
1451        });
1452        assert!(has_restricted);
1453        events.borrow_mut().clear();
1454
1455        trusted_worktrees.update(cx, |store, cx| {
1456            store.trust(
1457                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1458                None,
1459                cx,
1460            );
1461        });
1462        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1463            store.can_trust_local_worktree(worktree_id, cx)
1464        });
1465        assert!(can_trust, "should be trusted again after second trust()");
1466        assert_eq!(events.borrow().len(), 1);
1467        assert!(matches!(
1468            &events.borrow()[0],
1469            TrustedWorktreesEvent::Trusted(..)
1470        ));
1471
1472        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1473            store.has_restricted_worktrees(&worktree_store, cx)
1474        });
1475        assert!(!has_restricted);
1476    }
1477
1478    #[gpui::test]
1479    async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
1480        init_test(cx);
1481
1482        let fs = FakeFs::new(cx.executor());
1483        fs.insert_tree(
1484            path!("/"),
1485            json!({
1486                "local_project": { "main.rs": "fn main() {}" },
1487                "remote_project": { "lib.rs": "pub fn lib() {}" }
1488            }),
1489        )
1490        .await;
1491
1492        let project = Project::test(
1493            fs,
1494            [
1495                path!("/local_project").as_ref(),
1496                path!("/remote_project").as_ref(),
1497            ],
1498            cx,
1499        )
1500        .await;
1501        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1502        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1503            store
1504                .worktrees()
1505                .map(|worktree| worktree.read(cx).id())
1506                .collect()
1507        });
1508        assert_eq!(worktree_ids.len(), 2);
1509        let local_worktree = worktree_ids[0];
1510        let _remote_worktree = worktree_ids[1];
1511
1512        let trusted_worktrees = init_trust_global(worktree_store, cx);
1513
1514        let can_trust_local = trusted_worktrees.update(cx, |store, cx| {
1515            store.can_trust_local_worktree(local_worktree, cx)
1516        });
1517        assert!(!can_trust_local, "local worktree restricted on host_a");
1518
1519        trusted_worktrees.update(cx, |store, cx| {
1520            store.trust(
1521                HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
1522                None,
1523                cx,
1524            );
1525        });
1526
1527        let can_trust_local_after = trusted_worktrees.update(cx, |store, cx| {
1528            store.can_trust_local_worktree(local_worktree, cx)
1529        });
1530        assert!(
1531            can_trust_local_after,
1532            "local worktree should be trusted on local host"
1533        );
1534    }
1535}