1use crate::messenger::{Messenger, MessengerError, OPERATOR};
6use crate::provision::{read_ledger, LedgerEntry, NodeStatus};
7use crate::tasks::TaskStore;
8use serde_json::{json, Value};
9use std::path::{Path, PathBuf};
10use std::sync::{Condvar, Mutex};
11use std::time::{Duration, Instant};
12
13#[derive(Debug)]
14pub struct HubError {
15 pub status: u16,
16 pub message: String,
17}
18
19impl HubError {
20 fn new(status: u16, message: impl Into<String>) -> Self {
21 Self { status, message: message.into() }
22 }
23}
24
25impl From<MessengerError> for HubError {
26 fn from(e: MessengerError) -> Self {
27 match e {
28 MessengerError::Forbidden => Self::new(403, "refused"),
29 MessengerError::Bad(m) => Self::new(400, m),
30 MessengerError::Internal(m) => Self::new(500, m),
31 }
32 }
33}
34
35pub struct Hub {
36 data_dir: PathBuf,
37 messenger: Mutex<Messenger>,
38 tasks: Mutex<TaskStore>,
39 mail_signal: (Mutex<u64>, Condvar),
41 #[cfg(feature = "harness")]
43 kernel: Option<Mutex<crate::kernel::Kernel>>,
44}
45
46fn str_field<'a>(v: &'a Value, name: &str) -> Result<&'a str, HubError> {
47 v.get(name).and_then(Value::as_str).ok_or_else(|| HubError::new(400, format!("missing field: {name}")))
48}
49
50impl Hub {
51 pub fn open(data_dir: &Path) -> Result<Self, crate::error::ServerError> {
52 Ok(Self {
53 data_dir: data_dir.to_owned(),
54 messenger: Mutex::new(Messenger::open(data_dir)?),
55 tasks: Mutex::new(TaskStore::open(data_dir)?),
56 mail_signal: (Mutex::new(0), Condvar::new()),
57 #[cfg(feature = "harness")]
58 kernel: match crate::kernel::Kernel::open(data_dir) {
59 Ok(k) => Some(Mutex::new(k)),
60 Err(err) => {
61 tracing::warn!(error = %err, "task kernel unavailable");
62 None
63 }
64 },
65 })
66 }
67
68 fn bump_mail(&self) {
69 *self.mail_signal.0.lock().expect("signal") += 1;
70 self.mail_signal.1.notify_all();
71 }
72
73 fn entry(&self, node_id: &str) -> Option<LedgerEntry> {
74 read_ledger(&self.data_dir).ok()?.into_iter().find(|e| e.node_id == node_id)
75 }
76
77 fn admitted(&self, node_id: &str, capability: &str) -> bool {
79 node_id != OPERATOR
80 && self.entry(node_id).is_some_and(|e| {
81 matches!(e.status, NodeStatus::Member | NodeStatus::Trusted) && e.grants.capabilities.iter().any(|c| c == capability)
82 })
83 }
84
85 pub fn node_call(&self, node_id: &str, op: &str, payload: &Value) -> Result<Value, HubError> {
88 let capability = match op.split_once('.').map(|p| p.0) {
89 Some("mail") => "mail",
90 Some("task") => "tasks",
91 _ => return Err(HubError::new(404, "unknown operation")),
92 };
93 if !self.admitted(node_id, capability) {
94 return Err(HubError::new(403, "refused"));
95 }
96 match op {
97 "mail.send" => {
98 let key = payload.get("idempotency_key").and_then(Value::as_str).map(str::to_owned);
99 let id = self.messenger.lock().expect("messenger").send(node_id, str_field(payload, "to")?, str_field(payload, "subject")?, str_field(payload, "body")?, key)?;
100 self.bump_mail();
101 Ok(json!({ "message_id": id }))
102 }
103 "mail.inbox" => {
104 let since = payload.get("since_ms").and_then(Value::as_u64).unwrap_or(0);
105 let limit = payload.get("limit").and_then(Value::as_u64).unwrap_or(50).min(256) as u16;
106 let mail = self.messenger.lock().expect("messenger").inbox(node_id, since, limit)?;
107 Ok(json!({ "messages": mail }))
108 }
109 "mail.ack" => {
110 self.messenger.lock().expect("messenger").ack(node_id, str_field(payload, "message_id")?)?;
111 Ok(json!({ "ok": true }))
112 }
113 "task.list" => {
114 let tasks = self.tasks.lock().expect("tasks").list(Some(node_id), true).map_err(|e| HubError::new(500, e.to_string()))?;
115 Ok(json!({ "tasks": tasks }))
116 }
117 "task.update" => {
118 let id = payload.get("id").and_then(Value::as_i64).ok_or_else(|| HubError::new(400, "missing field: id"))?;
119 let status = str_field(payload, "status")?;
120 if !matches!(status, "in_progress" | "done") {
121 return Err(HubError::new(400, "status must be in_progress or done"));
122 }
123 let changed = self.tasks.lock().expect("tasks").set_status(id, status, Some(node_id)).map_err(|e| HubError::new(500, e.to_string()))?;
124 if changed { Ok(json!({ "ok": true })) } else { Err(HubError::new(403, "refused")) }
125 }
126 _ => Err(HubError::new(404, "unknown operation")),
127 }
128 }
129
130 pub fn operator_call(&self, method: &str, path: &str, body: &Value) -> Result<Value, HubError> {
132 let (path, query) = path.split_once('?').unwrap_or((path, ""));
133 let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
134 #[cfg(feature = "harness")]
135 if let ["v1", "kernel", rest @ ..] = parts.as_slice() {
136 let Some(kernel) = &self.kernel else { return Err(HubError::new(503, "task kernel unavailable")) };
137 return kernel.lock().expect("kernel").call(method, rest, body).map_err(|e| HubError::new(e.status, e.message));
138 }
139 match (method, parts.as_slice()) {
140 ("POST", ["v1", "tasks"]) => {
141 let assignee = body.get("assignee").and_then(Value::as_str);
142 if let Some(node) = assignee {
143 if !self.admitted(node, "tasks") {
144 return Err(HubError::new(403, "assignee does not hold the tasks capability"));
145 }
146 }
147 let task = self.tasks.lock().expect("tasks").create(str_field(body, "title")?, body.get("body").and_then(Value::as_str).unwrap_or(""), assignee).map_err(|e| HubError::new(500, e.to_string()))?;
148 Ok(json!({ "task": task }))
149 }
150 ("GET", ["v1", "tasks"]) => {
151 let assignee = query.split('&').find_map(|kv| kv.strip_prefix("assignee="));
152 let tasks = self.tasks.lock().expect("tasks").list(assignee, false).map_err(|e| HubError::new(500, e.to_string()))?;
153 Ok(json!({ "tasks": tasks }))
154 }
155 ("POST", ["v1", "tasks", id, "assign"]) => {
156 let id: i64 = id.parse().map_err(|_| HubError::new(400, "bad id"))?;
157 let node = str_field(body, "assignee")?;
158 if !self.admitted(node, "tasks") {
159 return Err(HubError::new(403, "assignee does not hold the tasks capability"));
160 }
161 if self.tasks.lock().expect("tasks").assign(id, node).map_err(|e| HubError::new(500, e.to_string()))? { Ok(json!({ "ok": true })) } else { Err(HubError::new(409, "task cannot be assigned")) }
162 }
163 ("POST", ["v1", "tasks", id, "cancel"]) => {
164 let id: i64 = id.parse().map_err(|_| HubError::new(400, "bad id"))?;
165 if self.tasks.lock().expect("tasks").set_status(id, "cancelled", None).map_err(|e| HubError::new(500, e.to_string()))? { Ok(json!({ "ok": true })) } else { Err(HubError::new(409, "task is final")) }
166 }
167 ("POST", ["v1", "mail", "send"]) => {
168 let id = self.messenger.lock().expect("messenger").send(OPERATOR, str_field(body, "to")?, str_field(body, "subject")?, str_field(body, "body")?, None)?;
169 self.bump_mail();
170 Ok(json!({ "message_id": id }))
171 }
172 ("POST", ["v1", "mail", "wait"]) => {
175 let since = body.get("since_ms").and_then(Value::as_u64).unwrap_or(0);
176 let deadline = Instant::now() + Duration::from_secs(body.get("timeout_secs").and_then(Value::as_u64).unwrap_or(25).min(30));
177 loop {
178 let seen = *self.mail_signal.0.lock().expect("signal");
179 let mail: Vec<_> = self.messenger.lock().expect("messenger").inbox(OPERATOR, since, 100)?.into_iter().filter(|m| m.created_at_ms > since).collect();
180 if !mail.is_empty() {
181 return Ok(json!({ "messages": mail }));
182 }
183 let now = Instant::now();
184 if now >= deadline {
185 return Ok(json!({ "messages": [] }));
186 }
187 let guard = self.mail_signal.0.lock().expect("signal");
188 if *guard == seen {
189 let _ = self.mail_signal.1.wait_timeout(guard, deadline - now).expect("signal wait");
190 }
191 }
192 }
193 ("POST", ["v1", "mail", "thread"]) => {
194 let mail = self.messenger.lock().expect("messenger").thread(str_field(body, "node")?, 200)?;
195 Ok(json!({ "messages": mail }))
196 }
197 ("POST", ["v1", "mail", "inbox"]) => {
198 let mail = self.messenger.lock().expect("messenger").inbox(OPERATOR, 0, 100)?;
199 Ok(json!({ "messages": mail }))
200 }
201 _ => Err(HubError::new(404, "not found")),
202 }
203 }
204}