1use serde::{Deserialize, Serialize};
28
29use super::{require_schema, validate_id, validate_text};
30use crate::{UtcTimestamp, ValidationError};
31
32pub const AGENT_PROFILE_LIST_SCHEMA: &str = "leanctx.hive.agent-profile-list/v1";
33pub const HANDLE_SCHEMA: &str = "leanctx.hive.handle/v1";
34pub const MAX_ROLES: usize = 8;
35pub const MAX_STRENGTHS: usize = 16;
36pub const MAX_PROFILES_PAGE: usize = 200;
37
38pub const SWARM_LIMITS: [&str; 6] = [
40 "open_swarms",
41 "active_claims",
42 "results_per_day",
43 "verifications_per_day",
44 "reports_per_day",
45 "messages_per_hour",
46];
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub struct ReputationV1 {
51 pub score: u64,
53 pub contributions: u64,
55 pub weight_pct: u8,
58}
59
60impl ReputationV1 {
61 pub fn validate(&self) -> Result<(), ValidationError> {
62 if self.weight_pct == 0 || self.weight_pct > 100 {
63 return Err(ValidationError::new("weight_pct is 1-100"));
64 }
65 Ok(())
66 }
67}
68
69pub fn role_ok(role: &str) -> bool {
71 (1..=32).contains(&role.len())
72 && role
73 .bytes()
74 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
75}
76
77pub fn validate_self_report(
79 display_name: Option<&str>,
80 model: Option<&str>,
81 roles: &[String],
82) -> Result<(), ValidationError> {
83 if let Some(name) = display_name {
84 validate_text(name, 64, "display_name", false)?;
85 }
86 if let Some(model) = model {
87 validate_text(model, 80, "model", false)?;
88 }
89 if roles.len() > MAX_ROLES
90 || !roles.iter().all(|r| role_ok(r))
91 || roles.windows(2).any(|pair| pair[0] >= pair[1])
92 {
93 return Err(ValidationError::new(
94 "up to 8 roles of 1-32 [a-z0-9-], sorted without repeats",
95 ));
96 }
97 Ok(())
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct PresenceProfileV1 {
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub display_name: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub model: Option<String>,
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub roles: Vec<String>,
110}
111
112impl PresenceProfileV1 {
113 pub fn validate(&self) -> Result<(), ValidationError> {
114 validate_self_report(
115 self.display_name.as_deref(),
116 self.model.as_deref(),
117 &self.roles,
118 )
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct AgentProfileV1 {
125 pub profile_id: String,
126 pub agent: String,
128 pub instance_label: String,
129 pub owner: String,
131 pub display_name: String,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub model: Option<String>,
135 #[serde(default)]
137 pub roles: Vec<String>,
138 #[serde(default)]
140 pub strengths: Vec<String>,
141 pub reputation: ReputationV1,
143 pub mine: bool,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub last_seen: Option<UtcTimestamp>,
147}
148
149impl AgentProfileV1 {
150 pub fn validate(&self) -> Result<(), ValidationError> {
151 validate_id(&self.profile_id, "profile_id")?;
152 validate_id(&self.agent, "agent")?;
153 validate_text(&self.instance_label, 100, "instance_label", false)?;
154 validate_text(&self.owner, 100, "owner", false)?;
155 validate_self_report(Some(&self.display_name), self.model.as_deref(), &self.roles)?;
156 if self.strengths.len() > MAX_STRENGTHS {
157 return Err(ValidationError::new("at most 16 strengths"));
158 }
159 for strength in &self.strengths {
160 validate_text(strength, 64, "strength", false)?;
161 }
162 self.reputation.validate()
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct AgentProfileListV1 {
169 pub schema: String,
170 pub profiles: Vec<AgentProfileV1>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub next_after: Option<String>,
174}
175
176impl AgentProfileListV1 {
177 pub fn validate(&self) -> Result<(), ValidationError> {
178 require_schema(&self.schema, AGENT_PROFILE_LIST_SCHEMA)?;
179 if self.profiles.len() > MAX_PROFILES_PAGE {
180 return Err(ValidationError::new("profile page is too long"));
181 }
182 self.profiles.iter().try_for_each(AgentProfileV1::validate)
183 }
184}
185
186pub fn handle_ok(handle: &str) -> bool {
188 let b = handle.as_bytes();
189 (3..=32).contains(&b.len())
190 && (b[0].is_ascii_lowercase() || b[0].is_ascii_digit())
191 && b.iter()
192 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-')
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct HandleV1 {
198 pub schema: String,
199 pub handle: String,
200 pub reputation: ReputationV1,
202 #[serde(default)]
203 pub strengths: Vec<String>,
204 pub since: UtcTimestamp,
205}
206
207impl HandleV1 {
208 pub fn validate(&self) -> Result<(), ValidationError> {
209 require_schema(&self.schema, HANDLE_SCHEMA)?;
210 if !handle_ok(&self.handle) {
211 return Err(ValidationError::new("handle is malformed"));
212 }
213 if self.strengths.len() > MAX_STRENGTHS {
214 return Err(ValidationError::new("at most 16 strengths"));
215 }
216 for strength in &self.strengths {
217 validate_text(strength, 64, "strength", false)?;
218 }
219 self.reputation.validate()
220 }
221}