kanade-agent 0.1.2

Windows-side resident daemon for the kanade endpoint-management system. Subscribes to commands.* over NATS, runs scripts, publishes WMI inventory + heartbeats, watches for self-updates
mod commands;
mod heartbeat;
mod inventory;
mod process;
mod self_update;

use std::path::PathBuf;

use anyhow::{Context, Result};
use clap::Parser;
use kanade_shared::config::load_agent_config;
use kanade_shared::subject;
use tracing::info;

const AGENT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Parser, Debug)]
#[command(
    name = "kanade-agent",
    about = "Windows endpoint management agent (kanade)",
    version
)]
struct Cli {
    #[arg(long, default_value = "agent.toml")]
    config: PathBuf,
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info,kanade_agent=debug".into()),
        )
        .init();

    let cli = Cli::parse();
    let cfg = load_agent_config(&cli.config)
        .with_context(|| format!("load config from {:?}", cli.config))?;
    info!(
        pc_id = %cfg.agent.id,
        nats_url = %cfg.agent.nats_url,
        version = AGENT_VERSION,
        "starting kanade-agent",
    );

    let client = async_nats::connect(&cfg.agent.nats_url)
        .await
        .with_context(|| format!("connect to NATS at {}", cfg.agent.nats_url))?;
    info!("connected to NATS");

    let cmd_all = client.subscribe(subject::COMMANDS_ALL).await?;
    let cmd_self = client
        .subscribe(subject::commands_pc(&cfg.agent.id))
        .await?;
    info!(
        commands_all = subject::COMMANDS_ALL,
        commands_self = %subject::commands_pc(&cfg.agent.id),
        groups = ?cfg.agent.groups,
        "subscribed",
    );

    let pc_id = cfg.agent.id.clone();
    tokio::spawn(heartbeat::heartbeat_loop(
        client.clone(),
        pc_id.clone(),
        AGENT_VERSION.to_string(),
    ));
    tokio::spawn(inventory::inventory_loop(
        client.clone(),
        pc_id.clone(),
        cfg.inventory.clone(),
    ));
    tokio::spawn(self_update::run(client.clone(), AGENT_VERSION.to_string()));

    // Spawn one command_loop per declared group (Sprint 4a wave rollout
    // publishes to commands.group.{name}).
    for group in &cfg.agent.groups {
        let sub = client
            .subscribe(subject::commands_group(group))
            .await
            .with_context(|| format!("subscribe commands.group.{group}"))?;
        tokio::spawn(commands::command_loop(client.clone(), pc_id.clone(), sub));
        info!(group = %group, "subscribed to group subject");
    }

    let _ = tokio::join!(
        commands::command_loop(client.clone(), pc_id.clone(), cmd_all),
        commands::command_loop(client.clone(), pc_id.clone(), cmd_self),
    );

    Ok(())
}