use std::path::{Path, PathBuf};
use thiserror::Error;
pub use aion_awl::WORKSPACE_ROOT_PLACEHOLDER;
const CLONES_DIRECTORY: &str = "clones";
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum WorkspaceRootError {
#[error("the aion home cannot be resolved, so there is no workspace root: {reason}")]
Unresolvable {
reason: String,
},
#[error(
"the workspace root `{path}` is not an absolute path; a relative root names a \
different location after a restart from a different directory"
)]
NotAbsolute {
path: String,
},
#[error(
"the workspace root `{path}` is not valid UTF-8, so it cannot be spliced into a \
declared command"
)]
NotUnicode {
path: String,
},
#[error(
"the workspace root `{path}` contains {character}, which would change the parsed \
shape of the declared command it is spliced into"
)]
ShapeChanging {
path: String,
character: &'static str,
},
#[error(
"the declared command carries {{workspace_root}} {placement}; every occurrence \
must stand alone as one whole, unquoted argv word, because the root is spliced \
into the command string before it is parsed"
)]
PlaceholderMisplaced {
placement: &'static str,
},
#[error(
"the workspace root `{path}` contains a NUL byte, which cannot cross `execve`, so no \
process can be launched in it"
)]
NotSpawnable {
path: String,
},
#[error("the workspace root directory `{path}` could not be created: {error}")]
CreationFailed {
path: String,
error: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpandedCommand {
pub command: String,
pub workspace_root: String,
}
#[derive(Debug, Clone)]
pub struct WorkspaceRoot {
resolution: Result<PathBuf, WorkspaceRootError>,
}
impl WorkspaceRoot {
#[must_use]
pub fn resolve() -> Self {
let resolution = crate::config::aion_home()
.map(|home| root_under(&home.path))
.map_err(|error| WorkspaceRootError::Unresolvable {
reason: error.to_string(),
});
Self { resolution }
}
#[must_use]
pub const fn from_resolution(resolution: Result<PathBuf, WorkspaceRootError>) -> Self {
Self { resolution }
}
#[must_use]
pub fn banner_value(&self) -> String {
match self.resolved() {
Ok(path) => path.display().to_string(),
Err(error) => format!("unresolvable: {error}"),
}
}
pub fn resolved(&self) -> Result<&Path, &WorkspaceRootError> {
match &self.resolution {
Ok(path) => Ok(path.as_path()),
Err(error) => Err(error),
}
}
pub fn expand(&self, command: &str) -> Result<Option<ExpandedCommand>, WorkspaceRootError> {
if !command.contains(WORKSPACE_ROOT_PLACEHOLDER) {
return Ok(None);
}
if let Some(placement) = misplaced_placeholder(command) {
return Err(WorkspaceRootError::PlaceholderMisplaced { placement });
}
let (root, root_text) = self.spliceable_root()?;
if let Some(character) = shape_changing_character(root_text) {
return Err(WorkspaceRootError::ShapeChanging {
path: root_text.to_owned(),
character,
});
}
create_root_directory(root).map_err(|error| WorkspaceRootError::CreationFailed {
path: root_text.to_owned(),
error: error.to_string(),
})?;
Ok(Some(ExpandedCommand {
command: command.replace(WORKSPACE_ROOT_PLACEHOLDER, root_text),
workspace_root: root_text.to_owned(),
}))
}
pub fn expand_setting(&self, value: &str) -> Result<Option<String>, WorkspaceRootError> {
if !value.contains(WORKSPACE_ROOT_PLACEHOLDER) {
return Ok(None);
}
let (root, root_text) = self.spliceable_root()?;
if root_text.contains('\0') {
return Err(WorkspaceRootError::NotSpawnable {
path: root_text.to_owned(),
});
}
create_root_directory(root).map_err(|error| WorkspaceRootError::CreationFailed {
path: root_text.to_owned(),
error: error.to_string(),
})?;
Ok(Some(value.replace(WORKSPACE_ROOT_PLACEHOLDER, root_text)))
}
fn spliceable_root(&self) -> Result<(&Path, &str), WorkspaceRootError> {
let root = self.resolution.as_ref().map_err(Clone::clone)?;
if !root.is_absolute() {
return Err(WorkspaceRootError::NotAbsolute {
path: root.to_string_lossy().into_owned(),
});
}
let root_text = root
.to_str()
.ok_or_else(|| WorkspaceRootError::NotUnicode {
path: root.to_string_lossy().into_owned(),
})?;
Ok((root.as_path(), root_text))
}
}
fn root_under(home: &Path) -> PathBuf {
home.join(CLONES_DIRECTORY)
}
fn create_root_directory(root: &Path) -> std::io::Result<()> {
let mut builder = std::fs::DirBuilder::new();
builder.recursive(true);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt as _;
builder.mode(0o700);
}
builder.create(root)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum QuoteContext {
Unquoted,
Single,
Double,
}
fn quote_context_at(command: &str, position: usize) -> QuoteContext {
let mut context = QuoteContext::Unquoted;
for (index, character) in command.char_indices() {
if index >= position {
break;
}
context = match (context, character) {
(QuoteContext::Unquoted, '\'') => QuoteContext::Single,
(QuoteContext::Unquoted, '"') => QuoteContext::Double,
(QuoteContext::Single, '\'') | (QuoteContext::Double, '"') => QuoteContext::Unquoted,
(current, _) => current,
};
}
context
}
fn misplaced_placeholder(command: &str) -> Option<&'static str> {
for (start, _) in command.match_indices(WORKSPACE_ROOT_PLACEHOLDER) {
let end = start + WORKSPACE_ROOT_PLACEHOLDER.len();
match quote_context_at(command, start) {
QuoteContext::Single => return Some("inside single quotes"),
QuoteContext::Double => return Some("inside double quotes"),
QuoteContext::Unquoted => {}
}
let starts_a_word = command[..start]
.chars()
.next_back()
.is_none_or(char::is_whitespace);
let ends_a_word = command[end..]
.chars()
.next()
.is_none_or(char::is_whitespace);
if !starts_a_word || !ends_a_word {
return Some("glued to adjacent text");
}
}
None
}
fn shape_changing_character(root: &str) -> Option<&'static str> {
for character in root.chars() {
if character.is_whitespace() {
return Some("whitespace");
}
match character {
'{' => return Some("`{`"),
'\'' => return Some("a single quote"),
'"' => return Some("a double quote"),
'\0' => return Some("a NUL byte"),
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::path::Path;
use super::{
ExpandedCommand, WORKSPACE_ROOT_PLACEHOLDER, WorkspaceRoot, WorkspaceRootError, root_under,
shape_changing_character,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn unresolved() -> WorkspaceRoot {
WorkspaceRoot::from_resolution(Err(WorkspaceRootError::Unresolvable {
reason: "AION_HOME must not be empty".to_owned(),
}))
}
#[test]
fn expansion_replaces_every_occurrence() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let root_text = root.to_string_lossy().into_owned();
let expanded = workspace
.expand("sh -c 'x' -- {workspace_root} $run_id {workspace_root}")?
.ok_or("a placeholder-bearing command must expand")?;
assert_eq!(
expanded,
ExpandedCommand {
command: format!("sh -c 'x' -- {root_text} $run_id {root_text}"),
workspace_root: root_text,
}
);
Ok(())
}
#[test]
fn a_command_without_the_placeholder_is_not_expanded() -> TestResult {
let scratch = tempfile::tempdir()?;
let resolved = WorkspaceRoot::from_resolution(Ok(scratch.path().join("clones")));
assert_eq!(resolved.expand("echo $greeting")?, None);
assert_eq!(unresolved().expand("echo $greeting")?, None);
Ok(())
}
#[test]
fn a_setting_expands_to_the_root_and_the_root_directory_is_made() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let root_text = root.to_string_lossy().into_owned();
assert!(
!root.exists(),
"the fixture must start with the root ABSENT, or the creation claim is vacuous"
);
assert_eq!(
workspace.expand_setting(WORKSPACE_ROOT_PLACEHOLDER)?,
Some(root_text.clone()),
"a bare placeholder is the root itself"
);
assert_eq!(
workspace.expand_setting("{workspace_root}/assistant")?,
Some(format!("{root_text}/assistant")),
"a setting is one whole value: the placeholder leads it and the rest rides along"
);
assert!(
root.is_dir(),
"a setting the harness will spawn in must exist by the time it is handed over"
);
Ok(())
}
#[test]
fn a_setting_without_the_placeholder_is_not_expanded() -> TestResult {
let scratch = tempfile::tempdir()?;
let resolved = WorkspaceRoot::from_resolution(Ok(scratch.path().join("clones")));
assert_eq!(resolved.expand_setting("/srv/agents/workspace")?, None);
assert_eq!(unresolved().expand_setting("/srv/agents/workspace")?, None);
Ok(())
}
#[test]
fn a_setting_carrying_the_placeholder_needs_a_usable_root() {
assert!(
matches!(
unresolved().expand_setting("{workspace_root}/assistant"),
Err(WorkspaceRootError::Unresolvable { .. })
),
"a box with no resolvable home refuses the launch by name"
);
assert!(
matches!(
WorkspaceRoot::from_resolution(Ok(PathBuf::from("relative/clones")))
.expand_setting("{workspace_root}/assistant"),
Err(WorkspaceRootError::NotAbsolute { .. })
),
"a relative root cannot make an absolute setting"
);
assert!(
matches!(
WorkspaceRoot::from_resolution(Ok(PathBuf::from("/srv/clo\0nes")))
.expand_setting("{workspace_root}")
.as_ref(),
Err(WorkspaceRootError::NotSpawnable { .. })
),
"a NUL cannot cross execve, so no process could ever be started there"
);
}
#[test]
fn a_setting_accepts_a_root_the_command_splice_refuses() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("my clones");
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
assert!(
matches!(
workspace.expand("ls {workspace_root}"),
Err(WorkspaceRootError::ShapeChanging {
character: "whitespace",
..
})
),
"the command splice still refuses a root that would reshape the parse"
);
assert_eq!(
workspace.expand_setting("{workspace_root}")?,
Some(root.to_string_lossy().into_owned()),
"the same root is a perfectly good directory to stand in"
);
Ok(())
}
#[test]
fn the_root_is_derived_as_the_homes_clones_directory() {
assert_eq!(
root_under(Path::new("/x")),
std::path::PathBuf::from("/x/clones")
);
assert_eq!(
root_under(Path::new("/Users/operator/.aion")),
std::path::PathBuf::from("/Users/operator/.aion/clones")
);
}
#[test]
fn the_banner_value_reports_the_path_or_the_failure() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let resolved = WorkspaceRoot::from_resolution(Ok(root.clone()));
assert_eq!(resolved.banner_value(), root.display().to_string());
let failed = unresolved().banner_value();
assert!(
failed.starts_with("unresolvable: "),
"an unresolved root must be reported as exactly that: {failed}"
);
assert!(
failed.contains("AION_HOME must not be empty"),
"the banner must carry the resolution failure's own reason: {failed}"
);
Ok(())
}
#[test]
fn a_placeholder_that_is_a_whole_bare_word_is_accepted() -> TestResult {
let scratch = tempfile::tempdir()?;
let workspace = WorkspaceRoot::from_resolution(Ok(scratch.path().join("clones")));
assert!(
workspace
.expand("sh -c 'x' -- {workspace_root} $run_id")?
.is_some(),
"a bare-word placeholder must expand"
);
Ok(())
}
#[test]
fn a_misplaced_placeholder_is_refused_naming_the_placement() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
for (command, placement) in [
("sh -c '{workspace_root}'", "inside single quotes"),
("echo \"{workspace_root}\"", "inside double quotes"),
("echo x{workspace_root}", "glued to adjacent text"),
("echo {workspace_root}/sub", "glued to adjacent text"),
("echo x{workspace_root}/sub", "glued to adjacent text"),
("echo ''{workspace_root}", "glued to adjacent text"),
("echo {{workspace_root}", "glued to adjacent text"),
(
"echo {workspace_root} x{workspace_root}",
"glued to adjacent text",
),
] {
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let Err(error) = workspace.expand(command) else {
return Err(format!("command {command:?} must be refused").into());
};
assert_eq!(
error,
WorkspaceRootError::PlaceholderMisplaced { placement },
"command {command:?} must be refused as {placement}"
);
}
assert!(
!root.exists(),
"a refused command must not create the root directory"
);
Ok(())
}
#[test]
fn a_declared_body_from_a_compiled_document_passes_the_placement_guard() -> TestResult {
let scratch = tempfile::tempdir()?;
let workspace = WorkspaceRoot::from_resolution(Ok(scratch.path().join("clones")));
let source = concat!(
"//! A declared body carrying the placeholder as a whole bare word.\n",
"workflow guard_probe\n",
" input run_id: String\n",
" outcome done: type RunOutcome, route success\n",
"\n",
"type RunOutcome { exit_code: Int, stdout: String, stderr: String }\n",
"\n",
"worker prober\n",
" action provision(run_id: String) -> RunOutcome\n",
" run \"sh -c 'printf %s \\\"$1/$2\\\"' -- {workspace_root} {{run_id}}\"\n",
"\n",
"step probe\n",
" provision(run_id: run_id) -> provisioned\n",
" provisioned |> route done\n",
);
let compiled = aion_awl::compile(source, Path::new("."))
.map_err(|error| format!("the probe document must compile: {error}"))?;
let command = compiled
.contract
.workers
.iter()
.flat_map(|worker| &worker.actions)
.find_map(|action| match &action.body {
Some(aion_package::ActionBodyContract::Run { command })
if action.name == "provision" =>
{
Some(command.clone())
}
_ => None,
})
.ok_or("the probe document must declare a bodied provision")?;
assert!(
command.contains(WORKSPACE_ROOT_PLACEHOLDER),
"the compiled body must still carry the placeholder verbatim: {command}"
);
assert!(
workspace.expand(&command)?.is_some(),
"a compiled declared body must pass the placement guard and expand"
);
Ok(())
}
#[tokio::test]
async fn every_accepted_printable_ascii_root_survives_the_real_parser() -> TestResult {
let scratch = tempfile::tempdir()?;
let mut executed = 0usize;
for code in 0x20u8..=0x7Eu8 {
let character = char::from(code);
let root = scratch.path().join(format!("with{character}char"));
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let probe = format!("printf %s {WORKSPACE_ROOT_PLACEHOLDER}");
match workspace.expand(&probe) {
Err(_) => {}
Ok(expanded) => {
let expanded =
expanded.ok_or("a placeholder-bearing probe must expand or refuse")?;
let action = aion_worker::shell::ShellAction::new(&expanded.command).map_err(
|error| format!("accepted root {root:?} failed to parse: {error}"),
)?;
let (context, _cancellation) = aion_worker::ActivityContext::new(
aion_core::WorkflowId::new_v4(),
aion_core::RunId::new_v4(),
aion_core::ActivityId::from_sequence_position(1),
1,
);
let outcome = action
.run(&std::collections::BTreeMap::new(), &context)
.await
.map_err(|error| {
format!("accepted root {root:?} failed to execute: {error}")
})?;
assert_eq!(
outcome.stdout, expanded.workspace_root,
"the command must observe exactly the accepted root {root:?}"
);
executed += 1;
}
}
}
assert!(
executed > 0,
"at least one printable root must be accepted, or this test refused everything \
and proved nothing"
);
Ok(())
}
#[test]
fn an_unresolved_root_refuses_a_placeholder_bearing_command_by_name() -> TestResult {
let command = format!("echo {WORKSPACE_ROOT_PLACEHOLDER}");
let Err(error) = unresolved().expand(&command) else {
return Err("an unresolved root must refuse expansion".into());
};
assert_eq!(
error,
WorkspaceRootError::Unresolvable {
reason: "AION_HOME must not be empty".to_owned(),
}
);
assert!(
error.to_string().contains("AION_HOME must not be empty"),
"the refusal must carry the resolution failure's own reason: {error}"
);
Ok(())
}
#[test]
fn a_relative_root_is_refused() -> TestResult {
let workspace = WorkspaceRoot::from_resolution(Ok(PathBuf::from("relative/clones")));
let command = format!("echo {WORKSPACE_ROOT_PLACEHOLDER}");
let Err(error) = workspace.expand(&command) else {
return Err("a relative root must be refused".into());
};
assert_eq!(
error,
WorkspaceRootError::NotAbsolute {
path: "relative/clones".to_owned(),
}
);
Ok(())
}
#[test]
fn a_shape_changing_root_is_refused_naming_the_character() -> TestResult {
for (fragment, character) in [
("with space", "whitespace"),
("with\ttab", "whitespace"),
("with\nnewline", "whitespace"),
("with{brace", "`{`"),
("with'single", "a single quote"),
("with\"double", "a double quote"),
("with\0nul", "a NUL byte"),
] {
let root = PathBuf::from(format!("/absolute/{fragment}"));
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let command = format!("echo {WORKSPACE_ROOT_PLACEHOLDER}");
let Err(error) = workspace.expand(&command) else {
return Err(format!("root {root:?} must be refused as shape-changing").into());
};
assert_eq!(
error,
WorkspaceRootError::ShapeChanging {
path: root.to_string_lossy().into_owned(),
character,
},
"root {root:?} must be refused naming {character}"
);
}
Ok(())
}
#[test]
fn a_clean_root_has_no_shape_changing_character() {
assert_eq!(
shape_changing_character("/Users/operator/.aion/clones"),
None
);
}
#[test]
fn the_directory_is_created_when_missing() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("nested").join("clones");
assert!(!root.exists(), "the root must start absent");
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let command = format!("echo {WORKSPACE_ROOT_PLACEHOLDER}");
let first = workspace.expand(&command)?;
assert!(first.is_some(), "expansion must succeed");
assert!(root.is_dir(), "expansion must create the missing root");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mode = std::fs::metadata(&root)?.permissions().mode() & 0o777;
assert_eq!(
mode, 0o700,
"a created root must be mode 0700, got {mode:o}"
);
}
assert_eq!(workspace.expand(&command)?, first);
Ok(())
}
#[test]
fn a_root_that_cannot_be_created_is_refused_naming_the_io_error() -> TestResult {
let scratch = tempfile::tempdir()?;
let file = scratch.path().join("occupied");
std::fs::write(&file, b"not a directory")?;
let root = file.join("clones");
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
let command = format!("echo {WORKSPACE_ROOT_PLACEHOLDER}");
let Err(error) = workspace.expand(&command) else {
return Err("creation beneath a regular file must fail".into());
};
let WorkspaceRootError::CreationFailed { path, error: io } = &error else {
return Err(format!("expected CreationFailed, got: {error}").into());
};
assert_eq!(path, &root.to_string_lossy().into_owned());
assert!(!io.is_empty(), "the io error's own words must be carried");
Ok(())
}
#[test]
fn resolved_reports_without_creating() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let workspace = WorkspaceRoot::from_resolution(Ok(root.clone()));
assert_eq!(workspace.resolved(), Ok(root.as_path()));
assert!(
!root.exists(),
"reporting the root must not create the directory"
);
let failed = unresolved();
let Err(error) = failed.resolved() else {
return Err("an unresolved root must report its failure".into());
};
assert!(matches!(error, WorkspaceRootError::Unresolvable { .. }));
Ok(())
}
}