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.")
|
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):
|
def cmd_update(args):
|
||||||
"""Update Hermes Agent to the latest version.
|
"""Update Hermes Agent to the latest version.
|
||||||
|
|
||||||
@@ -6399,6 +6481,13 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||||||
print()
|
print()
|
||||||
print("✓ Update complete!")
|
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.
|
# Write exit code *before* the gateway restart attempt.
|
||||||
# When running as ``hermes update --gateway`` (spawned by the gateway's
|
# When running as ``hermes update --gateway`` (spawned by the gateway's
|
||||||
# /update command), this process lives inside the gateway's systemd
|
# /update command), this process lives inside the gateway's systemd
|
||||||
|
|||||||
@@ -1055,10 +1055,37 @@ setup_path() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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
|
if [ "$ROOT_FHS_LAYOUT" = true ]; then
|
||||||
export PATH="$command_link_dir:$PATH"
|
export PATH="$command_link_dir:$PATH"
|
||||||
log_info "/usr/local/bin is already on PATH for all shells"
|
# 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"
|
log_success "hermes command ready"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user