use crate::fleet::audit::AuditLogger;
use crate::fleet::history::HistoryManager;
use crate::fleet::plan::{DeploymentPlan, DeploymentStatus, DeploymentStrategy};
use crate::fleet::progress::{DeployPhase, FleetProgress};
use crate::fleet::rollback::{
MAX_BACKUPS_PER_WORKER, REMOTE_BACKUP_DIR, REMOTE_RCH_PATH, RollbackManager, WorkerBackup,
};
use crate::fleet::ssh::{CommandOutput, FleetSshError, SshExecutor};
use crate::ui::context::OutputContext;
use crate::ui::theme::StatusIndicator;
use anyhow::Result;
use crate::error::{FleetError, SshError};
use futures::future::BoxFuture;
use rch_common::capability_probe::{
CapabilityRequirement, CapabilityVerdict, NamedToolProbe, ProbeSpec, assess_admissibility,
build_capability_probe_script, parse_capability_probe, remote_worker_binary_path,
};
use rch_common::disk_pressure_report::{
DiskRootKind, PressureLevel, PressureThresholds, assess_root,
};
use rch_common::fleet_provenance::{
ArtifactProvenance, FleetDeployAuditRecord, ProvenancePolicy, ProvenanceVerdict,
SignatureCheck, verify_artifact_provenance,
};
use rch_common::fleet_smoke_profile::{SmokeProfileEvent, SmokeScenario};
use rch_common::ssh_utils::shell_escape_path_with_home;
use rch_common::{IncidentReasonCode, WorkerCapabilities, WorkerConfig, WorkerId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
const REMOTE_WORKER_BINARY: &str = "~/.local/bin/rch-wkr";
static STAGED_BINARY_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum FleetResult {
Success {
deployed: usize,
skipped: usize,
failed: usize,
},
CanaryFailed { reason: String },
CanaryPending {
promoted: usize,
remaining: usize,
skipped: usize,
failed: usize,
},
Aborted { reason: String },
}
pub struct FleetExecutor {
parallelism: usize,
audit: Option<Arc<Mutex<AuditLogger>>>,
worker_configs: Arc<HashMap<String, WorkerConfig>>,
local_binary: PathBuf,
}
impl FleetExecutor {
pub fn new(
parallelism: usize,
audit: Option<AuditLogger>,
workers: &[&WorkerConfig],
local_binary: PathBuf,
) -> Result<Self> {
let worker_configs: HashMap<String, WorkerConfig> = workers
.iter()
.map(|w| (w.id.0.clone(), (*w).clone()))
.collect();
Ok(Self {
parallelism,
audit: audit.map(|a| Arc::new(Mutex::new(a))),
worker_configs: Arc::new(worker_configs),
local_binary,
})
}
pub async fn execute(
&self,
mut plan: DeploymentPlan,
ctx: &OutputContext,
) -> Result<FleetResult> {
let style = ctx.theme();
if let Some(ref audit) = self.audit {
let mut audit = audit.lock().await;
let strategy_str = match &plan.strategy {
DeploymentStrategy::AllAtOnce { parallelism } => {
format!("all-at-once({})", parallelism)
}
DeploymentStrategy::Canary { percent, .. } => format!("canary({}%)", percent),
DeploymentStrategy::Rolling { batch_size, .. } => {
format!("rolling({})", batch_size)
}
};
audit.log_deployment_started(
plan.id,
&plan.target_version,
plan.workers.len(),
&strategy_str,
)?;
}
let mut deployed = 0;
let mut skipped = 0;
let mut failed = 0;
let mut canary_pending_remaining: Option<usize> = None;
let strategy = plan.strategy.clone();
let worker_count = plan.workers.len();
let worker_ids: Vec<WorkerId> = plan
.workers
.iter()
.map(|w| WorkerId(w.worker_id.clone()))
.collect();
let progress = Arc::new(FleetProgress::new(ctx, &worker_ids));
match strategy {
DeploymentStrategy::AllAtOnce { parallelism } => {
let results = self
.deploy_batch(
&mut plan,
0..worker_count,
parallelism,
ctx,
progress.clone(),
)
.await?;
for (idx, success) in results {
if success {
if plan.workers[idx].status == DeploymentStatus::Skipped {
skipped += 1;
} else {
deployed += 1;
}
} else {
failed += 1;
}
}
}
DeploymentStrategy::Canary {
percent,
wait_secs,
auto_promote,
} => {
let canary_count = ((worker_count * (percent as usize)) / 100).max(1);
if !ctx.is_json() {
println!(
" {} Deploying to {} canary worker(s)...",
style.muted("→"),
canary_count
);
}
let canary_results = self
.deploy_batch(
&mut plan,
0..canary_count,
self.parallelism,
ctx,
progress.clone(),
)
.await?;
let canary_failed = canary_results.iter().filter(|(_, s)| !s).count();
if canary_failed > 0 {
progress.finish();
return Ok(FleetResult::CanaryFailed {
reason: format!("{} canary worker(s) failed", canary_failed),
});
}
if !ctx.is_json() {
println!(
" {} Canary successful. Waiting {}s before full rollout...",
StatusIndicator::Success.display(style),
wait_secs
);
}
tokio::time::sleep(std::time::Duration::from_secs(wait_secs)).await;
for (idx, success) in &canary_results {
if *success {
if plan.workers[*idx].status == DeploymentStatus::Skipped {
skipped += 1;
} else {
deployed += 1;
}
} else {
failed += 1;
}
}
if canary_count < worker_count {
if auto_promote {
if !ctx.is_json() {
println!(" {} Deploying to remaining workers...", style.muted("→"));
}
let remaining_results = self
.deploy_batch(
&mut plan,
canary_count..worker_count,
self.parallelism,
ctx,
progress.clone(),
)
.await?;
for (idx, success) in remaining_results {
if success {
if plan.workers[idx].status == DeploymentStatus::Skipped {
skipped += 1;
} else {
deployed += 1;
}
} else {
failed += 1;
}
}
} else {
canary_pending_remaining = Some(worker_count - canary_count);
}
}
}
DeploymentStrategy::Rolling {
batch_size,
wait_between,
} => {
let batch_size = batch_size.max(1);
let mut start = 0;
let mut batch_num = 0;
while start < worker_count {
let end = (start + batch_size).min(worker_count);
batch_num += 1;
if !ctx.is_json() {
println!(
" {} Batch {}: deploying to workers {}..{}",
style.muted("→"),
batch_num,
start + 1,
end
);
}
let batch_results = self
.deploy_batch(&mut plan, start..end, batch_size, ctx, progress.clone())
.await?;
for (idx, success) in batch_results {
if success {
if plan.workers[idx].status == DeploymentStatus::Skipped {
skipped += 1;
} else {
deployed += 1;
}
} else {
failed += 1;
}
}
start = end;
if start < worker_count {
if !ctx.is_json() {
println!(
" {} Waiting {}s before next batch...",
style.muted("→"),
wait_between
);
}
tokio::time::sleep(std::time::Duration::from_secs(wait_between)).await;
}
}
}
}
progress.finish();
if let Some(remaining) = canary_pending_remaining {
Ok(FleetResult::CanaryPending {
promoted: deployed,
remaining,
skipped,
failed,
})
} else {
Ok(FleetResult::Success {
deployed,
skipped,
failed,
})
}
}
async fn deploy_batch(
&self,
plan: &mut DeploymentPlan,
range: std::ops::Range<usize>,
parallelism: usize,
ctx: &OutputContext,
progress: Arc<FleetProgress>,
) -> Result<Vec<(usize, bool)>> {
use tokio::sync::Semaphore;
let effective_parallelism = parallelism.max(1);
let semaphore = Arc::new(Semaphore::new(effective_parallelism));
let mut handles = Vec::new();
let style = ctx.theme();
let is_json = ctx.is_json();
let policy = resolve_provenance_policy();
for idx in range.clone() {
let permit = semaphore.clone().acquire_owned().await?;
let worker_id = plan.workers[idx].worker_id.clone();
let target_version = plan.workers[idx].target_version.clone();
let current_version = plan.workers[idx].current_version.clone();
let force = plan.options.force;
let progress = progress.clone();
let worker_configs = self.worker_configs.clone();
let local_binary = self.local_binary.clone();
let run_id = plan.id.to_string();
let handle = tokio::spawn(async move {
let _permit = permit;
let worker_config = match worker_configs.get(&worker_id) {
Some(cfg) => cfg.clone(),
None => {
progress
.worker_failed(&worker_id, "worker config not found")
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
};
if !force && current_version.as_ref() == Some(&target_version) {
progress
.worker_skipped(&worker_id, "already at version")
.await;
return (idx, worker_id, true, DeploymentStatus::Skipped);
}
progress
.set_phase(&worker_id, DeployPhase::Connecting)
.await;
if let Err(e) = test_ssh_connectivity(&worker_config).await {
progress
.worker_failed(&worker_id, &format!("SSH failed: {}", e))
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
if let Err(e) = ensure_binary_matches_worker(&worker_config, &local_binary).await {
if let Some(reason) = platform_mismatch_skip_reason(&e) {
progress
.worker_skipped_with_notice(&worker_id, &reason)
.await;
return (idx, worker_id, true, DeploymentStatus::Skipped);
}
progress.worker_failed(&worker_id, &format!("{}", e)).await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
let mut previous_artifact_id: Option<String> = None;
if let Ok(mut rollback_manager) = RollbackManager::new() {
match backup_before_deploy(&worker_config, &mut rollback_manager).await {
Ok(Some(backup)) => {
previous_artifact_id = Some(backup.version.clone());
debug!(
worker = %worker_id,
version = %backup.version,
"Backup created, proceeding with deploy"
);
}
Ok(None) => {
debug!(worker = %worker_id, "No backup created (no existing version or skipped)");
}
Err(e) => {
warn!(worker = %worker_id, error = %e, "Backup failed (continuing deploy)");
}
}
}
progress.set_phase(&worker_id, DeployPhase::Uploading).await;
let gate_start = Instant::now();
let worker_triple = if provenance_manifest_path(&local_binary).exists() {
discover_worker_target_facts(&worker_config)
.await
.and_then(|facts| worker_target_triple(&facts))
} else {
None
};
let (verdict, provenance) =
run_provenance_gate(&local_binary, worker_triple.as_deref(), &policy).await;
let mut audit = FleetDeployAuditRecord::from_verdict(
run_id.clone(),
PROVENANCE_BEAD_ID,
worker_id.as_str(),
worker_config.user.as_str(),
REMOTE_WORKER_BINARY,
&provenance,
previous_artifact_id.clone(),
now_unix_ms(),
&verdict,
"operator",
provenance_detail(&verdict),
);
audit.set_duration_ms(
u64::try_from(gate_start.elapsed().as_millis()).unwrap_or(u64::MAX),
);
persist_provenance_audit(&audit);
if !verdict.may_transfer() {
let reason = verdict.reason_code().unwrap_or("provenance_rejected");
progress
.worker_failed(&worker_id, &format!("provenance gate rejected: {reason}"))
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
if let Err(e) = create_remote_directory(&worker_config).await {
progress
.worker_failed(&worker_id, &format!("mkdir failed: {}", e))
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
if let Err(e) = copy_binary_via_scp(&worker_config, &local_binary).await {
progress
.worker_failed(&worker_id, &format!("scp failed: {}", e))
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
progress
.set_phase(&worker_id, DeployPhase::Installing)
.await;
if let Err(e) = set_executable_permissions(&worker_config).await {
progress
.worker_failed(&worker_id, &format!("chmod failed: {}", e))
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
progress.set_phase(&worker_id, DeployPhase::Verifying).await;
if let Err(e) = verify_installation(&worker_config).await {
progress
.worker_failed(&worker_id, &format!("verify failed: {}", e))
.await;
return (idx, worker_id, false, DeploymentStatus::Failed);
}
progress.worker_complete(&worker_id, &target_version).await;
debug!(
"Successfully deployed {} to worker {}",
target_version, worker_id
);
(idx, worker_id, true, DeploymentStatus::Completed)
});
handles.push(handle);
}
let mut results = Vec::new();
for handle in handles {
let (idx, _worker_id, success, status) = handle.await?;
plan.workers[idx].status = status;
results.push((idx, success));
}
let _ = (style, is_json);
Ok(results)
}
}
async fn test_ssh_connectivity(worker: &WorkerConfig) -> Result<()> {
let ssh = SshExecutor::new(worker);
if ssh.check_connectivity().await? {
Ok(())
} else {
Err(SshError::ConnectionFailed {
host: worker.host.clone(),
user: worker.user.clone(),
key_path: worker.identity_file.clone().into(),
message: "connectivity check returned false".to_string(),
}
.into())
}
}
async fn create_remote_directory(worker: &WorkerConfig) -> Result<()> {
let ssh = SshExecutor::new(worker);
ssh.create_directory("~/.local/bin")
.await
.map_err(|e| anyhow::anyhow!("mkdir failed: {}", e))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BinaryOs {
Linux,
MacOs,
Windows,
}
impl BinaryOs {
fn as_str(self) -> &'static str {
match self {
BinaryOs::Linux => "linux",
BinaryOs::MacOs => "macos",
BinaryOs::Windows => "windows",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BinaryArch {
X86_64,
Aarch64,
X86,
Arm,
}
impl BinaryArch {
fn as_str(self) -> &'static str {
match self {
BinaryArch::X86_64 => "x86_64",
BinaryArch::Aarch64 => "aarch64",
BinaryArch::X86 => "x86",
BinaryArch::Arm => "arm",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BinaryPlatform {
os: BinaryOs,
arch: Option<BinaryArch>,
}
impl std::fmt::Display for BinaryPlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.arch {
Some(arch) => write!(f, "{}/{}", self.os.as_str(), arch.as_str()),
None => write!(f, "{}/unknown", self.os.as_str()),
}
}
}
const BINARY_MAGIC_PROBE_LEN: usize = 20;
fn detect_binary_platform(header: &[u8]) -> Option<BinaryPlatform> {
if header.len() >= 2 && &header[0..2] == b"MZ" {
return Some(BinaryPlatform {
os: BinaryOs::Windows,
arch: None,
});
}
if header.len() >= 20 && header[0..4] == [0x7F, b'E', b'L', b'F'] {
let little_endian = header[5] != 2;
let machine = if little_endian {
u16::from_le_bytes([header[18], header[19]])
} else {
u16::from_be_bytes([header[18], header[19]])
};
let arch = match machine {
0x3E => Some(BinaryArch::X86_64), 0xB7 => Some(BinaryArch::Aarch64), 0x03 => Some(BinaryArch::X86), 0x28 => Some(BinaryArch::Arm), _ => None,
};
return Some(BinaryPlatform {
os: BinaryOs::Linux,
arch,
});
}
if header.len() >= 8 {
let magic = &header[0..4];
let is_macho_le = magic == [0xCF, 0xFA, 0xED, 0xFE] || magic == [0xCE, 0xFA, 0xED, 0xFE];
let is_macho_be = magic == [0xFE, 0xED, 0xFA, 0xCF] || magic == [0xFE, 0xED, 0xFA, 0xCE];
if is_macho_le || is_macho_be {
let cputype = if is_macho_le {
u32::from_le_bytes([header[4], header[5], header[6], header[7]])
} else {
u32::from_be_bytes([header[4], header[5], header[6], header[7]])
};
let arch = match cputype {
0x0100_0007 => Some(BinaryArch::X86_64), 0x0100_000C => Some(BinaryArch::Aarch64), 0x0000_0007 => Some(BinaryArch::X86), 0x0000_000C => Some(BinaryArch::Arm), _ => None,
};
return Some(BinaryPlatform {
os: BinaryOs::MacOs,
arch,
});
}
if magic == [0xCA, 0xFE, 0xBA, 0xBE] || magic == [0xBE, 0xBA, 0xFE, 0xCA] {
return Some(BinaryPlatform {
os: BinaryOs::MacOs,
arch: None,
});
}
}
None
}
fn parse_worker_platform(uname_s: &str, uname_m: &str) -> Option<BinaryPlatform> {
let os = match uname_s.trim().to_ascii_lowercase().as_str() {
"linux" => BinaryOs::Linux,
"darwin" => BinaryOs::MacOs,
s if s.contains("mingw") || s.contains("msys") || s.contains("cygwin") => BinaryOs::Windows,
_ => return None,
};
let arch = match uname_m.trim().to_ascii_lowercase().as_str() {
"x86_64" | "amd64" => Some(BinaryArch::X86_64),
"aarch64" | "arm64" => Some(BinaryArch::Aarch64),
"i386" | "i486" | "i586" | "i686" | "x86" => Some(BinaryArch::X86),
m if m.starts_with("armv") || m == "arm" => Some(BinaryArch::Arm),
_ => None,
};
Some(BinaryPlatform { os, arch })
}
fn platforms_compatible(local: BinaryPlatform, worker: BinaryPlatform) -> bool {
if local.os != worker.os {
return false;
}
match (local.arch, worker.arch) {
(Some(a), Some(b)) => a == b,
_ => true,
}
}
fn read_binary_header(path: &Path) -> std::io::Result<Vec<u8>> {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut buf = vec![0u8; BINARY_MAGIC_PROBE_LEN];
let mut read = 0;
while read < buf.len() {
match file.read(&mut buf[read..])? {
0 => break,
n => read += n,
}
}
buf.truncate(read);
Ok(buf)
}
async fn detect_worker_platform(worker: &WorkerConfig) -> Option<BinaryPlatform> {
let ssh = SshExecutor::new(worker);
let output = ssh
.run_command("uname -s; uname -m")
.await
.ok()
.filter(CommandOutput::success)?;
let mut lines = output.stdout.lines();
let uname_s = lines.next()?;
let uname_m = lines.next().unwrap_or("");
parse_worker_platform(uname_s, uname_m)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] enum LinuxLibc {
Musl,
Gnu,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)] struct WorkerTargetFacts {
platform: BinaryPlatform,
libc: LinuxLibc,
remote_user: String,
rch_wkr_path: Option<String>,
}
#[allow(dead_code)] fn worker_target_triple(facts: &WorkerTargetFacts) -> Option<String> {
let arch = facts.platform.arch?;
let arch_str = match arch {
BinaryArch::X86_64 => "x86_64",
BinaryArch::Aarch64 => "aarch64",
BinaryArch::X86 | BinaryArch::Arm => return None,
};
match facts.platform.os {
BinaryOs::Linux => {
let libc = match facts.libc {
LinuxLibc::Gnu => "gnu",
LinuxLibc::Musl | LinuxLibc::Unknown => "musl",
};
Some(format!("{arch_str}-unknown-linux-{libc}"))
}
BinaryOs::MacOs => Some(format!("{arch_str}-apple-darwin")),
BinaryOs::Windows => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)] struct FleetArtifact {
name: String,
target_triple: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)] enum ArtifactResolveError {
UnknownTriple { worker_id: String },
NoCompatibleArtifact {
worker_id: String,
triple: String,
available: Vec<String>,
},
}
impl std::fmt::Display for ArtifactResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArtifactResolveError::UnknownTriple { worker_id } => write!(
f,
"cannot resolve fleet artifact for {worker_id}: worker target triple is unknown"
),
ArtifactResolveError::NoCompatibleArtifact {
worker_id,
triple,
available,
} => write!(
f,
"no fleet artifact for {worker_id} (target {triple}); available: [{}]",
available.join(", ")
),
}
}
}
#[allow(dead_code)] fn resolve_worker_artifact<'a>(
worker_id: &str,
triple: &str,
artifacts: &'a [FleetArtifact],
) -> Result<&'a FleetArtifact, ArtifactResolveError> {
artifacts
.iter()
.find(|a| a.target_triple == triple || a.name.contains(triple))
.ok_or_else(|| ArtifactResolveError::NoCompatibleArtifact {
worker_id: worker_id.to_string(),
triple: triple.to_string(),
available: artifacts.iter().map(|a| a.target_triple.clone()).collect(),
})
}
#[allow(dead_code)] async fn discover_worker_target_facts(worker: &WorkerConfig) -> Option<WorkerTargetFacts> {
let platform = detect_worker_platform(worker).await?;
let ssh = SshExecutor::new(worker);
let libc = if platform.os == BinaryOs::Linux {
match ssh
.run_command(
"ls /lib/ld-musl-* >/dev/null 2>&1 && echo musl || (ldd --version 2>&1 | head -1)",
)
.await
{
Ok(out) if out.success() => {
let lower = out.stdout.to_ascii_lowercase();
if lower.contains("musl") {
LinuxLibc::Musl
} else if lower.contains("gnu") || lower.contains("glibc") {
LinuxLibc::Gnu
} else {
LinuxLibc::Unknown
}
}
_ => LinuxLibc::Unknown,
}
} else {
LinuxLibc::Unknown
};
let rch_wkr_path = match ssh
.run_command("command -v rch-wkr 2>/dev/null || echo ~/.local/bin/rch-wkr")
.await
{
Ok(out) if out.success() => out.stdout.lines().next().map(str::to_string),
_ => None,
};
Some(WorkerTargetFacts {
platform,
libc,
remote_user: worker.user.clone(),
rch_wkr_path,
})
}
const PROVENANCE_BEAD_ID: &str = "bd-session-history-remediation-ocv9i.7.4";
const FLEET_PROVENANCE_ENV: &str = "RCH_FLEET_PROVENANCE";
fn resolve_provenance_policy() -> ProvenancePolicy {
ProvenancePolicy::from_env_value(std::env::var(FLEET_PROVENANCE_ENV).ok().as_deref())
}
fn provenance_manifest_path(local_binary: &Path) -> PathBuf {
let mut name = local_binary.as_os_str().to_os_string();
name.push(".provenance.json");
PathBuf::from(name)
}
fn load_artifact_provenance(local_binary: &Path, fallback_triple: &str) -> ArtifactProvenance {
let manifest = provenance_manifest_path(local_binary);
match std::fs::read(&manifest) {
Ok(bytes) => match serde_json::from_slice::<ArtifactProvenance>(&bytes) {
Ok(provenance) => return provenance,
Err(e) => warn!(
path = %manifest.display(),
error = %e,
"provenance manifest present but unparseable; treating as dev artifact"
),
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => warn!(
path = %manifest.display(),
error = %e,
"could not read provenance manifest; treating as dev artifact"
),
}
let name = local_binary
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("rch-wkr")
.to_string();
ArtifactProvenance::dev_artifact(name, fallback_triple)
}
async fn evaluate_signature_check(
provenance: &ArtifactProvenance,
local_binary: &Path,
) -> SignatureCheck {
let Some(sig) = provenance.signature.as_ref() else {
return SignatureCheck::Absent;
};
let bundle = PathBuf::from(&sig.bundle_ref);
match crate::update::verify::verify_signature(local_binary, &bundle).await {
Ok(true) => SignatureCheck::Valid,
Ok(false) => SignatureCheck::Invalid,
Err(e) => {
warn!(
bundle = %sig.bundle_ref,
error = %e,
"signature verification failed; treating as invalid (fail closed)"
);
SignatureCheck::Invalid
}
}
}
async fn run_provenance_gate(
local_binary: &Path,
worker_triple: Option<&str>,
policy: &ProvenancePolicy,
) -> (ProvenanceVerdict, ArtifactProvenance) {
let fallback = worker_triple.unwrap_or("unknown");
let provenance = load_artifact_provenance(local_binary, fallback);
let actual_sha = if provenance.expected_sha256.is_some() {
local_file_sha256_hex(local_binary).ok()
} else {
None
};
let signature_check = evaluate_signature_check(&provenance, local_binary).await;
let effective_triple =
worker_triple.map_or_else(|| provenance.target_triple.clone(), str::to_string);
let verdict = verify_artifact_provenance(
&provenance,
&effective_triple,
actual_sha.as_deref(),
signature_check,
policy,
);
(verdict, provenance)
}
fn provenance_detail(verdict: &ProvenanceVerdict) -> String {
match verdict {
ProvenanceVerdict::Verified => "artifact provenance verified".to_string(),
ProvenanceVerdict::DevArtifactAllowed { reason } => reason.clone(),
ProvenanceVerdict::Rejected { detail, .. } => detail.clone(),
}
}
fn persist_provenance_audit(record: &FleetDeployAuditRecord) {
match HistoryManager::new() {
Ok(history) => {
if let Err(e) = history.record_provenance_audit(record) {
warn!(
worker = %record.worker_id,
error = %e,
"failed to persist provenance audit record"
);
}
}
Err(e) => warn!(error = %e, "failed to open history manager for provenance audit"),
}
}
fn now_unix_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}
fn smoke_capability_probe_spec(worker: &WorkerConfig) -> ProbeSpec {
let declared_os = rch_common::declared_os(&worker.tags);
ProbeSpec::new(
worker.user.clone(),
remote_worker_binary_path(&worker.user, declared_os.as_deref()),
)
.with_tools(
worker
.tools
.iter()
.filter_map(|tool| NamedToolProbe::try_from(tool).ok()),
)
}
async fn run_smoke_capabilities_with_runner<R: CommandRunner>(
worker: &WorkerConfig,
req: &CapabilityRequirement,
runner: &R,
) -> Result<CapabilityVerdict, FleetSshError> {
let spec = smoke_capability_probe_spec(worker);
let script = build_capability_probe_script(&spec);
let output = runner.run_command(&script).await?;
let facts = parse_capability_probe(&output.stdout);
if facts.worker.is_none() && facts.os.is_none() {
return Ok(CapabilityVerdict::Rejected {
reason: IncidentReasonCode::WrongUserPathWorkerBinary,
detail: format!(
"capability probe at {REMOTE_WORKER_BINARY} produced no parseable facts as user {} (binary missing or unreachable)",
worker.user
),
});
}
Ok(assess_admissibility(&facts, req))
}
fn smoke_capability_requirement() -> CapabilityRequirement {
CapabilityRequirement::rust(0)
}
async fn run_smoke_disk_inode_with_runner<R: CommandRunner>(
worker: &WorkerConfig,
roots: &[String],
thresholds: &PressureThresholds,
runner: &R,
) -> Result<PressureLevel, FleetSshError> {
let mut spec = smoke_capability_probe_spec(worker);
spec.disk_roots = roots.to_vec();
let script = build_capability_probe_script(&spec);
let output = runner.run_command(&script).await?;
let facts = parse_capability_probe(&output.stdout);
if facts.disk_roots.is_empty() {
return Ok(PressureLevel::Unknown);
}
Ok(facts
.disk_roots
.iter()
.map(|root| assess_root(DiskRootKind::TempRoot, Some(root), thresholds).worst())
.fold(PressureLevel::Ok, PressureLevel::worse))
}
async fn run_smoke_worker_scenarios_with_runner<R: CommandRunner>(
run_id: &str,
bead_id: &str,
worker: &WorkerConfig,
disk_roots: &[String],
thresholds: &PressureThresholds,
runner: &R,
) -> Vec<SmokeProfileEvent> {
let mut events = Vec::with_capacity(4);
let worker_id = worker.id.to_string();
let scenario = SmokeScenario::WorkerCapabilitiesExactUserPath;
events.push(SmokeProfileEvent::started(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
));
let started = std::time::Instant::now();
let cap_result =
run_smoke_capabilities_with_runner(worker, &smoke_capability_requirement(), runner).await;
let cap_ms = smoke_elapsed_ms(started);
let cap_fingerprint = Some("rch-wkr capability probe (exact user/path)".to_string());
events.push(match cap_result {
Ok(CapabilityVerdict::Admissible) => SmokeProfileEvent::outcome(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
true,
None,
cap_fingerprint,
cap_ms,
),
Ok(CapabilityVerdict::Rejected { reason, .. }) => SmokeProfileEvent::outcome(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
false,
Some(reason.code().to_string()),
cap_fingerprint,
cap_ms,
),
Err(_) => SmokeProfileEvent::outcome(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
false,
Some(SMOKE_WORKER_UNREACHABLE.to_string()),
cap_fingerprint,
cap_ms,
),
});
let scenario = SmokeScenario::DiskInodeAdmission;
events.push(SmokeProfileEvent::started(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
));
let started = std::time::Instant::now();
let disk_result =
run_smoke_disk_inode_with_runner(worker, disk_roots, thresholds, runner).await;
let disk_ms = smoke_elapsed_ms(started);
let disk_fingerprint = Some("df -Pk / df -Pi (build root headroom)".to_string());
events.push(match disk_result {
Ok(level) if !matches!(level, PressureLevel::Critical | PressureLevel::Unknown) => {
SmokeProfileEvent::outcome(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
true,
None,
disk_fingerprint,
disk_ms,
)
}
Ok(level) => SmokeProfileEvent::outcome(
run_id,
bead_id,
Some(worker_id.clone()),
scenario,
false,
Some(match level {
PressureLevel::Critical => IncidentReasonCode::CriticalPressure.code().to_string(),
_ => "disk_pressure_unknown".to_string(),
}),
disk_fingerprint,
disk_ms,
),
Err(_) => SmokeProfileEvent::outcome(
run_id,
bead_id,
Some(worker_id),
scenario,
false,
Some(SMOKE_WORKER_UNREACHABLE.to_string()),
disk_fingerprint,
disk_ms,
),
});
events
}
const SMOKE_WORKER_UNREACHABLE: &str = "worker_unreachable";
fn smoke_elapsed_ms(started: std::time::Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
pub(crate) async fn run_smoke_worker_scenarios(
run_id: &str,
bead_id: &str,
worker: &WorkerConfig,
disk_roots: &[String],
) -> Vec<SmokeProfileEvent> {
let ssh = SshExecutor::new(worker);
run_smoke_worker_scenarios_with_runner(
run_id,
bead_id,
worker,
disk_roots,
&PressureThresholds::default(),
&ssh,
)
.await
}
fn platform_mismatch_skip_reason(error: &anyhow::Error) -> Option<String> {
match error.downcast_ref::<FleetError>() {
Some(FleetError::BinaryPlatformMismatch { local, worker, .. }) => Some(format!(
"worker platform {worker} differs from this controller's binary ({local}); \
deploy from a matching controller or release artifact"
)),
_ => None,
}
}
async fn ensure_binary_matches_worker(worker: &WorkerConfig, local_binary: &Path) -> Result<()> {
let header = match read_binary_header(local_binary) {
Ok(header) => header,
Err(e) => {
warn!(
worker = %worker.id,
path = %local_binary.display(),
error = %e,
"Could not read local binary header for platform check; proceeding"
);
return Ok(());
}
};
let local_platform = match detect_binary_platform(&header) {
Some(platform) => platform,
None => {
warn!(
worker = %worker.id,
path = %local_binary.display(),
"Unrecognised local binary format; skipping platform pre-check"
);
return Ok(());
}
};
let worker_platform = match detect_worker_platform(worker).await {
Some(platform) => platform,
None => {
warn!(
worker = %worker.id,
"Could not determine worker platform via uname; skipping platform pre-check"
);
return Ok(());
}
};
if platforms_compatible(local_platform, worker_platform) {
debug!(
worker = %worker.id,
local = %local_platform,
remote = %worker_platform,
"Binary platform matches worker"
);
return Ok(());
}
Err(FleetError::BinaryPlatformMismatch {
worker_id: worker.id.0.clone(),
local: local_platform.to_string(),
worker: worker_platform.to_string(),
}
.into())
}
async fn copy_binary_via_scp(worker: &WorkerConfig, local_binary: &Path) -> Result<()> {
let ssh = SshExecutor::new(worker);
let remote_path = REMOTE_WORKER_BINARY;
let staging_path = remote_worker_binary_staging_path();
debug!(
"SCP: {} -> {}@{}:{}",
local_binary.display(),
worker.user,
worker.host,
staging_path
);
ssh.copy_file(local_binary, &staging_path)
.await
.map_err(|e| anyhow::anyhow!("scp failed: {}", e))?;
let expected_sha256 = local_file_sha256_hex(local_binary)
.map_err(|e| anyhow::anyhow!("hash local binary {}: {}", local_binary.display(), e))?;
let finalize_cmd = staged_install_command(&staging_path, remote_path, &expected_sha256)?;
let output = ssh
.run_command(&finalize_cmd)
.await
.map_err(|e| anyhow::anyhow!("staged install failed: {}", e))?;
if output.success() {
return Ok(());
}
if let Ok(staging_path) = remote_shell_path(&staging_path) {
let cleanup_cmd = format!("rm -f {staging_path}");
let _ = ssh.run_command(&cleanup_cmd).await;
}
Err(anyhow::anyhow!(
"staged install rejected before switch (exit {}: {}); previous binary left active",
output.exit_code,
output.stderr.trim()
))
}
fn local_file_sha256_hex(path: &Path) -> std::io::Result<String> {
use sha2::{Digest, Sha256};
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 8192];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hex_lower(&hasher.finalize()))
}
fn hex_lower(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
async fn set_executable_permissions(worker: &WorkerConfig) -> Result<()> {
let ssh = SshExecutor::new(worker);
ssh.set_executable(REMOTE_WORKER_BINARY)
.await
.map_err(|e| anyhow::anyhow!("chmod failed: {}", e))
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PostDeployEligibility {
Eligible,
NotEligible {
reason_code: &'static str,
detail: String,
},
}
fn post_deploy_validation_command(remote_path: &str) -> Result<String> {
let p = remote_shell_path(remote_path)?;
Ok(format!(
"uname -s -m; file {p} 2>/dev/null || true; {p} --version"
))
}
fn classify_post_deploy(exit_code: i32, stderr: &str) -> PostDeployEligibility {
let trimmed = stderr.trim();
if exit_code == 126 || trimmed.contains("Exec format error") {
return PostDeployEligibility::NotEligible {
reason_code: "os_arch_mismatch",
detail: format!(
"deployed rch-wkr is not executable on this worker (exit {exit_code}: {trimmed}); \
the binary architecture does not match the worker"
),
};
}
if exit_code != 0 {
return PostDeployEligibility::NotEligible {
reason_code: "post_deploy_validation_failed",
detail: format!("post-deploy validation failed (exit {exit_code}: {trimmed})"),
};
}
PostDeployEligibility::Eligible
}
fn post_deploy_health_command(remote_path: &str) -> Result<String> {
let p = remote_shell_path(remote_path)?;
Ok(format!("{p} health"))
}
fn post_deploy_capabilities_command(remote_path: &str) -> Result<String> {
let p = remote_shell_path(remote_path)?;
Ok(format!("{p} capabilities"))
}
fn classify_capabilities_handshake(
exit_code: i32,
stdout: &str,
stderr: &str,
) -> PostDeployEligibility {
match classify_post_deploy(exit_code, stderr) {
PostDeployEligibility::Eligible => {}
not_eligible => return not_eligible,
}
match serde_json::from_str::<WorkerCapabilities>(stdout.trim()) {
Ok(_) => PostDeployEligibility::Eligible,
Err(e) => PostDeployEligibility::NotEligible {
reason_code: "capabilities_handshake_failed",
detail: format!(
"deployed rch-wkr ran but did not emit parseable WorkerCapabilities JSON \
(protocol handshake failed: {e})"
),
},
}
}
async fn verify_installation(worker: &WorkerConfig) -> Result<()> {
let ssh = SshExecutor::new(worker);
let worker_id = worker.id.as_str();
let ineligible =
|step: &'static str, reason_code: &'static str, detail: String| -> anyhow::Error {
warn!(
worker = worker_id,
step,
reason_code,
detail = %detail,
"post-deploy validation marked worker ineligible"
);
FleetError::HealthCheckFailed {
reason: format!("[{reason_code}] {detail}"),
}
.into()
};
let validation_cmd = post_deploy_validation_command(REMOTE_WORKER_BINARY)?;
let validation = ssh
.run_command(&validation_cmd)
.await
.map_err(|e| anyhow::anyhow!("post-deploy validation failed: {}", e))?;
if let PostDeployEligibility::NotEligible {
reason_code,
detail,
} = classify_post_deploy(validation.exit_code, &validation.stderr)
{
return Err(ineligible("exact_path_version", reason_code, detail));
}
let health_cmd = post_deploy_health_command(REMOTE_WORKER_BINARY)?;
let output = ssh
.run_command(&health_cmd)
.await
.map_err(|e| anyhow::anyhow!("health check failed: {}", e))?;
if !output.success() {
let stderr = output.stderr.trim();
if let PostDeployEligibility::NotEligible {
reason_code,
detail,
} = classify_post_deploy(output.exit_code, stderr)
{
return Err(ineligible("health", reason_code, detail));
}
return Err(FleetError::HealthCheckFailed {
reason: stderr.to_string(),
}
.into());
}
let capabilities_cmd = post_deploy_capabilities_command(REMOTE_WORKER_BINARY)?;
let caps = ssh
.run_command(&capabilities_cmd)
.await
.map_err(|e| anyhow::anyhow!("capabilities handshake failed: {}", e))?;
if let PostDeployEligibility::NotEligible {
reason_code,
detail,
} = classify_capabilities_handshake(caps.exit_code, &caps.stdout, &caps.stderr)
{
return Err(ineligible("capabilities_handshake", reason_code, detail));
}
info!(
worker = worker_id,
"post-deploy validation passed (exact-path version, health, capabilities handshake)"
);
Ok(())
}
fn remote_worker_binary_staging_path() -> String {
let counter = STAGED_BINARY_COUNTER.fetch_add(1, Ordering::Relaxed);
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0);
format!(
"~/.local/bin/.rch-wkr.tmp.{}.{}.{}",
std::process::id(),
nonce,
counter
)
}
fn remote_shell_path(remote_path: &str) -> Result<String> {
shell_escape_path_with_home(remote_path)
.ok_or_else(|| anyhow::anyhow!("invalid remote path contains control characters"))
}
fn staged_install_command(
staging_path: &str,
final_path: &str,
expected_sha256: &str,
) -> Result<String> {
let staged = remote_shell_path(staging_path)?;
let final_path = remote_shell_path(final_path)?;
if expected_sha256.len() != 64 || !expected_sha256.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(anyhow::anyhow!("invalid expected sha256 digest"));
}
Ok(format!(
"set -e; \
chmod +x {staged}; \
actual=$(sha256sum {staged} | cut -d' ' -f1); \
[ \"$actual\" = \"{expected_sha256}\" ] || {{ echo \"checksum mismatch: $actual != {expected_sha256}\" >&2; exit 1; }}; \
{staged} --version >/dev/null 2>&1 || {{ echo \"staged binary failed --version (wrong arch or corrupt)\" >&2; exit 1; }}; \
mv -f {staged} {final_path}"
))
}
fn is_safe_version_token(version: &str) -> bool {
!version.is_empty()
&& version
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '+' | '-'))
}
pub async fn backup_before_deploy(
worker: &WorkerConfig,
rollback_manager: &mut RollbackManager,
) -> Result<Option<WorkerBackup>> {
let ssh = SshExecutor::new(worker);
backup_before_deploy_with_runner(worker, rollback_manager, &ssh).await
}
trait CommandRunner {
fn run_command<'a>(
&'a self,
cmd: &'a str,
) -> BoxFuture<'a, Result<CommandOutput, FleetSshError>>;
}
impl<'a> CommandRunner for SshExecutor<'a> {
fn run_command<'b>(
&'b self,
cmd: &'b str,
) -> BoxFuture<'b, Result<CommandOutput, FleetSshError>> {
Box::pin(SshExecutor::run_command(self, cmd))
}
}
#[cfg(test)]
impl CommandRunner for crate::fleet::ssh::MockSshExecutor {
fn run_command<'a>(
&'a self,
cmd: &'a str,
) -> BoxFuture<'a, Result<CommandOutput, FleetSshError>> {
Box::pin(crate::fleet::ssh::MockSshExecutor::run_command(self, cmd))
}
}
async fn backup_before_deploy_with_runner<R: CommandRunner>(
worker: &WorkerConfig,
rollback_manager: &mut RollbackManager,
runner: &R,
) -> Result<Option<WorkerBackup>> {
debug!(worker = %worker.id, "Checking for existing version to backup");
let version_cmd = format!("{} --version 2>/dev/null", REMOTE_RCH_PATH);
let current_version = match runner.run_command(&version_cmd).await {
Ok(output) if output.success() => output.stdout.split_whitespace().nth(1).map(String::from),
Ok(_) => None,
Err(e) => {
debug!(worker = %worker.id, error = %e, "No existing rch-wkr found");
None
}
};
let version = match current_version {
Some(v) => v,
None => {
debug!(worker = %worker.id, "No existing version to backup");
return Ok(None);
}
};
if !is_safe_version_token(&version) {
warn!(
worker = %worker.id,
version = %version,
"Skipping backup: worker-reported version contains unsafe characters"
);
return Ok(None);
}
info!(worker = %worker.id, version = %version, "Creating backup before deploy");
let mkdir_cmd = format!("mkdir -p {}", REMOTE_BACKUP_DIR);
match runner.run_command(&mkdir_cmd).await {
Ok(output) if output.success() => {}
Ok(output) => {
warn!(
worker = %worker.id,
stderr = %output.stderr.trim(),
"Failed to create backup directory"
);
return Ok(None);
}
Err(e) => {
warn!(worker = %worker.id, error = %e, "Failed to create backup directory");
return Ok(None); }
}
let df_output = runner
.run_command("df -Pm ~/.rch 2>/dev/null | tail -1 | awk '{print $4}'")
.await;
if let Ok(output) = df_output
&& let Ok(mb) = output.stdout.trim().parse::<u64>()
&& mb < 50
{
warn!(
worker = %worker.id,
available_mb = %mb,
"Low disk space, skipping backup"
);
return Ok(None);
}
let remote_backup_path = format!("{}/rch-wkr-{}", REMOTE_BACKUP_DIR, version);
let copy_cmd = format!("cp {} {}", REMOTE_RCH_PATH, remote_backup_path);
match runner.run_command(©_cmd).await {
Ok(output) if output.success() => {}
Ok(output) => {
warn!(
worker = %worker.id,
stderr = %output.stderr.trim(),
"Failed to copy binary to backup"
);
return Ok(None);
}
Err(e) => {
warn!(worker = %worker.id, error = %e, "Failed to copy binary to backup");
return Ok(None); }
}
let hash_cmd = format!(
"sha256sum {} 2>/dev/null | cut -d' ' -f1",
remote_backup_path
);
let binary_hash = match runner.run_command(&hash_cmd).await {
Ok(output) if output.success() => {
let hash = output.stdout.trim().to_string();
if hash.len() == 64 {
hash
} else {
"unknown".to_string()
}
}
Ok(_) | Err(_) => {
warn!(worker = %worker.id, "Failed to calculate backup hash");
"unknown".to_string()
}
};
let backup = rollback_manager.create_backup_entry(
&worker.id.0,
&version,
&remote_backup_path,
&binary_hash,
);
if let Err(e) = rollback_manager.save_backup_entry(&backup) {
warn!(worker = %worker.id, error = %e, "Failed to save backup entry to registry");
return Ok(None); }
match rollback_manager.prune_old_backups(MAX_BACKUPS_PER_WORKER) {
Ok(removed) => {
for old_backup in removed {
debug!(
worker = %worker.id,
version = %old_backup.version,
"Cleaning up old backup"
);
let rm_cmd = format!("rm -f {}", old_backup.remote_path.display());
let _ = runner.run_command(&rm_cmd).await;
}
}
Err(e) => {
warn!(worker = %worker.id, error = %e, "Failed to prune old backups");
}
}
info!(
worker = %worker.id,
version = %version,
hash = %binary_hash,
"Backup created successfully"
);
Ok(Some(backup))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fleet_result_success_serializes() {
let result = FleetResult::Success {
deployed: 5,
skipped: 2,
failed: 1,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"status\":\"Success\""));
assert!(json.contains("\"deployed\":5"));
assert!(json.contains("\"skipped\":2"));
assert!(json.contains("\"failed\":1"));
}
#[test]
fn fleet_result_success_zero_values_serializes() {
let result = FleetResult::Success {
deployed: 0,
skipped: 0,
failed: 0,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"status\":\"Success\""));
assert!(json.contains("\"deployed\":0"));
}
#[test]
fn fleet_result_canary_failed_serializes() {
let result = FleetResult::CanaryFailed {
reason: "Health check failed".to_string(),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"status\":\"CanaryFailed\""));
assert!(json.contains("Health check failed"));
}
#[test]
fn fleet_result_canary_pending_serializes_distinctly_from_success() {
let pending = FleetResult::CanaryPending {
promoted: 5,
remaining: 95,
skipped: 0,
failed: 0,
};
let json = serde_json::to_string(&pending).unwrap();
assert!(json.contains("\"status\":\"CanaryPending\""));
assert!(json.contains("\"promoted\":5"));
assert!(json.contains("\"remaining\":95"));
let success = serde_json::to_string(&FleetResult::Success {
deployed: 5,
skipped: 0,
failed: 0,
})
.unwrap();
assert_ne!(json, success);
assert!(!json.contains("\"status\":\"Success\""));
}
#[test]
fn fleet_result_canary_pending_round_trips() {
let pending = FleetResult::CanaryPending {
promoted: 2,
remaining: 6,
skipped: 1,
failed: 0,
};
let json = serde_json::to_string(&pending).unwrap();
let back: FleetResult = serde_json::from_str(&json).unwrap();
match back {
FleetResult::CanaryPending {
promoted,
remaining,
skipped,
failed,
} => {
assert_eq!((promoted, remaining, skipped, failed), (2, 6, 1, 0));
}
other => panic!("expected CanaryPending, got {other:?}"),
}
}
#[test]
fn fleet_result_aborted_serializes() {
let result = FleetResult::Aborted {
reason: "User cancelled".to_string(),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"status\":\"Aborted\""));
assert!(json.contains("User cancelled"));
}
#[test]
fn fleet_result_variants_are_tagged() {
let success = serde_json::to_string(&FleetResult::Success {
deployed: 1,
skipped: 0,
failed: 0,
})
.unwrap();
let canary = serde_json::to_string(&FleetResult::CanaryFailed {
reason: "test".to_string(),
})
.unwrap();
let aborted = serde_json::to_string(&FleetResult::Aborted {
reason: "test".to_string(),
})
.unwrap();
assert!(success.contains("\"status\":\"Success\""));
assert!(canary.contains("\"status\":\"CanaryFailed\""));
assert!(aborted.contains("\"status\":\"Aborted\""));
}
fn test_worker_config() -> WorkerConfig {
WorkerConfig {
id: WorkerId("test-worker".to_string()),
host: "localhost".to_string(),
user: "test".to_string(),
identity_file: "/tmp/test_key".to_string(),
total_slots: 4,
priority: 1,
tags: vec![],
tools: Vec::new(),
}
}
fn test_binary_path() -> PathBuf {
PathBuf::from("/tmp/rch-wkr")
}
#[test]
fn fleet_executor_new_without_audit() {
let worker = test_worker_config();
let executor = FleetExecutor::new(4, None, &[&worker], test_binary_path());
assert!(executor.is_ok());
let executor = executor.unwrap();
assert_eq!(executor.parallelism, 4);
}
#[test]
fn fleet_executor_new_with_parallelism_one() {
let worker = test_worker_config();
let executor = FleetExecutor::new(1, None, &[&worker], test_binary_path()).unwrap();
assert_eq!(executor.parallelism, 1);
}
#[test]
fn fleet_executor_new_with_high_parallelism() {
let worker = test_worker_config();
let executor = FleetExecutor::new(100, None, &[&worker], test_binary_path()).unwrap();
assert_eq!(executor.parallelism, 100);
}
#[test]
fn fleet_executor_stores_worker_configs() {
let worker = test_worker_config();
let executor = FleetExecutor::new(4, None, &[&worker], test_binary_path()).unwrap();
assert!(executor.worker_configs.contains_key("test-worker"));
}
#[test]
fn fleet_executor_stores_binary_path() {
let worker = test_worker_config();
let binary_path = PathBuf::from("/custom/path/rch-wkr");
let executor = FleetExecutor::new(4, None, &[&worker], binary_path.clone()).unwrap();
assert_eq!(executor.local_binary, binary_path);
}
#[test]
fn fleet_result_success_large_counts() {
let result = FleetResult::Success {
deployed: 1000,
skipped: 500,
failed: 10,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"deployed\":1000"));
assert!(json.contains("\"skipped\":500"));
assert!(json.contains("\"failed\":10"));
}
#[test]
fn fleet_result_canary_failed_empty_reason() {
let result = FleetResult::CanaryFailed {
reason: String::new(),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"status\":\"CanaryFailed\""));
assert!(json.contains("\"reason\":\"\""));
}
#[test]
fn fleet_result_canary_failed_long_reason() {
let result = FleetResult::CanaryFailed {
reason: "x".repeat(1000),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"status\":\"CanaryFailed\""));
assert!(json.len() > 1000);
}
#[test]
fn fleet_result_aborted_special_chars_in_reason() -> Result<()> {
let result = FleetResult::Aborted {
reason: "User cancelled: \"interrupted\" <signal>".to_string(),
};
let json = serde_json::to_string(&result)?;
let deserialized: FleetResult = serde_json::from_str(&json)?;
let FleetResult::Aborted { reason } = deserialized else {
return Err(anyhow::anyhow!("expected Aborted variant"));
};
assert!(reason.contains("interrupted"));
assert!(reason.contains("<signal>"));
Ok(())
}
#[test]
fn fleet_result_deserialize_success() -> Result<()> {
let json = r#"{"status":"Success","deployed":3,"skipped":1,"failed":0}"#;
let result: FleetResult = serde_json::from_str(json)?;
let FleetResult::Success {
deployed,
skipped,
failed,
} = result
else {
return Err(anyhow::anyhow!("expected Success variant"));
};
assert_eq!(deployed, 3);
assert_eq!(skipped, 1);
assert_eq!(failed, 0);
Ok(())
}
#[test]
fn fleet_result_deserialize_canary_failed() -> Result<()> {
let json = r#"{"status":"CanaryFailed","reason":"worker timeout"}"#;
let result: FleetResult = serde_json::from_str(json)?;
let FleetResult::CanaryFailed { reason } = result else {
return Err(anyhow::anyhow!("expected CanaryFailed variant"));
};
assert_eq!(reason, "worker timeout");
Ok(())
}
#[test]
fn fleet_result_deserialize_aborted() -> Result<()> {
let json = r#"{"status":"Aborted","reason":"ctrl+c"}"#;
let result: FleetResult = serde_json::from_str(json)?;
let FleetResult::Aborted { reason } = result else {
return Err(anyhow::anyhow!("expected Aborted variant"));
};
assert_eq!(reason, "ctrl+c");
Ok(())
}
#[test]
fn fleet_result_roundtrip_all_variants() {
let variants = vec![
FleetResult::Success {
deployed: 10,
skipped: 2,
failed: 1,
},
FleetResult::CanaryFailed {
reason: "test failure".to_string(),
},
FleetResult::Aborted {
reason: "test abort".to_string(),
},
];
for original in variants {
let json = serde_json::to_string(&original).unwrap();
let restored: FleetResult = serde_json::from_str(&json).unwrap();
let json_again = serde_json::to_string(&restored).unwrap();
assert_eq!(json, json_again);
}
}
#[test]
fn fleet_executor_parallelism_zero() {
let worker = test_worker_config();
let executor = FleetExecutor::new(0, None, &[&worker], test_binary_path());
assert!(executor.is_ok());
assert_eq!(executor.unwrap().parallelism, 0);
}
#[test]
fn fleet_executor_very_large_parallelism() {
let worker = test_worker_config();
let executor =
FleetExecutor::new(usize::MAX, None, &[&worker], test_binary_path()).unwrap();
assert_eq!(executor.parallelism, usize::MAX);
}
#[test]
fn fleet_executor_empty_workers() {
let executor = FleetExecutor::new(4, None, &[], test_binary_path()).unwrap();
assert!(executor.worker_configs.is_empty());
}
#[test]
fn fleet_executor_multiple_workers() {
let mut worker1 = test_worker_config();
worker1.id = WorkerId("worker-1".to_string());
let mut worker2 = test_worker_config();
worker2.id = WorkerId("worker-2".to_string());
let executor =
FleetExecutor::new(4, None, &[&worker1, &worker2], test_binary_path()).unwrap();
assert_eq!(executor.worker_configs.len(), 2);
assert!(executor.worker_configs.contains_key("worker-1"));
assert!(executor.worker_configs.contains_key("worker-2"));
}
#[test]
fn staged_install_command_validates_then_renames_in_order() -> Result<()> {
let sha = "a".repeat(64);
let command = staged_install_command(
"~/.local/bin/.rch-wkr.tmp.123.456",
REMOTE_WORKER_BINARY,
&sha,
)?;
assert!(command.contains("set -e"));
assert!(command.contains("chmod +x \"$HOME/.local/bin/.rch-wkr.tmp.123.456\""));
assert!(command.contains("sha256sum \"$HOME/.local/bin/.rch-wkr.tmp.123.456\""));
assert!(
command.contains(&sha),
"expected digest embedded for comparison"
);
assert!(command.contains("\"$HOME/.local/bin/.rch-wkr.tmp.123.456\" --version"));
assert!(command.contains(
"mv -f \"$HOME/.local/bin/.rch-wkr.tmp.123.456\" \"$HOME/.local/bin/rch-wkr\""
));
let mv_pos = command.find("mv -f").expect("mv present");
let sha_pos = command.find("sha256sum").expect("checksum present");
let ver_pos = command.find("--version").expect("version present");
assert!(
mv_pos > sha_pos && mv_pos > ver_pos,
"mv must come after both guards"
);
Ok(())
}
#[test]
fn staged_install_command_rejects_non_hex_digest() {
assert!(
staged_install_command("~/.local/bin/.rch-wkr.tmp.1", REMOTE_WORKER_BINARY, "nope")
.is_err()
);
assert!(
staged_install_command(
"~/.local/bin/.rch-wkr.tmp.1",
REMOTE_WORKER_BINARY,
"abc; rm -rf /"
)
.is_err()
);
}
#[test]
fn local_file_sha256_matches_known_vector() {
let dir = tempfile::tempdir().unwrap();
let empty = dir.path().join("empty");
std::fs::write(&empty, b"").unwrap();
assert_eq!(
local_file_sha256_hex(&empty).unwrap(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let abc = dir.path().join("abc");
std::fs::write(&abc, b"abc").unwrap();
assert_eq!(
local_file_sha256_hex(&abc).unwrap(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn post_deploy_validation_runs_the_exact_quoted_path() -> Result<()> {
let cmd = post_deploy_validation_command(REMOTE_WORKER_BINARY)?;
assert!(cmd.contains("\"$HOME/.local/bin/rch-wkr\" --version"));
assert!(cmd.contains("uname -s -m"));
assert!(!cmd.contains(" rch-wkr --version"));
Ok(())
}
#[test]
fn classify_post_deploy_marks_exec_format_ineligible() {
match classify_post_deploy(126, "sh: 1: rch-wkr: Exec format error") {
PostDeployEligibility::NotEligible { reason_code, .. } => {
assert_eq!(reason_code, "os_arch_mismatch");
}
other => panic!("expected NotEligible(os_arch_mismatch), got {other:?}"),
}
assert!(matches!(
classify_post_deploy(1, "Exec format error"),
PostDeployEligibility::NotEligible {
reason_code: "os_arch_mismatch",
..
}
));
}
#[test]
fn classify_post_deploy_generic_failure_and_success() {
assert!(matches!(
classify_post_deploy(2, "some other error"),
PostDeployEligibility::NotEligible {
reason_code: "post_deploy_validation_failed",
..
}
));
assert_eq!(classify_post_deploy(0, ""), PostDeployEligibility::Eligible);
}
#[test]
fn post_deploy_capabilities_command_runs_the_exact_quoted_path() -> Result<()> {
let cmd = post_deploy_capabilities_command(REMOTE_WORKER_BINARY)?;
assert!(cmd.contains("\"$HOME/.local/bin/rch-wkr\" capabilities"));
assert!(!cmd.contains(" rch-wkr capabilities"));
Ok(())
}
#[test]
fn post_deploy_health_command_runs_the_exact_quoted_path() -> Result<()> {
let cmd = post_deploy_health_command(REMOTE_WORKER_BINARY)?;
assert!(cmd.contains("\"$HOME/.local/bin/rch-wkr\" health"));
assert!(!cmd.contains(" rch-wkr health"));
Ok(())
}
#[test]
fn capabilities_handshake_valid_json_is_eligible() {
assert_eq!(
classify_capabilities_handshake(0, "{}", ""),
PostDeployEligibility::Eligible
);
let full = r#"{"rustc_version":"rustc 1.80.0","num_cpus":32,"disk_free_gb":120.5}"#;
assert_eq!(
classify_capabilities_handshake(0, full, ""),
PostDeployEligibility::Eligible
);
assert_eq!(
classify_capabilities_handshake(0, " {}\n", ""),
PostDeployEligibility::Eligible
);
}
#[test]
fn capabilities_handshake_unparseable_json_is_ineligible() {
match classify_capabilities_handshake(0, "not json at all", "") {
PostDeployEligibility::NotEligible { reason_code, .. } => {
assert_eq!(reason_code, "capabilities_handshake_failed");
}
other => panic!("expected NotEligible(capabilities_handshake_failed), got {other:?}"),
}
assert!(matches!(
classify_capabilities_handshake(0, "", ""),
PostDeployEligibility::NotEligible {
reason_code: "capabilities_handshake_failed",
..
}
));
}
#[test]
fn capabilities_handshake_exec_format_is_os_arch_mismatch() {
match classify_capabilities_handshake(126, "", "sh: 1: rch-wkr: Exec format error") {
PostDeployEligibility::NotEligible { reason_code, .. } => {
assert_eq!(reason_code, "os_arch_mismatch");
}
other => panic!("expected NotEligible(os_arch_mismatch), got {other:?}"),
}
}
#[test]
fn capabilities_handshake_nonzero_exit_is_generic_failure() {
match classify_capabilities_handshake(1, "{}", "some transient error") {
PostDeployEligibility::NotEligible { reason_code, .. } => {
assert_eq!(reason_code, "post_deploy_validation_failed");
}
other => panic!("expected NotEligible(post_deploy_validation_failed), got {other:?}"),
}
}
#[test]
fn remote_worker_binary_staging_path_never_targets_live_binary() {
let staging_path = remote_worker_binary_staging_path();
assert!(staging_path.starts_with("~/.local/bin/.rch-wkr.tmp."));
assert_ne!(staging_path, REMOTE_WORKER_BINARY);
}
fn elf_header(machine: u16, little_endian: bool) -> Vec<u8> {
let mut h = vec![0u8; BINARY_MAGIC_PROBE_LEN];
h[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
h[4] = 2; h[5] = if little_endian { 1 } else { 2 };
let bytes = if little_endian {
machine.to_le_bytes()
} else {
machine.to_be_bytes()
};
h[18] = bytes[0];
h[19] = bytes[1];
h
}
fn macho_header(magic: [u8; 4], cputype: u32, little_endian: bool) -> Vec<u8> {
let mut h = vec![0u8; BINARY_MAGIC_PROBE_LEN];
h[0..4].copy_from_slice(&magic);
let bytes = if little_endian {
cputype.to_le_bytes()
} else {
cputype.to_be_bytes()
};
h[4..8].copy_from_slice(&bytes);
h
}
#[test]
fn detect_binary_platform_recognises_linux_elf() {
let platform = detect_binary_platform(&elf_header(0x3E, true)).unwrap();
assert_eq!(platform.os, BinaryOs::Linux);
assert_eq!(platform.arch, Some(BinaryArch::X86_64));
let aarch = detect_binary_platform(&elf_header(0xB7, true)).unwrap();
assert_eq!(aarch.arch, Some(BinaryArch::Aarch64));
}
#[test]
fn detect_binary_platform_recognises_macho() {
let arm =
detect_binary_platform(&macho_header([0xCF, 0xFA, 0xED, 0xFE], 0x0100_000C, true))
.unwrap();
assert_eq!(arm.os, BinaryOs::MacOs);
assert_eq!(arm.arch, Some(BinaryArch::Aarch64));
let x86 =
detect_binary_platform(&macho_header([0xCF, 0xFA, 0xED, 0xFE], 0x0100_0007, true))
.unwrap();
assert_eq!(x86.arch, Some(BinaryArch::X86_64));
}
#[test]
fn detect_binary_platform_recognises_fat_macho_as_unknown_arch() {
let fat = detect_binary_platform(&[0xCA, 0xFE, 0xBA, 0xBE, 0, 0, 0, 2]).unwrap();
assert_eq!(fat.os, BinaryOs::MacOs);
assert_eq!(fat.arch, None);
}
#[test]
fn detect_binary_platform_returns_none_for_garbage() {
assert!(detect_binary_platform(&[0x00, 0x01, 0x02, 0x03]).is_none());
assert!(detect_binary_platform(&[]).is_none());
}
fn facts(os: BinaryOs, arch: Option<BinaryArch>, libc: LinuxLibc) -> WorkerTargetFacts {
WorkerTargetFacts {
platform: BinaryPlatform { os, arch },
libc,
remote_user: "ubuntu".to_string(),
rch_wkr_path: Some("~/.local/bin/rch-wkr".to_string()),
}
}
#[test]
fn worker_target_triple_linux_defaults_to_musl() {
assert_eq!(
worker_target_triple(&facts(
BinaryOs::Linux,
Some(BinaryArch::X86_64),
LinuxLibc::Unknown
))
.as_deref(),
Some("x86_64-unknown-linux-musl")
);
assert_eq!(
worker_target_triple(&facts(
BinaryOs::Linux,
Some(BinaryArch::X86_64),
LinuxLibc::Musl
))
.as_deref(),
Some("x86_64-unknown-linux-musl")
);
assert_eq!(
worker_target_triple(&facts(
BinaryOs::Linux,
Some(BinaryArch::X86_64),
LinuxLibc::Gnu
))
.as_deref(),
Some("x86_64-unknown-linux-gnu")
);
assert_eq!(
worker_target_triple(&facts(
BinaryOs::Linux,
Some(BinaryArch::Aarch64),
LinuxLibc::Musl
))
.as_deref(),
Some("aarch64-unknown-linux-musl")
);
}
#[test]
fn worker_target_triple_macos_and_unsupported() {
assert_eq!(
worker_target_triple(&facts(
BinaryOs::MacOs,
Some(BinaryArch::Aarch64),
LinuxLibc::Unknown
))
.as_deref(),
Some("aarch64-apple-darwin")
);
assert_eq!(
worker_target_triple(&facts(
BinaryOs::MacOs,
Some(BinaryArch::X86_64),
LinuxLibc::Unknown
))
.as_deref(),
Some("x86_64-apple-darwin")
);
assert!(worker_target_triple(&facts(BinaryOs::Linux, None, LinuxLibc::Musl)).is_none());
assert!(
worker_target_triple(&facts(
BinaryOs::Linux,
Some(BinaryArch::X86),
LinuxLibc::Musl
))
.is_none()
);
assert!(
worker_target_triple(&facts(
BinaryOs::Windows,
Some(BinaryArch::X86_64),
LinuxLibc::Unknown
))
.is_none()
);
}
#[test]
fn resolve_worker_artifact_matches_by_triple_and_name() {
let artifacts = vec![
FleetArtifact {
name: "rch-wkr-v1-x86_64-unknown-linux-musl".to_string(),
target_triple: "x86_64-unknown-linux-musl".to_string(),
},
FleetArtifact {
name: "rch-wkr-v1-aarch64-apple-darwin".to_string(),
target_triple: "aarch64-apple-darwin".to_string(),
},
];
let got = resolve_worker_artifact("w1", "x86_64-unknown-linux-musl", &artifacts).unwrap();
assert_eq!(got.target_triple, "x86_64-unknown-linux-musl");
let by_name = vec![FleetArtifact {
name: "rch-wkr-aarch64-apple-darwin.tar.gz".to_string(),
target_triple: "darwin-aarch64".to_string(),
}];
assert!(resolve_worker_artifact("w2", "aarch64-apple-darwin", &by_name).is_ok());
}
#[test]
fn resolve_worker_artifact_fails_closed_with_no_match() {
let darwin_only = vec![FleetArtifact {
name: "rch-wkr-aarch64-apple-darwin".to_string(),
target_triple: "aarch64-apple-darwin".to_string(),
}];
let err =
resolve_worker_artifact("linux-worker", "x86_64-unknown-linux-musl", &darwin_only)
.unwrap_err();
match err {
ArtifactResolveError::NoCompatibleArtifact {
worker_id,
triple,
available,
} => {
assert_eq!(worker_id, "linux-worker");
assert_eq!(triple, "x86_64-unknown-linux-musl");
assert_eq!(available, vec!["aarch64-apple-darwin".to_string()]);
}
other => panic!("expected NoCompatibleArtifact, got {other:?}"),
}
assert!(resolve_worker_artifact("w", "x86_64-unknown-linux-musl", &[]).is_err());
}
#[test]
fn parse_worker_platform_normalises_uname() {
let linux = parse_worker_platform("Linux", "x86_64").unwrap();
assert_eq!(linux.os, BinaryOs::Linux);
assert_eq!(linux.arch, Some(BinaryArch::X86_64));
let mac = parse_worker_platform("Darwin", "arm64").unwrap();
assert_eq!(mac.os, BinaryOs::MacOs);
assert_eq!(mac.arch, Some(BinaryArch::Aarch64));
assert_eq!(
parse_worker_platform("Linux", "amd64").unwrap().arch,
Some(BinaryArch::X86_64)
);
assert_eq!(
parse_worker_platform("Linux", "riscv64").unwrap().arch,
None
);
assert!(parse_worker_platform("Plan9", "x86_64").is_none());
}
#[test]
fn platforms_compatible_rejects_the_p0_failure_mode() {
let mac_binary = BinaryPlatform {
os: BinaryOs::MacOs,
arch: Some(BinaryArch::Aarch64),
};
let linux_worker = BinaryPlatform {
os: BinaryOs::Linux,
arch: Some(BinaryArch::X86_64),
};
assert!(!platforms_compatible(mac_binary, linux_worker));
}
#[test]
fn platforms_compatible_rejects_arch_mismatch_same_os() {
let arm_linux = BinaryPlatform {
os: BinaryOs::Linux,
arch: Some(BinaryArch::Aarch64),
};
let x86_linux = BinaryPlatform {
os: BinaryOs::Linux,
arch: Some(BinaryArch::X86_64),
};
assert!(!platforms_compatible(arm_linux, x86_linux));
}
#[test]
fn platforms_compatible_accepts_exact_match_and_unknown_arch() {
let x86_linux = BinaryPlatform {
os: BinaryOs::Linux,
arch: Some(BinaryArch::X86_64),
};
assert!(platforms_compatible(x86_linux, x86_linux));
let unknown_linux = BinaryPlatform {
os: BinaryOs::Linux,
arch: None,
};
assert!(platforms_compatible(unknown_linux, x86_linux));
assert!(platforms_compatible(x86_linux, unknown_linux));
}
#[test]
fn read_binary_header_reads_leading_magic() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fake-elf");
std::fs::write(&path, elf_header(0x3E, true)).unwrap();
let header = read_binary_header(&path).unwrap();
let platform = detect_binary_platform(&header).unwrap();
assert_eq!(platform.os, BinaryOs::Linux);
assert_eq!(platform.arch, Some(BinaryArch::X86_64));
}
#[test]
fn platform_mismatch_is_a_skip_with_a_reason_other_errors_are_not() {
let mismatch: anyhow::Error = FleetError::BinaryPlatformMismatch {
worker_id: "mac-mini-max".to_string(),
local: "linux/x86_64".to_string(),
worker: "macos/aarch64".to_string(),
}
.into();
let reason = platform_mismatch_skip_reason(&mismatch).expect("mismatch is a skip");
assert!(reason.contains("macos/aarch64"), "{reason}");
assert!(reason.contains("linux/x86_64"), "{reason}");
let other: anyhow::Error = FleetError::HealthCheckFailed {
reason: "rch-wkr health exited 1".to_string(),
}
.into();
assert!(platform_mismatch_skip_reason(&other).is_none());
assert!(platform_mismatch_skip_reason(&anyhow::anyhow!("scp failed")).is_none());
}
#[tokio::test]
async fn ensure_binary_matches_worker_fails_open_on_unreadable_binary() {
let worker = test_worker_config();
let missing = PathBuf::from("/nonexistent/path/rch-wkr");
assert!(
ensure_binary_matches_worker(&worker, &missing)
.await
.is_ok()
);
}
#[tokio::test]
async fn backup_before_deploy_skips_on_low_disk() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let worker = test_worker_config();
let temp_dir = tempfile::tempdir().unwrap();
let mut manager = RollbackManager::with_path(temp_dir.path()).unwrap();
let mock = MockSshExecutor::new()
.with_command("--version", MockCommandResult::ok("rch-wkr 1.0.0"))
.with_command("mkdir -p", MockCommandResult::ok(""))
.with_command("df -Pm", MockCommandResult::ok("49"));
let result = backup_before_deploy_with_runner(&worker, &mut manager, &mock)
.await
.unwrap();
assert!(result.is_none());
assert!(manager.get_latest_backup(&worker.id.0).is_none());
}
#[tokio::test]
async fn backup_before_deploy_copy_failure_is_non_fatal() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let worker = test_worker_config();
let temp_dir = tempfile::tempdir().unwrap();
let mut manager = RollbackManager::with_path(temp_dir.path()).unwrap();
let mock = MockSshExecutor::new()
.with_command("--version", MockCommandResult::ok("rch-wkr 1.0.0"))
.with_command("mkdir -p", MockCommandResult::ok(""))
.with_command("df -Pm", MockCommandResult::ok("500"))
.with_command("cp ", MockCommandResult::err(1, "Permission denied"));
let result = backup_before_deploy_with_runner(&worker, &mut manager, &mock)
.await
.unwrap();
assert!(result.is_none());
assert!(manager.get_latest_backup(&worker.id.0).is_none());
}
#[test]
fn is_safe_version_token_accepts_conventional_and_rejects_metachars() {
for ok in ["1.0.0", "0.2.2", "1.2.3-rc.1+build.5", "rch_wkr-2024"] {
assert!(is_safe_version_token(ok), "{ok} should be accepted");
}
assert!(!is_safe_version_token(""), "empty must be rejected");
for bad in [
"1.0.0;rm",
"$(touch x)",
"1.0 0",
"1.0`id`",
"v*",
"a|b",
"a&b",
"a>b",
"a/b",
"a\nb",
] {
assert!(!is_safe_version_token(bad), "{bad} should be rejected");
}
}
#[tokio::test]
async fn backup_before_deploy_rejects_unsafe_version_token() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let worker = test_worker_config();
let temp_dir = tempfile::tempdir().unwrap();
let mut manager = RollbackManager::with_path(temp_dir.path()).unwrap();
let mock = MockSshExecutor::new()
.with_command("--version", MockCommandResult::ok("rch-wkr 1.0.0;rm"))
.with_command("mkdir -p", MockCommandResult::ok(""))
.with_command("df -Pm", MockCommandResult::ok("500"))
.with_command("cp ", MockCommandResult::ok(""));
let result = backup_before_deploy_with_runner(&worker, &mut manager, &mock)
.await
.unwrap();
assert!(result.is_none(), "unsafe version must skip the backup");
assert!(manager.get_latest_backup(&worker.id.0).is_none());
}
use rch_common::fleet_provenance::{SignatureMaterial, reason_code};
const TEST_TRIPLE: &str = "x86_64-unknown-linux-musl";
fn fake_binary(content: &[u8]) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rch-wkr");
std::fs::write(&path, content).unwrap();
(dir, path)
}
#[test]
fn provenance_manifest_path_appends_suffix() {
let p = provenance_manifest_path(Path::new("/tmp/rch-wkr"));
assert_eq!(p, PathBuf::from("/tmp/rch-wkr.provenance.json"));
}
#[test]
fn provenance_detail_renders_each_verdict() {
assert_eq!(
provenance_detail(&ProvenanceVerdict::Verified),
"artifact provenance verified"
);
assert_eq!(
provenance_detail(&ProvenanceVerdict::DevArtifactAllowed {
reason: "because dev".to_string()
}),
"because dev"
);
assert_eq!(
provenance_detail(&ProvenanceVerdict::Rejected {
reason_code: reason_code::CHECKSUM_MISMATCH.to_string(),
detail: "bad hash".to_string()
}),
"bad hash"
);
}
#[test]
fn load_artifact_provenance_falls_back_to_dev_artifact() {
let (_dir, bin) = fake_binary(b"hello");
let prov = load_artifact_provenance(&bin, TEST_TRIPLE);
assert_eq!(prov.target_triple, TEST_TRIPLE);
assert_eq!(prov.artifact_id, "rch-wkr");
assert!(prov.expected_sha256.is_none());
assert!(prov.signature.is_none());
assert!(prov.release_id.is_none());
}
#[test]
fn load_artifact_provenance_reads_sidecar_manifest() {
let (_dir, bin) = fake_binary(b"hello");
let manifest = ArtifactProvenance {
artifact_id: "rch-wkr-v1.0.42".to_string(),
release_id: Some("v1.0.42".to_string()),
target_triple: "aarch64-apple-darwin".to_string(),
expected_sha256: Some("deadbeef".to_string()),
signature: Some(SignatureMaterial {
bundle_ref: "/nonexistent/bundle.json".to_string(),
identity_pattern: "^https://example$".to_string(),
}),
builder_identity: Some("ci".to_string()),
expected_protocol_version: Some(2),
};
std::fs::write(
provenance_manifest_path(&bin),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let loaded = load_artifact_provenance(&bin, TEST_TRIPLE);
assert_eq!(loaded, manifest);
}
#[test]
fn load_artifact_provenance_unparseable_manifest_falls_back() {
let (_dir, bin) = fake_binary(b"hello");
std::fs::write(provenance_manifest_path(&bin), b"{ not json").unwrap();
let prov = load_artifact_provenance(&bin, TEST_TRIPLE);
assert_eq!(prov.target_triple, TEST_TRIPLE);
assert!(prov.expected_sha256.is_none());
}
#[tokio::test]
async fn gate_dev_artifact_under_dev_friendly_is_allowed() {
let (_dir, bin) = fake_binary(b"a local dev binary");
let (verdict, prov) =
run_provenance_gate(&bin, Some(TEST_TRIPLE), &ProvenancePolicy::dev_friendly()).await;
assert!(verdict.may_transfer());
assert_eq!(verdict.verification_status(), "dev_allowed");
assert_eq!(prov.target_triple, TEST_TRIPLE);
}
#[tokio::test]
async fn gate_dev_artifact_under_strict_is_rejected() {
let (_dir, bin) = fake_binary(b"a local dev binary");
let (verdict, _prov) =
run_provenance_gate(&bin, Some(TEST_TRIPLE), &ProvenancePolicy::STRICT).await;
assert!(!verdict.may_transfer());
assert_eq!(verdict.reason_code(), Some(reason_code::CHECKSUM_MISSING));
}
#[tokio::test]
async fn gate_manifest_matching_checksum_is_verified() {
let (_dir, bin) = fake_binary(b"release artifact bytes");
let sha = local_file_sha256_hex(&bin).unwrap();
let manifest = ArtifactProvenance {
artifact_id: "rch-wkr-v1.0.42".to_string(),
release_id: Some("v1.0.42".to_string()),
target_triple: TEST_TRIPLE.to_string(),
expected_sha256: Some(sha),
signature: None,
builder_identity: Some("github-actions".to_string()),
expected_protocol_version: Some(1),
};
std::fs::write(
provenance_manifest_path(&bin),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let (verdict, _prov) =
run_provenance_gate(&bin, Some(TEST_TRIPLE), &ProvenancePolicy::dev_friendly()).await;
assert_eq!(verdict, ProvenanceVerdict::Verified);
}
#[tokio::test]
async fn gate_manifest_checksum_mismatch_is_rejected() {
let (_dir, bin) = fake_binary(b"release artifact bytes");
let manifest = ArtifactProvenance {
artifact_id: "rch-wkr-v1.0.42".to_string(),
release_id: Some("v1.0.42".to_string()),
target_triple: TEST_TRIPLE.to_string(),
expected_sha256: Some(
"0000000000000000000000000000000000000000000000000000000000000000".to_string(),
),
signature: None,
builder_identity: None,
expected_protocol_version: None,
};
std::fs::write(
provenance_manifest_path(&bin),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let (verdict, _prov) =
run_provenance_gate(&bin, Some(TEST_TRIPLE), &ProvenancePolicy::dev_friendly()).await;
assert_eq!(verdict.reason_code(), Some(reason_code::CHECKSUM_MISMATCH));
assert!(!verdict.may_transfer());
}
#[tokio::test]
async fn gate_wrong_target_triple_is_rejected() {
let (_dir, bin) = fake_binary(b"release artifact bytes");
let sha = local_file_sha256_hex(&bin).unwrap();
let manifest = ArtifactProvenance {
artifact_id: "rch-wkr".to_string(),
release_id: None,
target_triple: "aarch64-apple-darwin".to_string(),
expected_sha256: Some(sha),
signature: None,
builder_identity: None,
expected_protocol_version: None,
};
std::fs::write(
provenance_manifest_path(&bin),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let (verdict, _prov) =
run_provenance_gate(&bin, Some(TEST_TRIPLE), &ProvenancePolicy::dev_friendly()).await;
assert_eq!(
verdict.reason_code(),
Some(reason_code::WRONG_TARGET_TRIPLE)
);
assert!(!verdict.may_transfer());
}
#[tokio::test]
async fn gate_unknown_worker_triple_judges_on_checksum() {
let (_dir, bin) = fake_binary(b"release artifact bytes");
let sha = local_file_sha256_hex(&bin).unwrap();
let manifest = ArtifactProvenance {
artifact_id: "rch-wkr".to_string(),
release_id: None,
target_triple: "aarch64-apple-darwin".to_string(),
expected_sha256: Some(sha),
signature: None,
builder_identity: None,
expected_protocol_version: None,
};
std::fs::write(
provenance_manifest_path(&bin),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let (verdict, _prov) =
run_provenance_gate(&bin, None, &ProvenancePolicy::dev_friendly()).await;
assert_eq!(verdict, ProvenanceVerdict::Verified);
}
fn smoke_worker() -> WorkerConfig {
WorkerConfig {
user: "rch".to_string(),
..Default::default()
}
}
#[tokio::test]
async fn smoke_capabilities_admits_healthy_worker() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT arch=x86_64\nRCH_FACT user=rch\n\
RCH_FACT rch_wkr_path=/home/rch/.local/bin/rch-wkr\nRCH_FACT worker_version=1.0.41\n\
RCH_FACT worker_protocol=3\nRCH_FACT cargo_version=cargo 1.98.0-nightly\n\
RCH_FACT toolchain=nightly\nRCH_FACT target=x86_64-unknown-linux-gnu\n\
RCH_FACT disk=/data/tmp;1048576;524288;900000\n";
let mock = MockSshExecutor::new()
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let verdict = run_smoke_capabilities_with_runner(
&smoke_worker(),
&CapabilityRequirement::rust(1),
&mock,
)
.await
.unwrap();
assert_eq!(verdict, CapabilityVerdict::Admissible);
}
#[tokio::test]
async fn smoke_capabilities_rejects_binary_unusable_as_user() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT arch=x86_64\nRCH_FACT user=rch\n";
let mock = MockSshExecutor::new()
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let verdict = run_smoke_capabilities_with_runner(
&smoke_worker(),
&CapabilityRequirement::rust(1),
&mock,
)
.await
.unwrap();
assert!(matches!(verdict, CapabilityVerdict::Rejected { .. }));
}
#[tokio::test]
async fn smoke_capabilities_unreachable_probe_is_rejected_not_passed() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let mock =
MockSshExecutor::new().with_command("--protocol-version", MockCommandResult::ok(""));
let verdict = run_smoke_capabilities_with_runner(
&smoke_worker(),
&CapabilityRequirement::rust(1),
&mock,
)
.await
.unwrap();
match verdict {
CapabilityVerdict::Rejected { reason, .. } => {
assert_eq!(reason, IncidentReasonCode::WrongUserPathWorkerBinary);
}
other => panic!("expected Rejected for an unreachable probe, got {other:?}"),
}
}
#[tokio::test]
async fn smoke_capabilities_rejects_protocol_below_requirement() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT arch=x86_64\nRCH_FACT user=rch\n\
RCH_FACT rch_wkr_path=/home/rch/.local/bin/rch-wkr\nRCH_FACT worker_version=1.0.41\n\
RCH_FACT worker_protocol=1\nRCH_FACT cargo_version=cargo 1.98.0-nightly\n\
RCH_FACT target=x86_64-unknown-linux-gnu\n";
let mock = MockSshExecutor::new()
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let verdict = run_smoke_capabilities_with_runner(
&smoke_worker(),
&CapabilityRequirement::rust(3),
&mock,
)
.await
.unwrap();
assert!(matches!(verdict, CapabilityVerdict::Rejected { .. }));
}
#[tokio::test]
async fn smoke_capabilities_default_admits_worker_without_protocol_version() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT arch=x86_64\nRCH_FACT user=rch\n\
RCH_FACT rch_wkr_path=/home/rch/.local/bin/rch-wkr\nRCH_FACT worker_version=1.0.41\n\
RCH_FACT cargo_version=cargo 1.98.0-nightly\n";
let mock = MockSshExecutor::new()
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let verdict = run_smoke_capabilities_with_runner(
&smoke_worker(),
&smoke_capability_requirement(),
&mock,
)
.await
.unwrap();
assert_eq!(
verdict,
CapabilityVerdict::Admissible,
"min_protocol=0 must admit a real worker lacking --protocol-version"
);
}
#[tokio::test]
async fn smoke_disk_inode_ok_with_headroom() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT disk=/data/tmp;1048576;524288;900000\n";
let mock = MockSshExecutor::new().with_command("df -Pk", MockCommandResult::ok(probe_out));
let level = run_smoke_disk_inode_with_runner(
&smoke_worker(),
&["/data/tmp".to_string()],
&PressureThresholds::default(),
&mock,
)
.await
.unwrap();
assert_eq!(level, PressureLevel::Ok);
}
#[tokio::test]
async fn smoke_disk_inode_critical_on_low_bytes() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT disk=/data/tmp;1048576;10240;900000\n";
let mock = MockSshExecutor::new().with_command("df -Pk", MockCommandResult::ok(probe_out));
let level = run_smoke_disk_inode_with_runner(
&smoke_worker(),
&["/data/tmp".to_string()],
&PressureThresholds::default(),
&mock,
)
.await
.unwrap();
assert_eq!(level, PressureLevel::Critical);
}
#[tokio::test]
async fn smoke_disk_inode_critical_on_low_inodes() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT disk=/data/tmp;1048576;524288;5000\n";
let mock = MockSshExecutor::new().with_command("df -Pk", MockCommandResult::ok(probe_out));
let level = run_smoke_disk_inode_with_runner(
&smoke_worker(),
&["/data/tmp".to_string()],
&PressureThresholds::default(),
&mock,
)
.await
.unwrap();
assert_eq!(level, PressureLevel::Critical);
}
#[tokio::test]
async fn smoke_disk_inode_unknown_when_no_roots() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let mock = MockSshExecutor::new().with_command(
"--protocol-version",
MockCommandResult::ok("RCH_FACT os=linux\n"),
);
let level = run_smoke_disk_inode_with_runner(
&smoke_worker(),
&[],
&PressureThresholds::default(),
&mock,
)
.await
.unwrap();
assert_eq!(level, PressureLevel::Unknown);
}
#[tokio::test]
async fn smoke_disk_inode_worst_root_wins() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT disk=/data/tmp;1048576;524288;900000\n\
RCH_FACT disk=/var/cache;1048576;10240;900000\n";
let mock = MockSshExecutor::new().with_command("df -Pk", MockCommandResult::ok(probe_out));
let level = run_smoke_disk_inode_with_runner(
&smoke_worker(),
&["/data/tmp".to_string(), "/var/cache".to_string()],
&PressureThresholds::default(),
&mock,
)
.await
.unwrap();
assert_eq!(level, PressureLevel::Critical);
}
fn smoke_worker_named(id: &str) -> WorkerConfig {
WorkerConfig {
id: WorkerId(id.to_string()),
user: "rch".to_string(),
..Default::default()
}
}
#[test]
fn smoke_capability_probe_spec_uses_absolute_path() {
let spec = smoke_capability_probe_spec(&smoke_worker()); assert_eq!(spec.rch_wkr_path, "/home/rch/.local/bin/rch-wkr");
assert!(!spec.rch_wkr_path.contains('~'));
let root = WorkerConfig {
user: "root".to_string(),
..Default::default()
};
assert_eq!(
smoke_capability_probe_spec(&root).rch_wkr_path,
"/root/.local/bin/rch-wkr"
);
}
#[tokio::test]
async fn smoke_worker_scenarios_all_pass_on_healthy_worker() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT arch=x86_64\nRCH_FACT user=rch\n\
RCH_FACT rch_wkr_path=/home/rch/.local/bin/rch-wkr\nRCH_FACT worker_version=1.0.41\n\
RCH_FACT cargo_version=cargo 1.98.0-nightly\n\
RCH_FACT disk=/tmp/rch;1048576;524288;900000\n";
let mock = MockSshExecutor::new()
.with_command("RCH_FACT", MockCommandResult::ok(probe_out))
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let events = run_smoke_worker_scenarios_with_runner(
"run-x",
"bd-...-16.6",
&smoke_worker_named("css"),
&["/tmp/rch".to_string()],
&PressureThresholds::default(),
&mock,
)
.await;
assert_eq!(events.len(), 4);
let tokens: Vec<(&str, &str)> = events
.iter()
.map(|e| (e.scenario.as_str(), e.event.as_str()))
.collect();
assert_eq!(
tokens,
vec![
("worker_capabilities_exact_user_path", "started"),
("worker_capabilities_exact_user_path", "passed"),
("disk_inode_admission", "started"),
("disk_inode_admission", "passed"),
]
);
for e in &events {
assert_eq!(e.worker_id.as_deref(), Some("css"));
assert_eq!(e.run_id, "run-x");
}
for e in events.iter().filter(|e| e.event == "passed") {
assert!(e.reason_code.is_none());
}
}
#[tokio::test]
async fn smoke_worker_scenarios_capabilities_fail_disk_pass() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT arch=x86_64\nRCH_FACT user=rch\n\
RCH_FACT disk=/tmp/rch;1048576;524288;900000\n";
let mock = MockSshExecutor::new()
.with_command("RCH_FACT", MockCommandResult::ok(probe_out))
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let events = run_smoke_worker_scenarios_with_runner(
"run-y",
"bd-...-16.6",
&smoke_worker_named("hz1"),
&["/tmp/rch".to_string()],
&PressureThresholds::default(),
&mock,
)
.await;
let cap = events
.iter()
.find(|e| e.scenario == "worker_capabilities_exact_user_path" && e.event == "failed")
.expect("capabilities failed event");
assert_eq!(cap.status, "fail");
assert_eq!(
cap.reason_code.as_deref(),
Some(IncidentReasonCode::WrongUserPathWorkerBinary.code())
);
assert!(
events
.iter()
.any(|e| e.scenario == "disk_inode_admission" && e.event == "passed")
);
}
#[tokio::test]
async fn smoke_worker_scenarios_disk_critical_fails() {
use crate::fleet::ssh::{MockCommandResult, MockSshExecutor};
let probe_out = "RCH_FACT os=linux\nRCH_FACT user=rch\n\
RCH_FACT rch_wkr_path=/home/rch/.local/bin/rch-wkr\nRCH_FACT worker_version=1.0.41\n\
RCH_FACT cargo_version=cargo 1.98.0-nightly\n\
RCH_FACT disk=/tmp/rch;1048576;10240;900000\n";
let mock = MockSshExecutor::new()
.with_command("RCH_FACT", MockCommandResult::ok(probe_out))
.with_command("--protocol-version", MockCommandResult::ok(probe_out));
let events = run_smoke_worker_scenarios_with_runner(
"run-z",
"bd-...-16.6",
&smoke_worker_named("hz1"),
&["/tmp/rch".to_string()],
&PressureThresholds::default(),
&mock,
)
.await;
let disk = events
.iter()
.find(|e| e.scenario == "disk_inode_admission" && e.event == "failed")
.expect("disk failed event");
assert_eq!(
disk.reason_code.as_deref(),
Some(IncidentReasonCode::CriticalPressure.code())
);
}
#[tokio::test]
async fn smoke_worker_scenarios_unreachable_fails_both() {
use crate::fleet::ssh::MockSshExecutor;
let mock = MockSshExecutor::unreachable();
let events = run_smoke_worker_scenarios_with_runner(
"run-w",
"bd-...-16.6",
&smoke_worker_named("dead"),
&["/tmp/rch".to_string()],
&PressureThresholds::default(),
&mock,
)
.await;
let failed: Vec<&str> = events
.iter()
.filter(|e| e.event == "failed")
.map(|e| e.scenario.as_str())
.collect();
assert_eq!(
failed,
vec![
"worker_capabilities_exact_user_path",
"disk_inode_admission"
]
);
for e in events.iter().filter(|e| e.event == "failed") {
assert_eq!(e.reason_code.as_deref(), Some("worker_unreachable"));
}
}
}