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