feat: add Slack image support with url_private auth and content-type validation [closes: OPE-23] (#1073)

* feat: add Slack image support with url_private auth and content-type validation

* fix: simplify Slack image fetch by removing manual redirect logic

* fix: use urlparse hostname check to resolve CodeQL URL sanitization warning

* Update agent/utils/multimodal.py

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* fix: simplify host matching conditions in multimodal image fetching

* reverting changes

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
Aran Yogesh 2026-03-20 14:34:14 -07:00 • committed by GitHub
parent 46d7ed9d43
commit f79e824d8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 2 deletions

View file

@ -8,6 +8,7 @@ import mimetypes
import os
import re
from typing import Any
from urllib.parse import urlparse
import httpx
from langchain_core.messages.content import create_image_block
@ -44,7 +45,8 @@ async def fetch_image_block(
try:
logger.debug("Fetching image from %s", image_url)
headers = None
if "uploads.linear.app" in image_url:
host = (urlparse(image_url).hostname or "").lower()
if host == "uploads.linear.app" or host.endswith(".uploads.linear.app"):
linear_api_key = os.environ.get("LINEAR_API_KEY", "")
if linear_api_key:
headers = {"Authorization": linear_api_key}
@ -53,7 +55,16 @@ async def fetch_image_block(
"LINEAR_API_KEY not set; cannot authenticate image fetch for %s",
image_url,
)
response = await client.get(image_url, headers=headers)
elif host == "files.slack.com" or host.endswith(".files.slack.com"):
slack_bot_token = os.environ.get("SLACK_BOT_TOKEN", "")
if slack_bot_token:
headers = {"Authorization": f"Bearer {slack_bot_token}"}
else:
logger.warning(
"SLACK_BOT_TOKEN not set; cannot authenticate image fetch for %s",
image_url,
)
response = await client.get(image_url, headers=headers, follow_redirects=True)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").split(";")[0].strip()
if not content_type:
@ -66,6 +77,15 @@ async def fetch_image_block(
return None
content_type = guessed
supported_types = {"image/jpeg", "image/png", "image/gif", "image/webp"}
if content_type not in supported_types:
logger.warning(
"Unsupported content type '%s' for %s; skipping image",
content_type,
image_url,
)
return None
encoded = base64.b64encode(response.content).decode("ascii")
logger.info(
"Fetched image %s (%s, %d bytes)",

View file

@ -771,6 +771,25 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
)
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
image_urls = dedupe_urls(
[url for msg in context_messages for url in extract_image_urls(msg.get("text", ""))]
+ [
f["url_private"]
for msg in context_messages
for f in msg.get("files", [])
if isinstance(f, dict)
and f.get("mimetype", "").startswith("image/")
and f.get("url_private")
]
)
if image_urls:
logger.info("Preparing %d image(s) for Slack mention", len(image_urls))
async with httpx.AsyncClient() as http_client:
for image_url in image_urls:
image_block = await fetch_image_block(image_url, http_client)
if image_block:
content_blocks.append(image_block)
configurable: dict[str, Any] = {
"repo": repo_config,
"slack_thread": {