pub(crate) fn resolve_binary_path(command: &str) -> Result<String, String> {
resolve_binary_path_inner(command, 0)
}
fn resolve_binary_path_inner(command: &str, depth: u8) -> Result<String, String> {
if depth > 5 {
return Err(format!(
"too many nested shebang wrappers resolving '{}'",
command
));
}
let candidate = if command.contains('/') {
std::path::PathBuf::from(command)
} else {
find_in_path(command).ok_or_else(|| format!("'{}' not found in $PATH", command))?
};
let resolved = std::fs::canonicalize(&candidate)
.map_err(|e| format!("cannot resolve '{}': {}", candidate.display(), e))?;
let mut header = [0u8; 256];
let n = {
use std::io::Read;
let mut f = std::fs::File::open(&resolved)
.map_err(|e| format!("cannot open '{}': {}", resolved.display(), e))?;
f.read(&mut header)
.map_err(|e| format!("cannot read '{}': {}", resolved.display(), e))?
};
let header = &header[..n];
if header.starts_with(b"\x7fELF") {
return Ok(resolved.to_string_lossy().into_owned());
}
if header.starts_with(b"#!") {
let line_end = header
.iter()
.position(|&b| b == b'\n')
.unwrap_or(header.len());
let line = String::from_utf8_lossy(&header[2..line_end]);
let mut parts = line.split_whitespace();
let interp = parts
.next()
.ok_or_else(|| format!("'{}' has an empty shebang", resolved.display()))?;
let next = if interp.ends_with("/env") || interp == "env" {
parts
.next()
.ok_or_else(|| format!("'{}' uses env with no interpreter", resolved.display()))?
} else {
interp
};
return resolve_binary_path_inner(next, depth + 1);
}
Err(format!(
"'{}' is neither an ELF binary nor a shebang script; specify --binary-path explicitly",
resolved.display()
))
}
fn find_in_path(cmd: &str) -> Option<std::path::PathBuf> {
let mut dirs: Vec<std::path::PathBuf> = Vec::new();
if let Some(user) = std::env::var_os("SUDO_USER")
&& let Some(home) = sudo_user_home(&user)
{
dirs.push(home.join(".local/bin"));
dirs.push(home.join("bin"));
if let Some(nvm_bin) = newest_nvm_bin(&home) {
dirs.push(nvm_bin);
}
}
if let Some(path) = std::env::var_os("PATH") {
dirs.extend(std::env::split_paths(&path));
}
for dir in dirs {
let full = dir.join(cmd);
if let Ok(meta) = std::fs::metadata(&full)
&& meta.is_file()
{
return Some(full);
}
}
None
}
fn sudo_user_home(user: &std::ffi::OsStr) -> Option<std::path::PathBuf> {
let user = user.to_str()?;
let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
for line in passwd.lines() {
let mut fields = line.split(':');
if fields.next() == Some(user) {
return fields.nth(4).map(std::path::PathBuf::from);
}
}
None
}
fn newest_nvm_bin(home: &std::path::Path) -> Option<std::path::PathBuf> {
let versions = home.join(".nvm/versions/node");
let mut entries: Vec<_> = std::fs::read_dir(&versions)
.ok()?
.filter_map(|e| e.ok())
.map(|e| e.path())
.collect();
entries.sort();
entries.last().map(|p| p.join("bin"))
}
pub(crate) fn binary_embeds_ssl(path: &str) -> bool {
use std::io::Read;
const NEEDLE: &[u8] = b"SSL_write";
let mut f = match std::fs::File::open(path) {
Ok(f) => f,
Err(_) => return false,
};
let mut buf = vec![0u8; 1 << 20]; let mut carry: Vec<u8> = Vec::new();
loop {
let n = match f.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
Err(_) => return false,
};
carry.extend_from_slice(&buf[..n]);
if carry.windows(NEEDLE.len()).any(|w| w == NEEDLE) {
return true;
}
let keep = NEEDLE.len() - 1;
if carry.len() > keep {
carry.drain(..carry.len() - keep);
}
}
false
}
pub(crate) fn parse_container_ref(binary_path: &str) -> Option<&str> {
binary_path
.strip_prefix("docker://")
.or_else(|| binary_path.strip_prefix("docker:"))
.filter(|r| !r.is_empty())
}
pub(crate) fn resolve_container_binary_path(reference: &str) -> Result<String, String> {
let output = std::process::Command::new("docker")
.args(["inspect", "--format", "{{.State.Pid}}", reference])
.output()
.map_err(|e| format!(
"failed to run `docker inspect` for container '{}': {} (is the Docker CLI installed and on $PATH?)",
reference, e
))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"`docker inspect {}` failed: {}",
reference,
stderr.trim()
));
}
let init_pid: u32 = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.map_err(|_| format!("could not determine host PID for container '{}'", reference))?;
if init_pid == 0 {
return Err(format!(
"container '{}' is not running (host PID 0)",
reference
));
}
let target_pid = find_ssl_pid_in_tree(init_pid).unwrap_or(init_pid);
let exe = format!("/proc/{}/exe", target_pid);
if target_pid == init_pid {
println!(
"✓ Resolved container '{}' to host PID {} → {}",
reference, target_pid, exe
);
} else {
println!(
"✓ Resolved container '{}' (init PID {}) to SSL-embedding host PID {} → {}",
reference, init_pid, target_pid, exe
);
}
Ok(exe)
}
fn find_ssl_pid_in_tree(root_pid: u32) -> Option<u32> {
let mut queue = std::collections::VecDeque::from([root_pid]);
let mut seen = std::collections::HashSet::new();
while let Some(pid) = queue.pop_front() {
if !seen.insert(pid) {
continue;
}
if binary_embeds_ssl(&format!("/proc/{}/exe", pid)) {
return Some(pid);
}
let children_path = format!("/proc/{}/task/{}/children", pid, pid);
if let Ok(children) = std::fs::read_to_string(&children_path) {
for child in children
.split_whitespace()
.filter_map(|s| s.parse::<u32>().ok())
{
queue.push_back(child);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::parse_container_ref;
#[test]
fn parses_docker_double_slash_scheme() {
assert_eq!(parse_container_ref("docker://openclaw"), Some("openclaw"));
assert_eq!(
parse_container_ref("docker://my-agent-1"),
Some("my-agent-1")
);
}
#[test]
fn parses_docker_colon_scheme() {
assert_eq!(parse_container_ref("docker:openclaw"), Some("openclaw"));
assert_eq!(
parse_container_ref("docker:abc123def456"),
Some("abc123def456")
);
}
#[test]
fn ignores_plain_filesystem_paths() {
assert_eq!(parse_container_ref("/proc/1234/exe"), None);
assert_eq!(parse_container_ref("/usr/bin/node"), None);
assert_eq!(
parse_container_ref("~/.nvm/versions/node/v20.0.0/bin/node"),
None
);
}
#[test]
fn rejects_empty_container_reference() {
assert_eq!(parse_container_ref("docker://"), None);
assert_eq!(parse_container_ref("docker:"), None);
}
}