liminal-server 0.24.9

Standalone server for the liminal messaging bus
Documentation
//! Builder grants use Lys's existing delegation and revocation routes.

use super::{BuilderGrants, SiteError};
use crate::services::lys_http::{LysHttp, segment};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::sync::Arc;

/// Operator-selected machine-held grants from which the two rights derive.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GrantSources {
    /// A source allowing delegation of the service's plan action to agents.
    pub plan: String,
    /// A source allowing delegation of the service's serve action to agents.
    pub serve: String,
    /// The credential Lys admits on its grant routes for the account holding both sources (a
    /// registrar or service-account app credential, mode 0600). Lys admits no agent or machine
    /// pass there; absent, the machine pass is offered.
    #[serde(default)]
    pub credential_path: Option<std::path::PathBuf>,
    /// The app credential that places resources (`[auth.lys].app_credential_path`) and the
    /// network a site's service is placed under; filled from `[auth.lys]`, never from this table.
    #[serde(skip)]
    pub placement: Option<(std::path::PathBuf, String)>,
}

/// Remote effects are idempotent by the immutable draft and effect label.
pub trait SiteEffects: Send + Sync {
    /// Returns the active agent's responsible person from its durable record.
    ///
    /// # Errors
    /// Unknown or retired agent; no guessed owner is returned.
    fn agent_owner(&self, agent: &str) -> Result<String, SiteError>;
    /// Delegates exactly two use-only grants on this service.
    ///
    /// # Errors
    /// Lys refusal or uncertain effect; an explicit retry reuses each operation.
    fn bind(
        &self,
        draft: &str,
        site: &str,
        agent: &str,
        at_ms: u64,
    ) -> Result<BuilderGrants, SiteError>;
    /// Revokes the recorded grant, never an inferred relation or ancestor.
    ///
    /// # Errors
    /// Lys refusal or transport failure.
    fn revoke(&self, draft: &str, label: &str, grant: &str) -> Result<(), SiteError>;
    /// Places `kind:id` under the exchange's network in Lys, so the network's grants reach it.
    ///
    /// # Errors
    /// Lys refusal or transport failure.
    fn place(&self, _kind: &str, _id: &str) -> Result<(), SiteError> {
        Ok(())
    }
}

/// Production grants use the renewing machine pass, never an agent's token.
pub struct LysSiteEffects {
    http: Arc<dyn LysHttp>,
    pass: Option<crate::link_pass::MachinePass>,
    agents: Arc<crate::agents::runtime::Agents>,
    sources: GrantSources,
    /// Bounds returned opaque grant ids before they enter the durable snapshot.
    metadata_bytes: std::num::NonZeroUsize,
}

impl LysSiteEffects {
    /// Binds configured sources to the existing machine identity and agent registry.
    #[must_use]
    pub fn new(
        http: Arc<dyn LysHttp>,
        pass: Option<crate::link_pass::MachinePass>,
        agents: Arc<crate::agents::runtime::Agents>,
        sources: GrantSources,
        metadata_bytes: std::num::NonZeroUsize,
    ) -> Self {
        Self {
            http,
            pass,
            agents,
            sources,
            metadata_bytes,
        }
    }

    fn delegate(&self, request: Delegation<'_>) -> Result<String, SiteError> {
        let Delegation {
            draft,
            site,
            agent,
            responsible,
            action,
            source,
            at_ms,
        } = request;
        let pass = self.bearer()?;
        let source_grant = self
            .http
            .get(&pass, &format!("grants/{}", segment(source)))
            .map_err(|_| SiteError::Invalid("Lys source grant read failed"))?;
        if !source_grant.succeeded() {
            return Err(SiteError::Pass(source_grant.refusal()));
        }
        if source_grant.body["standing"]["stands"] != true {
            return Err(SiteError::Invalid("Lys source grant is not standing"));
        }
        let ends_at = source_grant
            .body
            .get("effective_ends_at")
            .ok_or(SiteError::Invalid("Lys source grant window"))?;
        if !ends_at.is_null() && !ends_at.is_u64() {
            return Err(SiteError::Invalid("Lys source grant window"));
        }
        // Lys passes a grant on only for its own resource: a service-held source yields the
        // service, and a network-held source yields the network. liminal still lets the builder
        // plan and serve only the site it is bound to; unbinding revokes both grants.
        let resource = source_grant
            .body
            .get("resource")
            .filter(|resource| {
                resource["kind"] == "liminal.service" || resource["kind"] == "liminal.network"
            })
            .cloned()
            .unwrap_or_else(|| serde_json::json!({"kind":"liminal.service","id":site}));
        let answer = self.post(
            "grants",
            &serde_json::json!({
                "operation": operation(draft, action), "route":"api", "source":source,
                "recipient":agent, "responsible":responsible,
                "resource":resource, "relation":relation_of(action),
                "pass_on":{"kind":"use_only"}, "window":{"starts_at":at_ms / 1000,"ends_at":ends_at}
            }),
        )?;
        let grant = answer
            .get("grant")
            .and_then(serde_json::Value::as_str)
            .filter(|id| {
                !id.is_empty()
                    && id.len() <= self.metadata_bytes.get()
                    && !id.chars().any(char::is_control)
            })
            .ok_or(SiteError::Invalid("Lys grant receipt"))?;
        Ok(grant.into())
    }

    /// Places the site's service under its network, keyed by the service; Lys's own refusal of
    /// an existing placement is the placement already standing.
    fn place_in_network(&self, kind: &str, id: &str) -> Result<(), SiteError> {
        let Some((path, network)) = &self.sources.placement else {
            return Ok(());
        };
        let credential = zeroize::Zeroizing::new(
            std::fs::read_to_string(path)
                .map_err(|_| SiteError::Invalid("app credential unreadable"))?
                .trim()
                .to_owned(),
        );
        let answer = self
            .http
            .post(
                &credential,
                "apps/liminal/placements",
                &serde_json::json!({
                    "operation": operation(&format!("{kind}:{id}"), "placement"),
                    "child": {"kind": kind, "id": id},
                    "parent": {"kind": "liminal.network", "id": network},
                }),
            )
            .map_err(|_| SiteError::Invalid("Lys placement transport failed"))?;
        if answer.succeeded()
            || answer.body["reason"]
                == "placement_invalid: the resource is already placed in a parent"
        {
            return Ok(());
        }
        Err(SiteError::Pass(answer.refusal()))
    }

    /// The grant routes' bearer: the configured account credential, else the machine pass.
    fn bearer(&self) -> Result<zeroize::Zeroizing<String>, SiteError> {
        if let Some(path) = &self.sources.credential_path {
            let credential = std::fs::read_to_string(path)
                .map_err(|_| SiteError::Invalid("construction grant credential unreadable"))?;
            return Ok(zeroize::Zeroizing::new(credential.trim().to_owned()));
        }
        self.pass
            .as_ref()
            .ok_or(SiteError::Invalid("construction machine pass absent"))?
            .current()
            .map_err(|_| SiteError::Invalid("machine pass unavailable"))
    }

    fn post(&self, route: &str, body: &serde_json::Value) -> Result<serde_json::Value, SiteError> {
        let pass = self.bearer()?;
        let answer = self
            .http
            .post(&pass, route, body)
            .map_err(|_| SiteError::Invalid("Lys grant transport failed"))?;
        if !answer.succeeded() {
            return Err(SiteError::Pass(answer.refusal()));
        }
        if answer.body.get("degraded").is_some() {
            return Err(SiteError::Invalid("Lys grant projection degraded"));
        }
        Ok(answer.body)
    }
}

/// Domain separation gives every remote leg its own stable Lys operation id.
#[must_use]
pub fn operation(draft: &str, effect: &str) -> String {
    let mut hash = Sha256::new();
    for value in ["liminal-construction", draft, effect] {
        hash.update(value.as_bytes());
        hash.update([0]);
    }
    let bytes = hash.finalize();
    format!("op-{}", hex::encode(&bytes[..16]))
}

impl SiteEffects for LysSiteEffects {
    fn agent_owner(&self, agent: &str) -> Result<String, SiteError> {
        let record = self
            .agents
            .registry()
            .record(agent)
            .map_err(|_| SiteError::Invalid("builder agent absent"))?;
        if !record.active() {
            return Err(SiteError::Refused(format!("agent {agent} is retired")));
        }
        Ok(record.owner)
    }
    fn bind(
        &self,
        draft: &str,
        site: &str,
        agent: &str,
        at_ms: u64,
    ) -> Result<BuilderGrants, SiteError> {
        let owner = self.agent_owner(agent)?;
        // The network's grants reach the service only once Lys knows it sits in that network.
        self.place_in_network("liminal.service", site)?;
        let plan = self.delegate(Delegation {
            draft,
            site,
            agent,
            responsible: &owner,
            action: "plan",
            source: &self.sources.plan,
            at_ms,
        })?;
        let serve = self.delegate(Delegation {
            draft,
            site,
            agent,
            responsible: &owner,
            action: "serve",
            source: &self.sources.serve,
            at_ms,
        })?;
        Ok(BuilderGrants { plan, serve })
    }
    fn place(&self, kind: &str, id: &str) -> Result<(), SiteError> {
        self.place_in_network(kind, id)
    }
    fn revoke(&self, draft: &str, label: &str, grant: &str) -> Result<(), SiteError> {
        self.post(&format!("grants/{}/revoke", segment(grant)), &serde_json::json!({
            "operation":operation(draft,label), "route":"api", "reason":"construction binding ended"
        })).map(|_| ())
    }
}

/// The schema relation that carries one action: Lys holds each name of a kind once, so the
/// relation granting `plan` is `planner` and the one granting `serve` is `server`.
fn relation_of(action: &str) -> &str {
    match action {
        "plan" => "planner",
        "serve" => "server",
        other => other,
    }
}

#[derive(Clone, Copy)]
struct Delegation<'a> {
    draft: &'a str,
    site: &'a str,
    agent: &'a str,
    responsible: &'a str,
    action: &'a str,
    source: &'a str,
    at_ms: u64,
}