haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Read the authoritative draft before deciding it; a recorded receipt stays successful.
use super::{
    assistant_state::Pair,
    http::Response,
    signin::{Door, Issuer},
};
use crate::serve::storage::Failure;
use serde::Deserialize;
use serde_json::json;
use std::time::{Duration, Instant};

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Decision {
    draft: String,
    decision: String,
}
pub(super) fn decide<I: Issuer>(
    door: &Door<I>,
    pair: &Pair,
    pass: &str,
    bytes: &[u8],
) -> Result<Response, Failure> {
    let given: Decision = serde_json::from_slice(bytes).map_err(|_| invalid())?;
    if !super::assistant_wire::identifier(&given.draft)
        || !matches!(given.decision.as_str(), "approve" | "refuse")
    {
        return Err(invalid());
    }
    let mut url = url::Url::parse(door.issuer().assistant_url()?).map_err(|_| invalid())?;
    let encoded: String = url::form_urlencoded::byte_serialize(given.draft.as_bytes()).collect();
    url.set_fragment(Some(&format!("/access/drafts?draft={encoded}")));
    let url = url.to_string();
    let hub = door.assistant()?;
    let (agent, generation) = match hub.seat_identity(pair) {
        Ok(seat) => seat,
        Err(error) => return Ok(denied(&error, &given.draft, &url)),
    };
    let within = Duration::from_millis(door.issuer().bounds().total_ms.get());
    let started = Instant::now();
    let held = match door.issuer().assistant_draft(pass, &given.draft, within) {
        Ok(held) => held,
        Err(error) => return Ok(denied(&error, &given.draft, &url)),
    };
    if !belongs(&held, &given.draft, &agent, pair) {
        return Ok(denied(
            &Failure::new(
                "draft_other_seat",
                "this draft belongs to another assistant seat or store; open its Lys link to decide it",
            ),
            &given.draft,
            &url,
        ));
    }
    match hub.seat_identity(pair) {
        Ok(current) if current.1 == generation => {}
        Ok(_) => {
            return Ok(denied(
                &Failure::new(
                    "draft_other_seat",
                    "the assistant seat changed while the draft was being checked",
                ),
                &given.draft,
                &url,
            ));
        }
        Err(error) => return Ok(denied(&error, &given.draft, &url)),
    }
    let remaining = within.saturating_sub(started.elapsed());
    if remaining.is_zero() {
        return Ok(denied(
            &Failure::new(
                "lys_draft_deadline",
                "Lys draft lookup reached the configured sign-in request deadline",
            ),
            &given.draft,
            &url,
        ));
    }
    let answer = match door
        .issuer()
        .assistant_decide(pass, &held, &given.decision, remaining)
    {
        Ok(answer) => answer,
        Err(error) => return Ok(denied(&error, &given.draft, &url)),
    };
    let mut receipt = json!({"draft":given.draft,"lys":url,"decision":answer,"seat_told":false});
    let expected = if given.decision == "approve" {
        "approved"
    } else {
        "refused"
    };
    if answer["state"] == expected {
        match hub.operation_for(
            pair,
            Some(&generation),
            json!({"decided":{"draft":given.draft,"decision":given.decision}}),
        ) {
            Ok(id) => {
                receipt["seat_told"] = json!(true);
                receipt["id"] = json!(id);
            }
            Err(error) => {
                receipt["code"] = json!(error.code);
                receipt["message"] = json!(format!(
                    "Lys recorded the decision. Seat notification was not confirmed: {}. The seat also follows Lys draft state through /changes.",
                    error.message
                ));
            }
        }
    } else {
        receipt["code"] = json!(if answer["state"] == "waiting" {
            "draft_waiting"
        } else {
            "draft_state_changed"
        });
        receipt["message"] = json!(
            "Lys recorded the decision receipt. The seat also follows Lys draft state through /changes."
        );
    }
    Ok(Response::json(200, &receipt))
}
fn denied(error: &Failure, draft: &str, url: &str) -> Response {
    let status = match error.code.as_str() {
        "draft_other_seat" | "assistant_not_started" => 409,
        "lys_unavailable"
        | "lys_draft_deadline"
        | "lys_draft_pages_exceeded"
        | "lys_response_too_large"
        | "ProviderUnavailable"
        | "SessionsUnavailable" => 502,
        _ => 403,
    };
    Response::json(
        status,
        &json!({"code":error.code,"message":error.message,"draft":draft,"lys":url}),
    )
}
/// Whether the draft Lys holds is this seat's permission ask on this store.
fn belongs(held: &serde_json::Value, draft: &str, agent: &str, pair: &Pair) -> bool {
    held["id"] == draft
        && held["app"] == "haematite"
        && held["holder"] == agent
        && held["responsible"] == pair.0
        && held["target"]["kind"] == "haematite.database"
        && held["target"]["id"] == pair.1
        && held["target"]["action"] == "agent_permission"
}
fn invalid() -> Failure {
    Failure::new(
        "assistant_decision_invalid",
        "name a draft and approve or refuse",
    )
}