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