use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(not(windows))]
use std::sync::Arc;
#[cfg(not(windows))]
use saya_harness::runner::sandbox::RunSandbox;
use saya_harness::runner::sandbox::RunnerSpawn;
#[cfg(not(windows))]
use saya_harness::runner::{RunProgram, SharedCredentialSource, StaticCredentialSource};
#[cfg(not(windows))]
use super::session_definitions;
#[cfg(not(windows))]
use crate::commands::run::runner::place_guard;
pub(crate) const PROBE_REFUSED_NOTICE: &str =
"run_program is unavailable: the sandbox probe did not prove this host.";
pub(crate) struct SessionRunner {
pub(super) spawn: RunnerSpawn,
pub(super) scope: saya_types::RunnerScope,
pub(super) interpreters: Option<saya_types::InterpreterScope>,
pub(super) timeout: Duration,
pub(super) record_dir: PathBuf,
pub(super) definition: saya_agent::ToolDefinition,
}
impl SessionRunner {
pub(super) fn prompt_facts(&self) -> crate::approval_facts::RunnerFacts {
crate::approval_facts::RunnerFacts {
fs_roots: self.spawn.fs_roots().to_vec(),
net_allow: self.spawn.net_allow().to_vec(),
timeout_seconds: self.timeout.as_secs(),
runner_programs: self.scope.programs.clone(),
interpreter_programs: self
.interpreters
.as_ref()
.map(|scope| scope.programs.clone())
.unwrap_or_default(),
credentials_declared: 0,
}
}
}
pub(super) struct RunnerComposition {
pub(super) runner: Option<SessionRunner>,
pub(super) probe_notice: Option<String>,
}
pub(super) fn compose_runner(
runtime: &crate::config::runtime::RuntimeConfig,
workspace_root: &Path,
state_dir: &Path,
) -> Result<RunnerComposition, String> {
let jobs = &runtime.resolved.jobs.runner;
let Some(program_dir) = jobs.program_dir.as_ref() else {
return Ok(RunnerComposition {
runner: None,
probe_notice: None,
});
};
if jobs.allow.is_empty() {
return Ok(RunnerComposition {
runner: None,
probe_notice: None,
});
}
#[cfg(windows)]
{
let _ = (workspace_root, state_dir, program_dir);
Ok(RunnerComposition {
runner: None,
probe_notice: Some(PROBE_REFUSED_NOTICE.to_owned()),
})
}
#[cfg(not(windows))]
{
let sandbox = RunSandbox::new([workspace_root.to_path_buf()], Vec::<(String, u16)>::new())
.map_err(|error| {
format!("the session's sandbox policy could not be composed: {error}")
})?;
let program_dir = place_session(program_dir, sandbox.fs_roots())?;
let provision = sandbox
.prepare(&program_dir)
.map_err(|error| format!("the runner sandbox could not be prepared: {error}"))?;
let Some(spawn) = provision.spawn() else {
return Ok(RunnerComposition {
runner: None,
probe_notice: Some(PROBE_REFUSED_NOTICE.to_owned()),
});
};
let scope = saya_types::RunnerScope::new(jobs.allow.clone()).map_err(|error| {
format!("resolved [jobs.runner] allow is not a usable scope: {error}")
})?;
let interpreters = if runtime.resolved.jobs.interpreter.allow.is_empty() {
None
} else {
Some(
saya_types::InterpreterScope::new(runtime.resolved.jobs.interpreter.allow.clone())
.map_err(|error| {
format!("resolved [jobs.interpreter] allow is not a usable scope: {error}")
})?,
)
};
let timeout = std::time::Duration::from_secs(jobs.timeout_seconds);
let record_dir = state_dir.join("run_program");
let resolver: SharedCredentialSource = Arc::new(StaticCredentialSource::new(Vec::new()));
let definition = session_definitions::run_program(
RunProgram::for_step(
spawn.clone(),
Some(scope.clone()),
interpreters.clone(),
timeout,
resolver,
)
.definition(),
);
Ok(RunnerComposition {
runner: Some(SessionRunner {
spawn: spawn.clone(),
scope,
interpreters,
timeout,
record_dir,
definition,
}),
probe_notice: None,
})
}
}
pub(crate) const RUNNER_GAP_REMEDY: &str =
"programs run only through host commands (run_command), which ask for approval";
#[cfg(not(windows))]
fn place_session(program_dir: &Path, roots: &[PathBuf]) -> Result<PathBuf, String> {
place_guard(
program_dir,
roots,
|dir| {
format!(
"the runner program directory {} could not be resolved — set [jobs.runner] \
program_dir to an existing directory and stage its programs there",
dir.display()
)
},
|canonical, root| {
format!(
"the runner program directory {} overlaps this session's workspace root {} — \
a program directory inside, equal to, or containing the session's workspace \
root lets one session child write the binary the next run_program call \
validates green and executes, and the enforcement cannot express an \
exclusion (Seatbelt subpaths are allow-lists and Landlock has no subtractive \
rights), so a checked-in tool directory cannot be used from a session; \
stage programs outside the workspace root",
canonical.display(),
root.display()
)
},
)
}