use std::path::Path;
pub const PROTECTED_FILES: &[&str] = &["LocalGPT.md", ".localgpt_manifest.json", "IDENTITY.md"];
pub const PROTECTED_EXTERNAL_PATHS: &[&str] = &[".device_key", ".security_audit.jsonl"];
pub fn is_workspace_file_protected(filename: &str) -> bool {
let name = Path::new(filename)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(filename);
PROTECTED_FILES.contains(&name)
}
pub fn is_path_protected(path: &str, workspace: &Path, state_dir: &Path) -> bool {
let expanded = shellexpand::tilde(path);
let expanded_path = Path::new(expanded.as_ref());
if let Ok(canonical_workspace) = workspace.canonicalize()
&& let Ok(canonical_path) = expanded_path.canonicalize()
{
for &protected in PROTECTED_FILES {
let protected_full = canonical_workspace.join(protected);
if canonical_path == protected_full {
return true;
}
}
}
if is_workspace_file_protected(path) {
return true;
}
if let Ok(canonical_state) = state_dir.canonicalize()
&& let Ok(canonical_path) = expanded_path.canonicalize()
{
for &protected in PROTECTED_EXTERNAL_PATHS {
let protected_full = canonical_state.join(protected);
if canonical_path == protected_full {
return true;
}
}
}
let name = Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path);
PROTECTED_EXTERNAL_PATHS.contains(&name)
}
pub fn check_bash_command(command: &str) -> Vec<&'static str> {
let mut found = Vec::new();
for &name in PROTECTED_FILES {
if command.contains(name) {
found.push(name);
}
}
for &name in PROTECTED_EXTERNAL_PATHS {
if command.contains(name) {
found.push(name);
}
}
found
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn workspace_files_protected() {
assert!(is_workspace_file_protected("LocalGPT.md"));
assert!(is_workspace_file_protected(".localgpt_manifest.json"));
assert!(is_workspace_file_protected("IDENTITY.md"));
}
#[test]
fn regular_files_not_protected() {
assert!(!is_workspace_file_protected("MEMORY.md"));
assert!(!is_workspace_file_protected("HEARTBEAT.md"));
assert!(!is_workspace_file_protected("SOUL.md"));
assert!(!is_workspace_file_protected("config.toml"));
assert!(!is_workspace_file_protected("memory/2024-01-15.md"));
}
#[test]
fn path_with_directory_checks_filename() {
assert!(is_workspace_file_protected("workspace/LocalGPT.md"));
assert!(is_workspace_file_protected(
"/home/user/.localgpt/workspace/IDENTITY.md"
));
}
#[test]
fn bash_command_detection() {
let hits = check_bash_command("echo 'new rules' > LocalGPT.md");
assert!(hits.contains(&"LocalGPT.md"));
let hits = check_bash_command("cat .device_key");
assert!(hits.contains(&".device_key"));
let hits = check_bash_command("ls -la");
assert!(hits.is_empty());
}
#[test]
fn path_protected_with_real_paths() {
let tmp = tempfile::tempdir().unwrap();
let workspace = tmp.path().join("workspace");
let state_dir = tmp.path().join("state");
fs::create_dir_all(&workspace).unwrap();
fs::create_dir_all(&state_dir).unwrap();
let policy = workspace.join("LocalGPT.md");
fs::write(&policy, "test").unwrap();
assert!(is_path_protected(
policy.to_str().unwrap(),
&workspace,
&state_dir
));
let memory = workspace.join("MEMORY.md");
fs::write(&memory, "test").unwrap();
assert!(!is_path_protected(
memory.to_str().unwrap(),
&workspace,
&state_dir
));
}
}