Merge branch 'main' into bb/gui

This commit is contained in:
emozilla
2026-05-17 02:02:28 -04:00
45 changed files with 1528 additions and 171 deletions
+3
View File
@@ -829,6 +829,9 @@ SUPPORTED_DOCUMENT_TYPES = {
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".ts": "text/plain",
".py": "text/plain",
".sh": "text/plain",
}
+78 -10
View File
@@ -3564,6 +3564,43 @@ class DiscordAdapter(BasePlatformAdapter):
return bool(configured)
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
def _discord_allow_any_attachment(self) -> bool:
"""Return whether Discord attachments bypass the SUPPORTED_DOCUMENT_TYPES allowlist.
When True, any uploaded file is cached to disk and surfaced to the
agent as a local path so it can be inspected via terminal / read_file
/ ffprobe / etc. Default False preserves the historical behaviour of
dropping unsupported types with a warning log.
"""
configured = self.config.extra.get("allow_any_attachment")
if configured is not None:
if isinstance(configured, str):
return configured.lower() not in {"false", "0", "no", "off", ""}
return bool(configured)
return os.getenv("DISCORD_ALLOW_ANY_ATTACHMENT", "false").lower() in {"true", "1", "yes", "on"}
def _discord_max_attachment_bytes(self) -> int:
"""Return the per-attachment byte cap. 0 means unlimited.
The whole attachment is held in memory while being written to the
cache, so unlimited carries a real memory cost. Default 32 MiB
matches the historical hardcoded value.
"""
configured = self.config.extra.get("max_attachment_bytes")
if configured is None:
configured = os.getenv("DISCORD_MAX_ATTACHMENT_BYTES")
if configured is None or configured == "":
return 32 * 1024 * 1024
try:
value = int(configured)
except (TypeError, ValueError):
logger.warning(
"[Discord] Invalid max_attachment_bytes value %r, falling back to 32 MiB",
configured,
)
return 32 * 1024 * 1024
return max(0, value)
def _discord_free_response_channels(self) -> set:
"""Return Discord channel IDs where no bot mention is required.
@@ -4495,6 +4532,7 @@ class DiscordAdapter(BasePlatformAdapter):
if normalized_content.startswith("/"):
msg_type = MessageType.COMMAND
elif all_attachments:
_allow_any = self._discord_allow_any_attachment()
# Check attachment types
for att in all_attachments:
if att.content_type:
@@ -4509,9 +4547,15 @@ class DiscordAdapter(BasePlatformAdapter):
if att.filename:
_, doc_ext = os.path.splitext(att.filename)
doc_ext = doc_ext.lower()
if doc_ext in SUPPORTED_DOCUMENT_TYPES:
if doc_ext in SUPPORTED_DOCUMENT_TYPES or _allow_any:
msg_type = MessageType.DOCUMENT
break
elif _allow_any:
# No content_type at all (rare — discord usually fills it
# in). Treat as a document so downstream pipelines surface
# the path to the agent.
msg_type = MessageType.DOCUMENT
break
# When auto-threading kicked in, route responses to the new thread
effective_channel = auto_threaded_channel or message.channel
@@ -4594,31 +4638,48 @@ class DiscordAdapter(BasePlatformAdapter):
if not ext and content_type:
mime_to_ext = {v: k for k, v in SUPPORTED_DOCUMENT_TYPES.items()}
ext = mime_to_ext.get(content_type, "")
if ext not in SUPPORTED_DOCUMENT_TYPES:
allow_any_attachment = self._discord_allow_any_attachment()
in_allowlist = ext in SUPPORTED_DOCUMENT_TYPES
if not in_allowlist and not allow_any_attachment:
logger.warning(
"[Discord] Unsupported document type '%s' (%s), skipping",
ext or "unknown", content_type,
)
else:
MAX_DOC_BYTES = 32 * 1024 * 1024
if att.size and att.size > MAX_DOC_BYTES:
max_doc_bytes = self._discord_max_attachment_bytes()
if max_doc_bytes and att.size and att.size > max_doc_bytes:
logger.warning(
"[Discord] Document too large (%s bytes), skipping: %s",
att.size, att.filename,
"[Discord] Document too large (%s bytes > cap %s), skipping: %s",
att.size, max_doc_bytes, att.filename,
)
else:
try:
raw_bytes = await self._cache_discord_document(att, ext)
cached_path = cache_document_from_bytes(
raw_bytes, att.filename or f"document{ext}"
raw_bytes, att.filename or f"document{ext or '.bin'}"
)
doc_mime = SUPPORTED_DOCUMENT_TYPES[ext]
if in_allowlist:
doc_mime = SUPPORTED_DOCUMENT_TYPES[ext]
else:
# allow_any_attachment path: untyped file. Use the
# source content_type if discord gave us one,
# otherwise fall back to octet-stream so the agent
# knows it's binary and reaches for terminal tools.
doc_mime = (
content_type
if content_type and content_type != "unknown"
else "application/octet-stream"
)
media_urls.append(cached_path)
media_types.append(doc_mime)
logger.info("[Discord] Cached user document: %s", cached_path)
logger.info(
"[Discord] Cached user %s: %s",
"document" if in_allowlist else "attachment",
cached_path,
)
# Inject text content for plain-text documents (capped at 100 KB)
MAX_TEXT_INJECT_BYTES = 100 * 1024
if ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
if in_allowlist and ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
try:
text_content = raw_bytes.decode("utf-8")
display_name = att.filename or f"document{ext}"
@@ -4630,6 +4691,13 @@ class DiscordAdapter(BasePlatformAdapter):
pending_text_injection = injection
except UnicodeDecodeError:
pass
# NOTE: for the allow_any_attachment path we deliberately
# do NOT inject a path string here. ``gateway/run.py``
# already detects DOCUMENT-typed events with
# ``application/octet-stream`` MIME and emits a context
# note with the sandbox-translated cache path via
# ``to_agent_visible_cache_path()`` (important for
# Docker/Modal terminal backends).
except Exception as e:
logger.warning(
"[Discord] Failed to cache document %s: %s",
+7 -6
View File
@@ -54,6 +54,13 @@ from gateway.platforms.base import (
logger = logging.getLogger(__name__)
_BUILTIN_DELIVER_PLATFORMS = {
"telegram", "discord", "slack", "signal", "sms", "whatsapp",
"matrix", "mattermost", "homeassistant", "email", "dingtalk",
"feishu", "wecom", "wecom_callback", "weixin", "bluebubbles",
"qqbot", "yuanbao",
}
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8644
_INSECURE_NO_AUTH = "INSECURE_NO_AUTH"
@@ -238,12 +245,6 @@ class WebhookAdapter(BasePlatformAdapter):
# Cross-platform delivery — any platform with a gateway adapter.
# Check both built-in names and plugin-registered platforms.
_BUILTIN_DELIVER_PLATFORMS = {
"telegram", "discord", "slack", "signal", "sms", "whatsapp",
"matrix", "mattermost", "homeassistant", "email", "dingtalk",
"feishu", "wecom", "wecom_callback", "weixin", "bluebubbles",
"qqbot", "yuanbao",
}
_is_known_platform = deliver_type in _BUILTIN_DELIVER_PLATFORMS
if not _is_known_platform:
try: