diff --git a/gateway/authz_mixin.py b/gateway/authz_mixin.py index edb209d757..4a9a9d31b8 100644 --- a/gateway/authz_mixin.py +++ b/gateway/authz_mixin.py @@ -48,6 +48,7 @@ _ALLOW_BOTS_ENV = { # Gate reads use the shared per-profile isolated reader (allowlist leak under multiplex, #72348). +from gateway.platforms._shared import decode_json_list_literal as _decode_json_list_literal # noqa: E402 from gateway.platforms._shared import platform_gate_env as _auth_env # noqa: E402 @@ -67,9 +68,10 @@ def _registry_entry(platform): def _coerce_allow_set(raw) -> set[str]: - """Parse an allowlist (YAML list or comma-separated scalar) into a set of strings.""" + """Parse an allowlist (YAML list, JSON list literal string, or comma-separated scalar) into a set of strings.""" if raw is None: return set() + raw = _decode_json_list_literal(raw) if isinstance(raw, list): return {str(part).strip() for part in raw if str(part).strip()} return {part.strip() for part in str(raw).split(",") if part.strip()} diff --git a/gateway/platforms/_shared.py b/gateway/platforms/_shared.py index 3dea6065e0..b3ab6a65b1 100644 --- a/gateway/platforms/_shared.py +++ b/gateway/platforms/_shared.py @@ -86,6 +86,23 @@ def platform_gate_env(name: str, default: str = "") -> str: return (os.getenv(name) or default).strip() +def decode_json_list_literal(raw): + """Decode a JSON-encoded allowlist written by ``hermes config set``. + + String-typed defaults keep list literals verbatim on write (``allowed_chats`` is + declared as ``""``), so the config can hold ``'["-100","-200"]'`` as a string. + Malformed JSON passes through unchanged and keeps the legacy comma-split path. + """ + if isinstance(raw, str) and raw.lstrip()[:1] == "[": + try: + loaded = json.loads(raw) + except ValueError: + return raw + if isinstance(loaded, list): + return loaded + return raw + + def extra_or_secret(extra: Optional[dict], key: str, env: str, default: Any = "", *, blank_is_unset: bool = True) -> Any: """``config.extra[key]`` when set, else the scoped env var ``env`` (else ``default``). diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 00dbba0b59..1258c83fd8 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -18,7 +18,11 @@ from hermes_cli import setup_platforms logger = logging.getLogger(__name__) from agent.deadline import run_bounded_async -from gateway.platforms._shared import get_scoped_secret as _get_scoped_secret, platform_gate_env as _scoped_gate_env +from gateway.platforms._shared import ( + decode_json_list_literal as _decode_json_list_literal, + get_scoped_secret as _get_scoped_secret, + platform_gate_env as _scoped_gate_env, +) def _redact_telegram_error_text(error: object) -> str: @@ -33,23 +37,6 @@ def _redact_telegram_error_text(error: object) -> str: return "" -def _decode_json_list_literal(raw): - """Decode a JSON-encoded allowlist written by ``hermes config set``. - - String-typed defaults keep list literals verbatim on write (``allowed_chats`` is - declared as ``""``), so the config can hold ``'["-100","-200"]'`` as a string. - Malformed JSON passes through unchanged and keeps the legacy comma-split path. - """ - if isinstance(raw, str) and raw.lstrip()[:1] == "[": - try: - loaded = json.loads(raw) - except ValueError: - return raw - if isinstance(loaded, list): - return loaded - return raw - - def _consume_abandoned_task(task: asyncio.Task) -> None: """Observe a detached task's terminal exception to avoid noisy loop logs.""" try: diff --git a/tests/plugins/platforms/telegram/test_allowlist_json_adapter.py b/tests/plugins/platforms/telegram/test_allowlist_json_adapter.py index 528336b413..0ee911f3b0 100644 --- a/tests/plugins/platforms/telegram/test_allowlist_json_adapter.py +++ b/tests/plugins/platforms/telegram/test_allowlist_json_adapter.py @@ -50,3 +50,16 @@ def test_comma_and_malformed_strings_keep_the_legacy_split(): assert _adapter({"allowed_chats": "-100, -200"})._telegram_allowed_chats() == {"-100", "-200"} assert _adapter({"allowed_chats": ["-100", "-200"]})._telegram_allowed_chats() == {"-100", "-200"} assert _adapter({"allowed_chats": '["-100", "-200'})._telegram_allowed_chats() == {'["-100"', '"-200'} + + +def test_runner_side_allow_set_decodes_json_string(monkeypatch): + """The runner's central gate reads the same env chain (``TELEGRAM_GROUP_ALLOWED_CHATS`` + via the YAML bridge) and must not comma-split the brackets onto the ids either.""" + from gateway.authz_mixin import _coerce_allow_set + + monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_CHATS", '["-100","-200"]') + from gateway.platforms._shared import platform_gate_env + + assert _coerce_allow_set(platform_gate_env("TELEGRAM_GROUP_ALLOWED_CHATS")) == {"-100", "-200"} + assert _coerce_allow_set("-100, -200") == {"-100", "-200"} + assert _coerce_allow_set(["-100"]) == {"-100"}