Skip to content

fix(gateway): honor nested gateway.* form for 9 more top-level keys - #59779

Closed
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/gateway-config-nested-keys
Closed

fix(gateway): honor nested gateway.* form for 9 more top-level keys#59779
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/gateway-config-nested-keys

Conversation

@pierrenode

Copy link
Copy Markdown
Contributor

What does this PR do?

load_gateway_config() already accepted both the top-level key and the nested gateway.<key> form (written by hermes config set gateway.<key> ...) for multiplex_profiles, max_concurrent_sessions, streaming, and write_sessions_json — each fixed one at a time as users hit it (most recently #59320 for multiplex_profiles). Nine sibling top-level keys never got the same nested fallback: session_reset, quick_commands, stt, stt_echo_transcripts, group_sessions_per_user, thread_sessions_per_user, reset_triggers, always_log_local, and unauthorized_dm_behavior.

hermes config set gateway.<any-of-these> ... builds exactly this nested shape (hermes_cli/config.py's _set_nested has no schema, so it accepts any dotted path), so a user following the same pattern that legitimately works for gateway.multiplex_profiles/gateway.streaming gets a silent no-op for these nine keys instead — the setting is written to config.yaml, hermes config show may even look right, but the gateway never reads it.

Related Issue

No filed issue — found via pattern analysis of load_gateway_config(): 4 of 13 top-level keys it bridges already had the nested-fallback treatment, applied incrementally as separate bugs over time; the other 9 didn't. General class acknowledged in #34067 (hermes config set accepts unknown paths silently) but that issue is about validating writes, not about closing this specific read-side gap.

Type of Change

Changes Made

  • gateway/config.py: read gateway: {...} into a single gateway_section variable once (consolidating three separate yaml_cfg.get("gateway") calls already in the function), add the same top-level-wins/nested-fallback check for each of the nine keys, mirroring the existing write_sessions_json precedent exactly
  • tests/gateway/test_config.py: 10 new regression tests (one per key) + a test_top_level_still_wins_over_nested_gateway_section precedence check (verified: fail without the fix, pass with it — the stt test specifically asserts False, not the class default True, so it can't false-pass)

Note for the reviewer

Because every fallback here is guarded by isinstance(gateway_section, dict), this also makes the streaming fallback tolerate a scalar gateway: block (e.g. gateway: disabled) without crashing — the same crash #40837 (open) targets specifically for streaming. This PR doesn't set out to fix that PR's issue, but the consolidated guard covers it as a side effect; flagging it here rather than leaving it to be found in review.

How to Test

python3.11 -m pytest tests/gateway/test_config.py::TestLoadGatewayConfig -k "nested_gateway_section" -v --override-ini="addopts="

Also re-ran the full file and the multiplex-profile suites to confirm no regressions: tests/gateway/test_config.py (96 passed), tests/gateway/test_multiplex_*.py (63 passed).

Checklist

  • Read the Contributing Guide | Conventional Commits | No duplicate PR
  • Single logical fix | Tests added | Platform: macOS
  • Docs — N/A | Cross-platform — N/A

load_gateway_config() already accepted both the top-level key and the
nested gateway.<key> form (written by `hermes config set gateway.<key>
...`) for multiplex_profiles, max_concurrent_sessions, streaming, and
write_sessions_json — each fixed one at a time as users hit it (most
recently NousResearch#59320 for multiplex_profiles). Nine sibling top-level keys
never got the same nested fallback: session_reset, quick_commands, stt,
stt_echo_transcripts, group_sessions_per_user, thread_sessions_per_user,
reset_triggers, always_log_local, and unauthorized_dm_behavior.

`hermes config set gateway.<any-of-these> ...` builds exactly this nested
shape (hermes_cli/config.py's _set_nested has no schema, so it accepts
any dotted path), so a user following the same pattern that legitimately
works for gateway.multiplex_profiles/gateway.streaming gets a silent
no-op for these nine keys instead.

Fix: read `gateway: {...}` into a single `gateway_section` variable once
(consolidating three separate `yaml_cfg.get("gateway")` calls already in
the function) and add the same top-level-wins/nested-fallback check for
each of the nine keys, mirroring the existing write_sessions_json
precedent exactly.

Note: because every fallback here is guarded by
`isinstance(gateway_section, dict)`, this also makes the streaming
fallback tolerate a scalar `gateway:` block (e.g. `gateway: disabled`)
without crashing — the same crash NousResearch#40837 (open) targets specifically for
streaming. This change doesn't set out to fix that PR's issue, but the
consolidated guard covers it as a side effect; flagging it for the
reviewer rather than leaving it to be found in review.
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades P2 Medium — degraded but workaround exists labels Jul 6, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying the inconsistent nested gateway read path. The premise remains valid on current main: gateway/config.py:1021-1102 still bridges these settings only from top-level YAML while selected sibling settings already accept nested forms.

Problems

  • gateway/config.py:964 does not fully implement the documented top-level-wins rule: a present session_reset: {} is treated as absent and is replaced by gateway.session_reset. Similarly, gateway/config.py:983 replaces a present non-mapping top-level stt value with gateway.stt. The other additions use key-presence precedence, so these two cases are inconsistent.

Suggested changes

  • Fall back to nested session_reset / stt only when their top-level keys are absent, then add precedence tests for those cases.
  • Current main's a7f65e3b already added the scalar gateway: guard in this same region; retain that guard when resolving the salvage overlap.

Automated hermes-sweeper review.

Comment thread gateway/config.py
# Map config.yaml keys → GatewayConfig.from_dict() schema.
# Each key overwrites whatever gateway.json may have set.
sr = yaml_cfg.get("session_reset")
if not (sr and isinstance(sr, dict)) and isinstance(gateway_section, dict):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This falls back when session_reset is present but {}, contrary to the stated top-level-wins contract. Consider using top-level key presence to decide fallback, and add a precedence regression for an empty top-level mapping plus a populated nested value.

Comment thread gateway/config.py
)

stt_cfg = yaml_cfg.get("stt")
if not isinstance(stt_cfg, dict) and isinstance(gateway_section, dict):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A present non-mapping top-level stt is treated as absent and replaced by nested gateway.stt. Gate the nested fallback on key absence if top-level precedence must be unconditional.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
teknium1 added a commit that referenced this pull request Jul 20, 2026
…llback

Follow-up for salvaged PR #59779: the session_reset and stt fallbacks
used truthiness/type checks, so a present-but-empty top-level value was
silently replaced by the nested gateway.* form — inconsistent with the
key-presence precedence every other key in the block uses. Switch both
to 'key not in yaml_cfg' gating and add precedence regression tests.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #67982 — your commit was cherry-picked onto current main with your authorship preserved in git log (rebase-merge). We added one follow-up commit on top switching the session_reset/stt fallbacks to key-presence precedence (the two inconsistencies flagged in the sweeper review) plus precedence regression tests. Thanks for the thorough fix and the test coverage!

randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…llback

Follow-up for salvaged PR NousResearch#59779: the session_reset and stt fallbacks
used truthiness/type checks, so a present-but-empty top-level value was
silently replaced by the nested gateway.* form — inconsistent with the
key-presence precedence every other key in the block uses. Switch both
to 'key not in yaml_cfg' gating and add precedence regression tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants