use std::path::{Path, PathBuf};
use crate::Result;
use crate::harness::tool::SandboxMode;
use crate::harness::workspace::types::WorkspaceDescriptor;
impl WorkspaceDescriptor {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
trusted_roots: Vec::new(),
policy_id: String::new(),
sandbox: SandboxMode::Inherit,
}
}
pub fn with_trusted_root(mut self, root: impl Into<PathBuf>) -> Self {
self.trusted_roots.push(root.into());
self
}
pub fn with_policy_id(mut self, id: impl Into<String>) -> Self {
self.policy_id = id.into();
self
}
pub fn with_sandbox(mut self, sandbox: SandboxMode) -> Self {
self.sandbox = sandbox;
self
}
pub fn allows(&self, path: &Path) -> bool {
let Some(candidate) = anchored_normalize(path) else {
return false;
};
std::iter::once(&self.root)
.chain(self.trusted_roots.iter())
.filter_map(|root| anchored_normalize(root))
.any(|root| candidate.starts_with(&root))
}
pub fn enforce(&self, path: &Path, events: &crate::harness::events::EventSink) -> Result<()> {
if self.allows(path) {
return Ok(());
}
let rendered = path.display().to_string();
events.emit(crate::harness::events::AgentEvent::WorkspaceViolation {
path: rendered.clone(),
});
Err(crate::error::TinyAgentsError::Validation(format!(
"path `{rendered}` is outside the allowed workspace roots"
)))
}
}
fn anchored_normalize(path: &Path) -> Option<PathBuf> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().ok()?.join(path)
};
Some(normalize(&absolute))
}
fn normalize(path: &Path) -> PathBuf {
use std::path::Component;
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::ParentDir => match out.components().next_back() {
Some(Component::Normal(_)) => {
out.pop();
}
Some(Component::RootDir | Component::Prefix(_)) => {
}
_ => out.push(Component::ParentDir),
},
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}