fix(cron): routed-profile cron delivers through the shared bot for guild-scoped routes and profiles without a platforms block

SharedRouteAdapters.get called ProfileRoute.matches without guild_id, so the
documented Discord route shape (guild_id + chat_id) never authorized a cron
target and the satellite fell to standalone delivery ("DISCORD_BOT_TOKEN is
not set" every fire). A cron target has no inbound guild anchor; the route's
own guild_id is passed so its target-exact discriminators decide.

_resolve_target_transport then vetoed the authorized shared transport on the
SATELLITE's platforms.<p>.enabled (absent block or enabled: false), although
that block describes a connector the satellite never runs. The shared hit now
builds the transport directly (keeping the satellite's non-credential platform
settings), and a live native adapter with no config block is no longer read as
"disabled" (#89302) — same normalization the relay path already had.

Fixes #89302
Co-authored-by: web3blind <264741654+web3blind@users.noreply.github.com>
This commit is contained in:
Teknium
2026-09-11 09:35:08 -07:00
parent 32c538851a
commit 444fa8166a
3 changed files with 79 additions and 6 deletions

View File

@@ -1133,15 +1133,28 @@ def _resolve_target_transport(
"""Resolve ``(transport, pconfig, runtime_adapter, target_adapters)`` for one target, or
``(None, error)`` when it cannot be served (relay-fronted with no live transport, or not
configured/enabled)."""
from gateway.delivery import resolve_delivery_transport
from gateway.delivery import DeliveryTransport, resolve_delivery_transport
target_adapters = adapters
transport = None
if isinstance(adapters, _preflight.SharedRouteAdapters):
# Credentialless satellite: the primary adapter serves THIS target only when an exact
# primary route maps it to this profile; a miss fails closed below.
# See #101113.
shared = adapters.get(platform, target)
target_adapters = {platform: shared} if shared is not None else {}
transport = resolve_delivery_transport(platform, config, target_adapters)
if shared is not None:
# The PRIMARY's route authorized this exact native adapter. The satellite's own
# ``platforms.<p>`` block describes a connector it never runs (no credential), so
# neither its absence nor ``enabled: false`` may veto the shared transport; only its
# non-credential settings (continuable surface, reply mode) are kept (#89302, #103701).
from dataclasses import replace
from gateway.config import PlatformConfig
own = config.platforms.get(platform)
transport = DeliveryTransport(
shared, replace(own, enabled=True) if own is not None else PlatformConfig(enabled=True),
platform)
if transport is None:
transport = resolve_delivery_transport(platform, config, target_adapters)
if transport is not None:
pconfig = transport.config
runtime_adapter = transport.adapter
@@ -1159,9 +1172,11 @@ def _resolve_target_transport(
pconfig = config.platforms.get(platform)
runtime_adapter = None
if transport is not None and transport.is_relay:
# Relay transport carries the RELAY adapter's config (enablement already checked). The
# logical platform is deliberately NOT natively enabled, so the native gate must not apply.
if transport is not None and (transport.is_relay or pconfig is None):
# Relay transport carries the RELAY adapter's config (enablement already checked): the
# logical platform is deliberately NOT natively enabled. A live NATIVE adapter with no
# ``platforms.<p>`` block is the same shape — the owning process already authorized the
# adapter; "no config" is not "disabled" (#89302).
if pconfig is None:
from gateway.config import PlatformConfig
pconfig = PlatformConfig(enabled=True)

View File

@@ -198,12 +198,17 @@ class SharedRouteAdapters:
chat_id = str(target.get("chat_id") or "") or None
thread_id = target.get("thread_id")
thread_id = str(thread_id) if thread_id else None
# A cron target carries no inbound guild anchor, so a route's guild_id is matched against
# itself — the target-exact discriminators (chat_id/thread_id) authorize the send. Without
# this the documented ``guild_id + chat_id`` Discord route never authorized cron output.
for route in self._routes:
if str(route.platform).lower() != platform_key:
continue
if not (route.chat_id or route.thread_id):
continue # guild-only routes are not target-exact
if route.matches(str(route.platform), chat_id=chat_id, thread_id=thread_id):
if route.matches(
str(route.platform), guild_id=route.guild_id, chat_id=chat_id, thread_id=thread_id,
):
return adapter
return default

View File

@@ -107,3 +107,56 @@ def test_satellite_routes_exact_target_through_primary_adapter(tmp_path, monkeyp
def test_shared_view_is_falsy_without_routes_or_primary_adapters():
assert not SharedRouteAdapters({}, [])
assert SharedRouteAdapters({Platform.DISCORD: object()}, []).get(Platform.DISCORD) is None
def test_guild_scoped_route_authorizes_cron_target_even_when_satellite_has_no_platform_block(
tmp_path, monkeypatch,
):
"""The documented Discord route shape is ``guild_id + chat_id``. A cron target carries no guild
anchor, so the route must be matched on its target-exact discriminators; and the satellite's
missing/disabled ``platforms.discord`` block must not veto the PRIMARY's authorized transport
(#89302 sibling) — before, both fell to standalone "DISCORD_BOT_TOKEN is not set"."""
root = tmp_path / "root"
sat_home = root / "profiles" / "fitness"
sat_home.mkdir(parents=True)
(root / "config.yaml").write_text(yaml.safe_dump({
"gateway": {"multiplex_profiles": True, "profile_routes": [
{"platform": "discord", "guild_id": "G1", "chat_id": "C1", "profile": "fitness"}]},
}), encoding="utf-8")
monkeypatch.setattr("hermes_constants.get_default_hermes_root", lambda: root)
primary = _primary_adapter()
token = set_hermes_home_override(str(sat_home))
try:
shared = SharedRouteAdapters({Platform.DISCORD: primary}, _primary_profile_routes_for_current_home())
for satellite_platforms in ({}, {Platform.DISCORD: PlatformConfig(enabled=False)}):
primary.sent.clear()
config = MagicMock()
config.platforms = satellite_platforms
config.get_home_channel = lambda p: None
with patch("gateway.config.load_gateway_config", return_value=config):
error, standalone = _run(_job("C1"), shared)
assert error is None, error
assert primary.sent == ["C1"] and standalone == []
finally:
reset_hermes_home_override(token)
def test_live_native_adapter_without_platform_block_is_not_treated_as_disabled():
"""#89302: a live native adapter handed in by the gateway is the authorization; an absent
``platforms.<p>`` block in the firing profile means "no config", not "disabled"."""
from cron.scheduler_delivery import _resolve_target_transport
config = MagicMock()
config.platforms = {}
adapter = object()
resolved, err = _resolve_target_transport(
{"id": "j"}, Platform.DISCORD, "discord", {"platform": "discord", "chat_id": "C1"},
{Platform.DISCORD: adapter}, config)
assert err is None and resolved[2] is adapter and resolved[1].enabled
# an explicitly disabled block still vetoes
config.platforms = {Platform.DISCORD: PlatformConfig(enabled=False)}
resolved, err = _resolve_target_transport(
{"id": "j"}, Platform.DISCORD, "discord", {"platform": "discord", "chat_id": "C1"},
{Platform.DISCORD: adapter}, config)
assert resolved is None and "not configured/enabled" in err