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
use serde::Deserialize;
use std::path::{Path, PathBuf};

/// A launch declaration and its positive resource bounds. No field grants permission.
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Harness {
    /// One catalogue id; cdx uses channel wake instead of this host.
    pub id: String,
    /// The executable, resolved at launch without searching the current directory.
    pub program: PathBuf,
    /// Arguments passed without a shell. They never enter diagnostics.
    pub args: Vec<String>,
    /// A private existing harness directory prevents native account configuration inheritance.
    #[serde(default)]
    pub home: PathBuf,
    /// Additional inherited names; values are supplied by the launch.
    #[serde(default)]
    pub environment: Vec<String>,
    /// One unread frame keeps the read side draining without retaining a burst.
    #[serde(default = "reads")]
    pub read_queue: usize,
    /// Initialization allows a cold adapter start while bounding a silent peer.
    #[serde(default = "deadline_ms")]
    pub initialize_timeout_ms: u64,
    /// Session setup is bounded separately from a permission held for a person.
    #[serde(default = "deadline_ms")]
    pub session_timeout_ms: u64,
    /// Session root; absent inherits the directory selected by the Lys launch profile.
    pub cwd: Option<PathBuf>,
    /// A prior session explicitly requested for resume, never silently replaced.
    pub session: Option<String>,
    /// Unread prompts; 32 is one seat's unread mail, not a durable mailbox.
    #[serde(default = "prompt_queue")]
    pub prompt_queue: usize,
    /// Text bytes per stream item and prompt; 64 KiB covers one model turn, not a file transfer.
    #[serde(default = "content_bytes")]
    pub content_bytes: usize,
    /// ACP frame bytes; 256 KiB admits tool metadata without permitting an unbounded pipe allocation.
    #[serde(default = "frame_bytes")]
    pub acp_frame_bytes: usize,
    /// Held permissions; 1024 admits the 729-request pipe contract while bounding approval memory.
    #[serde(default = "permissions")]
    pub pending_permissions: usize,
    /// Pipe responses awaiting write; 2048 covers a permission burst without blocking stdout reads.
    #[serde(default = "writes")]
    pub write_queue: usize,
    /// Stream publications awaiting the network; 128 limits memory when a watcher is slow.
    #[serde(default = "streams")]
    pub stream_queue: usize,
    /// Issuer response bytes; 64 KiB admits structured refusals without retaining arbitrary bodies.
    #[serde(default = "content_bytes")]
    pub issuer_response_bytes: usize,
    /// Fifty conversations bound one history response.
    #[serde(default = "sessions")]
    pub session_list: usize,
    /// Titles need enough words to distinguish conversations in a picker.
    #[serde(default = "titles")]
    pub title_chars: usize,
}

const fn sessions() -> usize {
    50
}
const fn titles() -> usize {
    120
}

const fn prompt_queue() -> usize {
    32
}
const fn content_bytes() -> usize {
    65_536
}
const fn frame_bytes() -> usize {
    262_144
}
const fn permissions() -> usize {
    1024
}
const fn writes() -> usize {
    2048
}
const fn streams() -> usize {
    128
}

impl Harness {
    /// Checks catalogue membership, paths, and all positive limits before spawning.
    ///
    /// # Errors
    /// Returns a refusal naming the invalid field, without its value.
    pub fn validate(&self) -> anyhow::Result<()> {
        super::catalogue::validate(&self.id)?;
        super::environment::home(&self.home)?;
        super::environment::validate(&self.environment)?;
        anyhow::ensure!(
            self.initialize_timeout_ms > 0,
            "harness.initialize_timeout_ms must be positive"
        );
        anyhow::ensure!(
            self.session_timeout_ms > 0,
            "harness.session_timeout_ms must be positive"
        );
        anyhow::ensure!(
            !self.program.as_os_str().is_empty(),
            "harness.program is empty"
        );
        anyhow::ensure!(
            self.cwd.as_ref().is_none_or(|cwd| cwd.is_absolute()),
            "harness.cwd must be absolute"
        );
        anyhow::ensure!(
            self.session
                .as_ref()
                .is_none_or(|id| !id.trim().is_empty() && id.len() <= self.content_bytes),
            "harness.session is empty or exceeds content_bytes"
        );
        for (name, bound) in [
            ("session_list", self.session_list),
            ("title_chars", self.title_chars),
            ("prompt_queue", self.prompt_queue),
            ("content_bytes", self.content_bytes),
            ("acp_frame_bytes", self.acp_frame_bytes),
            ("pending_permissions", self.pending_permissions),
            ("read_queue", self.read_queue),
            ("write_queue", self.write_queue),
            ("stream_queue", self.stream_queue),
            ("issuer_response_bytes", self.issuer_response_bytes),
        ] {
            anyhow::ensure!(bound > 0, "harness.{name} must be positive");
        }
        for (name, bound) in [
            ("read_queue", self.read_queue),
            ("write_queue", self.write_queue),
            ("stream_queue", self.stream_queue),
        ] {
            anyhow::ensure!(
                bound <= tokio::sync::Semaphore::MAX_PERMITS,
                "harness.{name} exceeds channel capacity"
            );
        }
        Ok(())
    }
}

/// Where a seat reaches its door and its store (HAEMNET-003 R2).
#[derive(Clone)]
pub(super) enum Place {
    /// On the door's machine: the door's assistant socket and the store's
    /// serve config, each checked by its owner and mode.
    Local {
        door_socket: PathBuf,
        serve_config: PathBuf,
    },
    /// On any machine: the door's own https origin and the store's
    /// HAEMNET-001 endpoint. The seat's Lys agent pass is the whole
    /// admission at both.
    Network {
        door_url: url::Url,
        store: StoreEndpoint,
    },
}

/// The store's network endpoint, as the seat file names it.
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct StoreEndpoint {
    /// `<host>:<port>`, as `client::network::Remote` takes it.
    pub endpoint: String,
    /// The name the store's certificate is verified against.
    #[serde(default)]
    pub server_name: String,
    /// The page the record client asks for; the store's own, from its
    /// hello, when absent.
    pub max_page: Option<std::num::NonZeroU32>,
    /// Each call's bound; mcp's default of 30 seconds when absent.
    pub timeout_ms: Option<std::num::NonZeroU64>,
}

/// The seat file as written: exactly one of the two forms.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SeatFile {
    harness: Harness,
    door_socket: Option<PathBuf>,
    serve_config: Option<PathBuf>,
    door_url: Option<String>,
    store_endpoint: Option<StoreEndpoint>,
    issuer: String,
    store: String,
}

/// A seat decoded from JSON has no path until [`Seat::load`] gives it the
/// file's own.
#[derive(Clone, Deserialize)]
#[serde(try_from = "SeatFile")]
pub(super) struct Seat {
    pub path: PathBuf,
    pub harness: Harness,
    pub place: Place,
    pub issuer: String,
    pub store: String,
}

impl TryFrom<SeatFile> for Seat {
    type Error = String;
    fn try_from(file: SeatFile) -> Result<Self, String> {
        let place = place(
            file.door_socket,
            file.serve_config,
            file.door_url,
            file.store_endpoint,
        )
        .map_err(|error| error.to_string())?;
        Ok(Self {
            path: PathBuf::new(),
            harness: file.harness,
            place,
            issuer: file.issuer,
            store: file.store,
        })
    }
}

impl Seat {
    pub fn load(path: &Path) -> anyhow::Result<Self> {
        let bytes = std::fs::read(path)
            .map_err(|error| anyhow::anyhow!("seat_unreadable: {}", error.kind()))?;
        let fields: serde_json::Value = serde_json::from_slice(&bytes)
            .map_err(|_| anyhow::anyhow!("seat_invalid: expected a JSON seat object"))?;
        anyhow::ensure!(
            fields.get("key_file").is_none(),
            "seat_invalid: key_file is forbidden; the run supplies its launch proof"
        );
        let mut decoder = serde_json::Deserializer::from_slice(&bytes);
        let file: SeatFile = serde_path_to_error::deserialize(&mut decoder).map_err(|error| {
            let field: String = error
                .path()
                .to_string()
                .chars()
                .take(160)
                .filter(|character| {
                    character.is_ascii_alphanumeric() || ".[]_".contains(*character)
                })
                .collect();
            anyhow::anyhow!(
                "seat_invalid: field {field}, line {}, column {}, {:?}",
                error.inner().line(),
                error.inner().column(),
                error.inner().classify()
            )
        })?;
        decoder.end().map_err(|error| {
            anyhow::anyhow!(
                "seat_invalid: trailing input, line {}, column {}",
                error.line(),
                error.column()
            )
        })?;
        let mut seat = Self::try_from(file).map_err(anyhow::Error::msg)?;
        seat.path = std::fs::canonicalize(path)?;
        seat.harness.validate()?;
        super::environment::check_provider(&seat.harness.environment)?;
        anyhow::ensure!(
            !seat.store.is_empty() && !seat.issuer.is_empty(),
            "seat_store_and_issuer_required"
        );
        Ok(seat)
    }
    pub fn cwd(&self) -> anyhow::Result<PathBuf> {
        self.harness
            .cwd
            .clone()
            .map_or_else(|| Ok(std::env::current_dir()?), Ok)
    }
}

const fn reads() -> usize {
    1
}
const fn deadline_ms() -> u64 {
    30_000
}

/// The one form the seat file names; a file mixing the two, or naming one
/// only in part, is refused by name before anything starts.
fn place(
    door_socket: Option<PathBuf>,
    serve_config: Option<PathBuf>,
    door_url: Option<String>,
    store_endpoint: Option<StoreEndpoint>,
) -> anyhow::Result<Place> {
    let local = door_socket.is_some() || serve_config.is_some();
    let network = door_url.is_some() || store_endpoint.is_some();
    anyhow::ensure!(
        !(local && network),
        "seat_place_mixed: give door_socket and serve_config, or door_url and store_endpoint, never parts of both"
    );
    match (door_socket, serve_config, door_url, store_endpoint) {
        (Some(door_socket), Some(serve_config), None, None) => {
            anyhow::ensure!(
                door_socket.is_absolute() && serve_config.is_absolute(),
                "seat_paths_must_be_absolute"
            );
            Ok(Place::Local {
                door_socket,
                serve_config,
            })
        }
        (None, None, Some(door_url), Some(store)) => {
            let door_url = door_origin(&door_url)?;
            anyhow::ensure!(
                crate::client::network::endpoint_parses(&store.endpoint),
                "seat_store_endpoint_invalid: expected <host>:<port>"
            );
            anyhow::ensure!(
                !store.server_name.is_empty(),
                "seat_store_endpoint_requires_server_name"
            );
            crate::client::network::name(&store.server_name)
                .map_err(|_| anyhow::anyhow!("seat_store_server_name_invalid"))?;
            Ok(Place::Network { door_url, store })
        }
        _ => anyhow::bail!(
            "seat_place_incomplete: give door_socket and serve_config, or door_url and store_endpoint"
        ),
    }
}

/// The door's own https origin: no path but `/`, and no query, fragment or
/// credentials, so the seat names the door and nothing on it.
fn door_origin(text: &str) -> anyhow::Result<url::Url> {
    let url = url::Url::parse(text).map_err(|_| anyhow::anyhow!("seat_door_url_invalid"))?;
    anyhow::ensure!(url.scheme() == "https", "seat_door_url_requires_https");
    anyhow::ensure!(
        url.host_str().is_some_and(|host| !host.is_empty())
            && url.username().is_empty()
            && url.password().is_none()
            && url.path() == "/"
            && url.query().is_none()
            && url.fragment().is_none(),
        "seat_door_url_invalid: expected the door's origin, https://<host>[:<port>]"
    );
    Ok(url)
}