# NousResearch/hermes-agent · Discord component button authorization and pairing fixes

## Open now

- Nothing open on GitHub.

These items relate to authorization issues with Discord MESSAGE_COMPONENT button interactions, specifically around pairing-approved users and requester permissions for component prompts. The work involves checking pairing store state and granting appropriate access for button clicks.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #51730. Verify it addresses your case.
- **Recommended:** read PR #51730 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-51730`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #50627: v0.17.0 (v2026.6.19): Discord MESSAGE_COMPONENT button interactions broken

## Prior & competing PRs

- **#51730: fix(discord): authorize pairing-approved users for component button clicks (#50627)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/platforms/discord/adapter.py`, `tests/gateway/test_discord_component_auth.py`
  - fix hunks:
  - `plugins/platforms/discord/adapter.py`

    ```diff
    @@ -5709,7 +5709,8 @@ def _component_check_auth(
     
         Mirrors the gateway's external-surface authorization model: component
         button clicks must be explicitly authorized by a Discord user/role
    -    allowlist, a global user allowlist, or an explicit allow-all flag.
    +    allowlist, a global user allowlist, an explicit allow-all flag, or
    +    the pairing store (``hermes pairing approve``).
     
         Behavior:
     
    ```

    ```diff
    @@ -5719,6 +5720,7 @@ def _component_check_auth(
           - role allowlist set + interaction.user has no resolvable
             ``roles`` attribute (e.g. DM context with a role policy active)
             -> reject (fail closed)
    +      - user is approved in the pairing store -> allow
           - otherwise -> reject
         """
         if os.getenv("DISCORD_ALLOW_ALL_USERS", "").strip().lower() in {"true", "1", "yes"}:
    ```

    ```diff
    @@ -5740,11 +5742,13 @@ def _component_check_auth(
         if user is None:
             return False
     
    +    # Resolve user ID once for both allowlist and pairing checks.
    +    try:
    +        uid = str(user.id)
    +    except AttributeError:
    +        uid = ""
    +
         if has_users:
    -        try:
    -            uid = str(user.id)
    -        except AttributeError:
    -            uid = ""
             if "*" in user_set or (uid and uid in user_set):
                 return True
     
    ```

    ```diff
    @@ -5763,6 +5767,18 @@ def _component_check_auth(
             if user_role_ids & role_set:
                 return True
     
    +    # Check pairing store — mirrors ``authz_mixin._check_authorization``
    +    # so users approved via ``hermes pairing approve`` can interact with
    +    # component buttons even without DISCORD_ALLOWED_USERS set.
    +    if uid:
    +        try:
    +            from gateway.pairing import PairingStore
    +            store = PairingStore()
    +            if store.is_approved("discord", uid):
    +                return True
    +        except Exception:
    +            pass
    +
         return False
     
     
    ```
  - `tests/gateway/test_discord_component_auth.py`

    ```diff
    @@ -30,6 +30,8 @@
     
     @pytest.fixture(autouse=True)
     def _clear_component_auth_env(monkeypatch):
    +    from unittest.mock import MagicMock, patch
    +
         for name in (
             "DISCORD_ALLOW_ALL_USERS",
             "GATEWAY_ALLOW_ALL_USERS",
    ```

    ```diff
    @@ -37,6 +39,13 @@ def _clear_component_auth_env(monkeypatch):
         ):
             monkeypatch.delenv(name, raising=False)
     
    +    # Default-mock PairingStore so tests don't hit the filesystem.
    +    # Pairing-specific tests override this with explicit mock values.
    +    mock_store = MagicMock()
    +    mock_store.is_approved.return_value = False
    +    with patch("gateway.pairing.PairingStore", return_value=mock_store):
    +        yield
    +
     
     # ---------------------------------------------------------------------------
     # Direct helper coverage -- the views all delegate to this helper, so
    ```

    ```diff
    @@ -304,3 +313,39 @@ def test_view_empty_allowlists_allow_with_explicit_allow_all(monkeypatch):
         monkeypatch.setenv("DISCORD_ALLOW_ALL_USERS", "true")
         view = ExecApprovalView(session_key="s", allowed_user_ids=set())
         assert view._check_auth(_interaction(99999)) is True
    +
    +
    +# ---------------------------------------------------------------------------
    +# Pairing store: users approved via ``hermes pairing approve`` must be
    +# authorized even without DISCORD_ALLOWED_USERS / DISCORD_ALLOWED_ROLES.
    +# ---------------------------------------------------------------------------
    +
    +
    +def test_component_check_pairing_approved_user_passes(monkeypatch):
    +    """User approved in pairing store passes even without allowlists."""
    +    from unittest.mock import MagicMock, patch
    +
    +    mock_store = MagicMock()
    +    mock_store.is_approved.return_value = True
    +    # Override the autouse fixture's mock with approved=True
    +    with patch("gateway.pairing.PairingStore", return_value=mock_store):
    +        interaction = _interaction(11111)
    +        assert _component_check_auth(interaction, set(), set()) is True
    +    mock_store.is_approved.assert_called_once_with("discord", "11111")
    +
    +
    +def test_component_check_pairing_not_approved_user_rejected(monkeypatch):
    +    """User NOT in pairing store is still rejected (fail-closed)."""
    +    # The autouse fixture already mocks is_approved=False, so this
    +    # just verifies the fail-closed path still works.
    +    interaction = _interaction(99999)
    +    assert _component_check_auth(interaction, set(), set()) is False
    +
    +
    +def test_component_check_pairing_import_error_graceful(monkeypatch):
    +    """If PairingStore import fails, fall through to fail-closed."""
    +    from unittest.mock import patch
    +
    +    with patch("gateway.pairing.PairingStore", side_effect=ImportError("simulated")):
    +        interaction = _interaction(11111)
    +        assert _component_check_auth(interaction, set(), set()) is False
    ```
- **#50628: fix(discord): authorize requester on component prompts**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7ff48a629`, `#51730`, `#50628`, `#50633`, `#50714`, `#50633`
- **#50633: fix(discord): check pairing store for component button auth**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `7ff48a629`, `#51730`
- **#50714: fix(discord): authorize pairing-approved users for component button clicks (#50627)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7ff48a629`, `#51730`, `#50628`, `#50633`, `#50714`, `#50633`

## Fix status

- Merged to main in PR #51730 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-24`.
