use crate::error::{AgentError, Result};
use crate::identity::{IdentityStore, SessionIdentity};
use crate::backend::BackendKind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedSession {
pub session_id: String,
pub local_session: Option<String>,
pub source: &'static str,
}
pub trait SessionResolver: Send + Sync {
fn name(&self) -> &'static str;
fn resolve(&self) -> Result<Option<ResolvedSession>>;
}
pub struct FixedResolver(pub ResolvedSession);
impl SessionResolver for FixedResolver {
fn name(&self) -> &'static str {
self.0.source
}
fn resolve(&self) -> Result<Option<ResolvedSession>> {
Ok(Some(self.0.clone()))
}
}
#[derive(Default)]
pub struct ResolverChain {
sources: Vec<Box<dyn SessionResolver>>,
}
impl ResolverChain {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, r: impl SessionResolver + 'static) -> Self {
self.sources.push(Box::new(r));
self
}
pub fn resolve(&self) -> Result<ResolvedSession> {
let mut first: Option<ResolvedSession> = None;
for s in &self.sources {
let Some(found) = s.resolve()? else { continue };
match &first {
None => first = Some(found),
Some(f) if f.session_id != found.session_id => {
return Err(AgentError::Identity(format!("sources disagree about the session: {} says one id, {} another", f.source, found.source)));
}
Some(_) => {
if let (Some(f), Some(l)) = (first.as_mut(), found.local_session) {
match &f.local_session {
None => f.local_session = Some(l),
Some(have) if *have != l => return Err(AgentError::Identity("sources disagree about the local session".into())),
Some(_) => {}
}
}
}
}
}
first.ok_or_else(|| AgentError::Identity("no source knows which session this is".into()))
}
pub fn identity(&self, ids: &IdentityStore, tier: BackendKind, server_ref: &str) -> Result<SessionIdentity> {
let r = self.resolve()?;
let mut id = ids.resolve(&r.session_id, tier, server_ref)?;
if let Some(l) = &r.local_session {
ids.bind_local(&mut id, l)?;
}
Ok(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::MemoryVault;
use std::sync::Arc;
struct Nothing;
impl SessionResolver for Nothing {
fn name(&self) -> &'static str {
"nothing"
}
fn resolve(&self) -> Result<Option<ResolvedSession>> {
Ok(None)
}
}
fn fixed(id: &str, local: Option<&str>, source: &'static str) -> FixedResolver {
FixedResolver(ResolvedSession { session_id: id.into(), local_session: local.map(Into::into), source })
}
#[test]
fn first_source_wins_and_lower_ones_can_add_the_local_link() {
let c = ResolverChain::new().with(Nothing).with(fixed("s1", None, "agents-dir")).with(fixed("s1", Some("l1"), "attestation"));
let r = c.resolve().unwrap();
assert_eq!((r.session_id.as_str(), r.local_session.as_deref(), r.source), ("s1", Some("l1"), "agents-dir"));
let ids = IdentityStore::new(Arc::new(MemoryVault::new()));
let id = c.identity(&ids, BackendKind::Server, "https://p.example").unwrap();
assert_eq!(id.local_session.as_deref(), Some("l1"));
}
#[test]
fn disagreeing_sources_and_no_source_are_errors() {
assert!(ResolverChain::new().with(fixed("s1", None, "a")).with(fixed("s2", None, "b")).resolve().is_err());
assert!(ResolverChain::new().with(fixed("s1", Some("l1"), "a")).with(fixed("s1", Some("l2"), "b")).resolve().is_err());
assert!(ResolverChain::new().with(Nothing).resolve().is_err());
}
}