mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(install+update): add /usr/local/bin PATH guard for RHEL root non-login shells (#16191)
* fix(install): add /usr/local/bin PATH guard for RHEL root non-login shells The FHS-layout branch assumed /usr/local/bin is on PATH for every standard shell. That holds for login shells (via /etc/profile's pathmunge) but breaks on RHEL/CentOS/Rocky/Alma 8+ root in non-login interactive shells (su, sudo -s, tmux panes, some web terminals) — /etc/bashrc does not add /usr/local/bin and /root/.bash_profile doesn't either. Result: hermes command links to /usr/local/bin/hermes but the user has to type the absolute path each time. Probe a fresh 'bash -i -c' (non-login interactive, matching the user scenario) after symlinking. If hermes isn't resolvable, append an idempotent PATH guard to /root/.bashrc and /root/.bash_profile, same grep pattern already used by the ~/.local/bin branch below. No change on distros where /usr/local/bin is already inherited. * fix(update): repair RHEL root PATH on hermes update Existing RHEL/CentOS/Rocky/Alma root installs won't be repaired by the install.sh fix alone because 'hermes update' is an in-place git pull, not a rerun of install.sh. Port the same probe + idempotent .bashrc write into cmd_update so affected users get fixed automatically on next update. _ensure_fhs_path_guard() runs after 'Update complete!': - Linux + root + FHS-layout install (command at /usr/local/bin/hermes) only - Probe: env -i bash -i -c 'command -v hermes' — fresh non-login interactive shell, same scenario the user reports - On failure, append PATH guard to /root/.bashrc and /root/.bash_profile, skipping if any uncommented PATH line already mentions /usr/local/bin - Silent no-op on macOS, non-root, legacy layout, or shells that already resolve hermes
This commit is contained in:
@@ -5956,6 +5956,88 @@ def _cmd_update_check():
|
||||
print(f" Run '{recommended_update_command()}' to install.")
|
||||
|
||||
|
||||
def _ensure_fhs_path_guard() -> None:
|
||||
"""Ensure /usr/local/bin is on PATH for RHEL-family root non-login shells.
|
||||
|
||||
Mirrors the post-symlink probe added to ``scripts/install.sh`` so that
|
||||
existing FHS-layout root installs on RHEL/CentOS/Rocky/Alma 8+ get
|
||||
repaired on ``hermes update`` without requiring a reinstall. The
|
||||
installer's assumption that ``/usr/local/bin`` is on PATH for every
|
||||
standard shell breaks on those distros in non-login interactive shells
|
||||
(su, sudo -s, tmux panes, some web terminals): /etc/bashrc doesn't
|
||||
add /usr/local/bin and /root/.bash_profile doesn't either. Symptom:
|
||||
``hermes`` prints ``command not found`` even though the symlink lives
|
||||
at /usr/local/bin/hermes.
|
||||
|
||||
Silent no-op on: non-Linux, non-root, non-FHS installs, and any system
|
||||
where ``bash -i -c 'command -v hermes'`` already resolves. Idempotent.
|
||||
"""
|
||||
if sys.platform != "linux":
|
||||
return
|
||||
try:
|
||||
if os.geteuid() != 0:
|
||||
return
|
||||
except AttributeError:
|
||||
return
|
||||
# Only act when this is actually an FHS-layout install (command link at
|
||||
# /usr/local/bin/hermes, code at /usr/local/lib/hermes-agent).
|
||||
fhs_link = Path("/usr/local/bin/hermes")
|
||||
if not fhs_link.is_symlink() and not fhs_link.exists():
|
||||
return
|
||||
|
||||
# Probe a fresh non-login interactive bash the way the user will use it.
|
||||
# ``bash -i -c`` sources ~/.bashrc but NOT ~/.bash_profile or /etc/profile,
|
||||
# which is the exact scenario where RHEL root loses /usr/local/bin.
|
||||
home = os.environ.get("HOME") or "/root"
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
["env", "-i",
|
||||
f"HOME={home}",
|
||||
f"TERM={os.environ.get('TERM', 'dumb')}",
|
||||
"bash", "-i", "-c", "command -v hermes"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return # no bash or probe hung — don't block update on this
|
||||
if probe.returncode == 0:
|
||||
return # already on PATH, nothing to do
|
||||
|
||||
path_line = 'export PATH="/usr/local/bin:$PATH"'
|
||||
path_comment = (
|
||||
"# Hermes Agent — ensure /usr/local/bin is on PATH "
|
||||
"(RHEL non-login shells)"
|
||||
)
|
||||
wrote_any = False
|
||||
for candidate in (".bashrc", ".bash_profile"):
|
||||
cfg = Path(home) / candidate
|
||||
if not cfg.is_file():
|
||||
continue
|
||||
try:
|
||||
existing = cfg.read_text(errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
# Idempotency: skip if any uncommented PATH= line already references
|
||||
# /usr/local/bin. Mirrors the grep pattern used by install.sh.
|
||||
already_guarded = any(
|
||||
"/usr/local/bin" in line
|
||||
and "PATH" in line
|
||||
and not line.lstrip().startswith("#")
|
||||
for line in existing.splitlines()
|
||||
)
|
||||
if already_guarded:
|
||||
continue
|
||||
try:
|
||||
with cfg.open("a", encoding="utf-8") as f:
|
||||
f.write("\n" + path_comment + "\n" + path_line + "\n")
|
||||
except OSError as e:
|
||||
print(f" ⚠ Could not update {cfg}: {e}")
|
||||
continue
|
||||
print(f" ✓ Added /usr/local/bin to PATH in {cfg}")
|
||||
wrote_any = True
|
||||
if wrote_any:
|
||||
print(" (reload your shell or run 'source ~/.bashrc' to pick it up)")
|
||||
|
||||
|
||||
def cmd_update(args):
|
||||
"""Update Hermes Agent to the latest version.
|
||||
|
||||
@@ -6399,6 +6481,13 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
print()
|
||||
print("✓ Update complete!")
|
||||
|
||||
# Repair RHEL-family root installs where /usr/local/bin isn't on PATH
|
||||
# for non-login interactive shells. No-op on every other platform.
|
||||
try:
|
||||
_ensure_fhs_path_guard()
|
||||
except Exception as e:
|
||||
logger.debug("FHS PATH guard check failed: %s", e)
|
||||
|
||||
# Write exit code *before* the gateway restart attempt.
|
||||
# When running as ``hermes update --gateway`` (spawned by the gateway's
|
||||
# /update command), this process lives inside the gateway's systemd
|
||||
|
||||
@@ -1055,14 +1055,41 @@ setup_path() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# FHS layout: /usr/local/bin is on PATH for every standard shell, nothing to inject.
|
||||
# FHS layout: /usr/local/bin is normally on PATH for login shells (via
|
||||
# /etc/profile pathmunge), but on RHEL/CentOS/Rocky/Alma 8+ non-login
|
||||
# interactive root shells (su, sudo -s, tmux panes, some web terminals)
|
||||
# only source /etc/bashrc, which does NOT add /usr/local/bin — and
|
||||
# /root/.bash_profile doesn't either. So verify with `command -v` and
|
||||
# fall back to writing a PATH guard into /root/.bashrc when needed.
|
||||
if [ "$ROOT_FHS_LAYOUT" = true ]; then
|
||||
export PATH="$command_link_dir:$PATH"
|
||||
# Probe a fresh non-login interactive bash the way the user will use it.
|
||||
# `bash -i -c` sources ~/.bashrc but NOT ~/.bash_profile or /etc/profile,
|
||||
# which is the exact scenario where RHEL root loses /usr/local/bin.
|
||||
if env -i HOME="$HOME" TERM="${TERM:-dumb}" bash -i -c 'command -v hermes' \
|
||||
>/dev/null 2>&1; then
|
||||
log_info "/usr/local/bin is already on PATH for all shells"
|
||||
log_success "hermes command ready"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "hermes not on PATH in non-login shells (common on RHEL-family)"
|
||||
PATH_LINE='export PATH="/usr/local/bin:$PATH"'
|
||||
PATH_COMMENT='# Hermes Agent — ensure /usr/local/bin is on PATH (RHEL non-login shells)'
|
||||
for SHELL_CONFIG in "$HOME/.bashrc" "$HOME/.bash_profile"; do
|
||||
[ -f "$SHELL_CONFIG" ] || continue
|
||||
if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null \
|
||||
| grep -qE 'PATH=.*(/usr/local/bin|\$command_link_dir)'; then
|
||||
echo "" >> "$SHELL_CONFIG"
|
||||
echo "$PATH_COMMENT" >> "$SHELL_CONFIG"
|
||||
echo "$PATH_LINE" >> "$SHELL_CONFIG"
|
||||
log_success "Added /usr/local/bin to PATH in $SHELL_CONFIG"
|
||||
fi
|
||||
done
|
||||
log_success "hermes command ready"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if ~/.local/bin is on PATH; if not, add it to shell config.
|
||||
# Detect the user's actual login shell (not the shell running this script,
|
||||
# which is always bash when piped from curl).
|
||||
|
||||
Reference in New Issue
Block a user