fix(gateway): resolve sudo profile system installs
This commit is contained in:
+17
-3
@@ -1430,7 +1430,7 @@ def _profile_suffix() -> str:
|
||||
return hashlib.sha256(str(home).encode()).hexdigest()[:8]
|
||||
|
||||
|
||||
def _profile_arg(hermes_home: str | None = None) -> str:
|
||||
def _profile_arg(hermes_home: str | None = None, default_root: str | Path | None = None) -> str:
|
||||
"""Return ``--profile <name>`` only when HERMES_HOME is a named profile.
|
||||
|
||||
For ``~/.hermes/profiles/<name>``, returns ``"--profile <name>"``.
|
||||
@@ -1440,12 +1440,16 @@ def _profile_arg(hermes_home: str | None = None) -> str:
|
||||
hermes_home: Optional explicit HERMES_HOME path. Defaults to the current
|
||||
``get_hermes_home()`` value. Should be passed when generating a
|
||||
service definition for a different user (e.g. system service).
|
||||
default_root: Optional Hermes root to compare against. Used when
|
||||
generating a system service for another user from a sudo/root
|
||||
process, where ``Path.home()`` and ``get_default_hermes_root()``
|
||||
refer to root but the target profile lives under the service user.
|
||||
"""
|
||||
import re
|
||||
from hermes_constants import get_default_hermes_root
|
||||
|
||||
home = Path(hermes_home or str(get_hermes_home())).resolve()
|
||||
default = get_default_hermes_root().resolve()
|
||||
default = Path(default_root).resolve() if default_root else get_default_hermes_root().resolve()
|
||||
if home == default:
|
||||
return ""
|
||||
profiles_root = (default / "profiles").resolve()
|
||||
@@ -1459,6 +1463,16 @@ def _profile_arg(hermes_home: str | None = None) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _profile_arg_for_target_user(hermes_home: str, target_home_dir: str) -> str:
|
||||
"""Return the profile arg for a system service running as another user."""
|
||||
target_root = Path(target_home_dir) / ".hermes"
|
||||
try:
|
||||
Path(hermes_home).resolve().relative_to(target_root.resolve())
|
||||
return _profile_arg(hermes_home, default_root=target_root)
|
||||
except ValueError:
|
||||
return _profile_arg(hermes_home)
|
||||
|
||||
|
||||
def get_service_name() -> str:
|
||||
"""Derive a systemd service name scoped to this HERMES_HOME.
|
||||
|
||||
@@ -2384,7 +2398,7 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
|
||||
if system:
|
||||
username, group_name, home_dir = _system_service_identity(run_as_user)
|
||||
hermes_home = _hermes_home_for_target_user(home_dir)
|
||||
profile_arg = _profile_arg(hermes_home)
|
||||
profile_arg = _profile_arg_for_target_user(hermes_home, home_dir)
|
||||
# Remap all paths that may resolve under the calling user's home
|
||||
# (e.g. /root/) to the target user's home so the service can
|
||||
# actually access them.
|
||||
|
||||
+37
-1
@@ -354,6 +354,37 @@ def _apply_profile_override() -> None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _resolve_sudo_user_profile_env(name: str) -> str | None:
|
||||
"""Resolve `sudo hermes -p <name>` against the invoking user's home.
|
||||
|
||||
`_apply_profile_override()` runs before argparse, so `--run-as-user`
|
||||
is not available yet. For sudo invocations, the best available signal
|
||||
is SUDO_USER: root is only doing the privileged install/start action,
|
||||
while the profile store normally belongs to the user who invoked sudo.
|
||||
"""
|
||||
if name == "default":
|
||||
return None
|
||||
if not hasattr(os, "geteuid") or os.geteuid() != 0:
|
||||
return None
|
||||
sudo_user = os.environ.get("SUDO_USER", "").strip()
|
||||
if not sudo_user or sudo_user == "root":
|
||||
return None
|
||||
|
||||
try:
|
||||
import pwd
|
||||
|
||||
home = Path(pwd.getpwnam(sudo_user).pw_dir)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
candidate = home / ".hermes" / "profiles" / name
|
||||
try:
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# 1. Check for explicit -p / --profile flag. Historically this worked even
|
||||
# after the subcommand (`hermes chat -p coder`), so keep scanning broadly.
|
||||
# The exception is command-argv passthrough regions such as `mcp add --args`.
|
||||
@@ -441,7 +472,12 @@ def _apply_profile_override() -> None:
|
||||
from hermes_cli.profiles import resolve_profile_env
|
||||
|
||||
hermes_home = resolve_profile_env(profile_name)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
except FileNotFoundError as exc:
|
||||
hermes_home = _resolve_sudo_user_profile_env(profile_name)
|
||||
if not hermes_home:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except ValueError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -22,6 +22,7 @@ Usage::
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
@@ -421,7 +422,8 @@ def create_wrapper_script(name: str, target: Optional[str] = None) -> Optional[P
|
||||
else:
|
||||
wrapper_path = wrapper_dir / canon
|
||||
try:
|
||||
wrapper_path.write_text(f'#!/bin/sh\nexec hermes -p {profile} "$@"\n')
|
||||
hermes_exe = shutil.which("hermes") or "hermes"
|
||||
wrapper_path.write_text(f'#!/bin/sh\nexec {shlex.quote(hermes_exe)} -p {profile} "$@"\n')
|
||||
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
||||
return wrapper_path
|
||||
except OSError as e:
|
||||
|
||||
Reference in New Issue
Block a user