1use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use serde::Deserialize as _;
9use yapi_types::rpc::SourceInfo;
10use yapi_types::settings::{DefaultProjectTrust, FilteredPackage};
11
12use super::fetch::placed_name;
13use super::lock::fresh;
14use super::manifest::{Entry, MANIFEST_ENV, Manifest, Section, from_env};
15use super::{Mode, Project};
16use crate::extensions::{EventPolicy, Grants};
17use crate::packages::npm::{Npm, escaped};
18use crate::packages::resolve::{
19 PackageInput, ProjectOnly, ResolvedPaths, ResourceType, package_resources,
20};
21use crate::settings::{Scope, SettingsManager};
22use crate::tools::path::home_dir;
23use crate::trust::{TrustStore, resolve_trusted};
24
25pub fn project_manifest(project_root: &Path) -> Option<PathBuf> {
30 let name = from_env().ok()?;
31 Some(project_root.join(name.as_deref().unwrap_or("chest.toml")))
32}
33
34pub fn entry_label(source: &str, scope: &str, project_root: &Path) -> Option<String> {
39 let name = source.strip_prefix("chest:")?;
40 let manifest = match scope {
41 "user" => "~/.chest/chest.toml".to_owned(),
42 _ => {
43 let manifest = project_manifest(project_root)?;
44 manifest.file_name()?.to_string_lossy().into_owned()
45 }
46 };
47 Some(format!("extensions.{name} in {manifest}"))
48}
49
50fn consumes(manifest: &Manifest) -> bool {
53 [Section::Skills, Section::Agents, Section::Extensions]
54 .into_iter()
55 .any(|section| !manifest.section(section).is_empty())
56 || !manifest.patch.is_empty()
57}
58
59pub fn needs_trust(project_root: &Path) -> bool {
64 let Some(Ok(text)) = project_manifest(project_root).map(std::fs::read_to_string) else {
65 return false;
66 };
67 Manifest::parse(&text, Scope::Project).map_or(true, |manifest| consumes(&manifest))
68}
69
70#[derive(Clone, Debug, PartialEq)]
74pub struct Policy {
75 pub grants: Grants,
77 pub events: EventPolicy,
80 pub root: Option<PathBuf>,
83}
84
85impl Policy {
86 pub fn unrestricted() -> Policy {
89 Policy {
90 grants: Grants::default(),
91 events: EventPolicy::shared(),
92 root: None,
93 }
94 }
95
96 pub fn closed(root: &Path) -> Policy {
100 Policy {
101 grants: Grants::none(),
102 events: EventPolicy::isolated(),
103 root: Some(root.to_path_buf()),
104 }
105 }
106
107 fn intersection(&self, other: &Policy) -> Policy {
110 let canonical = |root: &PathBuf| root.canonicalize().unwrap_or_else(|_| root.clone());
111 let roots = (
112 self.root.as_ref().map(canonical),
113 other.root.as_ref().map(canonical),
114 );
115 let root = match roots {
116 (Some(left), Some(right)) if left.starts_with(&right) => Some(left),
117 (Some(left), Some(right)) if right.starts_with(&left) => Some(right),
118 _ => None,
119 };
120 Policy {
121 grants: self.grants.intersection(other.grants),
122 events: self.events.intersection(&other.events),
123 root,
124 }
125 }
126}
127
128pub type Policies = HashMap<(String, String), Policy>;
133
134#[derive(Clone, Debug, Default)]
137pub struct FilePolicies {
138 pub files: HashMap<PathBuf, Policy>,
141 pub closed: Vec<PathBuf>,
144}
145
146impl FilePolicies {
147 pub fn of(&self, file: &Path) -> Policy {
152 if self.closes(file) {
154 return Policy::closed(file);
155 }
156 self.files
157 .get(file)
158 .cloned()
159 .unwrap_or_else(Policy::unrestricted)
160 }
161
162 pub fn manages(&self, file: &Path) -> bool {
164 self.files.contains_key(file) || self.closes(file)
165 }
166
167 pub fn closes(&self, file: &Path) -> bool {
169 self.closed.iter().any(|root| file.starts_with(root))
170 }
171
172 pub fn also_loaded(&self, file: &Path, path: &str, source: &str) -> Option<(String, String)> {
177 if !self.manages(file) {
178 return None;
179 }
180 let message = match self.closes(file) {
181 true => format!(
182 "{path} is also loaded from {source}, but its chest entry does not load, so it runs with no capabilities"
183 ),
184 false => format!("{path} is also loaded from {source}; chest's grants apply"),
185 };
186 Some((escaped(path), escaped(&message)))
187 }
188}
189
190pub fn file_policies(
200 resolved: &ResolvedPaths,
201 installed: &Installed,
202 project_root: &Path,
203 problems: &mut Vec<(String, String)>,
204) -> FilePolicies {
205 let files = resolved.of(ResourceType::Extensions).iter();
206 let files = files.map(|file| &file.info).chain(&resolved.shadowed);
207 let mut found = FilePolicies {
208 files: HashMap::new(),
209 closed: installed
210 .closed
211 .iter()
212 .map(|closed| closed.root.clone())
213 .collect(),
214 };
215 let mut givers: Vec<(PathBuf, &SourceInfo, Vec<String>, [bool; 2])> = Vec::new();
219 for info in files.filter(|info| info.source.starts_with("chest:")) {
220 let key = (info.scope.clone(), info.source.clone());
221 let file = Path::new(&info.path);
222 let file = file.canonicalize().unwrap_or_else(|_| file.into());
223 let policy = installed.policies.get(&key).cloned().unwrap_or_else(|| {
224 let message = format!(
225 "{}: no chest entry grants this file, so it runs with no capabilities",
226 info.path
227 );
228 problems.push((escaped(&info.source), escaped(&message)));
229 Policy::closed(&file)
230 });
231 let label = entry_label(&info.source, &info.scope, project_root);
232 let label = label.unwrap_or_else(|| info.source.clone());
233 let policy = match found.files.get(&file) {
234 Some(other) => {
235 if let Some((.., labels, [grants, events])) =
236 givers.iter_mut().find(|giver| giver.0 == file)
237 {
238 if !labels.contains(&label) {
239 labels.push(label);
240 }
241 *grants |= other.grants != policy.grants;
242 *events |= other.events != policy.events;
243 }
244 other.intersection(&policy)
245 }
246 None => {
247 givers.push((file.clone(), info, vec![label], [false; 2]));
248 policy
249 }
250 };
251 found.files.insert(file, policy);
252 }
253 let only = |narrowed: [bool; 2], policy: &Policy, count: usize| {
256 let all = if count == 2 { "both" } else { "all" };
257 match narrowed {
258 [false, false] => None,
259 [true, false] => Some(format!("grants {all} give: {}", policy.grants)),
260 [false, true] => Some(format!("event channels {all} give")),
261 [true, true] => Some(format!(
262 "event channels and the grants {all} give: {}",
263 policy.grants
264 )),
265 }
266 };
267 for (file, info, labels, narrowed) in givers {
268 let (path, source) = (&info.path, &info.source);
269 let Some(policy) = found.files.get(&file) else {
270 continue;
271 };
272 let closer = installed
273 .closed
274 .iter()
275 .find(|closed| file.starts_with(&closed.root));
276 if let Some(Closed {
277 entry: closer,
278 remove,
279 install,
280 ..
281 }) = closer
282 {
283 let fix = match install {
285 Some((command, once)) => {
286 let [grants, events] = narrowed;
287 let narrowed = [
288 grants || policy.grants != once.grants,
289 events || policy.events != once.events,
290 ];
291 let after = policy.intersection(once);
292 let gives = only(narrowed, &after, labels.len() + 1);
293 let gives = gives.map(|gives| format!(", which gives it only the {gives}"));
294 format!(
295 "run `{command}`{}, or `{remove}`",
296 gives.unwrap_or_default()
297 )
298 }
299 None => format!("run `{remove}`"),
300 };
301 let message = format!(
302 "{path} is in {}, but {closer} holds it too and does not load, so it runs with no capabilities: {fix}",
303 labels.join(" and ")
304 );
305 problems.push((escaped(source), escaped(&message)));
306 continue;
307 }
308 let Some(narrowed) = only(narrowed, policy, labels.len()) else {
309 continue;
310 };
311 let message = format!(
312 "{path} is in {}, so it gets only the {narrowed}",
313 labels.join(" and ")
314 );
315 problems.push((escaped(source), escaped(&message)));
316 }
317 found
318}
319
320#[derive(Clone, Debug, Default)]
322pub struct Installed {
323 pub packages: Vec<PackageInput>,
325 pub skills: Vec<PackageInput>,
328 pub policies: Policies,
330 pub closed: Vec<Closed>,
334 pub identities: Vec<(String, String, String, bool)>,
338}
339
340#[derive(Clone, Debug)]
343pub struct Closed {
344 pub root: PathBuf,
346 pub entry: String,
348 pub remove: String,
350 pub install: Option<(String, Policy)>,
353}
354
355impl Installed {
356 fn extend(&mut self, other: Installed) {
357 self.packages.extend(other.packages);
358 self.skills.extend(other.skills);
359 self.policies.extend(other.policies);
360 self.closed.extend(other.closed);
361 self.identities.extend(other.identities);
362 }
363}
364
365pub fn reads_global(settings: &SettingsManager) -> bool {
369 !crate::config::project_only()
370 || crate::packages::resolve::ProjectOnly::from_settings(settings.document(Scope::Global))
371 .needs_chest()
372}
373
374#[derive(Clone, Debug, PartialEq)]
376enum Install {
377 Chest {
380 name: String,
381 path: String,
382 global: bool,
383 },
384 Settings { scope: Scope, source: String },
386}
387
388#[derive(Clone, Debug, Default, PartialEq)]
394pub struct LeftOut(Vec<(Install, Install)>);
395
396impl LeftOut {
397 pub fn new(
403 installed: &Installed,
404 settings: &SettingsManager,
405 keep: Option<&ProjectOnly>,
406 agent_dir: &Path,
407 ) -> LeftOut {
408 let rank = |source: &str, global: bool| match keep {
409 Some(keep) if global && !keep.keeps_package(source, agent_dir) => {
410 keep.needs_package(source, agent_dir).then_some(1)
411 }
412 _ => Some(0),
413 };
414 let chest = |global: bool| {
415 let identities = installed.identities.iter();
416 identities
417 .filter(move |(_, _, _, other)| *other == global)
418 .filter_map(move |(id, name, path, global)| {
419 let rank = rank(&format!("chest:{name}"), *global)?;
420 let (name, path, global) = (name.clone(), path.clone(), *global);
421 Some((rank, id.clone(), Install::Chest { name, path, global }))
422 })
423 };
424 let from_settings = |scope: Scope| {
425 let packages = crate::packages::settings_packages(settings, scope);
426 let sources = packages
427 .iter()
428 .filter_map(crate::packages::entry_source)
429 .filter_map(|source| {
430 let rank = rank(source, scope == Scope::Global)?;
431 let id = crate::packages::source::parse(source).identity(Path::new(""));
432 let source = source.to_owned();
433 Some((rank, id, Install::Settings { scope, source }))
434 });
435 sources.collect::<Vec<_>>()
436 };
437 let mut copies: Vec<(u8, String, Install)> = chest(false)
438 .chain(from_settings(Scope::Project))
439 .chain(chest(true))
440 .chain(from_settings(Scope::Global))
441 .collect();
442 let pair = |a: &Install, b: &Install| {
444 !matches!((a, b), (Install::Settings { .. }, Install::Settings { .. }))
445 };
446 if let Some(keep) = keep {
447 let home = home_dir();
449 let input = |copy: &Install| match copy {
450 Install::Chest { name, .. } => {
451 let source = format!("chest:{name}");
452 let mut inputs = installed.packages.iter();
453 let found =
454 inputs.find(|input| input.scope == "user" && input.source == source);
455 found.cloned()
456 }
457 Install::Settings { source, .. } => {
458 let global = crate::packages::settings_packages(settings, Scope::Global);
459 let inputs = crate::packages::resolve::package_inputs(
460 &[],
461 &global,
462 |_| agent_dir.to_path_buf(),
463 |source, _| crate::packages::install_location(source, agent_dir),
464 );
465 inputs.into_iter().find(|input| input.source == *source)
466 }
467 };
468 let counts: Vec<bool> = copies
469 .iter()
470 .enumerate()
471 .map(|(index, (rank, id, copy))| {
472 let mut others = copies.iter().enumerate();
473 *rank == 0
474 || !others.any(|(other_index, (_, other, install))| {
475 other_index != index && same_package(id, other) && pair(copy, install)
476 })
477 || input(copy)
478 .is_some_and(|input| keep.keeps_part(&input, agent_dir, &home))
479 })
480 .collect();
481 let copies_counts = copies.into_iter().zip(counts);
482 copies = copies_counts
483 .filter_map(|(copy, counts)| counts.then_some(copy))
484 .collect();
485 }
486 copies.sort_by_key(|(rank, ..)| *rank);
488 let mut left_out = Vec::new();
489 let mut loaded: Vec<(&str, &Install)> = Vec::new();
492 for (_, id, copy) in &copies {
493 let kept = loaded.iter().find(|(other, _)| same_package(other, id));
494 match kept {
495 Some((_, kept)) if pair(kept, copy) => {
496 left_out.push((copy.clone(), (*kept).clone()));
497 }
498 _ => loaded.push((id, copy)),
499 }
500 }
501 LeftOut(left_out)
502 }
503
504 pub fn chest(&self, input: &PackageInput) -> bool {
506 self.0.iter().any(|(copy, _)| match copy {
507 Install::Chest { name, global, .. } => {
508 input.source == format!("chest:{name}") && (input.scope == "user") == *global
509 }
510 Install::Settings { .. } => false,
511 })
512 }
513
514 pub fn settings(&self, scope: Scope, source: &str) -> bool {
517 let copy = Install::Settings {
518 scope,
519 source: source.to_owned(),
520 };
521 self.0.iter().any(|(left_out, _)| *left_out == copy)
522 }
523
524 pub fn problems(&self, approve: bool) -> Vec<(String, String)> {
529 let scope = |global: bool| {
530 if global {
531 Scope::Global
532 } else {
533 Scope::Project
534 }
535 };
536 let home = home_dir();
537 let home = home.canonicalize().unwrap_or(home);
538 let place = |copy: &Install| match copy {
539 Install::Chest { name, path, global } => {
540 let label = super::manifest_label(Path::new(path), scope(*global), &home);
541 format!("extensions.{name} in {label}")
542 }
543 Install::Settings { scope, source } => match scope {
544 Scope::Project => format!("{source} in the project's settings"),
545 Scope::Global => format!("{source} in the global settings"),
546 },
547 };
548 let local = match approve {
549 true => "yapi remove -l --approve",
550 false => "yapi remove -l",
551 };
552 let problems = self.0.iter().map(|(copy, kept)| {
553 let remove = match copy {
554 Install::Chest { name, path, global } => {
555 let command = super::command(Path::new(path), scope(*global));
556 format!("{command} remove {name}")
557 }
558 Install::Settings { scope, source } => match scope {
559 Scope::Project => format!("{local} {source}"),
560 Scope::Global => format!("yapi remove {source}"),
561 },
562 };
563 let message = format!(
564 "{} is left out, because {} installs the same package: remove it with `{remove}`",
565 place(copy),
566 place(kept),
567 );
568 let path = match (copy, kept) {
570 (Install::Chest { path, .. }, _) | (_, Install::Chest { path, .. }) => path.clone(),
571 _ => String::new(),
572 };
573 (escaped(&path), escaped(&message))
574 });
575 problems.collect()
576 }
577}
578
579pub fn duplicates(
585 installed: &Installed,
586 settings: &SettingsManager,
587 keep: Option<&ProjectOnly>,
588 agent_dir: &Path,
589) -> Vec<(String, String)> {
590 let default = settings
592 .document(Scope::Global)
593 .get("defaultProjectTrust")
594 .and_then(|value| DefaultProjectTrust::deserialize(value).ok());
595 let store = TrustStore::new(agent_dir);
596 let trusted = resolve_trusted(settings.project_dir(), &store, None, default);
597 LeftOut::new(installed, settings, keep, agent_dir).problems(!trusted.unwrap_or(false))
598}
599
600#[derive(Clone, Debug, Default)]
604pub struct Loaded {
605 pub installed: Installed,
607 pub problems: Vec<(String, String)>,
609}
610
611impl Loaded {
612 fn warn(&mut self, (path, message): (String, String)) {
616 self.problems.push((escaped(&path), escaped(&message)));
617 }
618}
619
620pub fn package_identity(entry: &Entry) -> Option<String> {
625 let package = match (&entry.npm, &entry.git) {
626 (Some(npm), _) => format!("npm:{npm}"),
627 (None, Some(git)) => {
628 crate::packages::source::parse(&format!("git:{git}")).identity(Path::new(""))
629 }
630 (None, None) => return None,
631 };
632 let path = entry.path.iter().flat_map(|path| path.split('/'));
633 let path: Vec<&str> = path
634 .filter(|segment| !matches!(*segment, "" | "."))
635 .collect();
636 Some(match path.is_empty() {
637 true => package,
638 false => format!("{package}#{}", path.join("/")),
639 })
640}
641
642pub fn same_package(a: &str, b: &str) -> bool {
646 let folder_of = |whole: &str, folder: &str| {
647 folder
648 .strip_prefix(whole)
649 .is_some_and(|rest| rest.starts_with('#'))
650 };
651 a == b || folder_of(a, b) || folder_of(b, a)
652}
653
654pub fn installed_inputs(
659 project: &Project,
660 project_root: &Path,
661 problems: &mut Vec<String>,
662) -> Installed {
663 let scope = match project.scope {
664 Scope::Project => "project",
665 Scope::Global => "user",
666 };
667 let mut installed = Installed::default();
668 let lock = project.lock.clone().unwrap_or_default();
669 let record = project.record();
670 let current = |section: Section, name: &str, entry: &Entry| {
672 lock.section(section)
673 .iter()
674 .find(|locked| locked.name == name)
675 .filter(|locked| fresh(entry, locked))
676 .filter(|locked| project.present(section, locked))
677 .filter(|locked| project.holds_locked(section, entry, locked, &record))
678 .cloned()
679 };
680 for (name, entry) in &project.manifest.extensions {
681 let patched = project.manifest.patch.contains_key(name);
682 let roots = project.extension_roots(name);
683 let label = format!("extensions.{name} in {}", project.label());
684 if let Some((root, module_root)) = &roots
687 && project.too_wide(module_root, &[project_root])
688 {
689 problems.push(format!(
690 "extensions.{name}: {} holds more than the extension, such as the project, the home or the agent folder, so it does not load",
691 module_root.display()
692 ));
693 let local = crate::packages::source::is_local(&format!("chest:{name}"));
694 let files = package_resources(root, None, local).extensions;
695 let files = files.iter().map(|file| Path::new(&file.info.path));
696 let remove = format!("{} remove {name}", project.command());
697 let closed = files.filter_map(|file| file.canonicalize().ok());
698 let closed = closed.map(|root| Closed {
699 root,
700 entry: label.clone(),
701 remove: remove.clone(),
702 install: None,
703 });
704 installed.closed.extend(closed);
705 continue;
706 }
707 let policy = |root: PathBuf| Policy {
708 grants: project.manifest.grants(entry),
709 events: entry.events.clone().unwrap_or_default(),
710 root: Some(root),
711 };
712 if !patched && current(Section::Extensions, name, entry).is_none() {
713 if let Some(root) = roots.and_then(|(_, root)| root.canonicalize().ok()) {
715 let install = Some((project.install_command(), policy(root.clone())));
716 installed.closed.push(Closed {
717 root,
718 entry: label,
719 remove: format!("{} remove {name}", project.command()),
720 install,
721 });
722 }
723 continue;
724 }
725 let Some((root, module_root)) = roots else {
726 continue;
727 };
728 if !root.exists() {
729 if patched || entry.git.is_none() && entry.npm.is_none() {
732 problems.push(format!(
733 "extensions.{name}: {} does not exist",
734 root.display()
735 ));
736 }
737 continue;
738 }
739 let source = format!("chest:{name}");
740 let filter = [&entry.skills, &entry.prompts, &entry.themes]
741 .iter()
742 .any(|list| list.is_some())
743 .then(|| FilteredPackage {
744 source: source.clone(),
745 autoload: None,
746 extensions: None,
747 skills: entry.skills.clone(),
748 prompts: entry.prompts.clone(),
749 themes: entry.themes.clone(),
750 });
751 let key = (scope.to_owned(), source.clone());
752 installed.policies.insert(key, policy(module_root));
753 if let Some(identity) = package_identity(entry) {
756 let path = project.path.display().to_string();
757 let global = project.scope == Scope::Global;
758 installed
759 .identities
760 .push((identity, name.clone(), path, global));
761 }
762 installed.packages.push(PackageInput {
763 source,
764 scope: scope.to_owned(),
765 root,
766 filter,
767 });
768 }
769 let Some(home) = project.homes(Section::Skills).into_iter().next() else {
771 return installed;
772 };
773 for (name, entry) in &project.manifest.skills {
774 let patch = project.manifest.patch.get(name);
775 let roots: Vec<PathBuf> = match (
776 patch.and_then(|patch| patch.path.as_deref()),
777 current(Section::Skills, name, entry),
778 ) {
779 (Some(path), _) => {
783 let patch = project.resolve(path);
784 if !patch.exists() {
785 problems.push(format!("skills.{name}: {} does not exist", patch.display()));
786 continue;
787 }
788 let package = entry.path.is_none() && (entry.git.is_some() || entry.npm.is_some());
789 let items = if package {
790 match super::fetch::items(&patch, Section::Skills, None, name) {
791 Ok((items, _)) => items,
792 Err(err) => {
793 problems.push(format!("skills.{name}: {err}"));
794 continue;
795 }
796 }
797 } else {
798 vec![(placed_name(Section::Skills, name), patch)]
799 };
800 items
801 .into_iter()
802 .map(|(item, from)| {
803 let placed = home.join(item);
804 if placed.exists() { placed } else { from }
805 })
806 .collect()
807 }
808 (None, Some(locked)) => project
809 .locked_items(Section::Skills, &locked)
810 .into_iter()
811 .map(|(item, _)| home.join(item))
812 .collect(),
813 (None, None) => continue,
814 };
815 for root in roots {
816 installed.skills.push(PackageInput {
817 source: format!("chest:{name}"),
818 scope: scope.to_owned(),
819 root,
820 filter: None,
821 });
822 }
823 }
824 installed
825}
826
827fn manifests(
831 project_root: &Path,
832 trusted: bool,
833 global: bool,
834) -> Vec<Result<Project, (String, String)>> {
835 let mut found = Vec::new();
836 if trusted && let Err(problem) = from_env() {
837 let problem = format!("{problem}, so no project manifest loads");
838 found.push(Err((MANIFEST_ENV.to_owned(), problem)));
839 }
840 let project = trusted.then(|| project_manifest(project_root)).flatten();
841 let project = project.map(|path| (path, Scope::Project));
842 let global = global.then(|| (home_dir().join(".chest/chest.toml"), Scope::Global));
843 for (path, scope) in [project, global].into_iter().flatten() {
844 if !path.exists() {
845 continue;
846 }
847 match Project::open(&path, scope) {
848 Ok(project) if consumes(&project.manifest) => found.push(Ok(project)),
849 Ok(_) => {}
850 Err(err) => found.push(Err((path.display().to_string(), err.to_string()))),
851 }
852 }
853 found
854}
855
856fn add(loaded: &mut Loaded, project: &Project, project_root: &Path) {
859 let mut problems = Vec::new();
860 loaded
861 .installed
862 .extend(installed_inputs(project, project_root, &mut problems));
863 let path = project.path.display().to_string();
864 for problem in problems {
866 loaded.warn((path.clone(), format!("{}: {problem}", project.label())));
867 }
868}
869
870pub fn installed(project_root: &Path, trusted: bool, global: bool) -> Loaded {
873 let mut loaded = Loaded::default();
874 for project in manifests(project_root, trusted, global) {
875 match project {
876 Ok(project) => add(&mut loaded, &project, project_root),
877 Err(problem) => loaded.warn(problem),
878 }
879 }
880 loaded
881}
882
883pub async fn load(project_root: &Path, trusted: bool, offline: bool, global: bool) -> Loaded {
889 let mut loaded = Loaded::default();
890 let mut npm = None;
891 for project in manifests(project_root, trusted, global) {
892 let mut project = match project {
893 Ok(project) => project,
894 Err(problem) => {
895 loaded.warn(problem);
896 continue;
897 }
898 };
899 let path = project.path.display().to_string();
900 let npm = npm.get_or_insert_with(|| Npm::new(crate::packages::npm::config()));
901 match project
902 .sync(Mode::Startup { offline }, npm, &mut |_| {})
903 .await
904 {
905 Ok(report) => {
906 for message in report.warnings.into_iter().chain(report.notices) {
907 loaded.warn((path.clone(), message));
908 }
909 }
910 Err(err) => loaded.warn((path.clone(), err.to_string())),
911 }
912 add(&mut loaded, &project, project_root);
913 }
914 loaded
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920 use crate::packages::resolve::Resolved;
921 use yapi_types::rpc::SourceInfo;
922
923 #[test]
925 fn startup_load_send() {
926 fn send<T: Send>(_: T) {}
927 send(load(Path::new("."), false, true, true));
928 }
929
930 #[test]
937 fn chest_files_fail_closed() {
938 let file = |path: &str, source: &str, scope: &str| SourceInfo {
939 path: path.into(),
940 source: source.into(),
941 scope: scope.into(),
942 origin: "package".into(),
943 base_dir: None,
944 };
945 let enabled = |info| Resolved {
946 info,
947 enabled: true,
948 };
949 let spoof = "/p/\x1b[2J.js";
951 let resolved = ResolvedPaths {
952 extensions: vec![
953 enabled(file("/p/a.js", "chest:a", "project")),
954 enabled(file("/p/b.js", "npm:b", "project")),
955 enabled(file(spoof, "chest:a", "project")),
956 ],
957 ..ResolvedPaths::default()
958 };
959 let mut problems = Vec::new();
960 let policies = file_policies(
961 &resolved,
962 &Installed::default(),
963 Path::new("/p"),
964 &mut problems,
965 );
966 let (file_a, file_spoof) = (PathBuf::from("/p/a.js"), PathBuf::from(spoof));
967 let files = HashMap::from([
968 (file_a.clone(), Policy::closed(&file_a)),
969 (file_spoof.clone(), Policy::closed(&file_spoof)),
970 ]);
971 assert_eq!(policies.files, files);
972 assert_eq!(problems.len(), 2, "{problems:?}");
973 let warned = policies.also_loaded(&file_spoof, spoof, "cli");
974 let warned = warned
975 .into_iter()
976 .flat_map(|(path, message)| [path, message]);
977 let texts: Vec<String> = problems
978 .iter()
979 .flat_map(|(source, message)| [source.clone(), message.clone()])
980 .chain(warned)
981 .collect();
982 assert!(
983 texts.len() == 6 && texts.iter().all(|text| !text.contains('\x1b')),
984 "{texts:?}"
985 );
986 let rooted = |root: &str| Policy {
987 root: Some(root.into()),
988 ..Policy::unrestricted()
989 };
990 let key = |scope: &str, name: &str| (scope.to_owned(), format!("chest:{name}"));
991 let filesystem = Grants {
992 filesystem: true,
993 ..Grants::none()
994 };
995 let network = Grants {
996 network: true,
997 ..Grants::none()
998 };
999 let narrow = |grants, events| Policy {
1000 grants,
1001 events,
1002 ..rooted("/p/sub")
1003 };
1004 let installed = Installed {
1005 policies: HashMap::from([
1006 (key("project", "s"), rooted("/p")),
1007 (key("user", "s"), narrow(filesystem, EventPolicy::shared())),
1008 (
1010 key("project", "e"),
1011 narrow(filesystem, EventPolicy::shared()),
1012 ),
1013 (
1014 key("user", "e"),
1015 narrow(filesystem, EventPolicy::isolated()),
1016 ),
1017 (key("project", "g"), rooted("/p")),
1018 (
1019 key("user", "g"),
1020 narrow(filesystem, EventPolicy::isolated()),
1021 ),
1022 ]),
1023 closed: [("/q", None), ("/p/sub/c.js", Some(network))]
1025 .map(|(root, grants)| Closed {
1026 root: root.into(),
1027 entry: "extensions.x in chest.toml".into(),
1028 remove: "chest remove x".into(),
1029 install: grants.map(|grants| {
1030 let policy = narrow(grants, EventPolicy::shared());
1031 ("chest install".to_owned(), policy)
1032 }),
1033 })
1034 .into(),
1035 ..Installed::default()
1036 };
1037 let resolved = ResolvedPaths {
1038 extensions: vec![
1039 enabled(file("/p/sub/a.js", "chest:s", "project")),
1040 enabled(file("/p/sub/b.js", "chest:s", "user")),
1041 enabled(file("/p/sub/c.js", "chest:s", "project")),
1042 enabled(file("/p/sub/e.js", "chest:e", "project")),
1043 enabled(file("/p/sub/g.js", "chest:g", "project")),
1044 ],
1045 shadowed: vec![
1046 file("/p/sub/a.js", "chest:s", "user"),
1047 file("/p/sub/b.js", "chest:s", "project"),
1048 file("/p/sub/c.js", "chest:s", "user"),
1049 file("/p/sub/e.js", "chest:e", "user"),
1050 file("/p/sub/g.js", "chest:g", "user"),
1051 ],
1052 ..ResolvedPaths::default()
1053 };
1054 problems.clear();
1055 let policies = file_policies(&resolved, &installed, Path::new("/p"), &mut problems);
1056 let root = |path: &str| policies.of(Path::new(path)).root;
1057 for file in ["/p/sub/a.js", "/p/sub/b.js"] {
1058 assert_eq!(root(file), Some("/p/sub".into()), "{file}");
1059 }
1060 let (project, user) = (
1065 "extensions.s in chest.toml",
1066 "extensions.s in ~/.chest/chest.toml",
1067 );
1068 let shared = |file: &str, first: &str, second: &str| {
1069 let message = format!(
1070 "{file} is in {first} and {second}, so it gets only the grants both give: fs"
1071 );
1072 ("chest:s".to_owned(), message)
1073 };
1074 let closed = format!(
1075 "/p/sub/c.js is in {project} and {user}, but extensions.x in chest.toml holds it too and does not load, so it runs with no capabilities: run `chest install`, which gives it only the grants all give: none, or `chest remove x`"
1076 );
1077 let narrowed = |name: &str, what: &str| {
1078 let message = format!(
1079 "/p/sub/{name}.js is in extensions.{name} in chest.toml and extensions.{name} in ~/.chest/chest.toml, so it gets only the {what}"
1080 );
1081 (format!("chest:{name}"), message)
1082 };
1083 let both = [
1084 shared("/p/sub/a.js", project, user),
1085 shared("/p/sub/b.js", user, project),
1086 ("chest:s".to_owned(), closed),
1087 narrowed("e", "event channels both give"),
1088 narrowed("g", "event channels and the grants both give: fs"),
1089 ];
1090 assert_eq!(problems, both);
1091 assert_eq!(root("/q/a.js"), Some("/q/a.js".into()));
1092 assert_eq!(root("/p/sub/c.js"), Some("/p/sub/c.js".into()));
1093 }
1094}