fix(web): resolve per-profile gateway state for ?profile= in /api/status

When ?profile=<name> was passed to /api/status, the handler used
_config_profile_scope to set the HERMES_HOME contextvar override, but the
gateway liveness check (get_running_pid_cached) and runtime status read
(read_runtime_status) both resolve _get_process_hermes_home(), which
deliberately ignores contextvar overrides (issue #56986) — it always reads
os.environ['HERMES_HOME'] or the platform default. A named profile's
gateway identity files (~/.hermes/profiles/<name>/gateway.pid,
gateway_state.json) were therefore never found and the endpoint always
reported the profile's gateway as stopped.

Fix: when ?profile=<name> is requested, resolve the profile directory and
pass explicit profile-scoped paths:
- get_running_pid_cached(pid_path=profile_dir / 'gateway.pid')
- read_runtime_status(path=profile_dir / 'gateway_state.json')
- get_runtime_status_running_pid(..., expected_home=profile_dir)

This is the same explicit-path pattern _collect_profile_gateway_topology
already uses for per-profile gateway state, and it works within the #56986
constraint (no HERMES_HOME env mutation; read-only cross-profile access).
Plain /api/status without ?profile= keeps the exact zero-arg calls, so its
behavior — including the pid-cache signature and runtime-status fallback —
is byte-for-byte unchanged.

Fixes #69143
This commit is contained in:
webtecnica 2026-07-24 12:25:44 -07:00 committed by Teknium
parent 6fba781945
commit c9c9b17d98
1 changed files with 34 additions and 3 deletions

View File

@ -3049,7 +3049,9 @@ async def get_status(profile: Optional[str] = None):
# skills-module attributes that a concurrent request would cross-restore
# across that await. Status only resolves get_hermes_home() at call time
# (config/env/gateway state), which the task-local contextvar covers.
profile_dir: Optional[Path] = None
if requested_profile and requested_profile.lower() != "current":
profile_dir = _resolve_profile_dir(requested_profile)
status_scope = _config_profile_scope(requested_profile)
status_scope.__enter__()
@ -3059,7 +3061,17 @@ async def get_status(profile: Optional[str] = None):
# Try local PID check first (same-host). If that fails and a remote
# GATEWAY_HEALTH_URL is configured, probe the gateway over HTTP so the
# dashboard works when the gateway runs in a separate container.
gateway_pid = get_running_pid_cached()
#
# When ?profile=<name> was given, scope PID and state reads to that
# profile's directory — gateway identity files (PID, lock, runtime
# status) are written to the per-profile home, not the process-level
# HERMES_HOME (see issue #69143). Plain /api/status keeps the exact
# zero-arg call so its behavior (and cache signature) is unchanged.
gateway_pid = (
get_running_pid_cached(pid_path=profile_dir / "gateway.pid")
if profile_dir
else get_running_pid_cached()
)
gateway_running = gateway_pid is not None
remote_health_body: dict | None = None
@ -3099,7 +3111,17 @@ async def get_status(profile: Optional[str] = None):
# Prefer the detailed health endpoint response (has full state) when the
# local runtime status file is absent or stale (cross-container).
local_runtime = read_runtime_status()
#
# When ?profile=<name> was given, read from the profile's directory so
# the state file resolves to the per-profile gateway_state.json, not
# the fixed process-level HERMES_HOME (see issue #69143). Plain
# /api/status keeps the exact zero-arg call so existing behavior
# (and monkeypatched call shapes) are unchanged.
local_runtime = (
read_runtime_status(path=profile_dir / "gateway_state.json")
if profile_dir
else read_runtime_status()
)
runtime = local_runtime
if runtime is None and remote_health_body and remote_health_body.get("gateway_state"):
runtime = remote_health_body
@ -3109,7 +3131,16 @@ async def get_status(profile: Optional[str] = None):
# is display-only. (Running os.kill on a remote PID is both wrong and
# trips the test live-system guard.)
if not gateway_running and local_runtime is not None:
runtime_pid = get_runtime_status_running_pid(local_runtime)
# expected_home scopes the OS-identity check to the requested
# profile so a recycled PID belonging to a different profile's
# live gateway is not reported running for this one.
runtime_pid = (
get_runtime_status_running_pid(
local_runtime, expected_home=profile_dir
)
if profile_dir
else get_runtime_status_running_pid(local_runtime)
)
if runtime_pid is not None:
gateway_running = True
gateway_pid = runtime_pid