Commit Graph

19 Commits

Author SHA1 Message Date
teknium1 67bd2f6571 fix(gateway/config): root-level platform blocks keep their adapter keys
A root-level `webhook:` block (the pre-`platforms:` spelling, still
supported by platform_section) is never copied into platforms_data, so
the removed _PORT_BRIDGE_KEYS table was its only route to `extra` and
the previous commit regressed it (port fell back to 8644). Bridge every
non-typed key of a root block in _bridged_keys with the same typed-key
exclusion and explicit-extra precedence as PlatformConfig.from_dict.

Found by independent review before merge.
2026-09-12 08:42:58 -07:00
teknium1 2f3ccfa3bd fix(gateway/config): promote every top-level platform key into extra
`platforms.webhook.port: 9100` (and `routes`, `secret`, api_server `key`/
`cors_origins`, any adapter setting) was silently dropped unless nested
under `extra:` — PlatformConfig.from_dict only read a fixed set of typed
fields. Two partial bridges (a per-platform port/host/secret table in the
loader and an api_server-only block) covered a few keys and had to be
extended for every new one.

from_dict now promotes every non-typed top-level key into `extra`, with an
explicit `extra:` value winning on a clash and typed fields never leaking
into `extra` on a to_dict/from_dict roundtrip. Both hand-written bridges
are removed.

Same direction as PRs #10208/#10211/#10453 (rainow's #10206 diagnosis) and
#20506; those targeted the pre-loader layout.

Fixes #10206
2026-09-12 08:42:58 -07:00
Teknium 9848e22ed6 feat(multiplex)!: drop gateway.multiplex_profile_allowlist — serve every profile
The multiplexing default gateway now serves default + every live named profile
under profiles/. profiles_to_serve(multiplex=True) is a pure directory read
(tombstoned profiles skipped, never mkdir); every reader — gateway served set,
/p/<profile>/ prefixes for api_server + webhook, the named-profile standalone
guard, the Desktop cron ticker (its #108428 standdown for a profile owned by a
running gateway is unchanged) — drops the allowlist parameter.

Config v43 migration deletes the key from user config.yaml; DEFAULT_CONFIG,
GatewayConfig and the top-level yaml bridge no longer carry it.

BREAKING: anyone who set an allowlist now has their excluded profiles served.
Archive or delete a profile you do not want served (Teknium approved).
2026-09-11 19:50:46 -07:00
Teknium 545e74d0ea fix(gateway): a secondary profile's config.yaml no longer poisons the process env under multiplex
Under gateway.multiplex_profiles every secondary profile's config loads inside
_profile_runtime_scope, yet every apply_yaml_config_fn hook (feishu, matrix,
whatsapp, slack, dingtalk, discord non-gate keys, telegram non-gate keys) and
gateway/config_loader.py::bridge_core_env_settings still wrote os.environ there.
First-writer-wins: the first secondary with a require_mention / allowlist /
allow_bots / reactions block made that policy the DEFAULT profile's (live:
TELEGRAM_REQUIRE_MENTION written from a secondary load), and secondaries read
the default's env for the same keys.

- gateway/platforms/_shared.py::yaml_env_setter: the one env-write shape for
  YAML->env bridges — env wins, skipped under an active secondary scope.
- Every hook now seeds its values into the profile's PlatformConfig.extra and
  uses yaml_env_setter; bridge_core_env_settings seeds telegram/signal
  require_mention into extra and skips the env write under scope.
- Readers that bypassed extra/scope now consult extra first (matrix flags +
  session_scope, slack reactions, telegram reactions/mention_patterns/_extra_bool,
  discord reactions/auto_thread/history_backfill/approval_mentions/allow_mentions,
  feishu allow_bots, dingtalk mention_patterns, signal require_mention).

Salvages the shape of PR #100604 (whatsapp, earliest report #80099), #100435
(discord) and #100448 (telegram) by @nftpoetrist on top of current main.

Fixes #80099

Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
2026-09-11 15:29:15 -07:00
Ben Barclay 6f5dda1aee fix(relay): read the opt-out through the loader's own YAML reader
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.
2026-09-11 10:45:33 +10:00
Ben Barclay 1d5debfa65 fix(config): apply managed overlay when the user config.yaml is absent
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.
2026-09-11 10:28:13 +10:00
Ben Barclay 337d69856f fix(relay): legacy gateway.json opt-out is advisory, like every platform
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.
2026-09-11 10:27:14 +10:00
Ben Barclay 5746b5bae6 fix(relay): veto the disable at the activation boundary only
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.
2026-09-11 10:25:38 +10:00
Victor Kyriazakos 4f3c4be7ca fix(config): reuse unchanged legacy gateway reads 2026-09-09 12:18:57 +00:00
Victor Kyriazakos 6ce2b72281 fix: apply managed gateway config without user YAML
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.
2026-09-08 14:42:54 +00:00
Victor Kyriazakos f2de102872 fix(relay): preserve legacy opt-out without standalone bootstrap
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.
2026-09-08 14:30:32 +00:00
Teknium 34e512ae58 fix(sessions): remove remaining timer migration and guidance 2026-09-07 06:10:54 -07:00
Teknium 1d5d059410 fix(gateway): stop time-triggered conversation rotation 2026-09-07 06:10:54 -07:00
Teknium e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium d03babfddc refactor(gateway): wave-2 final compaction — Platform._missing_ single exit, _cleanup_expired keep-set, expression-form display normalisers and small predicates 2026-09-02 22:42:55 -07:00
Teknium 225605ad7f refactor(gateway): wave-2 pairing/directory/registry compaction — rate-limit load/save helpers, _entry_created_at, shared _read_json, registry _probe + zip-restore, unified _enable_from_env (port-bound pop mode), _transport_owner, _coerce_num 2026-09-02 22:19:43 -07:00
Teknium 48a2007092 refactor(gateway): wave-2 config/authz compaction — shared bool-token parser, _extra_choice, authz phase helpers (_chat_scoped_grant, _legacy_telegram_chat_grant), unified adapter flag/policy readers, tristate display normaliser, hand-compacted field docs 2026-09-02 21:49:33 -07:00
Teknium fb0f7806ee refactor(gateway): table-driven bridge specs in config_loader, compact registry/display/routing docs 2026-09-02 18:52:41 -07:00
Teknium 5820d0b0d5 refactor(gateway/config): extract the config.yaml phase to gateway/config_loader.py; table-driven from_dict; defensive collapse
- load_gateway_config (536 LOC) is now a thin orchestrator: legacy gateway.json
  -> config_loader.load_yaml_layer -> GatewayConfig.from_dict -> env overrides
  -> validation. The yaml phase lives in gateway/config_loader.py as small
  functions driven by tables: _TOPLEVEL_BRIDGE (23 top-level/nested
  gateway.<key> bridges with 5 fallback modes), _SHARED_KEYS (28 per-platform
  keys copied into extra, with per-platform restrictions and transforms) and
  _PORT_BRIDGE_KEYS. Logger name kept as "gateway.config".
- GatewayConfig.from_dict: shared pick()/key_label() helpers replace the
  repeated "top-level key present else nested gateway.<key>" blocks; warning
  order preserved.
- _normalize_unauthorized_dm_behavior / _normalize_notice_delivery unified into
  _normalize_choice; _ensure_platform_extra_dict -> _dict_slot (also used by
  persist_home_channel); _getenv_int removed (dead since the env pass moved to
  config_env, which has its own _int_or).
- Platform._missing_: one _add_pseudo_member helper for both branches.
- Single warning sites in _coerce_optional_positive_int and
  coerce_systemd_watchdog_seconds; _validate_gateway_config placeholder pass
  flattened; small to_dict/getter collapses. Ruff F401/SIM102 clean.
- tests/hermes_cli/test_config_read_guard.py: allowlist gateway/config_loader.py
  (same owner as gateway/config.py — the extracted load_gateway_config phase).

gateway/config.py 2684 -> 1319 LOC (-50.9%); largest function now
GatewayConfig.from_dict at 119 LOC. Resolved-config parity: 160 cells
(16 yaml fixtures x 10 env sets) byte-identical to the integration base,
including captured log records and stderr.
2026-09-02 16:48:36 -07:00