1pub fn resolve_binary_path(command: &str) -> Result<String, String> {
26 resolve_binary_path_inner(command, 0)
28}
29
30pub fn resolve_binary_path_for_ssl(command: &str) -> Result<Option<String>, String> {
38 let launcher = resolve_command_path(command)?;
39 if is_openai_codex_native_binary(&launcher) {
40 return Ok(Some(canonicalize_path(&launcher)));
41 }
42 if let Some(path) = codex_native_binary_from_launcher(&launcher) {
43 return Ok(Some(path));
44 }
45
46 let resolved = resolve_binary_path(command)?;
47 if binary_embeds_ssl(&resolved) {
48 Ok(Some(resolved))
49 } else {
50 Ok(None)
51 }
52}
53
54fn resolve_binary_path_inner(command: &str, depth: u8) -> Result<String, String> {
55 if depth > 5 {
56 return Err(format!(
57 "too many nested shebang wrappers resolving '{}'",
58 command
59 ));
60 }
61
62 let resolved = resolve_command_path(command)?;
63
64 let mut header = [0u8; 256];
66 let n = {
67 use std::io::Read;
68 let mut f = std::fs::File::open(&resolved)
69 .map_err(|e| format!("cannot open '{}': {}", resolved.display(), e))?;
70 f.read(&mut header)
71 .map_err(|e| format!("cannot read '{}': {}", resolved.display(), e))?
72 };
73 let header = &header[..n];
74
75 if header.starts_with(b"\x7fELF") {
76 return Ok(resolved.to_string_lossy().into_owned());
77 }
78
79 if header.starts_with(b"#!") {
80 let line_end = header
82 .iter()
83 .position(|&b| b == b'\n')
84 .unwrap_or(header.len());
85 let line = String::from_utf8_lossy(&header[2..line_end]);
86 let mut parts = line.split_whitespace();
87 let interp = parts
88 .next()
89 .ok_or_else(|| format!("'{}' has an empty shebang", resolved.display()))?;
90 let next = if interp.ends_with("/env") || interp == "env" {
92 parts
93 .next()
94 .ok_or_else(|| format!("'{}' uses env with no interpreter", resolved.display()))?
95 } else {
96 interp
97 };
98 return resolve_binary_path_inner(next, depth + 1);
99 }
100
101 Err(format!(
102 "'{}' is neither an ELF binary nor a shebang script; specify --binary-path explicitly",
103 resolved.display()
104 ))
105}
106
107fn resolve_command_path(command: &str) -> Result<std::path::PathBuf, String> {
109 let candidate = if command.contains('/') {
110 std::path::PathBuf::from(command)
111 } else {
112 find_in_path(command).ok_or_else(|| format!("'{}' not found in $PATH", command))?
113 };
114
115 std::fs::canonicalize(&candidate)
116 .map_err(|e| format!("cannot resolve '{}': {}", candidate.display(), e))
117}
118
119fn find_in_path(cmd: &str) -> Option<std::path::PathBuf> {
126 let mut dirs: Vec<std::path::PathBuf> = Vec::new();
127
128 if let Some(path) = std::env::var_os("PATH") {
129 dirs.extend(std::env::split_paths(&path));
130 }
131
132 if let Some(user) = std::env::var_os("SUDO_USER")
133 && let Some(home) = sudo_user_home(&user)
134 {
135 dirs.push(home.join(".local/bin"));
136 dirs.push(home.join("bin"));
137 if let Some(nvm_bin) = newest_nvm_bin(&home) {
139 dirs.push(nvm_bin);
140 }
141 }
142
143 find_executable_in_dirs(cmd, dirs)
144}
145
146fn find_executable_in_dirs(
147 cmd: &str,
148 dirs: impl IntoIterator<Item = std::path::PathBuf>,
149) -> Option<std::path::PathBuf> {
150 for dir in dirs {
151 let full = dir.join(cmd);
152 if let Ok(meta) = std::fs::metadata(&full)
153 && meta.is_file()
154 {
155 return Some(full);
156 }
157 }
158 None
159}
160
161fn sudo_user_home(user: &std::ffi::OsStr) -> Option<std::path::PathBuf> {
163 let user = user.to_str()?;
164 let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
165 for line in passwd.lines() {
166 let mut fields = line.split(':');
167 if fields.next() == Some(user) {
168 return fields.nth(4).map(std::path::PathBuf::from);
170 }
171 }
172 None
173}
174
175fn newest_nvm_bin(home: &std::path::Path) -> Option<std::path::PathBuf> {
177 let versions = home.join(".nvm/versions/node");
178 let mut entries: Vec<_> = std::fs::read_dir(&versions)
179 .ok()?
180 .filter_map(|e| e.ok())
181 .map(|e| e.path())
182 .collect();
183 entries.sort();
184 entries.last().map(|p| p.join("bin"))
185}
186
187pub fn binary_embeds_ssl(path: &str) -> bool {
198 use std::io::Read;
199 const NEEDLES: &[&[u8]] = &[
200 b"SSL_write",
201 b"BoringSSLError",
202 b"OPENSSL_internal",
203 b"grok-cli",
204 ];
205 let mut f = match std::fs::File::open(path) {
206 Ok(f) => f,
207 Err(_) => return false,
208 };
209 let mut buf = vec![0u8; 1 << 20]; let mut carry: Vec<u8> = Vec::new();
212 let keep = NEEDLES
213 .iter()
214 .map(|needle| needle.len())
215 .max()
216 .unwrap_or(1)
217 .saturating_sub(1);
218 loop {
219 let n = match f.read(&mut buf) {
220 Ok(0) => break,
221 Ok(n) => n,
222 Err(_) => return false,
223 };
224 carry.extend_from_slice(&buf[..n]);
225 if NEEDLES
226 .iter()
227 .any(|needle| carry.windows(needle.len()).any(|w| w == *needle))
228 {
229 return true;
230 }
231 if carry.len() > keep {
232 carry.drain(..carry.len() - keep);
233 }
234 }
235 false
236}
237
238fn codex_native_binary_from_launcher(launcher: &std::path::Path) -> Option<String> {
239 let package_root = openai_codex_package_root(launcher)?;
240
241 let nested_scope = package_root.join("node_modules").join("@openai");
242 if let Some(path) = codex_native_binary_in_scope(&nested_scope) {
243 return Some(path);
244 }
245 if let Some(scope) = package_root.parent() {
246 if let Some(path) = codex_native_binary_in_scope(scope) {
247 return Some(path);
248 }
249 }
250
251 None
252}
253
254fn codex_native_binary_in_scope(openai_scope: &std::path::Path) -> Option<String> {
255 const CANDIDATES: &[(&str, &str)] = &[
256 ("codex-linux-x64", "x86_64-unknown-linux-musl"),
257 ("codex-linux-arm64", "aarch64-unknown-linux-musl"),
258 ];
259
260 for (package, target) in CANDIDATES {
261 let path = openai_scope
262 .join(package)
263 .join("vendor")
264 .join(target)
265 .join("bin")
266 .join("codex");
267 if is_elf_file(&path) {
268 return Some(canonicalize_path(&path));
269 }
270 }
271 None
272}
273
274fn is_openai_codex_native_binary(path: &std::path::Path) -> bool {
275 if !is_elf_file(path) || !file_name_eq(path, "codex") {
276 return false;
277 }
278
279 let Some(bin_dir) = path.parent().filter(|p| file_name_eq(p, "bin")) else {
280 return false;
281 };
282 let Some(target_dir) = bin_dir.parent().filter(|p| {
283 file_name_eq(p, "x86_64-unknown-linux-musl")
284 || file_name_eq(p, "aarch64-unknown-linux-musl")
285 }) else {
286 return false;
287 };
288 let Some(vendor_dir) = target_dir.parent().filter(|p| file_name_eq(p, "vendor")) else {
289 return false;
290 };
291 let Some(package_dir) = vendor_dir
292 .parent()
293 .filter(|p| file_name_eq(p, "codex-linux-x64") || file_name_eq(p, "codex-linux-arm64"))
294 else {
295 return false;
296 };
297 package_dir
298 .parent()
299 .is_some_and(|parent| file_name_eq(parent, "@openai"))
300}
301
302fn openai_codex_package_root(path: &std::path::Path) -> Option<std::path::PathBuf> {
303 for ancestor in path.ancestors() {
304 if file_name_eq(ancestor, "codex")
305 && ancestor
306 .parent()
307 .is_some_and(|parent| file_name_eq(parent, "@openai"))
308 && ancestor
309 .parent()
310 .and_then(|parent| parent.parent())
311 .is_some_and(|parent| file_name_eq(parent, "node_modules"))
312 {
313 return Some(ancestor.to_path_buf());
314 }
315 }
316 None
317}
318
319fn file_name_eq(path: &std::path::Path, expected: &str) -> bool {
320 path.file_name().and_then(|name| name.to_str()) == Some(expected)
321}
322
323fn is_elf_file(path: &std::path::Path) -> bool {
324 use std::io::Read;
325 let mut header = [0u8; 4];
326 let mut f = match std::fs::File::open(path) {
327 Ok(f) => f,
328 Err(_) => return false,
329 };
330 f.read_exact(&mut header).is_ok() && header == *b"\x7fELF"
331}
332
333fn canonicalize_path(path: &std::path::Path) -> String {
334 std::fs::canonicalize(path)
335 .map(|p| p.to_string_lossy().into_owned())
336 .unwrap_or_else(|_| path.to_string_lossy().into_owned())
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
340struct KubernetesRef<'a> {
341 namespace: &'a str,
342 pod: &'a str,
343 container: Option<&'a str>,
344}
345
346impl KubernetesRef<'_> {
347 fn label(&self) -> String {
348 match self.container {
349 Some(container) => format!("k8s://{}/{}/{}", self.namespace, self.pod, container),
350 None => format!("k8s://{}/{}", self.namespace, self.pod),
351 }
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
356struct RuntimeContainerRef {
357 runtime: String,
358 id: String,
359}
360
361pub fn parse_container_ref(binary_path: &str) -> Option<&str> {
365 binary_path
366 .strip_prefix("docker://")
367 .or_else(|| binary_path.strip_prefix("docker:"))
368 .filter(|r| !r.is_empty() && !r.contains('/'))
369}
370
371fn has_docker_scheme(binary_path: &str) -> bool {
372 binary_path.starts_with("docker://") || binary_path.starts_with("docker:")
373}
374
375fn parse_kubernetes_ref(binary_path: &str) -> Option<KubernetesRef<'_>> {
383 let reference = binary_path
384 .strip_prefix("k8s://")
385 .or_else(|| binary_path.strip_prefix("k8s:"))
386 .or_else(|| binary_path.strip_prefix("kubernetes://"))
387 .or_else(|| binary_path.strip_prefix("kubernetes:"))?;
388
389 let parts = reference.split('/').collect::<Vec<_>>();
390 if parts.is_empty() || parts.iter().any(|part| part.is_empty()) {
391 return None;
392 }
393
394 match parts.as_slice() {
395 [pod] => Some(KubernetesRef {
396 namespace: "default",
397 pod,
398 container: None,
399 }),
400 [namespace, pod] => Some(KubernetesRef {
401 namespace,
402 pod,
403 container: None,
404 }),
405 [namespace, pod, container] => Some(KubernetesRef {
406 namespace,
407 pod,
408 container: Some(container),
409 }),
410 _ => None,
411 }
412}
413
414fn has_kubernetes_scheme(binary_path: &str) -> bool {
415 binary_path.starts_with("k8s://")
416 || binary_path.starts_with("k8s:")
417 || binary_path.starts_with("kubernetes://")
418 || binary_path.starts_with("kubernetes:")
419}
420
421pub fn resolve_container_binary_arg(
422 binary_path: Option<&str>,
423) -> Result<Option<(String, String)>, String> {
424 let Some(binary_path) = binary_path else {
425 return Ok(None);
426 };
427
428 if let Some(reference) = parse_container_ref(binary_path) {
429 return resolve_container_binary_path(reference)
430 .map(|path| Some((reference.to_string(), path)));
431 }
432 if has_docker_scheme(binary_path) {
433 return Err(format!(
434 "invalid Docker container reference '{}'; expected docker://<name|id>",
435 binary_path
436 ));
437 }
438
439 if let Some(reference) = parse_kubernetes_ref(binary_path) {
440 let label = reference.label();
441 return resolve_kubernetes_binary_path(&reference).map(|path| Some((label, path)));
442 }
443 if has_kubernetes_scheme(binary_path) {
444 return Err(format!(
445 "invalid Kubernetes pod reference '{}'; expected k8s://pod, k8s://namespace/pod, or k8s://namespace/pod/container",
446 binary_path
447 ));
448 }
449
450 Ok(None)
451}
452
453pub fn resolve_container_binary_path(reference: &str) -> Result<String, String> {
466 let init_pid = resolve_docker_container_pid(reference)?;
467
468 find_ssl_target_in_tree(init_pid).ok_or_else(|| {
469 format!(
470 "container '{}' is running at host PID {}, but no SSL attach target was found in its process tree",
471 reference, init_pid
472 )
473 })
474}
475
476fn resolve_docker_container_pid(reference: &str) -> Result<u32, String> {
477 let output = std::process::Command::new("docker")
478 .args(["inspect", "--format", "{{.State.Pid}}", reference])
479 .output()
480 .map_err(|e| format!(
481 "failed to run `docker inspect` for container '{}': {} (is the Docker CLI installed and on $PATH?)",
482 reference, e
483 ))?;
484
485 if !output.status.success() {
486 let stderr = String::from_utf8_lossy(&output.stderr);
487 return Err(format!(
488 "`docker inspect {}` failed: {}",
489 reference,
490 stderr.trim()
491 ));
492 }
493
494 let init_pid: u32 = String::from_utf8_lossy(&output.stdout)
495 .trim()
496 .parse()
497 .map_err(|_| format!("could not determine host PID for container '{}'", reference))?;
498
499 if init_pid == 0 {
500 return Err(format!(
501 "container '{}' is not running (host PID 0)",
502 reference
503 ));
504 }
505
506 Ok(init_pid)
507}
508
509fn resolve_kubernetes_binary_path(reference: &KubernetesRef<'_>) -> Result<String, String> {
510 let pod = kubectl_get_pod(reference)?;
511 let container_id = select_kubernetes_container_id(&pod, reference)?;
512 let runtime = parse_runtime_container_id(&container_id)?;
513 let init_pid = resolve_runtime_container_pid(&runtime)?;
514
515 find_ssl_target_in_tree(init_pid).ok_or_else(|| {
516 let node = pod
517 .pointer("/spec/nodeName")
518 .and_then(serde_json::Value::as_str)
519 .unwrap_or("unknown");
520 format!(
521 "Kubernetes pod '{}/{}' container target '{}' is running at host PID {}, but no SSL attach target was found in its process tree. AgentSight must run on the node that hosts the pod (node: {}).",
522 reference.namespace,
523 reference.pod,
524 runtime.id,
525 init_pid,
526 node
527 )
528 })
529}
530
531fn kubectl_get_pod(reference: &KubernetesRef<'_>) -> Result<serde_json::Value, String> {
532 let output = kubectl_command()
533 .args([
534 "get",
535 "pod",
536 reference.pod,
537 "-n",
538 reference.namespace,
539 "-o",
540 "json",
541 ])
542 .output()
543 .map_err(|e| format!(
544 "failed to run `kubectl get pod {}` in namespace '{}': {} (is kubectl installed and configured?)",
545 reference.pod, reference.namespace, e
546 ))?;
547
548 if !output.status.success() {
549 let stderr = String::from_utf8_lossy(&output.stderr);
550 return Err(format!(
551 "`kubectl get pod {} -n {}` failed: {}",
552 reference.pod,
553 reference.namespace,
554 stderr.trim()
555 ));
556 }
557
558 serde_json::from_slice(&output.stdout).map_err(|e| {
559 format!(
560 "`kubectl get pod {} -n {} -o json` returned invalid JSON: {}",
561 reference.pod, reference.namespace, e
562 )
563 })
564}
565
566fn kubectl_command() -> std::process::Command {
567 let mut command = std::process::Command::new("kubectl");
568 if std::env::var_os("KUBECONFIG").is_none()
569 && let Some(user) = std::env::var_os("SUDO_USER")
570 && let Some(home) = sudo_user_home(&user)
571 {
572 let kubeconfig = home.join(".kube/config");
573 if kubeconfig.is_file() {
574 command.env("KUBECONFIG", kubeconfig);
575 }
576 }
577 command
578}
579
580fn select_kubernetes_container_id(
581 pod: &serde_json::Value,
582 reference: &KubernetesRef<'_>,
583) -> Result<String, String> {
584 let statuses = pod
585 .pointer("/status/containerStatuses")
586 .and_then(serde_json::Value::as_array)
587 .ok_or_else(|| {
588 format!(
589 "Kubernetes pod '{}/{}' has no status.containerStatuses yet",
590 reference.namespace, reference.pod
591 )
592 })?;
593
594 if let Some(container) = reference.container {
595 let status = statuses
596 .iter()
597 .find(|status| {
598 status.get("name").and_then(serde_json::Value::as_str) == Some(container)
599 })
600 .ok_or_else(|| {
601 format!(
602 "Kubernetes pod '{}/{}' has no container named '{}'",
603 reference.namespace, reference.pod, container
604 )
605 })?;
606 return container_id_from_status(status).ok_or_else(|| {
607 format!(
608 "Kubernetes pod '{}/{}' container '{}' has no containerID yet (is it running?)",
609 reference.namespace, reference.pod, container
610 )
611 });
612 }
613
614 let containers = statuses
615 .iter()
616 .filter_map(|status| {
617 let name = status.get("name").and_then(serde_json::Value::as_str)?;
618 let id = container_id_from_status(status)?;
619 Some((name, id))
620 })
621 .collect::<Vec<_>>();
622
623 match containers.as_slice() {
624 [(_, id)] => Ok(id.clone()),
625 [] => Err(format!(
626 "Kubernetes pod '{}/{}' has no running containers with a containerID",
627 reference.namespace, reference.pod
628 )),
629 _ => {
630 let names = containers
631 .iter()
632 .map(|(name, _)| *name)
633 .collect::<Vec<_>>()
634 .join(", ");
635 Err(format!(
636 "Kubernetes pod '{}/{}' has multiple containers ({}); specify one as k8s://{}/{}/<container>",
637 reference.namespace, reference.pod, names, reference.namespace, reference.pod
638 ))
639 }
640 }
641}
642
643fn container_id_from_status(status: &serde_json::Value) -> Option<String> {
644 status.pointer("/state/running")?;
645 let id = status.get("containerID")?.as_str()?.trim();
646 (!id.is_empty()).then(|| id.to_string())
647}
648
649fn parse_runtime_container_id(container_id: &str) -> Result<RuntimeContainerRef, String> {
650 let (runtime, id) = container_id.split_once("://").ok_or_else(|| {
651 format!(
652 "Kubernetes containerID '{}' is missing a runtime scheme",
653 container_id
654 )
655 })?;
656 let id = id.trim();
657 if runtime.trim().is_empty() || id.is_empty() {
658 return Err(format!(
659 "Kubernetes containerID '{}' is incomplete",
660 container_id
661 ));
662 }
663 Ok(RuntimeContainerRef {
664 runtime: runtime.to_string(),
665 id: id.to_string(),
666 })
667}
668
669fn resolve_runtime_container_pid(container: &RuntimeContainerRef) -> Result<u32, String> {
670 match container.runtime.as_str() {
671 "docker" => resolve_docker_container_pid(&container.id),
672 "containerd" | "cri-o" | "crio" => resolve_cri_container_pid(&container.id),
673 other => resolve_cri_container_pid(&container.id).map_err(|e| {
674 format!(
675 "unsupported Kubernetes container runtime '{}' for container '{}': {}",
676 other, container.id, e
677 )
678 }),
679 }
680}
681
682fn resolve_cri_container_pid(container_id: &str) -> Result<u32, String> {
683 let output = std::process::Command::new("crictl")
684 .args(["inspect", "--output", "json", container_id])
685 .output()
686 .map_err(|e| format!(
687 "failed to run `crictl inspect` for container '{}': {} (is crictl installed and configured for this node's CRI runtime?)",
688 container_id, e
689 ))?;
690
691 if !output.status.success() {
692 let stderr = String::from_utf8_lossy(&output.stderr);
693 return Err(format!(
694 "`crictl inspect {}` failed: {}",
695 container_id,
696 stderr.trim()
697 ));
698 }
699
700 let value: serde_json::Value = serde_json::from_slice(&output.stdout).map_err(|e| {
701 format!(
702 "`crictl inspect --output json {}` returned invalid JSON: {}",
703 container_id, e
704 )
705 })?;
706
707 parse_crictl_pid(&value).ok_or_else(|| {
708 format!(
709 "could not determine host PID for CRI container '{}'",
710 container_id
711 )
712 })
713}
714
715fn parse_crictl_pid(value: &serde_json::Value) -> Option<u32> {
716 ["/info/pid", "/status/pid"]
717 .into_iter()
718 .filter_map(|path| value.pointer(path))
719 .find_map(value_as_u32)
720 .filter(|pid| *pid != 0)
721}
722
723fn value_as_u32(value: &serde_json::Value) -> Option<u32> {
724 if let Some(pid) = value.as_u64() {
725 return u32::try_from(pid).ok();
726 }
727 value.as_str()?.parse().ok()
728}
729
730fn find_ssl_target_in_tree(root_pid: u32) -> Option<String> {
737 let mut queue = std::collections::VecDeque::from([root_pid]);
738 let mut seen = std::collections::HashSet::new();
739 while let Some(pid) = queue.pop_front() {
740 if !seen.insert(pid) {
741 continue;
742 }
743 let exe = format!("/proc/{}/exe", pid);
744 if binary_embeds_ssl(&exe) {
745 return Some(canonicalize_attach_path(&exe));
746 }
747 if let Some(path) = find_loaded_ssl_library(pid) {
748 return Some(path);
749 }
750 let children_path = format!("/proc/{}/task/{}/children", pid, pid);
751 if let Ok(children) = std::fs::read_to_string(&children_path) {
752 for child in children
753 .split_whitespace()
754 .filter_map(|s| s.parse::<u32>().ok())
755 {
756 queue.push_back(child);
757 }
758 }
759 }
760 None
761}
762
763fn find_loaded_ssl_library(pid: u32) -> Option<String> {
764 let maps = std::fs::read_to_string(format!("/proc/{pid}/maps")).ok()?;
765 for line in maps.lines() {
766 let path = line.split_whitespace().last()?;
767 if !path.starts_with('/') || !path.contains("libssl.so") {
768 continue;
769 }
770 let host_path = format!("/proc/{pid}/root{path}");
771 if std::fs::metadata(&host_path).is_ok() {
772 return Some(canonicalize_attach_path(&host_path));
773 }
774 }
775 None
776}
777
778fn canonicalize_attach_path(path: &str) -> String {
779 std::fs::canonicalize(path)
780 .map(|p| p.to_string_lossy().into_owned())
781 .unwrap_or_else(|_| path.to_string())
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use serde_json::json;
788
789 #[test]
790 fn parses_docker_double_slash_scheme() {
791 assert_eq!(parse_container_ref("docker://openclaw"), Some("openclaw"));
792 assert_eq!(
793 parse_container_ref("docker://my-agent-1"),
794 Some("my-agent-1")
795 );
796 }
797
798 #[test]
799 fn parses_docker_colon_scheme() {
800 assert_eq!(parse_container_ref("docker:openclaw"), Some("openclaw"));
801 assert_eq!(
803 parse_container_ref("docker:abc123def456"),
804 Some("abc123def456")
805 );
806 }
807
808 #[test]
809 fn ignores_plain_filesystem_paths() {
810 assert_eq!(parse_container_ref("/proc/1234/exe"), None);
811 assert_eq!(parse_container_ref("/usr/bin/node"), None);
812 assert_eq!(
813 parse_container_ref("~/.nvm/versions/node/v20.0.0/bin/node"),
814 None
815 );
816 }
817
818 #[test]
819 fn rejects_empty_container_reference() {
820 assert_eq!(parse_container_ref("docker://"), None);
821 assert_eq!(parse_container_ref("docker:"), None);
822 }
823
824 #[test]
825 fn rejects_slash_separated_docker_reference() {
826 assert_eq!(parse_container_ref("docker://foo/bar"), None);
827 assert_eq!(parse_container_ref("docker:foo/bar"), None);
828 }
829
830 #[test]
831 fn parses_kubernetes_pod_reference_with_default_namespace() {
832 assert_eq!(
833 parse_kubernetes_ref("k8s://openclaw"),
834 Some(KubernetesRef {
835 namespace: "default",
836 pod: "openclaw",
837 container: None,
838 })
839 );
840 }
841
842 #[test]
843 fn parses_kubernetes_namespaced_pod_reference() {
844 assert_eq!(
845 parse_kubernetes_ref("k8s://agents/openclaw"),
846 Some(KubernetesRef {
847 namespace: "agents",
848 pod: "openclaw",
849 container: None,
850 })
851 );
852 }
853
854 #[test]
855 fn parses_kubernetes_container_reference() {
856 let reference = parse_kubernetes_ref("kubernetes://agents/openclaw/gateway");
857 assert_eq!(
858 reference,
859 Some(KubernetesRef {
860 namespace: "agents",
861 pod: "openclaw",
862 container: Some("gateway"),
863 })
864 );
865 assert_eq!(
866 reference.as_ref().map(KubernetesRef::label),
867 Some("k8s://agents/openclaw/gateway".to_string())
868 );
869 }
870
871 #[test]
872 fn rejects_invalid_kubernetes_references() {
873 assert_eq!(parse_kubernetes_ref("k8s://"), None);
874 assert_eq!(parse_kubernetes_ref("k8s://agents/"), None);
875 assert_eq!(
876 parse_kubernetes_ref("k8s://agents/openclaw/gateway/extra"),
877 None
878 );
879 assert_eq!(parse_kubernetes_ref("/usr/bin/node"), None);
880 }
881
882 #[test]
883 fn invalid_container_scheme_errors_before_running_external_tools() {
884 assert!(
885 resolve_container_binary_arg(Some("docker://"))
886 .unwrap_err()
887 .contains("invalid Docker container reference")
888 );
889 assert!(
890 resolve_container_binary_arg(Some("k8s://agents/openclaw/gateway/extra"))
891 .unwrap_err()
892 .contains("invalid Kubernetes pod reference")
893 );
894 }
895
896 #[test]
897 fn selects_single_kubernetes_container_id() {
898 let pod = json!({
899 "status": {
900 "containerStatuses": [
901 {
902 "name": "gateway",
903 "state": {"running": {"startedAt": "2026-07-01T00:00:00Z"}},
904 "containerID": "containerd://abc123"
905 }
906 ]
907 }
908 });
909 let reference = KubernetesRef {
910 namespace: "agents",
911 pod: "openclaw",
912 container: None,
913 };
914
915 assert_eq!(
916 select_kubernetes_container_id(&pod, &reference).unwrap(),
917 "containerd://abc123"
918 );
919 }
920
921 #[test]
922 fn selects_explicit_kubernetes_container_id() {
923 let pod = json!({
924 "status": {
925 "containerStatuses": [
926 {
927 "name": "sidecar",
928 "state": {"running": {"startedAt": "2026-07-01T00:00:00Z"}},
929 "containerID": "containerd://sidecar123"
930 },
931 {
932 "name": "gateway",
933 "state": {"running": {"startedAt": "2026-07-01T00:00:00Z"}},
934 "containerID": "containerd://gateway123"
935 }
936 ]
937 }
938 });
939 let reference = KubernetesRef {
940 namespace: "agents",
941 pod: "openclaw",
942 container: Some("gateway"),
943 };
944
945 assert_eq!(
946 select_kubernetes_container_id(&pod, &reference).unwrap(),
947 "containerd://gateway123"
948 );
949 }
950
951 #[test]
952 fn requires_container_name_for_multi_container_pod() {
953 let pod = json!({
954 "status": {
955 "containerStatuses": [
956 {
957 "name": "sidecar",
958 "state": {"running": {"startedAt": "2026-07-01T00:00:00Z"}},
959 "containerID": "containerd://sidecar123"
960 },
961 {
962 "name": "gateway",
963 "state": {"running": {"startedAt": "2026-07-01T00:00:00Z"}},
964 "containerID": "containerd://gateway123"
965 }
966 ]
967 }
968 });
969 let reference = KubernetesRef {
970 namespace: "agents",
971 pod: "openclaw",
972 container: None,
973 };
974
975 let err = select_kubernetes_container_id(&pod, &reference).unwrap_err();
976 assert!(err.contains("multiple containers"));
977 assert!(err.contains("k8s://agents/openclaw/<container>"));
978 }
979
980 #[test]
981 fn ignores_non_running_kubernetes_container_id() {
982 let pod = json!({
983 "status": {
984 "containerStatuses": [
985 {
986 "name": "gateway",
987 "state": {"terminated": {"exitCode": 0}},
988 "containerID": "containerd://old123"
989 }
990 ]
991 }
992 });
993 let reference = KubernetesRef {
994 namespace: "agents",
995 pod: "openclaw",
996 container: None,
997 };
998
999 let err = select_kubernetes_container_id(&pod, &reference).unwrap_err();
1000 assert!(err.contains("no running containers"));
1001 }
1002
1003 #[test]
1004 fn parses_kubernetes_runtime_container_id() {
1005 assert_eq!(
1006 parse_runtime_container_id("containerd://abc123").unwrap(),
1007 RuntimeContainerRef {
1008 runtime: "containerd".to_string(),
1009 id: "abc123".to_string(),
1010 }
1011 );
1012 assert_eq!(
1013 parse_runtime_container_id("docker://def456").unwrap(),
1014 RuntimeContainerRef {
1015 runtime: "docker".to_string(),
1016 id: "def456".to_string(),
1017 }
1018 );
1019 assert!(parse_runtime_container_id("abc123").is_err());
1020 }
1021
1022 #[test]
1023 fn parses_crictl_pid_shapes() {
1024 assert_eq!(
1025 parse_crictl_pid(&json!({"info": {"pid": 1234}})),
1026 Some(1234)
1027 );
1028 assert_eq!(
1029 parse_crictl_pid(&json!({"status": {"pid": "5678"}})),
1030 Some(5678)
1031 );
1032 assert_eq!(parse_crictl_pid(&json!({"info": {"pid": 0}})), None);
1033 }
1034
1035 #[test]
1036 fn canonicalize_attach_path_resolves_proc_root_when_available() {
1037 assert_eq!(
1038 canonicalize_attach_path("/proc/self/root/etc/hosts"),
1039 "/etc/hosts"
1040 );
1041
1042 let dead_proc_path = "/proc/999999999/root/usr/lib/libssl.so";
1043 assert_eq!(canonicalize_attach_path(dead_proc_path), dead_proc_path);
1044 }
1045
1046 #[test]
1047 fn detects_boringssl_marker_in_static_binary() {
1048 let dir = tempfile::tempdir().unwrap();
1049 let path = dir.path().join("claude-like");
1050 std::fs::write(&path, b"prefix BoringSSLError suffix").unwrap();
1051
1052 assert!(binary_embeds_ssl(path.to_str().unwrap()));
1053 }
1054
1055 #[test]
1056 fn detects_grok_static_binary_marker() {
1057 let dir = tempfile::tempdir().unwrap();
1058 let path = dir.path().join("grok-like");
1059 std::fs::write(&path, b"prefix grok-cli suffix").unwrap();
1060
1061 assert!(binary_embeds_ssl(path.to_str().unwrap()));
1062 }
1063
1064 #[test]
1065 fn resolves_codex_npm_launcher_to_native_ssl_binary() {
1066 let dir = tempfile::tempdir().unwrap();
1067 let package_root = dir.path().join("node_modules/@openai/codex");
1068 let launcher = package_root.join("bin/codex.js");
1069 let native = package_root.join(
1070 "node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex",
1071 );
1072 std::fs::create_dir_all(launcher.parent().unwrap()).unwrap();
1073 std::fs::create_dir_all(native.parent().unwrap()).unwrap();
1074 std::fs::write(&launcher, b"#!/usr/bin/env node\n").unwrap();
1075 std::fs::write(&native, b"\x7fELFnative codex binary").unwrap();
1076
1077 let resolved = resolve_binary_path_for_ssl(launcher.to_str().unwrap()).unwrap();
1078 let expected = native
1079 .canonicalize()
1080 .unwrap()
1081 .to_string_lossy()
1082 .into_owned();
1083
1084 assert_eq!(resolved.as_deref(), Some(expected.as_str()));
1085 }
1086
1087 #[test]
1088 fn resolves_codex_npm_launcher_to_sibling_native_package() {
1089 let dir = tempfile::tempdir().unwrap();
1090 let package_root = dir.path().join("node_modules/@openai/codex");
1091 let launcher = package_root.join("bin/codex.js");
1092 let native = dir.path().join(
1093 "node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex",
1094 );
1095 std::fs::create_dir_all(launcher.parent().unwrap()).unwrap();
1096 std::fs::create_dir_all(native.parent().unwrap()).unwrap();
1097 std::fs::write(&launcher, b"#!/usr/bin/env node\n").unwrap();
1098 std::fs::write(&native, b"\x7fELFnative codex binary").unwrap();
1099
1100 let resolved = resolve_binary_path_for_ssl(launcher.to_str().unwrap()).unwrap();
1101 let expected = native
1102 .canonicalize()
1103 .unwrap()
1104 .to_string_lossy()
1105 .into_owned();
1106
1107 assert_eq!(resolved.as_deref(), Some(expected.as_str()));
1108 }
1109
1110 #[test]
1111 fn resolves_codex_native_package_binary_for_ssl() {
1112 let dir = tempfile::tempdir().unwrap();
1113 let native = dir.path().join(
1114 "node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex",
1115 );
1116 std::fs::create_dir_all(native.parent().unwrap()).unwrap();
1117 std::fs::write(&native, b"\x7fELFnative codex binary").unwrap();
1118
1119 let resolved = resolve_binary_path_for_ssl(native.to_str().unwrap()).unwrap();
1120 let expected = native
1121 .canonicalize()
1122 .unwrap()
1123 .to_string_lossy()
1124 .into_owned();
1125
1126 assert_eq!(resolved.as_deref(), Some(expected.as_str()));
1127 }
1128
1129 #[test]
1130 fn ignores_binary_without_static_ssl_markers() {
1131 let dir = tempfile::tempdir().unwrap();
1132 let path = dir.path().join("plain");
1133 std::fs::write(&path, b"no tls marker here").unwrap();
1134
1135 assert!(!binary_embeds_ssl(path.to_str().unwrap()));
1136 }
1137
1138 #[test]
1139 fn path_search_prefers_earlier_dirs() {
1140 let first = tempfile::tempdir().unwrap();
1141 let second = tempfile::tempdir().unwrap();
1142 let first_cmd = first.path().join("agent");
1143 let second_cmd = second.path().join("agent");
1144 std::fs::write(&first_cmd, b"first").unwrap();
1145 std::fs::write(&second_cmd, b"second").unwrap();
1146
1147 let found = find_executable_in_dirs(
1148 "agent",
1149 [first.path().to_path_buf(), second.path().to_path_buf()],
1150 )
1151 .unwrap();
1152
1153 assert_eq!(found, first_cmd);
1154 }
1155}