use anyhow::{Context, Result};
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
const BLOCKED_PATHS: &[&str] = &[
".ssh", ".gnupg", ".aws", ".npm", ".cargo", ".rustup", ".config",
];
pub fn load_allowlist(path: &PathBuf) -> Result<HashSet<String>> {
let mut allowed = HashSet::new();
allowed.insert("pi-coding-agent".to_string());
if path.exists() {
let content = fs::read_to_string(path).context("Failed to read allowlist")?;
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
allowed.insert(trimmed.to_string());
}
}
}
Ok(allowed)
}
#[cfg(test)]
fn is_blocked_path(path: &std::path::Path) -> bool {
let path_str = path.to_string_lossy();
for blocked in BLOCKED_PATHS {
if path_str.contains(blocked) {
return true;
}
}
path_str.contains("~")
}
pub fn scan_workspace_for_blocked_paths(workspace: &PathBuf) -> Result<Vec<PathBuf>> {
let agents_dir = workspace.join("agents");
let mut violations = Vec::new();
if !agents_dir.exists() {
return Ok(violations);
}
let entries = fs::read_dir(&agents_dir).context("Failed to read agents directory")?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
continue;
}
if let Some(name) = path.file_name() {
let name_str = name.to_string_lossy();
for blocked in BLOCKED_PATHS {
if name_str.contains(blocked) || name_str.starts_with('.') {
violations.push(path.clone());
break;
}
}
}
}
Ok(violations)
}
pub fn check_env_security(env_path: &PathBuf) -> Vec<String> {
let mut warnings = Vec::new();
if !env_path.exists() {
return warnings;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = fs::metadata(env_path) {
let mode = metadata.permissions().mode();
if mode & 0o077 != 0 {
warnings.push(".env file is readable by group/others (should be 0600)".to_string());
}
}
}
if let Ok(content) = fs::read_to_string(env_path) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
if !value.is_empty()
&& (value.starts_with("sk-") || value.starts_with("key-") || value.len() < 8)
{
warnings.push(format!("{} may have a weak or suspicious value", key));
}
}
}
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_blocked_path() {
assert!(is_blocked_path(std::path::Path::new("/home/user/.ssh")));
assert!(is_blocked_path(std::path::Path::new("/home/user/.gnupg")));
assert!(is_blocked_path(std::path::Path::new("/home/user/.aws")));
assert!(is_blocked_path(std::path::Path::new(
"/some/path/.ssh/config"
)));
assert!(is_blocked_path(std::path::Path::new("~/.ssh/id_rsa")));
assert!(!is_blocked_path(std::path::Path::new(
"/home/user/mygarden"
)));
}
#[test]
fn test_load_allowlist() {
let temp_dir = std::env::temp_dir();
let allowlist_path = temp_dir.join("test_allowlist_clawgarden");
fs::write(
&allowlist_path,
"pi-coding-agent\n# comment\ncustom-agent\n",
)
.unwrap();
let allowed = load_allowlist(&allowlist_path).unwrap();
assert!(allowed.contains("pi-coding-agent"));
assert!(allowed.contains("custom-agent"));
assert_eq!(allowed.len(), 2);
fs::remove_file(&allowlist_path).ok();
}
}