Skip to main content

gate4agent_node_core/
platform.rs

1use gate4agent_node_protocol::{
2    ArchitectureId, HostDescriptor, LocalTransportKind, OperatingSystemId, PathEncoding,
3    PathSemantics, PathStyle,
4};
5use std::path::{Path, PathBuf};
6
7#[cfg(windows)]
8pub const DEFAULT_NODE_ENDPOINT: &str = r"\\.\pipe\gate4agent-node";
9
10pub fn default_node_endpoint() -> Option<PathBuf> {
11    #[cfg(windows)]
12    {
13        return Some(PathBuf::from(DEFAULT_NODE_ENDPOINT));
14    }
15    #[cfg(unix)]
16    {
17        if let Some(root) = std::env::var_os("XDG_RUNTIME_DIR")
18            .filter(|value| !value.is_empty())
19            .map(PathBuf::from)
20            .filter(|path| path.is_absolute())
21        {
22            return Some(root.join("gate4agent-node.sock"));
23        }
24        secure_fallback_runtime_dir().map(|root| root.join("n.sock"))
25    }
26}
27
28pub fn default_state_path(node_id: &str) -> Option<PathBuf> {
29    #[cfg(windows)]
30    {
31        return std::env::var_os("LOCALAPPDATA")
32            .filter(|value| !value.is_empty())
33            .map(PathBuf::from)
34            .map(|root| {
35                root.join("Gate4Agent")
36                    .join("nodes")
37                    .join(node_id)
38                    .join("state-v1.json")
39            });
40    }
41    #[cfg(unix)]
42    {
43        #[cfg(target_os = "macos")]
44        let root = std::env::var_os("HOME")
45            .filter(|value| !value.is_empty())
46            .map(PathBuf::from)
47            .filter(|path| path.is_absolute())
48            .map(|home| home.join("Library").join("Application Support"))?;
49        #[cfg(not(target_os = "macos"))]
50        let root = std::env::var_os("XDG_STATE_HOME")
51            .filter(|value| !value.is_empty())
52            .map(PathBuf::from)
53            .filter(|path| path.is_absolute())
54            .or_else(|| {
55                std::env::var_os("HOME")
56                    .filter(|value| !value.is_empty())
57                    .map(PathBuf::from)
58                    .filter(|path| path.is_absolute())
59                    .map(|home| home.join(".local").join("state"))
60            })?;
61        Some(
62            root.join("gate4agent")
63                .join("nodes")
64                .join(node_id)
65                .join("state-v1.json"),
66        )
67    }
68}
69
70#[cfg(unix)]
71fn secure_fallback_runtime_dir() -> Option<PathBuf> {
72    use std::os::unix::fs::{MetadataExt, PermissionsExt};
73
74    let root = std::env::temp_dir().join(format!("g4a-{}", unsafe { libc::geteuid() }));
75    if !root.is_absolute() || std::fs::create_dir_all(&root).is_err() {
76        return None;
77    }
78    let metadata = std::fs::symlink_metadata(&root).ok()?;
79    if !metadata.file_type().is_dir() || metadata.uid() != unsafe { libc::geteuid() } {
80        return None;
81    }
82    std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700)).ok()?;
83    let metadata = std::fs::symlink_metadata(&root).ok()?;
84    if metadata.permissions().mode() & 0o7777 != 0o700 {
85        return None;
86    }
87    Some(root)
88}
89
90pub fn validate_endpoint(endpoint: &str) -> bool {
91    #[cfg(windows)]
92    {
93        return endpoint.starts_with(r"\\.\pipe\")
94            && endpoint.len() > r"\\.\pipe\".len()
95            && endpoint.len() <= 1_024;
96    }
97    #[cfg(unix)]
98    {
99        let path = Path::new(endpoint);
100        return !endpoint.is_empty()
101            && endpoint.len() <= 1_024
102            && !endpoint.chars().any(char::is_control)
103            && path.is_absolute()
104            && path.file_name().is_some();
105    }
106}
107
108pub fn root_identity(value: &str) -> String {
109    #[cfg(windows)]
110    {
111        return value.to_lowercase();
112    }
113    #[cfg(unix)]
114    {
115        value.to_owned()
116    }
117}
118
119pub fn roots_equal(left: &str, right: &str) -> bool {
120    #[cfg(windows)]
121    {
122        return left.eq_ignore_ascii_case(right);
123    }
124    #[cfg(unix)]
125    {
126        left == right
127    }
128}
129
130pub fn normalize_canonical_root(value: String) -> String {
131    #[cfg(windows)]
132    {
133        if let Some(rest) = value.strip_prefix(r"\\?\UNC\") {
134            return format!(r"\\{rest}");
135        }
136        if let Some(rest) = value.strip_prefix(r"\\?\") {
137            return rest.to_owned();
138        }
139    }
140    value
141}
142
143pub fn host_descriptor() -> Result<HostDescriptor, String> {
144    Ok(HostDescriptor {
145        operating_system: OperatingSystemId::new(std::env::consts::OS)
146            .map_err(|error| error.to_string())?,
147        architecture: ArchitectureId::new(std::env::consts::ARCH)
148            .map_err(|error| error.to_string())?,
149    })
150}
151
152pub fn path_semantics() -> PathSemantics {
153    #[cfg(windows)]
154    {
155        return PathSemantics {
156            style: PathStyle::Windows,
157            encoding: PathEncoding::Utf8,
158        };
159    }
160    #[cfg(unix)]
161    {
162        PathSemantics {
163            style: PathStyle::Posix,
164            encoding: PathEncoding::Utf8,
165        }
166    }
167}
168
169pub fn local_transport() -> LocalTransportKind {
170    #[cfg(windows)]
171    {
172        return LocalTransportKind::WindowsNamedPipe;
173    }
174    #[cfg(unix)]
175    {
176        LocalTransportKind::UnixDomainSocket
177    }
178}
179
180pub fn workspace_root_supported(value: &str) -> bool {
181    #[cfg(windows)]
182    if value.starts_with(r"\\") {
183        return false;
184    }
185    !value.is_empty()
186        && !value.chars().any(char::is_control)
187        && Path::new(value).is_absolute()
188}
189
190#[cfg(all(test, unix))]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn unix_platform_contract_is_native_and_case_sensitive() {
196        let endpoint = default_node_endpoint().expect("Unix runtime endpoint");
197        assert!(endpoint.is_absolute());
198        assert!(validate_endpoint(endpoint.to_str().expect("UTF-8 endpoint")));
199        assert!(!roots_equal("/tmp/Repo", "/tmp/repo"));
200        assert_eq!(path_semantics().style, PathStyle::Posix);
201        assert_eq!(path_semantics().encoding, PathEncoding::Utf8);
202        assert_eq!(local_transport(), LocalTransportKind::UnixDomainSocket);
203        let host = host_descriptor().unwrap();
204        assert_eq!(host.operating_system.as_str(), std::env::consts::OS);
205        assert_eq!(host.architecture.as_str(), std::env::consts::ARCH);
206    }
207}