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//! * "workspace"
31//!
32//! Even an empty Zed instance with no files or directories open is potentially dangerous: opening an Assistant Panel and creating new external agent thread might require installing and running MCP servers.
33//!
34//! Disabling the entire panel is possible with ai-related settings.
35//! Yet when it's enabled, it's still reasonably safe to use remote AI agents and control their permissions in the Assistant Panel.
36//!
37//! Unlike that, MCP servers are similar to language servers and may require fetching, installing and running packages or binaries.
38//! Given that those servers are not tied to any particular worktree, this level of trust is required to operate any MCP server.
39//!
40//! Workspace level of trust assumes all single file worktrees are trusted too, for the same host: if we allow global MCP server-related functionality, we can already allow spawning language servers for single file worktrees as well.
41//!
42//! * "directory worktree"
43//!
44//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
45//! 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.
46//!
47//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence we also allow workspace level of trust (hence, "single file worktree" level of trust also).
48//!
49//! * "path override"
50//!
51//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
52//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
53//!
54//! If we trust multiple projects to install and spawn various language server processes, we can also allow workspace trust requests for MCP servers installation and spawning.
55
56use collections::{HashMap, HashSet};
57use gpui::{
58 App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,
59};
60use remote::RemoteConnectionOptions;
61use rpc::{AnyProtoClient, proto};
62use settings::{Settings as _, WorktreeId};
63use std::{
64 path::{Path, PathBuf},
65 sync::Arc,
66};
67use util::debug_panic;
68
69use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
70
71pub fn init(
72 db_trusted_paths: TrustedPaths,
73 downstream_client: Option<(AnyProtoClient, u64)>,
74 upstream_client: Option<(AnyProtoClient, u64)>,
75 cx: &mut App,
76) {
77 if TrustedWorktrees::try_get_global(cx).is_none() {
78 let trusted_worktrees = cx.new(|_| {
79 TrustedWorktreesStore::new(
80 db_trusted_paths,
81 None,
82 None,
83 downstream_client,
84 upstream_client,
85 )
86 });
87 cx.set_global(TrustedWorktrees(trusted_worktrees))
88 }
89}
90
91/// An initialization call to set up trust global for a particular project (remote or local).
92pub fn track_worktree_trust(
93 worktree_store: Entity<WorktreeStore>,
94 remote_host: Option<RemoteHostLocation>,
95 downstream_client: Option<(AnyProtoClient, u64)>,
96 upstream_client: Option<(AnyProtoClient, u64)>,
97 cx: &mut App,
98) {
99 match TrustedWorktrees::try_get_global(cx) {
100 Some(trusted_worktrees) => {
101 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
102 let sync_upstream = trusted_worktrees.upstream_client.as_ref().map(|(_, id)| id)
103 != upstream_client.as_ref().map(|(_, id)| id);
104 trusted_worktrees.downstream_client = downstream_client;
105 trusted_worktrees.upstream_client = upstream_client;
106 trusted_worktrees.add_worktree_store(worktree_store, remote_host, cx);
107
108 if sync_upstream {
109 if let Some((upstream_client, upstream_project_id)) =
110 &trusted_worktrees.upstream_client
111 {
112 let trusted_paths = trusted_worktrees
113 .trusted_paths
114 .iter()
115 .flat_map(|(_, paths)| {
116 paths.iter().map(|trusted_path| trusted_path.to_proto())
117 })
118 .collect::<Vec<_>>();
119 if !trusted_paths.is_empty() {
120 upstream_client
121 .send(proto::TrustWorktrees {
122 project_id: *upstream_project_id,
123 trusted_paths,
124 })
125 .ok();
126 }
127 }
128 }
129 });
130 }
131 None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
132 }
133}
134
135/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for the host the [`TrustedWorktrees`] was initialized with.
136pub fn wait_for_default_workspace_trust(
137 what_waits: &'static str,
138 cx: &mut App,
139) -> Option<Task<()>> {
140 let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
141 wait_for_workspace_trust(
142 trusted_worktrees.read(cx).remote_host.clone(),
143 what_waits,
144 cx,
145 )
146}
147
148/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for a particular host.
149pub fn wait_for_workspace_trust(
150 remote_host: Option<impl Into<RemoteHostLocation>>,
151 what_waits: &'static str,
152 cx: &mut App,
153) -> Option<Task<()>> {
154 let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
155 let remote_host = remote_host.map(|host| host.into());
156
157 let remote_host = if trusted_worktrees.update(cx, |trusted_worktrees, cx| {
158 trusted_worktrees.can_trust_workspace(remote_host.clone(), cx)
159 }) {
160 None
161 } else {
162 Some(remote_host)
163 }?;
164
165 Some(cx.spawn(async move |cx| {
166 log::info!("Waiting for workspace to be trusted before starting {what_waits}");
167 let (tx, restricted_worktrees_task) = smol::channel::bounded::<()>(1);
168 let Ok(_subscription) = cx.update(|cx| {
169 cx.subscribe(&trusted_worktrees, move |_, e, _| {
170 if let TrustedWorktreesEvent::Trusted(trusted_host, trusted_paths) = e {
171 if trusted_host == &remote_host && trusted_paths.contains(&PathTrust::Workspace)
172 {
173 log::info!("Workspace is trusted for {what_waits}");
174 tx.send_blocking(()).ok();
175 }
176 }
177 })
178 }) else {
179 return;
180 };
181
182 restricted_worktrees_task.recv().await.ok();
183 }))
184}
185
186/// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
187pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
188
189impl Global for TrustedWorktrees {}
190
191impl TrustedWorktrees {
192 pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
193 cx.try_global::<Self>().map(|this| this.0.clone())
194 }
195}
196
197/// A collection of worktrees that are considered trusted and not trusted.
198/// This can be used when checking for this criteria before enabling certain features.
199///
200/// Emits an event each time the worktree was checked and found not trusted,
201/// or a certain worktree had been trusted.
202pub struct TrustedWorktreesStore {
203 downstream_client: Option<(AnyProtoClient, u64)>,
204 upstream_client: Option<(AnyProtoClient, u64)>,
205 worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
206 trusted_paths: TrustedPaths,
207 restricted: HashSet<WorktreeId>,
208 remote_host: Option<RemoteHostLocation>,
209 restricted_workspaces: HashSet<Option<RemoteHostLocation>>,
210}
211
212/// An identifier of a host to split the trust questions by.
213/// Each trusted data change and event is done for a particular host.
214/// A host may contain more than one worktree or even project open concurrently.
215#[derive(Debug, PartialEq, Eq, Clone, Hash)]
216pub struct RemoteHostLocation {
217 pub user_name: Option<SharedString>,
218 pub host_identifier: SharedString,
219}
220
221impl From<RemoteConnectionOptions> for RemoteHostLocation {
222 fn from(options: RemoteConnectionOptions) -> Self {
223 let (user_name, host_name) = match options {
224 RemoteConnectionOptions::Ssh(ssh) => (
225 ssh.username.map(SharedString::new),
226 SharedString::new(ssh.host),
227 ),
228 RemoteConnectionOptions::Wsl(wsl) => (
229 wsl.user.map(SharedString::new),
230 SharedString::new(wsl.distro_name),
231 ),
232 RemoteConnectionOptions::Docker(docker_connection_options) => (
233 Some(SharedString::new(docker_connection_options.name)),
234 SharedString::new(docker_connection_options.container_id),
235 ),
236 };
237 RemoteHostLocation {
238 user_name,
239 host_identifier: host_name,
240 }
241 }
242}
243
244/// A unit of trust consideration inside a particular host:
245/// either a familiar worktree, or a path that may influence other worktrees' trust.
246/// See module-level documentation on the trust model.
247#[derive(Debug, PartialEq, Eq, Clone, Hash)]
248pub enum PathTrust {
249 /// General, no worktrees or files open case.
250 /// E.g. MCP servers can be spawned from a blank Zed instance, but will do `npm i` and other potentially malicious actions.
251 Workspace,
252 /// A worktree that is familiar to this workspace.
253 /// Either a single file or a directory worktree.
254 Worktree(WorktreeId),
255 /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),
256 /// or a parent path coming out of the security modal.
257 AbsPath(PathBuf),
258}
259
260impl PathTrust {
261 fn to_proto(&self) -> proto::PathTrust {
262 match self {
263 Self::Workspace => proto::PathTrust {
264 content: Some(proto::path_trust::Content::Workspace(0)),
265 },
266 Self::Worktree(worktree_id) => proto::PathTrust {
267 content: Some(proto::path_trust::Content::WorktreeId(
268 worktree_id.to_proto(),
269 )),
270 },
271 Self::AbsPath(path_buf) => proto::PathTrust {
272 content: Some(proto::path_trust::Content::AbsPath(
273 path_buf.to_string_lossy().to_string(),
274 )),
275 },
276 }
277 }
278
279 pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {
280 Some(match proto.content? {
281 proto::path_trust::Content::WorktreeId(id) => {
282 Self::Worktree(WorktreeId::from_proto(id))
283 }
284 proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
285 proto::path_trust::Content::Workspace(_) => Self::Workspace,
286 })
287 }
288}
289
290/// A change of trust on a certain host.
291#[derive(Debug)]
292pub enum TrustedWorktreesEvent {
293 Trusted(Option<RemoteHostLocation>, HashSet<PathTrust>),
294 Restricted(Option<RemoteHostLocation>, HashSet<PathTrust>),
295}
296
297impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
298
299pub type TrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>;
300
301impl TrustedWorktreesStore {
302 fn new(
303 trusted_paths: TrustedPaths,
304 worktree_store: Option<Entity<WorktreeStore>>,
305 remote_host: Option<RemoteHostLocation>,
306 downstream_client: Option<(AnyProtoClient, u64)>,
307 upstream_client: Option<(AnyProtoClient, u64)>,
308 ) -> Self {
309 if let Some((upstream_client, upstream_project_id)) = &upstream_client {
310 let trusted_paths = trusted_paths
311 .iter()
312 .flat_map(|(_, paths)| paths.iter().map(|trusted_path| trusted_path.to_proto()))
313 .collect::<Vec<_>>();
314 if !trusted_paths.is_empty() {
315 upstream_client
316 .send(proto::TrustWorktrees {
317 project_id: *upstream_project_id,
318 trusted_paths,
319 })
320 .ok();
321 }
322 }
323
324 let worktree_stores = match worktree_store {
325 Some(worktree_store) => {
326 HashMap::from_iter([(worktree_store.downgrade(), remote_host.clone())])
327 }
328 None => HashMap::default(),
329 };
330
331 Self {
332 trusted_paths,
333 downstream_client,
334 upstream_client,
335 remote_host,
336 restricted_workspaces: HashSet::default(),
337 restricted: HashSet::default(),
338 worktree_stores,
339 }
340 }
341
342 /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.
343 pub fn has_restricted_worktrees(
344 &self,
345 worktree_store: &Entity<WorktreeStore>,
346 cx: &App,
347 ) -> bool {
348 let Some(remote_host) = self.worktree_stores.get(&worktree_store.downgrade()) else {
349 return false;
350 };
351 self.restricted_workspaces.contains(remote_host)
352 || self.restricted.iter().any(|restricted_worktree| {
353 worktree_store
354 .read(cx)
355 .worktree_for_id(*restricted_worktree, cx)
356 .is_some()
357 })
358 }
359
360 /// Adds certain entities on this host to the trusted list.
361 /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries
362 /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).
363 pub fn trust(
364 &mut self,
365 mut trusted_paths: HashSet<PathTrust>,
366 remote_host: Option<RemoteHostLocation>,
367 cx: &mut Context<Self>,
368 ) {
369 let mut new_workspace_trusted = false;
370 let mut new_trusted_single_file_worktrees = HashSet::default();
371 let mut new_trusted_other_worktrees = HashSet::default();
372 let mut new_trusted_abs_paths = HashSet::default();
373 for trusted_path in trusted_paths.iter().chain(
374 self.trusted_paths
375 .remove(&remote_host)
376 .iter()
377 .flat_map(|current_trusted| current_trusted.iter()),
378 ) {
379 match trusted_path {
380 PathTrust::Workspace => new_workspace_trusted = true,
381 PathTrust::Worktree(worktree_id) => {
382 self.restricted.remove(worktree_id);
383 if let Some((abs_path, is_file, host)) =
384 self.find_worktree_data(*worktree_id, cx)
385 {
386 if host == remote_host {
387 if is_file {
388 new_trusted_single_file_worktrees.insert(*worktree_id);
389 } else {
390 new_trusted_other_worktrees.insert((abs_path, *worktree_id));
391 new_workspace_trusted = true;
392 }
393 }
394 }
395 }
396 PathTrust::AbsPath(path) => {
397 new_workspace_trusted = true;
398 debug_assert!(
399 path.is_absolute(),
400 "Cannot trust non-absolute path {path:?}"
401 );
402 new_trusted_abs_paths.insert(path.clone());
403 }
404 }
405 }
406
407 if new_workspace_trusted {
408 new_trusted_single_file_worktrees.clear();
409 self.restricted_workspaces.remove(&remote_host);
410 trusted_paths.insert(PathTrust::Workspace);
411 }
412 new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
413 new_trusted_abs_paths
414 .iter()
415 .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))
416 });
417 if !new_trusted_other_worktrees.is_empty() {
418 new_trusted_single_file_worktrees.clear();
419 }
420 self.restricted = std::mem::take(&mut self.restricted)
421 .into_iter()
422 .filter(|restricted_worktree| {
423 let Some((restricted_worktree_path, is_file, restricted_host)) =
424 self.find_worktree_data(*restricted_worktree, cx)
425 else {
426 return false;
427 };
428 if restricted_host != remote_host {
429 return true;
430 }
431 let retain = (!is_file
432 || (!new_workspace_trusted && new_trusted_other_worktrees.is_empty()))
433 && new_trusted_abs_paths.iter().all(|new_trusted_path| {
434 !restricted_worktree_path.starts_with(new_trusted_path)
435 });
436 if !retain {
437 trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
438 }
439 retain
440 })
441 .collect();
442
443 {
444 let trusted_paths = self.trusted_paths.entry(remote_host.clone()).or_default();
445 trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
446 trusted_paths.extend(
447 new_trusted_other_worktrees
448 .into_iter()
449 .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
450 );
451 trusted_paths.extend(
452 new_trusted_single_file_worktrees
453 .into_iter()
454 .map(PathTrust::Worktree),
455 );
456 if trusted_paths.is_empty() && new_workspace_trusted {
457 trusted_paths.insert(PathTrust::Workspace);
458 }
459 }
460
461 cx.emit(TrustedWorktreesEvent::Trusted(
462 remote_host,
463 trusted_paths.clone(),
464 ));
465
466 if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
467 let trusted_paths = trusted_paths
468 .iter()
469 .map(|trusted_path| trusted_path.to_proto())
470 .collect::<Vec<_>>();
471 if !trusted_paths.is_empty() {
472 upstream_client
473 .send(proto::TrustWorktrees {
474 project_id: *upstream_project_id,
475 trusted_paths,
476 })
477 .ok();
478 }
479 }
480 }
481
482 /// Restricts certain entities on this host.
483 /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
484 pub fn restrict(
485 &mut self,
486 restricted_paths: HashSet<PathTrust>,
487 remote_host: Option<RemoteHostLocation>,
488 cx: &mut Context<Self>,
489 ) {
490 for restricted_path in restricted_paths {
491 match restricted_path {
492 PathTrust::Workspace => {
493 self.restricted_workspaces.insert(remote_host.clone());
494 cx.emit(TrustedWorktreesEvent::Restricted(
495 remote_host.clone(),
496 HashSet::from_iter([PathTrust::Workspace]),
497 ));
498 }
499 PathTrust::Worktree(worktree_id) => {
500 self.restricted.insert(worktree_id);
501 cx.emit(TrustedWorktreesEvent::Restricted(
502 remote_host.clone(),
503 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
504 ));
505 }
506 PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
507 }
508 }
509 }
510
511 /// Erases all trust information.
512 /// Requires Zed's restart to take proper effect.
513 pub fn clear_trusted_paths(&mut self) {
514 self.trusted_paths.clear();
515 }
516
517 /// Checks whether a certain worktree is trusted (or on a larger trust level).
518 /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
519 ///
520 /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
521 pub fn can_trust(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) -> bool {
522 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
523 return true;
524 }
525 if self.restricted.contains(&worktree_id) {
526 return false;
527 }
528
529 let Some((worktree_path, is_file, remote_host)) = self.find_worktree_data(worktree_id, cx)
530 else {
531 return false;
532 };
533
534 if self
535 .trusted_paths
536 .get(&remote_host)
537 .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
538 {
539 return true;
540 }
541
542 // See module documentation for details on trust level.
543 if is_file && self.trusted_paths.contains_key(&remote_host) {
544 return true;
545 }
546
547 let parent_path_trusted =
548 self.trusted_paths
549 .get(&remote_host)
550 .is_some_and(|trusted_paths| {
551 trusted_paths.iter().any(|trusted_path| {
552 let PathTrust::AbsPath(trusted_path) = trusted_path else {
553 return false;
554 };
555 worktree_path.starts_with(trusted_path)
556 })
557 });
558 if parent_path_trusted {
559 return true;
560 }
561
562 self.restricted.insert(worktree_id);
563 cx.emit(TrustedWorktreesEvent::Restricted(
564 remote_host,
565 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
566 ));
567 if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
568 downstream_client
569 .send(proto::RestrictWorktrees {
570 project_id: *downstream_project_id,
571 restrict_workspace: false,
572 worktree_ids: vec![worktree_id.to_proto()],
573 })
574 .ok();
575 }
576 if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
577 upstream_client
578 .send(proto::RestrictWorktrees {
579 project_id: *upstream_project_id,
580 restrict_workspace: false,
581 worktree_ids: vec![worktree_id.to_proto()],
582 })
583 .ok();
584 }
585 false
586 }
587
588 /// Checks whether a certain worktree is trusted globally (or on a larger trust level).
589 /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if checked for the first time and not trusted.
590 ///
591 /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
592 pub fn can_trust_workspace(
593 &mut self,
594 remote_host: Option<RemoteHostLocation>,
595 cx: &mut Context<Self>,
596 ) -> bool {
597 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
598 return true;
599 }
600 if self.restricted_workspaces.contains(&remote_host) {
601 return false;
602 }
603 if self.trusted_paths.contains_key(&remote_host) {
604 return true;
605 }
606
607 self.restricted_workspaces.insert(remote_host.clone());
608 cx.emit(TrustedWorktreesEvent::Restricted(
609 remote_host.clone(),
610 HashSet::from_iter([PathTrust::Workspace]),
611 ));
612
613 if remote_host == self.remote_host {
614 if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
615 downstream_client
616 .send(proto::RestrictWorktrees {
617 project_id: *downstream_project_id,
618 restrict_workspace: true,
619 worktree_ids: Vec::new(),
620 })
621 .ok();
622 }
623 if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
624 upstream_client
625 .send(proto::RestrictWorktrees {
626 project_id: *upstream_project_id,
627 restrict_workspace: true,
628 worktree_ids: Vec::new(),
629 })
630 .ok();
631 }
632 }
633 false
634 }
635
636 /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_workspace`] method calls) for a particular worktree store on a particular host.
637 pub fn restricted_worktrees(
638 &self,
639 worktree_store: &WorktreeStore,
640 remote_host: Option<RemoteHostLocation>,
641 cx: &App,
642 ) -> HashSet<Option<(WorktreeId, Arc<Path>)>> {
643 let mut single_file_paths = HashSet::default();
644 let other_paths = self
645 .restricted
646 .iter()
647 .filter_map(|&restricted_worktree_id| {
648 let worktree = worktree_store.worktree_for_id(restricted_worktree_id, cx)?;
649 let worktree = worktree.read(cx);
650 let abs_path = worktree.abs_path();
651 if worktree.is_single_file() {
652 single_file_paths.insert(Some((restricted_worktree_id, abs_path)));
653 None
654 } else {
655 Some((restricted_worktree_id, abs_path))
656 }
657 })
658 .map(Some)
659 .collect::<HashSet<_>>();
660
661 if !other_paths.is_empty() {
662 return other_paths;
663 } else if self.restricted_workspaces.contains(&remote_host) {
664 return HashSet::from_iter([None]);
665 } else {
666 single_file_paths
667 }
668 }
669
670 /// Switches the "trust nothing" mode to "automatically trust everything".
671 /// This does not influence already persisted data, but stops adding new worktrees there.
672 pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
673 for (remote_host, mut worktrees) in std::mem::take(&mut self.restricted)
674 .into_iter()
675 .flat_map(|restricted_worktree| {
676 let (_, _, host) = self.find_worktree_data(restricted_worktree, cx)?;
677 Some((restricted_worktree, host))
678 })
679 .fold(HashMap::default(), |mut acc, (worktree_id, remote_host)| {
680 acc.entry(remote_host)
681 .or_insert_with(HashSet::default)
682 .insert(PathTrust::Worktree(worktree_id));
683 acc
684 })
685 {
686 if self.restricted_workspaces.remove(&remote_host) {
687 worktrees.insert(PathTrust::Workspace);
688 }
689 self.trust(worktrees, remote_host, cx);
690 }
691
692 for remote_host in std::mem::take(&mut self.restricted_workspaces) {
693 self.trust(HashSet::from_iter([PathTrust::Workspace]), remote_host, cx);
694 }
695 }
696
697 /// Returns a normalized representation of the trusted paths to store in the DB.
698 pub fn trusted_paths_for_serialization(
699 &mut self,
700 cx: &mut Context<Self>,
701 ) -> (
702 HashSet<Option<RemoteHostLocation>>,
703 HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
704 ) {
705 let mut new_trusted_workspaces = HashSet::default();
706 let new_trusted_worktrees = self
707 .trusted_paths
708 .clone()
709 .into_iter()
710 .map(|(host, paths)| {
711 let abs_paths = paths
712 .into_iter()
713 .flat_map(|path| match path {
714 PathTrust::Worktree(worktree_id) => self
715 .find_worktree_data(worktree_id, cx)
716 .map(|(abs_path, ..)| abs_path.to_path_buf()),
717 PathTrust::AbsPath(abs_path) => Some(abs_path),
718 PathTrust::Workspace => {
719 new_trusted_workspaces.insert(host.clone());
720 None
721 }
722 })
723 .collect();
724 (host, abs_paths)
725 })
726 .collect();
727 (new_trusted_workspaces, new_trusted_worktrees)
728 }
729
730 fn find_worktree_data(
731 &mut self,
732 worktree_id: WorktreeId,
733 cx: &mut Context<Self>,
734 ) -> Option<(Arc<Path>, bool, Option<RemoteHostLocation>)> {
735 let mut worktree_data = None;
736 self.worktree_stores.retain(
737 |worktree_store, remote_host| match worktree_store.upgrade() {
738 Some(worktree_store) => {
739 if worktree_data.is_none() {
740 if let Some(worktree) =
741 worktree_store.read(cx).worktree_for_id(worktree_id, cx)
742 {
743 worktree_data = Some((
744 worktree.read(cx).abs_path(),
745 worktree.read(cx).is_single_file(),
746 remote_host.clone(),
747 ));
748 }
749 }
750 true
751 }
752 None => false,
753 },
754 );
755 worktree_data
756 }
757
758 fn add_worktree_store(
759 &mut self,
760 worktree_store: Entity<WorktreeStore>,
761 remote_host: Option<RemoteHostLocation>,
762 cx: &mut Context<Self>,
763 ) {
764 self.worktree_stores
765 .insert(worktree_store.downgrade(), remote_host.clone());
766
767 if let Some(trusted_paths) = self.trusted_paths.remove(&remote_host) {
768 self.trusted_paths.insert(
769 remote_host.clone(),
770 trusted_paths
771 .into_iter()
772 .map(|path_trust| match path_trust {
773 PathTrust::AbsPath(abs_path) => {
774 find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
775 .map(PathTrust::Worktree)
776 .unwrap_or_else(|| PathTrust::AbsPath(abs_path))
777 }
778 other => other,
779 })
780 .collect(),
781 );
782 }
783 }
784}
785
786pub fn find_worktree_in_store(
787 worktree_store: &WorktreeStore,
788 abs_path: &Path,
789 cx: &App,
790) -> Option<WorktreeId> {
791 let (worktree, path_in_worktree) = worktree_store.find_worktree(&abs_path, cx)?;
792 if path_in_worktree.is_empty() {
793 Some(worktree.read(cx).id())
794 } else {
795 None
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use std::{cell::RefCell, path::PathBuf, rc::Rc};
802
803 use collections::HashSet;
804 use gpui::TestAppContext;
805 use serde_json::json;
806 use settings::SettingsStore;
807 use util::path;
808
809 use crate::{FakeFs, Project};
810
811 use super::*;
812
813 fn init_test(cx: &mut TestAppContext) {
814 cx.update(|cx| {
815 if cx.try_global::<SettingsStore>().is_none() {
816 let settings_store = SettingsStore::test(cx);
817 cx.set_global(settings_store);
818 }
819 if cx.try_global::<TrustedWorktrees>().is_some() {
820 cx.remove_global::<TrustedWorktrees>();
821 }
822 });
823 }
824
825 fn init_trust_global(
826 worktree_store: Entity<WorktreeStore>,
827 cx: &mut TestAppContext,
828 ) -> Entity<TrustedWorktreesStore> {
829 cx.update(|cx| {
830 init(HashMap::default(), None, None, cx);
831 track_worktree_trust(worktree_store, None, None, None, cx);
832 TrustedWorktrees::try_get_global(cx).expect("global should be set")
833 })
834 }
835
836 #[gpui::test]
837 async fn test_single_worktree_trust(cx: &mut TestAppContext) {
838 init_test(cx);
839
840 let fs = FakeFs::new(cx.executor());
841 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
842 .await;
843
844 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
845 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
846 let worktree_id = worktree_store.read_with(cx, |store, cx| {
847 store.worktrees().next().unwrap().read(cx).id()
848 });
849
850 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
851
852 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
853 cx.update({
854 let events = events.clone();
855 |cx| {
856 cx.subscribe(&trusted_worktrees, move |_, event, _| {
857 events.borrow_mut().push(match event {
858 TrustedWorktreesEvent::Trusted(host, paths) => {
859 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
860 }
861 TrustedWorktreesEvent::Restricted(host, paths) => {
862 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
863 }
864 });
865 })
866 }
867 })
868 .detach();
869
870 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
871 assert!(!can_trust, "worktree should be restricted by default");
872
873 {
874 let events = events.borrow();
875 assert_eq!(events.len(), 1);
876 match &events[0] {
877 TrustedWorktreesEvent::Restricted(host, paths) => {
878 assert!(host.is_none());
879 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
880 }
881 _ => panic!("expected Restricted event"),
882 }
883 }
884
885 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
886 store.has_restricted_worktrees(&worktree_store, cx)
887 });
888 assert!(has_restricted, "should have restricted worktrees");
889
890 let restricted = worktree_store.read_with(cx, |ws, cx| {
891 trusted_worktrees
892 .read(cx)
893 .restricted_worktrees(ws, None, cx)
894 });
895 assert!(
896 restricted
897 .iter()
898 .any(|r| r.as_ref().map(|(id, _)| *id) == Some(worktree_id))
899 );
900
901 events.borrow_mut().clear();
902
903 let can_trust_again =
904 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
905 assert!(!can_trust_again, "worktree should still be restricted");
906 assert!(
907 events.borrow().is_empty(),
908 "no duplicate Restricted event on repeated can_trust"
909 );
910
911 trusted_worktrees.update(cx, |store, cx| {
912 store.trust(
913 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
914 None,
915 cx,
916 );
917 });
918
919 {
920 let events = events.borrow();
921 assert_eq!(events.len(), 1);
922 match &events[0] {
923 TrustedWorktreesEvent::Trusted(host, paths) => {
924 assert!(host.is_none());
925 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
926 }
927 _ => panic!("expected Trusted event"),
928 }
929 }
930
931 let can_trust_after =
932 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
933 assert!(can_trust_after, "worktree should be trusted after trust()");
934
935 let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
936 store.has_restricted_worktrees(&worktree_store, cx)
937 });
938 assert!(
939 !has_restricted_after,
940 "should have no restricted worktrees after trust"
941 );
942
943 let restricted_after = worktree_store.read_with(cx, |ws, cx| {
944 trusted_worktrees
945 .read(cx)
946 .restricted_worktrees(ws, None, cx)
947 });
948 assert!(
949 restricted_after.is_empty(),
950 "restricted set should be empty"
951 );
952 }
953
954 #[gpui::test]
955 async fn test_workspace_trust_no_worktrees(cx: &mut TestAppContext) {
956 init_test(cx);
957
958 let fs = FakeFs::new(cx.executor());
959 fs.insert_tree(path!("/root"), json!({})).await;
960
961 let project = Project::test(fs, Vec::<&Path>::new(), cx).await;
962 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
963
964 let trusted_worktrees = init_trust_global(worktree_store, cx);
965
966 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
967 cx.update({
968 let events = events.clone();
969 |cx| {
970 cx.subscribe(&trusted_worktrees, move |_, event, _| {
971 events.borrow_mut().push(match event {
972 TrustedWorktreesEvent::Trusted(host, paths) => {
973 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
974 }
975 TrustedWorktreesEvent::Restricted(host, paths) => {
976 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
977 }
978 });
979 })
980 }
981 })
982 .detach();
983
984 let can_trust_workspace =
985 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
986 assert!(
987 !can_trust_workspace,
988 "workspace should be restricted by default"
989 );
990
991 {
992 let events = events.borrow();
993 assert_eq!(events.len(), 1);
994 match &events[0] {
995 TrustedWorktreesEvent::Restricted(host, paths) => {
996 assert!(host.is_none());
997 assert!(paths.contains(&PathTrust::Workspace));
998 }
999 _ => panic!("expected Restricted event"),
1000 }
1001 }
1002
1003 events.borrow_mut().clear();
1004
1005 let can_trust_workspace_again =
1006 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1007 assert!(
1008 !can_trust_workspace_again,
1009 "workspace should still be restricted"
1010 );
1011 assert!(
1012 events.borrow().is_empty(),
1013 "no duplicate Restricted event on repeated can_trust_workspace"
1014 );
1015
1016 trusted_worktrees.update(cx, |store, cx| {
1017 store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1018 });
1019
1020 {
1021 let events = events.borrow();
1022 assert_eq!(events.len(), 1);
1023 match &events[0] {
1024 TrustedWorktreesEvent::Trusted(host, paths) => {
1025 assert!(host.is_none());
1026 assert!(paths.contains(&PathTrust::Workspace));
1027 }
1028 _ => panic!("expected Trusted event"),
1029 }
1030 }
1031
1032 let can_trust_workspace_after =
1033 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1034 assert!(
1035 can_trust_workspace_after,
1036 "workspace should be trusted after trust()"
1037 );
1038 }
1039
1040 #[gpui::test]
1041 async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
1042 init_test(cx);
1043
1044 let fs = FakeFs::new(cx.executor());
1045 fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
1046 .await;
1047
1048 let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
1049 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1050 let worktree_id = worktree_store.read_with(cx, |store, cx| {
1051 let worktree = store.worktrees().next().unwrap();
1052 let worktree = worktree.read(cx);
1053 assert!(worktree.is_single_file(), "expected single-file worktree");
1054 worktree.id()
1055 });
1056
1057 let trusted_worktrees = init_trust_global(worktree_store, cx);
1058
1059 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1060 cx.update({
1061 let events = events.clone();
1062 |cx| {
1063 cx.subscribe(&trusted_worktrees, move |_, event, _| {
1064 events.borrow_mut().push(match event {
1065 TrustedWorktreesEvent::Trusted(host, paths) => {
1066 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1067 }
1068 TrustedWorktreesEvent::Restricted(host, paths) => {
1069 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1070 }
1071 });
1072 })
1073 }
1074 })
1075 .detach();
1076
1077 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1078 assert!(
1079 !can_trust,
1080 "single-file worktree should be restricted by default"
1081 );
1082
1083 {
1084 let events = events.borrow();
1085 assert_eq!(events.len(), 1);
1086 match &events[0] {
1087 TrustedWorktreesEvent::Restricted(host, paths) => {
1088 assert!(host.is_none());
1089 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
1090 }
1091 _ => panic!("expected Restricted event"),
1092 }
1093 }
1094
1095 events.borrow_mut().clear();
1096
1097 trusted_worktrees.update(cx, |store, cx| {
1098 store.trust(
1099 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1100 None,
1101 cx,
1102 );
1103 });
1104
1105 {
1106 let events = events.borrow();
1107 assert_eq!(events.len(), 1);
1108 match &events[0] {
1109 TrustedWorktreesEvent::Trusted(host, paths) => {
1110 assert!(host.is_none());
1111 assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
1112 }
1113 _ => panic!("expected Trusted event"),
1114 }
1115 }
1116
1117 let can_trust_after =
1118 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1119 assert!(
1120 can_trust_after,
1121 "single-file worktree should be trusted after trust()"
1122 );
1123 }
1124
1125 #[gpui::test]
1126 async fn test_workspace_trust_unlocks_single_file_worktree(cx: &mut TestAppContext) {
1127 init_test(cx);
1128
1129 let fs = FakeFs::new(cx.executor());
1130 fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
1131 .await;
1132
1133 let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
1134 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1135 let worktree_id = worktree_store.read_with(cx, |store, cx| {
1136 let worktree = store.worktrees().next().unwrap();
1137 let worktree = worktree.read(cx);
1138 assert!(worktree.is_single_file(), "expected single-file worktree");
1139 worktree.id()
1140 });
1141
1142 let trusted_worktrees = init_trust_global(worktree_store, cx);
1143
1144 let can_trust_workspace =
1145 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1146 assert!(
1147 !can_trust_workspace,
1148 "workspace should be restricted by default"
1149 );
1150
1151 let can_trust_file =
1152 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1153 assert!(
1154 !can_trust_file,
1155 "single-file worktree should be restricted by default"
1156 );
1157
1158 trusted_worktrees.update(cx, |store, cx| {
1159 store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1160 });
1161
1162 let can_trust_workspace_after =
1163 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1164 assert!(
1165 can_trust_workspace_after,
1166 "workspace should be trusted after trust(Workspace)"
1167 );
1168
1169 let can_trust_file_after =
1170 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1171 assert!(
1172 can_trust_file_after,
1173 "single-file worktree should be trusted after workspace trust"
1174 );
1175 }
1176
1177 #[gpui::test]
1178 async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
1179 init_test(cx);
1180
1181 let fs = FakeFs::new(cx.executor());
1182 fs.insert_tree(
1183 path!("/root"),
1184 json!({
1185 "a.rs": "fn a() {}",
1186 "b.rs": "fn b() {}",
1187 "c.rs": "fn c() {}"
1188 }),
1189 )
1190 .await;
1191
1192 let project = Project::test(
1193 fs,
1194 [
1195 path!("/root/a.rs").as_ref(),
1196 path!("/root/b.rs").as_ref(),
1197 path!("/root/c.rs").as_ref(),
1198 ],
1199 cx,
1200 )
1201 .await;
1202 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1203 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1204 store
1205 .worktrees()
1206 .map(|worktree| {
1207 let worktree = worktree.read(cx);
1208 assert!(worktree.is_single_file());
1209 worktree.id()
1210 })
1211 .collect()
1212 });
1213 assert_eq!(worktree_ids.len(), 3);
1214
1215 let trusted_worktrees = init_trust_global(worktree_store, cx);
1216
1217 for &worktree_id in &worktree_ids {
1218 let can_trust =
1219 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1220 assert!(
1221 !can_trust,
1222 "worktree {worktree_id:?} should be restricted initially"
1223 );
1224 }
1225
1226 trusted_worktrees.update(cx, |store, cx| {
1227 store.trust(
1228 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1229 None,
1230 cx,
1231 );
1232 });
1233
1234 let can_trust_0 =
1235 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1236 let can_trust_1 =
1237 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1238 let can_trust_2 =
1239 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[2], cx));
1240
1241 assert!(!can_trust_0, "worktree 0 should still be restricted");
1242 assert!(can_trust_1, "worktree 1 should be trusted");
1243 assert!(!can_trust_2, "worktree 2 should still be restricted");
1244 }
1245
1246 #[gpui::test]
1247 async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
1248 init_test(cx);
1249
1250 let fs = FakeFs::new(cx.executor());
1251 fs.insert_tree(
1252 path!("/projects"),
1253 json!({
1254 "project_a": { "main.rs": "fn main() {}" },
1255 "project_b": { "lib.rs": "pub fn lib() {}" }
1256 }),
1257 )
1258 .await;
1259
1260 let project = Project::test(
1261 fs,
1262 [
1263 path!("/projects/project_a").as_ref(),
1264 path!("/projects/project_b").as_ref(),
1265 ],
1266 cx,
1267 )
1268 .await;
1269 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1270 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1271 store
1272 .worktrees()
1273 .map(|worktree| {
1274 let worktree = worktree.read(cx);
1275 assert!(!worktree.is_single_file());
1276 worktree.id()
1277 })
1278 .collect()
1279 });
1280 assert_eq!(worktree_ids.len(), 2);
1281
1282 let trusted_worktrees = init_trust_global(worktree_store, cx);
1283
1284 let can_trust_a =
1285 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1286 let can_trust_b =
1287 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1288 assert!(!can_trust_a, "project_a should be restricted initially");
1289 assert!(!can_trust_b, "project_b should be restricted initially");
1290
1291 trusted_worktrees.update(cx, |store, cx| {
1292 store.trust(
1293 HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1294 None,
1295 cx,
1296 );
1297 });
1298
1299 let can_trust_a =
1300 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1301 let can_trust_b =
1302 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1303 assert!(can_trust_a, "project_a should be trusted after trust()");
1304 assert!(!can_trust_b, "project_b should still be restricted");
1305
1306 trusted_worktrees.update(cx, |store, cx| {
1307 store.trust(
1308 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1309 None,
1310 cx,
1311 );
1312 });
1313
1314 let can_trust_a =
1315 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[0], cx));
1316 let can_trust_b =
1317 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_ids[1], cx));
1318 assert!(can_trust_a, "project_a should remain trusted");
1319 assert!(can_trust_b, "project_b should now be trusted");
1320 }
1321
1322 #[gpui::test]
1323 async fn test_directory_worktree_trust_enables_workspace(cx: &mut TestAppContext) {
1324 init_test(cx);
1325
1326 let fs = FakeFs::new(cx.executor());
1327 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1328 .await;
1329
1330 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1331 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1332 let worktree_id = worktree_store.read_with(cx, |store, cx| {
1333 let worktree = store.worktrees().next().unwrap();
1334 assert!(!worktree.read(cx).is_single_file());
1335 worktree.read(cx).id()
1336 });
1337
1338 let trusted_worktrees = init_trust_global(worktree_store, cx);
1339
1340 let can_trust_workspace =
1341 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1342 assert!(
1343 !can_trust_workspace,
1344 "workspace should be restricted initially"
1345 );
1346
1347 trusted_worktrees.update(cx, |store, cx| {
1348 store.trust(
1349 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1350 None,
1351 cx,
1352 );
1353 });
1354
1355 let can_trust_workspace_after =
1356 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1357 assert!(
1358 can_trust_workspace_after,
1359 "workspace should be trusted after trusting directory worktree"
1360 );
1361 }
1362
1363 #[gpui::test]
1364 async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
1365 init_test(cx);
1366
1367 let fs = FakeFs::new(cx.executor());
1368 fs.insert_tree(
1369 path!("/"),
1370 json!({
1371 "project": { "main.rs": "fn main() {}" },
1372 "standalone.rs": "fn standalone() {}"
1373 }),
1374 )
1375 .await;
1376
1377 let project = Project::test(
1378 fs,
1379 [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1380 cx,
1381 )
1382 .await;
1383 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1384 let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1385 let worktrees: Vec<_> = store.worktrees().collect();
1386 assert_eq!(worktrees.len(), 2);
1387 let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1388 (&worktrees[1], &worktrees[0])
1389 } else {
1390 (&worktrees[0], &worktrees[1])
1391 };
1392 assert!(!dir_worktree.read(cx).is_single_file());
1393 assert!(file_worktree.read(cx).is_single_file());
1394 (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1395 });
1396
1397 let trusted_worktrees = init_trust_global(worktree_store, cx);
1398
1399 let can_trust_file =
1400 trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
1401 assert!(
1402 !can_trust_file,
1403 "single-file worktree should be restricted initially"
1404 );
1405
1406 trusted_worktrees.update(cx, |store, cx| {
1407 store.trust(
1408 HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
1409 None,
1410 cx,
1411 );
1412 });
1413
1414 let can_trust_dir =
1415 trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
1416 let can_trust_file_after =
1417 trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
1418 assert!(can_trust_dir, "directory worktree should be trusted");
1419 assert!(
1420 can_trust_file_after,
1421 "single-file worktree should be trusted after directory worktree trust"
1422 );
1423 }
1424
1425 #[gpui::test]
1426 async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
1427 init_test(cx);
1428
1429 let fs = FakeFs::new(cx.executor());
1430 fs.insert_tree(
1431 path!("/workspace"),
1432 json!({
1433 "project_a": { "main.rs": "fn main() {}" },
1434 "project_b": { "lib.rs": "pub fn lib() {}" }
1435 }),
1436 )
1437 .await;
1438
1439 let project = Project::test(
1440 fs,
1441 [
1442 path!("/workspace/project_a").as_ref(),
1443 path!("/workspace/project_b").as_ref(),
1444 ],
1445 cx,
1446 )
1447 .await;
1448 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1449 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1450 store
1451 .worktrees()
1452 .map(|worktree| worktree.read(cx).id())
1453 .collect()
1454 });
1455 assert_eq!(worktree_ids.len(), 2);
1456
1457 let trusted_worktrees = init_trust_global(worktree_store, cx);
1458
1459 for &worktree_id in &worktree_ids {
1460 let can_trust =
1461 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1462 assert!(!can_trust, "worktree should be restricted initially");
1463 }
1464
1465 trusted_worktrees.update(cx, |store, cx| {
1466 store.trust(
1467 HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/workspace")))]),
1468 None,
1469 cx,
1470 );
1471 });
1472
1473 for &worktree_id in &worktree_ids {
1474 let can_trust =
1475 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1476 assert!(
1477 can_trust,
1478 "worktree should be trusted after parent path trust"
1479 );
1480 }
1481 }
1482
1483 #[gpui::test]
1484 async fn test_auto_trust_all(cx: &mut TestAppContext) {
1485 init_test(cx);
1486
1487 let fs = FakeFs::new(cx.executor());
1488 fs.insert_tree(
1489 path!("/"),
1490 json!({
1491 "project_a": { "main.rs": "fn main() {}" },
1492 "project_b": { "lib.rs": "pub fn lib() {}" },
1493 "single.rs": "fn single() {}"
1494 }),
1495 )
1496 .await;
1497
1498 let project = Project::test(
1499 fs,
1500 [
1501 path!("/project_a").as_ref(),
1502 path!("/project_b").as_ref(),
1503 path!("/single.rs").as_ref(),
1504 ],
1505 cx,
1506 )
1507 .await;
1508 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1509 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1510 store
1511 .worktrees()
1512 .map(|worktree| worktree.read(cx).id())
1513 .collect()
1514 });
1515 assert_eq!(worktree_ids.len(), 3);
1516
1517 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1518
1519 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1520 cx.update({
1521 let events = events.clone();
1522 |cx| {
1523 cx.subscribe(&trusted_worktrees, move |_, event, _| {
1524 events.borrow_mut().push(match event {
1525 TrustedWorktreesEvent::Trusted(host, paths) => {
1526 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1527 }
1528 TrustedWorktreesEvent::Restricted(host, paths) => {
1529 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1530 }
1531 });
1532 })
1533 }
1534 })
1535 .detach();
1536
1537 for &worktree_id in &worktree_ids {
1538 let can_trust =
1539 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1540 assert!(!can_trust, "worktree should be restricted initially");
1541 }
1542 let can_trust_workspace =
1543 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1544 assert!(
1545 !can_trust_workspace,
1546 "workspace should be restricted initially"
1547 );
1548
1549 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1550 store.has_restricted_worktrees(&worktree_store, cx)
1551 });
1552 assert!(has_restricted, "should have restricted worktrees");
1553
1554 events.borrow_mut().clear();
1555
1556 trusted_worktrees.update(cx, |store, cx| {
1557 store.auto_trust_all(cx);
1558 });
1559
1560 for &worktree_id in &worktree_ids {
1561 let can_trust =
1562 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1563 assert!(
1564 can_trust,
1565 "worktree {worktree_id:?} should be trusted after auto_trust_all"
1566 );
1567 }
1568
1569 let can_trust_workspace =
1570 trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
1571 assert!(
1572 can_trust_workspace,
1573 "workspace should be trusted after auto_trust_all"
1574 );
1575
1576 let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
1577 store.has_restricted_worktrees(&worktree_store, cx)
1578 });
1579 assert!(
1580 !has_restricted_after,
1581 "should have no restricted worktrees after auto_trust_all"
1582 );
1583
1584 let trusted_event_count = events
1585 .borrow()
1586 .iter()
1587 .filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
1588 .count();
1589 assert!(
1590 trusted_event_count > 0,
1591 "should have emitted Trusted events"
1592 );
1593 }
1594
1595 #[gpui::test]
1596 async fn test_wait_for_global_trust_already_trusted(cx: &mut TestAppContext) {
1597 init_test(cx);
1598
1599 let fs = FakeFs::new(cx.executor());
1600 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1601 .await;
1602
1603 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1604 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1605
1606 let trusted_worktrees = init_trust_global(worktree_store, cx);
1607
1608 trusted_worktrees.update(cx, |store, cx| {
1609 store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1610 });
1611
1612 let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
1613 assert!(task.is_none(), "should return None when already trusted");
1614 }
1615
1616 #[gpui::test]
1617 async fn test_wait_for_workspace_trust_resolves_on_trust(cx: &mut TestAppContext) {
1618 init_test(cx);
1619
1620 let fs = FakeFs::new(cx.executor());
1621 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1622 .await;
1623
1624 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1625 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1626
1627 let trusted_worktrees = init_trust_global(worktree_store, cx);
1628
1629 let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
1630 assert!(
1631 task.is_some(),
1632 "should return Some(Task) when not yet trusted"
1633 );
1634
1635 let task = task.unwrap();
1636
1637 cx.executor().run_until_parked();
1638
1639 trusted_worktrees.update(cx, |store, cx| {
1640 store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
1641 });
1642
1643 cx.executor().run_until_parked();
1644 task.await;
1645 }
1646
1647 #[gpui::test]
1648 async fn test_wait_for_default_workspace_trust_resolves_on_directory_worktree_trust(
1649 cx: &mut TestAppContext,
1650 ) {
1651 init_test(cx);
1652
1653 let fs = FakeFs::new(cx.executor());
1654 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1655 .await;
1656
1657 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1658 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1659 let worktree_id = worktree_store.read_with(cx, |store, cx| {
1660 let worktree = store.worktrees().next().unwrap();
1661 assert!(!worktree.read(cx).is_single_file());
1662 worktree.read(cx).id()
1663 });
1664
1665 let trusted_worktrees = init_trust_global(worktree_store, cx);
1666
1667 let task = cx.update(|cx| wait_for_default_workspace_trust("test", cx));
1668 assert!(
1669 task.is_some(),
1670 "should return Some(Task) when not yet trusted"
1671 );
1672
1673 let task = task.unwrap();
1674
1675 cx.executor().run_until_parked();
1676
1677 trusted_worktrees.update(cx, |store, cx| {
1678 store.trust(
1679 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1680 None,
1681 cx,
1682 );
1683 });
1684
1685 cx.executor().run_until_parked();
1686 task.await;
1687 }
1688
1689 #[gpui::test]
1690 async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
1691 init_test(cx);
1692
1693 let fs = FakeFs::new(cx.executor());
1694 fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1695 .await;
1696
1697 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1698 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1699 let worktree_id = worktree_store.read_with(cx, |store, cx| {
1700 store.worktrees().next().unwrap().read(cx).id()
1701 });
1702
1703 let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1704
1705 let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1706 cx.update({
1707 let events = events.clone();
1708 |cx| {
1709 cx.subscribe(&trusted_worktrees, move |_, event, _| {
1710 events.borrow_mut().push(match event {
1711 TrustedWorktreesEvent::Trusted(host, paths) => {
1712 TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1713 }
1714 TrustedWorktreesEvent::Restricted(host, paths) => {
1715 TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1716 }
1717 });
1718 })
1719 }
1720 })
1721 .detach();
1722
1723 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1724 assert!(!can_trust, "should be restricted initially");
1725 assert_eq!(events.borrow().len(), 1);
1726 events.borrow_mut().clear();
1727
1728 trusted_worktrees.update(cx, |store, cx| {
1729 store.trust(
1730 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1731 None,
1732 cx,
1733 );
1734 });
1735 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1736 assert!(can_trust, "should be trusted after trust()");
1737 assert_eq!(events.borrow().len(), 1);
1738 assert!(matches!(
1739 &events.borrow()[0],
1740 TrustedWorktreesEvent::Trusted(..)
1741 ));
1742 events.borrow_mut().clear();
1743
1744 trusted_worktrees.update(cx, |store, cx| {
1745 store.restrict(
1746 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1747 None,
1748 cx,
1749 );
1750 });
1751 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1752 assert!(!can_trust, "should be restricted after restrict()");
1753 assert_eq!(events.borrow().len(), 1);
1754 assert!(matches!(
1755 &events.borrow()[0],
1756 TrustedWorktreesEvent::Restricted(..)
1757 ));
1758
1759 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1760 store.has_restricted_worktrees(&worktree_store, cx)
1761 });
1762 assert!(has_restricted);
1763 events.borrow_mut().clear();
1764
1765 trusted_worktrees.update(cx, |store, cx| {
1766 store.trust(
1767 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1768 None,
1769 cx,
1770 );
1771 });
1772 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
1773 assert!(can_trust, "should be trusted again after second trust()");
1774 assert_eq!(events.borrow().len(), 1);
1775 assert!(matches!(
1776 &events.borrow()[0],
1777 TrustedWorktreesEvent::Trusted(..)
1778 ));
1779
1780 let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1781 store.has_restricted_worktrees(&worktree_store, cx)
1782 });
1783 assert!(!has_restricted);
1784 }
1785
1786 #[gpui::test]
1787 async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
1788 init_test(cx);
1789
1790 let fs = FakeFs::new(cx.executor());
1791 fs.insert_tree(
1792 path!("/"),
1793 json!({
1794 "local_project": { "main.rs": "fn main() {}" },
1795 "remote_project": { "lib.rs": "pub fn lib() {}" }
1796 }),
1797 )
1798 .await;
1799
1800 let project = Project::test(
1801 fs,
1802 [
1803 path!("/local_project").as_ref(),
1804 path!("/remote_project").as_ref(),
1805 ],
1806 cx,
1807 )
1808 .await;
1809 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1810 let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1811 store
1812 .worktrees()
1813 .map(|worktree| worktree.read(cx).id())
1814 .collect()
1815 });
1816 assert_eq!(worktree_ids.len(), 2);
1817 let local_worktree = worktree_ids[0];
1818 let _remote_worktree = worktree_ids[1];
1819
1820 let trusted_worktrees = init_trust_global(worktree_store, cx);
1821
1822 let host_a: Option<RemoteHostLocation> = None;
1823 let host_b = Some(RemoteHostLocation {
1824 user_name: Some("user".into()),
1825 host_identifier: "remote-host".into(),
1826 });
1827
1828 let can_trust_local =
1829 trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
1830 assert!(!can_trust_local, "local worktree restricted on host_a");
1831
1832 trusted_worktrees.update(cx, |store, cx| {
1833 store.trust(
1834 HashSet::from_iter([PathTrust::Workspace]),
1835 host_b.clone(),
1836 cx,
1837 );
1838 });
1839
1840 let can_trust_workspace_a = trusted_worktrees.update(cx, |store, cx| {
1841 store.can_trust_workspace(host_a.clone(), cx)
1842 });
1843 assert!(
1844 !can_trust_workspace_a,
1845 "host_a workspace should still be restricted"
1846 );
1847
1848 let can_trust_workspace_b = trusted_worktrees.update(cx, |store, cx| {
1849 store.can_trust_workspace(host_b.clone(), cx)
1850 });
1851 assert!(can_trust_workspace_b, "host_b workspace should be trusted");
1852
1853 trusted_worktrees.update(cx, |store, cx| {
1854 store.trust(
1855 HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
1856 host_a.clone(),
1857 cx,
1858 );
1859 });
1860
1861 let can_trust_local_after =
1862 trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
1863 assert!(
1864 can_trust_local_after,
1865 "local worktree should be trusted on host_a"
1866 );
1867
1868 let can_trust_workspace_a_after = trusted_worktrees.update(cx, |store, cx| {
1869 store.can_trust_workspace(host_a.clone(), cx)
1870 });
1871 assert!(
1872 can_trust_workspace_a_after,
1873 "host_a workspace should be trusted after directory trust"
1874 );
1875 }
1876}