runx-cli 0.6.19

Cargo-installed launcher for the runx governed agent workflow CLI.
Documentation
use std::collections::BTreeMap;
use std::env;
use std::fmt;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

use runx_contracts::{
    OperationalPolicy, OperationalPolicyError, OperationalPolicyReadback,
    OperationalPolicyValidationFinding, project_operational_policy_readback,
};
use serde::Serialize;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PolicyAction {
    Inspect,
    Lint,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyPlan {
    pub action: PolicyAction,
    pub path: PathBuf,
    pub json: bool,
}

pub fn run_native_policy(plan: PolicyPlan) -> ExitCode {
    let cwd = match env::current_dir() {
        Ok(cwd) => cwd,
        Err(error) => {
            let _ignored = crate::cli_io::write_stderr_code(&format!(
                "runx: failed to resolve cwd: {error}\n"
            ));
            return ExitCode::from(1);
        }
    };
    match run_policy_command(&plan, &crate::cli_io::env_map(), &cwd) {
        Ok(output) => crate::cli_io::write_stdout_code(&output.stdout, output.exit_code),
        Err(error) => {
            let _ignored = crate::cli_io::write_stderr_code(&format!("runx: {error}\n"));
            ExitCode::from(error.exit_code())
        }
    }
}

pub fn run_policy_command(
    plan: &PolicyPlan,
    env: &BTreeMap<String, String>,
    cwd: &Path,
) -> Result<PolicyCliOutput, PolicyCliError> {
    let resolved_path = resolve_policy_path(&plan.path, env, cwd);
    let raw = fs::read_to_string(&resolved_path)
        .map_err(|error| PolicyCliError::Read(resolved_path.clone(), error))?;
    let policy = serde_json::from_str::<OperationalPolicy>(&raw)
        .map_err(|error| PolicyCliError::Parse(resolved_path.clone(), error))?;
    let readback = project_operational_policy_readback(&policy)?;
    let findings = readback.findings.clone();
    let result = PolicyCommandResult {
        action: plan.action,
        status: if readback.valid { "success" } else { "failure" },
        path: display_policy_path(&resolved_path, env, cwd),
        policy: readback,
        findings,
    };
    let stdout = if plan.json {
        serde_json::to_string_pretty(&result)
            .map(|json| format!("{json}\n"))
            .map_err(PolicyCliError::Serialize)?
    } else {
        render_policy_result(&result)
    };
    let exit_code = if result.status == "success" { 0 } else { 1 };
    Ok(PolicyCliOutput { stdout, exit_code })
}

#[derive(Debug)]
pub struct PolicyCliOutput {
    pub stdout: String,
    pub exit_code: u8,
}

#[derive(Debug)]
pub enum PolicyCliError {
    Read(PathBuf, io::Error),
    Parse(PathBuf, serde_json::Error),
    Contract(OperationalPolicyError),
    Serialize(serde_json::Error),
}

impl PolicyCliError {
    fn exit_code(&self) -> u8 {
        match self {
            Self::Read(_, _) | Self::Parse(_, _) | Self::Contract(_) | Self::Serialize(_) => 1,
        }
    }
}

impl fmt::Display for PolicyCliError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Read(path, error) => {
                write!(
                    formatter,
                    "failed to read policy {}: {error}",
                    path.display()
                )
            }
            Self::Parse(path, error) => {
                write!(formatter, "invalid JSON policy {}: {error}", path.display())
            }
            Self::Contract(error) => write!(formatter, "{error}"),
            Self::Serialize(error) => write!(formatter, "failed to serialize policy: {error}"),
        }
    }
}

impl std::error::Error for PolicyCliError {}

impl From<OperationalPolicyError> for PolicyCliError {
    fn from(error: OperationalPolicyError) -> Self {
        Self::Contract(error)
    }
}

#[derive(Serialize)]
struct PolicyCommandResult {
    action: PolicyAction,
    status: &'static str,
    path: String,
    policy: OperationalPolicyReadback,
    findings: Vec<OperationalPolicyValidationFinding>,
}

impl PolicyCommandResult {
    fn findings(&self) -> &[OperationalPolicyValidationFinding] {
        &self.findings
    }
}

fn render_policy_result(result: &PolicyCommandResult) -> String {
    let mut lines = vec![
        String::new(),
        format!(
            "  {}  policy {}  {}",
            status_icon(result.status),
            policy_action_name(result.action),
            result.status
        ),
    ];
    lines.extend(render_key_value_rows(&[
        ("path", result.path.clone()),
        ("policy", result.policy.policy_id.clone()),
        ("schema", result.policy.schema_version.to_string()),
        ("sources", result.policy.sources.len().to_string()),
        ("targets", result.policy.targets.len().to_string()),
        ("runners", result.policy.runners.len().to_string()),
        ("findings", result.findings().len().to_string()),
    ]));
    push_sources(&mut lines, result);
    push_targets(&mut lines, result);
    push_findings(&mut lines, result.findings());
    lines.push(String::new());
    format!("{}\n", lines.join("\n"))
}

fn push_sources(lines: &mut Vec<String>, result: &PolicyCommandResult) {
    if result.policy.sources.is_empty() {
        return;
    }
    lines.push(String::new());
    lines.push("  sources".to_owned());
    for source in &result.policy.sources {
        lines.push(format!(
            "  - {}: {}; locators={}; thread={}; actions={}",
            source.source_id,
            source.provider,
            source.locator_count,
            source_thread_label(source.source_thread_required, &source.publish_mode),
            join_actions(&source.allowed_actions)
        ));
    }
}

fn push_targets(lines: &mut Vec<String>, result: &PolicyCommandResult) {
    if result.policy.targets.is_empty() {
        return;
    }
    lines.push(String::new());
    lines.push("  targets".to_owned());
    for target in &result.policy.targets {
        lines.push(format!(
            "  - {}: runners={}; available={}; owners={}; actions={}",
            target.repo,
            target.runner_ids.join(","),
            target.available_runner_count,
            target.owner_count,
            join_actions(&target.allowed_actions)
        ));
    }
}

fn push_findings(lines: &mut Vec<String>, findings: &[OperationalPolicyValidationFinding]) {
    if findings.is_empty() {
        return;
    }
    lines.push(String::new());
    lines.push("  findings".to_owned());
    for finding in findings {
        lines.push(format!(
            "  - {} {}: {}",
            finding.code, finding.path, finding.message
        ));
    }
}

fn render_key_value_rows(rows: &[(&str, String)]) -> Vec<String> {
    rows.iter()
        .map(|(key, value)| format!("  {key:<9} {value}"))
        .collect()
}

fn status_icon(status: &str) -> &'static str {
    if status == "success" { "ok" } else { "fail" }
}

fn policy_action_name(action: PolicyAction) -> &'static str {
    match action {
        PolicyAction::Inspect => "inspect",
        PolicyAction::Lint => "lint",
    }
}

fn source_thread_label(
    required: bool,
    mode: &runx_contracts::OperationalPolicyPublishMode,
) -> String {
    if required {
        mode.to_string()
    } else {
        "not-required".to_owned()
    }
}

fn join_actions(actions: &[runx_contracts::OperationalPolicyAction]) -> String {
    actions
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(",")
}

fn resolve_policy_path(path: &Path, env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
    if path.is_absolute() {
        return path.to_path_buf();
    }
    env.get("RUNX_CWD")
        .map(PathBuf::from)
        .or_else(|| env.get("INIT_CWD").map(PathBuf::from))
        .unwrap_or_else(|| cwd.to_path_buf())
        .join(path)
}

fn display_policy_path(path: &Path, env: &BTreeMap<String, String>, cwd: &Path) -> String {
    let base = env
        .get("RUNX_CWD")
        .map(PathBuf::from)
        .or_else(|| env.get("INIT_CWD").map(PathBuf::from))
        .unwrap_or_else(|| cwd.to_path_buf());
    path.strip_prefix(&base)
        .map(|relative| relative.to_string_lossy().replace('\\', "/"))
        .unwrap_or_else(|_| {
            path.file_name()
                .map(|name| name.to_string_lossy().into_owned())
                .unwrap_or_else(|| path.to_string_lossy().into_owned())
        })
}