pub mod ai_agents;
pub mod npm;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::fs::DirAccess;
use ai_agents::AiAgent;
use npm::Npm;
pub enum DirectoryAccess {
AllowAny,
AskAllowList(Vec<String>),
}
pub trait Profile {
fn redaction(&self) -> bool;
fn directory_access(&self) -> DirectoryAccess;
fn directory_gate(&self, program: &OsStr, debug: bool) -> Option<Arc<dyn DirAccess>> {
match self.directory_access() {
DirectoryAccess::AllowAny => None,
DirectoryAccess::AskAllowList(allowlist) => {
let prog = program_basename(program).to_string_lossy().into_owned();
let preapproved = allowlist.iter().map(PathBuf::from).collect();
Some(Arc::new(npm::DirGate::new(prog, preapproved, debug)) as Arc<dyn DirAccess>)
}
}
}
}
pub fn resolve(program: &OsStr) -> Option<Box<dyn Profile>> {
if npm::is_npm(program) {
Some(Box::new(Npm))
} else if ai_agents::is_ai_agent(program) {
Some(Box::new(AiAgent))
} else {
None
}
}
pub fn by_name(name: &str) -> Option<Box<dyn Profile>> {
match name {
"agent" => Some(Box::new(AiAgent)),
"npm" => Some(Box::new(Npm)),
_ => None,
}
}
pub fn unrestricted() -> Box<dyn Profile> {
Box::new(AiAgent)
}
pub fn names() -> Vec<&'static str> {
vec!["agent", "npm"]
}
pub fn program_basename(program: &OsStr) -> &OsStr {
Path::new(program).file_name().unwrap_or(program)
}
pub fn permitted_programs() -> Vec<&'static str> {
ai_agents::AI_AGENTS
.iter()
.copied()
.chain(npm::NPM_PROGRAMS.iter().copied())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ai_agents_redact_without_directory_prompts() {
let p = resolve(OsStr::new("claude")).unwrap();
assert!(p.redaction());
assert!(matches!(p.directory_access(), DirectoryAccess::AllowAny));
assert!(p.directory_gate(OsStr::new("claude"), false).is_none());
assert!(resolve(OsStr::new("/usr/bin/opencode")).is_some());
}
#[test]
fn package_managers_redact_and_gate_directories() {
let p = resolve(OsStr::new("npm")).unwrap();
assert!(p.redaction());
assert!(matches!(
p.directory_access(),
DirectoryAccess::AskAllowList(_)
));
assert!(p.directory_gate(OsStr::new("npm"), false).is_some());
assert!(resolve(OsStr::new("/usr/bin/yarn"))
.unwrap()
.directory_gate(OsStr::new("yarn"), false)
.is_some());
}
#[test]
fn unknown_programs_are_unrecognized() {
assert!(resolve(OsStr::new("cat")).is_none());
assert!(resolve(OsStr::new("/bin/sh")).is_none());
}
#[test]
fn by_name_selects_profiles() {
assert!(matches!(
by_name("npm").unwrap().directory_access(),
DirectoryAccess::AskAllowList(_)
));
assert!(matches!(
by_name("agent").unwrap().directory_access(),
DirectoryAccess::AllowAny
));
assert!(by_name("nope").is_none());
}
#[test]
fn permitted_list_covers_both_kinds() {
let list = permitted_programs();
assert!(list.contains(&"claude"));
assert!(list.contains(&"opencode"));
assert!(list.contains(&"npm"));
assert!(list.contains(&"pnpm"));
}
}