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
//! A deliberate act held for approval is written as a Lys draft carrying the
//! exact words the client sent (ACCESS-001 R3, D4). The live check here is a
//! double: Lys main has no draft routes yet (Gaia's ACCESS-001 R3), so these
//! tests pin haematite's side against lys-pass's client contract only.

use std::sync::{Arc, Mutex};

use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use lys_pass::deliberate::CheckAnswer;
use lys_pass::drafts::{DraftRequest, request_digest};
use lys_pass::{Error, Target};
use serde_json::{Value, json};

use super::tests::{ISSUER, KID, NOW, pass, role, signer};
use super::{Gate, LiveCheck};
use crate::serve::protocol::{Operation, Request};

type TestResult = Result<(), Box<dyn std::error::Error>>;

/// The double of Lys: every live check is held by draft, and each draft it is
/// asked to record is kept, answered `draft-1`, or refused when `reachable`
/// is false.
struct HeldLys {
    reachable: bool,
    drafts: Mutex<Vec<DraftRequest>>,
}

impl HeldLys {
    fn new(reachable: bool) -> Arc<Self> {
        Arc::new(Self {
            reachable,
            drafts: Mutex::new(Vec::new()),
        })
    }

    fn recorded(&self) -> Result<Vec<DraftRequest>, Box<dyn std::error::Error>> {
        Ok(self
            .drafts
            .lock()
            .map_err(|_| "the double's draft list was poisoned")?
            .clone())
    }
}

impl LiveCheck for HeldLys {
    fn ask(&self, _token: &str, _holder: &str, _target: &Target) -> Result<CheckAnswer, Error> {
        Ok(serde_json::from_value(json!({
            "allowed": false, "grant": "branch-by-draft", "mode": "by_draft", "path": [],
            "via": null, "refusal": null, "reason": null, "degraded": null,
        }))?)
    }

    fn draft(
        &self,
        _token: &str,
        request: &DraftRequest,
        _judgment: &CheckAnswer,
    ) -> Result<String, Error> {
        if !self.reachable {
            return Err(Error::CannotAsk("the double is unreachable"));
        }
        self.drafts
            .lock()
            .map_err(|_| Error::CannotAsk("the double's draft list was poisoned"))?
            .push(request.clone());
        Ok("draft-1".to_owned())
    }
}

fn gate(live: Arc<HeldLys>) -> Result<Gate, Error> {
    let x = URL_SAFE_NO_PAD.encode(signer().verifying_key().as_bytes());
    let keys = json!({"keys": [{"kty": "OKP", "crv": "Ed25519", "kid": KID, "x": x, "alg": "EdDSA", "use": "sig"}]});
    Gate::new(&keys.to_string(), ISSUER, "wards", live)
}

/// A fork request exactly as a client frames it, with its envelope id.
const FORK: &str = r#"{"id":"7","operation":{"method":"mutation.apply","params":{"request_id":"f","branch":"main","expected":{"generation":"1","sequence":"1"},"change":{"kind":"branch.fork","value":{"name":"trial","from":{"generation":"1","sequence":"1"}}}}}}"#;

/// The operation a frame carries, decoded as the transport decodes it.
fn operation(words: &str) -> Result<Operation, Box<dyn std::error::Error>> {
    Ok(serde_json::from_str::<Request>(words)?.operation)
}

/// S300: the held fork never runs; Lys records it as a draft whose words are
/// the bytes the client sent, digested as they are, under an operation id
/// scoped to this database and branch so a retry finds the same draft.
#[test]
fn a_held_change_is_written_as_a_draft_of_its_exact_words() -> TestResult {
    let lys = HeldLys::new(true);
    let gate = gate(Arc::clone(&lys))?;
    let admin = pass("administrator", &role(&["read"], "read-grant"));
    let held = gate
        .authorize(Some(&admin), &operation(FORK)?, Some(FORK), NOW)
        .err()
        .ok_or("a held fork was allowed")?;
    assert_eq!(held.code, "held_for_approval");
    assert_eq!(
        serde_json::from_str::<Value>(&held.message)?,
        json!({"draft": "draft-1", "grant": "branch-by-draft", "mode": "by_draft"})
    );
    let recorded = lys.recorded()?;
    let [draft] = recorded.as_slice() else {
        return Err(format!("expected one draft, Lys recorded {}", recorded.len()).into());
    };
    assert_eq!(draft.operation, "haematite:wards:main:f");
    assert_eq!(draft.grant, "branch-by-draft");
    assert_eq!(
        (
            draft.target.kind.as_str(),
            draft.target.id.as_str(),
            draft.target.action.as_str()
        ),
        ("haematite.database", "wards", "branch")
    );
    assert_eq!(draft.words, FORK);
    assert_eq!(draft.request_digest, request_digest(FORK));
    Ok(())
}

/// D10: when Lys cannot record the draft the act is refused by name, and
/// nothing runs in its place.
#[test]
fn a_draft_lys_cannot_record_is_refused_lys_could_not_be_asked() -> TestResult {
    let gate = gate(HeldLys::new(false))?;
    let admin = pass("administrator", &role(&["read"], "read-grant"));
    let refused = gate
        .authorize(Some(&admin), &operation(FORK)?, Some(FORK), NOW)
        .err()
        .ok_or("a held fork was allowed")?;
    assert_eq!(
        (refused.code.as_str(), refused.message.as_str()),
        ("lys_could_not_be_asked", "Lys could not be asked")
    );
    Ok(())
}

/// A held operation that is not a change has nothing to apply later, so it is
/// held and never drafted.
#[test]
fn a_held_read_is_never_drafted() -> TestResult {
    let lys = HeldLys::new(true);
    let gate = gate(Arc::clone(&lys))?;
    // drafts.run changes nothing of its own, and is deliberate (serve).
    let words = r#"{"id":"8","operation":{"method":"drafts.run","params":{}}}"#;
    let admin = pass("administrator", &role(&["read"], "read-grant"));
    let held = gate
        .authorize(Some(&admin), &operation(words)?, Some(words), NOW)
        .err()
        .ok_or("a held read was allowed")?;
    assert_eq!(held.code, "held_for_approval");
    assert!(lys.recorded()?.is_empty(), "a read was drafted");
    Ok(())
}