use std::path::PathBuf;
pub const DEFAULT_LLM_MODEL: &str = "claude-haiku-4-5-20251001";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub paths: Paths,
pub llm: LlmConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Paths {
pub home: PathBuf,
pub user_home: PathBuf,
}
impl Paths {
pub fn snapshots(&self) -> PathBuf {
self.home.join("snapshots")
}
pub fn scans(&self) -> PathBuf {
self.home.join("scans")
}
pub fn web_log(&self) -> PathBuf {
self.home.join("web_log")
}
pub fn paste_log(&self) -> PathBuf {
self.home.join("paste_log")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LlmConfig {
pub api_key: Option<String>,
pub model: String,
}
impl Config {
pub fn from_env() -> Self {
Self::from_env_lookup(|k| std::env::var(k).ok())
}
pub fn from_env_lookup<F>(lookup: F) -> Self
where
F: Fn(&str) -> Option<String>,
{
let user_home = lookup("HOME").unwrap_or_else(|| ".".into());
let home = lookup("AGENTSEC_HOME").map_or_else(
|| PathBuf::from(&user_home).join(".agentsec"),
PathBuf::from,
);
let user_home = PathBuf::from(user_home);
let api_key = lookup("ANTHROPIC_API_KEY").filter(|s| !s.is_empty());
let model = lookup("AGENTSEC_LLM_MODEL").unwrap_or_else(|| DEFAULT_LLM_MODEL.to_string());
Self {
paths: Paths { home, user_home },
llm: LlmConfig { api_key, model },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn map_lookup<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
let map: HashMap<&str, &str> = pairs.iter().copied().collect();
move |k| map.get(k).map(|s| (*s).to_string())
}
#[test]
fn empty_env_uses_all_defaults() {
let cfg = Config::from_env_lookup(|_| None);
assert_eq!(cfg.paths.user_home, PathBuf::from("."));
assert_eq!(cfg.paths.home, PathBuf::from("./.agentsec"));
assert_eq!(cfg.llm.api_key, None);
assert_eq!(cfg.llm.model, DEFAULT_LLM_MODEL);
}
#[test]
fn home_only_derives_agentsec_home() {
let cfg = Config::from_env_lookup(map_lookup(&[("HOME", "/home/alice")]));
assert_eq!(cfg.paths.user_home, PathBuf::from("/home/alice"));
assert_eq!(cfg.paths.home, PathBuf::from("/home/alice/.agentsec"));
}
#[test]
fn agentsec_home_overrides_default() {
let cfg = Config::from_env_lookup(map_lookup(&[
("HOME", "/home/alice"),
("AGENTSEC_HOME", "/var/lib/agentsec"),
]));
assert_eq!(cfg.paths.home, PathBuf::from("/var/lib/agentsec"));
assert_eq!(cfg.paths.user_home, PathBuf::from("/home/alice"));
}
#[test]
fn paths_methods_join_under_home() {
let cfg = Config::from_env_lookup(map_lookup(&[("AGENTSEC_HOME", "/tmp/x")]));
assert_eq!(cfg.paths.snapshots(), PathBuf::from("/tmp/x/snapshots"));
assert_eq!(cfg.paths.scans(), PathBuf::from("/tmp/x/scans"));
assert_eq!(cfg.paths.web_log(), PathBuf::from("/tmp/x/web_log"));
assert_eq!(cfg.paths.paste_log(), PathBuf::from("/tmp/x/paste_log"));
}
#[test]
fn empty_api_key_treated_as_absent() {
let cfg = Config::from_env_lookup(map_lookup(&[("ANTHROPIC_API_KEY", "")]));
assert_eq!(cfg.llm.api_key, None);
}
#[test]
fn api_key_and_model_are_picked_up() {
let cfg = Config::from_env_lookup(map_lookup(&[
("ANTHROPIC_API_KEY", "sk-test"),
("AGENTSEC_LLM_MODEL", "claude-sonnet-4-6"),
]));
assert_eq!(cfg.llm.api_key, Some("sk-test".to_string()));
assert_eq!(cfg.llm.model, "claude-sonnet-4-6");
}
}