use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::files::FileGlobs;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AgentConfig {
pub protected: Vec<String>,
pub exclude: Vec<String>,
pub enforce: bool,
}
#[derive(Debug, Clone)]
pub struct AgentPolicy {
files: FileGlobs,
enforce: bool,
}
impl AgentPolicy {
pub fn new(config: &AgentConfig, base: impl Into<PathBuf>) -> Self {
Self {
files: FileGlobs::new(&config.protected, &config.exclude, base),
enforce: config.enforce,
}
}
pub fn base(&self) -> &Path {
self.files.base()
}
pub fn enforce(&self) -> bool {
self.enforce
}
pub fn is_protected(&self, path: &Path) -> bool {
self.files.matches(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(protected: &[&str], exclude: &[&str]) -> AgentPolicy {
let config = AgentConfig {
protected: protected.iter().map(|s| s.to_string()).collect(),
exclude: exclude.iter().map(|s| s.to_string()).collect(),
enforce: false,
};
AgentPolicy::new(&config, "/project")
}
fn protected(policy: &AgentPolicy, path: &str) -> bool {
policy.is_protected(&Path::new("/project").join(path))
}
#[test]
fn a_bare_name_matches_at_any_depth() {
let p = policy(&[".env*", "*.pem"], &[]);
assert!(protected(&p, ".env"));
assert!(protected(&p, ".env.production"));
assert!(protected(&p, "services/api/.env"));
assert!(protected(&p, "certs/server.pem"));
assert!(!protected(&p, "src/env.rs"));
}
#[test]
fn a_pattern_with_a_slash_is_anchored() {
let p = policy(&["config/prod.yml", "/data/*.csv"], &[]);
assert!(protected(&p, "config/prod.yml"));
assert!(!protected(&p, "app/config/prod.yml"));
assert!(protected(&p, "data/users.csv"));
assert!(!protected(&p, "data/2026/users.csv"));
}
#[test]
fn a_directory_pattern_covers_its_contents() {
let p = policy(&["secrets/", "dumps/**"], &[]);
assert!(protected(&p, "secrets/a.txt"));
assert!(protected(&p, "nested/secrets/a/b.txt"));
assert!(protected(&p, "dumps/2026/db.sql"));
assert!(!protected(&p, "nested/dumps/db.sql"));
}
#[test]
fn exclusions_win() {
let p = policy(&[".env*"], &[".env.example"]);
assert!(protected(&p, ".env"));
assert!(!protected(&p, ".env.example"));
}
#[test]
fn dot_dot_cannot_escape_a_match() {
let p = policy(&[".env"], &[]);
assert!(protected(&p, "src/../.env"));
assert!(protected(&p, "./.env"));
}
#[test]
fn paths_outside_the_project_are_not_protected() {
let p = policy(&["*"], &[]);
assert!(!p.is_protected(Path::new("/elsewhere/.env")));
assert!(!p.is_protected(Path::new("/project")));
}
}