2026-07-11 13:16:10 +07:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum Verdict {
|
|
|
|
|
Allow,
|
|
|
|
|
Block(String),
|
|
|
|
|
Escalate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Verdict {
|
|
|
|
|
pub fn is_allowed(&self) -> bool {
|
|
|
|
|
matches!(self, Verdict::Allow)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Harness;
|
|
|
|
|
|
|
|
|
|
impl Harness {
|
|
|
|
|
pub fn classify(_cmd: &str, mode: &super::state::types::AgentMode) -> Verdict {
|
|
|
|
|
if mode.auto_approve() {
|
|
|
|
|
return Verdict::Allow;
|
|
|
|
|
}
|
2026-07-11 20:21:59 +07:00
|
|
|
if matches!(mode, super::state::types::AgentMode::Plan) {
|
|
|
|
|
return Verdict::Block("mutating tools are disabled in Plan mode".to_string());
|
|
|
|
|
}
|
|
|
|
|
Verdict::Escalate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn gate_tool_call(
|
|
|
|
|
tool_name: &str,
|
|
|
|
|
args: &serde_json::Value,
|
|
|
|
|
mode: &super::state::types::AgentMode,
|
|
|
|
|
workspace_roots: &[&std::path::Path],
|
|
|
|
|
) -> Verdict {
|
|
|
|
|
if let Err(e) = Self::run_catastrophic_guard(tool_name, args, workspace_roots) {
|
|
|
|
|
return Verdict::Block(e);
|
|
|
|
|
}
|
|
|
|
|
if !crate::tool::tool_is_risky(tool_name) {
|
|
|
|
|
return Verdict::Allow;
|
|
|
|
|
}
|
|
|
|
|
Self::classify(tool_name, mode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run_catastrophic_guard(
|
|
|
|
|
tool_name: &str,
|
|
|
|
|
args: &serde_json::Value,
|
|
|
|
|
workspace_roots: &[&std::path::Path],
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
use super::catastrophic::CatastrophicGuard;
|
|
|
|
|
match tool_name {
|
|
|
|
|
"bash" => {
|
|
|
|
|
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
CatastrophicGuard::check_all(cmd, workspace_roots)
|
|
|
|
|
}
|
|
|
|
|
"git_operator" => {
|
|
|
|
|
let operation = args.get("operation").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
let arg_list: Vec<String> = args
|
|
|
|
|
.get("args")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let cmd = format!("git {} {}", operation, arg_list.join(" "));
|
|
|
|
|
CatastrophicGuard::check_all(&cmd, workspace_roots)
|
|
|
|
|
}
|
|
|
|
|
"delete" => {
|
|
|
|
|
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
CatastrophicGuard::check_delete_path(std::path::Path::new(path), workspace_roots)
|
|
|
|
|
}
|
|
|
|
|
"web_download" | "download" => {
|
|
|
|
|
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
CatastrophicGuard::check_download_path(std::path::Path::new(path))
|
|
|
|
|
}
|
|
|
|
|
_ => Ok(()),
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn parse_verdict(text: &str) -> Option<Verdict> {
|
|
|
|
|
let trimmed = text.trim();
|
|
|
|
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(trimmed) {
|
|
|
|
|
if let Some(verdict) = v.get("verdict").and_then(|v| v.as_str()) {
|
|
|
|
|
return match verdict.to_lowercase().as_str() {
|
|
|
|
|
"allow" => Some(Verdict::Allow),
|
|
|
|
|
"block" => Some(Verdict::Block(
|
|
|
|
|
v.get("reason").and_then(|r| r.as_str()).unwrap_or("blocked").to_string()
|
|
|
|
|
)),
|
|
|
|
|
"escalate" => Some(Verdict::Escalate),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for line in trimmed.lines() {
|
|
|
|
|
let l = line.trim().to_lowercase();
|
|
|
|
|
if l.starts_with("verdict: allow") {
|
|
|
|
|
return Some(Verdict::Allow);
|
|
|
|
|
}
|
|
|
|
|
if l.starts_with("verdict: block") {
|
|
|
|
|
let reason = line.split_once(':').map(|x| x.1).unwrap_or("blocked").trim().to_string();
|
|
|
|
|
return Some(Verdict::Block(reason));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if trimmed.to_lowercase().contains("allow") {
|
|
|
|
|
return Some(Verdict::Allow);
|
|
|
|
|
}
|
|
|
|
|
if trimmed.to_lowercase().contains("block") {
|
|
|
|
|
return Some(Verdict::Block("blocked by classifier".to_string()));
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn classify(_cmd: &str, mode: &super::state::types::AgentMode) -> Verdict {
|
|
|
|
|
Harness::classify(_cmd, mode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Harness {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Harness
|
|
|
|
|
}
|
|
|
|
|
}
|