#[cfg(any(target_os = "linux", all(test, unix)))]
pub mod bwrap;
pub mod claude;
pub mod codex;
pub mod cursor;
pub mod install;
pub mod landlock;
mod patterns;
pub mod seatbelt;
#[cfg(any(target_os = "linux", test))]
pub mod seccomp;
use openmoat_core::ir::{Access, Allowance, Effect, Enforcement, Loss};
use openmoat_core::{EvalContext, Policy, PolicyError};
use serde::Serialize;
pub const PROJECT: &str = "/__moat_project__";
pub fn proxy_port(policy: &Policy) -> Option<u16> {
policy
.sandbox
.as_ref()
.and_then(|s| s.proxy_port)
.map(std::num::NonZeroU16::get)
}
pub fn lower_for_hosts(
policy: &Policy,
home: &str,
real_home: Option<String>,
moved_dirs: Vec<(String, String)>,
case_insensitive_paths: bool,
) -> Result<Enforcement, PolicyError> {
openmoat_core::ir::lower(
policy,
&EvalContext {
home: home.to_owned(),
project: Some(PROJECT.to_owned()),
real_home,
real_project: None,
moved_dirs,
cwd: PROJECT.to_owned(),
case_insensitive_paths,
},
)
}
#[derive(Debug)]
pub struct Plan {
pub default_read_roots: bool,
pub proxy_port: Option<u16>,
pub claude: claude::Generated,
pub codex: codex::Generated,
pub cursor: cursor::Generated,
}
fn with_read_roots(policy: &Policy) -> anyhow::Result<(Policy, bool)> {
let mut policy = policy.clone();
let filled = policy.sandbox.is_none();
if filled {
policy.sandbox = Policy::parse(openmoat_core::DEFAULT_POLICY)?.sandbox;
}
Ok((policy, filled))
}
#[derive(Debug, Clone, Default)]
pub struct Grants {
pub proxy_port: Option<u16>,
pub tmpdir: Option<String>,
pub program: Option<String>,
pub writes: Vec<String>,
}
fn lower_for_session(
policy: &Policy,
ctx: &EvalContext,
grants: Grants,
) -> anyhow::Result<(Enforcement, Grants)> {
let (policy, _) = with_read_roots(policy)?;
let tmpdir = std::fs::canonicalize(std::env::temp_dir())
.ok()
.map(|dir| crate::context::path_string(&dir));
Ok((
openmoat_core::ir::lower(&policy, ctx)?,
Grants { tmpdir, ..grants },
))
}
pub fn seatbelt_profile(
policy: &Policy,
ctx: &EvalContext,
grants: Grants,
) -> anyhow::Result<seatbelt::Generated> {
let (ir, grants) = lower_for_session(policy, ctx, grants)?;
Ok(seatbelt::generate(&ir, &grants))
}
pub fn landlock_rules(
policy: &Policy,
ctx: &EvalContext,
grants: Grants,
) -> anyhow::Result<landlock::Generated> {
let (ir, grants) = lower_for_session(policy, ctx, grants)?;
landlock::generate(&ir, &grants)
}
#[cfg(target_os = "linux")]
pub fn bwrap_mounts(
policy: &Policy,
ctx: &EvalContext,
grants: Grants,
) -> anyhow::Result<bwrap::Generated> {
let (ir, grants) = lower_for_session(policy, ctx, grants)?;
let project = ctx
.real_project
.iter()
.chain(&ctx.project)
.cloned()
.collect::<Vec<_>>();
bwrap::generate(&ir, &grants, &project)
}
pub fn codex_config_path() -> anyhow::Result<std::path::PathBuf> {
let hooks = crate::install::HostConfig::for_host(openmoat_hosts::Host::Codex)?.settings_path;
Ok(codex_config_beside(&hooks))
}
pub fn codex_config_beside(hooks: &std::path::Path) -> std::path::PathBuf {
hooks.with_file_name("config.toml")
}
impl Plan {
pub fn new(policy: &Policy) -> anyhow::Result<Self> {
let (policy, default_read_roots) = with_read_roots(policy)?;
let (home, real_home) = crate::context::home_spellings()?;
let homes: Vec<String> = std::iter::once(home.clone())
.chain(real_home.clone())
.collect();
let ir = lower_for_hosts(
&policy,
&home,
real_home,
crate::context::moved_dirs()?,
crate::context::CASE_INSENSITIVE_PATHS,
)?;
let proxy_port = proxy_port(&policy);
let mut claude = claude::generate(&ir, proxy_port, cfg!(target_os = "linux"))?;
let claude_settings = install::settings_path(openmoat_hosts::Host::ClaudeCode)?;
claude::protect(&mut claude, &claude_settings);
let mut codex = codex::generate(&ir, &homes, proxy_port)?;
if let Some(codex_home) = codex_config_path()?.parent() {
codex::protect(&mut codex, codex_home);
}
if cfg!(target_os = "linux") {
codex::read_own_binary(&mut codex, codex::own_binary().as_deref());
codex::deny_names(&mut codex);
}
Ok(Self {
default_read_roots,
proxy_port,
claude,
codex,
cursor: cursor::generate(&ir, proxy_port)?,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Report {
pub losses: Vec<Loss>,
pub allowances: Vec<Allowance>,
}
impl Report {
fn loss(&mut self, kind: openmoat_core::Kind, rule: &str, message: String) {
let loss = Loss {
kind,
rule: rule.to_owned(),
message,
};
if !self.losses.contains(&loss) {
self.losses.push(loss);
}
}
fn allowance(
&mut self,
kind: openmoat_core::Kind,
rule: &str,
patterns: Vec<String>,
message: &str,
) {
self.allowances.push(Allowance {
kind,
rule: rule.to_owned(),
patterns,
message: message.to_owned(),
});
}
fn proxy_only(&mut self, net: &Access) {
if net.default == Effect::Allow {
let message = "every host is reachable only through OpenMoat's proxy";
self.loss(openmoat_core::Kind::Net, "default.net", message.into());
}
for rule in &net.allow {
let message = "these hosts are reachable only through OpenMoat's proxy: a program that \
ignores HTTP(S)_PROXY cannot reach them";
self.loss(openmoat_core::Kind::Net, &rule.id, message.into());
}
}
}
#[cfg(test)]
pub fn assert_golden(name: &str, actual: &str) {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/sandbox")
.join(name);
if std::env::var_os("MOAT_UPDATE_GOLDEN").is_some() {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, actual).unwrap();
}
let expected = std::fs::read_to_string(&path).unwrap_or_default();
assert_eq!(
actual,
expected,
"MOAT_UPDATE_GOLDEN=1 updates {}",
path.display()
);
}