use crate::settings::ProjectTrustDefault;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Serialize, Deserialize)]
struct TrustFile {
#[serde(flatten)]
decisions: BTreeMap<String, bool>,
}
pub fn trust_file_path() -> Option<PathBuf> {
kiss_ai::paths::agent_dir().map(|dir| dir.join("trust.json"))
}
fn read_trust_file() -> TrustFile {
trust_file_path()
.and_then(|path| kiss_ai::config_file::read_json(&path).ok())
.unwrap_or_default()
}
pub fn gated_paths(cwd: &Path) -> Vec<PathBuf> {
let mut paths = vec![cwd.join(".kiss"), cwd.join(".mcp.json")];
paths.extend(crate::skills::project_agents_skill_dirs(cwd));
paths
}
pub fn has_project_resources(cwd: &Path) -> bool {
gated_paths(cwd).iter().any(|path| path.exists())
}
pub fn saved_decision(cwd: &Path) -> Option<bool> {
let file = read_trust_file();
for dir in cwd.ancestors() {
if let Some(&decision) = file.decisions.get(&dir.display().to_string()) {
return Some(decision);
}
}
None
}
pub fn save_decision(path: &Path, trusted: bool) -> anyhow::Result<()> {
let file_path = trust_file_path().ok_or_else(|| anyhow::anyhow!(
"The trust decision cannot be saved because the home directory is unknown. Set HOME, then try again."
))?;
kiss_ai::config_file::with_lock(&file_path, || {
let mut file: TrustFile = kiss_ai::config_file::read_json(&file_path)?;
file.decisions.insert(path.display().to_string(), trusted);
kiss_ai::config_file::write_atomic(
&file_path,
serde_json::to_string_pretty(&file)?.as_bytes(),
)
})
}
pub fn resolve(cwd: &Path, run_override: Option<bool>) -> bool {
decide(
cwd,
run_override,
crate::Settings::load(cwd, false).default_project_trust,
)
}
fn decide(cwd: &Path, cli_override: Option<bool>, default: ProjectTrustDefault) -> bool {
if let Some(explicit) = cli_override {
return explicit;
}
if let Some(saved) = saved_decision(cwd) {
return saved;
}
if !has_project_resources(cwd) {
return true; }
matches!(default, ProjectTrustDefault::Always)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_trust_gated_project_file_needs_a_decision() {
let empty = tempfile::tempdir().unwrap();
assert!(!has_project_resources(empty.path()));
let mcp_only = tempfile::tempdir().unwrap();
std::fs::write(mcp_only.path().join(".mcp.json"), "{}").unwrap();
assert!(has_project_resources(mcp_only.path()));
let repo = tempfile::tempdir().unwrap();
std::fs::create_dir(repo.path().join(".git")).unwrap();
std::fs::create_dir_all(repo.path().join(".agents/skills")).unwrap();
let nested = repo.path().join("crates/app");
std::fs::create_dir_all(&nested).unwrap();
assert!(has_project_resources(&nested));
}
}