fix(gateway): prevent duplicate Photon sidecar storms

This commit is contained in:
Nick Edson 2026-07-09 21:45:14 -07:00 committed by Teknium
parent 88ff722f94
commit cf19ac8ff3
2 changed files with 43 additions and 12 deletions

View file

@ -10342,12 +10342,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if owner is not None:
logger.error(
"Profile '%s' and '%s' both configure %s with the same "
"credential — refusing to start the duplicate (a single "
"bot token cannot be polled twice). Give each profile its "
"own %s credential.",
"credential — refusing to start the duplicate (one "
"credential cannot be consumed twice). Give each profile "
"its own %s credential.",
owner, profile_name, platform.value, platform.value,
)
await self._safe_adapter_disconnect(adapter, platform)
# This adapter has not connected and therefore owns no
# resources to clean up. Calling disconnect here can mutate
# the shared platform state and, for a same-credential Photon
# adapter, shut down the primary profile's live sidecar.
continue
claimed[(platform, fp)] = profile_name
@ -10591,13 +10594,23 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
def _adapter_credential_fingerprint(adapter: Any) -> Optional[str]:
"""Return a stable, log-safe fingerprint of an adapter's credential.
Used only to detect two profiles claiming the same bot token. Returns a
salted hash (never the token itself) of the adapter's primary
credential, or None when no credential is discoverable (in which case
we don't attempt conflict detection for it).
Used only to detect two profiles claiming the same platform credential.
Returns a salted hash (never the credential itself) of the adapter's
primary credential, or None when no credential is discoverable (in
which case we don't attempt conflict detection for it).
"""
token = None
for attr in ("token", "bot_token", "_token", "api_token", "_bot_token"):
for attr in (
"token",
"bot_token",
"_token",
"api_token",
"_bot_token",
# Photon/Spectrum authenticates with project credentials instead
# of a bot token. Including its secret keeps multiplexed profiles
# from spawning competing sidecars for the same account and port.
"_project_secret",
):
val = getattr(adapter, attr, None)
if isinstance(val, str) and val.strip():
token = val.strip()

View file

@ -41,6 +41,22 @@ class TestCredentialFingerprint:
self.bot_token = "alt-token"
assert GatewayRunner._adapter_credential_fingerprint(_AltAdapter()) is not None
def test_reads_photon_project_secret(self):
class _PhotonAdapter:
def __init__(self, secret):
self._project_secret = secret
fp1 = GatewayRunner._adapter_credential_fingerprint(
_PhotonAdapter("shared-project-secret")
)
fp2 = GatewayRunner._adapter_credential_fingerprint(
_PhotonAdapter("shared-project-secret")
)
assert fp1 == fp2
assert fp1 is not None
assert "shared-project-secret" not in fp1
def test_reads_platform_config_token(self):
class _Config:
token = "config-token"
@ -718,8 +734,10 @@ class TestSecondaryProfileConfigHandling:
assert connect_calls == [(relay, Platform.RELAY)]
@pytest.mark.asyncio
async def test_secondary_same_config_token_is_refused(self, monkeypatch):
"""Adapters that keep their token on config still trip the mux guard."""
async def test_secondary_same_config_token_is_refused_without_disconnect(
self, monkeypatch
):
"""A never-connected duplicate must not disturb shared live state."""
from gateway.config import GatewayConfig, Platform, PlatformConfig
class _ConfigTokenAdapter:
@ -762,7 +780,7 @@ class TestSecondaryProfileConfigHandling:
)
assert connected == 0
assert duplicate.disconnected is True
assert duplicate.disconnected is False
assert runner._profile_adapters["reviewer"] == {}
def test_port_binding_set_covers_known_listeners(self):