From 490c486ff65b766d9de0fe0e6f26e1778aaa8fb3 Mon Sep 17 00:00:00 2001 From: manishbyatroy Date: Sun, 7 Jun 2026 04:29:48 -0700 Subject: [PATCH] fix(simplex): accept display name in SIMPLEX_ALLOWED_USERS SIMPLEX_ALLOWED_USERS silently denied every contact when operators listed display names instead of numeric contactIds. The SimpleX UI never surfaces the numeric id, so display names are what operators naturally put in the env var. _is_user_authorized only compared source.user_id (the contactId), so the allowlist never matched. Expand check_ids to include source.user_name for the simplex platform, mirroring the existing WhatsApp phone-LID aliasing pattern. Adds doc + setup-prompt clarification and three regression tests. Salvaged from PR #40393. Adds manishbyatroy to release.py AUTHOR_MAP. --- gateway/run.py | 15 +++ plugins/platforms/simplex/adapter.py | 8 +- scripts/release.py | 1 + .../gateway/test_unauthorized_dm_behavior.py | 103 ++++++++++++++++++ website/docs/user-guide/messaging/simplex.md | 8 +- 5 files changed, 129 insertions(+), 6 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index cb93dce1c15..ecbe1a86605 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -7316,6 +7316,21 @@ class GatewayRunner: if normalized_user_id: check_ids.add(normalized_user_id) + # SimpleX: SIMPLEX_ALLOWED_USERS accepts either the numeric contactId + # or the contact's display name. The adapter sets user_id=contactId for + # stability across renames, but the SimpleX UI never surfaces the + # numeric id — operators only see display names, so that's what they + # naturally put in the env var. Match both so the allowlist works + # regardless of which form was chosen. + # Plugin platform: compare by value since Platform.SIMPLEX is not a + # hardcoded enum member (it's a dynamic plugin platform). + if ( + source.platform is not None + and source.platform.value == "simplex" + and source.user_name + ): + check_ids.add(source.user_name) + return bool(check_ids & allowed_ids) def _get_unauthorized_dm_behavior(self, platform: Optional[Platform]) -> str: diff --git a/plugins/platforms/simplex/adapter.py b/plugins/platforms/simplex/adapter.py index 52f93dedc30..ccf08ce3993 100644 --- a/plugins/platforms/simplex/adapter.py +++ b/plugins/platforms/simplex/adapter.py @@ -20,7 +20,11 @@ Required environment variables: (default: ws://127.0.0.1:5225) Optional environment variables: - SIMPLEX_ALLOWED_USERS Comma-separated contact IDs (allowlist) + SIMPLEX_ALLOWED_USERS Comma-separated allowlist. Each entry may be + either a numeric contactId (stable across + renames; visible via `/contacts` in the CLI) + or a contact display name (what the SimpleX + UI shows). Both forms are accepted. SIMPLEX_ALLOW_ALL_USERS Set 'true' to allow all contacts SIMPLEX_HOME_CHANNEL Default contact/group ID for cron delivery SIMPLEX_HOME_CHANNEL_NAME Human label for the home channel @@ -706,7 +710,7 @@ def interactive_setup() -> None: save_env_value(var, value) _prompt("SIMPLEX_WS_URL", "Daemon WebSocket URL (default ws://127.0.0.1:5225)") - _prompt("SIMPLEX_ALLOWED_USERS", "Allowed contact IDs (comma-separated; blank=skip)") + _prompt("SIMPLEX_ALLOWED_USERS", "Allowed contactIds or display names (comma-separated; blank=skip)") _prompt("SIMPLEX_HOME_CHANNEL", "Home channel contact/group ID (or empty)") print("Done. Make sure the simplex-chat daemon is running before starting the gateway.") diff --git a/scripts/release.py b/scripts/release.py index 42b3893d0f8..e43d011fb2a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -46,6 +46,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { "266365592+bmoore210@users.noreply.github.com": "bmoore210", + "manishbyatroy@gmail.com": "manishbyatroy", "chilltulpa@gmail.com": "TheGardenGallery", "al@randomsnowflake.me": "randomsnowflake", "zakame@zakame.net": "zakame", diff --git a/tests/gateway/test_unauthorized_dm_behavior.py b/tests/gateway/test_unauthorized_dm_behavior.py index 0aaad477c33..d2cc53aae84 100644 --- a/tests/gateway/test_unauthorized_dm_behavior.py +++ b/tests/gateway/test_unauthorized_dm_behavior.py @@ -100,6 +100,109 @@ def test_whatsapp_lid_user_matches_phone_allowlist_via_session_mapping(monkeypat assert runner._is_user_authorized(source) is True +def test_simplex_allowlist_accepts_display_name(monkeypatch): + """SIMPLEX_ALLOWED_USERS should match the contact's display name as well + as the numeric contactId. The SimpleX UI surfaces only display names, so + operators naturally put those in the env var — and the adapter sets + user_id=contactId for stability. Both forms must work. (#TBD)""" + _clear_auth_env(monkeypatch) + monkeypatch.delenv("SIMPLEX_ALLOWED_USERS", raising=False) + monkeypatch.setenv("SIMPLEX_ALLOWED_USERS", "hujikuji") + + # Register the simplex plugin so the env-var lookup resolves. + from gateway.platform_registry import platform_registry, PlatformEntry + platform_registry.register(PlatformEntry( + name="simplex", + label="SimpleX Chat", + adapter_factory=lambda cfg: None, + check_fn=lambda: True, + allowed_users_env="SIMPLEX_ALLOWED_USERS", + allow_all_env="SIMPLEX_ALLOW_ALL_USERS", + )) + + simplex = Platform("simplex") + runner, _adapter = _make_runner( + simplex, + GatewayConfig(platforms={simplex: PlatformConfig(enabled=True)}), + ) + + # contactId in the allowlist would still work — but the operator chose + # the display name. Verify the gateway honors it. + source = SessionSource( + platform=simplex, + user_id="4", # adapter sets this to the numeric contactId + chat_id="hujikuji", + user_name="hujikuji", # adapter sets this to displayName + chat_type="dm", + ) + assert runner._is_user_authorized(source) is True + + +def test_simplex_allowlist_accepts_numeric_contact_id(monkeypatch): + """The numeric contactId form must still work — the new display-name + matching must not regress existing setups.""" + _clear_auth_env(monkeypatch) + monkeypatch.delenv("SIMPLEX_ALLOWED_USERS", raising=False) + monkeypatch.setenv("SIMPLEX_ALLOWED_USERS", "4") + + from gateway.platform_registry import platform_registry, PlatformEntry + platform_registry.register(PlatformEntry( + name="simplex", + label="SimpleX Chat", + adapter_factory=lambda cfg: None, + check_fn=lambda: True, + allowed_users_env="SIMPLEX_ALLOWED_USERS", + allow_all_env="SIMPLEX_ALLOW_ALL_USERS", + )) + + simplex = Platform("simplex") + runner, _adapter = _make_runner( + simplex, + GatewayConfig(platforms={simplex: PlatformConfig(enabled=True)}), + ) + + source = SessionSource( + platform=simplex, + user_id="4", + chat_id="hujikuji", + user_name="hujikuji", + chat_type="dm", + ) + assert runner._is_user_authorized(source) is True + + +def test_simplex_allowlist_denies_unlisted(monkeypatch): + """Sanity check: an unrelated SimpleX user is still rejected.""" + _clear_auth_env(monkeypatch) + monkeypatch.delenv("SIMPLEX_ALLOWED_USERS", raising=False) + monkeypatch.setenv("SIMPLEX_ALLOWED_USERS", "hujikuji") + + from gateway.platform_registry import platform_registry, PlatformEntry + platform_registry.register(PlatformEntry( + name="simplex", + label="SimpleX Chat", + adapter_factory=lambda cfg: None, + check_fn=lambda: True, + allowed_users_env="SIMPLEX_ALLOWED_USERS", + allow_all_env="SIMPLEX_ALLOW_ALL_USERS", + )) + + simplex = Platform("simplex") + runner, _adapter = _make_runner( + simplex, + GatewayConfig(platforms={simplex: PlatformConfig(enabled=True)}), + ) + + source = SessionSource( + platform=simplex, + user_id="7", + chat_id="stranger", + user_name="stranger", + chat_type="dm", + ) + assert runner._is_user_authorized(source) is False + + def test_star_wildcard_in_allowlist_authorizes_any_user(monkeypatch): """WHATSAPP_ALLOWED_USERS=* should act as allow-all wildcard.""" _clear_auth_env(monkeypatch) diff --git a/website/docs/user-guide/messaging/simplex.md b/website/docs/user-guide/messaging/simplex.md index 0a5f4f72ca5..646038f67f1 100644 --- a/website/docs/user-guide/messaging/simplex.md +++ b/website/docs/user-guide/messaging/simplex.md @@ -52,20 +52,20 @@ SIMPLEX_HOME_CHANNEL= | Variable | Required | Description | |---|---|---| | `SIMPLEX_WS_URL` | Yes | WebSocket URL of the simplex-chat daemon | -| `SIMPLEX_ALLOWED_USERS` | Recommended | Comma-separated contact IDs allowed to use the agent | +| `SIMPLEX_ALLOWED_USERS` | Recommended | Comma-separated allowlist. Each entry can be a numeric `contactId` **or** a display name — both forms work. | | `SIMPLEX_ALLOW_ALL_USERS` | Optional | Set `true` to allow every contact (use carefully) | | `SIMPLEX_HOME_CHANNEL` | Optional | Default contact ID for cron job delivery | | `SIMPLEX_HOME_CHANNEL_NAME` | Optional | Human label for the home channel | -## Find your contact ID +## Find your contact ID or display name -After starting the daemon, open a conversation with your agent contact. The contact ID will appear in session logs or via `hermes send_message action=list`. +After starting the daemon, open a conversation with your agent contact. The numeric `contactId` appears in session logs or via `hermes send_message action=list`. If you'd rather use the display name shown in the SimpleX UI, that works too — `SIMPLEX_ALLOWED_USERS` accepts either form. ## Authorization By default **all contacts are denied**. You must either: -1. Set `SIMPLEX_ALLOWED_USERS` to a comma-separated list of contact IDs, or +1. Set `SIMPLEX_ALLOWED_USERS` to a comma-separated list of `contactId`s and/or display names (e.g. `SIMPLEX_ALLOWED_USERS=4,alice` matches either contactId 4 or the contact whose display name is "alice"), or 2. Use **DM pairing** — send any message to the bot and it will reply with a pairing code. Enter that code via `hermes pairing approve simplex `. ## Using SimpleX with cron jobs