use std::collections::BTreeMap;
use crate::config::schema::{NetworkMode, PolicyExtraConfig};
use crate::sandbox::backend::{AGENT_BIN, AGENT_HOME, STATE_PATH, SandboxLaunch, WORKSPACE_PATH};
use crate::sandbox::runtime::{AgentRuntime, SANDBOX_PATH};
pub const AGENT_PROFILE: &str = "ai-agent";
pub const OFFLINE_PROFILE: &str = "untrusted";
pub const POLICY_FILENAME: &str = "policy.toml";
pub const RESOLV_FILENAME: &str = "resolv.conf";
const SYSTEM_READ: [&str; 17] = [
"/usr",
"/lib",
"/lib64",
"/bin",
"/sbin",
"/proc",
"/etc/ld.so.cache",
"/etc/ld.so.conf",
"/etc/ld.so.conf.d",
"/etc/ssl",
"/etc/ca-certificates.conf",
"/etc/ca-certificates",
"/var/lib/ca-certificates",
"/etc/pki",
"/etc/nsswitch.conf",
"/etc/services",
"/etc/protocols",
];
const SYSTEM_EXECUTE: [&str; 5] = ["/usr", "/lib", "/lib64", "/bin", "/sbin"];
pub const RESOLV_CONF: &str = "# Generated by errand. The host's own resolver is not shown to a session.\n\
nameserver 1.1.1.1\n\
nameserver 1.0.0.1\n\
options edns0";
fn quoted(name: &str) -> String {
serde_json::to_string(name).expect("a name always quotes")
}
fn placed(host: &str, at: &str) -> String {
format!(
"{{ path = {host_quoted}, at = {at_quoted} }}",
host_quoted = quoted(host),
at_quoted = quoted(at)
)
}
pub fn policy_path(launch: &SandboxLaunch) -> String {
std::path::Path::new(&launch.state_dir)
.join(POLICY_FILENAME)
.to_string_lossy()
.into_owned()
}
#[derive(Debug, Clone)]
pub struct PolicyOptions<'a> {
pub launch: &'a SandboxLaunch,
pub network: NetworkMode,
pub egress_ports: Option<&'a [u16]>,
pub runtime: &'a AgentRuntime,
pub file_max: &'a str,
pub resolv_conf: &'a str,
pub extra: Option<&'a PolicyExtraConfig>,
pub env: Option<&'a BTreeMap<String, String>>,
pub path_extra: Option<&'a [String]>,
}
pub fn policy_contents(options: &PolicyOptions) -> String {
let launch = options.launch;
let empty = PolicyExtraConfig {
read: Vec::new(),
write: Vec::new(),
execute: Vec::new(),
};
let extra = options.extra.unwrap_or(&empty);
let no_env = BTreeMap::new();
let operator_env = options.env.unwrap_or(&no_env);
let mut read: Vec<String> = SYSTEM_READ.iter().map(|path| quoted(path)).collect();
read.push(placed(options.resolv_conf, "/etc/resolv.conf"));
read.push(placed(&launch.project_path, WORKSPACE_PATH));
read.push(placed(&launch.state_dir, STATE_PATH));
read.extend(options.runtime.read_paths.iter().map(|path| quoted(path)));
read.extend(extra.read.iter().map(|path| quoted(path)));
let mut path: Vec<String> = vec![AGENT_BIN.to_owned()];
path.extend(options.runtime.path_entries.iter().cloned());
if let Some(path_extra) = options.path_extra {
path.extend(path_extra.iter().cloned());
}
path.extend(SANDBOX_PATH.iter().map(|entry| (*entry).to_owned()));
let mut execute_set: Vec<String> = SYSTEM_EXECUTE
.iter()
.map(|entry| (*entry).to_owned())
.collect();
execute_set.push(AGENT_BIN.to_owned());
execute_set.extend(options.runtime.path_entries.iter().cloned());
execute_set.extend(options.runtime.read_paths.iter().cloned());
execute_set.extend(extra.execute.iter().cloned());
let mut seen_execute = std::collections::HashSet::new();
let execute: Vec<String> = execute_set
.into_iter()
.filter(|entry| seen_execute.insert(entry.clone()))
.map(|entry| quoted(&entry))
.collect();
let pass: Vec<String> = {
let mut names: Vec<String> = launch.env.keys().cloned().collect();
names.sort();
names.iter().map(|name| quoted(name)).collect()
};
let mut set = vec![format!("PATH = {}", quoted(&path.join(":")))];
set.push(format!("HOME = {}", quoted(AGENT_HOME)));
set.extend(
operator_env
.iter()
.filter(|(name, _)| !launch.env.contains_key(*name))
.map(|(name, value)| format!("{name} = {}", quoted(value))),
);
let write: Vec<String> = [
placed(&launch.project_path, WORKSPACE_PATH),
placed(&launch.state_dir, STATE_PATH),
]
.into_iter()
.chain(extra.write.iter().map(|path| quoted(path)))
.collect();
let mut lines = vec![
"# Generated per session by errand. Do not edit.".to_owned(),
"[filesystem]".to_owned(),
"reset = true".to_owned(),
format!("read = [{}]", read.join(", ")),
format!("write = [{}]", write.join(", ")),
format!("execute = [{}]", execute.join(", ")),
String::new(),
"[env]".to_owned(),
format!("pass = [{}]", pass.join(", ")),
format!("set = {{ {} }}", set.join(", ")),
String::new(),
"[resources]".to_owned(),
format!("file_max = {}", quoted(options.file_max)),
];
if options.network != NetworkMode::None {
let open_ports: &[u16] = match options.egress_ports {
Some(ports) if !ports.is_empty() => ports,
_ => &[443],
};
let allow = open_ports
.iter()
.map(|port| format!("{{ host = \"*\", port = {port} }}"))
.collect::<Vec<_>>()
.join(", ");
lines.push(String::new());
lines.push("[network]".to_owned());
lines.push(format!("egress_allow = [{allow}]"));
}
format!("{}\n", lines.join("\n"))
}
#[cfg(test)]
mod tests;