From d0133fd8e4cab217743b89b23e09aaa57c1be375 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 8 Jun 2026 15:17:43 +1000 Subject: [PATCH] feat(relay): register RelayAdapter through platform registry (flagged off by default) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit register_relay_adapter() registers the generic 'relay' platform via the same PlatformRegistry path as plugin adapters — no core dispatch changes. OFF by default (dark-launch): only registers when HERMES_GATEWAY_RELAY is truthy (or force=True for tests), so existing single-tenant/direct deployments are unaffected. Factory builds a transport-less RelayAdapter with a placeholder descriptor; the real descriptor is negotiated at handshake. Phase 1, Task 1.3 of the gateway-relay plan. --- gateway/relay/__init__.py | 70 +++++++++++++++++++ .../gateway/relay/test_relay_registration.py | 55 +++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 tests/gateway/relay/test_relay_registration.py diff --git a/gateway/relay/__init__.py b/gateway/relay/__init__.py index 1917ba1a3cf..ba7931851a9 100644 --- a/gateway/relay/__init__.py +++ b/gateway/relay/__init__.py @@ -8,4 +8,74 @@ a deprecation cycle until at least two real Class-1 platforms (Discord + Telegram) have shaken out the schema. See ``docs/relay-connector-contract.md`` for the formal cross-repo interface. + +Registration is OFF by default: ``register_relay_adapter()`` only registers the +``relay`` platform when the relay feature flag is enabled, so existing +single-tenant/direct deployments are completely unaffected (dark-launch posture). """ + +from __future__ import annotations + +import os + + +def relay_enabled() -> bool: + """Whether the relay adapter should be registered. + + Off by default. Enabled when ``HERMES_GATEWAY_RELAY=1`` (or true/yes/on). + A config-file gate can be layered on later; the env flag is the minimal + dark-launch switch so default deployments never register the adapter. + """ + return os.environ.get("HERMES_GATEWAY_RELAY", "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + + +def register_relay_adapter(force: bool = False) -> bool: + """Register the generic ``relay`` platform via the platform registry. + + No-op unless the relay flag is set (or ``force=True`` for tests). Returns + True if registration happened. Additive: uses the same registry path as + plugin adapters, so no core dispatch changes are needed. + + The factory builds a transport-less ``RelayAdapter`` with a placeholder + descriptor; the real ``CapabilityDescriptor`` is negotiated at handshake + time via the transport's ``handshake()``. (Wiring the live transport + + handshake into ``GatewayRunner`` is later-phase work; this task only proves + the adapter is constructible through the registry behind the flag.) + """ + if not (force or relay_enabled()): + return False + + from gateway.platform_registry import PlatformEntry, platform_registry + from gateway.relay.adapter import RelayAdapter + from gateway.relay.descriptor import CONTRACT_VERSION, CapabilityDescriptor + + def _factory(config): + placeholder = CapabilityDescriptor( + contract_version=CONTRACT_VERSION, + platform="relay", + label="Relay", + max_message_length=4096, + supports_draft_streaming=False, + supports_edit=True, + supports_threads=False, + markdown_dialect="plain", + len_unit="chars", + ) + return RelayAdapter(config, placeholder) + + platform_registry.register( + PlatformEntry( + name="relay", + label="Relay", + adapter_factory=_factory, + check_fn=lambda: True, + source="builtin", + emoji="\U0001f50c", + ) + ) + return True diff --git a/tests/gateway/relay/test_relay_registration.py b/tests/gateway/relay/test_relay_registration.py new file mode 100644 index 00000000000..e40e99548c5 --- /dev/null +++ b/tests/gateway/relay/test_relay_registration.py @@ -0,0 +1,55 @@ +"""RelayAdapter registration via the platform registry (relay Phase 1, Task 1.3). + +Verifies the relay platform is registered ONLY behind the flag (dark-launch), +constructed through the same registry path as plugin adapters. +""" + +from __future__ import annotations + +import pytest + +from gateway.config import PlatformConfig +from gateway.platform_registry import platform_registry +from gateway.relay import register_relay_adapter, relay_enabled +from gateway.relay.adapter import RelayAdapter + + +@pytest.fixture(autouse=True) +def _clean_registry(monkeypatch): + """Ensure each test starts/ends with no 'relay' entry and a clean env.""" + monkeypatch.delenv("HERMES_GATEWAY_RELAY", raising=False) + platform_registry.unregister("relay") + yield + platform_registry.unregister("relay") + + +def test_off_by_default(): + assert relay_enabled() is False + assert register_relay_adapter() is False + assert platform_registry.is_registered("relay") is False + + +def test_enabled_by_env_flag(monkeypatch): + monkeypatch.setenv("HERMES_GATEWAY_RELAY", "1") + assert relay_enabled() is True + assert register_relay_adapter() is True + assert platform_registry.is_registered("relay") is True + + +def test_force_registers_without_flag(): + assert register_relay_adapter(force=True) is True + assert platform_registry.is_registered("relay") is True + + +def test_create_adapter_yields_relay_adapter(): + register_relay_adapter(force=True) + adapter = platform_registry.create_adapter("relay", PlatformConfig()) + assert isinstance(adapter, RelayAdapter) + # Placeholder descriptor until handshake negotiates the real one. + assert adapter.descriptor.platform == "relay" + + +@pytest.mark.parametrize("val,expected", [("0", False), ("", False), ("true", True), ("ON", True), ("yes", True)]) +def test_flag_parsing(monkeypatch, val, expected): + monkeypatch.setenv("HERMES_GATEWAY_RELAY", val) + assert relay_enabled() is expected