liminal-cli 0.2.0

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
use anyhow::{Context, bail};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use liminal_server::config::types::MachineConfig;
use lys_core::ca::create_certificate_request;
use lys_core::keys::Ed25519Identity;
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;
use zeroize::Zeroizing;

fn read_seed(path: &Path) -> anyhow::Result<Zeroizing<[u8; 32]>> {
    let metadata = std::fs::symlink_metadata(path).context("cannot inspect agent seed")?;
    if !metadata.is_file() {
        bail!("agent seed must be a regular file");
    }
    if metadata.permissions().mode() & 0o777 != 0o600 {
        bail!("agent seed permissions must be 0600");
    }
    if metadata.len() > 66 {
        bail!("agent seed must be 64 hexadecimal digits");
    }
    let mut digits = Zeroizing::new(String::new());
    std::fs::File::open(path)
        .context("cannot open agent seed")?
        .take(66)
        .read_to_string(&mut digits)
        .context("cannot read agent seed")?;
    let mut seed = Zeroizing::new([0; 32]);
    hex::decode_to_slice(digits.trim(), &mut *seed)
        .map_err(|_| anyhow::anyhow!("agent seed must be 64 hexadecimal digits"))?;
    Ok(seed)
}

fn seed(path: &Path) -> anyhow::Result<Zeroizing<[u8; 32]>> {
    match std::fs::symlink_metadata(path) {
        Ok(_) => return read_seed(path),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => return Err(error).context("cannot inspect agent seed"),
    }
    let parent = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    let mut random = Zeroizing::new([0; 32]);
    std::fs::File::open("/dev/urandom")
        .and_then(|mut file| file.read_exact(&mut *random))
        .context("cannot generate agent seed")?;
    // Publish only a complete, durable seed, without replacing a concurrent creator.
    let mut pending = tempfile::Builder::new()
        .prefix(".liminal-agent-")
        .tempfile_in(parent)
        .context("cannot create agent seed")?;
    let digits = Zeroizing::new(hex::encode(random.as_slice()));
    pending
        .write_all(digits.as_bytes())
        .context("cannot write agent seed")?;
    pending
        .as_file()
        .sync_all()
        .context("cannot flush agent seed")?;
    match pending.persist_noclobber(path) {
        Ok(file) => {
            drop(file);
            std::fs::File::open(parent)
                .and_then(|directory| directory.sync_all())
                .context("cannot flush agent seed directory")?;
            Ok(random)
        }
        Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => read_seed(path),
        Err(error) => Err(error.error).context("cannot publish agent seed"),
    }
}

pub fn run(path: &Path) -> anyhow::Result<()> {
    let text = std::fs::read_to_string(path).context("cannot read server config")?;
    let config: toml::Value = toml::from_str(&text).context("cannot parse server config")?;
    let section = config
        .get("auth")
        .and_then(|auth| auth.get("lys"))
        .and_then(|lys| lys.get("machine"))
        .context("auth.lys.machine is required")?;
    let machine: MachineConfig = section
        .clone()
        .try_into()
        .context("cannot read agent configuration")?;
    if machine.agent_id.is_empty()
        || !machine
            .agent_id
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
    {
        bail!("auth.lys.machine.agent_id must be one nonempty route segment");
    }
    if machine.identity_path.as_os_str().is_empty() {
        bail!("auth.lys.machine.identity_path is required");
    }
    let key = Arc::new(Ed25519Identity::from_seed(&seed(&machine.identity_path)?));
    let csr = create_certificate_request(&key, &machine.agent_id)
        .context("cannot sign agent certificate request")?;
    let encoded = STANDARD.encode(csr);
    let stdout = std::io::stdout();
    let mut output = stdout.lock();
    writeln!(output, "agent_id: {}", machine.agent_id)?;
    writeln!(
        output,
        "public_key: {}",
        hex::encode(key.public_key_bytes())
    )?;
    writeln!(output, "-----BEGIN CERTIFICATE REQUEST-----")?;
    for line in encoded.as_bytes().chunks(64) {
        writeln!(output, "{}", std::str::from_utf8(line)?)?;
    }
    writeln!(output, "-----END CERTIFICATE REQUEST-----")?;
    output.flush().context("cannot write agent identity")
}