1use std::sync::Arc;
12
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value};
15
16use crate::backend::agent_state::{agent_status_of_block, AgentState};
17use crate::backend::reactive::types::InjectionRequest;
18use crate::backend::rpc::engine::WshRpcEngine;
19use crate::backend::widget_access::{self, AccessError, STORAGE_MAX_BYTES, STORAGE_VALUE_MAX_BYTES, SEND_MAX_BYTES};
20use crate::backend::widget_packages::{self, WidgetPackageInfo};
21use crate::backend::widget_net;
22
23use super::AppState;
24
25#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
26#[ts(export, export_to = "../../../frontend/types/rpc/")]
27pub struct WidgetSessionReq {
28 pub id: String,
29 pub hash: String,
30 pub blockid: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
34#[ts(export, export_to = "../../../frontend/types/rpc/")]
35pub struct WidgetSessionResult {
36 pub token: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
40#[ts(export, export_to = "../../../frontend/types/rpc/")]
41pub struct WidgetEndSessionReq {
42 pub token: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
46#[ts(export, export_to = "../../../frontend/types/rpc/")]
47pub struct WidgetCallReq {
48 pub token: String,
49 pub method: String,
50 #[serde(default)]
51 #[ts(type = "unknown")]
52 pub params: Value,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
56#[ts(export, export_to = "../../../frontend/types/rpc/")]
57pub struct WidgetCallResult {
58 #[ts(type = "unknown")]
59 pub result: Value,
60}
61
62fn packages() -> Vec<WidgetPackageInfo> {
63 widget_packages::service().map(|s| s.list()).unwrap_or_default()
64}
65
66pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
67 engine.register_typed("widgets.session", |req: WidgetSessionReq, _ctx| async move {
68 let token = widget_access::sessions().open(&packages(), &req.id, &req.hash).map_err(|e| e.to_rpc())?;
69 Ok(WidgetSessionResult { token })
70 });
71
72 engine.register_typed("widgets.endsession", |req: WidgetEndSessionReq, _ctx| async move {
73 widget_access::sessions().close(&req.token);
74 Ok(json!({}))
75 });
76
77 let st = state.clone();
78 engine.register_typed("widgets.call", move |req: WidgetCallReq, _ctx| {
79 let st = st.clone();
80 async move { call(&st, &packages(), req).await.map(|result| WidgetCallResult { result }).map_err(|e| e.to_rpc()) }
81 });
82}
83
84fn param<'a>(params: &'a Value, name: &str) -> Option<&'a str> {
85 params.get(name).and_then(Value::as_str)
86}
87
88async fn call(st: &AppState, packages: &[WidgetPackageInfo], req: WidgetCallReq) -> Result<Value, AccessError> {
89 let (_session, pkg) = widget_access::sessions().authorize(packages, &req.token)?;
90 let p = &req.params;
91 let store_err = |e: crate::backend::storage::error::StoreError| AccessError::internal(e.to_string());
92 match req.method.as_str() {
93 "storage.get" => {
94 widget_access::require(&pkg, "storage")?;
95 let key = param(p, "key").unwrap_or_default();
96 widget_access::check_key(key)?;
97 let value = st.mstore.widget_storage_get(&pkg.id, key).map_err(store_err)?;
98 Ok(json!({ "value": value.and_then(|v| serde_json::from_str::<Value>(&v).ok()) }))
99 }
100 "storage.set" => {
101 widget_access::require(&pkg, "storage")?;
102 let key = param(p, "key").unwrap_or_default();
103 widget_access::check_key(key)?;
104 let value = p.get("value").ok_or_else(|| AccessError::invalid("storage.set needs { key, value }"))?;
105 let text = value.to_string();
106 if text.len() > STORAGE_VALUE_MAX_BYTES {
107 return Err(AccessError::limit("storageValue", "a stored value is limited to 1 MB"));
108 }
109 if !st.mstore.widget_storage_set(&pkg.id, key, &text, STORAGE_MAX_BYTES).map_err(store_err)? {
110 return Err(AccessError::limit("storage", "a widget's storage is limited to 5 MB"));
111 }
112 storage_changed(st, &pkg.id, key);
113 Ok(json!({}))
114 }
115 "storage.delete" => {
116 widget_access::require(&pkg, "storage")?;
117 let key = param(p, "key").unwrap_or_default();
118 widget_access::check_key(key)?;
119 st.mstore.widget_storage_delete(&pkg.id, key).map_err(store_err)?;
120 storage_changed(st, &pkg.id, key);
121 Ok(json!({}))
122 }
123 "storage.list" => {
124 widget_access::require(&pkg, "storage")?;
125 let keys = st.mstore.widget_storage_keys(&pkg.id, param(p, "prefix").unwrap_or_default()).map_err(store_err)?;
126 Ok(json!({ "keys": keys }))
127 }
128 "net.fetch" => {
129 let fetch: widget_net::FetchRequest =
130 serde_json::from_value(p.clone()).map_err(|e| AccessError::invalid(format!("net.fetch: {e}")))?;
131 let resp = widget_net::fetch(&pkg.name, &pkg.granted, fetch).await?;
132 serde_json::to_value(resp).map_err(|e| AccessError::internal(e.to_string()))
133 }
134 "agents.list" => {
135 widget_access::require(&pkg, "agents:read")?;
136 Ok(json!({ "agents": agent_rows(st) }))
137 }
138 "agents.send" => {
139 widget_access::require(&pkg, "agents:send")?;
140 let agent = param(p, "agent").unwrap_or_default();
141 let text = param(p, "text").unwrap_or_default();
142 if agent.is_empty() || text.is_empty() {
143 return Err(AccessError::invalid("agents.send needs { agent, text }"));
144 }
145 if text.len() > SEND_MAX_BYTES {
146 return Err(AccessError::limit("message", "a message from a widget is limited to 8 KB"));
147 }
148 if !st.reactive_handler.list_agents().iter().any(|a| a.agent_id.eq_ignore_ascii_case(agent)) {
149 return Err(AccessError::not_found(format!("no agent named {agent} is running on this machine")));
150 }
151 widget_access::sessions().take_send(&pkg.id)?;
152 let id = uuid::Uuid::new_v4().to_string();
153 let resp = st.reactive_handler.inject_message(InjectionRequest {
157 target_agent: agent.to_string(),
158 message: text.to_string(),
159 source_agent: Some(format!("widget:{}", pkg.id)),
160 request_id: Some(id.clone()),
161 delivery_tier: Some("host".to_string()),
162 ..Default::default()
163 });
164 if !resp.success {
165 return Err(AccessError::internal(resp.error.unwrap_or_else(|| "the message wasn't delivered".to_string())));
166 }
167 tracing::info!(widget = %pkg.id, %agent, "widget sent a message to an agent");
168 Ok(json!({ "id": id }))
169 }
170 other => Err(AccessError::new(-32601, format!("srv doesn't answer {other}"))),
171 }
172}
173
174fn storage_changed(st: &AppState, widget_id: &str, key: &str) {
177 let event = crate::backend::mps_events::WidgetStorageEvent { id: widget_id.to_string(), keys: vec![key.to_string()] };
178 st.broker.publish_event::<crate::backend::mps_events::WidgetStorage>(&event, vec![], 0);
179}
180
181fn agent_rows(st: &AppState) -> Vec<Value> {
183 let mut rows: Vec<Value> = st
184 .reactive_handler
185 .list_agents()
186 .iter()
187 .map(|a| {
188 let state = match agent_status_of_block(&st.mstore, &a.block_id).map(|s| s.state) {
189 Some(AgentState::Working) => "working",
190 Some(AgentState::Stopped) | Some(AgentState::Error) => "stopped",
191 _ => "idle",
192 };
193 json!({ "id": a.agent_id, "name": a.agent_id, "state": state })
194 })
195 .collect();
196 rows.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
197 rows
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::backend::widget_packages::{WidgetKind, WidgetState};
204
205 fn pkg(granted: &[&str]) -> WidgetPackageInfo {
206 WidgetPackageInfo {
207 id: "acme.calls".into(),
208 name: "Calls".into(),
209 version: "1.0.0".into(),
210 description: None,
211 author: None,
212 homepage: None,
213 icon: "x".into(),
214 default_hue: None,
215 kind: WidgetKind::Sandboxed,
216 permissions: granted.iter().map(|s| s.to_string()).collect(),
217 granted: granted.iter().map(|s| s.to_string()).collect(),
218 state: WidgetState::Approved,
219 error: None,
220 hash: "h1".into(),
221 panes: vec![],
222 commands: vec![],
223 status_items: vec![],
224 signature: crate::backend::widget_signature::describe("acme.x", None, &Default::default()),
225 files_url: None,
226 implied: false,
227 folder: String::new(),
228 }
229 }
230
231 async fn run(st: &AppState, packages: &[WidgetPackageInfo], token: &str, method: &str, params: Value) -> Result<Value, AccessError> {
232 call(st, packages, WidgetCallReq { token: token.into(), method: method.into(), params }).await
233 }
234
235 #[tokio::test]
238 async fn srv_refuses_each_call_without_its_grant() {
239 let st = crate::server::tests::test_state();
240 let none = vec![pkg(&[])];
241 let token = widget_access::sessions().open(&none, "acme.calls", "h1").unwrap();
242 let cases = [
243 ("storage.get", json!({ "key": "k" }), "storage"),
244 ("storage.set", json!({ "key": "k", "value": 1 }), "storage"),
245 ("storage.delete", json!({ "key": "k" }), "storage"),
246 ("storage.list", json!({}), "storage"),
247 ("agents.list", json!({}), "agents:read"),
248 ("agents.send", json!({ "agent": "a", "text": "t" }), "agents:send"),
249 ("net.fetch", json!({ "url": "https://api.github.com/user" }), "net:https://api.github.com"),
250 ];
251 for (method, params, permission) in cases {
252 let e = run(&st, &none, &token, method, params).await.unwrap_err();
253 assert_eq!(e.code, 1001, "{method}");
254 assert_eq!(e.data, Some(json!({ "permission": permission })), "{method}");
255 }
256
257 let seen = Arc::new(std::sync::Mutex::new(Vec::<Value>::new()));
259 let log = seen.clone();
260 st.broker.add_observer(Arc::new(move |ev| {
261 if ev.event == crate::backend::mps::EVENT_WIDGET_STORAGE {
262 log.lock().unwrap().push(ev.data.clone().unwrap_or_default());
263 }
264 }));
265 let some = vec![pkg(&["storage", "agents:send"])];
266 run(&st, &some, &token, "storage.set", json!({ "key": "note:1", "value": { "text": "hi" } })).await.unwrap();
267 assert_eq!(seen.lock().unwrap().as_slice(), [json!({ "id": "acme.calls", "keys": ["note:1"] })]);
268 assert_eq!(run(&st, &some, &token, "storage.get", json!({ "key": "note:1" })).await.unwrap(), json!({ "value": { "text": "hi" } }));
269 assert_eq!(run(&st, &some, &token, "storage.list", json!({ "prefix": "note:" })).await.unwrap(), json!({ "keys": ["note:1"] }));
270 run(&st, &some, &token, "storage.delete", json!({ "key": "note:1" })).await.unwrap();
271 assert_eq!(run(&st, &some, &token, "storage.get", json!({ "key": "note:1" })).await.unwrap(), json!({ "value": null }));
272 let e = run(&st, &some, &token, "agents.send", json!({ "agent": "nobody", "text": "hi" })).await.unwrap_err();
274 assert_eq!(e.code, 1003);
275
276 let mut changed = some.clone();
278 changed[0].hash = "h2".into();
279 assert_eq!(run(&st, &changed, &token, "storage.list", json!({})).await.unwrap_err().code, 1005);
280 widget_access::sessions().close(&token);
281 }
282}