use std::path::{Path, PathBuf};
const SYSTEM_BIN_DIRS: &[&str] = &["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
const ABSOLUTE_TOOL_DIRS: &[&str] = &["/opt/homebrew/bin", "/usr/local/bin"];
const HOME_RELATIVE_BIN_DIRS: &[&str] = &[".local/bin", ".cargo/bin"];
pub fn daemon_path_dirs() -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
let push = |p: PathBuf, acc: &mut Vec<PathBuf>| {
if !acc.contains(&p) {
acc.push(p);
}
};
for d in ABSOLUTE_TOOL_DIRS {
push(PathBuf::from(d), &mut dirs);
}
if let Some(home) = dirs::home_dir() {
for rel in HOME_RELATIVE_BIN_DIRS {
push(home.join(rel), &mut dirs);
}
}
for d in SYSTEM_BIN_DIRS {
push(PathBuf::from(d), &mut dirs);
}
dirs
}
pub fn daemon_path_env() -> String {
daemon_path_dirs()
.into_iter()
.filter_map(|p| p.to_str().map(str::to_owned))
.collect::<Vec<_>>()
.join(":")
}
pub fn resolve_binary(name: &str) -> Option<PathBuf> {
resolve_binary_in(name, &daemon_path_dirs())
}
fn resolve_binary_in(name: &str, fallback_dirs: &[PathBuf]) -> Option<PathBuf> {
if name.contains(std::path::MAIN_SEPARATOR) {
let p = PathBuf::from(name);
return p.is_file().then_some(p);
}
if let Some(path_var) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_var) {
if let Some(hit) = candidate(&dir, name) {
return Some(hit);
}
}
}
for dir in fallback_dirs {
if let Some(hit) = candidate(dir, name) {
return Some(hit);
}
}
None
}
fn candidate(dir: &Path, name: &str) -> Option<PathBuf> {
let p = dir.join(name);
if !p.is_file() {
return None;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(&p) {
Ok(meta) if meta.permissions().mode() & 0o111 != 0 => Some(p),
_ => None,
}
}
#[cfg(not(unix))]
{
Some(p)
}
}
const EPHEMERAL_PATH_SEGMENTS: &[&str] = &[
"target/debug",
"target/release",
".claude/worktrees/",
".base/.worktrees/",
];
const SYSTEM_TEMP_ROOTS: &[&str] = &[
"/tmp",
"/private/tmp",
"/var/tmp",
"/private/var/tmp",
"/var/folders",
"/private/var/folders",
];
pub fn is_under_system_temp(path: &Path) -> bool {
if SYSTEM_TEMP_ROOTS.iter().any(|root| path.starts_with(root)) {
return true;
}
let tmp = std::env::temp_dir();
if tmp.parent().is_none() {
return false;
}
if path.starts_with(&tmp) {
return true;
}
tmp.canonicalize()
.is_ok_and(|real| real.parent().is_some() && path.starts_with(real))
}
pub fn is_ephemeral_build_path(path: &Path) -> bool {
if is_under_system_temp(path) {
return true;
}
let s = path.to_string_lossy();
EPHEMERAL_PATH_SEGMENTS.iter().any(|seg| s.contains(seg))
}
pub fn canonical_bin_dir() -> Option<PathBuf> {
canonical_bin_dir_from(
dirs::home_dir().as_deref(),
std::env::var("CARGO_HOME").ok().as_deref(),
)
}
pub fn canonical_bin_dir_from(home: Option<&Path>, cargo_home: Option<&str>) -> Option<PathBuf> {
match cargo_home {
Some(h) if !h.is_empty() => Some(PathBuf::from(h).join("bin")),
_ => home.map(|h| h.join(".cargo").join("bin")),
}
}
const CRATE_BINARIES: &[(&str, &[&str])] = &[
("trusty-installer", &["trusty-installer", "tctl"]),
("trusty-mpm", &["tm", "trusty-mpm"]),
(
"trusty-memory",
&["trusty-memory", "trusty-memory-mcp-bridge"],
),
("trusty-search", &["trusty-search", "trusty-embedderd"]),
("trusty-agents", &["tagent"]),
("trusty-audit", &["trusty-audit", "taudit"]),
("trusty-code", &["tcode"]),
];
pub fn installed_binaries(crate_name: &str) -> Vec<String> {
CRATE_BINARIES
.iter()
.find(|(c, _)| *c == crate_name)
.map(|(_, bins)| bins.iter().map(|b| (*b).to_owned()).collect())
.unwrap_or_else(|| vec![crate_name.to_owned()])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_ephemeral_build_path_flags_build_and_worktree_paths() {
for p in [
"/Users/x/trusty-tools/target/debug/deps/trusty_mpm-abc123",
"/Users/x/trusty-tools/target/release/tm",
"/Users/x/repo/.claude/worktrees/fix-123/target/debug/tm",
"/Users/x/repo/.base/.worktrees/abc/crates/trusty-mpm",
] {
assert!(
is_ephemeral_build_path(Path::new(p)),
"{p} must be flagged as an ephemeral build path"
);
}
}
#[test]
fn is_ephemeral_build_path_flags_system_temp_paths() {
let temp_derived = std::env::temp_dir().join("claude-4485/scratchpad/base-bins/tm");
let missed: Vec<String> = [
PathBuf::from(
"/private/tmp/claude-502/-Users-x-proj/9f1c/scratchpad/base-bins/trusty-mpm",
),
PathBuf::from("/tmp/claude-502/scratchpad/base-bins/tm"),
PathBuf::from("/tmp/tm"),
PathBuf::from("/var/tmp/tm"),
PathBuf::from("/var/folders/qz/9x_2/T/cargo-install-abc/tm"),
temp_derived,
]
.into_iter()
.filter(|p| !is_ephemeral_build_path(p))
.map(|p| p.display().to_string())
.collect();
assert!(
missed.is_empty(),
"a system temp root is never an installed location (#4485), but these were \
accepted as stable: {missed:#?}"
);
}
#[test]
fn is_ephemeral_build_path_ignores_temp_lookalike_paths() {
for p in [
"/Users/x/tmp/bin/tm",
"/tmpfoo/bin/tm",
"/opt/tmp-tools/bin/trusty-mpm",
] {
assert!(
!is_ephemeral_build_path(Path::new(p)),
"{p} is not under a temp ROOT and must NOT be flagged"
);
}
}
#[test]
fn is_under_system_temp_is_true_for_temp_and_false_for_worktrees() {
for p in [
"/private/var/folders/xx/T/.tmpAbC123/project",
"/tmp/claude-502/session/scratchpad",
] {
assert!(
is_under_system_temp(Path::new(p)),
"{p} is under a system temp root and must be flagged"
);
}
let worktree = Path::new("/Users/x/repo/.claude/worktrees/eng-1");
assert!(
!is_under_system_temp(worktree),
"a worktree checkout is a durable PROJECT root — it must not be \
flagged as temp (#4638)"
);
assert!(
is_ephemeral_build_path(worktree),
"sanity: the same path IS an ephemeral BINARY location, which is \
exactly why the turn recorder must not use that predicate"
);
}
#[test]
fn is_ephemeral_build_path_accepts_installed_paths() {
for p in ["/Users/x/.cargo/bin/trusty-mpm", "/usr/local/bin/tm", "tm"] {
assert!(
!is_ephemeral_build_path(Path::new(p)),
"{p} must NOT be flagged as an ephemeral build path"
);
}
}
#[test]
fn daemon_path_dirs_orders_user_before_system() {
let dirs = daemon_path_dirs();
let pos = |needle: &str| dirs.iter().position(|p| p == &PathBuf::from(needle));
let homebrew = pos("/opt/homebrew/bin").expect("homebrew dir present");
let usr_bin = pos("/usr/bin").expect("/usr/bin present");
assert!(
homebrew < usr_bin,
"Homebrew must precede /usr/bin so it shadows older system copies"
);
}
#[test]
fn daemon_path_dirs_expands_home() {
let home = dirs::home_dir().expect("home dir resolvable in test env");
let dirs = daemon_path_dirs();
assert!(
dirs.contains(&home.join(".local/bin")),
"~/.local/bin must be expanded to the real home"
);
assert!(
dirs.contains(&home.join(".cargo/bin")),
"~/.cargo/bin must be expanded to the real home"
);
assert!(
dirs.iter().all(|p| !p.starts_with("~")),
"launchd does not expand ~; paths must be absolute"
);
}
#[test]
fn daemon_path_dirs_dedupes() {
let dirs = daemon_path_dirs();
let mut sorted = dirs.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
dirs.len(),
"daemon_path_dirs must not contain duplicates"
);
}
#[test]
fn daemon_path_env_contains_expected_dirs() {
let env = daemon_path_env();
let home = dirs::home_dir().expect("home dir resolvable in test env");
assert!(env.contains("/opt/homebrew/bin"), "PATH missing Homebrew");
assert!(
env.contains("/usr/local/bin"),
"PATH missing /usr/local/bin"
);
assert!(
env.contains(home.join(".local/bin").to_str().unwrap()),
"PATH missing expanded ~/.local/bin"
);
assert!(
env.contains(home.join(".cargo/bin").to_str().unwrap()),
"PATH missing expanded ~/.cargo/bin"
);
for sys in SYSTEM_BIN_DIRS {
assert!(env.contains(sys), "PATH missing system dir {sys}");
}
}
fn make_temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("bin_resolve_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn write_executable(path: &Path) {
std::fs::write(path, b"#!/bin/sh\n").expect("write fixture");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path).expect("stat fixture").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).expect("chmod fixture");
}
}
#[test]
fn resolve_binary_finds_in_well_known_dir() {
let tmp = make_temp_dir("well_known");
let bin = tmp.join("fake-tool-xyz");
write_executable(&bin);
let hit = candidate(&tmp, "fake-tool-xyz");
assert_eq!(hit.as_deref(), Some(bin.as_path()));
let explicit = bin.to_str().expect("utf8 temp path");
assert_eq!(resolve_binary(explicit).as_deref(), Some(bin.as_path()));
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn resolve_binary_finds_a_binary_outside_the_process_path() {
let tmp = make_temp_dir("outside_path");
let name = "trusty-fake-uv-4125";
let bin = tmp.join(name);
write_executable(&bin);
assert!(
resolve_binary(name).is_none(),
"fixture dir must not be on the process PATH for this test to mean anything"
);
assert_eq!(
resolve_binary_in(name, std::slice::from_ref(&tmp)).as_deref(),
Some(bin.as_path()),
"a binary outside the process PATH must still resolve from the fallback dirs"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[cfg(unix)]
#[test]
fn candidate_requires_execute_bit_on_unix() {
use std::os::unix::fs::PermissionsExt;
let tmp = make_temp_dir("exec_bit");
let data = tmp.join("not-a-binary");
std::fs::write(&data, b"plain data\n").expect("write data file");
let mut perms = std::fs::metadata(&data).expect("stat data").permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&data, perms).expect("chmod data");
assert_eq!(
candidate(&tmp, "not-a-binary"),
None,
"a non-executable regular file must not resolve as a runnable binary"
);
let exe = tmp.join("a-binary");
write_executable(&exe);
assert_eq!(
candidate(&tmp, "a-binary").as_deref(),
Some(exe.as_path()),
"an executable file must resolve"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn resolve_binary_returns_none_for_missing() {
assert!(
resolve_binary("definitely-not-a-real-binary-zzz-1298").is_none(),
"a nonexistent binary must resolve to None"
);
}
#[test]
fn resolve_binary_accepts_absolute_path() {
let sh = PathBuf::from("/bin/sh");
if sh.is_file() {
assert_eq!(resolve_binary("/bin/sh"), Some(sh));
}
assert!(
resolve_binary("/no/such/path/here-1298").is_none(),
"a non-existent explicit path must resolve to None"
);
}
#[test]
fn canonical_bin_dir_from_honours_cargo_home() {
let got = canonical_bin_dir_from(Some(Path::new("/home/u")), Some("/opt/ch"));
assert_eq!(got, Some(PathBuf::from("/opt/ch/bin")));
}
#[test]
fn canonical_bin_dir_from_falls_back_to_dot_cargo() {
let got = canonical_bin_dir_from(Some(Path::new("/home/u")), None);
assert_eq!(got, Some(PathBuf::from("/home/u/.cargo/bin")));
}
#[test]
fn canonical_bin_dir_from_treats_empty_cargo_home_as_unset() {
let got = canonical_bin_dir_from(Some(Path::new("/home/u")), Some(""));
assert_eq!(got, Some(PathBuf::from("/home/u/.cargo/bin")));
}
#[test]
fn canonical_bin_dir_from_is_none_without_either_input() {
assert_eq!(canonical_bin_dir_from(None, None), None);
assert_eq!(canonical_bin_dir_from(None, Some("")), None);
}
#[test]
fn installed_binaries_covers_multi_binary_crates() {
for (krate, expected) in CRATE_BINARIES {
assert_eq!(
installed_binaries(krate),
expected.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>(),
"binary set for {krate} must come from its CRATE_BINARIES row"
);
}
for (krate, alias) in [
("trusty-installer", "tctl"),
("trusty-mpm", "tm"),
("trusty-search", "trusty-embedderd"),
("trusty-agents", "tagent"),
] {
assert!(
installed_binaries(krate).iter().any(|b| b == alias),
"{krate} must install `{alias}` (#5777 hard requirement)"
);
}
}
#[test]
fn installed_binaries_defaults_to_crate_name() {
assert_eq!(installed_binaries("tga"), vec!["tga".to_owned()]);
assert_eq!(
installed_binaries("trusty-console"),
vec!["trusty-console".to_owned()]
);
}
}