-
Notifications
You must be signed in to change notification settings - Fork 53.2k
gateway.multiplex_profiles defaults to on, gated by a boot-time serve guard that shows in status #112854
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gateway.multiplex_profiles defaults to on, gated by a boot-time serve guard that shows in status #112854
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1775,26 +1775,32 @@ async def _async_profile_runtime_scope(profile_home: "Path"): | |
|
|
||
|
|
||
| def load_gateway_config_for_runner() -> "GatewayConfig": | ||
| """Load gateway config for the process-level GatewayRunner. Multiplexed: reload under the default | ||
| profile's ``_profile_runtime_scope`` so platform tokens in its ``.env`` resolve via the secret | ||
| scope; unscoped ``_getenv`` falls to ``os.environ``, which often lacks a token living only under | ||
| """Load gateway config for the process-level GatewayRunner. An UNSET ``multiplex_profiles`` is | ||
| settled first by ``resolve_multiplex_mode`` (the default is on; the boot guard keeps a fleet that | ||
| still runs per-profile gateways standalone). Multiplexed: reload under the default profile's | ||
| ``_profile_runtime_scope`` so platform tokens in its ``.env`` resolve via the secret scope; | ||
| unscoped ``_getenv`` falls to ``os.environ``, which often lacks a token living only under | ||
| ``profiles/<name>/.env``. Off -> identical to ``load_gateway_config()``. | ||
|
|
||
| See #64674. | ||
| """ | ||
| from hermes_cli.gateway_multiplex_mode import log_multiplex_decision, resolve_multiplex_mode | ||
| cfg = load_gateway_config() | ||
| if not getattr(cfg, "multiplex_profiles", False): | ||
| log_multiplex_decision(resolve_multiplex_mode(cfg)) | ||
| if not cfg.multiplex_profiles: | ||
| return cfg | ||
| try: | ||
| home = get_hermes_home() | ||
| except Exception: | ||
| return cfg | ||
| try: | ||
| with _profile_runtime_scope(Path(home)): | ||
| return load_gateway_config() | ||
| scoped = load_gateway_config() | ||
| except Exception: | ||
| logger.debug("multiplex default-scope config reload failed; using unscoped load", exc_info=True) | ||
| return cfg | ||
| scoped.multiplex_profiles = cfg.multiplex_profiles # the verdict above, not a second unset flag | ||
| return scoped | ||
|
|
||
|
|
||
| async def _discover_gateway_mcp_tools(config: object) -> None: | ||
|
|
@@ -3401,6 +3407,8 @@ def __init__(self, config: Optional[GatewayConfig] = None): | |
| # With multiplex_profiles on, load under the default profile secret scope so bot tokens in its | ||
| # .env resolve as secondary profiles' do; explicit config= injection (tests) is left untouched. | ||
| # See #64674. | ||
| # An injected config (tests, ``gateway run --config``) is taken verbatim: an unset flag there | ||
| # stays None (= standalone); only the loaded path runs the boot-time default-on guard. | ||
| self.config = config if config is not None else load_gateway_config_for_runner() | ||
| # Multiplexer flag flips agent.secret_scope.get_secret() to fail-closed on unscoped credential | ||
| # reads, so a missed migration crashes loudly instead of leaking a cross-profile value. | ||
|
|
@@ -5459,6 +5467,9 @@ def _utf8_stdio() -> None: | |
| import yaml | ||
| with open(args.config, encoding="utf-8") as f: | ||
| config = GatewayConfig.from_dict(yaml.safe_load(f) or {}) | ||
| # Same boot-time verdict the loaded config gets when the file leaves the flag unset. | ||
| from hermes_cli.gateway_multiplex_mode import log_multiplex_decision, resolve_multiplex_mode | ||
| log_multiplex_decision(resolve_multiplex_mode(config)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two issues on the |
||
|
|
||
| # start_gateway() completes teardown before returning/raising SystemExit; force-exit after so a | ||
| # wedged non-daemon worker can't block Py_FinalizeEx's join. SystemExit caught so EVERY path exits. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1482,6 +1482,19 @@ def _print_gateway_process_mismatch(snapshot: GatewayRuntimeSnapshot) -> None: | |
| print(" can refuse to start another copy until this process stops.") | ||
|
|
||
|
|
||
| def _print_multiplex_standalone_reason() -> None: | ||
| """The boot guard kept an unset-default gateway standalone: say so in status, with the remedy.""" | ||
| try: | ||
| from gateway.status import read_runtime_status | ||
| reason = (read_runtime_status() or {}).get("multiplex_standalone_reason") | ||
| except Exception: | ||
| return | ||
| if reason: | ||
| print(f"⚠ Serving the default profile only (gateway.multiplex_profiles unset): {reason}") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a named-profile gateway this prints "⚠ Serving the default profile only … this is profile 'coder's own gateway" — self-contradictory — and the remedy line lacks the |
||
| print(" Fold every profile onto this gateway: hermes gateway migrate --multiplex") | ||
| print(" Keep per-profile gateways: hermes config set gateway.multiplex_profiles false") | ||
|
|
||
|
|
||
| def _print_served_ingress_urls(profile: str | None = None) -> None: | ||
| """Callback URLs of inbound-port platforms the live multiplexer serves for secondary profiles | ||
| (the value to paste into the Twilio / LINE / Teams / BlueBubbles console).""" | ||
|
|
@@ -4451,23 +4464,10 @@ def named_profile_served_by_running_multiplexer(profile_name: str | None = None) | |
| if recorded is not None: | ||
| return normalize_profile_name(suffix) in {normalize_profile_name(p) for p in recorded} | ||
|
|
||
| from gateway.config import _env_multiplex_profiles_override | ||
| cfg_path = default_root / "config.yaml" | ||
| cfg = {} | ||
| if cfg_path.exists(): | ||
| from hermes_cli.config import read_user_config_raw | ||
| cfg = read_user_config_raw(cfg_path) | ||
|
|
||
| env_multiplex = _env_multiplex_profiles_override() | ||
| if env_multiplex is False: | ||
| return False | ||
| if env_multiplex is not True: | ||
| if not cfg_path.exists(): | ||
| return False | ||
| if not (cfg.get("multiplex_profiles") or (cfg.get("gateway", {}) or {}).get("multiplex_profiles")): | ||
| return False | ||
|
|
||
| return True # a multiplexing default gateway serves every named profile | ||
| # No record (older gateway): only an EXPLICIT opt-in counts. The unset default is settled by | ||
| # the gateway at boot (it may have stayed standalone); a CLI process must not guess it on. | ||
| from hermes_cli.gateway_multiplex_mode import explicit_multiplex_flag | ||
| return explicit_multiplex_flag(default_root) is True # a multiplexer serves every named profile | ||
| except Exception: | ||
| logger.debug("Multiplexer-serving probe failed", exc_info=True) | ||
| return False | ||
|
|
@@ -6434,13 +6434,15 @@ def _cmd_status(args): | |
| else: | ||
| _gw_windows().status(deep=deep) | ||
| _print_gateway_process_mismatch(snapshot) | ||
| _print_multiplex_standalone_reason() | ||
| _print_served_ingress_urls() | ||
| else: | ||
| pids = list(snapshot.gateway_pids) | ||
| if pids: | ||
| print(f"✓ Gateway is running (PID: {', '.join(map(str, pids))})") | ||
| print(" (Running manually, not as a system service)") | ||
| _print_runtime_health() | ||
| _print_multiplex_standalone_reason() | ||
| _print_served_ingress_urls() | ||
| print() | ||
| _print_lines(*_STATUS_RUNNING_HINTS[_status_host_kind()]) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fallback flip (
False→True) makes any unrecognized string an explicit opt-in:'flase','ture','enabled',''all parse asTrueand skip the boot guard entirely. Pre-PR they parsedFalse. The env-var path handles garbage correctly (warn + unset) — the config path should match. All three reviewers hit this independently; reproduced at head. Suggested: unrecognized → warn +Noneso the guard decides.