use openmoat_core::Action;
use crate::realpath::{FsPathResolver, Resolution};
#[must_use]
pub fn approvable(action: &Action, cwd: Option<&str>, home: &str) -> Option<Action> {
let absolute = |p: &String| openmoat_core::resolve_path(p, home, None, cwd);
let all = |paths: &[String]| {
let all: Vec<String> = paths.iter().map(absolute).collect::<Option<_>>()?;
(!all.is_empty()).then_some(all)
};
Some(match action {
Action::Shell { command } => Action::Shell {
command: command.trim().to_owned(),
},
Action::FsRead { path } => Action::FsRead {
path: absolute(path)?,
},
Action::FsWrite { path } => Action::FsWrite {
path: absolute(path)?,
},
Action::Patch { writes } => Action::Patch {
writes: all(writes)?,
},
Action::ReadFiles { paths } => Action::ReadFiles { paths: all(paths)? },
_ => return None,
})
}
#[must_use]
pub fn files(action: &Action) -> (&[String], &[String]) {
match action {
Action::FsRead { path } => (std::slice::from_ref(path), &[]),
Action::ReadFiles { paths } => (paths, &[]),
Action::FsWrite { path } => (&[], std::slice::from_ref(path)),
Action::Patch { writes } => (&[], writes),
_ => (&[], &[]),
}
}
pub(super) fn real_files(action: &Action) -> Option<Vec<String>> {
let (reads, writes) = files(action);
let real: Vec<String> = reads
.iter()
.chain(writes)
.map(|p| match FsPathResolver::resolution(p) {
Resolution::Itself => Some(p.clone()),
Resolution::Elsewhere(real) => Some(real),
Resolution::Unresolvable => None,
})
.collect::<Option<_>>()?;
let same = real.iter().eq(reads.iter().chain(writes));
Some(if same { Vec::new() } else { real })
}
#[must_use]
pub fn describe(action: &Action) -> String {
if let Action::Shell { command } = action {
return format!("run \"{command}\"");
}
let (reads, writes) = files(action);
let verb = if writes.is_empty() { "read" } else { "write" };
format!("{verb} {}", [reads, writes].concat().join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::approvals::{Grants, Overlay, Scope};
const NOW: i64 = 1_790_985_600_000;
#[test]
fn file_grants_match_the_exact_absolute_paths() {
let read = |path: &str, cwd| {
let action = Action::FsRead {
path: path.to_owned(),
};
approvable(&action, cwd, "/h")
};
let reviewed = read("src/../notes.md", Some("/p")).unwrap();
assert_eq!(describe(&reviewed), "read /p/notes.md");
assert_eq!(read("~/notes.md", None), read("/h/notes.md", None));
assert_eq!(read("notes.md", None), None, "relative without a cwd");
let mut g = Grants::default();
let c1 = Scope {
host: "cursor",
session_id: "c1",
project: "/p",
};
g.grant(c1, &reviewed, NOW, None);
assert!(g.matches(c1, &read("/p/notes.md", None).unwrap(), NOW));
assert!(!g.matches(c1, &read("/p/other.md", None).unwrap(), NOW));
let write = Action::FsWrite {
path: "/p/notes.md".to_owned(),
};
assert!(!g.matches(c1, &write, NOW), "a read is not a write");
let mut o = Overlay::default();
assert!(!o.covers(&reviewed));
let rule = o.allow_files(&["/p/notes.md".to_owned()], &[]).unwrap();
assert_eq!(rule.fs_read, ["/p/notes.md"]);
assert_eq!(rule.fs_write, Vec::<String>::new());
assert!(o.covers(&reviewed) && !o.covers(&write));
assert!(o.allow_files(&[], &["/p/*.md".to_owned()]).is_err());
}
}