#![deny(missing_docs)]
#[cfg(target_os = "linux")]
pub mod bwrap;
#[cfg(target_os = "linux")]
pub mod bwrap_proxy;
pub mod defaults;
pub mod fs;
pub mod ipc;
pub mod monitor;
pub mod path_defense;
pub mod policy;
pub mod policy_check;
#[cfg(unix)]
pub mod pool;
pub mod proxy;
pub mod rlimits;
#[cfg(target_os = "macos")]
pub mod seatbelt;
#[cfg(target_os = "macos")]
pub mod seatbelt_cache;
#[cfg(target_os = "linux")]
pub mod stage2;
pub mod violations;
pub mod worker;
#[cfg(unix)]
pub mod worker_client;
pub mod workspace;
pub use path_defense::{
find_symlink_in_path, is_dangerous_system_path, is_path_inside, paths_for_write_check,
resolve_deepest_existing_ancestor,
};
pub use policy::{
DomainPattern, FsPolicy, MitmConfig, NetPolicy, PathPattern, ResourceLimits, SandboxPolicy,
TrustPreference,
};
pub use policy_check::is_fully_denied;
pub use proxy::{
BuiltInProxy, BuiltInSocks5Proxy, DEFAULT_DEV_ALLOWLIST, DEFAULT_NO_PROXY, ExternalProxy,
Filter, PROXY_PORT_ENV_KEY, ProxyHandle, Socks5Server, ca_bundle_for_policy, proxy_env_vars,
socks5_env_vars,
};
pub use violations::{
DEFAULT_RING_CAPACITY, SandboxViolationStore, Violation, ViolationKind, global_store,
render_block,
};
pub use workspace::{CwdProvider, GitWorktreeProvider, WorkspaceProvider};
#[cfg(target_os = "macos")]
pub use workspace::ClonefileProvider;
use anyhow::Result;
use std::path::Path;
use tokio::process::Command;
#[derive(Debug)]
pub struct SandboxTransformRequest<'a> {
pub command: &'a str,
pub project_root: &'a Path,
pub policy: &'a SandboxPolicy,
pub proxy_port: Option<u16>,
}
#[derive(Debug)]
pub struct SandboxExecRequest {
pub command: Command,
}
#[derive(Debug, Clone)]
pub struct DependencyReport {
pub backend: &'static str,
pub available: bool,
pub reason: Option<String>,
}
pub trait SandboxRuntime: Send + Sync {
fn transform(&self, req: SandboxTransformRequest<'_>) -> Result<SandboxExecRequest>;
fn check_dependencies(&self) -> DependencyReport;
}
#[cfg(target_os = "macos")]
#[derive(Debug, Default)]
pub struct SeatbeltRuntime;
#[cfg(target_os = "macos")]
impl SandboxRuntime for SeatbeltRuntime {
fn transform(&self, req: SandboxTransformRequest<'_>) -> Result<SandboxExecRequest> {
let command = match req.proxy_port {
Some(port) => seatbelt::build_command_with_proxy(
req.command,
req.project_root,
req.policy,
port,
req.policy.net.allow_local_binding,
req.policy.net.weaker_macos_isolation,
)?,
None => seatbelt::build_command(req.command, req.project_root, req.policy)?,
};
Ok(SandboxExecRequest { command })
}
fn check_dependencies(&self) -> DependencyReport {
if seatbelt::is_available() {
DependencyReport {
backend: "seatbelt",
available: true,
reason: None,
}
} else {
DependencyReport {
backend: "seatbelt",
available: false,
reason: Some(
"sandbox-exec failed to run a probe command. Check macOS \
SIP / TCC restrictions."
.into(),
),
}
}
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Default)]
pub struct BwrapRuntime;
#[cfg(target_os = "linux")]
impl SandboxRuntime for BwrapRuntime {
fn transform(&self, req: SandboxTransformRequest<'_>) -> Result<SandboxExecRequest> {
let command = match try_build_kernel_enforced(&req) {
Ok(Some(cmd)) => cmd,
Ok(None) => {
if req.proxy_port.is_some() {
warn_proxy_unenforced_on_linux_once();
}
bwrap::build_command(req.command, req.project_root, req.policy)?
}
Err(e) => {
tracing::warn!(
"koda-sandbox: kernel-enforced egress setup failed ({e:#}); \
falling back to env-var enforcement."
);
warn_proxy_unenforced_on_linux_once();
bwrap::build_command(req.command, req.project_root, req.policy)?
}
};
Ok(SandboxExecRequest { command })
}
fn check_dependencies(&self) -> DependencyReport {
if bwrap::is_available() {
DependencyReport {
backend: "bwrap",
available: true,
reason: None,
}
} else {
DependencyReport {
backend: "bwrap",
available: false,
reason: Some(
"bwrap not installed or unable to create user namespaces. \
Install: apt install bubblewrap | dnf install bubblewrap"
.into(),
),
}
}
}
}
#[cfg(target_os = "linux")]
fn try_build_kernel_enforced(
req: &SandboxTransformRequest<'_>,
) -> Result<Option<tokio::process::Command>> {
use anyhow::Context as _;
let Some(port) = req.proxy_port else {
return Ok(None);
};
let uds = bwrap_proxy::proxy_uds_path(std::process::id(), port);
if !uds.exists() {
return Ok(None);
}
let stage2 = bwrap::stage2_binary().context("locate koda-sandbox-stage2")?;
let cmd = bwrap::build_command_with_proxy(
req.command,
req.project_root,
req.policy,
port,
&uds,
&stage2,
)?;
Ok(Some(cmd))
}
#[cfg(target_os = "linux")]
fn warn_proxy_unenforced_on_linux_once() {
use std::sync::OnceLock;
static WARNED: OnceLock<()> = OnceLock::new();
WARNED.get_or_init(|| {
tracing::warn!(
"koda-sandbox: built-in proxy is active but the kernel-enforced \
egress path didn't activate for this session (UDS bridge or \
stage 2 helper unavailable). Well-behaved HTTP clients (curl, \
gh, npm, pip, cargo, go, node, python) honor HTTPS_PROXY and \
route through the filter; ill-behaved binaries can still reach \
the network directly."
);
});
}
#[derive(Debug, Default)]
pub struct UnsandboxedRuntime;
impl SandboxRuntime for UnsandboxedRuntime {
fn transform(&self, req: SandboxTransformRequest<'_>) -> Result<SandboxExecRequest> {
let mut command = Command::new("sh");
command
.arg("-c")
.arg(req.command)
.current_dir(req.project_root);
Ok(SandboxExecRequest { command })
}
fn check_dependencies(&self) -> DependencyReport {
DependencyReport {
backend: "none",
available: false,
reason: Some(
"No kernel sandbox backend available on this platform. \
Commands run unsandboxed."
.into(),
),
}
}
}
#[must_use]
pub fn current_runtime() -> Box<dyn SandboxRuntime> {
#[cfg(target_os = "macos")]
{
Box::new(SeatbeltRuntime)
}
#[cfg(target_os = "linux")]
{
if bwrap::is_available() {
Box::new(BwrapRuntime)
} else {
Box::new(UnsandboxedRuntime)
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Box::new(UnsandboxedRuntime)
}
}
#[must_use]
pub fn is_available() -> bool {
current_runtime().check_dependencies().available
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsandboxed_runtime_produces_sh_command() {
let policy = SandboxPolicy::default();
let req = SandboxTransformRequest {
command: "echo hi",
project_root: Path::new("/tmp"),
policy: &policy,
proxy_port: None,
};
let runtime = UnsandboxedRuntime;
let result = runtime.transform(req).unwrap();
let program = result.command.as_std().get_program();
assert_eq!(program, "sh");
}
#[test]
fn unsandboxed_runtime_reports_unavailable() {
let report = UnsandboxedRuntime.check_dependencies();
assert_eq!(report.backend, "none");
assert!(!report.available);
assert!(report.reason.is_some());
}
#[test]
fn current_runtime_is_constructible() {
let runtime = current_runtime();
let _report = runtime.check_dependencies(); }
#[test]
fn unsandboxed_runtime_ignores_proxy_port() {
let policy = SandboxPolicy::default();
let with = UnsandboxedRuntime
.transform(SandboxTransformRequest {
command: "true",
project_root: Path::new("/tmp"),
policy: &policy,
proxy_port: Some(8080),
})
.unwrap();
let without = UnsandboxedRuntime
.transform(SandboxTransformRequest {
command: "true",
project_root: Path::new("/tmp"),
policy: &policy,
proxy_port: None,
})
.unwrap();
assert_eq!(with.command.as_std().get_program(), "sh");
assert_eq!(without.command.as_std().get_program(), "sh");
assert_eq!(
with.command.as_std().get_args().collect::<Vec<_>>(),
without.command.as_std().get_args().collect::<Vec<_>>(),
);
}
#[cfg(target_os = "macos")]
#[test]
fn seatbelt_runtime_emits_proxied_profile_when_port_set() {
let policy = SandboxPolicy::strict_default();
let req = SandboxTransformRequest {
command: "true",
project_root: Path::new("/tmp"),
policy: &policy,
proxy_port: Some(54321),
};
let cmd = SeatbeltRuntime.transform(req).unwrap().command;
let profile = cmd
.as_std()
.get_args()
.nth(1) .and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();
assert!(
profile.contains("localhost:54321"),
"proxied profile must allow loopback proxy port via localhost; got: {profile}"
);
assert!(
!profile.contains("(allow network*)"),
"proxied profile must NOT include the open-network allow; got: {profile}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn seatbelt_runtime_keeps_open_network_when_port_none() {
let policy = SandboxPolicy::strict_default();
let req = SandboxTransformRequest {
command: "true",
project_root: Path::new("/tmp"),
policy: &policy,
proxy_port: None,
};
let cmd = SeatbeltRuntime.transform(req).unwrap().command;
let profile = cmd
.as_std()
.get_args()
.nth(1)
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();
assert!(
profile.contains("(allow network*)"),
"open-network profile must include the blanket allow; got: {profile}"
);
assert!(
!profile.contains("localhost:") || !profile.contains("network-outbound"),
"open-network profile must NOT include the proxied loopback outbound rule; got: {profile}"
);
}
#[cfg(target_os = "linux")]
#[test]
fn bwrap_runtime_accepts_proxy_port_without_error() {
let policy = SandboxPolicy::strict_default();
let req = SandboxTransformRequest {
command: "true",
project_root: Path::new("/tmp"),
policy: &policy,
proxy_port: Some(54321),
};
let _ = BwrapRuntime.transform(req);
}
}