klieo-ops 3.5.0

Operational layer above klieo-core: supervisor, governor, gates, escalation, worklog, handoff.
Documentation
//! Source-agent identity → Ed25519 verifying-key registry for Handoff
//! envelope verification.
//!
//! Spec § 1.7 + § 2.6: receivers MUST resolve `HandoffEnvelope::source_identity`
//! against a trusted key registry before adopting. v0.4 closes this gap
//! by making the registry a required dependency of `KvHandoff` — there is
//! no fail-open path.

use crate::key_registry::{load_ed25519_yaml, KeyRegistryError, StaticEd25519KeyRegistry};
use crate::types::AgentId;
use ed25519_dalek::VerifyingKey;
use std::collections::HashMap;
use std::path::Path;
use thiserror::Error;

/// Errors raised by registry loading + lookup.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SourceIdentityRegistryError {
    /// Wraps the underlying key-registry error.
    #[error(transparent)]
    Load(#[from] KeyRegistryError),
}

/// Lookup the expected verifying key for a source agent.
pub trait SourceIdentityRegistry: Send + Sync {
    /// Resolve the trusted Ed25519 verifying key for `agent`, or `None`
    /// if the agent is not registered.
    fn lookup(&self, agent: &AgentId) -> Option<VerifyingKey>;
}

/// In-memory registry, loadable from a YAML file at construction.
///
/// YAML shape:
/// ```yaml
/// claims-triage: "BASE64_ED25519_PUBKEY"
/// underwriter-v1: "BASE64_ED25519_PUBKEY"
/// ```
pub struct StaticSourceIdentityRegistry {
    inner: StaticEd25519KeyRegistry<AgentId>,
}

impl StaticSourceIdentityRegistry {
    /// Build from an in-memory map.
    #[must_use]
    pub fn from_map(keys: HashMap<AgentId, VerifyingKey>) -> Self {
        Self {
            inner: StaticEd25519KeyRegistry::from_map(keys),
        }
    }

    /// Load from a YAML file.
    pub fn from_yaml(path: impl AsRef<Path>) -> Result<Self, SourceIdentityRegistryError> {
        let keys = load_ed25519_yaml(path, AgentId)?;
        Ok(Self {
            inner: StaticEd25519KeyRegistry::from_map(keys),
        })
    }
}

impl SourceIdentityRegistry for StaticSourceIdentityRegistry {
    fn lookup(&self, agent: &AgentId) -> Option<VerifyingKey> {
        use crate::key_registry::Ed25519KeyRegistry as _;
        self.inner.lookup(agent)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::SigningKey;

    #[test]
    fn round_trip_via_yaml() {
        let sk = SigningKey::from_bytes(&[5u8; 32]);
        let pk = sk.verifying_key();
        use base64::Engine as _;
        let b64 = base64::engine::general_purpose::STANDARD.encode(pk.to_bytes());
        let yaml = format!("claims-triage: \"{b64}\"\n");
        let path = std::env::temp_dir().join("klieo-test-source-identity.yaml");
        std::fs::write(&path, yaml).unwrap();
        let reg = StaticSourceIdentityRegistry::from_yaml(&path).expect("load");
        let resolved = reg.lookup(&AgentId("claims-triage".into())).expect("found");
        assert_eq!(resolved.to_bytes(), pk.to_bytes());
    }

    #[test]
    fn missing_agent_returns_none() {
        let reg = StaticSourceIdentityRegistry::from_map(HashMap::new());
        assert!(reg.lookup(&AgentId("ghost".into())).is_none());
    }
}