use nono::CapabilitySet;
use nono::Result;
#[cfg(target_os = "macos")]
use std::path::Path;
#[cfg(target_os = "macos")]
pub fn write_protect_verified_files(
caps: &mut CapabilitySet,
verified_paths: &[std::path::PathBuf],
) -> Result<()> {
for path in verified_paths {
add_literal_write_deny(caps, path)?;
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn write_protect_verified_files(
_caps: &mut CapabilitySet,
_verified_paths: &[std::path::PathBuf],
) -> Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
fn add_literal_write_deny(caps: &mut CapabilitySet, path: &Path) -> Result<()> {
let path_str = path.display().to_string();
validate_seatbelt_path(&path_str)?;
let deny_rule = format!("(deny file-write-data (literal \"{path_str}\"))");
caps.add_platform_rule(deny_rule)?;
if let Ok(canonical) = std::fs::canonicalize(path) {
if canonical != path {
let canonical_str = canonical.display().to_string();
validate_seatbelt_path(&canonical_str)?;
let canonical_rule = format!("(deny file-write-data (literal \"{canonical_str}\"))");
caps.add_platform_rule(canonical_rule)?;
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn validate_seatbelt_path(path_str: &str) -> Result<()> {
if path_str.contains('"') || path_str.contains('\\') {
return Err(nono::NonoError::ConfigParse(format!(
"path contains characters not permitted in Seatbelt rules: {path_str}"
)));
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn write_protect_with_no_paths_is_noop() {
let mut caps = CapabilitySet::new();
write_protect_verified_files(&mut caps, &[]).unwrap();
assert!(caps.platform_rules().is_empty());
}
#[cfg(target_os = "macos")]
#[test]
fn write_protect_adds_deny_write_rule() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("SKILLS.md");
std::fs::write(&file, "content").unwrap();
let mut caps = CapabilitySet::new();
write_protect_verified_files(&mut caps, std::slice::from_ref(&file)).unwrap();
let rules = caps.platform_rules();
assert!(!rules.is_empty());
assert!(
rules
.iter()
.any(|r| r.contains("deny file-write-data")
&& r.contains(&file.display().to_string()))
);
assert!(!rules.iter().any(|r| r.contains("deny file-read-data")));
}
#[cfg(target_os = "macos")]
#[test]
fn write_protect_rejects_path_with_quote() {
let mut caps = CapabilitySet::new();
let bad_path =
std::path::PathBuf::from("/tmp/SKILLS\") (allow file-write* (subpath \"/\")) ;.md");
let result = write_protect_verified_files(&mut caps, &[bad_path]);
assert!(result.is_err());
}
#[cfg(target_os = "macos")]
#[test]
fn write_protect_rejects_path_with_backslash() {
let mut caps = CapabilitySet::new();
let bad_path = std::path::PathBuf::from("/tmp/SKILLS\\.md");
let result = write_protect_verified_files(&mut caps, &[bad_path]);
assert!(result.is_err());
}
}