use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use super::error::HiveErrorDetailV1;
use super::{require_schema, validate_id, validate_text};
use crate::{UtcTimestamp, ValidationError};
pub const BUS_POLL_SCHEMA: &str = "leanctx.hive.bus-poll/v1";
pub const BUS_EVENTS_SCHEMA: &str = "leanctx.hive.bus-events/v1";
pub const BUS_LEASE_REQUEST_SCHEMA: &str = "leanctx.hive.bus-lease-request/v1";
pub const BUS_LEASE_RENEW_REQUEST_SCHEMA: &str = "leanctx.hive.bus-lease-renew-request/v1";
pub const BUS_LEASE_SCHEMA: &str = "leanctx.hive.bus-lease/v1";
pub const MAX_BUS_BODY_BYTES: usize = 64 * 1024;
pub const MAX_BUS_EVENTS_PAGE: usize = 500;
pub const MAX_BUS_LEASE_MS: u64 = 15 * 60 * 1000;
pub fn validate_channel(channel: &str) -> Result<(), ValidationError> {
let mut parts = channel.splitn(3, '/');
let org = parts.next().and_then(|part| part.strip_prefix("org:"));
let vault = parts.next().and_then(|part| part.strip_prefix("vault:"));
let name = parts.next();
match (org, vault, name) {
(Some(org), Some(vault), Some(name)) => {
validate_id(org, "channel org")?;
super::validate_vault_id(vault)?;
validate_id(name, "channel name")
}
_ => Err(ValidationError::new(
"channel must be org:<org>/vault:<vault>/<name>",
)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PresenceState {
Active,
Idle,
Closed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BusPriority {
Low,
Normal,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BusPrivacy {
Public,
Team,
Private,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ControlAction {
Hold,
Veto,
Stop,
Resume,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusAgentV1 {
pub agent_id: String,
pub role: String,
pub vault_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusLeaseHolderV1 {
pub agent_id: String,
pub instance_id: String,
pub expires_at: UtcTimestamp,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "t")]
pub enum BusFrameV1 {
#[serde(rename = "hello")]
Hello {
instance_id: String,
agents: Vec<BusAgentV1>,
#[serde(default)]
cursors: BTreeMap<String, u64>,
},
#[serde(rename = "presence")]
Presence {
agent_id: String,
state: PresenceState,
#[serde(default, skip_serializing_if = "Option::is_none")]
profile: Option<super::profiles::PresenceProfileV1>,
},
#[serde(rename = "publish")]
Publish {
client_msg_id: String,
channel: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
to: Option<String>,
from: String,
kind: String,
priority: BusPriority,
privacy: BusPrivacy,
body: Value,
},
#[serde(rename = "ack")]
Ack { channel: String, seq: u64 },
#[serde(rename = "published")]
Published {
client_msg_id: String,
channel: String,
seq: u64,
server_time: UtcTimestamp,
},
#[serde(rename = "deliver")]
Deliver {
seq: u64,
channel: String,
from: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
to: Option<String>,
kind: String,
priority: BusPriority,
privacy: BusPrivacy,
body: Value,
server_time: UtcTimestamp,
},
#[serde(rename = "presence_update")]
PresenceUpdate {
agent_id: String,
instance_id: String,
role: String,
vault_id: String,
state: PresenceState,
server_time: UtcTimestamp,
#[serde(default, skip_serializing_if = "Option::is_none")]
profile_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
profile: Option<super::profiles::PresenceProfileV1>,
},
#[serde(rename = "control")]
Control {
control_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_ref: Option<String>,
action: ControlAction,
reason: String,
issued_by: String,
seq: u64,
server_time: UtcTimestamp,
},
#[serde(rename = "handoff.offer")]
HandoffOffer {
handoff_id: String,
channel: String,
from: String,
to: String,
bundle_ref: String,
deadline: UtcTimestamp,
},
#[serde(rename = "handoff.ack")]
HandoffAck { handoff_id: String, by: String },
#[serde(rename = "handoff.reject")]
HandoffReject {
handoff_id: String,
by: String,
reason: String,
},
#[serde(rename = "message")]
Message(super::messages::AgentMessageV1),
#[serde(rename = "message.stored")]
MessageStored(super::messages::MessageStoredV1),
#[serde(rename = "message.deliver")]
MessageDeliver(super::messages::MessageDeliveryV1),
#[serde(rename = "message.receipt")]
MessageReceipt(super::messages::MessageReceiptV1),
#[serde(rename = "message.status")]
MessageStatus(super::messages::MessageStatusV1),
#[serde(rename = "task.event")]
TaskEvent(super::messages::TaskEventV1),
#[serde(rename = "knowledge.event")]
KnowledgeEvent(super::knowledge::KnowledgeEventV1),
#[serde(rename = "ping")]
Ping { nonce: u64 },
#[serde(rename = "pong")]
Pong { nonce: u64 },
#[serde(rename = "error")]
Error {
#[serde(default, skip_serializing_if = "Option::is_none")]
client_msg_id: Option<String>,
error: HiveErrorDetailV1,
},
#[serde(other)]
Unknown,
}
impl BusFrameV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
match self {
Self::Hello {
instance_id,
agents,
cursors,
} => {
validate_id(instance_id, "instance_id")?;
if agents.len() > 64 || cursors.len() > 256 {
return Err(ValidationError::new("hello lists are too long"));
}
for agent in agents {
validate_id(&agent.agent_id, "agent_id")?;
validate_text(&agent.role, 64, "role", false)?;
super::validate_vault_id(&agent.vault_id)?;
}
cursors
.keys()
.try_for_each(|channel| validate_channel(channel))
}
Self::Presence {
agent_id, profile, ..
} => {
validate_id(agent_id, "agent_id")?;
profile
.as_ref()
.map_or(Ok(()), super::profiles::PresenceProfileV1::validate)
}
Self::Publish {
client_msg_id,
channel,
to,
from,
kind,
body,
..
} => {
validate_id(client_msg_id, "client_msg_id")?;
validate_channel(channel)?;
validate_id(from, "from")?;
if let Some(to) = to {
validate_id(to, "to")?;
}
validate_id(kind, "kind")?;
validate_body(body)
}
Self::Ack { channel, .. } => validate_channel(channel),
Self::HandoffOffer {
handoff_id,
channel,
from,
to,
bundle_ref,
..
} => {
validate_id(handoff_id, "handoff_id")?;
validate_channel(channel)?;
validate_id(from, "from")?;
validate_id(to, "to")?;
validate_text(bundle_ref, 512, "bundle_ref", false)
}
Self::HandoffAck { handoff_id, by } => {
validate_id(handoff_id, "handoff_id")?;
validate_id(by, "by")
}
Self::HandoffReject {
handoff_id,
by,
reason,
} => {
validate_id(handoff_id, "handoff_id")?;
validate_id(by, "by")?;
validate_text(reason, 500, "reason", false)
}
Self::Message(message) => message.validate(),
Self::MessageReceipt(receipt) => receipt.validate(),
Self::Ping { .. } | Self::Pong { .. } => Ok(()),
Self::Published { .. }
| Self::Deliver { .. }
| Self::PresenceUpdate { .. }
| Self::Control { .. }
| Self::MessageStored(_)
| Self::MessageDeliver(_)
| Self::MessageStatus(_)
| Self::TaskEvent(_)
| Self::KnowledgeEvent(_)
| Self::Error { .. }
| Self::Unknown => Err(ValidationError::new("not a client frame")),
}
}
}
fn validate_body(body: &Value) -> Result<(), ValidationError> {
let size = serde_json::to_vec(body)
.map_err(|error| ValidationError::new(format!("body: {error}")))?
.len();
if size > MAX_BUS_BODY_BYTES {
return Err(ValidationError::new("bus body exceeds 64 KiB"));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BusPollV1 {
pub schema: String,
pub frames: Vec<BusFrameV1>,
pub cursor: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BusEventsV1 {
pub schema: String,
pub channel: String,
pub events: Vec<BusFrameV1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_after_seq: Option<u64>,
}
impl BusEventsV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, BUS_EVENTS_SCHEMA)?;
validate_channel(&self.channel)?;
if self.events.len() > MAX_BUS_EVENTS_PAGE {
return Err(ValidationError::new("event page is too long"));
}
let mut previous = None;
for event in &self.events {
let BusFrameV1::Deliver { seq, channel, .. } = event else {
return Err(ValidationError::new("catch-up events are deliver frames"));
};
if channel != &self.channel || previous.is_some_and(|previous| *seq <= previous) {
return Err(ValidationError::new(
"events must belong to the channel in ascending seq",
));
}
previous = Some(*seq);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BusLeaseRequestV1 {
pub schema: String,
pub resource_kind: String,
pub resource_ref: String,
pub vault_id: String,
pub owner_agent_id: String,
pub duration_ms: u64,
}
impl BusLeaseRequestV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, BUS_LEASE_REQUEST_SCHEMA)?;
validate_id(&self.resource_kind, "resource_kind")?;
super::validate_hex64(&self.resource_ref, "resource_ref")?;
super::validate_vault_id(&self.vault_id)?;
validate_id(&self.owner_agent_id, "owner_agent_id")?;
validate_duration(self.duration_ms)
}
}
fn validate_duration(duration_ms: u64) -> Result<(), ValidationError> {
if (1..=MAX_BUS_LEASE_MS).contains(&duration_ms) {
Ok(())
} else {
Err(ValidationError::new("duration_ms must be 1-900000"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BusLeaseRenewRequestV1 {
pub schema: String,
pub fencing_token: u64,
pub duration_ms: u64,
}
impl BusLeaseRenewRequestV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, BUS_LEASE_RENEW_REQUEST_SCHEMA)?;
validate_duration(self.duration_ms)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusLeaseV1 {
pub schema: String,
pub granted: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lease_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fencing_token: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<UtcTimestamp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub holder: Option<BusLeaseHolderV1>,
pub server_time: UtcTimestamp,
}
impl BusLeaseV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, BUS_LEASE_SCHEMA)?;
let granted_fields =
self.lease_id.is_some() && self.fencing_token.is_some() && self.expires_at.is_some();
if self.granted != granted_fields || (self.granted && self.holder.is_some()) {
return Err(ValidationError::new(
"a granted lease has id, token and expiry; a refused one has none",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channels_have_three_parts() {
assert!(
validate_channel("org:org_1/vault:3f6c1e9a0b7d4c2e8f5a1b6c9d0e7f42/general").is_ok()
);
assert!(validate_channel("org:org_1/vault:prj_1/general").is_err());
assert!(validate_channel("org:org_1/general").is_err());
assert!(validate_channel("vault:prj_1/org:org_1/general").is_err());
}
#[test]
fn unknown_frames_are_tolerated() {
let frame: BusFrameV1 = serde_json::from_str(r#"{"t":"future","x":1}"#).unwrap();
assert_eq!(frame, BusFrameV1::Unknown);
}
}