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::{config::Seat, handshake::Version, tools::Identity};
use agent_client_protocol_schema::{v1, v2};
use lys_pass::{
    Decision as Right, KeySet, Target, VerifiedPass,
    drafts::{DraftCreated, DraftRequest},
};
use serde_json::{Value, json};
use std::{
    collections::BTreeSet,
    io::Read as _,
    sync::Arc,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

const DRAFT_ID_BYTES: usize = 128;

pub(super) struct Request {
    pub words: Value,
    options: Vec<(String, String)>,
}
impl Request {
    pub fn decode(
        version: Version,
        params: Value,
        session: &str,
        bound: usize,
    ) -> anyhow::Result<Self> {
        let (id, title, tool, options) = match version {
            Version::One => {
                let request: v1::RequestPermissionRequest = serde_json::from_value(params)?;
                (
                    request.session_id.to_string(),
                    request
                        .tool_call
                        .fields
                        .title
                        .clone()
                        .ok_or_else(|| anyhow::anyhow!("permission_title_missing"))?,
                    serde_json::to_value(request.tool_call)?,
                    serde_json::to_value(request.options)?,
                )
            }
            Version::Two => {
                let request: v2::RequestPermissionRequest = serde_json::from_value(params)?;
                let tool = match request.subject {
                    Some(v2::RequestPermissionSubject::ToolCall(subject)) => {
                        serde_json::to_value(subject.tool_call)?
                    }
                    Some(subject) => {
                        let input = serde_json::to_value(subject)?;
                        json!({"kind":if input["type"]=="command" {"execute"}else{"other"},"rawInput":input})
                    }
                    None => Value::Null,
                };
                (
                    request.session_id.to_string(),
                    request.title,
                    tool,
                    serde_json::to_value(request.options)?,
                )
            }
        };
        anyhow::ensure!(
            id == session && !title.trim().is_empty(),
            "permission_session_or_title_invalid"
        );
        let offered = options
            .as_array()
            .filter(|offered| !offered.is_empty())
            .ok_or_else(|| anyhow::anyhow!("permission_options_empty"))?;
        let mut ids = BTreeSet::new();
        let mut choices = Vec::new();
        for option in offered {
            let id = option["optionId"]
                .as_str()
                .filter(|id| !id.is_empty())
                .ok_or_else(|| anyhow::anyhow!("permission_option_id_invalid"))?;
            let kind = option["kind"]
                .as_str()
                .ok_or_else(|| anyhow::anyhow!("permission_option_kind_invalid"))?;
            anyhow::ensure!(
                ids.insert(id) && option["name"].as_str().is_some_and(|name| !name.is_empty()),
                "permission_option_duplicate_or_unnamed"
            );
            choices.push((id.to_owned(), kind.to_owned()));
        }
        let mut words =
            json!({"title":title,"options":options,"tool_kind":tool["kind"],"truncated":false});
        if let Some(input) = tool.get("rawInput") {
            words["input"] = input.clone();
        }
        fit_words(&mut words, bound)?;
        Ok(Self {
            words,
            options: choices,
        })
    }
}

fn fit_words(words: &mut Value, bound: usize) -> anyhow::Result<()> {
    if serde_json::to_vec(words)?.len() <= bound {
        return Ok(());
    }
    words["truncated"] = json!(true);
    if let Some(object) = words.as_object_mut() {
        object.remove("input");
    }
    if serde_json::to_vec(words)?.len() <= bound {
        return Ok(());
    }
    let title = words["title"]
        .as_str()
        .ok_or_else(|| anyhow::anyhow!("permission_title_missing"))?
        .to_owned();
    words["title"] = json!("");
    anyhow::ensure!(
        serde_json::to_vec(words)?.len() <= bound,
        "permission_words_exceed_content_bytes"
    );
    let mut low = 0;
    let mut high = title.chars().count();
    while low < high {
        let middle = low + (high - low).div_ceil(2);
        words["title"] = json!(title.chars().take(middle).collect::<String>());
        if serde_json::to_vec(words)?.len() <= bound {
            low = middle;
        } else {
            high = middle - 1;
        }
    }
    words["title"] = json!(title.chars().take(low).collect::<String>());
    Ok(())
}

#[derive(Clone, Copy)]
pub(super) enum Decision {
    Approve,
    Refuse,
    Cancelled,
}
pub(super) fn decide(request: &Request, decision: Decision) -> (Value, &'static str) {
    let (kind, decided) = match decision {
        Decision::Approve => (Some("allow_once"), "approve"),
        Decision::Refuse => (Some("reject_once"), "refuse"),
        Decision::Cancelled => (None, "cancelled"),
    };
    if let Some(kind) = kind
        && let Some((id, _)) = request.options.iter().find(|(_, offered)| offered == kind)
    {
        return (
            json!({"outcome":{"outcome":"selected","optionId":id}}),
            decided,
        );
    }
    (json!({"outcome":{"outcome":"cancelled"}}), "cancelled")
}

pub(super) struct Held {
    pub draft: String,
    pub asked_at_ms: u64,
}

pub(super) struct Pending {
    pub id: Value,
    pub request: Request,
    pub conversation: String,
    pub draft: Option<String>,
    pub cancelled: bool,
    pub holding: bool,
    pub answer_queued: bool,
}

#[derive(Clone)]
pub(super) struct Issuer {
    pub(super) origin: String,
    pub(super) agent: String,
    launch: Arc<String>,
    pub(super) store: String,
    bound: usize,
    pub(super) http: reqwest::Client,
    pub(super) changes: reqwest::Client,
    watch_pass: Arc<tokio::sync::Mutex<watch_pass::Cached>>,
    slots: Arc<tokio::sync::Semaphore>,
}
impl Issuer {
    pub fn new(seat: &Seat, identity: &Identity) -> anyhow::Result<Self> {
        let http = reqwest::Client::builder()
            .no_proxy()
            .redirect(reqwest::redirect::Policy::none())
            .connect_timeout(Duration::from_secs(5))
            .timeout(Duration::from_secs(15))
            .build()?;
        let origin = super::tools::origin(&seat.issuer)?.to_owned();
        Ok(Self {
            origin,
            agent: identity.agent.clone(),
            launch: Arc::new(identity.launch.clone()),
            store: seat.store.clone(),
            bound: seat.harness.issuer_response_bytes,
            changes: reqwest::Client::builder()
                .no_proxy()
                .redirect(reqwest::redirect::Policy::none())
                .connect_timeout(Duration::from_secs(5))
                .build()?,
            watch_pass: Arc::new(tokio::sync::Mutex::new(watch_pass::Cached::new(
                identity.pass.clone(),
            )?)),
            http,
            slots: Arc::new(tokio::sync::Semaphore::new(seat.harness.stream_queue)),
        })
    }
    pub fn words_budget(&self, conversation: &str, bound: usize) -> anyhow::Result<usize> {
        // Reserve the complete approval envelope before the immutable draft words are held.
        let draft = "x".repeat(DRAFT_ID_BYTES);
        let envelope = json!({"kind":"permission_ask","conversation":conversation,"seq":u64::MAX,"durable":false,"draft":draft,"url":self.url(&draft)?,"words":{},"asked_at_ms":u64::MAX,"truncated":false});
        bound
            .checked_sub(serde_json::to_vec(&envelope)?.len().saturating_sub(2))
            .filter(|bound| *bound > 0)
            .ok_or_else(|| anyhow::anyhow!("permission_envelope_exceeds_content_bytes"))
    }
    pub fn url(&self, draft: &str) -> anyhow::Result<String> {
        let mut url = url::Url::parse(&self.origin)?;
        let encoded: String = url::form_urlencoded::byte_serialize(draft.as_bytes()).collect();
        url.set_fragment(Some(&format!("/access/drafts?draft={encoded}")));
        Ok(url.into())
    }
    pub async fn hold(&self, operation: String, words: Value) -> anyhow::Result<Held> {
        let permit = self.slots.acquire().await?;
        let response = self
            .http
            .post(format!("{}/api/agents/{}/pass", self.origin, self.agent))
            .headers(super::tools::proof_headers(&self.launch)?)
            .json(&json!({"audience":"haematite"}))
            .send()
            .await
            .map_err(|_| anyhow::anyhow!("permission_issuer_unreachable"))?;
        let answer = self.json(response).await?;
        anyhow::ensure!(
            answer["audience"] == "haematite",
            "permission_pass_audience_differs"
        );
        let token = answer["pass"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("permission_pass_missing"))?;
        let pass = self.verified(token).await?;
        let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
        let target = Target::new("haematite.database", &self.store, "agent_permission")?;
        let Right::Held { grant, .. } =
            pass.evaluate(&target.kind, &target.id, &target.action, now)?
        else {
            anyhow::bail!("permission_not_held");
        };
        let words = serde_json::to_string(&words)?;
        let request = DraftRequest {
            operation,
            grant: grant.to_owned(),
            target,
            request_digest: lys_pass::drafts::request_digest(&words),
            words,
        };
        let response = self
            .http
            .post(format!("{}/api/product-drafts", self.origin))
            .bearer_auth(token)
            .json(&request)
            .send()
            .await
            .map_err(|_| anyhow::anyhow!("permission_issuer_unreachable"))?;
        let answer = self.json(response).await?;
        let asked_at_ms = u64::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis())?;
        let created: DraftCreated = serde_json::from_value(answer)
            .map_err(|_| anyhow::anyhow!("permission_draft_response_invalid"))?;
        anyhow::ensure!(
            !created.draft.is_empty()
                && created.draft.len() <= DRAFT_ID_BYTES
                && created
                    .draft
                    .bytes()
                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')),
            "permission_draft_id_invalid"
        );
        drop(permit);
        Ok(Held {
            draft: created.draft,
            asked_at_ms,
        })
    }
    async fn verified(&self, token: &str) -> anyhow::Result<VerifiedPass> {
        let response = self
            .http
            .get(format!("{}/oauth/jwks", self.origin))
            .send()
            .await
            .map_err(|_| anyhow::anyhow!("permission_issuer_unreachable"))?;
        let keys = KeySet::from_json(&serde_json::to_string(&self.json(response).await?)?)?;
        let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
        let pass = VerifiedPass::verify(token, &keys, &self.origin, "haematite", now)?;
        anyhow::ensure!(
            pass.claims().holder.id == self.agent
                && pass.claims().holder.kind == "agent"
                && pass.claims().holder.responsible.is_some(),
            "permission_pass_holder_differs"
        );
        Ok(pass)
    }
    pub async fn responsible(&self, token: &str) -> anyhow::Result<String> {
        let pass = self.verified(token).await?;
        pass.claims()
            .holder
            .responsible
            .clone()
            .filter(|person| !person.is_empty() && person.len() <= 128 && person != &self.agent)
            .ok_or_else(|| anyhow::anyhow!("permission_pass_responsible_invalid"))
    }
    pub(super) async fn json(&self, mut response: reqwest::Response) -> anyhow::Result<Value> {
        anyhow::ensure!(
            response.status().is_success(),
            "permission_issuer_refused: HTTP {}",
            response.status().as_u16()
        );
        let mut bytes = Vec::new();
        while let Some(chunk) = response
            .chunk()
            .await
            .map_err(|_| anyhow::anyhow!("permission_issuer_response_unreadable"))?
        {
            anyhow::ensure!(
                chunk.len() <= self.bound.saturating_sub(bytes.len()),
                "issuer_response_bytes_exceeded"
            );
            bytes.extend_from_slice(&chunk);
        }
        serde_json::from_slice(&bytes)
            .map_err(|_| anyhow::anyhow!("permission_issuer_response_invalid"))
    }
}

pub(super) fn operation() -> anyhow::Result<String> {
    use std::fmt::Write as _;
    let mut bytes = [0_u8; 16];
    std::fs::File::open("/dev/urandom")?.read_exact(&mut bytes)?;
    let mut operation = String::from("assistant-");
    for byte in bytes {
        write!(operation, "{byte:02x}")?;
    }
    Ok(operation)
}

#[path = "watch_pass.rs"]
mod watch_pass;