diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 2595fc702c1..3fa87d378c7 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -1331,9 +1331,13 @@ class DiscordAdapter(BasePlatformAdapter): budget = parsed except ValueError: pass - # Leave ~20% headroom (min 0.5s) so the outer wait_for can't pre-empt our - # own straggler cancellation, and never go below 1s for the happy path. - return max(1.0, budget - max(0.5, budget * 0.2)) + # Stay strictly below the budget so the gateway's outer wait_for can't + # pre-empt our own straggler cancellation. Reserve ~20% (min 0.5s) of + # headroom, and never let the floor push us back up to/over the budget + # on tiny budgets — cap at 90% of the budget as a hard ceiling. + headroom = max(0.5, budget * 0.2) + deadline = max(1.0, budget - headroom) + return min(deadline, budget * 0.9) async def disconnect(self) -> None: """Disconnect from Discord.""" diff --git a/scripts/release.py b/scripts/release.py index c2de7f6701d..f128673dad9 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { + "cypher@augmentl.com": "Nickperillo", # PR #8008 salvage (Discord channel-name matching + flush pending sends on shutdown) "telos@apex-z.com": "telos-oc", # PR #14353 salvage (propagate custom_providers key_env into ProviderDef.api_key_env_vars; named + bare-custom self-heal paths) "256073454+Kolektori@users.noreply.github.com": "Kolektori", # PR #6436 salvage (require approval for host-bound Docker commands; container guard fast-path) "41764686+LIC99@users.noreply.github.com": "LIC99", # PR #4682 salvage (warn + default to manual on unknown approvals.mode; #4261) diff --git a/tests/gateway/test_discord_slash_auth.py b/tests/gateway/test_discord_slash_auth.py index 39d06ba74fb..f353dbd13a4 100644 --- a/tests/gateway/test_discord_slash_auth.py +++ b/tests/gateway/test_discord_slash_auth.py @@ -128,13 +128,15 @@ _SENTINEL = object() def _make_interaction( user_id, *, channel_id=12345, guild_id=42, in_dm=False, in_thread=False, - parent_channel_id=None, user=_SENTINEL, + parent_channel_id=None, user=_SENTINEL, channel_name=None, ): """Build a mock Discord Interaction with a still-unresponded response. ``channel_id`` may be set to ``None`` to simulate a guild interaction payload missing a resolvable channel id (fail-closed exercise). Pass ``user=None`` to simulate a payload missing the user object. + ``channel_name`` attaches a ``.name`` to the channel so channel-name / + ``#name`` allow/ignore matching can be exercised (mirrors on_message). """ import discord @@ -146,10 +148,14 @@ def _make_interaction( channel = discord.Thread() channel.id = channel_id channel.parent_id = parent_channel_id + if channel_name is not None: + channel.name = channel_name elif channel_id is None: channel = None else: channel = SimpleNamespace(id=channel_id) + if channel_name is not None: + channel.name = channel_name if user is _SENTINEL: user_obj = SimpleNamespace(id=int(user_id), name=f"user_{user_id}") @@ -275,6 +281,26 @@ async def test_channel_allowlist_wildcard_passes(adapter, monkeypatch): assert await adapter._check_slash_authorization(interaction, "/help") is True +@pytest.mark.asyncio +async def test_channel_allowlist_matches_by_name(adapter, monkeypatch): + """Allowlist configured by channel NAME matches slash interactions too — + the same name-form matching on_message gained. Without it, a deployment + using DISCORD_ALLOWED_CHANNELS=cypher (by name) would reject every slash + command even though messages in that channel pass. + """ + monkeypatch.setenv("DISCORD_ALLOWED_CHANNELS", "cypher") + interaction = _make_interaction("100200300", channel_id=9999, channel_name="cypher") + assert await adapter._check_slash_authorization(interaction, "/help") is True + + +@pytest.mark.asyncio +async def test_channel_allowlist_matches_by_hash_name(adapter, monkeypatch): + """``#name`` form in the allowlist also matches slash interactions.""" + monkeypatch.setenv("DISCORD_ALLOWED_CHANNELS", "#cypher") + interaction = _make_interaction("100200300", channel_id=9999, channel_name="cypher") + assert await adapter._check_slash_authorization(interaction, "/help") is True + + @pytest.mark.asyncio async def test_channel_allowlist_does_not_apply_to_dms(adapter, monkeypatch): """DMs aren't channel-gated — they go through on_message's DM lockdown.""" @@ -304,6 +330,14 @@ async def test_ignored_channel_wildcard_blocks_all(adapter, monkeypatch): assert await adapter._check_slash_authorization(interaction, "/help") is False +@pytest.mark.asyncio +async def test_ignored_channel_matches_by_name(adapter, monkeypatch): + """Ignore list configured by channel NAME blocks slash interactions too.""" + monkeypatch.setenv("DISCORD_IGNORED_CHANNELS", "cypher") + interaction = _make_interaction("100200300", channel_id=9999, channel_name="cypher") + assert await adapter._check_slash_authorization(interaction, "/help") is False + + # --------------------------------------------------------------------------- # Cross-platform admin notification # ---------------------------------------------------------------------------