haematite-cli 0.21.12

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Prerequisites are inspected without opening credentials, sockets or logs.
//! A network store is the one thing reached: over verified TLS, asked only
//! hello, which changes nothing (HAEMNET-003 R1).

use rustix::fs::{Access, AtFlags, CWD, accessat};
use std::fs;

use super::children::Config;
use crate::client::network::{Refused, Trust};
use crate::client::{Address, Reached, probe};

/// Each network store reached over TLS verified against `trust` and asked
/// hello: a line for each that says whether it was reached and whether it
/// answered, and the refusals of each that was reached and is not verified
/// TLS. A store not reached at all is a line, not a refusal: it answers its
/// routes by name as a stopped socket store does.
///
/// # Errors
///
/// The refusals, one per line, named `door_store_tls_untrusted`,
/// `door_store_not_tls` or `door_store_tls_refused` with the store's name.
pub(super) fn stores(
    config: &super::config::Config,
    trust: &Trust,
) -> Result<Vec<String>, (Vec<String>, String)> {
    let addresses = config.addresses(trust).map_err(|why| (Vec::new(), why))?;
    let mut lines = Vec::new();
    let mut refusals = Vec::new();
    for (name, address) in addresses {
        let Address::Network(remote) = address else {
            continue;
        };
        let endpoint = remote.endpoint();
        match probe(&remote, config.timeout()) {
            Reached::Answered => lines.push(format!(
                "door_store_reached: store {name}: {endpoint}: hello answered"
            )),
            Reached::Unanswered(why) => lines.push(format!(
                "door_store_reached: store {name}: {endpoint}: hello not answered: {why}"
            )),
            Reached::Refused(Refused::Unreached(why)) => {
                lines.push(format!("door_store_unreached: store {name}: {why}"));
            }
            Reached::Refused(refused) => refusals.push(format!(
                "{}: store {name}: {}",
                refused.code(),
                refused.why()
            )),
        }
    }
    if refusals.is_empty() {
        Ok(lines)
    } else {
        Err((lines, refusals.join("\n")))
    }
}

pub(super) fn configuration(config: &super::config::Config) -> Result<(), String> {
    let mut errors = Vec::new();
    if let Err(error) = super::lys::check_secret(&config.lys.secret_file) {
        errors.push(error);
    }
    if let Some(assistant) = &config.assistant
        && let Err(error) = super::assistant_socket::check_parent(&assistant.socket)
    {
        errors.push(format!("{}: {}", error.code, error.message));
    }
    for config in &config.children {
        if let Err(error) = child(config) {
            errors.push(error);
        }
    }
    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors.join("\n"))
    }
}

pub(super) fn child(config: &Config) -> Result<(), String> {
    let program_error = |error| {
        format!(
            "door_child_program_missing: {}: {}: {error}",
            config.name,
            config.program.display()
        )
    };
    let program =
        fs::metadata(&config.program).map_err(|error| program_error(error.to_string()))?;
    if !program.is_file() {
        return Err(program_error(
            "program must resolve to a regular file".to_owned(),
        ));
    }
    accessat(CWD, &config.program, Access::EXEC_OK, AtFlags::EACCESS)
        .map_err(|error| program_error(error.to_string()))?;
    log(config)
}

fn log(config: &Config) -> Result<(), String> {
    let log_error = |error| {
        format!(
            "door_child_log_unwritable: {}: {}: {error}",
            config.name,
            config.log.display()
        )
    };
    let mut path = config.log.clone();
    loop {
        match fs::metadata(&path) {
            Ok(log) => {
                if log.is_dir() {
                    return Err(log_error("log must not resolve to a folder".to_owned()));
                }
                return accessat(CWD, &path, Access::WRITE_OK, AtFlags::EACCESS)
                    .map_err(|error| log_error(error.to_string()));
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => return Err(log_error(error.to_string())),
        }
        match fs::symlink_metadata(&path) {
            Ok(link) if link.file_type().is_symlink() => {
                let target = fs::read_link(&path).map_err(|error| log_error(error.to_string()))?;
                path = if target.is_absolute() {
                    target
                } else {
                    path.parent()
                        .ok_or_else(|| log_error("log must have a parent folder".to_owned()))?
                        .join(target)
                };
            }
            Ok(_) => {
                return Err(log_error(
                    "log changed while checking its target".to_owned(),
                ));
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => break,
            Err(error) => return Err(log_error(error.to_string())),
        }
    }
    let folder = path
        .parent()
        .ok_or_else(|| log_error("log must have a parent folder".to_owned()))?;
    if !fs::metadata(folder)
        .map_err(|error| log_error(error.to_string()))?
        .is_dir()
    {
        return Err(log_error("log parent must be a folder".to_owned()));
    }
    accessat(
        CWD,
        folder,
        Access::WRITE_OK | Access::EXEC_OK,
        AtFlags::EACCESS,
    )
    .map_err(|error| log_error(error.to_string()))?;
    Ok(())
}