relay_explicitly_disabled() went through hermes_cli.config.read_raw_config,
which turns a malformed config.yaml into {} and then applies the managed
overlay. load_gateway_config() raises on the same file and falls back to
env + gateway.json WITHOUT the managed layer. With a managed
relay.enabled: false and an injected URL the two readers disagreed: the
loader kept relay enabled and suppressed native Slack, while startup
refused to register relay — a profile with no route at all.
Extract the loader's file selection into config_loader.read_yaml_layers()
and have both consumers use it; the predicate mirrors the loader's
fallback (no YAML layer) on a parse error. Regression test fails on the
previous predicate.
The 16×9 side-effect matrix and the 3×4×2 startup run patched ~15
GatewayStartupMixin internals plus gateway.run._hermes_home to drive
start(); that is a harness for one revision of run_startup.py, not a
contract, and it would break on the next phase split.
Keep what the fix promises and nothing else:
- the real startup hook (_start_register_plugins_relay_hooks) makes
no connector call, registers nothing and leaves GATEWAY_RELAY_* alone
under every accepted spelling of the disable (top, nested, string
'false', top-beats-nested, managed-only);
- URL-only activation without an 'enabled' key is unchanged (control,
green on base);
- the standalone predicate and load_gateway_config() are two readers of
one rule, and the vetoed relay does not suppress native adapters;
- a gateway.json disable stays advisory for relay as for telegram.
218 -> 12 tests; all 15 non-control cases fail on the merge base.
Narrow the PR's load_yaml_layer rewrite to the one behavior the relay
fix needs: an absent user file is an empty layer, so administrator-
pinned values (a managed relay opt-out among them) still reach the
gateway on a host with no config.yaml.
Everything else returns to the loader's existing contract. A MALFORMED
user file still raises so load_gateway_config() keeps emitting its
'Failed to process config.yaml — falling back to .env / gateway.json'
warning instead of silently applying the managed layer over the broken
one; the loader keeps its own yaml read rather than importing the CLI's
raw reader. Changing the malformed-file contract touches every platform
and belongs in its own PR.
The loader stamps _enabled_explicit only from YAML blocks; a gateway.json
`enabled: false` is re-enabled by env presence for slack, telegram and
everyone else. Tagging the marker for platforms.relay alone in the legacy
reader made relay the one platform whose gateway.json verdict beat the
env — a per-platform pin nobody would find later. If legacy opt-outs
should become authoritative, that is one rule for all platforms in its
own PR.
relay_explicitly_disabled() now reads the marker the loader itself sets
instead of re-deriving the verdict from 'enabled' in block, so the
standalone predicate and load_gateway_config() cannot disagree.
Every relay object (adapter, media client, WS transport) is constructed
inside the registry factory that register_relay_adapter() installs, and
that call plus self_provision_relay()/send_relay_policy() all resolve
through relay_url(). So the opt-out has three reachable boundaries:
relay_url(), relay_fronted_platforms() (env-sourced, standalone cron)
and the force=/url= bypass in register_relay_adapter().
The guards on RelayAdapter.connect, _get_media_client,
RelayMediaClient.enabled/download and every WS dial/redial were
unreachable unless config flipped on a RUNNING gateway — which the docs
in this PR say is unsupported. Half-honoring that flip is worse than not
honoring it: inbound kept flowing over the open socket while media
downloads silently returned None and the next redial terminated.
Drop those five guards, the _RelayDisabled terminal exception they
needed, and the legacy-JSON lru_cache that existed only to make a full
config merge affordable on the per-message path. The activation-time
predicate runs a handful of times per boot; the shared raw-config and
managed-scope readers already cache on mtime.
Read an empty raw user layer on absent, invalid or unreadable YAML before applying the existing managed overlay. Preserve raw defaults and platform precedence so managed relay opt-out prevents env-exclusive native suppression.
Prove absent-YAML startup/native routing, managed enabled and URL-only controls, managed leaf precedence, and invalid-file siblings. Mutation: 17 failed/30 passed. Canonical 50-file suite: 682 passed.
Carry legacy relay enabled intent through the canonical env sweep without changing other platforms. Read opt-out config through the cached non-bootstrap reader and existing managed overlay. Reproduce both Salt findings before fixes, cover native startup/delivery and isolated cold imports, and mutation-check the regressions.
Relay bootstrap treated endpoint availability as activation, bypassing the
platform configuration's explicit opt-out. An injected relay URL could
provision credentials from a native-only profile and suppress its native
connectors even when relay was disabled.
Reuse the gateway's platform merge and boolean normalization for the
explicit-disable decision. Guard registration overrides, discovery, media,
and all dial/retry entrypoints; preserve URL-only legacy activation and
explicit operator enrollment. Skip relay-exclusive native suppression for
an opted-out relay.
Add real-config startup and standalone regression coverage, including
inherited credentials, managed config, config precedence, native delivery,
and fresh-token retry. Document restart semantics without claiming live
socket teardown.