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