liminal_server/auth_lys/live/
drafts_join.rs1use 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
8pub 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 #[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}