symbi_runtime/integrations/agentpin/
types.rs1use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use thiserror::Error;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum DiscoveryMode {
13 #[default]
15 WellKnown,
16 Bundle,
18 Local,
20 Chain,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AgentPinConfig {
27 #[serde(default = "default_enabled")]
29 pub enabled: bool,
30 #[serde(default = "default_key_store_path")]
32 pub key_store_path: PathBuf,
33 #[serde(default = "default_discovery_cache_path")]
35 pub discovery_cache_path: PathBuf,
36 #[serde(default = "default_cache_ttl_secs")]
38 pub cache_ttl_secs: u64,
39 #[serde(default = "default_clock_skew_secs")]
41 pub clock_skew_secs: i64,
42 #[serde(default = "default_max_ttl_secs")]
44 pub max_ttl_secs: i64,
45 pub audience: Option<String>,
47 #[serde(default)]
49 pub discovery_mode: DiscoveryMode,
50 pub trust_bundle_path: Option<PathBuf>,
52 pub local_discovery_dir: Option<PathBuf>,
54 pub local_revocation_dir: Option<PathBuf>,
56}
57
58fn default_enabled() -> bool {
59 false
60}
61
62fn default_key_store_path() -> PathBuf {
63 let mut p = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
64 p.push(".symbiont");
65 p.push("agentpin_keys.json");
66 p
67}
68
69fn default_discovery_cache_path() -> PathBuf {
70 let mut p = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
71 p.push(".symbiont");
72 p.push("agentpin_discovery");
73 p
74}
75
76fn default_cache_ttl_secs() -> u64 {
77 3600
78}
79
80fn default_clock_skew_secs() -> i64 {
81 60
82}
83
84fn default_max_ttl_secs() -> i64 {
85 86400
86}
87
88impl Default for AgentPinConfig {
89 fn default() -> Self {
90 Self {
91 enabled: default_enabled(),
92 key_store_path: default_key_store_path(),
93 discovery_cache_path: default_discovery_cache_path(),
94 cache_ttl_secs: default_cache_ttl_secs(),
95 clock_skew_secs: default_clock_skew_secs(),
96 max_ttl_secs: default_max_ttl_secs(),
97 audience: None,
98 discovery_mode: DiscoveryMode::default(),
99 trust_bundle_path: None,
100 local_discovery_dir: None,
101 local_revocation_dir: None,
102 }
103 }
104}
105
106#[derive(Error, Debug, Clone)]
108pub enum AgentPinError {
109 #[error("Credential verification failed: {reason}")]
110 VerificationFailed { reason: String },
111
112 #[error("Discovery document fetch failed for {domain}: {reason}")]
113 DiscoveryFetchFailed { domain: String, reason: String },
114
115 #[error("Key store error: {reason}")]
116 KeyStoreError { reason: String },
117
118 #[error("Configuration error: {reason}")]
119 ConfigError { reason: String },
120
121 #[error("IO error: {reason}")]
122 IoError { reason: String },
123
124 #[error("Credential expired")]
125 CredentialExpired,
126
127 #[error("Agent not found in discovery: {agent_id}")]
128 AgentNotFound { agent_id: String },
129
130 #[error("Key pin mismatch for domain: {domain}")]
131 KeyPinMismatch { domain: String },
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct AgentVerificationResult {
137 pub valid: bool,
139 pub agent_id: Option<String>,
141 pub issuer: Option<String>,
143 pub capabilities: Vec<String>,
145 pub delegation_verified: Option<bool>,
147 pub error_message: Option<String>,
149 pub warnings: Vec<String>,
151}
152
153impl AgentVerificationResult {
154 pub fn success(agent_id: String, issuer: String, capabilities: Vec<String>) -> Self {
156 Self {
157 valid: true,
158 agent_id: Some(agent_id),
159 issuer: Some(issuer),
160 capabilities,
161 delegation_verified: None,
162 error_message: None,
163 warnings: vec![],
164 }
165 }
166
167 pub fn failure(error_message: String) -> Self {
169 Self {
170 valid: false,
171 agent_id: None,
172 issuer: None,
173 capabilities: vec![],
174 delegation_verified: None,
175 error_message: Some(error_message),
176 warnings: vec![],
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_default_config() {
187 let config = AgentPinConfig::default();
188 assert!(!config.enabled);
189 assert_eq!(config.cache_ttl_secs, 3600);
190 assert_eq!(config.clock_skew_secs, 60);
191 assert_eq!(config.max_ttl_secs, 86400);
192 assert!(config.audience.is_none());
193 assert!(config
194 .key_store_path
195 .to_string_lossy()
196 .contains("agentpin_keys.json"));
197 assert_eq!(config.discovery_mode, DiscoveryMode::WellKnown);
198 assert!(config.trust_bundle_path.is_none());
199 assert!(config.local_discovery_dir.is_none());
200 assert!(config.local_revocation_dir.is_none());
201 }
202
203 #[test]
204 fn test_config_serialization_roundtrip() {
205 let config = AgentPinConfig {
206 enabled: true,
207 audience: Some("example.com".to_string()),
208 ..Default::default()
209 };
210
211 let json = serde_json::to_string(&config).unwrap();
212 let deserialized: AgentPinConfig = serde_json::from_str(&json).unwrap();
213
214 assert!(deserialized.enabled);
215 assert_eq!(deserialized.audience, Some("example.com".to_string()));
216 assert_eq!(deserialized.cache_ttl_secs, config.cache_ttl_secs);
217 }
218
219 #[test]
220 fn test_verification_result_success() {
221 let result = AgentVerificationResult::success(
222 "agent-001".to_string(),
223 "maker.example.com".to_string(),
224 vec!["execute:code".to_string()],
225 );
226 assert!(result.valid);
227 assert_eq!(result.agent_id, Some("agent-001".to_string()));
228 assert_eq!(result.issuer, Some("maker.example.com".to_string()));
229 assert_eq!(result.capabilities.len(), 1);
230 assert!(result.error_message.is_none());
231 }
232
233 #[test]
234 fn test_verification_result_failure() {
235 let result = AgentVerificationResult::failure("signature invalid".to_string());
236 assert!(!result.valid);
237 assert!(result.agent_id.is_none());
238 assert_eq!(result.error_message, Some("signature invalid".to_string()));
239 }
240
241 #[test]
242 fn test_verification_result_serialization() {
243 let result = AgentVerificationResult::success(
244 "agent-001".to_string(),
245 "maker.example.com".to_string(),
246 vec!["read:data".to_string()],
247 );
248 let json = serde_json::to_string(&result).unwrap();
249 let deserialized: AgentVerificationResult = serde_json::from_str(&json).unwrap();
250 assert_eq!(deserialized.valid, result.valid);
251 assert_eq!(deserialized.agent_id, result.agent_id);
252 }
253
254 #[test]
255 fn test_discovery_mode_serde() {
256 assert_eq!(
257 serde_json::to_string(&DiscoveryMode::WellKnown).unwrap(),
258 "\"wellknown\""
259 );
260 assert_eq!(
261 serde_json::to_string(&DiscoveryMode::Bundle).unwrap(),
262 "\"bundle\""
263 );
264 assert_eq!(
265 serde_json::to_string(&DiscoveryMode::Local).unwrap(),
266 "\"local\""
267 );
268 assert_eq!(
269 serde_json::to_string(&DiscoveryMode::Chain).unwrap(),
270 "\"chain\""
271 );
272 }
273
274 #[test]
275 fn test_agentpin_error_display() {
276 let err = AgentPinError::VerificationFailed {
277 reason: "bad sig".to_string(),
278 };
279 assert!(err.to_string().contains("bad sig"));
280
281 let err = AgentPinError::KeyPinMismatch {
282 domain: "evil.com".to_string(),
283 };
284 assert!(err.to_string().contains("evil.com"));
285 }
286}