clawft_kernel/node_facts/
runtimes.rs1use clawft_types::placement::{CapabilityState, Provenance};
11
12use super::host::ProbeHost;
13use super::linux::{emulated_from_listing, host_binfmt_emulated};
14use super::probe::{Collected, ProbeConfig, cap, normalize_arch, str_list};
15
16const ROSETTA: &str = "/Library/Apple/usr/share/rosetta/rosetta";
17
18pub const BINFMT_LIST: &str = "mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc 2>/dev/null; \
20cd /proc/sys/fs/binfmt_misc && for f in *; do [ \"$f\" = register ] || [ \"$f\" = status ] || \
21echo \"$f $(head -n1 \"$f\")\"; done";
22
23fn host_emulated(host: &dyn ProbeHost, arch: &str) -> Vec<String> {
24 match host.os().as_str() {
25 "linux" => host_binfmt_emulated(host, arch),
26 "macos" if host.exists(ROSETTA) && arch != "x86_64" => vec!["x86_64".into()],
27 _ => Vec::new(),
28 }
29}
30
31pub fn probe(host: &dyn ProbeHost, arch: &str, cfg: &ProbeConfig, c: &mut Collected) {
33 let host_emu = host_emulated(host, arch);
34 if let Some(n) = cap("runtime.native", Provenance::Probed) {
35 c.push(
36 n.with_attr("arches_native", str_list(&[arch]))
37 .with_attr("arches_emulated", str_list(&host_emu)),
38 );
39 }
40 docker(host, arch, &host_emu, cfg, c);
41 podman(host, arch, &host_emu, c);
42 if host.os() == "macos" {
43 apple_container(host, arch, &host_emu, c);
44 }
45 inference(host, arch, c);
46}
47
48pub fn docker_variant(operating_system: &str) -> &'static str {
50 if operating_system.contains("OrbStack") {
51 "orbstack"
52 } else if operating_system.contains("Docker Desktop") {
53 "desktop"
54 } else {
55 "engine"
56 }
57}
58
59pub fn buildx_arches(out: &str) -> Vec<String> {
61 let Some(line) = out
62 .lines()
63 .find(|l| l.trim_start().starts_with("Platforms:"))
64 else {
65 return Vec::new();
66 };
67 let mut v: Vec<String> = line
68 .split_once(':')
69 .map(|(_, r)| r)
70 .unwrap_or_default()
71 .split(',')
72 .filter_map(|p| p.trim().strip_prefix("linux/"))
73 .map(|p| match p {
74 "arm/v7" | "arm/v6" => normalize_arch(p),
75 other => normalize_arch(other.split('/').next().unwrap_or(other)),
76 })
77 .filter(|a| !a.is_empty())
78 .collect();
79 v.sort();
80 v.dedup();
81 v
82}
83
84fn docker(
85 host: &dyn ProbeHost,
86 arch: &str,
87 host_emu: &[String],
88 cfg: &ProbeConfig,
89 c: &mut Collected,
90) {
91 if host.which("docker").is_none() {
92 return;
93 }
94 let info = host
95 .run("docker", &["info", "--format", "{{json .}}"])
96 .and_then(|s| serde_json::from_str::<serde_json::Value>(s.trim()).ok());
97 let Some(info) = info.filter(|v| v["ServerVersion"].is_string()) else {
98 if let Some(d) = cap("runtime.container.docker", Provenance::Probed) {
99 c.push(d.with_state(CapabilityState::Degraded));
100 }
101 c.note(
102 "docker",
103 "docker CLI present but the engine did not answer `docker info`; degraded",
104 );
105 return;
106 };
107 let os = info["OperatingSystem"].as_str().unwrap_or_default();
108 let variant = docker_variant(os);
109 let native = info["Architecture"]
110 .as_str()
111 .map(normalize_arch)
112 .unwrap_or_else(|| arch.into());
113 let local_linux_engine = host.os() == "linux" && variant == "engine";
114 let (mut emulated, source) = if local_linux_engine {
115 (
116 host_emu.to_vec(),
117 "host binfmt_misc (shared kernel)".to_string(),
118 )
119 } else {
120 vm_emulation(host, &native, cfg)
121 };
122 emulated.retain(|a| *a != native);
123 let Some(mut d) = cap("runtime.container.docker", Provenance::Probed) else {
124 return;
125 };
126 d = d
127 .with_attr("variant", variant)
128 .with_attr("arches_native", str_list(&[native.as_str()]))
129 .with_attr("arches_emulated", str_list(&emulated));
130 if let Some(v) = info["ServerVersion"].as_str() {
131 d = d.with_attr("version", v);
132 }
133 c.push(d);
134 let context = host
137 .run("docker", &["context", "show"])
138 .map(|s| public_context(s.trim()))
139 .unwrap_or_else(|| "unknown".into());
140 c.note(
141 "docker",
142 format!(
143 "docker info (variant {variant}); emulation from {source}; \
144 probed as the daemon's user with docker context {context}"
145 ),
146 );
147}
148
149const KNOWN_CONTEXTS: &[&str] = &["default", "desktop-linux", "orbstack", "colima", "rancher-desktop"];
152
153fn public_context(name: &str) -> String {
154 if KNOWN_CONTEXTS.contains(&name) {
155 name.to_string()
156 } else if name.is_empty() {
157 "unknown".into()
158 } else {
159 "custom".into()
160 }
161}
162
163fn vm_emulation(host: &dyn ProbeHost, native: &str, cfg: &ProbeConfig) -> (Vec<String>, String) {
164 let mut arches = host
165 .run("docker", &["buildx", "inspect"])
166 .map(|o| buildx_arches(&o))
167 .unwrap_or_default();
168 let mut sources = vec!["buildx platforms"];
169 let image = cfg.docker_probe_image.as_deref().filter(|img| {
170 super::probe::valid_image_ref(img)
171 && host
172 .run("docker", &["image", "inspect", "--format", "{{.Id}}", img])
173 .is_some()
174 });
175 match image {
176 Some(img) => {
177 let cached = cfg.emulation_cache.as_ref().and_then(|c| c.get(img));
178 let listing = cached.or_else(|| {
179 let l = host.run(
180 "docker",
181 &[
182 "run",
183 "--rm",
184 "--privileged",
185 "--pull=never",
186 "--network=none",
187 "--entrypoint",
188 "/bin/sh",
189 img,
190 "-c",
191 BINFMT_LIST,
192 ],
193 );
194 if let (Some(l), Some(c)) = (&l, &cfg.emulation_cache) {
195 c.put(img, l);
196 }
197 l
198 });
199 if let Some(l) = listing {
200 arches.extend(emulated_from_listing(&l, native));
201 sources.push("engine VM binfmt_misc");
202 }
203 }
204 None => sources.push("(engine VM binfmt not listed: probe image not present locally)"),
205 }
206 arches.retain(|a| a != native);
207 arches.sort();
208 arches.dedup();
209 (arches, sources.join(" + "))
210}
211
212fn podman(host: &dyn ProbeHost, arch: &str, host_emu: &[String], c: &mut Collected) {
213 if host.which("podman").is_none() {
214 return;
215 }
216 let info = host
217 .run("podman", &["info", "--format", "json"])
218 .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
219 let Some(mut p) = cap("runtime.container.podman", Provenance::Probed) else {
220 return;
221 };
222 let Some(info) = info else {
223 c.push(p.with_state(CapabilityState::Degraded));
224 c.note(
225 "podman",
226 "podman present but `podman info` failed; degraded",
227 );
228 return;
229 };
230 let native = info["host"]["arch"]
231 .as_str()
232 .map(normalize_arch)
233 .unwrap_or_else(|| arch.into());
234 let emulated: Vec<String> = if host.os() == "linux" {
235 host_emu.to_vec()
236 } else {
237 Vec::new()
238 };
239 p = p
240 .with_attr("arches_native", str_list(&[native.as_str()]))
241 .with_attr("arches_emulated", str_list(&emulated));
242 if let Some(v) = info["version"]["Version"].as_str() {
243 p = p.with_attr("version", v);
244 }
245 c.push(p);
246}
247
248fn apple_container(host: &dyn ProbeHost, arch: &str, host_emu: &[String], c: &mut Collected) {
249 let Some(out) = host.run("container", &["--version"]) else {
250 return;
251 };
252 let Some(mut a) = cap("runtime.container.apple", Provenance::Probed) else {
253 return;
254 };
255 if let Some(v) = out
256 .split("version")
257 .nth(1)
258 .and_then(|r| r.split_whitespace().next())
259 {
260 a = a.with_attr("version", v);
261 }
262 a = a
263 .with_attr("arches_native", str_list(&[arch]))
264 .with_attr("arches_emulated", str_list(host_emu));
265 if host.run("container", &["system", "status"]).is_none() {
266 a = a.with_state(CapabilityState::Degraded);
267 c.note(
268 "container",
269 "Apple container CLI present but its system service is not running; degraded",
270 );
271 }
272 c.push(a);
273}
274
275fn inference(host: &dyn ProbeHost, arch: &str, c: &mut Collected) {
276 if host.which("llama-server").is_some()
277 && let Some(mut l) = cap("runtime.infer.llamacpp", Provenance::Probed)
278 {
279 let ver = host.run_all("llama-server", &["--version"]).and_then(|o| {
280 o.lines()
281 .find_map(|l| l.trim().strip_prefix("version:"))
282 .and_then(|v| v.split_whitespace().next().map(str::to_string))
283 });
284 if let Some(v) = ver {
285 l = l.with_attr("version", v.as_str());
286 }
287 c.push(
288 l.with_attr("formats", str_list(&["gguf"]))
289 .with_attr("arches_native", str_list(&[arch])),
290 );
291 }
292 if (host.which("mlx_lm.server").is_some() || host.which("mlx_lm.generate").is_some())
293 && let Some(m) = cap("runtime.infer.mlx-lm", Provenance::Probed)
294 {
295 c.push(
296 m.with_attr("formats", str_list(&["mlx"]))
297 .with_attr("arches_native", str_list(&[arch])),
298 );
299 c.note(
300 "runtime.infer.mlx-lm",
301 "mlx_lm entry point on PATH; version not probed (python import is slow)",
302 );
303 }
304 if host.which("ollama").is_some()
305 && let Some(mut o) = cap("runtime.infer.ollama", Provenance::Probed)
306 {
307 if let Some(v) = host
308 .run("ollama", &["--version"])
309 .and_then(|s| s.split("version is").nth(1).map(|v| v.trim().to_string()))
310 .filter(|v| !v.is_empty())
311 {
312 o = o.with_attr("version", v.as_str());
313 }
314 c.push(
315 o.with_attr("formats", str_list(&["gguf"]))
316 .with_attr("arches_native", str_list(&[arch])),
317 );
318 }
319}