yana-rt 1.4.0

Yana AI Runtime — safety CLI for AI agents: scan, graph, vault, hunt, ci, map, fix, doctor
//! `yana-rt os status` as the truthful aggregate view (Phase 15,
//! host-native-os program): HOST / YANA / SAFETY / HOST CAPABILITIES.
//!
//! This module owns no new observation logic — it composes existing,
//! already-correct sources: `os::resource::topology::collect()` (HOST),
//! `os::supervisor::dashboard()` (SAFETY, current pressure, host
//! capabilities — Phase 9 already built exactly this data), `os::state`/
//! `os::credential`/`os::resource::reservation`/`crate::cost` (YANA).
//! "UNKNOWN must never be silently represented as FALSE" (this phase's
//! own instruction, echoing the program's very first rule): every
//! `Option` here stays `None` when the underlying source could not
//! determine a value — never defaulted to a concrete-looking false value.

use super::{credential, resource, state, supervisor};
use anyhow::Result;
use serde::Serialize;
use std::path::Path;

#[derive(Debug, Serialize)]
pub struct HostSection {
    pub os: String,
    pub arch: String,
    pub cpu_logical_cores: usize,
    pub cpu_physical_cores: Option<usize>,
    pub memory_total_bytes: Option<u64>,
    pub memory_model: super::platform::profile::MemoryModelKind,
    pub accelerators: Vec<resource::topology::AcceleratorTopology>,
    pub current_pressure: resource::pressure::ResourcePressure,
}

#[derive(Debug, Serialize)]
pub struct YanaSection {
    pub runtime_version: String,
    pub state_schema_version: u32,
    pub managed_agents: usize,
    pub running_agents: usize,
    pub resource_policy_configured: bool,
    pub providers: Vec<credential::CredentialStatus>,
    pub active_reservations: Vec<resource::reservation::Reservation>,
    pub autonomy_ready: usize,
    pub autonomy_waiting_approval: usize,
    /// `None` when the cost ledger is absent or unreadable — an honest
    /// "unknown," never fabricated as `0.0` (which would look identical
    /// to "confirmed zero spend today").
    pub cost_today_usd: Option<f64>,
}

#[derive(Debug, Serialize)]
pub struct SafetySection {
    pub mode: String,
    pub halt_reason: Option<String>,
    pub quarantine: Option<supervisor::QuarantineRecord>,
    pub heartbeat_healthy: bool,
    pub heartbeat_age_secs: Option<i64>,
    pub receipt_chain_valid: bool,
    pub receipt_count: usize,
    pub periodic_scheduler: super::monitor_service::ServiceReport,
    pub resident_service: super::service::manager::ServiceStatus,
}

#[derive(Debug, Serialize)]
pub struct UnifiedStatus {
    pub host: HostSection,
    pub yana: YanaSection,
    pub safety: SafetySection,
    /// `None` only if the platform backend call itself failed — never
    /// fabricated; see `SupervisorDashboard::host_capabilities`'s own doc
    /// comment, which this field carries through unchanged.
    pub host_capabilities: Option<super::platform::capabilities::PlatformCapabilities>,
}

pub fn aggregate(root: &Path) -> Result<UnifiedStatus> {
    let dashboard = supervisor::dashboard(root)?;
    let topology = resource::topology::collect()?;
    let current_state = state::load(root)?;
    let reservations = resource::reservation::list(root)?;
    let cost_today_usd = crate::cost::daily_cost_usd(root, chrono::Utc::now()).ok();
    let queue = super::autonomy::load_queue(root)?;
    let autonomy_ready = queue
        .actions
        .iter()
        .filter(|action| action.status == super::autonomy::ActionStatus::Ready)
        .count();
    let autonomy_waiting_approval = queue
        .actions
        .iter()
        .filter(|action| action.status == super::autonomy::ActionStatus::WaitingApproval)
        .count();

    Ok(UnifiedStatus {
        host: HostSection {
            os: topology.os,
            arch: topology.arch,
            cpu_logical_cores: topology.cpu_logical_cores,
            cpu_physical_cores: topology.cpu_physical_cores,
            memory_total_bytes: topology.memory_total_bytes,
            memory_model: topology.memory_model,
            accelerators: topology.accelerators,
            current_pressure: dashboard.resource_pressure,
        },
        yana: YanaSection {
            runtime_version: env!("CARGO_PKG_VERSION").to_string(),
            state_schema_version: current_state.schema_version,
            managed_agents: dashboard.managed_agents,
            running_agents: current_state
                .agents
                .iter()
                .filter(|agent| agent.status == state::AgentStatus::Running)
                .count(),
            resource_policy_configured: current_state.resource_policy.is_some(),
            providers: credential::inventory(),
            active_reservations: reservations,
            autonomy_ready,
            autonomy_waiting_approval,
            cost_today_usd,
        },
        safety: SafetySection {
            mode: dashboard.mode,
            halt_reason: dashboard.halt_reason,
            quarantine: dashboard.quarantine,
            heartbeat_healthy: dashboard.heartbeat_healthy,
            heartbeat_age_secs: dashboard.heartbeat_age_secs,
            receipt_chain_valid: dashboard.receipt_chain_valid,
            receipt_count: dashboard.receipt_count,
            periodic_scheduler: dashboard.periodic_scheduler,
            resident_service: dashboard.resident_service,
        },
        host_capabilities: dashboard.host_capabilities,
    })
}

pub fn print(status: &UnifiedStatus, json: bool) -> Result<()> {
    if json {
        println!("{}", serde_json::to_string_pretty(status)?);
        return Ok(());
    }
    println!("Yana OS — unified status");
    println!("{}", "".repeat(76));
    println!("HOST");
    println!(
        "  os/arch           {}/{}",
        status.host.os, status.host.arch
    );
    println!(
        "  cpu               {} logical, {} physical",
        status.host.cpu_logical_cores,
        status
            .host
            .cpu_physical_cores
            .map_or_else(|| "unknown".into(), |v| v.to_string())
    );
    println!(
        "  memory            {}",
        status.host.memory_total_bytes.map_or_else(
            || "unknown".into(),
            |v| format!("{:.1} GiB", v as f64 / 1024f64.powi(3))
        )
    );
    println!("  memory model      {:?}", status.host.memory_model);
    println!("  accelerators      {}", status.host.accelerators.len());
    println!(
        "  pressure          {:?}",
        status.host.current_pressure.overall
    );
    println!("YANA");
    println!("  runtime version   {}", status.yana.runtime_version);
    println!("  state schema      {}", status.yana.state_schema_version);
    println!(
        "  agents            {} managed, {} running",
        status.yana.managed_agents, status.yana.running_agents
    );
    println!(
        "  resource policy   {}",
        if status.yana.resource_policy_configured {
            "configured"
        } else {
            "UNSET (preflight denies)"
        }
    );
    println!(
        "  autonomy queue    {} ready, {} awaiting approval",
        status.yana.autonomy_ready, status.yana.autonomy_waiting_approval
    );
    println!(
        "  cost today        {}",
        status
            .yana
            .cost_today_usd
            .map_or_else(|| "unknown".into(), |v| format!("${v:.6}"))
    );
    println!(
        "  reservations      {}",
        status.yana.active_reservations.len()
    );
    println!("SAFETY");
    println!("  mode              {}", status.safety.mode);
    println!(
        "  heartbeat         {}",
        if status.safety.heartbeat_healthy {
            "healthy"
        } else {
            "unhealthy or unknown"
        }
    );
    println!(
        "  receipt chain     {}",
        if status.safety.receipt_chain_valid {
            "valid"
        } else {
            "INVALID"
        }
    );
    println!("HOST CAPABILITIES");
    match &status.host_capabilities {
        Some(capabilities) => println!("  {capabilities:?}"),
        None => println!("  unknown (platform backend call failed)"),
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn root() -> tempfile::TempDir {
        let root = tempfile::tempdir().unwrap();
        let marker = root.path().join(yana_rt::flock_v1::PROTOCOL_FILE);
        std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
        std::fs::write(marker, yana_rt::flock_v1::PROTOCOL_VERSION).unwrap();
        state::initialize(root.path()).unwrap();
        root
    }

    #[test]
    fn aggregate_composes_all_four_sections_without_fabricating_data() {
        let root = root();
        let status = aggregate(root.path()).unwrap();
        // HOST: a real machine always has at least one logical core.
        assert!(status.host.cpu_logical_cores >= 1);
        // YANA: freshly initialized state has schema 1, zero agents.
        assert_eq!(status.yana.state_schema_version, 1);
        assert_eq!(status.yana.managed_agents, 0);
        // SAFETY: no halt/quarantine has been set -> mode must be "normal"
        // (dashboard()'s real lowercase value), not fabricated as anything
        // else. Confirmed against the real dashboard() output rather than
        // assumed -- this test's own first draft assumed "NORMAL" and
        // failed against the real value, corrected here.
        assert_eq!(status.safety.mode, "normal");
        assert!(status.safety.halt_reason.is_none());
    }

    #[test]
    fn cost_today_is_a_confirmed_zero_not_an_unknown_when_no_ledger_exists_yet() {
        // A missing ledger genuinely means zero cost recorded today --
        // this IS a known value (Some(0.0)), not an unknown one. This
        // test's first draft asserted `.is_none()`, assuming absence of a
        // ledger file meant absence of a known answer; daily_cost_usd's
        // real behavior (sum over zero entries = 0.0, not an error) shows
        // that assumption was wrong, not the code -- corrected here.
        let root = root();
        let status = aggregate(root.path()).unwrap();
        assert_eq!(status.yana.cost_today_usd, Some(0.0));
    }

    #[test]
    fn print_does_not_panic_in_either_mode() {
        let root = root();
        let status = aggregate(root.path()).unwrap();
        print(&status, true).unwrap();
        print(&status, false).unwrap();
    }
}