rho-cli 0.1.28

Rho CLI tools for encrypted agent collaboration, dataset publishing, controlled runs, and result release workflows
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use rho_core::{
    RhoResult, Tool, ToolManifest, ensure_parent, normalize_actor_id, require_arg, to_yaml,
    validate_actor_id,
};

fn usage() -> ! {
    eprintln!(
        "usage: rho tools <install-minimal|list|show> ...\n\
install-minimal --shared-root <path> --owner <actor>\n\
list --shared-root <path>\n\
show --shared-root <path> <tool-id>"
    );
    std::process::exit(2);
}

fn tool_dir(shared_root: &Path) -> PathBuf {
    shared_root.join(".rho/tools")
}

fn tool_path(shared_root: &Path, id: &str) -> PathBuf {
    tool_dir(shared_root).join(format!("{id}.yaml"))
}

fn write_tool(
    shared_root: &Path,
    id: &str,
    action_type: &str,
    owner: &str,
    approval_required: bool,
) -> RhoResult<()> {
    let path = tool_path(shared_root, id);
    ensure_parent(&path)?;
    fs::write(
        &path,
        to_yaml(&ToolManifest {
            version: 1,
            tool: Tool {
                id: id.to_string(),
                action_type: action_type.to_string(),
                owner: owner.to_string(),
                approval_required,
                command_template: default_command_template(action_type),
            },
        })?,
    )?;
    println!("{}", path.display());
    Ok(())
}

fn default_command_template(action_type: &str) -> Vec<String> {
    match action_type {
        "run_mock_data" | "run_real_data" => vec![
            "python3".to_string(),
            "CODE_PATH".to_string(),
            "DATASET_CSV".to_string(),
        ],
        _ => Vec::new(),
    }
}

pub fn run(args: &[String]) -> RhoResult<()> {
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    let rest = &args[1..];
    match command {
        "install-minimal" => {
            let shared_root =
                PathBuf::from(require_arg(rest, "--shared-root").unwrap_or_else(|_| usage()));
            let owner =
                normalize_actor_id(&require_arg(rest, "--owner").unwrap_or_else(|_| usage()))?;
            validate_actor_id(&owner)?;
            write_tool(&shared_root, "run_mock", "run_mock_data", &owner, true)?;
            write_tool(&shared_root, "run_real", "run_real_data", &owner, true)?;
            write_tool(
                &shared_root,
                "publish_results",
                "release_results",
                &owner,
                true,
            )?;
        }
        "list" => {
            let shared_root =
                PathBuf::from(require_arg(rest, "--shared-root").unwrap_or_else(|_| usage()));
            let dir = tool_dir(&shared_root);
            if !dir.is_dir() {
                return Ok(());
            }
            let mut entries = fs::read_dir(dir)?
                .filter_map(Result::ok)
                .filter_map(|entry| entry.path().file_stem()?.to_str().map(ToOwned::to_owned))
                .collect::<Vec<_>>();
            entries.sort();
            for entry in entries {
                println!("{entry}");
            }
        }
        "show" => {
            let shared_root =
                PathBuf::from(require_arg(rest, "--shared-root").unwrap_or_else(|_| usage()));
            let tool_id = rest.first().cloned().unwrap_or_else(|| usage());
            print!("{}", fs::read_to_string(tool_path(&shared_root, &tool_id))?);
        }
        _ => usage(),
    }
    Ok(())
}