use std::collections::HashMap;
use std::path::PathBuf;
pub fn trust_store_path() -> PathBuf {
crate::store::global_dir()
.unwrap_or_else(|| PathBuf::from(".tokenix"))
.join("trusted_filters.json")
}
pub fn repo_input_hashes(root: &std::path::Path) -> std::collections::BTreeMap<String, String> {
use sha2::{Digest, Sha256};
let mut hashes = std::collections::BTreeMap::new();
let mut add = |label: String, path: &std::path::Path| {
if let Ok(content) = std::fs::read(path) {
hashes.insert(label, hex::encode(Sha256::digest(&content)));
}
};
for sub in ["filters", "secret-rules", "egress-rules"] {
let dir = root.join(".tokenix").join(sub);
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
add(format!(".tokenix/{sub}/{name}"), &path);
}
}
}
}
for rel in [".mcp.json", "opencode.json"] {
add(rel.to_string(), &root.join(rel));
}
add(
".vscode/mcp.json".to_string(),
&root.join(".vscode").join("mcp.json"),
);
hashes
}
fn load_trust_store() -> HashMap<String, std::collections::BTreeMap<String, String>> {
std::fs::read_to_string(trust_store_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn repo_inputs_trusted(root: &std::path::Path) -> bool {
let current = repo_input_hashes(root);
if current.is_empty() {
return true; }
let store = load_trust_store();
match store.get(&root.to_string_lossy().to_string()) {
Some(trusted) => *trusted == current,
None => false,
}
}
pub fn set_repo_inputs_trust(root: &std::path::Path, trust: bool) -> std::io::Result<usize> {
let mut store = load_trust_store();
let key = root.to_string_lossy().to_string();
let count;
if trust {
let current = repo_input_hashes(root);
count = current.len();
store.insert(key, current);
} else {
count = store.remove(&key).map(|m| m.len()).unwrap_or(0);
}
let path = trust_store_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(
&path,
serde_json::to_string_pretty(&store).unwrap_or_default(),
)?;
crate::store::restrict_to_owner(&path);
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repo_input_hashes_cover_every_executable_repo_input() {
let dir = std::env::temp_dir().join(format!("tokenix-gate-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join(".tokenix").join("filters")).unwrap();
std::fs::create_dir_all(dir.join(".tokenix").join("secret-rules")).unwrap();
std::fs::create_dir_all(dir.join(".tokenix").join("egress-rules")).unwrap();
std::fs::create_dir_all(dir.join(".vscode")).unwrap();
std::fs::write(dir.join(".tokenix/filters/a.toml"), "x").unwrap();
std::fs::write(dir.join(".tokenix/secret-rules/b.toml"), "x").unwrap();
std::fs::write(dir.join(".tokenix/egress-rules/c.toml"), "x").unwrap();
std::fs::write(dir.join(".mcp.json"), "{}").unwrap();
std::fs::write(dir.join("opencode.json"), "{}").unwrap();
std::fs::write(dir.join(".vscode/mcp.json"), "{}").unwrap();
let hashes = repo_input_hashes(&dir);
for expected in [
".tokenix/filters/a.toml",
".tokenix/secret-rules/b.toml",
".tokenix/egress-rules/c.toml",
".mcp.json",
"opencode.json",
".vscode/mcp.json",
] {
assert!(
hashes.contains_key(expected),
"{expected} not gated: {hashes:?}"
);
}
assert!(
!repo_inputs_trusted(&dir),
"a repo with executable inputs must not be trusted until approved"
);
let _ = std::fs::remove_dir_all(&dir);
}
}