1use std::collections::HashMap;
4use std::panic;
5use std::sync::{Arc, Mutex};
6
7use crate::error::HawkError;
8
9pub type Result<T> = std::result::Result<T, TalonError>;
10
11#[derive(Debug, thiserror::Error)]
12pub enum TalonError {
13 #[error("signature verification failed for '{0}'")]
14 InvalidSignature(String),
15 #[error("talon not found: '{0}'")]
16 NotFound(String),
17 #[error("talon already installed: '{0}'")]
18 AlreadyInstalled(String),
19 #[error("talon lifecycle error: {0}")]
20 Lifecycle(String),
21 #[error("talon panicked: {0}")]
22 Panicked(String),
23}
24
25impl From<TalonError> for HawkError {
26 fn from(e: TalonError) -> Self {
27 HawkError::Config(e.to_string())
28 }
29}
30
31#[derive(Debug, Clone)]
32pub struct Capability {
33 pub name: String,
34 pub description: String,
35}
36
37#[derive(Debug, Clone, Default)]
38pub struct TalonConfig {
39 pub settings: HashMap<String, String>,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum TalonStatus {
44 Loaded,
45 Unloaded,
46 Failed(String),
47}
48
49#[derive(Debug, Clone)]
50pub struct TalonRecord {
51 pub name: String,
52 pub version: String,
53 pub status: TalonStatus,
54 pub capabilities: Vec<Capability>,
55 pub signature: String,
56}
57
58pub trait Talon: Send + Sync {
59 fn name(&self) -> &str;
60 fn version(&self) -> &str;
61 fn load(&mut self) -> Result<()>;
62 fn unload(&mut self) -> Result<()>;
63 fn configure(&mut self, config: TalonConfig) -> Result<()>;
64 fn capabilities(&self) -> Vec<Capability>;
65}
66
67fn expected_signature(name: &str, version: &str) -> String {
73 use sha2::{Digest, Sha256};
74 let mut h = Sha256::new();
75 h.update(format!("{name}:{version}").as_bytes());
76 hex::encode(h.finalize())
77}
78
79fn verify_signature(name: &str, version: &str, signature: &str) -> bool {
80 expected_signature(name, version) == signature
81}
82
83pub fn make_signature(name: &str, version: &str) -> String {
84 expected_signature(name, version)
85}
86
87pub struct TalonRegistry {
90 records: Arc<Mutex<HashMap<String, TalonRecord>>>,
91 instances: Arc<Mutex<HashMap<String, Box<dyn Talon>>>>,
92}
93
94impl TalonRegistry {
95 pub fn new() -> Self {
96 Self {
97 records: Arc::new(Mutex::new(HashMap::new())),
98 instances: Arc::new(Mutex::new(HashMap::new())),
99 }
100 }
101
102 pub fn install(&self, name: &str, version: &str, signature: &str, capabilities: Vec<Capability>) -> Result<()> {
103 if !verify_signature(name, version, signature) {
104 eprintln!("SECURITY WARNING: signature verification failed for talon '{name}'. Installation rejected.");
105 return Err(TalonError::InvalidSignature(name.to_string()));
106 }
107 let mut records = self.records.lock().unwrap();
108 if records.contains_key(name) {
109 return Err(TalonError::AlreadyInstalled(name.to_string()));
110 }
111 records.insert(name.to_string(), TalonRecord {
112 name: name.to_string(),
113 version: version.to_string(),
114 status: TalonStatus::Unloaded,
115 capabilities,
116 signature: signature.to_string(),
117 });
118 Ok(())
119 }
120
121 pub fn load(&self, name: &str) -> Result<()> {
122 let mut instances = self.instances.lock().unwrap();
123 let mut records = self.records.lock().unwrap();
124
125 let record = records.get_mut(name).ok_or_else(|| TalonError::NotFound(name.to_string()))?;
126
127 if let Some(t) = instances.get_mut(name) {
128 let t_ptr = t.as_mut() as *mut dyn Talon;
129 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
130 unsafe { &mut *t_ptr }.load()
132 }));
133 match result {
134 Ok(Ok(())) => { record.status = TalonStatus::Loaded; Ok(()) }
135 Ok(Err(e)) => {
136 let msg = e.to_string();
137 record.status = TalonStatus::Failed(msg.clone());
138 Err(TalonError::Lifecycle(msg))
139 }
140 Err(_) => {
141 let msg = "talon panicked during load".to_string();
142 record.status = TalonStatus::Failed(msg.clone());
143 Err(TalonError::Panicked(msg))
144 }
145 }
146 } else {
147 record.status = TalonStatus::Loaded;
149 Ok(())
150 }
151 }
152
153 pub fn unload(&self, name: &str) -> Result<()> {
154 let mut instances = self.instances.lock().unwrap();
155 let mut records = self.records.lock().unwrap();
156
157 let record = records.get_mut(name).ok_or_else(|| TalonError::NotFound(name.to_string()))?;
158
159 if let Some(t) = instances.get_mut(name) {
160 let t_ptr = t.as_mut() as *mut dyn Talon;
161 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| unsafe { &mut *t_ptr }.unload()));
162 match result {
163 Ok(Ok(())) => {}
164 Ok(Err(e)) => {
165 let msg = e.to_string();
166 record.status = TalonStatus::Failed(msg.clone());
167 return Err(TalonError::Lifecycle(msg));
168 }
169 Err(_) => {
170 let msg = "talon panicked during unload".to_string();
171 record.status = TalonStatus::Failed(msg.clone());
172 return Err(TalonError::Panicked(msg));
173 }
174 }
175 }
176
177 record.status = TalonStatus::Unloaded;
178 Ok(())
179 }
180
181 pub fn list(&self) -> Vec<TalonRecord> {
182 self.records.lock().unwrap().values().cloned().collect()
183 }
184
185 pub fn get_capabilities(&self, name: &str) -> Option<Vec<Capability>> {
186 self.records.lock().unwrap().get(name).map(|r| r.capabilities.clone())
187 }
188
189 pub fn is_authorized(agent_manifest_talons: &[String], talon_name: &str) -> bool {
190 agent_manifest_talons.iter().any(|t| t == talon_name)
191 }
192
193 pub fn register_instance(&self, talon: Box<dyn Talon>) -> Result<()> {
194 let name = talon.name().to_string();
195 self.instances.lock().unwrap().insert(name, talon);
196 Ok(())
197 }
198}
199
200impl Default for TalonRegistry {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 struct GoodTalon { loaded: bool }
211 impl GoodTalon { fn new() -> Self { Self { loaded: false } } }
212 impl Talon for GoodTalon {
213 fn name(&self) -> &str { "good-talon" }
214 fn version(&self) -> &str { "1.0.0" }
215 fn load(&mut self) -> Result<()> { self.loaded = true; Ok(()) }
216 fn unload(&mut self) -> Result<()> { self.loaded = false; Ok(()) }
217 fn configure(&mut self, _: TalonConfig) -> Result<()> { Ok(()) }
218 fn capabilities(&self) -> Vec<Capability> {
219 vec![Capability { name: "browse".to_string(), description: "web browsing".to_string() }]
220 }
221 }
222
223 struct PanickingTalon;
224 impl Talon for PanickingTalon {
225 fn name(&self) -> &str { "panic-talon" }
226 fn version(&self) -> &str { "0.1.0" }
227 fn load(&mut self) -> Result<()> { panic!("intentional panic for isolation test"); }
228 fn unload(&mut self) -> Result<()> { Ok(()) }
229 fn configure(&mut self, _: TalonConfig) -> Result<()> { Ok(()) }
230 fn capabilities(&self) -> Vec<Capability> { vec![] }
231 }
232
233 struct FailingTalon;
234 impl Talon for FailingTalon {
235 fn name(&self) -> &str { "fail-talon" }
236 fn version(&self) -> &str { "0.1.0" }
237 fn load(&mut self) -> Result<()> { Err(TalonError::Lifecycle("load failed".to_string())) }
238 fn unload(&mut self) -> Result<()> { Ok(()) }
239 fn configure(&mut self, _: TalonConfig) -> Result<()> { Ok(()) }
240 fn capabilities(&self) -> Vec<Capability> { vec![] }
241 }
242
243 fn install_good(registry: &TalonRegistry) {
244 let sig = make_signature("good-talon", "1.0.0");
245 registry.install("good-talon", "1.0.0", &sig, vec![
246 Capability { name: "browse".to_string(), description: "web browsing".to_string() },
247 ]).unwrap();
248 }
249
250 #[test]
251 fn load_and_unload_lifecycle() {
252 let registry = TalonRegistry::new();
253 install_good(®istry);
254 registry.register_instance(Box::new(GoodTalon::new())).unwrap();
255 registry.load("good-talon").unwrap();
256 assert_eq!(registry.records.lock().unwrap()["good-talon"].status, TalonStatus::Loaded);
257 registry.unload("good-talon").unwrap();
258 assert_eq!(registry.records.lock().unwrap()["good-talon"].status, TalonStatus::Unloaded);
259 }
260
261 #[test]
262 fn valid_signature_accepted() {
263 let registry = TalonRegistry::new();
264 let sig = make_signature("my-talon", "2.0.0");
265 assert!(registry.install("my-talon", "2.0.0", &sig, vec![]).is_ok());
266 }
267
268 #[test]
269 fn invalid_signature_rejected() {
270 let registry = TalonRegistry::new();
271 assert!(matches!(registry.install("my-talon", "2.0.0", "bad-sig", vec![]), Err(TalonError::InvalidSignature(_))));
272 }
273
274 #[test]
275 fn wrong_version_signature_rejected() {
276 let registry = TalonRegistry::new();
277 let sig = make_signature("my-talon", "1.0.0");
278 assert!(matches!(registry.install("my-talon", "2.0.0", &sig, vec![]), Err(TalonError::InvalidSignature(_))));
279 }
280
281 #[test]
282 fn panicking_talon_does_not_crash_process() {
283 let registry = TalonRegistry::new();
284 let sig = make_signature("panic-talon", "0.1.0");
285 registry.install("panic-talon", "0.1.0", &sig, vec![]).unwrap();
286 registry.register_instance(Box::new(PanickingTalon)).unwrap();
287 let result = registry.load("panic-talon");
288 assert!(matches!(result, Err(TalonError::Panicked(_))));
289 assert!(matches!(registry.records.lock().unwrap()["panic-talon"].status, TalonStatus::Failed(_)));
290 }
291
292 #[test]
293 fn failing_talon_is_marked_failed() {
294 let registry = TalonRegistry::new();
295 let sig = make_signature("fail-talon", "0.1.0");
296 registry.install("fail-talon", "0.1.0", &sig, vec![]).unwrap();
297 registry.register_instance(Box::new(FailingTalon)).unwrap();
298 let result = registry.load("fail-talon");
299 assert!(matches!(result, Err(TalonError::Lifecycle(_))));
300 assert!(matches!(registry.records.lock().unwrap()["fail-talon"].status, TalonStatus::Failed(_)));
301 }
302
303 #[test]
304 fn authorized_agent_can_use_talon() {
305 let declared = vec!["browser-talon".to_string(), "github-talon".to_string()];
306 assert!(TalonRegistry::is_authorized(&declared, "browser-talon"));
307 }
308
309 #[test]
310 fn unauthorized_agent_cannot_use_talon() {
311 let declared = vec!["github-talon".to_string()];
312 assert!(!TalonRegistry::is_authorized(&declared, "browser-talon"));
313 }
314
315 #[test]
316 fn empty_manifest_talons_denies_all() {
317 assert!(!TalonRegistry::is_authorized(&[], "any-talon"));
318 }
319
320 #[test]
321 fn get_capabilities_returns_installed_caps() {
322 let registry = TalonRegistry::new();
323 install_good(®istry);
324 let caps = registry.get_capabilities("good-talon").unwrap();
325 assert_eq!(caps.len(), 1);
326 assert_eq!(caps[0].name, "browse");
327 }
328
329 #[test]
330 fn get_capabilities_returns_none_for_unknown() {
331 let registry = TalonRegistry::new();
332 assert!(registry.get_capabilities("unknown").is_none());
333 }
334
335 #[test]
336 fn list_returns_all_installed() {
337 let registry = TalonRegistry::new();
338 install_good(®istry);
339 let sig2 = make_signature("other-talon", "0.5.0");
340 registry.install("other-talon", "0.5.0", &sig2, vec![]).unwrap();
341 assert_eq!(registry.list().len(), 2);
342 }
343
344 #[test]
345 fn duplicate_install_rejected() {
346 let registry = TalonRegistry::new();
347 install_good(®istry);
348 let sig = make_signature("good-talon", "1.0.0");
349 assert!(matches!(registry.install("good-talon", "1.0.0", &sig, vec![]), Err(TalonError::AlreadyInstalled(_))));
350 }
351
352 #[test]
353 fn load_unknown_talon_returns_not_found() {
354 let registry = TalonRegistry::new();
355 assert!(matches!(registry.load("ghost"), Err(TalonError::NotFound(_))));
356 }
357}