gate4agent_node_core/
platform.rs1use 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}