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