use crate::orchestrator::TaskOrchestrator;
use crate::state::StepOutcome;
impl TaskOrchestrator {
pub(super) async fn execute_research_step(&self, query: &str) -> Result<StepOutcome, String> {
let Some(llm) = self.llm.as_ref() else {
return Ok(StepOutcome {
stdout: format!("Research query: {query}"),
stderr: String::new(),
exit_code: None,
artifacts: vec![],
summary: format!("Researched (no LLM attached): {query}"),
});
};
let messages = vec![
cortex::llm::Message::system(crate::prompts::RESEARCH_SYSTEM),
cortex::llm::Message::user(query.to_string()),
];
match llm.generate(&messages).await {
Ok(resp) => Ok(StepOutcome {
stdout: resp.content.clone(),
stderr: String::new(),
exit_code: None,
artifacts: vec![],
summary: summary_first_line(&resp.content, &format!("Research: {query}")),
}),
Err(e) => Err(format!("LLM research failed: {e}")),
}
}
pub(super) async fn execute_review_step(&self, artifact: &str) -> Result<StepOutcome, String> {
let Some(llm) = self.llm.as_ref() else {
return Ok(StepOutcome {
stdout: String::new(),
stderr: String::new(),
exit_code: None,
artifacts: vec![artifact.to_string()],
summary: format!("Review requested (no LLM attached): {artifact}"),
});
};
let messages = vec![
cortex::llm::Message::system(crate::prompts::REVIEW_SYSTEM),
cortex::llm::Message::user(format!("Review this artifact: {artifact}")),
];
match llm.generate(&messages).await {
Ok(resp) => Ok(StepOutcome {
stdout: resp.content.clone(),
stderr: String::new(),
exit_code: None,
artifacts: vec![artifact.to_string()],
summary: summary_first_line(&resp.content, &format!("Reviewed: {artifact}")),
}),
Err(e) => Err(format!("LLM review failed: {e}")),
}
}
pub(super) async fn execute_notify_step(
&self,
channel: &str,
message: &str,
) -> Result<StepOutcome, String> {
let Some(dispatcher) = self.dispatcher.as_ref() else {
return Ok(StepOutcome {
stdout: String::new(),
stderr: String::new(),
exit_code: None,
artifacts: vec![],
summary: format!("Notify (no dispatcher attached): {channel}: {message}"),
});
};
let mut intent = channel::DeliveryIntent::new(
message,
channel::DeliveryCategory::Report,
channel::UrgencyLevel::Normal,
);
if !channel.is_empty() && channel != "default" {
intent = intent.with_preferred(channel);
}
match dispatcher.dispatch(intent).await {
Ok(receipt) => Ok(StepOutcome {
stdout: format!("Delivered via {} ({})", receipt.channel_id, receipt.reason),
stderr: String::new(),
exit_code: None,
artifacts: vec![],
summary: format!("Notified via {}: {message}", receipt.channel_id),
}),
Err(channel::ChannelError::NoChannelAvailable(_, _)) => Ok(StepOutcome {
stdout: String::new(),
stderr: String::new(),
exit_code: None,
artifacts: vec![],
summary: format!(
"Notify (no external channel — included in this report): {message}"
),
}),
Err(e) => Err(format!("Notify delivery failed: {e}")),
}
}
pub(super) async fn execute_sandbox_step(
&self,
command: &str,
workdir: &std::path::Path,
) -> Result<StepOutcome, String> {
let sandbox = match self.sandbox.as_ref() {
Some(s) => s,
None => {
return Err("Sandbox not available".to_string());
}
};
let parsed = parse_sandbox_command(command)?;
let (binary, args) = parsed
.argv
.split_first()
.ok_or_else(|| "Empty command".to_string())?;
let cmd =
sandbox::SandboxCommand::new(binary, args.to_vec()).with_workdir(workdir.to_path_buf());
match sandbox.run(cmd).await {
Err(e) => Err(humanize_sandbox_error(binary, &e)),
Ok(outcome) if outcome.exit_code != 0 => {
let stderr_tail: String = outcome
.stderr
.lines()
.rev()
.take(5)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
let mut msg = format!("Command failed (exit {}): {command}", outcome.exit_code);
if !stderr_tail.trim().is_empty() {
msg.push_str("\nstderr: ");
msg.push_str(&stderr_tail);
}
Err(msg)
}
Ok(outcome) => {
let mut artifacts = Vec::new();
if let Some((path, append)) = &parsed.redirect {
let write_result = if *append {
use std::io::Write as _;
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|mut f| f.write_all(outcome.stdout.as_bytes()))
} else {
std::fs::write(path, outcome.stdout.as_bytes())
};
if let Err(e) = write_result {
return Err(format!("Redirect to {} failed: {e}", path.display()));
}
artifacts.push(path.to_string_lossy().into_owned());
}
Ok(StepOutcome {
stdout: outcome.stdout,
stderr: outcome.stderr,
exit_code: Some(outcome.exit_code),
artifacts,
summary: format!("Command succeeded: {command}"),
})
}
}
}
pub(super) async fn execute_shell_step(
&self,
command: &str,
workdir: &std::path::Path,
) -> Result<StepOutcome, String> {
let sandbox = match self.sandbox.as_ref() {
Some(s) => s,
None => return Err("Sandbox not available".to_string()),
};
let trimmed = command.trim();
if trimmed.is_empty() {
return Err("Empty shell command".to_string());
}
let cmd = sandbox::SandboxCommand::shell(trimmed).with_workdir(workdir.to_path_buf());
match sandbox.run(cmd).await {
Err(e) => Err(humanize_sandbox_error("sh", &e)),
Ok(outcome) if outcome.exit_code != 0 => {
let stderr_tail: String = outcome
.stderr
.lines()
.rev()
.take(5)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
let mut msg = format!(
"Shell command failed (exit {}): {trimmed}",
outcome.exit_code
);
if !stderr_tail.trim().is_empty() {
msg.push_str("\nstderr: ");
msg.push_str(&stderr_tail);
}
Err(msg)
}
Ok(outcome) => Ok(StepOutcome {
stdout: outcome.stdout,
stderr: outcome.stderr,
exit_code: Some(outcome.exit_code),
artifacts: vec![],
summary: format!("Shell command succeeded: {trimmed}"),
}),
}
}
pub(super) async fn delegate_implement_step(
&self,
spec: &str,
agent: &str,
) -> Result<StepOutcome, String> {
let registry = self.agents.as_ref().ok_or_else(|| {
"No specialist agents are configured — install one (e.g. claude-code, \
aider, codex) on your PATH and it will be picked up on the next boot."
.to_string()
})?;
let primary = registry.get(agent).map_err(|_| {
let known = registry.list();
if known.is_empty() {
format!(
"Specialist agent '{agent}' isn't available — no agents are \
currently registered. Install one on your PATH or pick a \
different planner."
)
} else {
format!(
"Specialist agent '{agent}' isn't available. Available agents: {}.",
known.join(", ")
)
}
})?;
let task_spec = self.build_delegate_task_spec(spec).await;
let task = delegate::AgentTask::new(task_spec);
let outcome = delegate::run_with_escalation(
primary,
registry.as_ref(),
task,
&self.delegation_policy,
)
.await;
match outcome {
delegate::EscalationOutcome::Succeeded(result) => {
self.record_delegate_episode(agent, spec, &result, None)
.await;
Ok(StepOutcome {
stdout: result.stdout,
stderr: result.stderr,
exit_code: result.exit_code,
artifacts: result
.artifacts
.iter()
.map(|a| a.reference.clone())
.collect(),
summary: format!("{agent}: {}", result.summary),
})
}
delegate::EscalationOutcome::Recovered { via, result } => {
self.record_delegate_episode(agent, spec, &result, Some(&via))
.await;
Ok(StepOutcome {
stdout: result.stdout,
stderr: result.stderr,
exit_code: result.exit_code,
artifacts: result
.artifacts
.iter()
.map(|a| a.reference.clone())
.collect(),
summary: format!("{agent} failed; recovered via {via}: {}", result.summary),
})
}
delegate::EscalationOutcome::EscalateToHuman { reason } => Err(reason),
}
}
}
#[derive(Debug)]
pub(crate) struct ParsedCommand {
pub argv: Vec<String>,
pub redirect: Option<(std::path::PathBuf, bool)>,
}
pub(crate) fn parse_sandbox_command(command: &str) -> Result<ParsedCommand, String> {
let tokens = tokenize_shell(command)?;
if tokens.is_empty() {
return Err("Empty command".to_string());
}
for tok in &tokens {
if tok == "|" || tok == "||" || tok == "&&" || tok == ";" || tok == "&" || tok == "<" {
return Err(format!(
"Shell metacharacter {tok:?} not supported — sandbox runs argv directly. \
Split this into multiple steps or capture output via the trailing `> path` form."
));
}
if tok.contains('`') || tok.contains("$(") {
return Err(format!(
"Shell substitution in {tok:?} not supported — sandbox runs argv directly."
));
}
}
let mut tokens = tokens;
let mut redirect = None;
if tokens.len() >= 3 {
let last = tokens.len() - 1;
let op = &tokens[last - 1];
if op == ">" || op == ">>" {
let append = op == ">>";
let path = std::path::PathBuf::from(tokens.remove(last));
tokens.pop(); redirect = Some((path, append));
}
}
if tokens.iter().any(|t| t == ">" || t == ">>") {
return Err(
"Multiple or non-trailing `>` redirects are not supported — sandbox runs argv directly."
.to_string(),
);
}
Ok(ParsedCommand {
argv: tokens,
redirect,
})
}
fn tokenize_shell(input: &str) -> Result<Vec<String>, String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_single = false;
let mut in_double = false;
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\'' if !in_double => {
in_single = !in_single;
}
'"' if !in_single => {
in_double = !in_double;
}
'\\' if !in_single => {
if let Some(next) = chars.next() {
cur.push(next);
}
}
c if c.is_whitespace() && !in_single && !in_double => {
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
}
c @ ('>' | '<' | '|' | '&' | ';') if !in_single && !in_double => {
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
let mut op = String::from(c);
if let Some(&peek) = chars.peek() {
if (c == '>' && peek == '>')
|| (c == '|' && peek == '|')
|| (c == '&' && peek == '&')
{
op.push(peek);
chars.next();
}
}
out.push(op);
}
_ => cur.push(c),
}
}
if in_single || in_double {
return Err("Unterminated quoted string".to_string());
}
if !cur.is_empty() {
out.push(cur);
}
Ok(out)
}
pub(crate) fn humanize_sandbox_error(binary: &str, e: &sandbox::SandboxError) -> String {
use sandbox::SandboxError;
match e {
SandboxError::Forbidden(msg) => {
if msg.contains("not in allowlist") {
format!(
"Cannot run `{binary}` here — it isn't on the sandbox allowlist. \
Add it under `security.exec_allowlist` if this command is safe to run."
)
} else if msg.contains("explicitly forbidden") {
format!("`{binary}` is explicitly forbidden by config and cannot run.")
} else {
format!("`{binary}` was blocked: {msg}")
}
}
SandboxError::PathNotAllowed(_) => format!(
"Working directory for `{binary}` isn't allowed. Add the path under \
`security.allowed_paths` to permit it."
),
SandboxError::Timeout(_) => format!("`{binary}` was killed for taking too long."),
other => format!("`{binary}` failed to run: {other}"),
}
}
pub(crate) fn summary_first_line(s: &str, default_label: &str) -> String {
let line = s
.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("");
if line.is_empty() {
return default_label.to_string();
}
if line.chars().count() > 160 {
let truncated: String = line.chars().take(157).collect();
format!("{truncated}…")
} else {
line.to_string()
}
}
#[cfg(test)]
mod parse_tests {
use super::*;
#[test]
fn parses_simple_argv() {
let p = parse_sandbox_command("find /tmp -type f").unwrap();
assert_eq!(p.argv, vec!["find", "/tmp", "-type", "f"]);
assert!(p.redirect.is_none());
}
#[test]
fn extracts_trailing_redirect() {
let p = parse_sandbox_command("find /tmp -type f > /tmp/list.txt").unwrap();
assert_eq!(p.argv, vec!["find", "/tmp", "-type", "f"]);
let (path, append) = p.redirect.unwrap();
assert_eq!(path, std::path::PathBuf::from("/tmp/list.txt"));
assert!(!append);
}
#[test]
fn extracts_trailing_append_redirect() {
let p = parse_sandbox_command("ls /etc >> /tmp/log").unwrap();
let (_, append) = p.redirect.unwrap();
assert!(append);
}
#[test]
fn handles_unspaced_redirect() {
let p = parse_sandbox_command("find /tmp -type f >/tmp/list.txt").unwrap();
assert_eq!(p.argv, vec!["find", "/tmp", "-type", "f"]);
assert_eq!(
p.redirect.unwrap().0,
std::path::PathBuf::from("/tmp/list.txt")
);
}
#[test]
fn rejects_pipe() {
let err = parse_sandbox_command("ls | grep foo").unwrap_err();
assert!(err.contains("Shell metacharacter"));
}
#[test]
fn rejects_command_substitution() {
let err = parse_sandbox_command("echo $(pwd)").unwrap_err();
assert!(err.contains("Shell substitution"));
}
#[test]
fn respects_quotes() {
let p = parse_sandbox_command(r#"grep "hello world" file.txt"#).unwrap();
assert_eq!(p.argv, vec!["grep", "hello world", "file.txt"]);
}
#[test]
fn redirect_inside_quotes_is_argument_not_operator() {
let p = parse_sandbox_command(r#"echo "a > b""#).unwrap();
assert_eq!(p.argv, vec!["echo", "a > b"]);
assert!(p.redirect.is_none());
}
#[test]
fn rejects_redirect_in_middle() {
let err = parse_sandbox_command("foo > bar baz").unwrap_err();
assert!(err.contains("non-trailing"));
}
}