1use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use std::collections::BTreeMap;
27
28use super::error::HiveErrorDetailV1;
29use super::{require_schema, validate_id, validate_text};
30use crate::{UtcTimestamp, ValidationError};
31
32pub const BUS_POLL_SCHEMA: &str = "leanctx.hive.bus-poll/v1";
33pub const BUS_EVENTS_SCHEMA: &str = "leanctx.hive.bus-events/v1";
34pub const BUS_LEASE_REQUEST_SCHEMA: &str = "leanctx.hive.bus-lease-request/v1";
35pub const BUS_LEASE_RENEW_REQUEST_SCHEMA: &str = "leanctx.hive.bus-lease-renew-request/v1";
36pub const BUS_LEASE_SCHEMA: &str = "leanctx.hive.bus-lease/v1";
37
38pub const MAX_BUS_BODY_BYTES: usize = 64 * 1024;
40pub const MAX_BUS_EVENTS_PAGE: usize = 500;
42pub const MAX_BUS_LEASE_MS: u64 = 15 * 60 * 1000;
44
45pub fn validate_channel(channel: &str) -> Result<(), ValidationError> {
47 let mut parts = channel.splitn(3, '/');
48 let org = parts.next().and_then(|part| part.strip_prefix("org:"));
49 let vault = parts.next().and_then(|part| part.strip_prefix("vault:"));
50 let name = parts.next();
51 match (org, vault, name) {
52 (Some(org), Some(vault), Some(name)) => {
53 validate_id(org, "channel org")?;
54 super::validate_vault_id(vault)?;
55 validate_id(name, "channel name")
56 }
57 _ => Err(ValidationError::new(
58 "channel must be org:<org>/vault:<vault>/<name>",
59 )),
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum PresenceState {
67 Active,
68 Idle,
69 Closed,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum BusPriority {
76 Low,
77 Normal,
78 High,
79 Critical,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum BusPrivacy {
87 Public,
88 Team,
89 Private,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ControlAction {
96 Hold,
98 Veto,
100 Stop,
102 Resume,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct BusAgentV1 {
108 pub agent_id: String,
109 pub role: String,
110 pub vault_id: String,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct BusLeaseHolderV1 {
116 pub agent_id: String,
117 pub instance_id: String,
118 pub expires_at: UtcTimestamp,
119}
120
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(tag = "t")]
124pub enum BusFrameV1 {
125 #[serde(rename = "hello")]
127 Hello {
128 instance_id: String,
129 agents: Vec<BusAgentV1>,
130 #[serde(default)]
132 cursors: BTreeMap<String, u64>,
133 },
134 #[serde(rename = "presence")]
136 Presence {
137 agent_id: String,
138 state: PresenceState,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 profile: Option<super::profiles::PresenceProfileV1>,
142 },
143 #[serde(rename = "publish")]
145 Publish {
146 client_msg_id: String,
148 channel: String,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 to: Option<String>,
151 from: String,
152 kind: String,
153 priority: BusPriority,
154 privacy: BusPrivacy,
155 body: Value,
156 },
157 #[serde(rename = "ack")]
159 Ack { channel: String, seq: u64 },
160 #[serde(rename = "published")]
162 Published {
163 client_msg_id: String,
164 channel: String,
165 seq: u64,
166 server_time: UtcTimestamp,
167 },
168 #[serde(rename = "deliver")]
170 Deliver {
171 seq: u64,
172 channel: String,
173 from: String,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 to: Option<String>,
176 kind: String,
177 priority: BusPriority,
178 privacy: BusPrivacy,
179 body: Value,
180 server_time: UtcTimestamp,
181 },
182 #[serde(rename = "presence_update")]
184 PresenceUpdate {
185 agent_id: String,
186 instance_id: String,
187 role: String,
188 vault_id: String,
189 state: PresenceState,
190 server_time: UtcTimestamp,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 profile_id: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 profile: Option<super::profiles::PresenceProfileV1>,
197 },
198 #[serde(rename = "control")]
200 Control {
201 control_id: String,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 target_agent: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 target_ref: Option<String>,
208 action: ControlAction,
209 reason: String,
210 issued_by: String,
211 seq: u64,
212 server_time: UtcTimestamp,
213 },
214 #[serde(rename = "handoff.offer")]
216 HandoffOffer {
217 handoff_id: String,
218 channel: String,
219 from: String,
220 to: String,
221 bundle_ref: String,
223 deadline: UtcTimestamp,
224 },
225 #[serde(rename = "handoff.ack")]
227 HandoffAck { handoff_id: String, by: String },
228 #[serde(rename = "handoff.reject")]
230 HandoffReject {
231 handoff_id: String,
232 by: String,
233 reason: String,
234 },
235 #[serde(rename = "message")]
237 Message(super::messages::AgentMessageV1),
238 #[serde(rename = "message.stored")]
240 MessageStored(super::messages::MessageStoredV1),
241 #[serde(rename = "message.deliver")]
243 MessageDeliver(super::messages::MessageDeliveryV1),
244 #[serde(rename = "message.receipt")]
246 MessageReceipt(super::messages::MessageReceiptV1),
247 #[serde(rename = "message.status")]
249 MessageStatus(super::messages::MessageStatusV1),
250 #[serde(rename = "task.event")]
252 TaskEvent(super::messages::TaskEventV1),
253 #[serde(rename = "knowledge.event")]
256 KnowledgeEvent(super::knowledge::KnowledgeEventV1),
257 #[serde(rename = "ping")]
258 Ping { nonce: u64 },
259 #[serde(rename = "pong")]
260 Pong { nonce: u64 },
261 #[serde(rename = "error")]
263 Error {
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 client_msg_id: Option<String>,
266 error: HiveErrorDetailV1,
267 },
268 #[serde(other)]
270 Unknown,
271}
272
273impl BusFrameV1 {
274 pub fn validate(&self) -> Result<(), ValidationError> {
276 match self {
277 Self::Hello {
278 instance_id,
279 agents,
280 cursors,
281 } => {
282 validate_id(instance_id, "instance_id")?;
283 if agents.len() > 64 || cursors.len() > 256 {
284 return Err(ValidationError::new("hello lists are too long"));
285 }
286 for agent in agents {
287 validate_id(&agent.agent_id, "agent_id")?;
288 validate_text(&agent.role, 64, "role", false)?;
289 super::validate_vault_id(&agent.vault_id)?;
290 }
291 cursors
292 .keys()
293 .try_for_each(|channel| validate_channel(channel))
294 }
295 Self::Presence {
296 agent_id, profile, ..
297 } => {
298 validate_id(agent_id, "agent_id")?;
299 profile
300 .as_ref()
301 .map_or(Ok(()), super::profiles::PresenceProfileV1::validate)
302 }
303 Self::Publish {
304 client_msg_id,
305 channel,
306 to,
307 from,
308 kind,
309 body,
310 ..
311 } => {
312 validate_id(client_msg_id, "client_msg_id")?;
313 validate_channel(channel)?;
314 validate_id(from, "from")?;
315 if let Some(to) = to {
316 validate_id(to, "to")?;
317 }
318 validate_id(kind, "kind")?;
319 validate_body(body)
320 }
321 Self::Ack { channel, .. } => validate_channel(channel),
322 Self::HandoffOffer {
323 handoff_id,
324 channel,
325 from,
326 to,
327 bundle_ref,
328 ..
329 } => {
330 validate_id(handoff_id, "handoff_id")?;
331 validate_channel(channel)?;
332 validate_id(from, "from")?;
333 validate_id(to, "to")?;
334 validate_text(bundle_ref, 512, "bundle_ref", false)
335 }
336 Self::HandoffAck { handoff_id, by } => {
337 validate_id(handoff_id, "handoff_id")?;
338 validate_id(by, "by")
339 }
340 Self::HandoffReject {
341 handoff_id,
342 by,
343 reason,
344 } => {
345 validate_id(handoff_id, "handoff_id")?;
346 validate_id(by, "by")?;
347 validate_text(reason, 500, "reason", false)
348 }
349 Self::Message(message) => message.validate(),
350 Self::MessageReceipt(receipt) => receipt.validate(),
351 Self::Ping { .. } | Self::Pong { .. } => Ok(()),
352 Self::Published { .. }
353 | Self::Deliver { .. }
354 | Self::PresenceUpdate { .. }
355 | Self::Control { .. }
356 | Self::MessageStored(_)
357 | Self::MessageDeliver(_)
358 | Self::MessageStatus(_)
359 | Self::TaskEvent(_)
360 | Self::KnowledgeEvent(_)
361 | Self::Error { .. }
362 | Self::Unknown => Err(ValidationError::new("not a client frame")),
363 }
364 }
365}
366
367fn validate_body(body: &Value) -> Result<(), ValidationError> {
368 let size = serde_json::to_vec(body)
369 .map_err(|error| ValidationError::new(format!("body: {error}")))?
370 .len();
371 if size > MAX_BUS_BODY_BYTES {
372 return Err(ValidationError::new("bus body exceeds 64 KiB"));
373 }
374 Ok(())
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379pub struct BusPollV1 {
380 pub schema: String,
381 pub frames: Vec<BusFrameV1>,
382 pub cursor: String,
384}
385
386#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388pub struct BusEventsV1 {
389 pub schema: String,
390 pub channel: String,
391 pub events: Vec<BusFrameV1>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub next_after_seq: Option<u64>,
395}
396
397impl BusEventsV1 {
398 pub fn validate(&self) -> Result<(), ValidationError> {
399 require_schema(&self.schema, BUS_EVENTS_SCHEMA)?;
400 validate_channel(&self.channel)?;
401 if self.events.len() > MAX_BUS_EVENTS_PAGE {
402 return Err(ValidationError::new("event page is too long"));
403 }
404 let mut previous = None;
405 for event in &self.events {
406 let BusFrameV1::Deliver { seq, channel, .. } = event else {
407 return Err(ValidationError::new("catch-up events are deliver frames"));
408 };
409 if channel != &self.channel || previous.is_some_and(|previous| *seq <= previous) {
410 return Err(ValidationError::new(
411 "events must belong to the channel in ascending seq",
412 ));
413 }
414 previous = Some(*seq);
415 }
416 Ok(())
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422#[serde(deny_unknown_fields)]
423pub struct BusLeaseRequestV1 {
424 pub schema: String,
425 pub resource_kind: String,
427 pub resource_ref: String,
429 pub vault_id: String,
430 pub owner_agent_id: String,
431 pub duration_ms: u64,
432}
433
434impl BusLeaseRequestV1 {
435 pub fn validate(&self) -> Result<(), ValidationError> {
436 require_schema(&self.schema, BUS_LEASE_REQUEST_SCHEMA)?;
437 validate_id(&self.resource_kind, "resource_kind")?;
438 super::validate_hex64(&self.resource_ref, "resource_ref")?;
439 super::validate_vault_id(&self.vault_id)?;
440 validate_id(&self.owner_agent_id, "owner_agent_id")?;
441 validate_duration(self.duration_ms)
442 }
443}
444
445fn validate_duration(duration_ms: u64) -> Result<(), ValidationError> {
446 if (1..=MAX_BUS_LEASE_MS).contains(&duration_ms) {
447 Ok(())
448 } else {
449 Err(ValidationError::new("duration_ms must be 1-900000"))
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455#[serde(deny_unknown_fields)]
456pub struct BusLeaseRenewRequestV1 {
457 pub schema: String,
458 pub fencing_token: u64,
459 pub duration_ms: u64,
460}
461
462impl BusLeaseRenewRequestV1 {
463 pub fn validate(&self) -> Result<(), ValidationError> {
464 require_schema(&self.schema, BUS_LEASE_RENEW_REQUEST_SCHEMA)?;
465 validate_duration(self.duration_ms)
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474pub struct BusLeaseV1 {
475 pub schema: String,
476 pub granted: bool,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub lease_id: Option<String>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub fencing_token: Option<u64>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 pub expires_at: Option<UtcTimestamp>,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub holder: Option<BusLeaseHolderV1>,
485 pub server_time: UtcTimestamp,
486}
487
488impl BusLeaseV1 {
489 pub fn validate(&self) -> Result<(), ValidationError> {
490 require_schema(&self.schema, BUS_LEASE_SCHEMA)?;
491 let granted_fields =
492 self.lease_id.is_some() && self.fencing_token.is_some() && self.expires_at.is_some();
493 if self.granted != granted_fields || (self.granted && self.holder.is_some()) {
494 return Err(ValidationError::new(
495 "a granted lease has id, token and expiry; a refused one has none",
496 ));
497 }
498 Ok(())
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn channels_have_three_parts() {
508 assert!(
509 validate_channel("org:org_1/vault:3f6c1e9a0b7d4c2e8f5a1b6c9d0e7f42/general").is_ok()
510 );
511 assert!(validate_channel("org:org_1/vault:prj_1/general").is_err());
512 assert!(validate_channel("org:org_1/general").is_err());
513 assert!(validate_channel("vault:prj_1/org:org_1/general").is_err());
514 }
515
516 #[test]
517 fn unknown_frames_are_tolerated() {
518 let frame: BusFrameV1 = serde_json::from_str(r#"{"t":"future","x":1}"#).unwrap();
519 assert_eq!(frame, BusFrameV1::Unknown);
520 }
521}