feat(slack): set HermesAgent User-Agent on slack-bolt client

Hermes already identifies itself on outbound calls to model providers
and monitoring endpoints — see hermes_cli/models.py and
plugins/model-providers/gmi/__init__.py
("Attribution so GMI can identify traffic from Hermes Agent"). The
Slack adapter is the one outbound surface that doesn't carry the
same identifier, so HermesAgent-driven Slack traffic is
indistinguishable from any other Bolt-Python app at the Slack
platform layer.

This sets `user_agent_prefix=f"HermesAgent/{_HERMES_VERSION}"` on the
AsyncWebClient instances constructed in gateway/platforms/slack.py
and threads them through AsyncApp via its `client` kwarg. Both
kwargs are first-class in slack-sdk and slack-bolt; no new
dependencies. Resulting header looks like:

    HermesAgent/<version> Python/3.x slackclient/3.x ...

No behavioral change for users — the Slack API ignores User-Agent
semantically; it lands in logs and analytics. Reversible.

Tests in tests/gateway/test_slack.py:
- TestSlackUserAgent pins the prefix shape and runs connect()
  end-to-end (multi-token config) to assert every AsyncWebClient
  carries the prefix and AsyncApp receives the pre-built client.
- TestSlackProxyBehavior fakes updated to tolerate the new kwargs
  via **_kwargs so they don't break on future passthroughs.
This commit is contained in:
Michael Brooks 2026-06-10 12:24:44 -07:00 • committed by Teknium
parent c5b62fdbae
commit 2946805299
2 changed files with 126 additions and 8 deletions

View file

@ -67,6 +67,15 @@ except ImportError: # pragma: no cover - plugin loaded outside package context
logger = logging.getLogger(__name__)
# User-Agent prefix for outbound Slack API calls so platform partners can
# identify HermesAgent traffic — matching other Hermes outbound surfaces
# that already set ``HermesAgent/<version>`` for platform-partner attribution.
try:
from hermes_cli import __version__ as _HERMES_VERSION
except Exception:
_HERMES_VERSION = "unknown"
_HERMES_SLACK_USER_AGENT_PREFIX = f"HermesAgent/{_HERMES_VERSION}"
_SLACK_ERROR_BODY_LIMIT_BYTES = 8 * 1024
@ -1879,12 +1888,19 @@ class SlackAdapter(BasePlatformAdapter):
# First token is the primary — used for AsyncApp / Socket Mode
primary_token = bot_tokens[0]
self._app = AsyncApp(token=primary_token)
primary_client = AsyncWebClient(
token=primary_token,
user_agent_prefix=_HERMES_SLACK_USER_AGENT_PREFIX,
)
self._app = AsyncApp(token=primary_token, client=primary_client)
_apply_slack_proxy(self._app.client, proxy_url)
# Register each bot token and map team_id → client
for token in bot_tokens:
client = AsyncWebClient(token=token)
client = AsyncWebClient(
token=token,
user_agent_prefix=_HERMES_SLACK_USER_AGENT_PREFIX,
)
_apply_slack_proxy(client, proxy_url)
auth_response = await client.auth_test()
team_id = auth_response.get("team_id", "")