fix(gateway): harden multiplex credential cluster salvage

Follow-ups on top of the cherry-picked cluster commits:

- slack: scope-authoritative app-token read — get_secret() with a
  narrow UnscopedSecretError fallback to os.getenv. Keeps @kohoj's
  correct semantics (scoped profile can never silently inherit the
  default profile's Socket Mode app) while fixing the regression where
  the default-profile startup loop and background reconnect rebuild,
  which call connect() unscoped under multiplex, would raise and
  fail-loop. Supersedes the 'or os.getenv' variant from #64461 which
  reintroduced the cross-profile fallback leak.
- test: unscoped-multiplex fallback regression test for connect().
- run.py: convert the last legacy self.adapters.get(source.platform)
  site (_rename_discord_auto_thread) to _adapter_for_source(source)
  so profile-routed Discord sources rename threads on the right
  adapter (from #57417's sweep).
- AUTHOR_MAP entry for @aguung.
fix/verification-admin-route-recovery
Teknium 2026-07-16 05:12:27 -07:00
parent 64746b4bd3
commit 01d3268e02
3 changed files with 69 additions and 1 deletions

View File

@ -14090,7 +14090,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
"""Best-effort semantic rename of a newly auto-created Discord thread.""" """Best-effort semantic rename of a newly auto-created Discord thread."""
if not await asyncio.to_thread(self._is_discord_auto_thread_lane, source): if not await asyncio.to_thread(self._is_discord_auto_thread_lane, source):
return return
adapter = self.adapters.get(source.platform) if getattr(self, "adapters", None) else None adapter = self._adapter_for_source(source) if getattr(self, "adapters", None) else None
if adapter is None: if adapter is None:
return return
rename_thread = getattr(adapter, "rename_thread", None) rename_thread = getattr(adapter, "rename_thread", None)

View File

@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
# Auto-extracted from noreply emails + manual overrides # Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = { AUTHOR_MAP = {
"agungsubastian1963@gmail.com": "aguung", # PR #64461 salvage (gateway: multiplex secret_scope for authz/Slack/webhooks)
"doogie@spark.local": "SAMBAS123", # PR #64986 salvage (gateway: multiplex primary bot token scope) "doogie@spark.local": "SAMBAS123", # PR #64986 salvage (gateway: multiplex primary bot token scope)
"emrekoca2003@gmail.com": "kocaemre", # PR #36051 salvage (docs: audit round 3 code/doc reconciliation) "emrekoca2003@gmail.com": "kocaemre", # PR #36051 salvage (docs: audit round 3 code/doc reconciliation)
"205466933+wesleion@users.noreply.github.com": "wesleion", # PR #36049 salvage (telegram: per-topic free-response allowlist) "205466933+wesleion@users.noreply.github.com": "wesleion", # PR #36049 salvage (telegram: per-topic free-response allowlist)

View File

@ -330,6 +330,73 @@ class TestAppMentionHandler:
assert created_handlers assert created_handlers
assert created_handlers[0].app_token == "xapp-profile" assert created_handlers[0].app_token == "xapp-profile"
@pytest.mark.asyncio
async def test_connect_unscoped_multiplex_falls_back_to_env(self):
"""Default-profile connect (multiplex active, NO scope installed) must
fall back to process env instead of raising UnscopedSecretError
the primary startup loop and background reconnect rebuild both call
connect() unscoped (#59739 salvage follow-up)."""
config = PlatformConfig(enabled=True, token="xoxb-default")
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_DEFAULT",
"user": "defaultbot",
"team_id": "T_DEFAULT",
"team": "DefaultTeam",
}
)
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)
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.set_multiplex_active(False)
assert result is True
assert created_handlers
assert created_handlers[0].app_token == "xapp-default"
class TestSlackConnectCleanup: class TestSlackConnectCleanup:
"""Regression coverage for failed connect() cleanup.""" """Regression coverage for failed connect() cleanup."""