haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::Options;
use crate::mcp::lys_pass::{LysAgent, PassSource};
use serde_json::json;
use std::io::{BufRead as _, BufReader, Read as _, Write as _};
use std::net::TcpListener;
use std::os::unix::fs::{PermissionsExt as _, symlink};
use std::path::Path;

fn proof(path: &Path, generation: usize) -> anyhow::Result<()> {
    std::fs::write(
        path,
        serde_json::to_vec(&json!({"headers":{
            "lys-agent-pass":format!("synthetic-run-{generation}"),
            "lys-seat":format!("synthetic seat {generation}")
        }}))?,
    )?;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
    Ok(())
}

fn agent(path: &Path, issuer: &str, helper: bool) -> anyhow::Result<LysAgent> {
    let arguments = vec![
        "--lys-agent".to_owned(),
        issuer.to_owned(),
        "fixture".to_owned(),
        "--launch-proof-file".to_owned(),
        path.to_string_lossy().into_owned(),
    ];
    if helper {
        return crate::mcp::pass_header::parse(arguments.into_iter()).map_err(anyhow::Error::msg);
    }
    let mut arguments = arguments;
    arguments.extend(["--config".to_owned(), "/unused.json".to_owned()]);
    let options = Options::parse(arguments.into_iter())?;
    match options.pass {
        Some(PassSource::Lys(agent)) => Ok(agent),
        _ => anyhow::bail!("launch proof did not select the Lys pass source"),
    }
}

#[test]
fn assist004_r1_mcp_and_helper_send_only_the_launch_headers() -> anyhow::Result<()> {
    for helper in [false, true] {
        let directory = tempfile::tempdir()?;
        let path = directory.path().join("synthetic.json");
        proof(&path, 1)?;
        let listener = TcpListener::bind("127.0.0.1:0")?;
        let mut source = agent(&path, &format!("http://{}", listener.local_addr()?), helper)?;
        let server = std::thread::spawn(move || receive(&listener, 1));
        let answer = source
            .ask_now()
            .map_err(|error| anyhow::anyhow!("{}", error.code));
        let received = server
            .join()
            .map_err(|_| anyhow::anyhow!("issuer panicked"))??;
        answer?;
        assert!(
            received,
            "issuer did not receive the exact synthetic headers and audience"
        );
    }
    Ok(())
}

fn receive(listener: &TcpListener, generation: usize) -> anyhow::Result<bool> {
    let (mut stream, _) = listener.accept()?;
    let mut reader = BufReader::new(stream.try_clone()?);
    let mut headers = Vec::new();
    let mut line = String::new();
    reader.read_line(&mut line)?;
    let route = line == "POST /api/agents/fixture/pass HTTP/1.1\r\n";
    loop {
        line.clear();
        anyhow::ensure!(
            reader.read_line(&mut line)? != 0,
            "request ended before headers"
        );
        if line == "\r\n" {
            break;
        }
        headers.push(line.trim().to_owned());
    }
    let length: usize = headers
        .iter()
        .find_map(|line| line.strip_prefix("content-length: "))
        .ok_or_else(|| anyhow::anyhow!("request length absent"))?
        .parse()?;
    let mut body = vec![0; length];
    reader.read_exact(&mut body)?;
    let valid = route
        && headers.contains(&format!("lys-agent-pass: synthetic-run-{generation}"))
        && headers.contains(&format!("lys-seat: synthetic seat {generation}"))
        && !headers.iter().any(|line| {
            line.starts_with("lys-agent-signature:") || line.starts_with("lys-grant-token:")
        })
        && serde_json::from_slice::<serde_json::Value>(&body)? == json!({"audience":"haematite"});
    let answer = br#"{"pass":"synthetic-answer","expires_at":4102444800,"audience":"haematite"}"#;
    write!(
        stream,
        "HTTP/1.1 200 OK\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
        answer.len()
    )?;
    stream.write_all(answer)?;
    stream.flush()?;
    Ok(valid)
}

fn refused(change: impl FnOnce(&Path) -> anyhow::Result<()>, name: &str) -> anyhow::Result<()> {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("synthetic.json");
    proof(&path, 1)?;
    change(&path)?;
    let mut source = agent(&path, "http://127.0.0.1:1", false)?;
    let error = source
        .ask_now()
        .err()
        .ok_or_else(|| anyhow::anyhow!("invalid proof accepted"))?;
    assert!(
        error.message.contains(name),
        "expected named {name} refusal"
    );
    assert!(
        !error.message.contains("synthetic"),
        "proof contents entered a refusal"
    );
    Ok(())
}

#[test]
fn assist004_r1_shared_proof_is_refused_without_contents() -> anyhow::Result<()> {
    refused(
        |path| {
            Ok(std::fs::set_permissions(
                path,
                std::fs::Permissions::from_mode(0o644),
            )?)
        },
        "proof_file_not_private",
    )
}

#[test]
fn assist004_r1_symlink_proof_is_refused_without_contents() -> anyhow::Result<()> {
    refused(
        |path| {
            let target = path.with_extension("target");
            std::fs::rename(path, &target)?;
            Ok(symlink(target, path)?)
        },
        "proof_file_unreadable",
    )
}

#[test]
fn assist004_r1_oversize_proof_is_refused_without_contents() -> anyhow::Result<()> {
    refused(
        |path| Ok(std::fs::write(path, "synthetic".repeat(9000))?),
        "proof_file_exceeds_bound",
    )
}

#[test]
fn assist004_r1_missing_pass_is_refused_without_contents() -> anyhow::Result<()> {
    refused(
        |path| {
            Ok(std::fs::write(
                path,
                br#"{"headers":{"lys-seat":"synthetic"}}"#,
            )?)
        },
        "proof_headers_invalid",
    )
}

#[test]
fn assist004_r1_replacement_is_read_at_each_ask() -> anyhow::Result<()> {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("synthetic.json");
    proof(&path, 1)?;
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let mut source = agent(&path, &format!("http://{}", listener.local_addr()?), false)?;
    let server = std::thread::spawn(move || {
        Ok::<_, anyhow::Error>(receive(&listener, 1)? && receive(&listener, 2)?)
    });
    source
        .ask_now()
        .map_err(|error| anyhow::anyhow!("{}", error.code))?;
    let replacement = path.with_extension("replacement");
    proof(&replacement, 2)?;
    std::fs::rename(replacement, &path)?;
    source
        .ask_now()
        .map_err(|error| anyhow::anyhow!("{}", error.code))?;
    assert!(
        server
            .join()
            .map_err(|_| anyhow::anyhow!("issuer panicked"))??,
        "a pass ask used headers from the previous proof"
    );
    Ok(())
}