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}