hematite/agent/
trust_resolver.rs1use std::path::{Path, PathBuf};
2
3use crate::agent::config::WorkspaceTrustConfig;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum WorkspaceTrustPolicy {
7 Trusted,
8 RequireApproval,
9 Denied,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum WorkspaceTrustSource {
14 DefaultWorkspace,
15 Allowlist,
16 UnknownWorkspace,
17 Denylist,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct WorkspaceTrustDecision {
22 pub policy: WorkspaceTrustPolicy,
23 pub source: WorkspaceTrustSource,
24 pub workspace_display: String,
25 pub matched_root: Option<String>,
26 pub reason: Option<String>,
27}
28
29pub fn resolve_workspace_trust(
30 workspace_root: &Path,
31 config: &WorkspaceTrustConfig,
32) -> WorkspaceTrustDecision {
33 let workspace = normalize_path(workspace_root);
34 let workspace_display = workspace.to_string_lossy().replace('\\', "/");
35
36 let denied_roots = resolve_roots(workspace_root, &config.deny);
37 if let Some(root) = denied_roots
38 .iter()
39 .find(|root| path_matches(&workspace, root))
40 {
41 let matched = root.to_string_lossy().replace('\\', "/");
42 return WorkspaceTrustDecision {
43 policy: WorkspaceTrustPolicy::Denied,
44 source: WorkspaceTrustSource::Denylist,
45 workspace_display,
46 matched_root: Some(matched.clone()),
47 reason: Some(format!("workspace matches denied trust root: {}", matched)),
48 };
49 }
50
51 let allow_roots = resolve_roots(workspace_root, &config.allow);
52 if let Some(root) = allow_roots
53 .iter()
54 .find(|root| path_matches(&workspace, root))
55 {
56 let matched = root.to_string_lossy().replace('\\', "/");
57 let source = if root == &workspace {
58 WorkspaceTrustSource::DefaultWorkspace
59 } else {
60 WorkspaceTrustSource::Allowlist
61 };
62 return WorkspaceTrustDecision {
63 policy: WorkspaceTrustPolicy::Trusted,
64 source,
65 workspace_display,
66 matched_root: Some(matched),
67 reason: None,
68 };
69 }
70
71 WorkspaceTrustDecision {
72 policy: WorkspaceTrustPolicy::RequireApproval,
73 source: WorkspaceTrustSource::UnknownWorkspace,
74 workspace_display,
75 matched_root: None,
76 reason: Some(
77 "workspace is not trust-allowlisted, so destructive or external actions require approval."
78 .to_string(),
79 ),
80 }
81}
82
83fn resolve_roots(workspace_root: &Path, configured_roots: &[String]) -> Vec<PathBuf> {
84 configured_roots
85 .iter()
86 .map(|root| {
87 let path = Path::new(root);
88 if path.is_absolute() {
89 normalize_path(path)
90 } else {
91 normalize_path(&workspace_root.join(path))
92 }
93 })
94 .collect()
95}
96
97fn path_matches(candidate: &Path, root: &Path) -> bool {
98 candidate == root || candidate.starts_with(root)
99}
100
101fn normalize_path(path: &Path) -> PathBuf {
102 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::{resolve_workspace_trust, WorkspaceTrustPolicy, WorkspaceTrustSource};
108 use crate::agent::config::WorkspaceTrustConfig;
109 use std::path::PathBuf;
110
111 #[test]
112 fn trusts_current_workspace_by_default() {
113 let root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
114 let decision = resolve_workspace_trust(&root, &WorkspaceTrustConfig::default());
115 assert_eq!(decision.policy, WorkspaceTrustPolicy::Trusted);
116 assert_eq!(decision.source, WorkspaceTrustSource::DefaultWorkspace);
117 }
118
119 #[test]
120 fn denied_root_takes_precedence() {
121 let root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
122 let config = WorkspaceTrustConfig {
123 allow: vec![".".to_string()],
124 deny: vec![root.to_string_lossy().to_string()],
125 };
126 let decision = resolve_workspace_trust(&root, &config);
127 assert_eq!(decision.policy, WorkspaceTrustPolicy::Denied);
128 assert_eq!(decision.source, WorkspaceTrustSource::Denylist);
129 }
130
131 #[test]
132 fn unknown_workspace_requires_approval() {
133 let root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
134 let config = WorkspaceTrustConfig {
135 allow: vec!["./somewhere-else".to_string()],
136 deny: Vec::new(),
137 };
138 let decision = resolve_workspace_trust(&root, &config);
139 assert_eq!(decision.policy, WorkspaceTrustPolicy::RequireApproval);
140 assert_eq!(decision.source, WorkspaceTrustSource::UnknownWorkspace);
141 }
142}