1use crate::error::{AgentError, Result};
7use crate::identity::{IdentityStore, SessionIdentity};
8use crate::backend::BackendKind;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ResolvedSession {
13 pub session_id: String,
15 pub local_session: Option<String>,
17 pub source: &'static str,
19}
20
21pub trait SessionResolver: Send + Sync {
22 fn name(&self) -> &'static str;
23 fn resolve(&self) -> Result<Option<ResolvedSession>>;
25}
26
27pub struct FixedResolver(pub ResolvedSession);
29
30impl SessionResolver for FixedResolver {
31 fn name(&self) -> &'static str {
32 self.0.source
33 }
34 fn resolve(&self) -> Result<Option<ResolvedSession>> {
35 Ok(Some(self.0.clone()))
36 }
37}
38
39#[derive(Default)]
42pub struct ResolverChain {
43 sources: Vec<Box<dyn SessionResolver>>,
44}
45
46impl ResolverChain {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn with(mut self, r: impl SessionResolver + 'static) -> Self {
52 self.sources.push(Box::new(r));
53 self
54 }
55
56 pub fn resolve(&self) -> Result<ResolvedSession> {
57 let mut first: Option<ResolvedSession> = None;
58 for s in &self.sources {
59 let Some(found) = s.resolve()? else { continue };
60 match &first {
61 None => first = Some(found),
62 Some(f) if f.session_id != found.session_id => {
63 return Err(AgentError::Identity(format!("sources disagree about the session: {} says one id, {} another", f.source, found.source)));
64 }
65 Some(_) => {
66 if let (Some(f), Some(l)) = (first.as_mut(), found.local_session) {
68 match &f.local_session {
69 None => f.local_session = Some(l),
70 Some(have) if *have != l => return Err(AgentError::Identity("sources disagree about the local session".into())),
71 Some(_) => {}
72 }
73 }
74 }
75 }
76 }
77 first.ok_or_else(|| AgentError::Identity("no source knows which session this is".into()))
78 }
79
80 pub fn identity(&self, ids: &IdentityStore, tier: BackendKind, server_ref: &str) -> Result<SessionIdentity> {
83 let r = self.resolve()?;
84 let mut id = ids.resolve(&r.session_id, tier, server_ref)?;
85 if let Some(l) = &r.local_session {
86 ids.bind_local(&mut id, l)?;
87 }
88 Ok(id)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::vault::MemoryVault;
96 use std::sync::Arc;
97
98 struct Nothing;
99 impl SessionResolver for Nothing {
100 fn name(&self) -> &'static str {
101 "nothing"
102 }
103 fn resolve(&self) -> Result<Option<ResolvedSession>> {
104 Ok(None)
105 }
106 }
107 fn fixed(id: &str, local: Option<&str>, source: &'static str) -> FixedResolver {
108 FixedResolver(ResolvedSession { session_id: id.into(), local_session: local.map(Into::into), source })
109 }
110
111 #[test]
112 fn first_source_wins_and_lower_ones_can_add_the_local_link() {
113 let c = ResolverChain::new().with(Nothing).with(fixed("s1", None, "agents-dir")).with(fixed("s1", Some("l1"), "attestation"));
114 let r = c.resolve().unwrap();
115 assert_eq!((r.session_id.as_str(), r.local_session.as_deref(), r.source), ("s1", Some("l1"), "agents-dir"));
116 let ids = IdentityStore::new(Arc::new(MemoryVault::new()));
117 let id = c.identity(&ids, BackendKind::Server, "https://p.example").unwrap();
118 assert_eq!(id.local_session.as_deref(), Some("l1"));
119 }
120
121 #[test]
122 fn disagreeing_sources_and_no_source_are_errors() {
123 assert!(ResolverChain::new().with(fixed("s1", None, "a")).with(fixed("s2", None, "b")).resolve().is_err());
124 assert!(ResolverChain::new().with(fixed("s1", Some("l1"), "a")).with(fixed("s1", Some("l2"), "b")).resolve().is_err());
125 assert!(ResolverChain::new().with(Nothing).resolve().is_err());
126 }
127}