use crate::fs::allow_list::CascadingAllowList;
use fuser::Request;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::time::SystemTime;
use tracing::{debug, warn};
pub struct Policy {
root: PathBuf,
root_matcher: ignore::gitignore::Gitignore,
dir_matchers: HashMap<PathBuf, Option<ignore::gitignore::Gitignore>>,
allow_list: CascadingAllowList,
last_loaded: SystemTime,
config_mtimes: HashMap<PathBuf, Option<SystemTime>>,
matcher_cache: RwLock<HashMap<PathBuf, PathBuf>>,
}
impl Policy {
pub fn load(root: &Path) -> Self {
let agentignore = root.join(".agentignore");
let agentallow = root.join(".agentallow");
let mut config_mtimes = HashMap::new();
config_mtimes.insert(agentignore.clone(), Self::get_mtime(&agentignore));
config_mtimes.insert(agentallow.clone(), Self::get_mtime(&agentallow));
let root_matcher = Self::build_matcher(root, &[root.to_path_buf()]);
let allow_list = CascadingAllowList::load(root);
let mut dir_matchers = HashMap::new();
Self::scan_dir_matchers(root, root, &mut dir_matchers, &[]);
for dir in dir_matchers.keys() {
let cfg_path = dir.join(".agentignore");
config_mtimes
.entry(cfg_path.clone())
.or_insert_with(|| Self::get_mtime(&cfg_path));
let allow_path = dir.join(".agentallow");
config_mtimes
.entry(allow_path.clone())
.or_insert_with(|| Self::get_mtime(&allow_path));
}
Self {
root: root.to_path_buf(),
root_matcher,
dir_matchers,
allow_list,
last_loaded: SystemTime::now(),
config_mtimes,
matcher_cache: RwLock::new(HashMap::new()),
}
}
fn build_matcher(root: &Path, config_dirs: &[PathBuf]) -> ignore::gitignore::Gitignore {
let mut builder = ignore::gitignore::GitignoreBuilder::new(root);
for dir in config_dirs {
let agentignore = dir.join(".agentignore");
if agentignore.exists()
&& let Some(err) = builder.add(&agentignore)
{
warn!("Error loading .agentignore from {:?}: {}", dir, err);
}
}
builder.build().unwrap_or_else(|_| {
ignore::gitignore::GitignoreBuilder::new(root)
.build()
.unwrap()
})
}
fn scan_dir_matchers(
root: &Path,
current: &Path,
matchers: &mut HashMap<PathBuf, Option<ignore::gitignore::Gitignore>>,
ancestor_config_dirs: &[PathBuf],
) {
if let Ok(entries) = std::fs::read_dir(current) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let agentignore = path.join(".agentignore");
let mut current_config_dirs = ancestor_config_dirs.to_vec();
if agentignore.exists() {
current_config_dirs.push(path.clone());
let mut all_configs = vec![root.to_path_buf()];
all_configs.extend(current_config_dirs.iter().cloned());
let matcher = Self::build_matcher(root, &all_configs);
matchers.insert(path.clone(), Some(matcher));
} else {
matchers.insert(path.clone(), None);
}
Self::scan_dir_matchers(root, &path, matchers, ¤t_config_dirs);
}
}
}
}
fn get_mtime(path: &Path) -> Option<SystemTime> {
std::fs::metadata(path).ok()?.modified().ok()
}
fn get_matcher_for_path(&self, path: &Path) -> &ignore::gitignore::Gitignore {
{
let cache = self
.matcher_cache
.read()
.expect("matcher_cache RwLock poisoned — fatal process state");
if let Some(cached_dir) = cache.get(path) {
if cached_dir.as_os_str().is_empty() {
return &self.root_matcher;
}
if let Some(Some(matcher)) = self.dir_matchers.get(cached_dir) {
return matcher;
}
}
}
let mut current = path.to_path_buf();
while current.starts_with(&self.root) {
if let Some(Some(_)) = self.dir_matchers.get(¤t) {
self.matcher_cache
.write()
.expect("matcher_cache RwLock poisoned — fatal process state")
.insert(path.to_path_buf(), current.clone());
return self.dir_matchers.get(¤t).unwrap().as_ref().unwrap();
}
if current == self.root {
break;
}
current = current.parent().unwrap_or(&self.root).to_path_buf();
}
self.matcher_cache
.write()
.expect("matcher_cache RwLock poisoned — fatal process state")
.insert(path.to_path_buf(), PathBuf::new());
&self.root_matcher
}
pub fn has_config_changed(&self) -> bool {
let agentignore = self.root.join(".agentignore");
let agentallow = self.root.join(".agentallow");
let current = Self::get_mtime(&agentignore);
if current != self.config_mtimes.get(&agentignore).copied().flatten() {
return true;
}
let current = Self::get_mtime(&agentallow);
if current != self.config_mtimes.get(&agentallow).copied().flatten() {
return true;
}
for dir in self.dir_matchers.keys() {
let cfg = dir.join(".agentignore");
let mtime = Self::get_mtime(&cfg);
if mtime != self.config_mtimes.get(&cfg).copied().flatten() {
return true;
}
let allow = dir.join(".agentallow");
let mtime = Self::get_mtime(&allow);
if mtime != self.config_mtimes.get(&allow).copied().flatten() {
return true;
}
}
false
}
pub fn check_and_reload(&mut self) -> bool {
let agentignore = self.root.join(".agentignore");
let agentallow = self.root.join(".agentallow");
let current_agentignore_mtime = Self::get_mtime(&agentignore);
let current_agentallow_mtime = Self::get_mtime(&agentallow);
let mut needs_reload = current_agentignore_mtime
!= self.config_mtimes.get(&agentignore).copied().flatten()
|| current_agentallow_mtime != self.config_mtimes.get(&agentallow).copied().flatten();
let mut dir_matchers = std::mem::take(&mut self.dir_matchers);
for dir in dir_matchers.keys() {
let agentignore = dir.join(".agentignore");
let mtime = Self::get_mtime(&agentignore);
if mtime != self.config_mtimes.get(&agentignore).copied().flatten() {
needs_reload = true;
self.config_mtimes.insert(agentignore, mtime);
}
let agentallow = dir.join(".agentallow");
let allow_mtime = Self::get_mtime(&agentallow);
if allow_mtime != self.config_mtimes.get(&agentallow).copied().flatten() {
needs_reload = true;
self.config_mtimes.insert(agentallow, allow_mtime);
}
}
if needs_reload {
debug!("Config files changed, reloading policy");
self.root_matcher = Self::build_matcher(&self.root, &[self.root.to_path_buf()]);
dir_matchers.clear();
Self::scan_dir_matchers(&self.root, &self.root, &mut dir_matchers, &[]);
self.allow_list = CascadingAllowList::load(&self.root);
self.last_loaded = SystemTime::now();
self.matcher_cache
.write()
.expect("matcher_cache RwLock poisoned — fatal process state")
.clear();
self.config_mtimes
.insert(agentignore.clone(), current_agentignore_mtime);
self.config_mtimes
.insert(agentallow.clone(), current_agentallow_mtime);
self.dir_matchers = dir_matchers;
debug!("Policy reloaded successfully");
} else {
self.dir_matchers = dir_matchers;
}
needs_reload
}
pub fn is_hidden(&self, real_path: &Path) -> bool {
if let Some(name) = real_path.file_name()
&& (name == ".agentignore" || name == ".agentallow")
{
return true;
}
let is_dir = real_path.is_dir();
let matcher = self.get_matcher_for_path(real_path);
matches!(
matcher.matched_path_or_any_parents(real_path, is_dir),
ignore::Match::Ignore(_)
)
}
pub fn is_request_allowed(&self, path: &Path, req: &Request) -> bool {
self.allow_list.is_allowed(path, req.pid())
}
pub fn is_allowed_raw(&self, path: &Path, pid: u32) -> bool {
self.allow_list.is_allowed(path, pid)
}
}