use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::agent::process::{
harness_base_env, probe_harness_version, run_harness_probe, untested_version_message,
version_is_tested, VersionProbe,
};
use crate::errors::RuntimeError;
use crate::process_driver::{
check_driver_interface, check_required_env, LoginCheck, LoginOutput, LoginState, ProcessDriver,
StandaloneDriver,
};
use crate::shell::HostAccountEnv;
use crate::types::CapabilityPolicy;
pub const LOGIN_REASON_MAX_CHARS: usize = 200;
pub fn harness_needs_report(
driver_bytes: &[u8],
name: &str,
version: &str,
command: Option<&str>,
policy: &CapabilityPolicy,
workdir: &Path,
) -> crate::harness_needs::HarnessReport {
let driver_ref = format!("{name}@{version}");
let unresolved = |reason: String| crate::harness_needs::unresolved_report(&driver_ref, reason);
let wasm = match crate::artifact::extract_root_wasm(name, version, driver_bytes) {
Ok(wasm) => wasm,
Err(error) => return unresolved(error.to_string()),
};
let contracts = murmur_artifact::extract_wit_contracts(&wasm).ok().flatten();
if let Err(error) = check_driver_interface(
"process",
name,
version,
contracts.as_ref(),
crate::served_wit_level(),
) {
return unresolved(error.to_string());
}
let standalone = match StandaloneDriver::compile_named(&wasm, name, version) {
Ok(standalone) => standalone,
Err(error) => return unresolved(error.to_string()),
};
standalone.rt.block_on(async {
let description = match async {
let mut driver = standalone.instantiate().await?;
driver.describe().await
}
.await
{
Ok(description) => description,
Err(error) => return unresolved(error.to_string()),
};
let binary = crate::runtime::resolve_harness_binary(command, &description.binary)
.ok()
.map(|(path, _)| path);
crate::harness_needs::resolve_capsule_harness(
&driver_ref,
&description,
binary.as_deref(),
policy,
workdir,
)
.grants
.report
})
}
#[derive(Debug)]
pub struct HarnessCheck {
pub harness: String,
pub binary: HarnessBinary,
pub version: Option<HarnessVersion>,
pub login: Option<HarnessLogin>,
}
#[derive(Debug)]
pub enum HarnessBinary {
Resolved { path: PathBuf, source: String },
NotFound(RuntimeError),
}
#[derive(Debug, PartialEq, Eq)]
pub enum HarnessVersion {
Tested { line: String },
Untested { message: String },
}
#[derive(Debug, PartialEq, Eq)]
pub enum HarnessLogin {
LoggedIn,
LoggedOut {
reason: String,
},
NotChecked {
reason: String,
},
}
pub fn check_process_harness(
driver_bytes: &[u8],
name: &str,
version: &str,
command: Option<&str>,
policy: &CapabilityPolicy,
cwd: &Path,
) -> Result<HarnessCheck, RuntimeError> {
let wasm = crate::artifact::extract_root_wasm(name, version, driver_bytes)?;
check_process_harness_wasm(
&wasm,
name,
version,
command,
&harness_base_env(policy, &HostAccountEnv::from_host()),
policy,
cwd,
)
}
fn check_process_harness_wasm(
wasm: &[u8],
name: &str,
version: &str,
command: Option<&str>,
probe_env: &BTreeMap<String, String>,
policy: &CapabilityPolicy,
cwd: &Path,
) -> Result<HarnessCheck, RuntimeError> {
let contracts = murmur_artifact::extract_wit_contracts(wasm).ok().flatten();
check_driver_interface(
"process",
name,
version,
contracts.as_ref(),
crate::served_wit_level(),
)?;
let standalone = StandaloneDriver::compile_named(wasm, name, version)?;
standalone.rt.block_on(async {
let mut driver = standalone.instantiate().await?;
let description = driver.describe().await?;
check_required_env(name, version, &description, &policy.env_allow)?;
let harness = description.harness.clone();
let resolved = crate::runtime::resolve_harness_binary(command, &description.binary);
let grants = crate::harness_needs::resolve_capsule_harness(
&format!("{name}@{version}"),
&description,
resolved.as_ref().ok().map(|(path, _)| path.as_path()),
policy,
cwd,
)
.grants;
if let Some(refusal) = grants.refusal {
return Err(refusal);
}
let (path, source) = match resolved {
Ok(resolved) => resolved,
Err(error) => {
return Ok(HarnessCheck {
harness,
binary: HarnessBinary::NotFound(error),
version: None,
login: None,
})
}
};
let probe =
probe_harness_version(&path, &description.version_args, probe_env, cwd, None).await;
let version = match &probe {
VersionProbe::Read(line) if version_is_tested(line, &description.tested_versions) => {
HarnessVersion::Tested { line: line.clone() }
}
_ => HarnessVersion::Untested {
message: untested_version_message(&probe, &description.tested_versions),
},
};
let login = check_login(&mut driver, &description.login_check, &path, probe_env, cwd).await;
Ok(HarnessCheck {
harness,
binary: HarnessBinary::Resolved { path, source },
version: Some(version),
login: Some(login),
})
})
}
async fn check_login(
driver: &mut ProcessDriver,
login_check: &LoginCheck,
binary: &Path,
probe_env: &BTreeMap<String, String>,
cwd: &Path,
) -> HarnessLogin {
let args = match login_check {
LoginCheck::Unavailable(reason) => {
return HarnessLogin::NotChecked {
reason: login_reason(reason),
}
}
LoginCheck::Command(args) => args,
};
let output = match run_harness_probe(binary, args, probe_env, cwd, None).await {
Ok(output) => output,
Err(reason) => {
return HarnessLogin::NotChecked {
reason: login_reason(&reason),
}
}
};
let output = LoginOutput {
exit_code: output.exit_code,
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
};
match driver.read_login(output).await {
Ok(LoginState::LoggedIn) => HarnessLogin::LoggedIn,
Ok(LoginState::LoggedOut(reason)) => HarnessLogin::LoggedOut {
reason: login_reason(&reason),
},
Ok(LoginState::Unknown(reason)) => HarnessLogin::NotChecked {
reason: login_reason(&reason),
},
Err(error) => HarnessLogin::NotChecked {
reason: login_reason(&error.to_string()),
},
}
}
fn login_reason(reason: &str) -> String {
let first_line = reason.lines().next().unwrap_or_default().trim();
first_line.chars().take(LOGIN_REASON_MAX_CHARS).collect()
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
use super::*;
const DRIVER: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../murmur-cli/tests/fixtures/process-driver/tool/process-driver.wasm"
));
const NO_LOGIN_CHECK_DRIVER: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../murmur-cli/tests/fixtures/process-driver/tool/process-driver-no-login-check.wasm"
));
struct RestoreEnv(Vec<(&'static str, Option<std::ffi::OsString>)>);
impl RestoreEnv {
fn set(vars: &[(&'static str, &str)]) -> Self {
let previous = vars
.iter()
.map(|(name, value)| {
let old = std::env::var_os(name);
std::env::set_var(name, value);
(*name, old)
})
.collect();
Self(previous)
}
}
impl Drop for RestoreEnv {
fn drop(&mut self) {
for (name, old) in self.0.drain(..).rev() {
match old {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
}
}
fn policy() -> CapabilityPolicy {
CapabilityPolicy {
env_allow: vec!["HOME".to_string(), "FIXTURE_HARNESS_PROFILE".to_string()],
..CapabilityPolicy::default()
}
}
fn harness(dir: &TempDir, body: &str) -> PathBuf {
let path = dir.path().join("harness");
std::fs::write(&path, format!("#!/bin/bash\n{body}\n")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
fn check(
wasm: &[u8],
binary: &Path,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> HarnessCheck {
check_process_harness_wasm(
wasm,
"local/fixture-driver",
"1.0.0",
binary.to_str(),
env,
&policy(),
cwd,
)
.unwrap()
}
#[test]
fn harness_check_probes_run_with_the_harness_base_env() {
let home = std::env::var("HOME").expect("the test process has a HOME");
let env = {
let _restore = RestoreEnv::set(&[
("FIXTURE_HARNESS_PROFILE", "declared"),
("MURMUR_TEST_HARNESS_CHECK_UNDECLARED", "leak"),
]);
harness_base_env(
&policy(),
&HostAccountEnv::from_pairs(&[("USER", "alice"), ("LOGNAME", "alice-login")]),
)
};
let dir = TempDir::new().unwrap();
let binary = harness(
&dir,
&r#"ok=no
if [ "${USER:-}" = alice ] && [ "${LOGNAME:-}" = alice-login ] \
&& [ "${FIXTURE_HARNESS_PROFILE:-}" = declared ] && [ "${HOME:-}" = '@HOME@' ] \
&& [ -z "${MURMUR_TEST_HARNESS_CHECK_UNDECLARED:-}" ]; then ok=yes; fi
if [ "$1" = auth ] && [ "$2" = status ]; then
if [ $ok = yes ]; then echo '{"loggedIn": true}'; else echo '{"loggedIn": false}'; fi
exit 0
fi
if [ $ok = yes ]; then echo "fixture-harness 1.0.0"; else echo "fixture-harness 0.0.1"; fi"#
.replace("@HOME@", &home),
);
let found = check(DRIVER, &binary, &env, dir.path());
assert_eq!(found.harness, "fixture-harness");
match &found.binary {
HarnessBinary::Resolved { path, source } => {
assert_eq!(path, &std::fs::canonicalize(&binary).unwrap());
assert_eq!(source, "inference.command");
}
other => panic!("unexpected binary: {other:?}"),
}
assert_eq!(
found.version,
Some(HarnessVersion::Tested {
line: "fixture-harness 1.0.0".to_string()
})
);
assert_eq!(found.login, Some(HarnessLogin::LoggedIn));
}
#[test]
fn harness_check_reports_the_drivers_logged_out_reason_only() {
let dir = TempDir::new().unwrap();
let binary = harness(
&dir,
r#"if [ "$1" = auth ]; then echo '{"loggedIn": false, "token": "secret-77"}'; exit 0; fi
echo "fixture-harness 1.0.0""#,
);
let found = check(DRIVER, &binary, &BTreeMap::new(), dir.path());
let Some(HarnessLogin::LoggedOut { reason }) = &found.login else {
panic!("unexpected login: {:?}", found.login);
};
assert_eq!(
reason,
"fixture-harness is not logged in for this user; run fixture-cli auth login"
);
assert!(!format!("{found:?}").contains("secret-77"));
}
#[test]
fn harness_check_cannot_tell_without_a_login_command_or_a_readable_answer() {
let dir = TempDir::new().unwrap();
let binary = harness(
&dir,
r#"if [ "$1" = auth ]; then echo 'not json'; exit 0; fi
echo "fixture-harness 1.0.0""#,
);
let unavailable = check(NO_LOGIN_CHECK_DRIVER, &binary, &BTreeMap::new(), dir.path());
assert_eq!(
unavailable.login,
Some(HarnessLogin::NotChecked {
reason: "fixture-harness has no way to report its login without a turn".to_string()
})
);
let unreadable = check(DRIVER, &binary, &BTreeMap::new(), dir.path());
assert_eq!(
unreadable.login,
Some(HarnessLogin::NotChecked {
reason: "fixture-harness printed no loggedIn field".to_string()
})
);
}
#[test]
fn harness_check_gives_the_same_timeout_reason_for_login_and_version() {
let dir = TempDir::new().unwrap();
let binary = harness(&dir, "exec sleep 30");
let found = check(DRIVER, &binary, &BTreeMap::new(), dir.path());
let reason = "it did not answer within 10s";
assert_eq!(
found.login,
Some(HarnessLogin::NotChecked {
reason: reason.to_string()
})
);
let Some(HarnessVersion::Untested { message }) = &found.version else {
panic!("unexpected version: {:?}", found.version);
};
assert_eq!(
message,
&format!(
"the harness version could not be read: {reason}; this process driver is tested \
against 1.0.0"
)
);
}
#[test]
fn harness_check_reports_an_untested_version_in_the_runs_words() {
let dir = TempDir::new().unwrap();
let binary = harness(
&dir,
r#"if [ "$1" = auth ]; then echo '{"loggedIn": true}'; exit 0; fi
echo "fixture-harness 0.9.0""#,
);
let found = check(DRIVER, &binary, &BTreeMap::new(), dir.path());
assert_eq!(
found.version,
Some(HarnessVersion::Untested {
message: untested_version_message(
&VersionProbe::Read("fixture-harness 0.9.0".to_string()),
&["1.0.0".to_string()]
)
})
);
}
#[test]
fn harness_check_runs_no_probe_for_a_binary_that_does_not_resolve() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("no-such-harness");
let found = check(DRIVER, &missing, &BTreeMap::new(), dir.path());
match &found.binary {
HarnessBinary::NotFound(RuntimeError::HarnessBinaryNotFound {
binary,
binary_source,
}) => {
assert_eq!(binary, missing.to_str().unwrap());
assert_eq!(binary_source, "inference.command");
}
other => panic!("unexpected binary: {other:?}"),
}
assert!(found.version.is_none());
assert!(found.login.is_none());
}
#[test]
fn harness_check_refuses_an_undeclared_required_variable() {
let dir = TempDir::new().unwrap();
let err = check_process_harness_wasm(
DRIVER,
"local/fixture-driver",
"1.0.0",
Some("/bin/true"),
&BTreeMap::new(),
&CapabilityPolicy::default(),
dir.path(),
)
.unwrap_err();
assert!(
matches!(err, RuntimeError::ProcessDriverRequiredEnvNotAllowed { .. }),
"{err:?}"
);
}
#[test]
fn harness_check_login_reason_is_one_line_of_at_most_200_chars() {
assert_eq!(login_reason(" first line \nsecond line"), "first line");
let long = "é".repeat(300);
let cut = login_reason(&long);
assert_eq!(cut.chars().count(), LOGIN_REASON_MAX_CHARS);
assert_eq!(login_reason(""), "");
}
}