use anyhow::{Context, Result};
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
#[allow(dead_code)]
pub const SECURITY_PATH_BLOCKED: i32 = 77;
#[allow(dead_code)]
const DEFAULT_BLOCKED_PATHS: &[&str] = &[
".ssh", ".gnupg", ".aws", ".npm", ".cargo", ".rustup", ".config",
];
#[allow(dead_code)]
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)
}
#[allow(dead_code)]
fn is_blocked_path(path: &std::path::Path) -> bool {
let path_str = path.to_string_lossy();
for blocked in DEFAULT_BLOCKED_PATHS {
if path_str.contains(blocked) {
return true;
}
}
if path_str.contains("~") {
return true;
}
false
}
#[allow(dead_code)]
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 DEFAULT_BLOCKED_PATHS {
if name_str.contains(blocked) || name_str.starts_with('.') {
violations.push(path.clone());
break;
}
}
}
}
Ok(violations)
}
#[allow(dead_code)]
pub fn run_security_preflight() -> Result<()> {
let workspace = std::env::var("GARDEN_WORKSPACE")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
println!("Scanning workspace at {:?}", workspace);
let violations = scan_workspace_for_blocked_paths(&workspace)?;
if !violations.is_empty() {
eprintln!("SECURITY: Found blocked path violations:");
for v in &violations {
eprintln!(" - {:?}", v);
}
anyhow::bail!("Blocked paths detected in workspace. Cannot proceed for security reasons.");
}
if let Ok(entries) = fs::read_dir(&workspace) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.contains(".env")
|| name.contains("credentials")
|| name.contains("secret")
|| name.contains("key")
{
eprintln!(
"SECURITY WARNING: Potentially sensitive file found: {}",
name
);
}
}
}
}
let allowlist_path = workspace.join("agents/.allowlist");
if allowlist_path.exists() {
match load_allowlist(&allowlist_path) {
Ok(allowed) => {
println!("Allowlist loaded: {} entries", allowed.len());
}
Err(e) => {
eprintln!("Warning: Could not load allowlist: {}", e);
}
}
} else {
println!("No allowlist file found, using defaults.");
}
Ok(())
}
#[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")));
}
#[test]
fn test_load_allowlist() {
let temp_dir = std::env::temp_dir();
let allowlist_path = temp_dir.join("test_allowlist");
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();
}
}