Skip to main content

symbi_runtime/integrations/agentpin/
types.rs

1//! AgentPin Integration Types
2//!
3//! Configuration, error, and result types for the AgentPin integration.
4
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// How discovery documents are resolved.
10#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum DiscoveryMode {
13    /// Standard `.well-known` HTTPS fetch (default).
14    #[default]
15    WellKnown,
16    /// Pre-shared trust bundle loaded from a file.
17    Bundle,
18    /// Local filesystem directory containing `{domain}.json` files.
19    Local,
20    /// Chain: try sync resolvers (bundle → local) then fall back to async.
21    Chain,
22}
23
24/// AgentPin integration configuration
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AgentPinConfig {
27    /// Whether AgentPin verification is enabled
28    #[serde(default = "default_enabled")]
29    pub enabled: bool,
30    /// Path to the TOFU key pin store file
31    #[serde(default = "default_key_store_path")]
32    pub key_store_path: PathBuf,
33    /// Path to the discovery document cache directory
34    #[serde(default = "default_discovery_cache_path")]
35    pub discovery_cache_path: PathBuf,
36    /// Discovery document cache TTL in seconds
37    #[serde(default = "default_cache_ttl_secs")]
38    pub cache_ttl_secs: u64,
39    /// Maximum allowed clock skew in seconds
40    #[serde(default = "default_clock_skew_secs")]
41    pub clock_skew_secs: i64,
42    /// Maximum allowed credential TTL in seconds
43    #[serde(default = "default_max_ttl_secs")]
44    pub max_ttl_secs: i64,
45    /// Expected audience claim (this service's domain)
46    pub audience: Option<String>,
47    /// How discovery documents are obtained
48    #[serde(default)]
49    pub discovery_mode: DiscoveryMode,
50    /// Path to a trust bundle JSON file (used when discovery_mode = bundle or chain)
51    pub trust_bundle_path: Option<PathBuf>,
52    /// Path to a directory of discovery docs (used when discovery_mode = local or chain)
53    pub local_discovery_dir: Option<PathBuf>,
54    /// Path to a directory of revocation docs (used when discovery_mode = local or chain)
55    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/// Errors from AgentPin operations
107#[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/// Result of verifying an AgentPin credential
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct AgentVerificationResult {
137    /// Whether verification succeeded
138    pub valid: bool,
139    /// Agent ID from the credential
140    pub agent_id: Option<String>,
141    /// Issuer domain from the credential
142    pub issuer: Option<String>,
143    /// Capabilities granted by the credential
144    pub capabilities: Vec<String>,
145    /// Whether the delegation chain was verified
146    pub delegation_verified: Option<bool>,
147    /// Error message if verification failed
148    pub error_message: Option<String>,
149    /// Warnings generated during verification
150    pub warnings: Vec<String>,
151}
152
153impl AgentVerificationResult {
154    /// Create a successful verification result
155    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    /// Create a failed verification result
168    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}