clawft_kernel/
agent_registry.rs1use std::sync::Arc;
12
13use chrono::{DateTime, Utc};
14use dashmap::DashMap;
15
16#[derive(Debug, Clone)]
18pub struct RegisteredAgent {
19 pub agent_id: String,
21 pub name: String,
23 pub pubkey: [u8; 32],
25 pub registered_at: DateTime<Utc>,
27}
28
29#[derive(Debug, Default, Clone)]
37pub struct AgentRegistry {
38 inner: Arc<DashMap<String, RegisteredAgent>>,
39 by_name: Arc<DashMap<String, String>>,
43}
44
45impl AgentRegistry {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn register(&self, name: String, pubkey: [u8; 32]) -> RegisteredAgent {
63 let agent_id = uuid::Uuid::new_v4().to_string();
64 let entry = RegisteredAgent {
65 agent_id: agent_id.clone(),
66 name: name.clone(),
67 pubkey,
68 registered_at: Utc::now(),
69 };
70 self.inner.insert(agent_id.clone(), entry.clone());
71 self.by_name
74 .entry(name)
75 .or_insert_with(|| agent_id);
76 entry
77 }
78
79 pub fn get(&self, agent_id: &str) -> Option<RegisteredAgent> {
81 self.inner.get(agent_id).map(|e| e.clone())
82 }
83
84 pub fn get_by_name(&self, name: &str) -> Option<RegisteredAgent> {
86 let id = self.by_name.get(name)?;
87 self.get(id.value())
88 }
89
90 pub fn get_or_register(&self, name: String, pubkey: [u8; 32]) -> RegisteredAgent {
96 if let Some(existing) = self.get_by_name(&name) {
97 return existing;
98 }
99 let agent_id = uuid::Uuid::new_v4().to_string();
100 let entry = RegisteredAgent {
101 agent_id: agent_id.clone(),
102 name: name.clone(),
103 pubkey,
104 registered_at: Utc::now(),
105 };
106 let winner_id = match self.by_name.entry(name.clone()) {
109 dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(),
110 dashmap::mapref::entry::Entry::Vacant(v) => {
111 v.insert(agent_id.clone());
112 agent_id.clone()
113 }
114 };
115 if winner_id == agent_id {
116 self.inner.insert(agent_id, entry.clone());
117 entry
118 } else {
119 self.get(&winner_id).unwrap_or(entry)
122 }
123 }
124
125 pub fn len(&self) -> usize {
127 self.inner.len()
128 }
129
130 pub fn is_empty(&self) -> bool {
132 self.inner.is_empty()
133 }
134
135 pub fn list(&self) -> Vec<RegisteredAgent> {
137 self.inner.iter().map(|e| e.value().clone()).collect()
138 }
139}
140
141pub fn register_payload(name: &str, pubkey: &[u8; 32], ts: u64) -> Vec<u8> {
150 let mut buf = Vec::with_capacity(9 + name.len() + 1 + 32 + 1 + 8);
151 buf.extend_from_slice(b"register\0");
152 buf.extend_from_slice(name.as_bytes());
153 buf.push(0);
154 buf.extend_from_slice(pubkey);
155 buf.push(0);
156 buf.extend_from_slice(&ts.to_le_bytes());
157 buf
158}
159
160pub fn publish_payload(topic: &str, message: &str, ts: u64, actor_id: &str) -> Vec<u8> {
165 let mut buf =
166 Vec::with_capacity(12 + topic.len() + 1 + message.len() + 1 + 8 + 1 + actor_id.len());
167 buf.extend_from_slice(b"ipc.publish\0");
168 buf.extend_from_slice(topic.as_bytes());
169 buf.push(0);
170 buf.extend_from_slice(message.as_bytes());
171 buf.push(0);
172 buf.extend_from_slice(&ts.to_le_bytes());
173 buf.push(0);
174 buf.extend_from_slice(actor_id.as_bytes());
175 buf
176}
177
178pub fn subscribe_payload(topic: &str, ts: u64, actor_id: &str) -> Vec<u8> {
183 let mut buf = Vec::with_capacity(14 + topic.len() + 1 + 8 + 1 + actor_id.len());
184 buf.extend_from_slice(b"ipc.subscribe\0");
185 buf.extend_from_slice(topic.as_bytes());
186 buf.push(0);
187 buf.extend_from_slice(&ts.to_le_bytes());
188 buf.push(0);
189 buf.extend_from_slice(actor_id.as_bytes());
190 buf
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn register_and_lookup() {
199 let reg = AgentRegistry::new();
200 let entry = reg.register("python-bridge".into(), [7u8; 32]);
201 assert_eq!(reg.len(), 1);
202 let fetched = reg.get(&entry.agent_id).unwrap();
203 assert_eq!(fetched.name, "python-bridge");
204 assert_eq!(fetched.pubkey, [7u8; 32]);
205 }
206
207 #[test]
208 fn register_assigns_unique_ids() {
209 let reg = AgentRegistry::new();
210 let a = reg.register("a".into(), [1u8; 32]);
211 let b = reg.register("b".into(), [2u8; 32]);
212 assert_ne!(a.agent_id, b.agent_id);
213 }
214
215 #[test]
216 fn get_or_register_is_idempotent_by_name() {
217 let reg = AgentRegistry::new();
218 let a = reg.get_or_register("agent.chat:user:alice".into(), [1u8; 32]);
219 let b = reg.get_or_register("agent.chat:user:alice".into(), [1u8; 32]);
220 assert_eq!(a.agent_id, b.agent_id);
221 assert_eq!(reg.len(), 1);
222 }
223
224 #[test]
225 fn get_or_register_isolates_callers() {
226 let reg = AgentRegistry::new();
227 let a = reg.get_or_register("agent.chat:user:alice".into(), [1u8; 32]);
228 let b = reg.get_or_register("agent.chat:user:bob".into(), [1u8; 32]);
229 assert_ne!(a.agent_id, b.agent_id);
230 assert_eq!(reg.len(), 2);
231 assert_eq!(
232 reg.get_by_name("agent.chat:user:alice")
233 .unwrap()
234 .agent_id,
235 a.agent_id
236 );
237 assert_eq!(
238 reg.get_by_name("agent.chat:user:bob").unwrap().agent_id,
239 b.agent_id
240 );
241 }
242
243 #[test]
244 fn payload_layouts_are_stable() {
245 let pk = [0xAAu8; 32];
246 let p = register_payload("foo", &pk, 42);
247 assert_eq!(&p[..9], b"register\0");
249 assert_eq!(&p[9..12], b"foo");
250 assert_eq!(p[12], 0);
251 assert_eq!(&p[13..45], &pk);
252 }
253
254 #[test]
255 fn publish_payload_round() {
256 let p1 = publish_payload("t1", "hi", 100, "aid");
257 let p2 = publish_payload("t1", "hi", 100, "aid");
258 assert_eq!(p1, p2);
259 let p3 = publish_payload("t1", "hi", 101, "aid");
260 assert_ne!(p1, p3, "ts must affect the payload");
261 }
262}