返回 CodeWhale
authority.rs
根目录 / crates / tui / src / hooks / authority.rs
1 //! User-owned approval of the exact project hooks file, separate from folder trust.
2 use sha2::{Digest, Sha256};
3 use std::path::{Path, PathBuf};
4
5 #[derive(Debug, Clone, PartialEq, Eq)]
6 pub struct ProjectHookAuthority {
7 pub workspace: PathBuf,
8 pub digest: String,
9 }
10
11 /// Read once for both the digest and parsing. Repository symlinks and special
12 /// files are not executable configuration, even if the workspace is trusted.
13 pub(crate) fn review_project_hooks(
14 workspace: &Path,
15 ) -> Result<(ProjectHookAuthority, String), String> {
16 let workspace = workspace
17 .canonicalize()
18 .map_err(|_| "Cannot resolve hooks workspace")?;
19 let mut path = workspace.clone();
20 for component in [".codewhale", "hooks.toml"] {
21 path.push(component);
22 let metadata =
23 std::fs::symlink_metadata(&path).map_err(|_| "Cannot read project hooks path")?;
24 if metadata.file_type().is_symlink() {
25 return Err("Project hooks path must not contain symlinks".into());
26 }
27 if (component == ".codewhale" && !metadata.is_dir())
28 || (component == "hooks.toml" && !metadata.is_file())
29 {
30 return Err("Project hooks must be a regular file in .codewhale".into());
31 }
32 }
33 let contents = super::config::read_project_hooks_file(&path)
34 .map_err(|_| "Cannot read project hooks file (maximum 1 MiB)")?;
35 let digest = Sha256::digest(contents.as_bytes())
36 .iter()
37 .map(|byte| format!("{byte:02x}"))
38 .collect();
39 Ok((ProjectHookAuthority { workspace, digest }, contents))
40 }
41
42 pub(crate) fn approved_project_hooks(
43 workspace: &Path,
44 ) -> Result<(ProjectHookAuthority, String), String> {
45 if !crate::config::is_workspace_trusted(workspace) {
46 return Err("Project hooks require workspace trust and separate hook approval".into());
47 }
48 let (authority, contents) = review_project_hooks(workspace)?;
49 if crate::config::hook_receipt_for_workspace(&authority.workspace).as_deref()
50 != Some(&authority.digest)
51 {
52 return Err("Project hooks are unapproved or changed; use /hooks review, then /hooks approve <digest>".into());
53 }
54 Ok((authority, contents))
55 }
56
57 pub(crate) fn verify_hook_authorities(
58 plugin: Option<&crate::plugins::types::PluginAuthority>,
59 project: Option<&ProjectHookAuthority>,
60 ) -> Result<(), String> {
61 if let Some(authority) = plugin {
62 crate::plugins::registry::verify_plugin_component_authority(
63 authority,
64 crate::plugins::activation::PluginActivationCapability::Hooks,
65 )?;
66 }
67 if let Some(authority) = project {
68 let (current, _) = approved_project_hooks(&authority.workspace)?;
69 if &current != authority {
70 return Err("Project hooks changed after loading; review and approve again".into());
71 }
72 }
73 Ok(())
74 }
75
76 pub(crate) fn approve_project_hooks(workspace: &Path, reviewed_digest: &str) -> Result<(), String> {
77 if !crate::config::is_workspace_trusted(workspace) {
78 return Err("Trust the workspace before approving its hooks".into());
79 }
80 let (authority, contents) = review_project_hooks(workspace)?;
81 if reviewed_digest != authority.digest {
82 return Err("Hooks do not match the reviewed digest; use /hooks review again".into());
83 }
84 let parsed =
85 toml::from_str::<super::HooksConfig>(&contents).map_err(|_| "Invalid hooks TOML")?;
86 if parsed.validate().iter().any(|problem| problem.rejected) {
87 return Err("Hooks contain invalid entries; correct them before approval".into());
88 }
89 crate::config::save_workspace_hook_receipt(&authority.workspace, reviewed_digest)
90 .map_err(|_| "Could not save hook approval in user config".to_string())?;
91 Ok(())
92 }
93
93 lines RUST