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