mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fix(slack): scope app token in multiplex gateway
This commit is contained in:
parent
6160a80253
commit
ea2c9bc10f
2 changed files with 77 additions and 6 deletions
|
|
@ -36,6 +36,7 @@ from pathlib import Path as _Path
|
|||
|
||||
sys.path.insert(0, str(_Path(__file__).resolve().parents[3]))
|
||||
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
from gateway.platforms.helpers import MessageDeduplicator
|
||||
from gateway.platforms.base import (
|
||||
|
|
@ -972,13 +973,16 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
return False
|
||||
|
||||
raw_token = self.config.token
|
||||
# Multiplex: profile secrets live in secret_scope, not process os.environ.
|
||||
# Prefer get_secret; fall back to os.getenv for single-profile / tests.
|
||||
# Multiplex: profile secrets live in the secret scope, not process
|
||||
# os.environ. When a scope is installed (secondary-profile connect),
|
||||
# it is AUTHORITATIVE — do not fall through to os.getenv, or a
|
||||
# secondary profile missing SLACK_APP_TOKEN silently inherits the
|
||||
# default profile's Socket Mode app (#59739). Only an UNSCOPED read
|
||||
# under multiplex (default-profile startup loop, background reconnect
|
||||
# rebuild) falls back to process env, which is that profile's own.
|
||||
try:
|
||||
from agent.secret_scope import get_secret
|
||||
|
||||
app_token = get_secret("SLACK_APP_TOKEN") or os.getenv("SLACK_APP_TOKEN")
|
||||
except Exception:
|
||||
app_token = get_secret("SLACK_APP_TOKEN")
|
||||
except UnscopedSecretError:
|
||||
app_token = os.getenv("SLACK_APP_TOKEN")
|
||||
|
||||
if not raw_token:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from unittest.mock import AsyncMock, MagicMock, patch, call
|
|||
|
||||
import pytest
|
||||
|
||||
import agent.secret_scope as secret_scope
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
from gateway.run import GatewayRunner
|
||||
from gateway.platforms.base import (
|
||||
|
|
@ -263,6 +264,72 @@ class TestAppMentionHandler:
|
|||
expected
|
||||
), f"Slack slash regex does not match {expected}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_uses_profile_scoped_app_token(self):
|
||||
"""Socket Mode must use the active profile's app token in multiplex mode."""
|
||||
config = PlatformConfig(enabled=True, token="xoxb-profile")
|
||||
adapter = SlackAdapter(config)
|
||||
|
||||
def _noop_decorator(_matcher):
|
||||
def decorator(fn):
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
mock_app = MagicMock()
|
||||
mock_app.event = _noop_decorator
|
||||
mock_app.command = _noop_decorator
|
||||
mock_app.action = _noop_decorator
|
||||
mock_app.client = AsyncMock()
|
||||
|
||||
mock_web_client = AsyncMock()
|
||||
mock_web_client.auth_test = AsyncMock(
|
||||
return_value={
|
||||
"user_id": "U_PROFILE",
|
||||
"user": "profilebot",
|
||||
"team_id": "T_PROFILE",
|
||||
"team": "ProfileTeam",
|
||||
}
|
||||
)
|
||||
|
||||
created_handlers = []
|
||||
|
||||
class FakeSocketModeHandler:
|
||||
def __init__(self, app, app_token, proxy=None):
|
||||
self.app = app
|
||||
self.app_token = app_token
|
||||
self.proxy = proxy
|
||||
self.client = MagicMock(proxy=None)
|
||||
created_handlers.append(self)
|
||||
|
||||
async def start_async(self):
|
||||
return None
|
||||
|
||||
async def close_async(self):
|
||||
return None
|
||||
|
||||
secret_scope.set_multiplex_active(True)
|
||||
token = secret_scope.set_secret_scope({"SLACK_APP_TOKEN": "xapp-profile"})
|
||||
try:
|
||||
with (
|
||||
patch.object(_slack_mod, "AsyncApp", return_value=mock_app),
|
||||
patch.object(_slack_mod, "AsyncWebClient", return_value=mock_web_client),
|
||||
patch.object(
|
||||
_slack_mod, "AsyncSocketModeHandler", FakeSocketModeHandler
|
||||
),
|
||||
patch.dict(os.environ, {"SLACK_APP_TOKEN": "xapp-default"}),
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)),
|
||||
patch("asyncio.create_task", side_effect=_fake_create_task),
|
||||
):
|
||||
result = await adapter.connect()
|
||||
finally:
|
||||
secret_scope.reset_secret_scope(token)
|
||||
secret_scope.set_multiplex_active(False)
|
||||
|
||||
assert result is True
|
||||
assert created_handlers
|
||||
assert created_handlers[0].app_token == "xapp-profile"
|
||||
|
||||
|
||||
class TestSlackConnectCleanup:
|
||||
"""Regression coverage for failed connect() cleanup."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue