//! Command classification against the corpus. use std::path::{Path, PathBuf}; /// How a command is classified. #[derive(Debug)] pub enum CommandClass { /// In corpus directory — governed execution via org-ops wrapper. Governed { corpus_binary: PathBuf }, /// Not in corpus but exists on PATH — allowed but untracked. Ungoverned, /// Was in corpus manifest but binary removed — DENIED (killswitch). Denied { reason: String }, /// Shell builtins and navigation — zero governance overhead. Free, } /// Free commands: zero governance overhead. pub const FREE_COMMANDS: &[&str] = &[ "ls", "ll", "la", "dir", "cat", "head", "tail", "less", "more", "grep", "awk", "sed", "echo", "printf", "pwd", "cd", "pushd", "popd", "env", "export", "set", "unset", "which", "whereis", "type", "file", "stat", "wc", "sort", "uniq", "tr", "cut", "date", "cal", "whoami", "id", "hostname", "uname", "clear", "history", "alias", "true", "false", "test", "man", "help", "fg", "bg", "jobs", "kill", "ps", "top", "df", "du", "free", "uptime", "find", "xargs", "tee", "touch", "mkdir", "rmdir", "cp", "mv", "rm", "ln", "chmod", "chown", "diff", "patch", "tar", "gzip", "gunzip", "zip", "unzip", "ssh", "scp", "rsync", "curl", "wget", "ping", "dig", "nslookup", "git", "vim", "vi", "nano", "tree", "watch", "source", ".", ]; /// Classify a command against the corpus. pub fn classify_command( command_line: &str, corpus_cid: &str, corpus_base: &Path, ) -> CommandClass { let cmd_name = command_line.split_whitespace().next().unwrap_or(""); if cmd_name.is_empty() { return CommandClass::Free; } // Strip path prefix to get bare name: let bare_name = Path::new(cmd_name) .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| cmd_name.to_string()); // Free commands — zero overhead: if FREE_COMMANDS.contains(&bare_name.as_str()) { return CommandClass::Free; } // Ungoverned corpus — everything is free: if corpus_cid == "sha256:ungoverned" { return CommandClass::Free; } // Check corpus directory: let corpus_dir = corpus_base.join(corpus_cid); if !corpus_dir.exists() { // No corpus mounted — treat as ungoverned return CommandClass::Ungoverned; } let binary_path = corpus_dir.join(&bare_name); if binary_path.exists() { CommandClass::Governed { corpus_binary: binary_path, } } else { // Not in corpus dir — could be a PATH binary or truly missing // Check if it's on PATH: let on_path = std::process::Command::new("which") .arg(&bare_name) .output() .map(|o| o.status.success()) .unwrap_or(false); if on_path { CommandClass::Ungoverned } else { CommandClass::Denied { reason: format!("'{}' not found in corpus or PATH", bare_name), } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_free_commands() { let base = Path::new("/nonexistent"); assert!(matches!( classify_command("ls -la", "sha256:test", base), CommandClass::Free )); assert!(matches!( classify_command("echo hello", "sha256:test", base), CommandClass::Free )); assert!(matches!( classify_command("cat /etc/hosts", "sha256:test", base), CommandClass::Free )); } #[test] fn test_ungoverned_corpus_is_free() { let base = Path::new("/nonexistent"); assert!(matches!( classify_command("kubectl get pods", "sha256:ungoverned", base), CommandClass::Free )); } #[test] fn test_governed_binary() { let dir = tempfile::tempdir().unwrap(); let cid = "sha256:test-corpus"; let corpus_dir = dir.path().join(cid); std::fs::create_dir_all(&corpus_dir).unwrap(); std::fs::write(corpus_dir.join("my-tool"), "#!/bin/bash\necho ok").unwrap(); assert!(matches!( classify_command("my-tool --flag", cid, dir.path()), CommandClass::Governed { .. } )); } #[test] fn test_empty_command() { assert!(matches!( classify_command("", "sha256:test", Path::new("/")), CommandClass::Free )); } }