use super::root::Root;
use super::wire::Request;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Effect {
Read,
TargetWrite,
Process,
OpenWorld,
Destructive,
Secret,
}
impl Effect {
pub(crate) fn word(self) -> &'static str {
match self {
Effect::Read => "read",
Effect::TargetWrite => "target write",
Effect::Process => "process",
Effect::OpenWorld => "open-world",
Effect::Destructive => "destructive",
Effect::Secret => "secret",
}
}
pub fn of(word: &str) -> Option<Effect> {
[
Effect::Read,
Effect::TargetWrite,
Effect::Process,
Effect::OpenWorld,
Effect::Destructive,
Effect::Secret,
]
.into_iter()
.find(|e| e.word().replace(' ', "-") == word)
}
#[must_use]
pub fn worst(self, other: Effect) -> Effect {
if other > self { other } else { self }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Classified {
pub effect: Effect,
pub why: String,
}
impl Classified {
fn new(effect: Effect, why: impl Into<String>) -> Self {
Self {
effect,
why: why.into(),
}
}
}
const READ_FILE: &str = "read_file";
const LOAD_SKILL: &str = "load_skill";
const MESSAGE: &str = "message";
const DISPATCH: &str = "dispatch";
const MULTI_TOOL: &str = "multi_tool";
const APPLY_PATCH: &str = "apply_patch";
const CD: &str = "cd";
const BASH: &str = "bash";
pub fn classify(request: &Request, root: &Root, policy: &super::policy::Policy) -> Classified {
match request.name.as_str() {
READ_FILE => Classified::new(Effect::Read, "reads a file"),
LOAD_SKILL => Classified::new(
Effect::TargetWrite,
"writes a skill body into the agent worktree",
),
MESSAGE => Classified::new(
Effect::TargetWrite,
"deposits into another agent's inbox through the world's own gated verb",
),
DISPATCH => Classified::new(
Effect::Process,
"mints an agent, under the harness's own budget and depth gates",
),
MULTI_TOOL => Classified::new(
Effect::Read,
"an envelope whose every inner invocation is adjudicated on its own",
),
APPLY_PATCH => patch(&request.field("input"), root),
CD => move_to(&request.field("path"), root),
BASH => super::bash::classify(&request.field("command"), root, policy),
other => Classified::new(
Effect::OpenWorld,
format!("{other} is not a tool this control can classify"),
),
}
}
fn move_to(path: &str, root: &Root) -> Classified {
let dest = root.resolve(path);
if root.holds(&dest) {
Classified::new(
Effect::Read,
format!("moves to {} inside the writable root", dest.display()),
)
} else {
Classified::new(
Effect::OpenWorld,
format!("moves to {}, outside the writable root", dest.display()),
)
}
}
fn patch(envelope: &str, root: &Root) -> Classified {
let paths = patch_paths(envelope);
if root.holds_all(&paths) {
Classified::new(
Effect::TargetWrite,
"patches files inside the writable root",
)
} else {
Classified::new(
Effect::OpenWorld,
format!(
"patches {}, outside the writable root",
outside(&paths, root)
),
)
}
}
fn outside(paths: &[String], root: &Root) -> String {
paths
.iter()
.find(|p| !root.holds(&root.resolve(p)))
.cloned()
.unwrap_or_default()
}
fn patch_paths(envelope: &str) -> Vec<String> {
const MARKERS: [&str; 4] = [
"*** Add File: ",
"*** Delete File: ",
"*** Update File: ",
"*** Move to: ",
];
envelope
.lines()
.filter_map(|line| {
MARKERS
.iter()
.find_map(|m| line.trim_end().strip_prefix(m))
.map(|p| p.trim().to_owned())
})
.filter(|p| !p.is_empty())
.collect()
}
#[cfg(test)]
mod tests;