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 std::fs;
use std::os::unix::fs::{PermissionsExt, symlink};

use serde_json::Value;

use super::agent::{Agent, NO_PASS};
use super::bundle::install_current;
use super::tests::{Fixture, TestResult};

fn agent(
    issuer: &str,
    id: &str,
    key: &std::path::Path,
) -> Result<Agent, Box<dyn std::error::Error>> {
    Agent::parse(Some(issuer.into()), Some(id.into()), Some(key.into()))?
        .ok_or_else(|| "agent proof missing".into())
}

fn read(fixture: &Fixture, name: &str) -> Result<Value, Box<dyn std::error::Error>> {
    Ok(serde_json::from_slice(&fs::read(
        fixture.bundle.join(name),
    )?)?)
}

#[test]
fn install_binds_both_plugin_environments_without_copying_the_key() -> TestResult {
    let fixture = Fixture::new()?;
    let key = fixture.directory.path().join("agent-key");
    fs::write(&key, "fixture-private-key-not-a-bundle-payload")?;
    let key = fs::canonicalize(key)?;
    let alias = fixture.directory.path().join("key-alias");
    symlink(&key, &alias)?;
    let proof = agent("https://lys.example", "agent-1", &alias)?;
    let report = install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
        Ok(fixture.binary.clone())
    })?;
    assert_eq!(report.files, 10);
    let claude = read(&fixture, ".claude-plugin/plugin.json")?;
    let codex = read(&fixture, ".mcp.json")?;
    let claude = &claude["mcpServers"]["haematite"];
    let codex = &codex["mcpServers"]["haematite"];
    assert_eq!(claude["command"], "${CLAUDE_PLUGIN_ROOT}/scripts/mcp.sh");
    assert_eq!(
        codex["command"],
        fixture
            .bundle
            .join("scripts/mcp.sh")
            .to_str()
            .ok_or("path")?
    );
    assert_eq!(claude["env"], codex["env"]);
    assert_eq!(codex["env"]["HAEMATITE_LYS_ISSUER"], "https://lys.example");
    assert_eq!(codex["env"]["HAEMATITE_LYS_AGENT"], "agent-1");
    assert_eq!(
        codex["env"]["HAEMATITE_KEY_FILE"],
        key.to_str().ok_or("path")?
    );
    for name in ["HAEMATITE_BIN", "HAEMATITE_KEY_FILE"] {
        let path = std::path::Path::new(codex["env"][name].as_str().ok_or("env path")?);
        assert!(path.is_absolute());
        assert!(path.is_file());
    }
    assert!(
        read(&fixture, ".claude-plugin/plugin.json")?
            .get("userConfig")
            .is_none()
    );
    assert_eq!(
        fs::metadata(fixture.bundle.join("scripts/mcp.sh"))?
            .permissions()
            .mode()
            & 0o777,
        0o700
    );
    let manifest = read(&fixture, "bundle.json")?;
    for name in manifest["files"].as_object().ok_or("inventory")?.keys() {
        let bytes = fs::read(fixture.bundle.join(name))?;
        assert!(
            !bytes
                .windows(b"fixture-private-key-not-a-bundle-payload".len())
                .any(|window| window == b"fixture-private-key-not-a-bundle-payload")
        );
    }
    let repeat = install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
        Ok(fixture.binary.clone())
    })?;
    assert!(repeat.already_installed);
    let changed = agent("https://lys.example", "agent-2", &key)?;
    let error = install_current(&fixture.config, &fixture.bundle, Some(&changed), || {
        Ok(fixture.binary.clone())
    })
    .err()
    .ok_or("changed identity overwrote bundle")?;
    assert_eq!(error.code, "bundle_conflict");
    Ok(())
}

#[test]
fn install_launcher_passes_the_agent_proof_to_the_operator() -> TestResult {
    let fixture = Fixture::new()?;
    fs::write(&fixture.binary, "#!/bin/sh\nprintf '%s\\n' \"$@\"\n")?;
    let key = fixture.directory.path().join("key");
    fs::write(&key, "fixture-key")?;
    let key = fs::canonicalize(key)?;
    let proof = agent("https://lys.example", "agent-1", &key)?;
    install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
        Ok(fixture.binary.clone())
    })?;
    let mcp = read(&fixture, ".mcp.json")?;
    let server = &mcp["mcpServers"]["haematite"];
    let mut command = std::process::Command::new(server["command"].as_str().ok_or("command")?);
    command.env_clear();
    for (name, value) in server["env"].as_object().ok_or("env")? {
        command.env(name, value.as_str().ok_or("value")?);
    }
    let output = command.output()?;
    assert!(output.status.success());
    let args = String::from_utf8(output.stdout)?;
    assert_eq!(
        args.lines().collect::<Vec<_>>(),
        vec![
            "mcp",
            "--remote",
            "https://haematite.ablative.com.au/mcp",
            "--lys-agent",
            "https://lys.example",
            "agent-1",
            "--key-file",
            key.to_str().ok_or("path")?
        ]
    );
    assert!(output.stderr.is_empty());
    Ok(())
}

#[test]
fn install_without_an_agent_documents_the_named_refusal() -> TestResult {
    let fixture = Fixture::new()?;
    super::install(&fixture.binary, &fixture.config, &fixture.bundle)?;
    let page = fs::read_to_string(fixture.bundle.join("skills/haematite/SKILL.md"))?;
    assert!(page.contains(NO_PASS));
    for name in [".mcp.json", ".claude-plugin/plugin.json"] {
        assert!(
            read(&fixture, name)?["mcpServers"]["haematite"]["env"]
                .get("HAEMATITE_CONFIG")
                .is_none()
        );
    }
    Ok(())
}

#[test]
fn install_validates_identity_and_key_paths_before_publication() -> TestResult {
    let fixture = Fixture::new()?;
    let key = fixture.directory.path().join("key");
    fs::write(&key, "fixture-key")?;
    let key = fs::canonicalize(key)?;
    for (issuer, id) in [
        ("https://lys.example/path", "agent-1"),
        ("http://lys.example", "agent-1"),
        ("https://user@lys.example", "agent-1"),
        ("https://lys.example#fragment", "agent-1"),
        ("https://lys.example", "agent/1"),
        ("https://lys.example", ""),
    ] {
        let error = agent(issuer, id, &key)
            .err()
            .ok_or("invalid identity accepted")?;
        assert!(error.to_string().starts_with("bundle_lys_agent_invalid"));
    }
    let missing = agent(
        "https://lys.example",
        "agent-1",
        &fixture.directory.path().join("missing"),
    )
    .err()
    .ok_or("missing key accepted")?;
    assert!(missing.to_string().starts_with("bundle_lys_key_invalid"));
    assert!(!fixture.bundle.exists());
    Ok(())
}

#[test]
fn enrolment_defaults_to_central_services_without_a_local_config() -> TestResult {
    let fixture = Fixture::new()?;
    let key = fixture.directory.path().join("key");
    fs::write(&key, "fixture-key")?;
    let proof =
        Agent::parse(None, Some("agent-1".into()), Some(key))?.ok_or("missing enrolment")?;
    install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
        Ok(fixture.binary.clone())
    })?;
    for name in [".mcp.json", ".claude-plugin/plugin.json"] {
        let config = read(&fixture, name)?;
        let env = &config["mcpServers"]["haematite"]["env"];
        assert_eq!(
            env["HAEMATITE_URL"],
            "https://haematite.ablative.com.au/mcp"
        );
        assert_eq!(env["HAEMATITE_LYS_ISSUER"], "https://lys.ablative.com.au");
        assert!(env.get("HAEMATITE_CONFIG").is_none());
    }
    Ok(())
}