37dcc0a6e8
Under gateway.multiplex_profiles the default gateway serves every profile, yet four startup/status paths still reasoned from the wrong source: * A secondary profile's API_SERVER_KEY (which the docs REQUIRE for /p/<profile>/ auth) auto-enabled api_server in that profile's config, so _load_secondary_profile_config raised SecondaryPortBindingConfigError and the whole profile was skipped. gateway/config_env.py::_enable_from_env now leaves `enabled` alone for port-binding platforms while a multiplexer loads a NON-default profile (home override + multiplex flag, the same signal gateway.config uses for scoped reads); the credential still lands in extra so the shared listener can authenticate the prefix. Default profile unchanged. * "Is this profile served?" was re-derived from the default config.yaml plus GATEWAY_MULTIPLEX_PROFILES as seen by the CLI process. `hermes -p coder ...` loads coder's .env, so an env-only opt-in on the default profile was invisible (guard never fired, status said stopped) and an allowlist edit flipped the answer before the restart. named_profile_served_by_running_multiplexer now reads the pid-verified default gateway_state.json served_profiles (written by _record_served_profiles) first and falls back to config derivation only when the key is absent. The record helpers live in hermes_cli/gateway_multiplex_served.py. * The served-profile guard ran only inside `gateway run`. `hermes -p X gateway start|install|restart` reached the service manager, whose unit then exited 78 forever (systemd parks it while the CLI prints "started"; launchd KeepAlive respawns every 30 s). The service verbs now run the same guard up front (exit 78, same message) and accept --force; the Desktop /api/gateway/start route returns 409 for a served profile instead of spawning a doomed child. * Status surfaces disagreed: `hermes -p coder status` said stopped, `hermes -p coder cron status` said "cron jobs will NOT fire" while `cron list` said fine, and the default `hermes status` never listed served profiles. Both now route through the probe / the recorded served set. The -p/--profile matcher in _scan_gateway_pids and gateway.status._command_line_belongs_to_profile compares the flag token for equality (`-p ops` no longer claims -- or lets `gateway stop` SIGTERM -- an `-p ops-2` gateway). Docs: multi-profile-gateways.md now describes the start/install refusal, the --force flags, the API_SERVER_KEY behaviour and the single default-home gateway_state.json (the per-profile runtime_status.json claim was wrong). Fixes #100397 Addresses #89726 #97360 #71344 (cherry picked from commit d002c1864a7b6a22c53758b16b7b0cc79aea2edf)
44 lines
2.2 KiB
Python
44 lines
2.2 KiB
Python
"""Which profiles does the LIVE default multiplexer serve? One answer for every CLI/dashboard surface.
|
|
|
|
``gateway/run_adapters.py::_record_served_profiles`` writes ``served_profiles`` into the default
|
|
home's ``gateway_state.json`` at startup. That record is the truth about the running process; the
|
|
default ``config.yaml`` plus ``GATEWAY_MULTIPLEX_PROFILES`` as seen by the *CLI* process is only a
|
|
guess (``hermes -p coder ...`` loads coder's ``.env``, so an env-only opt-in on the default profile
|
|
is invisible to it, and an allowlist edited after start flips the guess before the restart).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def live_default_gateway_pid() -> Optional[int]:
|
|
"""PID of the default profile's gateway when its pid record names a live process, else None."""
|
|
from hermes_constants import get_default_hermes_root
|
|
from gateway.status import _pid_exists, _pid_from_record, _read_pid_record
|
|
rec = _read_pid_record(get_default_hermes_root() / "gateway.pid")
|
|
pid = _pid_from_record(rec) if rec else None
|
|
return pid if pid and _pid_exists(pid) else None
|
|
|
|
|
|
def recorded_served_profiles(default_root: Optional[Path] = None) -> Optional[list[str]]:
|
|
"""``served_profiles`` the live default gateway recorded, or None when the key is absent (a record
|
|
from before the multiplexer recorded it, or a stopped/absent gateway). Callers fall back to config
|
|
derivation only on None: an empty list is an authoritative "serves nobody else"."""
|
|
from hermes_constants import get_default_hermes_root
|
|
from gateway.status import read_runtime_status
|
|
if live_default_gateway_pid() is None:
|
|
return None
|
|
runtime = read_runtime_status((default_root or get_default_hermes_root()) / "gateway_state.json")
|
|
served = (runtime or {}).get("served_profiles")
|
|
return [str(p) for p in served] if isinstance(served, list) else None
|
|
|
|
|
|
def multiplexer_served_secondaries() -> list[str]:
|
|
"""Named profiles the live default multiplexer serves (excludes ``default``); empty when none."""
|
|
return [p for p in (recorded_served_profiles() or []) if p and p != "default"]
|