pub(crate) struct HostLaunch {
deny: Vec<String>,
config: saya_config::ResolvedHostCommands,
}
impl HostLaunch {
pub(crate) fn from_options(
options: &crate::cli::GlobalOptions,
runtime: &RuntimeConfig,
) -> Self {
Self {
deny: options.deny.clone(),
config: runtime.resolved.host_commands.clone(),
}
}
pub(crate) fn unstated(runtime: &RuntimeConfig) -> Self {
Self {
deny: Vec::new(),
config: runtime.resolved.host_commands.clone(),
}
}
#[cfg(test)]
pub(crate) fn for_tests_stated(runtime: &RuntimeConfig) -> Self {
Self::from_options(
&crate::cli::GlobalOptions {
deny: Vec::new(),
..Default::default()
},
runtime,
)
}
#[cfg(test)]
pub(crate) fn from_deny_for_tests(deny: Vec<String>) -> Self {
Self {
deny,
config: saya_config::ResolvedHostCommands::default(),
}
}
pub(crate) fn deny_list(&self) -> Vec<String> {
self.deny.clone()
}
}
use crate::config::runtime::RuntimeConfig;
#[cfg(not(windows))]
pub(crate) const NO_PATH_NOTICE: &str = "No PATH is set, so the host-command lane is not composed: \
run_command is unavailable; set PATH to reach host programs.";
#[cfg(windows)]
pub(crate) const WINDOWS_HOST_UNAVAILABLE_NOTICE: &str = "Host commands are unavailable on Windows, so the host-command lane is not composed: \
run_command is unavailable; host execution remains refused until the runner supports Windows.";
pub(crate) fn compose_host_lane(
launch: &HostLaunch,
workspace_root: Option<&std::path::Path>,
path: Option<String>,
) -> Result<(Option<SessionHost>, Option<String>), String> {
let Some(root) = workspace_root else {
return Ok((None, None));
};
#[cfg(windows)]
{
let _ = (launch, root, path);
Ok((None, Some(WINDOWS_HOST_UNAVAILABLE_NOTICE.to_owned())))
}
#[cfg(not(windows))]
{
let Some(path_value) = path else {
return Ok((None, Some(NO_PATH_NOTICE.to_owned())));
};
Ok((compose_host(launch, Some(root), path_value)?, None))
}
}
pub(crate) struct SessionHost {
pub(crate) config: saya_harness::host::HostConfig,
pub(crate) facts: crate::approval_facts::HostFacts,
}
pub(crate) fn compose_host(
launch: &HostLaunch,
workspace_root: Option<&std::path::Path>,
path_value: String,
) -> Result<Option<SessionHost>, String> {
let Some(root) = workspace_root else {
return Ok(None);
};
if cfg!(windows) {
return Err(
"host commands are unavailable on Windows: the process-group kill is \
unix-verified (the runner's posture), so the lane fails closed"
.to_owned(),
);
}
let timeout = std::time::Duration::from_secs(launch.config.timeout_seconds);
let mut config =
saya_harness::host::HostConfig::new(path_value, root.to_path_buf(), timeout)
.map_err(|error| format!("the host-command lane could not be composed: {error}"))?;
let mut pass_env = Vec::new();
for name in &launch.config.pass_env {
let value = std::env::var(name).map_err(|_| {
format!("host command refused: `pass_env` names `{name}`, which is not set")
})?;
pass_env.push((name.clone(), value));
}
config = config
.with_extra_env(pass_env)
.map_err(|error| format!("the host-command lane could not be composed: {error}"))?;
Ok(Some(SessionHost {
config,
facts: crate::approval_facts::HostFacts {
workspace_root: root.to_path_buf(),
timeout_seconds: launch.config.timeout_seconds,
pass_env: launch.config.pass_env.clone(),
},
}))
}