Skip to main content

feagi_agent/sdk/common/
agent_descriptor.rs

1use base64::{engine::general_purpose, Engine as _};
2use feagi_structures::FeagiDataError;
3
4const MAX_MANUFACTURER_LENGTH: usize = 20;
5const MAX_AGENT_NAME_LENGTH: usize = 20;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct AgentDescriptor {
9    instance_id: u32,
10    manufacturer: [u8; MAX_MANUFACTURER_LENGTH], //ASCII
11    agent_name: [u8; MAX_AGENT_NAME_LENGTH],     //ASCII
12    agent_version: u32,
13}
14
15impl AgentDescriptor {
16    /// Total size in bytes of the AgentDescriptor structure
17    pub const SIZE_BYTES: usize = 4 + MAX_MANUFACTURER_LENGTH + MAX_AGENT_NAME_LENGTH + 4; // instance_id + manufacturer + agent_name + agent_version
18
19    pub fn new(
20        instance_id: u32,
21        manufacturer: &str,
22        agent_name: &str,
23        agent_version: u32,
24    ) -> Result<Self, FeagiDataError> {
25        if !manufacturer.is_ascii() || !agent_name.is_ascii() {
26            return Err(FeagiDataError::BadParameters(
27                "ASCII characters only!".to_string(),
28            ));
29        }
30
31        if manufacturer.len() > MAX_MANUFACTURER_LENGTH {
32            return Err(FeagiDataError::BadParameters(format!(
33                "Manufacturer is too long! Max length is {} characters!",
34                MAX_MANUFACTURER_LENGTH
35            )));
36        }
37        if agent_name.len() > MAX_AGENT_NAME_LENGTH {
38            return Err(FeagiDataError::BadParameters(format!(
39                "Agent name is too long! Max length is {} characters!",
40                MAX_AGENT_NAME_LENGTH
41            )));
42        }
43        if agent_version == 0 {
44            return Err(FeagiDataError::BadParameters(
45                "Agent Version cannot be zero!".to_string(),
46            ));
47        }
48
49        // Create fixed-size arrays padded with null bytes
50        let mut manufacturer_bytes = [0u8; MAX_MANUFACTURER_LENGTH];
51        manufacturer_bytes[..manufacturer.len()].copy_from_slice(manufacturer.as_bytes());
52
53        let mut agent_name_bytes = [0u8; MAX_AGENT_NAME_LENGTH];
54        agent_name_bytes[..agent_name.len()].copy_from_slice(agent_name.as_bytes());
55
56        Ok(AgentDescriptor {
57            instance_id,
58            manufacturer: manufacturer_bytes,
59            agent_name: agent_name_bytes,
60            agent_version,
61        })
62    }
63
64    /// Get the instance ID
65    pub fn instance_id(&self) -> u32 {
66        self.instance_id
67    }
68
69    /// Get the manufacturer name as a string slice (without null padding)
70    pub fn manufacturer(&self) -> &str {
71        let end = self
72            .manufacturer
73            .iter()
74            .position(|&b| b == 0)
75            .unwrap_or(MAX_MANUFACTURER_LENGTH);
76        // Safe: we validated ASCII in constructor
77        std::str::from_utf8(&self.manufacturer[..end]).unwrap_or("")
78    }
79
80    /// Get the agent name as a string slice (without null padding)
81    pub fn agent_name(&self) -> &str {
82        let end = self
83            .agent_name
84            .iter()
85            .position(|&b| b == 0)
86            .unwrap_or(MAX_AGENT_NAME_LENGTH);
87        // Safe: we validated ASCII in constructor
88        std::str::from_utf8(&self.agent_name[..end]).unwrap_or("")
89    }
90
91    /// Get the agent version
92    pub fn agent_version(&self) -> u32 {
93        self.agent_version
94    }
95
96    /// Get the raw manufacturer bytes (including null padding)
97    pub fn manufacturer_bytes(&self) -> &[u8; MAX_MANUFACTURER_LENGTH] {
98        &self.manufacturer
99    }
100
101    /// Get the raw agent name bytes (including null padding)
102    pub fn agent_name_bytes(&self) -> &[u8; MAX_AGENT_NAME_LENGTH] {
103        &self.agent_name
104    }
105
106    /// Serialize to a fixed-size byte array
107    #[allow(clippy::wrong_self_convention)]
108    pub fn to_bytes(&self) -> [u8; Self::SIZE_BYTES] {
109        let mut bytes = [0u8; Self::SIZE_BYTES];
110        let mut offset = 0;
111
112        // instance_id (4 bytes, little-endian)
113        bytes[offset..offset + 4].copy_from_slice(&self.instance_id.to_le_bytes());
114        offset += 4;
115
116        // manufacturer (MAX_MANUFACTURER_LENGTH bytes)
117        bytes[offset..offset + MAX_MANUFACTURER_LENGTH].copy_from_slice(&self.manufacturer);
118        offset += MAX_MANUFACTURER_LENGTH;
119
120        // agent_name (MAX_AGENT_NAME_LENGTH bytes)
121        bytes[offset..offset + MAX_AGENT_NAME_LENGTH].copy_from_slice(&self.agent_name);
122        offset += MAX_AGENT_NAME_LENGTH;
123
124        // agent_version (4 bytes, little-endian)
125        bytes[offset..offset + 4].copy_from_slice(&self.agent_version.to_le_bytes());
126
127        bytes
128    }
129
130    /// Deserialize from a fixed-size byte array
131    pub fn from_bytes(bytes: &[u8; Self::SIZE_BYTES]) -> Result<Self, FeagiDataError> {
132        let mut offset = 0;
133
134        // instance_id (4 bytes, little-endian)
135        let instance_id = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
136        offset += 4;
137
138        // manufacturer (MAX_MANUFACTURER_LENGTH bytes)
139        let mut manufacturer = [0u8; MAX_MANUFACTURER_LENGTH];
140        manufacturer.copy_from_slice(&bytes[offset..offset + MAX_MANUFACTURER_LENGTH]);
141        offset += MAX_MANUFACTURER_LENGTH;
142
143        // Validate manufacturer is ASCII
144        for &b in &manufacturer {
145            if b != 0 && !b.is_ascii() {
146                return Err(FeagiDataError::DeserializationError(
147                    "Manufacturer contains non-ASCII characters".to_string(),
148                ));
149            }
150        }
151
152        // agent_name (MAX_AGENT_NAME_LENGTH bytes)
153        let mut agent_name = [0u8; MAX_AGENT_NAME_LENGTH];
154        agent_name.copy_from_slice(&bytes[offset..offset + MAX_AGENT_NAME_LENGTH]);
155        offset += MAX_AGENT_NAME_LENGTH;
156
157        // Validate agent_name is ASCII
158        for &b in &agent_name {
159            if b != 0 && !b.is_ascii() {
160                return Err(FeagiDataError::DeserializationError(
161                    "Agent name contains non-ASCII characters".to_string(),
162                ));
163            }
164        }
165
166        // agent_version (4 bytes, little-endian)
167        let agent_version = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
168
169        if agent_version == 0 {
170            return Err(FeagiDataError::DeserializationError(
171                "Agent Version cannot be zero!".to_string(),
172            ));
173        }
174
175        Ok(AgentDescriptor {
176            instance_id,
177            manufacturer,
178            agent_name,
179            agent_version,
180        })
181    }
182
183    /// Encode the AgentDescriptor to a base64 string
184    pub fn to_base64(&self) -> String {
185        general_purpose::STANDARD.encode(self.to_bytes())
186    }
187
188    /// Try to decode an AgentDescriptor from a base64 string
189    pub fn try_from_base64(encoded: &str) -> Result<Self, FeagiDataError> {
190        let decoded = general_purpose::STANDARD.decode(encoded).map_err(|e| {
191            FeagiDataError::DeserializationError(format!("Failed to decode base64 string: {}", e))
192        })?;
193
194        if decoded.len() != Self::SIZE_BYTES {
195            return Err(FeagiDataError::DeserializationError(format!(
196                "Invalid AgentDescriptor length: expected {} bytes, got {}",
197                Self::SIZE_BYTES,
198                decoded.len()
199            )));
200        }
201
202        let mut bytes = [0u8; Self::SIZE_BYTES];
203        bytes.copy_from_slice(&decoded);
204        Self::from_bytes(&bytes)
205    }
206}