use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use agent_bridle::{
AxisEnforcement, ConfinedCommand, EnforcementFloor, Gate, Tool, ToolContext, ToolError,
};
use crate::caveats::{Caveats, Scope};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecOrigin {
AgentInfluenced,
TrustedInfra,
BrokerMediated,
}
impl ExecOrigin {
fn gate(self) -> Gate {
match self {
Self::AgentInfluenced => Gate::new(0).with_strength_floor(AxisEnforcement::Kernel),
Self::TrustedInfra => Gate::new(0),
Self::BrokerMediated => Gate::new(0).with_enforcement_floor(EnforcementFloor::CONFINED),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NetGrant {
#[default]
Unrestricted,
DenyAll,
}
#[derive(Debug, Clone)]
pub struct ExecRequest {
origin: ExecOrigin,
program: String,
args: Vec<String>,
cwd: PathBuf,
caveats: Caveats,
env: Vec<(String, String)>,
net_grant: NetGrant,
net_guard_bin: Option<PathBuf>,
timeout: Option<Duration>,
scratch_dirs: Vec<PathBuf>,
held_read_roots: Vec<agent_bridle::HeldReadRoot>,
}
impl ExecRequest {
#[must_use]
pub fn new(
origin: ExecOrigin,
program: impl Into<String>,
args: impl IntoIterator<Item = impl Into<String>>,
cwd: impl Into<PathBuf>,
caveats: Caveats,
) -> Self {
Self {
origin,
program: program.into(),
args: args.into_iter().map(Into::into).collect(),
cwd: cwd.into(),
caveats,
env: Vec::new(),
timeout: None,
net_grant: NetGrant::Unrestricted,
net_guard_bin: None,
scratch_dirs: Vec::new(),
held_read_roots: Vec::new(),
}
}
#[must_use]
pub fn held_read_roots(
mut self,
roots: impl IntoIterator<Item = agent_bridle::HeldReadRoot>,
) -> Self {
self.held_read_roots.extend(roots);
self
}
#[must_use]
pub fn with_build_git_reads(mut self, workspace: &Path) -> Self {
let (paths, held) = crate::git_hardening::build_git_reads(workspace);
if let Scope::Only(reads) = &mut self.caveats.fs_read {
reads.extend(paths);
self.held_read_roots.extend(held);
}
self
}
pub(crate) fn build_held_read_roots(&self) -> Vec<agent_bridle::HeldReadRoot> {
self.held_read_roots.clone()
}
#[must_use]
pub fn scratch_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.scratch_dirs.push(dir.into());
self
}
#[must_use]
pub fn net_grant(mut self, grant: NetGrant) -> Self {
self.net_grant = grant;
self
}
#[must_use]
pub fn net_guard_bin(mut self, path: impl Into<PathBuf>) -> Self {
self.net_guard_bin = Some(path.into());
self
}
#[must_use]
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
self.env.push((key.into(), val.into()));
self
}
#[must_use]
pub fn envs(mut self, vars: impl IntoIterator<Item = (String, String)>) -> Self {
self.env.extend(vars);
self
}
#[must_use]
pub fn env_grants(&self) -> &[(String, String)] {
&self.env
}
#[must_use]
pub fn caveats(&self) -> &Caveats {
&self.caveats
}
pub(crate) fn prepare_scratch(&self) -> std::io::Result<Vec<ScratchDir>> {
self.scratch_dirs
.iter()
.map(|dir| {
crate::scratch::validate_build_grant(dir, &self.caveats.fs_write)?;
ScratchDir::create(dir)
})
.collect()
}
}
#[derive(Debug)]
pub enum ExecRefused {
ConfinementUnenforceable(String),
Authorize(String),
Spawn(std::io::Error),
}
impl std::fmt::Display for ExecRefused {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConfinementUnenforceable(why) => write!(
f,
"refused (fail-closed): confinement cannot be kernel-enforced here — {why}"
),
Self::Authorize(why) => write!(f, "refused: {why}"),
Self::Spawn(e) => write!(f, "confined spawn failed: {e}"),
}
}
}
impl std::error::Error for ExecRefused {}
#[derive(Debug)]
pub struct ConfinedOutput {
pub success: bool,
pub code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub sandbox_kind: agent_bridle::SandboxKind,
pub timed_out: bool,
}
struct ExecSpawnTool;
#[async_trait::async_trait]
impl Tool for ExecSpawnTool {
fn name(&self) -> &str {
"confined_exec"
}
fn schema(&self) -> serde_json::Value {
serde_json::json!({})
}
async fn invoke(
&self,
_args: serde_json::Value,
_cx: &ToolContext,
) -> agent_bridle::ToolResult<serde_json::Value> {
Ok(serde_json::Value::Null)
}
}
fn mint_context(origin: ExecOrigin, caveats: &Caveats) -> Result<ToolContext, ExecRefused> {
origin
.gate()
.authorize(&ExecSpawnTool, caveats)
.map_err(|e| ExecRefused::Authorize(e.to_string()))
}
#[must_use]
pub fn kernel_fs_fence_available() -> bool {
#[cfg(target_os = "linux")]
{
agent_bridle::landlock_is_supported()
}
#[cfg(target_os = "macos")]
{
agent_bridle::seatbelt_is_supported()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
false
}
}
pub struct ConstrainedExecutor;
impl ConstrainedExecutor {
pub fn run(req: &ExecRequest) -> Result<ConfinedOutput, ExecRefused> {
Self::run_until_cancelled(req, None)
}
pub async fn run_async(req: ExecRequest) -> Result<ConfinedOutput, ExecRefused> {
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
struct CancelOnDrop(Arc<AtomicBool>);
impl Drop for CancelOnDrop {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
let cancel = CancelOnDrop(Arc::new(AtomicBool::new(false)));
let flag = Arc::clone(&cancel.0);
let result =
tokio::task::spawn_blocking(move || Self::run_until_cancelled(&req, Some(&flag)))
.await
.map_err(|error| ExecRefused::Spawn(std::io::Error::other(error)))?;
drop(cancel);
result
}
fn run_until_cancelled(
req: &ExecRequest,
cancel: Option<&std::sync::atomic::AtomicBool>,
) -> Result<ConfinedOutput, ExecRefused> {
if cancel.is_some_and(|flag| flag.load(std::sync::atomic::Ordering::Acquire)) {
return Err(ExecRefused::Spawn(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"confined execution cancelled before spawn",
)));
}
let _scratch = req.prepare_scratch().map_err(ExecRefused::Spawn)?;
#[cfg(target_os = "linux")]
let cgroup = if req.net_grant == NetGrant::DenyAll {
CgroupHandle::create()
} else {
None
};
#[cfg(target_os = "linux")]
let cgroup_procs = cgroup.as_ref().map(CgroupHandle::procs);
#[cfg(not(target_os = "linux"))]
let cgroup_procs: Option<PathBuf> = None;
let (program, args, caveats) = Self::resolve_net_floor(req, cgroup_procs.as_deref())?;
let cx = mint_context(req.origin, &caveats)?;
let mut cmd = ConfinedCommand::new(&program)
.sandbox_policy(std::sync::Arc::new(runtime_sandbox_policy()))
.args(&args)
.current_dir(&req.cwd)
.new_process_group()
.stdin(agent_bridle::ConfinedStdio::Null)
.stdout(agent_bridle::ConfinedStdio::Piped)
.stderr(agent_bridle::ConfinedStdio::Piped)
.held_read_roots(req.held_read_roots.iter().cloned());
for (k, v) in &req.env {
cmd = cmd.env(k, v);
}
let mut child = cmd.spawn(&cx).map_err(|e| match e {
ToolError::Denied { .. } => ExecRefused::ConfinementUnenforceable(e.to_string()),
other => ExecRefused::Authorize(other.to_string()),
})?;
let sandbox_kind = child.sandbox_kind;
let pgid = child.child.id();
#[cfg(target_os = "windows")]
let windows_job = WindowsJob::for_child(&child.child);
let out_pipe = child.child.stdout.take();
let err_pipe = child.child.stderr.take();
let out_thread = std::thread::spawn(move || drain_pipe(out_pipe));
let err_thread = std::thread::spawn(move || drain_pipe(err_pipe));
let deadline = req.timeout.map(|t| Instant::now() + t);
let mut timed_out = false;
let status = loop {
if let Some(s) = child.child.try_wait().map_err(ExecRefused::Spawn)? {
break s;
}
let deadline_reached = deadline.is_some_and(|dl| Instant::now() >= dl);
let cancelled =
cancel.is_some_and(|flag| flag.load(std::sync::atomic::Ordering::Acquire));
if deadline_reached || cancelled {
#[cfg(target_os = "linux")]
if let Some(cg) = &cgroup {
cg.kill();
}
#[cfg(target_os = "windows")]
kill_child_tree(&mut child.child, pgid, windows_job.as_ref());
#[cfg(not(target_os = "windows"))]
kill_child_tree(&mut child.child, pgid);
timed_out = deadline_reached;
break child.child.wait().map_err(ExecRefused::Spawn)?;
}
std::thread::sleep(Duration::from_millis(20));
};
#[cfg(target_os = "linux")]
if let Some(cg) = &cgroup {
cg.kill();
}
#[cfg(target_os = "windows")]
kill_child_tree(&mut child.child, pgid, windows_job.as_ref());
#[cfg(not(target_os = "windows"))]
kill_child_tree(&mut child.child, pgid);
let stdout = out_thread.join().unwrap_or_default();
let stderr = err_thread.join().unwrap_or_default();
Ok(ConfinedOutput {
success: status.success() && !timed_out,
code: status.code(),
stdout,
stderr,
sandbox_kind,
timed_out,
})
}
fn resolve_net_floor(
req: &ExecRequest,
cgroup_procs: Option<&Path>,
) -> Result<(String, Vec<String>, Caveats), ExecRefused> {
#[cfg(not(target_os = "linux"))]
let _ = cgroup_procs;
match req.net_grant {
NetGrant::Unrestricted => {
Ok((req.program.clone(), req.args.clone(), req.caveats.clone()))
}
#[cfg(target_os = "linux")]
NetGrant::DenyAll => {
let (guard, prefix) = Self::resolve_net_guard(req)?;
let mut caveats = req.caveats.clone();
if let Some(dir) = Path::new(&guard).parent() {
extend_read_root(&mut caveats, dir.to_string_lossy().into_owned());
}
let mut args: Vec<String> = prefix;
if let Some(procs) = cgroup_procs {
let procs = procs.to_string_lossy().into_owned();
extend_write_root(&mut caveats, procs.clone());
args.push("--cgroup-procs".to_string());
args.push(procs);
}
args.push("--".to_string());
args.push(req.program.clone());
args.extend(req.args.iter().cloned());
Ok((guard, args, caveats))
}
#[cfg(target_os = "macos")]
NetGrant::DenyAll => {
if !agent_bridle::seatbelt_is_supported() {
return Err(ExecRefused::ConfinementUnenforceable(
"NetGrant::DenyAll needs a kernel egress floor; the Seatbelt \
backend (sandbox-exec) is unavailable on this host"
.into(),
));
}
Ok((
req.program.clone(),
req.args.clone(),
deny_all_net(&req.caveats),
))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
NetGrant::DenyAll => Err(ExecRefused::ConfinementUnenforceable(
"NetGrant::DenyAll needs a kernel egress floor (Linux seccomp guard or \
macOS Seatbelt), unavailable on this platform"
.into(),
)),
}
}
#[cfg(target_os = "linux")]
fn resolve_net_guard(req: &ExecRequest) -> Result<(String, Vec<String>), ExecRefused> {
if let Some(p) = &req.net_guard_bin {
return if p.is_file() {
Ok((p.to_string_lossy().into_owned(), Vec::new()))
} else {
Err(ExecRefused::ConfinementUnenforceable(format!(
"net-guard binary not found at {}",
p.display()
)))
};
}
let exe = std::env::current_exe().map_err(|e| {
ExecRefused::ConfinementUnenforceable(format!(
"cannot resolve current executable to locate the net guard: {e}"
))
})?;
for cand in [
exe.parent().map(|d| d.join("newt-net-guard")),
exe.parent()
.and_then(Path::parent)
.map(|d| d.join("newt-net-guard")),
]
.into_iter()
.flatten()
{
if cand.is_file() {
return Ok((cand.to_string_lossy().into_owned(), Vec::new()));
}
}
if exe.file_name().and_then(|n| n.to_str()) == Some("newt") {
return Ok((
exe.to_string_lossy().into_owned(),
vec!["__net-guard".to_string()],
));
}
Err(ExecRefused::ConfinementUnenforceable(format!(
"no net guard: no `newt-net-guard` next to {} and it is not the `newt` \
binary, so the `__net-guard` self-exec dispatch is unavailable",
exe.display()
)))
}
}
#[must_use]
pub fn net_guard_available() -> bool {
#[cfg(target_os = "linux")]
{
let Ok(exe) = std::env::current_exe() else {
return false;
};
for cand in [
exe.parent().map(|d| d.join("newt-net-guard")),
exe.parent()
.and_then(Path::parent)
.map(|d| d.join("newt-net-guard")),
]
.into_iter()
.flatten()
{
if cand.is_file() {
return true;
}
}
exe.file_name().and_then(|n| n.to_str()) == Some("newt")
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(any(target_os = "macos", test))]
fn deny_all_net(caveats: &Caveats) -> Caveats {
let mut narrowed = caveats.clone();
narrowed.net = Scope::only([] as [String; 0]);
narrowed
}
#[cfg(target_os = "linux")]
fn extend_read_root(caveats: &mut Caveats, dir: String) {
caveats.fs_read = match &caveats.fs_read {
Scope::All => Scope::All,
Scope::Only(set) => Scope::only(set.iter().cloned().chain(std::iter::once(dir))),
};
}
#[cfg(target_os = "linux")]
fn extend_write_root(caveats: &mut Caveats, path: String) {
caveats.fs_write = match &caveats.fs_write {
Scope::All => Scope::All,
Scope::Only(set) => Scope::only(set.iter().cloned().chain(std::iter::once(path))),
};
}
#[cfg(target_os = "linux")]
pub struct CgroupHandle {
dir: PathBuf,
}
#[must_use]
pub fn cgroup_subtree_kill_available() -> bool {
#[cfg(target_os = "linux")]
{
CgroupHandle::create().is_some()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(target_os = "linux")]
impl CgroupHandle {
fn create() -> Option<Self> {
let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let rel = self_cg
.lines()
.find_map(|l| l.strip_prefix("0::"))?
.trim()
.trim_start_matches('/');
let pid = std::process::id();
static SEQ: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = PathBuf::from(format!("/sys/fs/cgroup/{rel}/newt-exec-{pid}-{n}"));
std::fs::create_dir(&dir).ok()?;
if dir.join("cgroup.kill").exists() {
Some(Self { dir })
} else {
let _ = std::fs::remove_dir(&dir);
None
}
}
fn procs(&self) -> PathBuf {
self.dir.join("cgroup.procs")
}
fn kill(&self) {
let _ = std::fs::write(self.dir.join("cgroup.kill"), "1");
}
}
#[cfg(target_os = "linux")]
impl Drop for CgroupHandle {
fn drop(&mut self) {
self.kill();
for _ in 0..50 {
if std::fs::remove_dir(&self.dir).is_ok() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
fn drain_pipe(pipe: Option<impl Read>) -> Vec<u8> {
let mut buf = Vec::new();
if let Some(mut p) = pipe {
let _ = p.read_to_end(&mut buf);
}
buf
}
#[cfg(target_os = "windows")]
pub(crate) struct WindowsJob {
handle: windows_sys::Win32::Foundation::HANDLE,
}
#[cfg(target_os = "windows")]
impl WindowsJob {
fn for_child(child: &std::process::Child) -> Option<Self> {
use std::os::windows::io::AsRawHandle;
Self::for_process(child.as_raw_handle())
}
pub(crate) fn for_process(process: std::os::windows::io::RawHandle) -> Option<Self> {
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
let handle = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
if handle.is_null() {
return None;
}
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let configured = unsafe {
SetInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
(&limits as *const JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
} != 0;
if !configured {
unsafe {
CloseHandle(handle);
}
return None;
}
let process = process as HANDLE;
let assigned = unsafe { AssignProcessToJobObject(handle, process) } != 0;
if !assigned {
unsafe {
CloseHandle(handle);
}
return None;
}
Some(Self { handle })
}
fn terminate(&self) {
use windows_sys::Win32::System::JobObjects::TerminateJobObject;
unsafe {
let _ = TerminateJobObject(self.handle, 1);
}
}
}
#[cfg(target_os = "windows")]
impl Drop for WindowsJob {
fn drop(&mut self) {
use windows_sys::Win32::Foundation::CloseHandle;
unsafe {
CloseHandle(self.handle);
}
}
}
#[cfg(target_os = "windows")]
fn kill_child_tree(child: &mut std::process::Child, _pgid: u32, job: Option<&WindowsJob>) {
if let Some(job) = job {
job.terminate();
}
let _ = child.kill();
}
#[cfg(not(target_os = "windows"))]
fn kill_child_tree(_child: &mut std::process::Child, pgid: u32) {
#[cfg(unix)]
kill_process_group(pgid);
#[cfg(not(unix))]
{
let _ = pgid;
let _ = _child.kill();
}
}
#[cfg(unix)]
pub fn kill_process_group(pgid: u32) {
unsafe {
let _ = libc::killpg(pgid as libc::pid_t, libc::SIGKILL);
}
}
pub(crate) fn runtime_sandbox_policy() -> agent_bridle::SandboxPolicy {
let policy = agent_bridle::SandboxPolicy {
child_network: agent_bridle::ChildNetworkPolicy::DenyDirect,
macos_private_ptys: cfg!(target_os = "macos"),
..agent_bridle::SandboxPolicy::default()
};
#[cfg(target_os = "macos")]
{
let mut policy = policy;
if let Some(developer) = selected_developer_directory() {
policy.base_read_paths.extra.push(developer.to_owned());
if let Some(contents) = Path::new(developer).parent() {
for name in ["Frameworks", "SharedFrameworks"] {
let frameworks = contents.join(name);
if frameworks.is_dir() {
policy
.base_read_paths
.extra
.push(frameworks.to_string_lossy().into_owned());
}
}
}
}
policy
}
#[cfg(not(target_os = "macos"))]
policy
}
#[must_use]
pub(crate) fn developer_exec_twins<'a>(exec: impl IntoIterator<Item = &'a String>) -> Vec<String> {
#[cfg(target_os = "macos")]
if let Some(developer) = selected_developer_directory() {
return exec
.into_iter()
.filter(|name| !name.contains('/') && Path::new("/usr/bin").join(name).is_file())
.map(|name| Path::new(developer).join("usr/bin").join(name))
.filter_map(|twin| twin.canonicalize().ok())
.filter(|twin| twin.is_file())
.flat_map(|twin| {
let companions: Vec<PathBuf> = twin
.parent()
.and_then(Path::parent)
.map(|version| {
FRAMEWORK_LAUNCHER_COMPANIONS
.iter()
.map(|relative| version.join(relative))
.filter(|companion| companion.is_file())
.collect()
})
.unwrap_or_default();
std::iter::once(twin).chain(companions)
})
.map(|path| path.to_string_lossy().into_owned())
.collect();
}
let _ = exec;
Vec::new()
}
#[cfg(target_os = "macos")]
const FRAMEWORK_LAUNCHER_COMPANIONS: &[&str] = &["Resources/Python.app/Contents/MacOS/Python"];
#[cfg(target_os = "macos")]
pub(crate) fn selected_developer_directory() -> Option<&'static str> {
static DEVELOPER: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
DEVELOPER
.get_or_init(|| {
let output = std::process::Command::new("/usr/bin/xcode-select")
.arg("-p")
.env_clear()
.current_dir("/")
.stdin(std::process::Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let path = std::path::PathBuf::from(String::from_utf8(output.stdout).ok()?.trim());
if !path.is_absolute() || !path.is_dir() {
return None;
}
path.canonicalize().ok()?.to_str().map(str::to_owned)
})
.as_deref()
}
#[must_use]
pub fn workspace_confined_caveats(workspace: &Path) -> Caveats {
let root = workspace.to_string_lossy().into_owned();
Caveats {
fs_read: Scope::only([root.clone()]),
fs_write: Scope::only([root]),
exec: Scope::All,
net: Scope::none(),
max_calls: crate::caveats::CountBound::Unlimited,
valid_for_generation: Scope::All,
}
}
#[must_use]
pub fn build_tool_request(
workspace: &Path,
cwd: &Path,
program: impl Into<String>,
args: impl IntoIterator<Item = impl Into<String>>,
network: &Scope<String>,
) -> ExecRequest {
let mut caveats = build_tool_caveats(workspace);
caveats.net = crate::caveats::spawn_net_scope(network);
let net_grant = if caveats.net == Scope::none() {
NetGrant::DenyAll
} else {
NetGrant::Unrestricted
};
toolchain_request(
ExecOrigin::AgentInfluenced,
workspace,
cwd,
program,
args,
caveats,
)
.net_grant(net_grant)
.env("CARGO_NET_OFFLINE", "true")
}
#[must_use]
pub fn build_scratch_dir(workspace: &Path) -> PathBuf {
scratch_dir_under(workspace, Some(crate::scratch::build_scratch_base()))
}
#[must_use]
pub fn build_scratch_root(workspace: &Path) -> PathBuf {
operator_build_scratch_root(workspace, Some(crate::scratch::build_scratch_base()))
.expect("the Build scratch base is always configured or managed")
}
fn scratch_dir_under(workspace: &Path, base: Option<PathBuf>) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
operator_build_scratch_root(workspace, base)
.unwrap_or_else(|| workspace.join("target").join("tmp"))
.join(format!(
"{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
))
}
fn operator_build_scratch_root(workspace: &Path, base: Option<PathBuf>) -> Option<PathBuf> {
base.map(|base| {
let identity =
content_addressable::RawContentId::from_content(workspace.to_string_lossy().as_bytes());
crate::scratch::managed_build_partition(&base, &identity)
.unwrap_or_else(|| base.join(identity.to_string()))
})
}
pub(crate) struct ScratchDir {
path: PathBuf,
created_target: bool,
managed: Option<crate::scratch::ManagedRun>,
}
impl ScratchDir {
fn create(path: &Path) -> std::io::Result<Self> {
if let Some(managed) = crate::scratch::acquire_build_run(path)? {
return Ok(Self {
path: path.to_owned(),
created_target: false,
managed: Some(managed),
});
}
let tmp = path.parent().unwrap_or(path);
let target = tmp.parent().unwrap_or(tmp);
let created_target = !target.exists();
match std::fs::create_dir(target) {
Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => return Err(error),
_ => {}
}
Self::private_dir(tmp, false)?;
Self::private_dir(path, true)?;
Ok(Self {
path: path.to_path_buf(),
created_target,
managed: None,
})
}
fn private_dir(dir: &Path, exclusive: bool) -> std::io::Result<()> {
#[cfg(unix)]
let builder = {
let mut builder = std::fs::DirBuilder::new();
std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
builder
};
#[cfg(not(unix))]
let builder = std::fs::DirBuilder::new();
match builder.create(dir) {
Err(error) if !exclusive && error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
other => other,
}
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
if self.managed.is_some() {
return;
}
if std::fs::symlink_metadata(&self.path).is_ok_and(|m| m.is_dir()) {
let _ = std::fs::remove_dir_all(&self.path);
}
if let Some(tmp) = self.path.parent() {
let _ = std::fs::remove_dir(tmp);
if self.created_target {
if let Some(target) = tmp.parent() {
let _ = std::fs::remove_dir(target);
}
}
}
}
}
pub const CRATES_IO_FETCH_HOSTS: &[&str] = &["index.crates.io", "static.crates.io"];
#[must_use]
pub fn dependency_fetch_request(
workspace: &Path,
cwd: &Path,
network: &Scope<String>,
) -> ExecRequest {
let writes: Vec<String> = operator_tool_home("CARGO_HOME", ".cargo")
.map(|home| cargo_home_fetch_write_roots(&home))
.unwrap_or_default();
let mut caveats = build_tool_caveats_with_writes(workspace, &writes);
caveats.net = network.clone();
toolchain_request(
ExecOrigin::TrustedInfra,
workspace,
cwd,
"cargo",
["fetch", "--locked"],
caveats,
)
}
fn cargo_home_fetch_write_roots(cargo_home: &str) -> Vec<String> {
let base = Path::new(cargo_home);
[
"registry",
".package-cache",
".package-cache-mutate",
".global-cache",
".global-cache-journal",
".global-cache-wal",
".global-cache-shm",
]
.into_iter()
.map(|entry| base.join(entry).to_string_lossy().into_owned())
.collect()
}
fn toolchain_request(
origin: ExecOrigin,
workspace: &Path,
cwd: &Path,
program: impl Into<String>,
args: impl IntoIterator<Item = impl Into<String>>,
caveats: Caveats,
) -> ExecRequest {
let scratch = build_scratch_dir(workspace);
let mut request = ExecRequest::new(origin, program, args, cwd, caveats)
.env("HOME", scratch.to_string_lossy())
.env("TMPDIR", scratch.to_string_lossy())
.env("TMP", scratch.to_string_lossy())
.env("TEMP", scratch.to_string_lossy())
.scratch_dir(scratch)
.env(
"CARGO_TARGET_DIR",
workspace.join("target").to_string_lossy(),
)
.env("RUSTC_WRAPPER", "")
.env("RUSTC_WORKSPACE_WRAPPER", "");
for (name, default) in [("CARGO_HOME", ".cargo"), ("RUSTUP_HOME", ".rustup")] {
if let Some(path) = operator_tool_home(name, default) {
request = request.env(name, path);
}
}
let mut paths = Vec::new();
#[cfg(target_os = "macos")]
if let Some(developer) = selected_developer_directory() {
paths.push(PathBuf::from(developer).join("Toolchains/XcodeDefault.xctoolchain/usr/bin"));
paths.push(PathBuf::from(developer).join("usr/bin"));
if let Some(sdk) = selected_macos_sdk() {
request = request.env("SDKROOT", sdk);
}
}
if let Some(path) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&path));
}
if let Ok(path) = std::env::join_paths(paths) {
request = request.env("PATH", path.to_string_lossy());
}
request.with_build_git_reads(workspace)
}
fn operator_tool_home(name: &str, default: &str) -> Option<String> {
std::env::var_os(name)
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(default)))
.filter(|path| path.is_absolute())
.map(|path| path.to_string_lossy().into_owned())
}
#[cfg(target_os = "macos")]
fn selected_macos_sdk() -> Option<String> {
let developer = Path::new(selected_developer_directory()?);
[
"Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk",
"SDKs/MacOSX.sdk",
]
.into_iter()
.filter_map(|relative| developer.join(relative).canonicalize().ok())
.find(|path| path.is_dir() && path.starts_with(developer))
.map(|path| path.to_string_lossy().into_owned())
}
#[must_use]
pub fn build_tool_caveats(workspace: &Path) -> Caveats {
build_tool_caveats_with_writes(workspace, &[])
}
#[must_use]
pub fn build_tool_caveats_with_writes(workspace: &Path, extra_write_roots: &[String]) -> Caveats {
build_caveats_with_scratch(
workspace,
extra_write_roots,
Some(crate::scratch::build_scratch_base()),
)
}
fn build_caveats_with_scratch(
workspace: &Path,
extra_write_roots: &[String],
scratch_base: Option<PathBuf>,
) -> Caveats {
let scratch = operator_build_scratch_root(workspace, scratch_base)
.map(|root| root.to_string_lossy().into_owned());
let mut write_roots = vec![workspace.to_string_lossy().into_owned()];
write_roots.extend(extra_write_roots.iter().cloned());
write_roots.extend(scratch.clone());
let mut read_roots = build_tool_read_roots(workspace);
read_roots.extend(scratch);
Caveats {
fs_read: Scope::only(read_roots),
fs_write: Scope::only(write_roots),
exec: Scope::All,
net: Scope::none(),
max_calls: crate::caveats::CountBound::Unlimited,
valid_for_generation: Scope::All,
}
}
fn build_tool_read_roots(workspace: &Path) -> Vec<String> {
let mut roots = vec![workspace.to_string_lossy().into_owned()];
roots.extend(toolchain_read_roots());
roots
}
#[must_use]
pub fn toolchain_read_roots() -> Vec<String> {
let mut roots = Vec::new();
if let Some(home) = operator_tool_home("CARGO_HOME", ".cargo") {
roots.extend(cargo_home_read_roots(&home));
}
if let Some(path) = operator_tool_home("RUSTUP_HOME", ".rustup") {
roots.push(path);
}
#[cfg(all(unix, not(target_os = "macos")))]
{
roots.push("/usr/include".to_string());
roots.push("/usr/local/include".to_string());
}
if let Some(path) = operator_tool_home("XDG_BIN_HOME", ".local/bin") {
roots.push(path);
}
roots
}
fn cargo_home_read_roots(cargo_home: &str) -> Vec<String> {
let base = Path::new(cargo_home);
let mut roots: Vec<String> = ["bin", "registry", "git"]
.into_iter()
.map(|subdir| base.join(subdir).to_string_lossy().into_owned())
.collect();
roots.push(base.join("config.toml").to_string_lossy().into_owned());
roots.push(base.join("config").to_string_lossy().into_owned());
roots
}
pub const BUILD_TOOL_EXEC: &[&str] = &["cargo", "just", "make"];
#[must_use]
pub fn is_build_tool_exec(target: &str) -> bool {
let trimmed = target.trim();
let name = Path::new(trimmed)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(trimmed);
BUILD_TOOL_EXEC.contains(&name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_build_tool_exec_matches_known_names_only() {
assert!(is_build_tool_exec("cargo"));
assert!(is_build_tool_exec("just"));
assert!(is_build_tool_exec("make"));
assert!(is_build_tool_exec("/usr/bin/cargo"));
assert!(!is_build_tool_exec("rm"));
assert!(!is_build_tool_exec("bash"));
}
struct EnvRestore {
key: &'static str,
saved: Option<std::ffi::OsString>,
}
impl EnvRestore {
fn capture(key: &'static str) -> Self {
Self {
key,
saved: std::env::var_os(key),
}
}
}
impl Drop for EnvRestore {
fn drop(&mut self) {
crate::process_env::set_or_remove(
self.key,
self.saved.as_deref().and_then(|v| v.to_str()),
);
}
}
#[test]
fn build_tool_read_roots_include_c_headers_and_local_bin() {
let _env = crate::process_env::lock();
let _restore_home = EnvRestore::capture("HOME");
let _restore_xdg_bin = EnvRestore::capture("XDG_BIN_HOME");
let fake_home = std::env::temp_dir().join("fake-home-for-build-tool-read-roots-test");
let abs_bin_home = std::env::temp_dir()
.join("abs-bin-home")
.to_string_lossy()
.into_owned();
crate::process_env::set_var("HOME", fake_home.to_str().unwrap());
crate::process_env::remove_var("XDG_BIN_HOME");
let roots = build_tool_read_roots(Path::new("/workspace"));
#[cfg(all(unix, not(target_os = "macos")))]
{
assert!(roots.iter().any(|r| r == "/usr/include"));
assert!(roots.iter().any(|r| r == "/usr/local/include"));
}
let local_bin = fake_home.join(".local/bin").to_string_lossy().into_owned();
assert!(roots.contains(&local_bin));
let read_scope = Scope::only(roots.clone());
for forbidden in [
fake_home.clone(),
fake_home.join(".newt"),
fake_home.join(".config"),
fake_home.join(".local/share"),
fake_home.join(".ssh"),
] {
let forbidden = forbidden.to_string_lossy().into_owned();
assert!(
!crate::caveats::permits_path(&read_scope, &forbidden),
"a build-lane root must not grant read over {forbidden}"
);
}
crate::process_env::set_var("XDG_BIN_HOME", &abs_bin_home);
let roots_abs = build_tool_read_roots(Path::new("/workspace"));
assert!(roots_abs.contains(&abs_bin_home));
assert!(!roots_abs.contains(&local_bin));
crate::process_env::set_var("XDG_BIN_HOME", "relative/bin");
let roots_relative = build_tool_read_roots(Path::new("/workspace"));
assert!(!roots_relative.iter().any(|r| r == "relative/bin"));
assert!(!roots_relative.contains(&local_bin));
}
#[test]
fn toolchain_read_roots_narrows_cargo_home_and_drops_the_cache_dir() {
let _env = crate::process_env::lock();
let _restore_home = EnvRestore::capture("HOME");
let _restore_cargo = EnvRestore::capture("CARGO_HOME");
let _restore_xdg_cache = EnvRestore::capture("XDG_CACHE_HOME");
let fake_home = std::env::temp_dir().join("fake-home-for-toolchain-read-roots-test");
crate::process_env::set_var("HOME", fake_home.to_str().unwrap());
crate::process_env::remove_var("CARGO_HOME");
crate::process_env::remove_var("XDG_CACHE_HOME");
let roots = toolchain_read_roots();
let cargo_home = fake_home.join(".cargo");
for subdir in ["bin", "registry", "git"] {
let expected = cargo_home.join(subdir).to_string_lossy().into_owned();
assert!(roots.contains(&expected), "missing {expected} in {roots:?}");
}
for config in ["config.toml", "config"] {
let expected = cargo_home.join(config).to_string_lossy().into_owned();
assert!(roots.contains(&expected), "missing {expected} in {roots:?}");
}
assert!(!roots
.iter()
.any(|r| r == cargo_home.to_string_lossy().as_ref()));
let read_scope = Scope::only(roots.clone());
for secret in ["credentials.toml", "credentials"] {
let forbidden = cargo_home.join(secret).to_string_lossy().into_owned();
assert!(
!crate::caveats::permits_path(&read_scope, &forbidden),
"toolchain_read_roots must never grant read over {forbidden}"
);
}
let default_cache = fake_home.join(".cache").to_string_lossy().into_owned();
assert!(!roots.iter().any(|r| r == &default_cache));
let hf_token = fake_home
.join(".cache/huggingface/token")
.to_string_lossy()
.into_owned();
assert!(!crate::caveats::permits_path(&read_scope, &hf_token));
}
#[test]
fn already_cancelled_request_never_attempts_spawn() {
let request = ExecRequest::new(
ExecOrigin::AgentInfluenced,
"missing-program",
["unused"],
"/missing",
workspace_confined_caveats(Path::new("/missing")),
);
let cancel = std::sync::atomic::AtomicBool::new(true);
assert!(
matches!(ConstrainedExecutor::run_until_cancelled(&request, Some(&cancel)), Err(ExecRefused::Spawn(error)) if error.kind() == std::io::ErrorKind::Interrupted)
);
}
#[test]
fn dependency_fetch_is_online_to_crates_io_only_and_never_writes_credentials() {
let _env = crate::process_env::lock();
let network = Scope::only(CRATES_IO_FETCH_HOSTS.iter().map(|host| (*host).to_owned()));
let request = dependency_fetch_request(Path::new("/ws"), Path::new("/ws/sub"), &network);
assert_eq!(request.origin, ExecOrigin::TrustedInfra);
assert_eq!(request.net_grant, NetGrant::Unrestricted);
assert_eq!(request.args, ["fetch", "--locked"]);
assert_eq!(
request.caveats.net,
Scope::only(CRATES_IO_FETCH_HOSTS.iter().map(|host| (*host).to_owned()))
);
assert!(request
.env_grants()
.iter()
.all(|(name, _)| name != "CARGO_NET_OFFLINE"));
if let Some(home) = operator_tool_home("CARGO_HOME", ".cargo") {
let home = Path::new(&home);
let registry = home.join("registry").to_string_lossy().into_owned();
let credentials = home.join("credentials.toml").to_string_lossy().into_owned();
assert!(crate::caveats::permits_path(
&request.caveats.fs_write,
®istry
));
assert!(!crate::caveats::permits_path(
&request.caveats.fs_write,
&credentials
));
assert!(!crate::caveats::permits_path(
&request.caveats.fs_read,
&credentials
));
}
}
#[test]
fn dependency_fetch_network_grant_does_not_expand_filesystem_authority() {
let _env = crate::process_env::lock();
let ws = Path::new("/ws");
let narrow = Scope::only(CRATES_IO_FETCH_HOSTS.iter().map(|host| (*host).to_owned()));
let baseline = dependency_fetch_request(ws, ws, &narrow);
let request = dependency_fetch_request(ws, ws, &Scope::All);
assert_eq!(request.caveats.net, Scope::All);
assert_eq!(request.caveats.fs_read, baseline.caveats.fs_read);
assert_eq!(request.caveats.fs_write, baseline.caveats.fs_write);
assert_eq!(request.args, baseline.args);
let denied = dependency_fetch_request(ws, ws, &Scope::none());
assert_eq!(denied.caveats.net, Scope::none());
}
#[test]
fn build_request_discovers_toolchain_before_replacing_home() {
let _env = crate::process_env::lock();
let request = build_tool_request(
Path::new("/ws"),
Path::new("/ws"),
"cargo",
["test"],
&Scope::none(),
);
let env: std::collections::BTreeMap<_, _> = request.env_grants().iter().cloned().collect();
assert_eq!(
env.get("HOME").map(Path::new),
Some(request.scratch_dirs[0].as_path())
);
assert_eq!(
env.get("CARGO_TARGET_DIR").map(std::path::Path::new),
Some(Path::new("/ws").join("target").as_path())
);
if let Some(path) = operator_tool_home("RUSTUP_HOME", ".rustup") {
assert_eq!(env.get("RUSTUP_HOME"), Some(&path));
assert!(crate::caveats::permits_path(
&request.caveats.fs_read,
&path
));
assert!(!crate::caveats::permits_path(
&request.caveats.fs_write,
&path
));
}
if let Some(path) = operator_tool_home("CARGO_HOME", ".cargo") {
assert_eq!(env.get("CARGO_HOME"), Some(&path));
for subdir in ["bin", "registry", "git"] {
assert!(crate::caveats::permits_path(
&request.caveats.fs_read,
&Path::new(&path).join(subdir).to_string_lossy()
));
}
assert!(!crate::caveats::permits_path(
&request.caveats.fs_read,
&Path::new(&path).join("credentials.toml").to_string_lossy()
));
assert!(!crate::caveats::permits_path(
&request.caveats.fs_write,
&path
));
}
assert!(env.keys().all(|name| [
"HOME",
"TMPDIR",
"TMP",
"TEMP",
"PATH",
"CARGO_HOME",
"RUSTUP_HOME",
"CARGO_TARGET_DIR",
"CARGO_NET_OFFLINE",
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"SDKROOT"
]
.contains(&name.as_str())));
assert_eq!(request.net_grant, NetGrant::DenyAll);
assert_eq!(request.caveats.exec, Scope::All);
assert_eq!(
request.caveats.fs_write,
build_tool_caveats(Path::new("/ws")).fs_write
);
let scratch = &request.scratch_dirs[0];
assert_eq!(request.scratch_dirs.len(), 1);
assert_eq!(env.get("TMPDIR").map(Path::new), Some(scratch.as_path()));
assert!(!scratch.starts_with("/ws"), "{scratch:?}");
assert!(crate::caveats::permits_path(
&request.caveats.fs_write,
&scratch.to_string_lossy()
));
assert!(crate::caveats::permits_path(
&request.caveats.fs_read,
&scratch.to_string_lossy()
));
}
#[test]
fn toolchain_requests_keep_home_outside_the_workspace() {
let _env = crate::process_env::lock();
let workspace = Path::new("/ws");
for request in [
build_tool_request(workspace, workspace, "cargo", ["check"], &Scope::none()),
dependency_fetch_request(workspace, workspace, &Scope::none()),
] {
let env: std::collections::BTreeMap<_, _> =
request.env_grants().iter().cloned().collect();
let home = Path::new(&env["HOME"]);
assert!(
!home.starts_with(workspace),
"HOME must not pollute the repo"
);
assert_eq!(home, request.scratch_dirs[0]);
assert_eq!(env["HOME"], env["TMPDIR"]);
for (name, default) in [("CARGO_HOME", ".cargo"), ("RUSTUP_HOME", ".rustup")] {
assert_eq!(env.get(name).cloned(), operator_tool_home(name, default));
}
assert!(crate::caveats::permits_path(
&request.caveats.fs_write,
&home.to_string_lossy()
));
assert!(!crate::caveats::permits_path(
&request.caveats.fs_write,
&home.parent().unwrap().parent().unwrap().to_string_lossy()
));
}
}
#[test]
fn each_build_scratch_dir_is_unique() {
let ws = Path::new("/ws");
assert_ne!(build_scratch_dir(ws), build_scratch_dir(ws));
}
#[test]
fn build_network_authority_narrows_hosts_and_preserves_filesystem_fence() {
let _env = crate::process_env::lock();
let ws = Path::new("/ws");
let baseline = build_tool_caveats(ws);
for (network, expected) in [
(Scope::none(), Scope::none()),
(Scope::only(["example.test".into()]), Scope::none()),
(Scope::All, Scope::All),
] {
let request = build_tool_request(ws, ws, "cargo", ["test"], &network);
assert_eq!(request.caveats.net, expected);
assert_eq!(request.caveats.fs_read, baseline.fs_read);
assert_eq!(request.caveats.fs_write, baseline.fs_write);
assert_eq!(request.caveats.exec, baseline.exec);
assert_eq!(request.origin, ExecOrigin::AgentInfluenced);
assert_eq!(
request.net_grant,
if expected == Scope::none() {
NetGrant::DenyAll
} else {
NetGrant::Unrestricted
}
);
assert!(request
.env_grants()
.contains(&("CARGO_NET_OFFLINE".into(), "true".into())));
}
}
#[cfg(unix)]
#[test]
fn scratch_dir_is_owner_only_and_gone_after_the_run() {
use std::os::unix::fs::PermissionsExt;
let ws = tempfile::tempdir().unwrap();
let path = build_scratch_dir(ws.path());
let guard = ScratchDir::create(&path).unwrap();
let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
assert_eq!(mode(&path), 0o700);
assert_eq!(mode(path.parent().unwrap()), 0o700);
std::fs::write(path.join("cc.tmp"), b"x").unwrap();
drop(guard);
assert!(!path.exists(), "scratch dir must be removed");
assert!(
!ws.path().join("target").exists(),
"an empty target/ this run created is removed too"
);
}
#[cfg(unix)]
#[test]
fn scratch_cleanup_deletes_nothing_else() {
let ws = tempfile::tempdir().unwrap();
let target = ws.path().join("target");
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("debug/keep"), b"k").unwrap();
let other = ScratchDir::create(&build_scratch_dir(ws.path())).unwrap();
let path = build_scratch_dir(ws.path());
drop(ScratchDir::create(&path).unwrap());
assert!(target.join("debug/keep").is_file());
assert!(other.path.is_dir(), "a concurrent run's dir is untouched");
drop(other);
assert!(target.join("debug/keep").is_file());
}
#[cfg(unix)]
#[test]
fn a_symlink_at_the_scratch_path_is_refused() {
let ws = tempfile::tempdir().unwrap();
let elsewhere = tempfile::tempdir().unwrap();
let path = scratch_dir_under(ws.path(), None);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(elsewhere.path(), &path).unwrap();
assert!(ScratchDir::create(&path).is_err());
assert!(std::fs::symlink_metadata(&path)
.unwrap()
.file_type()
.is_symlink());
assert!(elsewhere.path().is_dir());
}
#[test]
fn a_missing_workspace_is_refused_not_created() {
let root = tempfile::tempdir().unwrap();
let ws = root.path().join("missing").join("ws");
assert!(ScratchDir::create(&scratch_dir_under(&ws, None)).is_err());
assert!(!root.path().join("missing").exists());
}
#[cfg(unix)]
#[test]
fn a_build_lane_run_leaves_no_scratch_behind() {
let ws = tempfile::tempdir().unwrap();
let ws = ws.path().canonicalize().unwrap();
let req = build_tool_request(
&ws,
&ws,
"sh",
["-c", "printf %s \"$TMPDIR\"; touch \"$TMPDIR/x\""],
&Scope::none(),
);
let scratch = req.scratch_dirs[0].clone();
if let Ok(out) = ConstrainedExecutor::run(&req) {
if out.success {
assert_eq!(
String::from_utf8_lossy(&out.stdout),
scratch.to_string_lossy()
);
}
}
assert!(!scratch.exists());
}
#[test]
fn agent_influenced_mints_under_a_kernel_strength_floor() {
let caveats = workspace_confined_caveats(Path::new("/ws"));
let agent_cx = mint_context(ExecOrigin::AgentInfluenced, &caveats).unwrap();
assert_eq!(agent_cx.strength_floor().exec(), AxisEnforcement::Kernel);
assert_eq!(agent_cx.strength_floor().net(), AxisEnforcement::Kernel);
let trusted_cx = mint_context(ExecOrigin::TrustedInfra, &caveats).unwrap();
assert_ne!(trusted_cx.strength_floor().exec(), AxisEnforcement::Kernel);
assert_ne!(trusted_cx.strength_floor().net(), AxisEnforcement::Kernel);
}
#[test]
fn broker_mediated_mints_under_the_per_axis_confined_floor() {
let caveats = workspace_confined_caveats(Path::new("/ws"));
let agent_cx = mint_context(ExecOrigin::AgentInfluenced, &caveats).unwrap();
let broker_cx = mint_context(ExecOrigin::BrokerMediated, &caveats).unwrap();
assert_eq!(
broker_cx.strength_floor().fs_read(),
agent_cx.strength_floor().fs_read()
);
assert_eq!(
broker_cx.strength_floor().fs_write(),
agent_cx.strength_floor().fs_write()
);
assert_eq!(
broker_cx.strength_floor().net(),
agent_cx.strength_floor().net()
);
assert_eq!(
broker_cx.strength_floor().fs_read(),
AxisEnforcement::Kernel
);
assert_eq!(broker_cx.strength_floor().net(), AxisEnforcement::Kernel);
assert_eq!(
broker_cx.strength_floor().exec(),
AxisEnforcement::Interceptor
);
assert_ne!(
broker_cx.strength_floor().exec(),
agent_cx.strength_floor().exec()
);
}
#[test]
fn deny_all_net_is_a_pure_attenuation_of_the_net_axis_only() {
let base = workspace_confined_caveats(Path::new("/ws"));
let narrowed = deny_all_net(&base);
assert_eq!(narrowed.net, Scope::only([] as [String; 0]));
assert_eq!(narrowed.fs_read, base.fs_read);
assert_eq!(narrowed.fs_write, base.fs_write);
assert_eq!(narrowed.exec, base.exec);
assert_eq!(deny_all_net(&narrowed), narrowed);
}
#[test]
fn request_env_is_exactly_the_grants_nothing_inherited() {
let req = ExecRequest::new(
ExecOrigin::AgentInfluenced,
"sh",
["-c", "true"],
"/ws",
workspace_confined_caveats(Path::new("/ws")),
)
.env("HOME", "/ws")
.envs([("LANG".into(), "C".into())]);
assert_eq!(
req.env_grants(),
&[
("HOME".to_string(), "/ws".to_string()),
("LANG".to_string(), "C".to_string())
]
);
}
#[test]
fn timeout_builder_sets_the_bound_default_is_unbounded() {
let base = ExecRequest::new(
ExecOrigin::AgentInfluenced,
"sh",
["-c", "true"],
"/ws",
workspace_confined_caveats(Path::new("/ws")),
);
assert_eq!(base.timeout, None);
let bounded = base.timeout(Duration::from_secs(5));
assert_eq!(bounded.timeout, Some(Duration::from_secs(5)));
}
#[test]
fn build_tool_caveats_calibrated_reads_fenced_writes_no_net() {
use crate::caveats::ScopeExt;
let cav = build_tool_caveats(Path::new("/ws"));
assert!(cav.fs_read.permits(&"/ws".to_string()));
assert!(
!cav.fs_read
.permits(&"/home/someone-else/.ssh/id_rsa".to_string()),
"a build tool must not be able to READ arbitrary secrets (disclosure via output)"
);
assert!(
matches!(cav.fs_read, Scope::Only(_)),
"reads are a calibrated set, never open"
);
assert!(cav.fs_write.permits(&"/ws".to_string()));
assert!(
!cav.fs_write.permits(&"/home/user/.cargo".to_string()),
"no write to the shared package cache (poisoning) outside the fence"
);
assert!(!cav.fs_write.permits(&"/etc".to_string()));
assert!(
!cav.fs_write.permits(&"/tmp".to_string()),
"no write to shared /tmp — only the private workspace scratch partition"
);
assert!(
!cav.net.permits(&"evil.example".to_string()),
"no network — the exfil channel is closed"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_build_lane_child_gets_a_writable_scratch_dir_outside_the_workspace() {
let ws = tempfile::tempdir().unwrap();
let ws = ws.path().canonicalize().unwrap();
let script = "import os, tempfile\n\
d = tempfile.mkdtemp(); open(os.path.join(d, 'x'), 'w').write('ok'); print(d)\n";
let req = build_tool_request(&ws, &ws, "python3", ["-c", script], &Scope::All);
let out = ConstrainedExecutor::run(&req).expect("the build lane spawns");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.success, "stdout: {stdout}\nstderr: {stderr}");
assert!(
!std::path::Path::new(stdout.trim()).starts_with(&ws),
"{stdout}"
);
}
#[test]
fn an_operator_build_scratch_base_moves_the_run_and_is_granted() {
let ws = Path::new("/ws");
let base = std::env::temp_dir().join("operator-build-scratch");
let root = operator_build_scratch_root(ws, Some(base.clone())).unwrap();
assert_eq!(root.parent(), Some(base.as_path()));
assert_eq!(
Some(root.clone()),
operator_build_scratch_root(ws, Some(base.clone()))
);
assert_ne!(
Some(root.clone()),
operator_build_scratch_root(Path::new("/other"), Some(base.clone()))
);
let run = scratch_dir_under(ws, Some(base.clone()));
assert_eq!(run.parent(), Some(root.as_path()));
assert!(
!run.starts_with(ws),
"an operator base leaves the workspace"
);
let root = root.to_string_lossy().into_owned();
let cav = build_caveats_with_scratch(ws, &[], Some(base));
assert!(crate::caveats::permits_path(
&cav.fs_write,
&run.to_string_lossy()
));
assert!(crate::caveats::permits_path(&cav.fs_read, &root));
let default = build_caveats_with_scratch(ws, &[], None);
assert_eq!(default.fs_write, Scope::only(["/ws".to_owned()]));
assert!(scratch_dir_under(ws, None).starts_with("/ws/target/tmp"));
}
#[test]
fn build_tool_caveats_with_writes_grants_only_the_named_extra_root() {
use crate::caveats::ScopeExt;
let cav = build_tool_caveats_with_writes(Path::new("/ws"), &["/crew/target".to_string()]);
assert!(cav.fs_write.permits(&"/ws".to_string()));
assert!(
cav.fs_write.permits(&"/crew/target".to_string()),
"the explicitly-granted extra write root is permitted"
);
assert!(!cav.fs_write.permits(&"/crew/other".to_string()));
}
#[test]
fn workspace_caveats_fence_reads_writes_to_workspace_and_denies_net() {
use crate::caveats::ScopeExt;
let cav = workspace_confined_caveats(Path::new("/ws"));
assert!(cav.fs_write.permits(&"/ws".to_string()));
assert!(!cav.fs_write.permits(&"/etc".to_string()));
assert!(cav.fs_read.permits(&"/ws".to_string()));
assert!(!cav.fs_read.permits(&"/etc/passwd".to_string()));
assert!(!cav.net.permits(&"example.com".to_string()));
assert!(matches!(cav.net, Scope::Only(ref s) if s.is_empty()));
}
#[test]
fn exec_spawn_tool_declares_no_special_ceiling() {
let tool = ExecSpawnTool;
assert_eq!(tool.name(), "confined_exec");
assert_eq!(tool.required(), Caveats::top());
}
#[test]
fn refusal_displays_are_honest_about_fail_closed() {
let r = ExecRefused::ConfinementUnenforceable("no Landlock".into());
assert!(r.to_string().contains("fail-closed"));
}
}
#[cfg(all(test, target_os = "linux"))]
#[path = "confined_exec_git_tests.rs"]
mod git_read_race_tests;