mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(discord): extend channel-name matching to slash-command auth; clamp flush deadline to disconnect budget
Follow-up to the salvaged #8008 fix: - Sibling-site fix: _evaluate_slash_authorization gated DISCORD_ALLOWED_CHANNELS / DISCORD_IGNORED_CHANNELS on numeric IDs only, so name/#name config that now works for on_message still silently failed for slash-command interactions. Refactor the channel-key helper to _discord_channel_keys_from_channel(channel, parent) and reuse it at the interaction gate. Fail-closed on missing channel id is preserved. - The contributor's hardcoded 8s flush deadline could be hard-cancelled mid-flush: _teardown_adapter already wraps cancel_background_tasks() in the per-adapter disconnect budget (HERMES_GATEWAY_ADAPTER_DISCONNECT_TIMEOUT, default 5s). The flush deadline now derives from that budget with headroom so it always completes inside it. - AUTHOR_MAP: map cypher@augmentl.com -> Nickperillo for CI. - Tests: slash-auth name/#name allow + name ignore matching.
This commit is contained in:
parent
cb9308f0a6
commit
b6045170bb
3 changed files with 43 additions and 4 deletions
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue