1use std::sync::Arc;
4
5use super::live::{HttpLysAsk, PreparedOwner};
6use super::lys_pass_adapter::LysPass;
7use crate::agents::waiting::durable::ApprovalAgents;
8use crate::services::lifecycle::{Lifecycle, site};
9
10pub struct SiteRequest<'a> {
12 pub lifecycle: &'a Arc<Lifecycle>,
13 pub owners: &'a Arc<ApprovalAgents>,
14 pub config: &'a crate::config::types::LysConfig,
15 pub handle: &'a tokio::runtime::Handle,
16 pub operation: &'a str,
17 pub target: &'a str,
18 pub change: site::SiteChange,
19 pub at_ms: u64,
20}
21
22pub struct PermissionRequest<'a> {
24 pub lifecycle: &'a Arc<Lifecycle>,
25 pub owners: &'a Arc<ApprovalAgents>,
26 pub config: &'a crate::config::types::LysConfig,
27 pub handle: &'a tokio::runtime::Handle,
28 pub operation: &'a str,
29 pub words: &'a str,
30 pub at_ms: u64,
31}
32
33pub(crate) fn validate_permission_words(words: &str) -> Result<(), lys_pass::Error> {
34 #[derive(serde::Deserialize)]
35 #[serde(deny_unknown_fields)]
36 struct Words {
37 title: String,
38 options: Vec<OptionWord>,
39 }
40 #[derive(serde::Deserialize)]
41 #[serde(deny_unknown_fields, rename_all = "camelCase")]
42 struct OptionWord {
43 option_id: String,
44 name: String,
45 kind: String,
46 }
47 let value: Words =
48 serde_json::from_str(words).map_err(|_| lys_pass::Error::Invalid("permission words"))?;
49 let mut ids = std::collections::BTreeSet::new();
50 if value.title.trim().is_empty()
51 || value.options.is_empty()
52 || value.options.iter().any(|option| {
53 option.option_id.is_empty()
54 || option.name.is_empty()
55 || !ids.insert(&option.option_id)
56 || (option.kind.is_empty() || option.kind.chars().any(char::is_control))
57 })
58 {
59 return Err(lys_pass::Error::Invalid("permission options"));
60 }
61 Ok(())
62}
63
64impl PermissionRequest<'_> {
65 pub(super) fn execute(self, pass: &LysPass) -> Result<serde_json::Value, lys_pass::Error> {
66 validate_permission_words(self.words)?;
67 let limits = self
68 .config
69 .drafts
70 .ok_or(lys_pass::Error::CannotAsk("draft limits missing"))?;
71 let keeper = Arc::new(PreparedOwner::new(
72 self.owners.clone(),
73 self.lifecycle.clone(),
74 pass.shared_verified(),
75 limits.pull,
76 limits.waiting,
77 ));
78 let mut live = HttpLysAsk::new(
79 self.config
80 .api_url
81 .as_deref()
82 .unwrap_or(&self.config.issuer),
83 self.handle.clone(),
84 )?
85 .with_drafts(keeper);
86 if let Some(path) = &self.config.app_credential_path {
87 let credential = std::fs::read_to_string(path)
88 .map_err(|_| lys_pass::Error::CannotAsk("draft connector credential unreadable"))?;
89 live = live.asking_as(credential.trim().to_owned());
90 }
91 let target = lys_pass::Target::new("liminal.agent", pass.holder(), "agent_permission")?;
92 let answer = live.ask_prepared(
93 pass.raw_token(),
94 &super::live::PreparedAct {
95 holder: pass.holder(),
96 target: &target,
97 operation: self.operation,
98 words: self.words,
99 at_ms: self.at_ms,
100 },
101 limits.pull,
102 )?;
103 match answer.decision {
104 super::lys_pass_adapter::LiveAnswer::Held { draft, .. } => {
105 Ok(serde_json::json!({"state":"held","draft":draft,"words":self.words}))
106 }
107 _ => Err(lys_pass::Error::CannotAsk(
108 "permission requires a held draft",
109 )),
110 }
111 }
112}
113
114impl SiteRequest<'_> {
115 pub(super) fn execute(self, pass: &LysPass) -> Result<serde_json::Value, lys_pass::Error> {
116 let limits = self
117 .config
118 .drafts
119 .ok_or(lys_pass::Error::CannotAsk("draft limits missing"))?;
120 let keeper = Arc::new(PreparedOwner::new(
121 self.owners.clone(),
122 self.lifecycle.clone(),
123 pass.shared_verified(),
124 limits.pull,
125 limits.waiting,
126 ));
127 let mut live = HttpLysAsk::new(
128 self.config
129 .api_url
130 .as_deref()
131 .unwrap_or(&self.config.issuer),
132 self.handle.clone(),
133 )?
134 .with_drafts(keeper);
135 if let Some(path) = &self.config.app_credential_path {
136 let credential = std::fs::read_to_string(path)
137 .map_err(|_| lys_pass::Error::CannotAsk("draft connector credential unreadable"))?;
138 live = live.asking_as(credential.trim().to_owned());
139 }
140 site::preparation::hold_verified(
141 site::preparation::VerifiedContext {
142 lifecycle: self.lifecycle,
143 pass: pass.verified(),
144 token: pass.raw_token(),
145 live: &live,
146 at_ms: self.at_ms,
147 decisions: self.config.decisions,
148 warned: pass.warned(),
149 limits: limits.pull,
150 },
151 self.operation,
152 self.target,
153 self.change,
154 )
155 .map_err(|error| {
156 tracing::warn!(%error, "the construction act was not held");
157 lys_pass::Error::CannotAsk("prepared construction refused")
158 })
159 }
160}
161
162pub(crate) struct SiteHolder {
164 pub lifecycle: Arc<Lifecycle>,
165 pub owners: Arc<ApprovalAgents>,
166 pub config: crate::config::types::LysConfig,
167 pub handle: tokio::runtime::Handle,
168}
169
170impl std::fmt::Debug for SiteHolder {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.debug_struct("SiteHolder").finish_non_exhaustive()
173 }
174}
175
176impl crate::server::connection::services::SiteDraftHolder for SiteHolder {
177 fn hold(
178 &self,
179 lifecycle: &Lifecycle,
180 session: &dyn super::session::LysSession,
181 operation: &str,
182 target: &str,
183 change: site::SiteChange,
184 at_ms: u64,
185 ) -> Result<serde_json::Value, site::SiteError> {
186 if !std::ptr::eq(lifecycle, self.lifecycle.as_ref()) {
187 return Err(site::SiteError::Invalid("construction owner mismatch"));
188 }
189 session
190 .hold_site(SiteRequest {
191 lifecycle: &self.lifecycle,
192 owners: &self.owners,
193 config: &self.config,
194 handle: &self.handle,
195 operation,
196 target,
197 change,
198 at_ms,
199 })
200 .map_err(|error| {
201 tracing::warn!(target = %target, %error, "a construction draft could not be held");
203 site::SiteError::Pass(error.name().into())
204 })
205 }
206}