mail4agent_messenger_shell/
resolve.rs1use std::path::{Path, PathBuf};
7
8use m4a_agent::{AgentError, ResolvedSession, SessionResolver};
9
10fn found(session_id: &str, local: &Option<String>, source: &'static str) -> Option<ResolvedSession> {
11 Some(ResolvedSession { session_id: session_id.to_string(), local_session: local.clone(), source })
12}
13
14macro_rules! with_local {
15 ($t:ident) => {
16 impl $t {
17 pub fn with_local(mut self, local_session: impl Into<String>) -> Self {
19 self.local = Some(local_session.into());
20 self
21 }
22 }
23 };
24}
25
26pub struct ConfiguredResolver {
28 pub session_id: Option<String>,
29 local: Option<String>,
30}
31impl ConfiguredResolver {
32 pub fn new(session_id: Option<String>) -> Self {
33 Self { session_id: session_id.filter(|s| !s.is_empty()), local: None }
34 }
35}
36with_local!(ConfiguredResolver);
37impl SessionResolver for ConfiguredResolver {
38 fn name(&self) -> &'static str {
39 "configured"
40 }
41 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
42 Ok(self.session_id.as_deref().and_then(|s| found(s, &self.local, "configured")))
43 }
44}
45
46pub struct AgentsDirResolver {
49 agents_dir: PathBuf,
50 agent_id: String,
51 aliases: Vec<(String, String)>,
52 local: Option<String>,
53}
54impl AgentsDirResolver {
55 pub fn new(agents_dir: impl Into<PathBuf>, agent_id: impl Into<String>, session_ids_env: &str) -> Self {
56 Self { agents_dir: agents_dir.into(), agent_id: agent_id.into(), aliases: crate::machine::parse_session_ids(session_ids_env), local: None }
57 }
58}
59with_local!(AgentsDirResolver);
60impl SessionResolver for AgentsDirResolver {
61 fn name(&self) -> &'static str {
62 "agents-dir"
63 }
64 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
65 let mut all = match crate::machine::load_agents_dir(&self.agents_dir) {
66 Ok(a) => a,
67 Err(_) => return Ok(None),
68 };
69 all.retain(|s| s.agent_id.as_deref() == Some(self.agent_id.as_str()));
70 crate::machine::apply_session_ids(&mut all, &self.aliases);
71 Ok(all.first().and_then(|s| found(&s.session_id, &self.local, "agents-dir")))
72 }
73}
74
75pub struct GrokActiveResolver {
77 index_text: String,
78 sessions_root: PathBuf,
79 cwd: String,
80 local: Option<String>,
81}
82impl GrokActiveResolver {
83 pub fn new(index_text: impl Into<String>, sessions_root: impl Into<PathBuf>, cwd: impl Into<String>) -> Self {
84 Self { index_text: index_text.into(), sessions_root: sessions_root.into(), cwd: cwd.into(), local: None }
85 }
86}
87with_local!(GrokActiveResolver);
88impl SessionResolver for GrokActiveResolver {
89 fn name(&self) -> &'static str {
90 "grok-active"
91 }
92 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
93 let heard = crate::grok_listen::hear(&self.index_text, &self.sessions_root).map_err(|e| AgentError::Identity(e.to_string()))?;
94 let mut rows = heard.iter().filter(|h| h.cwd == self.cwd);
95 let first = rows.next();
96 if rows.next().is_some() {
97 return Err(AgentError::Identity("two live Grok sessions share this working directory".into()));
98 }
99 Ok(first.and_then(|h| found(&h.session_id, &self.local, "grok-active")))
100 }
101}
102
103pub struct ProviderRecordResolver {
105 store_root: PathBuf,
106 provider: String,
107 vendor_session: String,
108 local: Option<String>,
109}
110impl ProviderRecordResolver {
111 pub fn new(store_root: &Path, provider: &str, vendor_session: &str) -> Self {
112 Self { store_root: store_root.to_path_buf(), provider: provider.into(), vendor_session: vendor_session.into(), local: None }
113 }
114}
115with_local!(ProviderRecordResolver);
116impl SessionResolver for ProviderRecordResolver {
117 fn name(&self) -> &'static str {
118 "provider-record"
119 }
120 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
121 let live = crate::provider::registry::live_sessions(&self.store_root);
122 Ok(live.iter().find(|s| s.kind.provider.id() == self.provider && s.session_id == self.vendor_session).and_then(|s| found(&s.session_id, &self.local, "provider-record")))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use m4a_agent::{BackendKind, IdentityStore, MemoryVault, ResolverChain};
130 use std::sync::Arc;
131
132 #[test]
133 fn the_sources_agree_through_one_chain_and_link_the_local_session() {
134 let root = tempfile::tempdir().unwrap();
135 let agents = root.path().join("agents");
136 std::fs::create_dir_all(agents.join("bot-7")).unwrap();
137 std::fs::write(agents.join("bot-7/profile.json"), r#"{"name":"Alice"}"#).unwrap();
138 let chain = ResolverChain::new()
140 .with(ConfiguredResolver::new(None))
141 .with(AgentsDirResolver::new(&agents, "bot-7", "bot-7=mail-7").with_local("local-7"))
142 .with(ConfiguredResolver::new(Some("mail-7".into())));
143 let r = chain.resolve().unwrap();
144 assert_eq!((r.session_id.as_str(), r.local_session.as_deref(), r.source), ("mail-7", Some("local-7"), "agents-dir"));
145 let ids = IdentityStore::new(Arc::new(MemoryVault::new()));
146 let id = chain.identity(&ids, BackendKind::Server, "https://p.example").unwrap();
147 assert_eq!((id.session_id.as_str(), id.local_session.as_deref()), ("mail-7", Some("local-7")));
148 assert!(ResolverChain::new().with(AgentsDirResolver::new(&agents, "nobody", "")).resolve().is_err());
150 let clash = ResolverChain::new().with(AgentsDirResolver::new(&agents, "bot-7", "")).with(ConfiguredResolver::new(Some("mail-7".into())));
152 assert!(clash.resolve().is_err(), "agent id bot-7 vs configured mail-7");
153 }
154}