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