#![cfg(unix)]
use agent_workspace_contract::{
content_revision, revision_matches, DirEntry, FileVisibilityOptions, WorkspaceVersionConflict,
WORKSPACE_MAX_FILE_BYTES, WORKSPACE_MAX_RESULTS, WORKSPACE_MAX_SEARCH_OUTPUT_BYTES,
};
use anyhow::{Context, Result};
use sha2::{Digest as _, Sha256};
use std::ffi::{CString, OsStr};
use std::fs::File;
use std::io::{Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone)]
pub(crate) struct SecureRoot {
root: PathBuf,
}
impl SecureRoot {
pub(crate) fn new(root: PathBuf) -> Self {
Self { root }
}
fn root_fd(&self) -> Result<OwnedFd> {
std::fs::create_dir_all(&self.root)?;
open_path(
libc::AT_FDCWD,
self.root.as_os_str(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
)
.with_context(|| format!("open workspace root {}", self.root.display()))
}
fn components(path: &str) -> Result<Vec<&OsStr>> {
let path = Path::new(path.trim());
anyhow::ensure!(!path.is_absolute(), "workspace path must be relative");
let mut values = Vec::new();
for component in path.components() {
match component {
Component::Normal(value) => values.push(value),
Component::CurDir => {}
_ => anyhow::bail!("workspace path contains a forbidden component"),
}
}
Ok(values)
}
fn parent_fd(&self, path: &str, create: bool) -> Result<(OwnedFd, CString)> {
let values = Self::components(path)?;
let (name, parents) = values
.split_last()
.context("workspace operation requires a file name")?;
let mut dir = self.root_fd()?;
for component in parents {
match open_path(
dir.as_raw_fd(),
component,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
) {
Ok(next) => dir = next,
Err(error) if create && is_not_found(&error) => {
mkdir_at(dir.as_raw_fd(), component, 0o700)?;
dir = open_path(
dir.as_raw_fd(),
component,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
)?;
}
Err(error) => return Err(error),
}
}
Ok((dir, cstring(name)?))
}
fn directory_fd(&self, path: &str) -> Result<OwnedFd> {
let mut dir = self.root_fd()?;
for component in Self::components(path)? {
dir = open_path(
dir.as_raw_fd(),
component,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
)?;
}
Ok(dir)
}
pub(crate) fn list_dir(
&self,
path: &str,
options: Option<FileVisibilityOptions>,
) -> Result<Vec<DirEntry>> {
let dir = self.directory_fd(path)?;
let mut entries = Vec::new();
for name in directory_names(dir.as_raw_fd())? {
if options.is_some_and(|options| !crate::search::is_visible_name(&name, options)) {
continue;
}
anyhow::ensure!(
entries.len() < WORKSPACE_MAX_RESULTS,
"directory result limit exceeded"
);
let stat = stat_at(dir.as_raw_fd(), &name)?;
let kind = stat.st_mode & libc::S_IFMT;
if kind == libc::S_IFLNK {
continue;
}
entries.push(DirEntry {
name,
is_dir: kind == libc::S_IFDIR,
size: (kind == libc::S_IFREG).then_some(stat.st_size.max(0) as u64),
});
}
entries.sort_by(|left, right| left.name.cmp(&right.name));
Ok(entries)
}
pub(crate) fn walk(
&self,
path: &str,
max_depth: usize,
options: Option<FileVisibilityOptions>,
) -> Result<Vec<String>> {
let dir = self.directory_fd(path)?;
let prefix = Self::components(path)?
.iter()
.map(|value| value.to_string_lossy())
.collect::<Vec<_>>()
.join("/");
let mut result = Vec::new();
let mut bytes = 0;
walk_fd(
dir.as_raw_fd(),
&prefix,
max_depth.min(64),
options,
&mut result,
&mut bytes,
)?;
Ok(result)
}
pub(crate) fn read(&self, path: &str) -> Result<Vec<u8>> {
let (parent, name) = self.parent_fd(path, false)?;
let fd = open_cstr(
parent.as_raw_fd(),
&name,
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
)?;
let mut file = File::from(fd);
let metadata = file.metadata()?;
anyhow::ensure!(metadata.is_file(), "workspace path is not a regular file");
anyhow::ensure!(
metadata.len() <= WORKSPACE_MAX_FILE_BYTES as u64,
"workspace file exceeds {WORKSPACE_MAX_FILE_BYTES} bytes"
);
let mut bytes = Vec::with_capacity(metadata.len() as usize);
file.read_to_end(&mut bytes)?;
anyhow::ensure!(
bytes.len() <= WORKSPACE_MAX_FILE_BYTES,
"workspace file exceeds {WORKSPACE_MAX_FILE_BYTES} bytes"
);
Ok(bytes)
}
pub(crate) fn revision(&self, path: &str) -> Result<Option<String>> {
let (parent, name) = self.parent_fd(path, false)?;
let fd = match open_cstr(
parent.as_raw_fd(),
&name,
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
) {
Ok(fd) => fd,
Err(error) if is_not_found(&error) => return Ok(None),
Err(error) => return Err(error),
};
let mut file = File::from(fd);
anyhow::ensure!(
file.metadata()?.is_file(),
"workspace path is not a regular file"
);
let mut hash = Sha256::new();
let mut total = 0_usize;
let mut buffer = [0_u8; 16 * 1024];
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
break;
}
total = total.saturating_add(read);
anyhow::ensure!(
total <= WORKSPACE_MAX_FILE_BYTES,
"workspace file exceeds {WORKSPACE_MAX_FILE_BYTES} bytes"
);
hash.update(&buffer[..read]);
}
Ok(Some(format!("sha256:{:x}", hash.finalize())))
}
pub(crate) fn is_regular_file(&self, path: &str) -> Result<bool> {
let (parent, name) = self.parent_fd(path, false)?;
let stat = stat_at(parent.as_raw_fd(), name.to_str()?)?;
Ok(stat.st_mode & libc::S_IFMT == libc::S_IFREG)
}
pub(crate) fn write_if_match(
&self,
path: &str,
bytes: &[u8],
expected: Option<&str>,
) -> Result<String> {
anyhow::ensure!(
bytes.len() <= WORKSPACE_MAX_FILE_BYTES,
"workspace file is too large"
);
let root = self.root_fd()?;
let lock_name = CString::new(".agent-workspace-write.lock").unwrap();
let lock_fd = open_cstr(
root.as_raw_fd(),
&lock_name,
libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
)?;
let _lock = AdvisoryLock::exclusive(lock_fd)?;
let actual = self.revision(path)?;
if let Some(expected) = expected {
if !revision_matches(expected, actual.as_deref()) {
return Err(WorkspaceVersionConflict {
expected: expected.to_string(),
actual,
}
.into());
}
}
let (parent, target) = self.parent_fd(path, true)?;
let temp = CString::new(format!(
".agent-write-{}.tmp",
uuid::Uuid::new_v4().simple()
))?;
let fd = open_cstr(
parent.as_raw_fd(),
&temp,
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
)?;
let mut file = File::from(fd);
let result = (|| -> Result<()> {
file.write_all(bytes)?;
file.sync_all()?;
rename_at(parent.as_raw_fd(), &temp, parent.as_raw_fd(), &target)?;
fsync_fd(parent.as_raw_fd())?;
Ok(())
})();
if result.is_err() {
let _ = unlink_at(parent.as_raw_fd(), &temp, 0);
}
result?;
Ok(content_revision(bytes))
}
pub(crate) fn write_chunk(&self, path: &str, offset: u64, bytes: &[u8]) -> Result<u64> {
anyhow::ensure!(
offset.saturating_add(bytes.len() as u64) <= WORKSPACE_MAX_FILE_BYTES as u64,
"upload exceeds file quota"
);
let root = self.root_fd()?;
let lock_name = CString::new(".agent-workspace-write.lock").unwrap();
let lock = open_cstr(
root.as_raw_fd(),
&lock_name,
libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
)?;
let _lock = AdvisoryLock::exclusive(lock)?;
let (parent, name) = self.parent_fd(path, true)?;
let fd = open_cstr(
parent.as_raw_fd(),
&name,
libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
)?;
let file = File::from(fd);
let length = file.metadata()?.len();
if length > offset {
let end = offset.saturating_add(bytes.len() as u64);
anyhow::ensure!(
end <= length,
"upload retry overlaps partially committed data"
);
let mut existing = vec![0; bytes.len()];
let read = unsafe {
libc::pread(
file.as_raw_fd(),
existing.as_mut_ptr().cast(),
existing.len(),
offset as libc::off_t,
)
};
anyhow::ensure!(
read == bytes.len() as isize && existing == bytes,
"upload retry payload conflict"
);
return Ok(length);
}
anyhow::ensure!(length == offset, "upload offset conflict");
let mut written = 0;
while written < bytes.len() {
let count = unsafe {
libc::pwrite(
file.as_raw_fd(),
bytes[written..].as_ptr().cast(),
bytes.len() - written,
(offset + written as u64) as libc::off_t,
)
};
if count < 0 {
return Err(std::io::Error::last_os_error().into());
}
written += count as usize;
}
file.sync_all()?;
Ok(offset + bytes.len() as u64)
}
pub(crate) fn remove(&self, path: &str) -> Result<()> {
self.remove_if_match(path, None)
}
pub(crate) fn remove_if_match(&self, path: &str, expected: Option<&str>) -> Result<()> {
let root = self.root_fd()?;
let lock_name = CString::new(".agent-workspace-write.lock").unwrap();
let lock = open_cstr(
root.as_raw_fd(),
&lock_name,
libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
)?;
let _lock = AdvisoryLock::exclusive(lock)?;
if let Some(expected) = expected {
let actual = self.revision(path)?;
if !revision_matches(expected, actual.as_deref()) {
return Err(WorkspaceVersionConflict {
expected: expected.to_string(),
actual,
}
.into());
}
}
let (parent, name) = self.parent_fd(path, false)?;
unlink_at(parent.as_raw_fd(), &name, 0)
}
pub(crate) fn create_dir_all(&self, path: &str) -> Result<()> {
let mut dir = self.root_fd()?;
for component in Self::components(path)? {
match open_path(
dir.as_raw_fd(),
component,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
) {
Ok(next) => dir = next,
Err(error) if is_not_found(&error) => {
mkdir_at(dir.as_raw_fd(), component, 0o700)?;
dir = open_path(
dir.as_raw_fd(),
component,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
)?;
}
Err(error) => return Err(error),
}
}
Ok(())
}
}
struct AdvisoryLock(OwnedFd);
impl AdvisoryLock {
fn exclusive(fd: OwnedFd) -> Result<Self> {
let result = unsafe { libc::flock(fd.as_raw_fd(), libc::LOCK_EX) };
if result != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(Self(fd))
}
}
impl Drop for AdvisoryLock {
fn drop(&mut self) {
unsafe {
libc::flock(self.0.as_raw_fd(), libc::LOCK_UN);
}
}
}
fn cstring(value: &OsStr) -> Result<CString> {
CString::new(value.as_bytes()).context("workspace path contains NUL")
}
fn open_path(dir: RawFd, path: &OsStr, flags: i32, mode: libc::mode_t) -> Result<OwnedFd> {
open_cstr(dir, &cstring(path)?, flags, mode)
}
fn open_cstr(dir: RawFd, path: &CString, flags: i32, mode: libc::mode_t) -> Result<OwnedFd> {
let fd = unsafe { libc::openat(dir, path.as_ptr(), flags, libc::c_uint::from(mode)) };
if fd < 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
}
fn mkdir_at(dir: RawFd, name: &OsStr, mode: libc::mode_t) -> Result<()> {
let name = cstring(name)?;
if unsafe { libc::mkdirat(dir, name.as_ptr(), mode) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().into())
}
}
fn rename_at(from_dir: RawFd, from: &CString, to_dir: RawFd, to: &CString) -> Result<()> {
if unsafe { libc::renameat(from_dir, from.as_ptr(), to_dir, to.as_ptr()) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().into())
}
}
fn unlink_at(dir: RawFd, name: &CString, flags: i32) -> Result<()> {
if unsafe { libc::unlinkat(dir, name.as_ptr(), flags) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().into())
}
}
fn fsync_fd(fd: RawFd) -> Result<()> {
if unsafe { libc::fsync(fd) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().into())
}
}
fn is_not_found(error: &anyhow::Error) -> bool {
error
.downcast_ref::<std::io::Error>()
.is_some_and(|error| error.kind() == std::io::ErrorKind::NotFound)
}
fn directory_names(fd: RawFd) -> Result<Vec<String>> {
let duplicated = unsafe { libc::dup(fd) };
if duplicated < 0 {
return Err(std::io::Error::last_os_error().into());
}
let directory = unsafe { libc::fdopendir(duplicated) };
if directory.is_null() {
unsafe { libc::close(duplicated) };
return Err(std::io::Error::last_os_error().into());
}
let mut values = Vec::new();
loop {
let entry = unsafe { libc::readdir(directory) };
if entry.is_null() {
break;
}
let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }
.to_string_lossy()
.into_owned();
if name != "." && name != ".." {
values.push(name);
}
}
unsafe { libc::closedir(directory) };
Ok(values)
}
fn stat_at(fd: RawFd, name: &str) -> Result<libc::stat> {
let name = CString::new(name)?;
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
if unsafe {
libc::fstatat(
fd,
name.as_ptr(),
stat.as_mut_ptr(),
libc::AT_SYMLINK_NOFOLLOW,
)
} != 0
{
Err(std::io::Error::last_os_error().into())
} else {
Ok(unsafe { stat.assume_init() })
}
}
fn walk_fd(
fd: RawFd,
prefix: &str,
depth: usize,
options: Option<FileVisibilityOptions>,
output: &mut Vec<String>,
output_bytes: &mut usize,
) -> Result<()> {
if depth == 0 {
return Ok(());
}
let mut names = directory_names(fd)?;
names.sort();
for name in names {
if options.is_some_and(|options| !crate::search::is_visible_name(&name, options)) {
continue;
}
let stat = match stat_at(fd, &name) {
Ok(stat) => stat,
Err(error) if is_not_found(&error) => continue,
Err(error) => return Err(error),
};
let kind = stat.st_mode & libc::S_IFMT;
if kind == libc::S_IFLNK {
continue;
}
let relative = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
anyhow::ensure!(
output.len() < WORKSPACE_MAX_RESULTS,
"workspace tree result limit exceeded"
);
*output_bytes = output_bytes.saturating_add(relative.len());
anyhow::ensure!(
*output_bytes <= WORKSPACE_MAX_SEARCH_OUTPUT_BYTES,
"workspace tree output limit exceeded"
);
output.push(relative.clone());
if kind == libc::S_IFDIR {
let child = open_path(
fd,
OsStr::new(&name),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0,
)?;
walk_fd(
child.as_raw_fd(),
&relative,
depth - 1,
options,
output,
output_bytes,
)?;
}
}
Ok(())
}