Skip to main content

liminal_server/auth_lys/live/
drafts_join.rs

1use super::{
2    Arc, DraftRequest, Error, GrantMode, Mode, PreparedDraftKeeper, drafts, request_digest,
3};
4use crate::agents::records::AgentAct;
5use crate::services::lifecycle::{Lifecycle, site::SiteAct};
6use drafts::waiting::durable::{ApprovalAgents, Prepared};
7
8/// One verified identity prepares exact words against the owning registries.
9/// It carries verified claims only; the bearer is borrowed for the live call.
10pub struct PreparedOwner {
11    agents: Arc<ApprovalAgents>,
12    lifecycle: Arc<Lifecycle>,
13    identity: Arc<lys_pass::VerifiedPass>,
14    limits: drafts::DraftLimits,
15    waiting: drafts::waiting::WaitingLimits,
16}
17impl PreparedOwner {
18    /// Shares the actual target owners and configured admission capacities.
19    #[must_use]
20    pub const fn new(
21        agents: Arc<ApprovalAgents>,
22        lifecycle: Arc<Lifecycle>,
23        identity: Arc<lys_pass::VerifiedPass>,
24        limits: drafts::DraftLimits,
25        waiting: drafts::waiting::WaitingLimits,
26    ) -> Self {
27        Self {
28            agents,
29            lifecycle,
30            identity,
31            limits,
32            waiting,
33        }
34    }
35    fn exact(&self, holder: &str, request: &DraftRequest, at_ms: u64) -> Result<(), Error> {
36        if holder != self.identity.claims().holder.id
37            || request_digest(&request.words) != request.request_digest
38        {
39            return Err(Error::Invalid("prepared identity or digest mismatch"));
40        }
41        if request.target.kind == "liminal.agent" && request.target.action == "agent_permission" {
42            if request.target.id != holder || self.identity.claims().holder.kind != "agent" {
43                return Err(Error::Invalid("permission holder mismatch"));
44            }
45            crate::auth_lys::prepared::validate_permission_words(&request.words)?;
46            return drafts::validate_request_bounds(request, self.limits)
47                .map_err(drafts::prepared_failure);
48        }
49        let exact = match request.target.kind.as_str() {
50            "liminal.agent" => {
51                let act: AgentAct = serde_json::from_str(&request.words)
52                    .map_err(|_| Error::Invalid("prepared agent words"))?;
53                let change = act.change.clone();
54                let action = match &change {
55                    crate::agents::records::AgentEvent::Create(_) => "create_agent",
56                    crate::agents::records::AgentEvent::Start => "start",
57                    crate::agents::records::AgentEvent::Stop => "stop",
58                    crate::agents::records::AgentEvent::Retire => "retire",
59                    _ => return Err(Error::Invalid("prepared agent action")),
60                };
61                if act.name != request.target.id || action != request.target.action {
62                    return Err(Error::Invalid("prepared agent target"));
63                }
64                serde_json::to_string(
65                    &AgentAct::prepare(act.name, change, &self.identity, at_ms)
66                        .map_err(|_| Error::Invalid("prepared agent identity"))?,
67                )
68            }
69            "liminal.network" | "liminal.service" | "liminal.template" => {
70                let act: SiteAct = serde_json::from_str(&request.words)
71                    .map_err(|_| Error::Invalid("prepared construction words"))?;
72                if act.permission_id(self.lifecycle.network_id()) != request.target.id
73                    || act.change.kind() != request.target.kind
74                    || act.change.action() != request.target.action
75                {
76                    return Err(Error::Invalid("prepared construction target"));
77                }
78                serde_json::to_string(
79                    &SiteAct::prepare(act.target, act.change, &self.identity)
80                        .map_err(|_| Error::Invalid("prepared construction identity"))?,
81                )
82            }
83            _ => return Err(Error::Invalid("prepared target kind")),
84        }
85        .map_err(|_| Error::Invalid("prepared words encoding"))?;
86        if exact != request.words {
87            return Err(Error::Invalid(
88                "prepared words differ from verified operation",
89            ));
90        }
91        drafts::validate_request_bounds(request, self.limits).map_err(drafts::prepared_failure)
92    }
93}
94impl PreparedDraftKeeper for PreparedOwner {
95    fn prepare(&self, holder: &str, request: &DraftRequest, at_ms: u64) -> Result<(), Error> {
96        self.exact(holder, request, at_ms)?;
97        let mode = match self.identity.evaluate(
98            &request.target.kind,
99            &request.target.id,
100            &request.target.action,
101            at_ms / 1000,
102        )? {
103            lys_pass::Decision::Held { grant, mode } if grant == request.grant => match mode {
104                Mode::ByDraft => GrantMode::ByDraft,
105                Mode::ByTwo => GrantMode::ByTwo,
106                Mode::Outright => return Err(Error::Invalid("prepared approval mode")),
107            },
108            _ => return Err(Error::Invalid("prepared operation is not held by its pass")),
109        };
110        let agent = if self.identity.claims().holder.kind == "agent" {
111            let record = self
112                .agents
113                .agents()
114                .registry()
115                .record(holder)
116                .map_err(|_| Error::Invalid("prepared agent is not bound"))?;
117            if !record.active()
118                || self.identity.claims().holder.responsible.as_deref()
119                    != Some(record.owner.as_str())
120            {
121                return Err(Error::Invalid("prepared agent is retired"));
122            }
123            Some(record.name)
124        } else {
125            None
126        };
127        self.agents
128            .journal()
129            .update(|snapshot| {
130                if let Some(old) = snapshot.prepared.get(&request.operation) {
131                    if old.holder != holder
132                        || old.request.grant != request.grant
133                        || old.request.target != request.target
134                        || old.request.words != request.words
135                        || old.request.request_digest != request.request_digest
136                        || old.mode != mode
137                    {
138                        return Err(drafts::DraftError::ReceiptMismatch);
139                    }
140                    return Ok(());
141                }
142                snapshot.prepared.insert(
143                    request.operation.clone(),
144                    Prepared {
145                        holder: holder.into(),
146                        agent,
147                        request: request.clone(),
148                        at_ms,
149                        mode,
150                        draft: None,
151                    },
152                );
153                Ok(())
154            })
155            .map_err(drafts::prepared_failure)
156    }
157    fn held(
158        &self,
159        holder: &str,
160        id: &str,
161        request: &DraftRequest,
162        at_ms: u64,
163    ) -> Result<(), Error> {
164        self.exact(holder, request, at_ms)?;
165        if !drafts::valid_metadata(id, self.limits.metadata_bytes) {
166            return Err(Error::Invalid("held draft identifier bound"));
167        }
168        let prepared = self
169            .agents
170            .journal()
171            .read(|snapshot| snapshot.prepared.get(&request.operation).cloned())
172            .map_err(drafts::prepared_failure)?
173            .ok_or(Error::Invalid("draft preparation missing"))?;
174        if prepared.holder != holder
175            || prepared.request.grant != request.grant
176            || prepared.request.target != request.target
177            || prepared.request.words != request.words
178            || prepared.request.request_digest != request.request_digest
179        {
180            return Err(Error::Invalid("held preparation mismatch"));
181        }
182        if request.target.kind != "liminal.agent" {
183            let act: SiteAct = serde_json::from_str(&request.words)
184                .map_err(|_| Error::Invalid("held construction words"))?;
185            self.lifecycle
186                .keep_held_site(
187                    id,
188                    request,
189                    &act,
190                    &self.identity,
191                    prepared.mode,
192                    prepared.at_ms,
193                )
194                .map_err(|_| Error::CannotAsk("held construction journal failed"))?;
195        }
196        let state = self
197            .agents
198            .journal()
199            .update(|snapshot| {
200                if snapshot.prepared.values().any(|old| {
201                    old.draft.as_deref() == Some(id) && old.request.operation != request.operation
202                }) {
203                    return Err(drafts::DraftError::ReceiptMismatch);
204                }
205                let old = snapshot
206                    .prepared
207                    .get_mut(&request.operation)
208                    .ok_or(drafts::DraftError::Contract)?;
209                if old.draft.as_deref().is_some_and(|bound| bound != id) {
210                    return Err(drafts::DraftError::ReceiptMismatch);
211                }
212                old.draft = Some(id.into());
213                if let Some(agent) = &prepared.agent {
214                    let waiting = snapshot.waiting.entry(agent.clone()).or_default();
215                    waiting
216                        .hold(id, &request.words, prepared.at_ms, self.waiting)
217                        .map_err(|_| drafts::DraftError::Bound)?;
218                    return waiting
219                        .snapshot()
220                        .map(Some)
221                        .map_err(|_| drafts::DraftError::Store);
222                }
223                Ok(None)
224            })
225            .map_err(drafts::prepared_failure)?;
226        if let (Some(agent), Some(state)) = (&prepared.agent, state) {
227            if state["state"] != "waiting" {
228                return Ok(());
229            }
230            let oldest = state["draft"]
231                .as_str()
232                .ok_or(Error::Invalid("held state draft"))?;
233            let words = state["words"]
234                .as_str()
235                .ok_or(Error::Invalid("held state words"))?;
236            crate::agents::state::write_waiting(self.agents.agents(), agent, oldest, words)
237                .map_err(|_| Error::CannotAsk("held waiting writer failed"))?;
238            self.agents
239                .agents()
240                .retained()
241                .publish(&[(format!("agents.{agent}.state"), state)])
242                .map_err(|_| Error::CannotAsk("held waiting metadata failed"))?;
243        }
244        Ok(())
245    }
246}