use std::{
fs,
path::{Component, Path, PathBuf},
};
use super::super::{create_private_directory, secure_directory};
#[derive(Clone, Debug)]
pub(super) struct SessionSubagentsDir {
path: PathBuf,
}
impl SessionSubagentsDir {
pub(super) fn parse(rho_root: &Path, path: &Path) -> anyhow::Result<Self> {
let sessions_root = rho_root.join("sessions");
let relative = path.strip_prefix(&sessions_root).map_err(|_| {
anyhow::anyhow!(
"session delegated run directory is outside {}",
sessions_root.display()
)
})?;
let components = relative.components().collect::<Vec<_>>();
let [workspace, session, subagents] = components.as_slice() else {
anyhow::bail!(
"invalid session delegated run directory: {}",
path.display()
);
};
anyhow::ensure!(
matches!(subagents, Component::Normal(name) if *name == "subagents"),
"invalid session delegated run directory: {}",
path.display()
);
anyhow::ensure!(
matches!(workspace, Component::Normal(_)) && matches!(session, Component::Normal(_)),
"invalid session delegated run directory: {}",
path.display()
);
let workspace_path = sessions_root.join(workspace.as_os_str());
let session_path = workspace_path.join(session.as_os_str());
for ancestor in [&sessions_root, &workspace_path, &session_path] {
anyhow::ensure!(
is_trusted_directory(ancestor),
"{} is not a trusted session directory",
ancestor.display()
);
}
Ok(Self {
path: path.to_path_buf(),
})
}
pub(super) fn ensure_ready(&self) -> anyhow::Result<()> {
if self.path.exists() {
secure_directory(&self.path)?;
} else {
create_private_directory(&self.path)?;
}
Ok(())
}
pub(super) fn run_directory(&self, id: &str) -> PathBuf {
self.path.join(id)
}
}
pub(crate) fn is_trusted_directory(path: &Path) -> bool {
fs::symlink_metadata(path)
.map(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
.unwrap_or(false)
}
pub(super) fn validate_run_directory(rho_root: &Path, id: &str, path: &Path) -> anyhow::Result<()> {
anyhow::ensure!(
path.file_name().and_then(|name| name.to_str()) == Some(id),
"delegated run index contains an invalid path for '{id}': {}",
path.display()
);
let global_root = rho_root.join("subagents");
if path.parent() == Some(global_root.as_path()) && is_trusted_directory(&global_root) {
return Ok(());
}
let parent = path.parent().ok_or_else(|| {
anyhow::anyhow!(
"delegated run index contains an invalid path for '{id}': {}",
path.display()
)
})?;
SessionSubagentsDir::parse(rho_root, parent).map_err(|_| {
anyhow::anyhow!(
"delegated run index contains an invalid path for '{id}': {}",
path.display()
)
})?;
Ok(())
}
pub(super) fn scan_session_directories(rho_root: &Path, id: &str) -> anyhow::Result<Vec<PathBuf>> {
let sessions_root = rho_root.join("sessions");
let mut matches = Vec::new();
if !is_trusted_directory(&sessions_root) {
return Ok(matches);
}
for workspace in fs::read_dir(sessions_root)? {
let workspace = workspace?.path();
if !is_trusted_directory(&workspace) {
continue;
}
for session in fs::read_dir(workspace)? {
let session = session?.path();
if !is_trusted_directory(&session) {
continue;
}
let subagents = session.join("subagents");
let Ok(session_subagents) = SessionSubagentsDir::parse(rho_root, &subagents) else {
continue;
};
if !is_trusted_directory(&session_subagents.path) {
continue;
}
let candidate = session_subagents.run_directory(id);
if is_trusted_directory(&candidate) {
matches.push(candidate);
}
}
}
matches.sort();
Ok(matches)
}
pub(super) fn prepare_private_directory(path: &Path) -> std::io::Result<()> {
fs::create_dir_all(path)?;
secure_directory(path)
}