use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPinConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_key_store_path")]
pub key_store_path: PathBuf,
#[serde(default = "default_discovery_cache_path")]
pub discovery_cache_path: PathBuf,
#[serde(default = "default_cache_ttl_secs")]
pub cache_ttl_secs: u64,
#[serde(default = "default_clock_skew_secs")]
pub clock_skew_secs: i64,
#[serde(default = "default_max_ttl_secs")]
pub max_ttl_secs: i64,
pub audience: Option<String>,
}
fn default_enabled() -> bool {
false
}
fn default_key_store_path() -> PathBuf {
let mut p = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
p.push(".symbiont");
p.push("agentpin_keys.json");
p
}
fn default_discovery_cache_path() -> PathBuf {
let mut p = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
p.push(".symbiont");
p.push("agentpin_discovery");
p
}
fn default_cache_ttl_secs() -> u64 {
3600
}
fn default_clock_skew_secs() -> i64 {
60
}
fn default_max_ttl_secs() -> i64 {
86400
}
impl Default for AgentPinConfig {
fn default() -> Self {
Self {
enabled: default_enabled(),
key_store_path: default_key_store_path(),
discovery_cache_path: default_discovery_cache_path(),
cache_ttl_secs: default_cache_ttl_secs(),
clock_skew_secs: default_clock_skew_secs(),
max_ttl_secs: default_max_ttl_secs(),
audience: None,
}
}
}
#[derive(Error, Debug, Clone)]
pub enum AgentPinError {
#[error("Credential verification failed: {reason}")]
VerificationFailed { reason: String },
#[error("Discovery document fetch failed for {domain}: {reason}")]
DiscoveryFetchFailed { domain: String, reason: String },
#[error("Key store error: {reason}")]
KeyStoreError { reason: String },
#[error("Configuration error: {reason}")]
ConfigError { reason: String },
#[error("IO error: {reason}")]
IoError { reason: String },
#[error("Credential expired")]
CredentialExpired,
#[error("Agent not found in discovery: {agent_id}")]
AgentNotFound { agent_id: String },
#[error("Key pin mismatch for domain: {domain}")]
KeyPinMismatch { domain: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentVerificationResult {
pub valid: bool,
pub agent_id: Option<String>,
pub issuer: Option<String>,
pub capabilities: Vec<String>,
pub delegation_verified: Option<bool>,
pub error_message: Option<String>,
pub warnings: Vec<String>,
}
impl AgentVerificationResult {
pub fn success(agent_id: String, issuer: String, capabilities: Vec<String>) -> Self {
Self {
valid: true,
agent_id: Some(agent_id),
issuer: Some(issuer),
capabilities,
delegation_verified: None,
error_message: None,
warnings: vec![],
}
}
pub fn failure(error_message: String) -> Self {
Self {
valid: false,
agent_id: None,
issuer: None,
capabilities: vec![],
delegation_verified: None,
error_message: Some(error_message),
warnings: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = AgentPinConfig::default();
assert!(!config.enabled);
assert_eq!(config.cache_ttl_secs, 3600);
assert_eq!(config.clock_skew_secs, 60);
assert_eq!(config.max_ttl_secs, 86400);
assert!(config.audience.is_none());
assert!(config
.key_store_path
.to_string_lossy()
.contains("agentpin_keys.json"));
}
#[test]
fn test_config_serialization_roundtrip() {
let config = AgentPinConfig {
enabled: true,
audience: Some("example.com".to_string()),
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: AgentPinConfig = serde_json::from_str(&json).unwrap();
assert!(deserialized.enabled);
assert_eq!(deserialized.audience, Some("example.com".to_string()));
assert_eq!(deserialized.cache_ttl_secs, config.cache_ttl_secs);
}
#[test]
fn test_verification_result_success() {
let result = AgentVerificationResult::success(
"agent-001".to_string(),
"maker.example.com".to_string(),
vec!["execute:code".to_string()],
);
assert!(result.valid);
assert_eq!(result.agent_id, Some("agent-001".to_string()));
assert_eq!(result.issuer, Some("maker.example.com".to_string()));
assert_eq!(result.capabilities.len(), 1);
assert!(result.error_message.is_none());
}
#[test]
fn test_verification_result_failure() {
let result = AgentVerificationResult::failure("signature invalid".to_string());
assert!(!result.valid);
assert!(result.agent_id.is_none());
assert_eq!(result.error_message, Some("signature invalid".to_string()));
}
#[test]
fn test_verification_result_serialization() {
let result = AgentVerificationResult::success(
"agent-001".to_string(),
"maker.example.com".to_string(),
vec!["read:data".to_string()],
);
let json = serde_json::to_string(&result).unwrap();
let deserialized: AgentVerificationResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.valid, result.valid);
assert_eq!(deserialized.agent_id, result.agent_id);
}
#[test]
fn test_agentpin_error_display() {
let err = AgentPinError::VerificationFailed {
reason: "bad sig".to_string(),
};
assert!(err.to_string().contains("bad sig"));
let err = AgentPinError::KeyPinMismatch {
domain: "evil.com".to_string(),
};
assert!(err.to_string().contains("evil.com"));
}
}