use super::{
Arc, DraftRequest, Error, GrantMode, Mode, PreparedDraftKeeper, drafts, request_digest,
};
use crate::agents::records::AgentAct;
use crate::services::lifecycle::{Lifecycle, site::SiteAct};
use drafts::waiting::durable::{ApprovalAgents, Prepared};
pub struct PreparedOwner {
agents: Arc<ApprovalAgents>,
lifecycle: Arc<Lifecycle>,
identity: Arc<lys_pass::VerifiedPass>,
limits: drafts::DraftLimits,
waiting: drafts::waiting::WaitingLimits,
}
impl PreparedOwner {
#[must_use]
pub const fn new(
agents: Arc<ApprovalAgents>,
lifecycle: Arc<Lifecycle>,
identity: Arc<lys_pass::VerifiedPass>,
limits: drafts::DraftLimits,
waiting: drafts::waiting::WaitingLimits,
) -> Self {
Self {
agents,
lifecycle,
identity,
limits,
waiting,
}
}
fn exact(&self, holder: &str, request: &DraftRequest, at_ms: u64) -> Result<(), Error> {
if holder != self.identity.claims().holder.id
|| request_digest(&request.words) != request.request_digest
{
return Err(Error::Invalid("prepared identity or digest mismatch"));
}
if request.target.kind == "liminal.agent" && request.target.action == "agent_permission" {
if request.target.id != holder || self.identity.claims().holder.kind != "agent" {
return Err(Error::Invalid("permission holder mismatch"));
}
crate::auth_lys::prepared::validate_permission_words(&request.words)?;
return drafts::validate_request_bounds(request, self.limits)
.map_err(drafts::prepared_failure);
}
let exact = match request.target.kind.as_str() {
"liminal.agent" => {
let act: AgentAct = serde_json::from_str(&request.words)
.map_err(|_| Error::Invalid("prepared agent words"))?;
let change = act.change.clone();
let action = match &change {
crate::agents::records::AgentEvent::Create(_) => "create_agent",
crate::agents::records::AgentEvent::Start => "start",
crate::agents::records::AgentEvent::Stop => "stop",
crate::agents::records::AgentEvent::Retire => "retire",
_ => return Err(Error::Invalid("prepared agent action")),
};
if act.name != request.target.id || action != request.target.action {
return Err(Error::Invalid("prepared agent target"));
}
serde_json::to_string(
&AgentAct::prepare(act.name, change, &self.identity, at_ms)
.map_err(|_| Error::Invalid("prepared agent identity"))?,
)
}
"liminal.network" | "liminal.service" | "liminal.template" => {
let act: SiteAct = serde_json::from_str(&request.words)
.map_err(|_| Error::Invalid("prepared construction words"))?;
if act.permission_id(self.lifecycle.network_id()) != request.target.id
|| act.change.kind() != request.target.kind
|| act.change.action() != request.target.action
{
return Err(Error::Invalid("prepared construction target"));
}
serde_json::to_string(
&SiteAct::prepare(act.target, act.change, &self.identity)
.map_err(|_| Error::Invalid("prepared construction identity"))?,
)
}
_ => return Err(Error::Invalid("prepared target kind")),
}
.map_err(|_| Error::Invalid("prepared words encoding"))?;
if exact != request.words {
return Err(Error::Invalid(
"prepared words differ from verified operation",
));
}
drafts::validate_request_bounds(request, self.limits).map_err(drafts::prepared_failure)
}
}
impl PreparedDraftKeeper for PreparedOwner {
fn prepare(&self, holder: &str, request: &DraftRequest, at_ms: u64) -> Result<(), Error> {
self.exact(holder, request, at_ms)?;
let mode = match self.identity.evaluate(
&request.target.kind,
&request.target.id,
&request.target.action,
at_ms / 1000,
)? {
lys_pass::Decision::Held { grant, mode } if grant == request.grant => match mode {
Mode::ByDraft => GrantMode::ByDraft,
Mode::ByTwo => GrantMode::ByTwo,
Mode::Outright => return Err(Error::Invalid("prepared approval mode")),
},
_ => return Err(Error::Invalid("prepared operation is not held by its pass")),
};
let agent = if self.identity.claims().holder.kind == "agent" {
let record = self
.agents
.agents()
.registry()
.record(holder)
.map_err(|_| Error::Invalid("prepared agent is not bound"))?;
if !record.active()
|| self.identity.claims().holder.responsible.as_deref()
!= Some(record.owner.as_str())
{
return Err(Error::Invalid("prepared agent is retired"));
}
Some(record.name)
} else {
None
};
self.agents
.journal()
.update(|snapshot| {
if let Some(old) = snapshot.prepared.get(&request.operation) {
if old.holder != holder
|| old.request.grant != request.grant
|| old.request.target != request.target
|| old.request.words != request.words
|| old.request.request_digest != request.request_digest
|| old.mode != mode
{
return Err(drafts::DraftError::ReceiptMismatch);
}
return Ok(());
}
snapshot.prepared.insert(
request.operation.clone(),
Prepared {
holder: holder.into(),
agent,
request: request.clone(),
at_ms,
mode,
draft: None,
},
);
Ok(())
})
.map_err(drafts::prepared_failure)
}
fn held(
&self,
holder: &str,
id: &str,
request: &DraftRequest,
at_ms: u64,
) -> Result<(), Error> {
self.exact(holder, request, at_ms)?;
if !drafts::valid_metadata(id, self.limits.metadata_bytes) {
return Err(Error::Invalid("held draft identifier bound"));
}
let prepared = self
.agents
.journal()
.read(|snapshot| snapshot.prepared.get(&request.operation).cloned())
.map_err(drafts::prepared_failure)?
.ok_or(Error::Invalid("draft preparation missing"))?;
if prepared.holder != holder
|| prepared.request.grant != request.grant
|| prepared.request.target != request.target
|| prepared.request.words != request.words
|| prepared.request.request_digest != request.request_digest
{
return Err(Error::Invalid("held preparation mismatch"));
}
if request.target.kind != "liminal.agent" {
let act: SiteAct = serde_json::from_str(&request.words)
.map_err(|_| Error::Invalid("held construction words"))?;
self.lifecycle
.keep_held_site(
id,
request,
&act,
&self.identity,
prepared.mode,
prepared.at_ms,
)
.map_err(|_| Error::CannotAsk("held construction journal failed"))?;
}
let state = self
.agents
.journal()
.update(|snapshot| {
if snapshot.prepared.values().any(|old| {
old.draft.as_deref() == Some(id) && old.request.operation != request.operation
}) {
return Err(drafts::DraftError::ReceiptMismatch);
}
let old = snapshot
.prepared
.get_mut(&request.operation)
.ok_or(drafts::DraftError::Contract)?;
if old.draft.as_deref().is_some_and(|bound| bound != id) {
return Err(drafts::DraftError::ReceiptMismatch);
}
old.draft = Some(id.into());
if let Some(agent) = &prepared.agent {
let waiting = snapshot.waiting.entry(agent.clone()).or_default();
waiting
.hold(id, &request.words, prepared.at_ms, self.waiting)
.map_err(|_| drafts::DraftError::Bound)?;
return waiting
.snapshot()
.map(Some)
.map_err(|_| drafts::DraftError::Store);
}
Ok(None)
})
.map_err(drafts::prepared_failure)?;
if let (Some(agent), Some(state)) = (&prepared.agent, state) {
if state["state"] != "waiting" {
return Ok(());
}
let oldest = state["draft"]
.as_str()
.ok_or(Error::Invalid("held state draft"))?;
let words = state["words"]
.as_str()
.ok_or(Error::Invalid("held state words"))?;
crate::agents::state::write_waiting(self.agents.agents(), agent, oldest, words)
.map_err(|_| Error::CannotAsk("held waiting writer failed"))?;
self.agents
.agents()
.retained()
.publish(&[(format!("agents.{agent}.state"), state)])
.map_err(|_| Error::CannotAsk("held waiting metadata failed"))?;
}
Ok(())
}
}