1use std::sync::{Mutex, OnceLock};
13
14use serde::{Deserialize, Serialize};
15use tokio::sync::oneshot;
16
17use super::widget_packages::{WidgetKind, WidgetPackageInfo};
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
20#[ts(export, export_to = "../../../frontend/types/rpc/")]
21pub struct WidgetInstallRequest {
22 pub id: String,
23 pub hash: String,
24 pub name: String,
25 pub version: String,
26 pub description: Option<String>,
27 pub author: Option<String>,
28 pub kind: WidgetKind,
29 pub permissions: Vec<String>,
30 pub signature: crate::backend::widget_signature::WidgetSignatureInfo,
32 pub agent: String,
34 #[ts(type = "number")]
35 pub requested_ms: u64,
36}
37
38impl WidgetInstallRequest {
39 pub fn of(pkg: &WidgetPackageInfo, agent: &str) -> Self {
40 Self {
41 id: pkg.id.clone(),
42 hash: pkg.hash.clone(),
43 name: pkg.name.clone(),
44 version: pkg.version.clone(),
45 description: pkg.description.clone(),
46 author: pkg.author.clone(),
47 kind: pkg.kind.clone(),
48 permissions: pkg.permissions.clone(),
49 signature: pkg.signature.clone(),
50 agent: agent.to_string(),
51 requested_ms: agentmux_common::time::now_ms_u64(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Answer {
58 Approved,
59 Declined,
60}
61
62#[derive(Default)]
63pub struct Requests {
64 pending: Mutex<Vec<(WidgetInstallRequest, Vec<oneshot::Sender<Answer>>)>>,
65}
66
67pub fn requests() -> &'static Requests {
68 static REQUESTS: OnceLock<Requests> = OnceLock::new();
69 REQUESTS.get_or_init(Requests::default)
70}
71
72const MAX_PENDING: usize = 32;
74
75impl Requests {
76 pub fn ask(&self, req: WidgetInstallRequest) -> oneshot::Receiver<Answer> {
83 let (tx, rx) = oneshot::channel();
84 let mut pending = self.pending.lock().unwrap_or_else(|p| p.into_inner());
85 let same = |r: &WidgetInstallRequest| r.id == req.id && r.hash == req.hash && r.signature.fingerprint == req.signature.fingerprint;
86 pending.retain_mut(|(r, waiters)| {
87 if r.id != req.id || same(r) {
88 return true;
89 }
90 for tx in waiters.drain(..) {
91 let _ = tx.send(Answer::Declined);
92 }
93 false
94 });
95 if let Some((_, waiters)) = pending.iter_mut().find(|(r, _)| same(r)) {
96 waiters.push(tx);
97 } else {
98 if pending.len() >= MAX_PENDING {
99 pending.remove(0);
100 }
101 pending.push((req, vec![tx]));
102 }
103 rx
104 }
105
106 pub fn answer(&self, id: &str, hash: &str, answer: Answer) -> bool {
110 let mut pending = self.pending.lock().unwrap_or_else(|p| p.into_inner());
111 let mut found = false;
112 pending.retain_mut(|(r, waiters)| {
113 if r.id != id {
114 return true;
115 }
116 let this_version = r.hash == hash;
117 found |= this_version;
118 for tx in waiters.drain(..) {
119 let _ = tx.send(if this_version { answer } else { Answer::Declined });
120 }
121 false
122 });
123 found
124 }
125
126 pub fn decline_all(&self, id: &str) -> bool {
129 let mut pending = self.pending.lock().unwrap_or_else(|p| p.into_inner());
130 let before = pending.len();
131 pending.retain_mut(|(r, waiters)| {
132 if r.id != id {
133 return true;
134 }
135 for tx in waiters.drain(..) {
136 let _ = tx.send(Answer::Declined);
137 }
138 false
139 });
140 pending.len() != before
141 }
142
143 pub fn list(&self) -> Vec<WidgetInstallRequest> {
144 self.pending.lock().unwrap_or_else(|p| p.into_inner()).iter().map(|(r, _)| r.clone()).collect()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn req(id: &str, hash: &str, agent: &str) -> WidgetInstallRequest {
153 WidgetInstallRequest {
154 id: id.into(),
155 hash: hash.into(),
156 name: id.into(),
157 version: "1.0.0".into(),
158 description: None,
159 author: None,
160 kind: WidgetKind::Sandboxed,
161 permissions: vec![],
162 signature: crate::backend::widget_signature::describe(id, None, &Default::default()),
163 agent: agent.into(),
164 requested_ms: 0,
165 }
166 }
167
168 #[tokio::test]
169 async fn a_re_signed_package_replaces_the_waiting_request() {
170 let r = Requests::default();
171 let first = r.ask(req("acme.x", "h1", "a"));
172 let mut resigned = req("acme.x", "h1", "a");
173 resigned.signature.fingerprint = Some("AAAA-BBBB-CCCC-DDDD".into());
174 let second = r.ask(resigned);
175 assert_eq!(first.await.unwrap(), Answer::Declined, "the old signer's prompt is gone");
176 let list = r.list();
177 assert_eq!(list.len(), 1);
178 assert_eq!(list[0].signature.fingerprint.as_deref(), Some("AAAA-BBBB-CCCC-DDDD"));
179 assert!(r.answer("acme.x", "h1", Answer::Approved));
180 assert_eq!(second.await.unwrap(), Answer::Approved);
181 }
182
183 #[tokio::test]
184 async fn the_users_answer_reaches_every_agent_that_asked_for_that_version() {
185 let r = Requests::default();
186 let a = r.ask(req("acme.x", "h1", "Lark"));
187 let b = r.ask(req("acme.x", "h1", "Korp"));
188 assert_eq!(r.list().len(), 1, "one prompt per version");
189 assert!(r.answer("acme.x", "h1", Answer::Approved));
190 assert_eq!(a.await.unwrap(), Answer::Approved);
191 assert_eq!(b.await.unwrap(), Answer::Approved);
192 assert!(r.list().is_empty());
193 assert!(!r.answer("acme.x", "h1", Answer::Approved), "nothing left to answer");
194 }
195
196 #[tokio::test]
197 async fn uninstalling_declines_the_widgets_requests() {
198 let r = Requests::default();
199 let waiting = r.ask(req("acme.x", "h1", "Lark"));
200 let other = r.ask(req("acme.y", "h1", "Lark"));
201 assert!(r.decline_all("acme.x"));
202 assert_eq!(waiting.await.unwrap(), Answer::Declined);
203 assert_eq!(r.list().iter().map(|q| q.id.as_str()).collect::<Vec<_>>(), ["acme.y"]);
204 assert!(!r.decline_all("acme.x"), "nothing left for it");
205 drop(other);
206 }
207
208 #[tokio::test]
209 async fn a_newer_version_replaces_the_older_request() {
210 let r = Requests::default();
211 let old = r.ask(req("acme.x", "h1", "Lark"));
212 let new = r.ask(req("acme.x", "h2", "Lark"));
213 assert_eq!(old.await.unwrap(), Answer::Declined, "the old request is declined");
214 assert_eq!(r.list().iter().map(|q| q.hash.as_str()).collect::<Vec<_>>(), ["h2"]);
215 let other = r.ask(req("acme.y", "h9", "Lark"));
216 r.answer("acme.x", "h2", Answer::Declined);
217 assert_eq!(new.await.unwrap(), Answer::Declined);
218 assert_eq!(r.list().len(), 1, "another widget's request stays");
219 drop(other);
220 }
221}