agentmux_srv\registry/
paths.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Resolve the shared registry directory (`<shared_home>/agents/registry/`).
5
6use std::path::PathBuf;
7
8/// Resolve the GLOBAL `<home>/shared/agents/registry/` directory.
9///
10/// P0.3 re-roots the named-**instance** registry from the old
11/// channel-scoped `channels/<ch>/agents/registry/` (which walked
12/// `AGENTMUX_DATA_DIR` up three levels and so landed inside the current
13/// channel) to the channel-independent `~/.agentmux/shared/`. An agent
14/// named in one channel is then visible in every channel, exactly like the
15/// definition store ([`resolve_shared_definitions_dir`]) — the two are now
16/// siblings under `shared/agents/`.
17///
18/// The base directory that instance `working_directory` values are stored
19/// *relative to* is tracked separately (the Store's `registry_agents_base`,
20/// fed from `AGENTMUX_AGENTS_DIR`) — it must stay the current channel's
21/// agents dir even though the registry files are now global.
22///
23/// Returns `None` only if the global shared root can't be resolved — caller
24/// treats this as "registry disabled," no write attempts.
25pub fn resolve_shared_registry_dir() -> Option<PathBuf> {
26    resolve_global_shared_root().map(|h| h.join("agents").join("registry"))
27}
28
29/// Resolve the GLOBAL `<home>/shared/agents/definitions/` directory.
30///
31/// Sibling of [`resolve_shared_registry_dir`]: since P0.3 both the definition
32/// store and the instance registry are **channel-independent**, resolved via
33/// the same [`resolve_global_shared_root`] so an agent created/named in one
34/// channel is visible in every channel (cross-channel agent persistence,
35/// P0.2/P0.3). Resolves via the launcher-exported `AGENTMUX_SHARED_DIR`, with
36/// a test override and a `~/.agentmux/shared` fallback.
37pub fn resolve_shared_definitions_dir() -> Option<PathBuf> {
38    resolve_global_shared_root().map(|h| h.join("agents").join("definitions"))
39}
40
41/// Resolve the GLOBAL `<home>/shared/agents/transcripts/` directory.
42///
43/// Sibling of [`resolve_shared_registry_dir`] / [`resolve_shared_definitions_dir`]:
44/// the agent's *conversation transcript* is the last per-channel agent surface
45/// (definitions, instances, workspaces, and auth all became global in
46/// #1387–#1396). Backing the `agent:<defId>:current` FileStore zone with a store
47/// rooted here makes a conversation load when you open the agent from *any*
48/// build/channel — the open path reads the same zone regardless of which channel
49/// wrote it. See `docs/analysis/ANALYSIS_CROSS_CHANNEL_CONVERSATION_HISTORY_2026_06_14.md`.
50///
51/// Returns `None` only if the global shared root can't be resolved — caller
52/// treats this as "global transcripts disabled" and falls back to the
53/// per-channel store.
54pub fn resolve_shared_transcripts_dir() -> Option<PathBuf> {
55    resolve_global_shared_root().map(|h| h.join("agents").join("transcripts"))
56}
57
58/// The global, channel-independent `<home>/shared` root.
59fn resolve_global_shared_root() -> Option<PathBuf> {
60    if let Ok(s) = std::env::var("AGENTMUX_HOME_OVERRIDE") {
61        if !s.is_empty() {
62            // Consistent with data_paths everywhere else: the override is the
63            // ~/.agentmux root, with the shared dir at root/shared. (reagent
64            // P2 on #1385.)
65            return Some(PathBuf::from(s).join("shared"));
66        }
67    }
68    if let Ok(s) = std::env::var("AGENTMUX_SHARED_DIR") {
69        if !s.is_empty() {
70            return Some(PathBuf::from(s));
71        }
72    }
73    dirs::home_dir().map(|h| h.join(".agentmux").join("shared"))
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::sync::Mutex;
80
81    // Process-global env access — serialize so parallel tests don't race.
82    static ENV_LOCK: Mutex<()> = Mutex::new(());
83
84    fn clear() {
85        std::env::remove_var("AGENTMUX_HOME_OVERRIDE");
86        std::env::remove_var("AGENTMUX_DATA_DIR");
87        std::env::remove_var("AGENTMUX_SHARED_DIR");
88    }
89
90    #[test]
91    fn override_wins() {
92        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
93        clear();
94        // The override is the ~/.agentmux root; the global registry lives at
95        // root/shared/agents/registry (P0.3 re-root).
96        std::env::set_var("AGENTMUX_HOME_OVERRIDE", "/tmp/test-home");
97        let r = resolve_shared_registry_dir().unwrap();
98        assert_eq!(
99            r,
100            PathBuf::from("/tmp/test-home/shared/agents/registry")
101        );
102        clear();
103    }
104
105    #[test]
106    fn registry_uses_shared_dir_and_ignores_data_dir() {
107        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
108        clear();
109        // The instance registry is GLOBAL now: it resolves from the shared
110        // root, NOT by walking up the per-version AGENTMUX_DATA_DIR (which
111        // would land in the current channel). Set both and confirm the
112        // channel-scoped data dir is ignored.
113        std::env::set_var("AGENTMUX_SHARED_DIR", "/home/user/.agentmux/shared");
114        std::env::set_var(
115            "AGENTMUX_DATA_DIR",
116            "/home/user/.agentmux/channels/stable/versions/0.44.2/data",
117        );
118        let r = resolve_shared_registry_dir().unwrap();
119        assert_eq!(
120            r,
121            PathBuf::from("/home/user/.agentmux/shared/agents/registry")
122        );
123        // Sibling of the definition store — both under shared/agents/.
124        let d = resolve_shared_definitions_dir().unwrap();
125        assert_eq!(r.parent(), d.parent());
126        clear();
127    }
128
129    #[test]
130    fn falls_back_to_home() {
131        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
132        clear();
133        // No env set — uses dirs::home_dir()/.agentmux/shared. Non-None.
134        assert!(resolve_shared_registry_dir().is_some());
135    }
136
137    #[test]
138    fn definitions_dir_uses_shared_dir() {
139        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
140        clear();
141        std::env::set_var("AGENTMUX_SHARED_DIR", "/home/user/.agentmux/shared");
142        let r = resolve_shared_definitions_dir().unwrap();
143        assert_eq!(
144            r,
145            PathBuf::from("/home/user/.agentmux/shared/agents/definitions")
146        );
147        clear();
148    }
149
150    #[test]
151    fn definitions_dir_override_wins() {
152        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
153        clear();
154        std::env::set_var("AGENTMUX_HOME_OVERRIDE", "/tmp/test-home");
155        let r = resolve_shared_definitions_dir().unwrap();
156        // Override is the ~/.agentmux root; shared lives at root/shared.
157        assert_eq!(
158            r,
159            PathBuf::from("/tmp/test-home/shared/agents/definitions")
160        );
161        clear();
162    }
163}