use super::{BuilderGrants, SiteError};
use crate::services::lys_http::{LysHttp, segment};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GrantSources {
pub plan: String,
pub serve: String,
#[serde(default)]
pub credential_path: Option<std::path::PathBuf>,
#[serde(skip)]
pub placement: Option<(std::path::PathBuf, String)>,
}
pub trait SiteEffects: Send + Sync {
fn agent_owner(&self, agent: &str) -> Result<String, SiteError>;
fn bind(
&self,
draft: &str,
site: &str,
agent: &str,
at_ms: u64,
) -> Result<BuilderGrants, SiteError>;
fn revoke(&self, draft: &str, label: &str, grant: &str) -> Result<(), SiteError>;
fn place(&self, _kind: &str, _id: &str) -> Result<(), SiteError> {
Ok(())
}
}
pub struct LysSiteEffects {
http: Arc<dyn LysHttp>,
pass: Option<crate::link_pass::MachinePass>,
agents: Arc<crate::agents::runtime::Agents>,
sources: GrantSources,
metadata_bytes: std::num::NonZeroUsize,
}
impl LysSiteEffects {
#[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"));
}
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())
}
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()))
}
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)
}
}
#[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)?;
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(|_| ())
}
}
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,
}