haematite-cli 0.21.3

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::Deserialize;
use std::{collections::BTreeMap, io::Read as _, path::Path};

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Proof {
    headers: BTreeMap<String, String>,
}

pub(in crate::assistant) fn read(path: &str, bound: usize) -> anyhow::Result<String> {
    use std::os::unix::fs::{MetadataExt as _, OpenOptionsExt as _, PermissionsExt as _};
    anyhow::ensure!(
        Path::new(path).is_absolute(),
        "assistant_needs_lys: proof_file_must_be_absolute"
    );
    let file = std::fs::OpenOptions::new()
        .read(true)
        .custom_flags(i32::try_from(
            (rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::NONBLOCK).bits(),
        )?)
        .open(path)
        .map_err(|error| {
            anyhow::anyhow!(
                "assistant_needs_lys: proof_file_unreadable: {}",
                error.kind()
            )
        })?;
    let metadata = file.metadata().map_err(|error| {
        anyhow::anyhow!(
            "assistant_needs_lys: proof_file_metadata_failed: {}",
            error.kind()
        )
    })?;
    anyhow::ensure!(
        metadata.is_file()
            && metadata.uid() == rustix::process::geteuid().as_raw()
            && metadata.permissions().mode().trailing_zeros() >= 6,
        "assistant_needs_lys: proof_file_not_private"
    );
    let mut bytes = Vec::new();
    file.take(u64::try_from(bound)?.saturating_add(1))
        .read_to_end(&mut bytes)
        .map_err(|error| {
            anyhow::anyhow!(
                "assistant_needs_lys: proof_file_unreadable: {}",
                error.kind()
            )
        })?;
    anyhow::ensure!(
        bytes.len() <= bound,
        "assistant_needs_lys: proof_file_exceeds_bound"
    );
    let text = std::str::from_utf8(&bytes)
        .map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_file_invalid"))?;
    let validated: Proof = serde_json::from_str(text)
        .map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_file_invalid"))?;
    let canonical = serde_json::to_string(&serde_json::json!({"headers":validated.headers}))?;
    headers(&canonical)?;
    Ok(canonical)
}

pub(in crate::assistant) fn headers(text: &str) -> anyhow::Result<HeaderMap> {
    let proof: Proof = serde_json::from_str(text)
        .map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_headers_invalid"))?;
    anyhow::ensure!(
        proof.headers.contains_key("lys-agent-pass") && proof.headers.len() <= 2,
        "assistant_needs_lys: proof_headers_invalid"
    );
    let mut headers = HeaderMap::new();
    for (name, value) in proof.headers {
        anyhow::ensure!(
            matches!(name.as_str(), "lys-agent-pass" | "lys-seat")
                && !value.is_empty()
                && value.bytes().all(|byte| byte.is_ascii_graphic()),
            "assistant_needs_lys: proof_headers_invalid"
        );
        let name = HeaderName::from_bytes(name.as_bytes())
            .map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_header_name_invalid"))?;
        let mut value = HeaderValue::from_str(&value)
            .map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_header_value_invalid"))?;
        value.set_sensitive(true);
        headers.insert(name, value);
    }
    Ok(headers)
}