use harn_vm::orchestration::{CapabilityPolicy, SandboxProfile};
use super::policy::PermissionPolicy;
#[cfg(feature = "hostlib")]
pub use harn_hostlib::sandbox;
impl PermissionPolicy {
fn required_side_effect_level(&self) -> &'static str {
if !self.net.is_empty() {
"network"
} else if !self.exec.is_empty() {
"process_exec"
} else if !self.write.is_empty() {
"workspace_write"
} else if !self.read.is_empty() {
"read_only"
} else {
"none"
}
}
pub fn to_capability_policy(
&self,
sandbox_profile: SandboxProfile,
workspace_roots: Vec<String>,
) -> CapabilityPolicy {
let mut capabilities = std::collections::BTreeMap::new();
let mut workspace = Vec::new();
if !self.read.is_empty() {
workspace.extend(["read_text", "list", "exists"].map(String::from));
}
if !self.write.is_empty() {
workspace.extend(["write_text", "delete"].map(String::from));
}
if !workspace.is_empty() {
capabilities.insert("workspace".to_string(), workspace);
}
if !self.exec.is_empty() {
capabilities.insert("process".to_string(), vec!["exec".to_string()]);
}
CapabilityPolicy {
capabilities,
workspace_roots,
side_effect_level: Some(self.required_side_effect_level().to_string()),
sandbox_profile,
..Default::default()
}
}
#[cfg(feature = "hostlib")]
pub fn to_network_policy(&self) -> sandbox::NetworkPolicy {
if self.net.iter().any(|host| host == "*" || host == "**") {
sandbox::NetworkPolicy::Unrestricted
} else {
sandbox::NetworkPolicy::Limited {
allowed_hosts: self.net.clone(),
}
}
}
#[cfg(feature = "hostlib")]
pub fn to_sandbox_spec(&self) -> sandbox::SandboxSpec {
sandbox::SandboxSpec {
network_policy: self.to_network_policy(),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_dev_lowers_to_process_exec_capability_policy() {
let policy = PermissionPolicy::local_dev();
let cap =
policy.to_capability_policy(SandboxProfile::OsHardened, vec!["/work".to_string()]);
assert_eq!(cap.side_effect_level.as_deref(), Some("process_exec"));
assert_eq!(cap.sandbox_profile, SandboxProfile::OsHardened);
assert_eq!(cap.workspace_roots, vec!["/work".to_string()]);
assert_eq!(
cap.capabilities.get("process"),
Some(&vec!["exec".to_string()])
);
let workspace = cap.capabilities.get("workspace").expect("workspace caps");
assert!(workspace.contains(&"read_text".to_string()));
assert!(workspace.contains(&"write_text".to_string()));
}
#[test]
fn read_only_policy_grants_only_read_capabilities() {
let policy = PermissionPolicy {
read: vec!["src/**".to_string()],
..PermissionPolicy::empty()
};
let cap = policy.to_capability_policy(SandboxProfile::Worktree, Vec::new());
assert_eq!(cap.side_effect_level.as_deref(), Some("read_only"));
assert_eq!(cap.capabilities.get("process"), None);
let workspace = cap.capabilities.get("workspace").expect("workspace caps");
assert!(workspace.contains(&"read_text".to_string()));
assert!(!workspace.contains(&"write_text".to_string()));
}
#[test]
fn empty_policy_lowers_to_none_level_with_no_capabilities() {
let cap =
PermissionPolicy::empty().to_capability_policy(SandboxProfile::Worktree, Vec::new());
assert_eq!(cap.side_effect_level.as_deref(), Some("none"));
assert!(cap.capabilities.is_empty());
}
#[cfg(feature = "hostlib")]
#[test]
fn net_allowlist_lowers_to_egress_policy() {
use sandbox::NetworkPolicy;
let deny_all = PermissionPolicy::empty().to_network_policy();
assert_eq!(
deny_all,
NetworkPolicy::Limited {
allowed_hosts: Vec::new()
}
);
let allowlist = PermissionPolicy {
net: vec!["api.github.com".to_string()],
..PermissionPolicy::empty()
}
.to_network_policy();
assert_eq!(
allowlist,
NetworkPolicy::Limited {
allowed_hosts: vec!["api.github.com".to_string()]
}
);
let wildcard = PermissionPolicy {
net: vec!["*".to_string()],
..PermissionPolicy::empty()
}
.to_network_policy();
assert_eq!(wildcard, NetworkPolicy::Unrestricted);
}
#[cfg(feature = "hostlib")]
#[test]
fn sandbox_spec_carries_egress_policy() {
let spec = PermissionPolicy {
net: vec!["api.github.com".to_string()],
..PermissionPolicy::empty()
}
.to_sandbox_spec();
assert_eq!(
spec.network_policy,
sandbox::NetworkPolicy::Limited {
allowed_hosts: vec!["api.github.com".to_string()]
}
);
}
}