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