use serde::{Deserialize, Serialize};
use super::{require_schema, validate_id, validate_text};
use crate::{UtcTimestamp, ValidationError};
pub const AGENT_PROFILE_LIST_SCHEMA: &str = "leanctx.hive.agent-profile-list/v1";
pub const MAX_ROLES: usize = 8;
pub const MAX_STRENGTHS: usize = 16;
pub const MAX_PROFILES_PAGE: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReputationV1 {
pub score: u64,
pub contributions: u64,
pub weight_pct: u8,
}
impl ReputationV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.weight_pct == 0 || self.weight_pct > 100 {
return Err(ValidationError::new("weight_pct is 1-100"));
}
Ok(())
}
}
pub fn role_ok(role: &str) -> bool {
(1..=32).contains(&role.len())
&& role
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}
pub fn validate_self_report(
display_name: Option<&str>,
model: Option<&str>,
roles: &[String],
) -> Result<(), ValidationError> {
if let Some(name) = display_name {
validate_text(name, 64, "display_name", false)?;
}
if let Some(model) = model {
validate_text(model, 80, "model", false)?;
}
if roles.len() > MAX_ROLES
|| !roles.iter().all(|r| role_ok(r))
|| roles.windows(2).any(|pair| pair[0] >= pair[1])
{
return Err(ValidationError::new(
"up to 8 roles of 1-32 [a-z0-9-], sorted without repeats",
));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentProfileV1 {
pub profile_id: String,
pub agent: String,
pub instance_label: String,
pub owner: String,
pub display_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub strengths: Vec<String>,
pub reputation: ReputationV1,
pub mine: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_seen: Option<UtcTimestamp>,
}
impl AgentProfileV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
validate_id(&self.profile_id, "profile_id")?;
validate_id(&self.agent, "agent")?;
validate_text(&self.instance_label, 100, "instance_label", false)?;
validate_text(&self.owner, 100, "owner", false)?;
validate_self_report(Some(&self.display_name), self.model.as_deref(), &self.roles)?;
if self.strengths.len() > MAX_STRENGTHS {
return Err(ValidationError::new("at most 16 strengths"));
}
for strength in &self.strengths {
validate_text(strength, 64, "strength", false)?;
}
self.reputation.validate()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentProfileListV1 {
pub schema: String,
pub profiles: Vec<AgentProfileV1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_after: Option<String>,
}
impl AgentProfileListV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, AGENT_PROFILE_LIST_SCHEMA)?;
if self.profiles.len() > MAX_PROFILES_PAGE {
return Err(ValidationError::new("profile page is too long"));
}
self.profiles.iter().try_for_each(AgentProfileV1::validate)
}
}