fix(slack): honor NO_PROXY for Slack transport

This commit is contained in:
Badgerbees
2026-04-18 13:47:43 +07:00
committed by Teknium
parent 7eaad06a87
commit 55f212a7a2
3 changed files with 275 additions and 6 deletions

View File

@@ -336,6 +336,39 @@ def proxy_kwargs_for_aiohttp(proxy_url: str | None) -> tuple[dict, dict]:
return {}, {"proxy": proxy_url}
def is_host_excluded_by_no_proxy(hostname: str, no_proxy_value: str | None = None) -> bool:
"""Return True when ``hostname`` matches a ``NO_PROXY`` entry.
Supports comma- or whitespace-separated entries with optional leading dots
and ``*.`` wildcards, which match both the apex domain and subdomains.
"""
raw = no_proxy_value
if raw is None:
raw = os.environ.get("NO_PROXY") or os.environ.get("no_proxy") or ""
raw = raw.strip()
if not raw:
return False
lower_hostname = hostname.lower()
for entry in re.split(r"[\s,]+", raw):
normalized = entry.strip().lower()
if not normalized:
continue
if normalized == "*":
return True
if normalized.startswith("*."):
normalized = normalized[2:]
elif normalized.startswith("."):
normalized = normalized[1:]
if lower_hostname == normalized or lower_hostname.endswith(f".{normalized}"):
return True
return False
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path

View File

@@ -41,6 +41,8 @@ from gateway.platforms.base import (
ProcessingOutcome,
SendResult,
SUPPORTED_DOCUMENT_TYPES,
is_host_excluded_by_no_proxy,
resolve_proxy_url,
safe_url_for_log,
cache_document_from_bytes,
)
@@ -217,6 +219,40 @@ def _serialize_slack_blocks_for_agent(blocks: list, max_chars: int = 6000) -> st
return f"[Slack Block Kit payload for this message]\n```json\n{payload}\n```"
def _apply_slack_proxy(client: Any, proxy_url: Optional[str]) -> None:
"""Apply a resolved proxy to a Slack SDK client or clear it explicitly."""
if hasattr(client, "proxy"):
client.proxy = proxy_url
_SLACK_PROXY_HOSTS = (
"slack.com",
"files.slack.com",
"wss-primary.slack.com",
)
def _resolve_slack_proxy_url() -> Optional[str]:
"""Resolve a proxy URL that Slack SDK clients can safely use."""
proxy_url = resolve_proxy_url()
if not proxy_url:
return None
normalized = proxy_url.lower()
if not normalized.startswith(("http://", "https://")):
logger.info(
"[Slack] Ignoring unsupported proxy scheme for Slack transport: %s",
safe_url_for_log(proxy_url),
)
return None
if any(is_host_excluded_by_no_proxy(host) for host in _SLACK_PROXY_HOSTS):
logger.info("[Slack] NO_PROXY bypasses Slack proxy configuration")
return None
return proxy_url
class SlackAdapter(BasePlatformAdapter):
"""
Slack bot adapter using Socket Mode.
@@ -237,13 +273,13 @@ class SlackAdapter(BasePlatformAdapter):
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform.SLACK)
self._app: Optional[AsyncApp] = None
self._handler: Optional[AsyncSocketModeHandler] = None
self._app: Optional[Any] = None
self._handler: Optional[Any] = None
self._bot_user_id: Optional[str] = None
self._user_name_cache: Dict[str, str] = {} # user_id → display name
self._socket_mode_task: Optional[asyncio.Task] = None
# Multi-workspace support
self._team_clients: Dict[str, AsyncWebClient] = {} # team_id → WebClient
self._team_clients: Dict[str, Any] = {} # team_id → WebClient
self._team_bot_user_ids: Dict[str, str] = {} # team_id → bot_user_id
self._channel_team: Dict[str, str] = {} # channel_id → team_id
# Dedup cache: prevents duplicate bot responses when Socket Mode
@@ -350,6 +386,10 @@ class SlackAdapter(BasePlatformAdapter):
logger.error("[Slack] SLACK_APP_TOKEN not set")
return False
proxy_url = _resolve_slack_proxy_url()
if proxy_url:
logger.info("[Slack] Using proxy for Slack transport: %s", safe_url_for_log(proxy_url))
# Support comma-separated bot tokens for multi-workspace
bot_tokens = [t.strip() for t in raw_token.split(",") if t.strip()]
@@ -377,10 +417,12 @@ class SlackAdapter(BasePlatformAdapter):
# First token is the primary — used for AsyncApp / Socket Mode
primary_token = bot_tokens[0]
self._app = AsyncApp(token=primary_token)
_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)
_apply_slack_proxy(client, proxy_url)
auth_response = await client.auth_test()
team_id = auth_response.get("team_id", "")
bot_user_id = auth_response.get("user_id", "")
@@ -473,7 +515,8 @@ class SlackAdapter(BasePlatformAdapter):
self._app.action(_action_id)(self._handle_approval_action)
# Start Socket Mode handler in background
self._handler = AsyncSocketModeHandler(self._app, app_token)
self._handler = AsyncSocketModeHandler(self._app, app_token, proxy=proxy_url)
_apply_slack_proxy(self._handler.client, proxy_url)
self._socket_mode_task = asyncio.create_task(self._handler.start_async())
self._running = True
@@ -503,7 +546,7 @@ class SlackAdapter(BasePlatformAdapter):
logger.info("[Slack] Disconnected")
def _get_client(self, chat_id: str) -> AsyncWebClient:
def _get_client(self, chat_id: str) -> Any:
"""Return the workspace-specific WebClient for a channel."""
team_id = self._channel_team.get(chat_id)
if team_id and team_id in self._team_clients: