Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b248fa6
feat(teams): scaffold cards/graph/auth_graph modules for outbound files
May 14, 2026
485a215
feat(teams): register Graph deps under lazy_deps + teams-files extra
May 14, 2026
af4da40
feat(teams): card builders for FileConsent/FileInfo/FileDownload
May 14, 2026
0f26da5
feat(teams): MSAL-backed Graph token provider with per-scope caching
May 14, 2026
14e00ba
feat(teams): Graph client — upload_to_sharepoint + download_hosted_co…
May 14, 2026
add3dcc
feat(teams): outbound send_document/send_video/send_voice with DM-vs-…
May 14, 2026
5bbbda3
fix(teams): align build_file_download_card signature with upstream co…
May 14, 2026
76fbcff
fix(teams): bound _pending_uploads memory + clean up on send failure
May 14, 2026
20b3229
feat(teams): fileConsent/invoke handler — PUT bytes to OneDrive + Fil…
May 14, 2026
5c551c6
feat(teams): inbound Graph fallback for hosted-content attachments
May 14, 2026
6a05893
feat(teams): declare TEAMS_SHAREPOINT_SITE_ID/FOLDER in plugin.yaml
May 14, 2026
e7bdb5e
feat(tools): module-level registry for running adapter instances
May 14, 2026
743c744
feat(tools): _send_teams — outbound media via running Teams adapter
May 14, 2026
ac64303
feat(send_message): wire Teams into media-capable dispatch + allowlist
May 14, 2026
b3ba40e
feat(gateway): publish/clear adapters in running-adapter registry
May 14, 2026
714aebc
fix(teams): bridge cross-loop calls from agent worker to gateway loop
May 14, 2026
60b7122
docs(plans): track loop-bridge follow-up (registry generalization)
May 14, 2026
905e3d9
fix(teams): forward contentUrl from card dicts to SDK Attachment
May 15, 2026
6f6a07c
Fix 401 on inbound Bot Framework attachment URLs
May 15, 2026
c0ed1aa
Resolve safe ext for wildcard-MIME Teams attachments
May 15, 2026
8f7a71a
Log dropped text/html attachment payload (Test #7 diagnostic)
May 15, 2026
ecf03f0
Apply allowlist to HTML <img> branch in extract_images
May 15, 2026
15aedc9
Downgrade dropped-attachment forensics log to DEBUG
May 15, 2026
0097d81
Pre-merge cleanup: drop stale impl plan + scrub task-number breadcrumbs
May 15, 2026
c59da3f
Fix FileConsent card stuck-in-grey state on Accept/Decline
May 15, 2026
733a002
Drop stale plan files from docs/plans/
May 15, 2026
bafbb33
Restore docs/plans/2026-05-02-telegram-dm-user-managed-multisession-t…
May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1996,6 +1996,14 @@ def extract_images(content: str) -> Tuple[List[Tuple[str, str]], str]:
- <img src="https://example.com/image.png">
- <img src="https://example.com/image.png"></img>

Both the markdown and HTML branches require the URL to either end in a
known image extension (.png/.jpg/.jpeg/.gif/.webp) or contain a known
image-CDN host fragment. Without that guard, the bot can boomerang any
``<img>`` tag it merely *quotes* in prose (e.g. an inbound Teams AMS
URL or a teaching-example placeholder like ``<img src="https://...">``)
as a real outbound attachment, which the destination platform can't
authenticate and renders as a broken-image icon.

Args:
content: The response text to scan.

Expand All @@ -2005,21 +2013,38 @@ def extract_images(content: str) -> Tuple[List[Tuple[str, str]], str]:
images = []
cleaned = content

def _looks_like_image_url(url: str) -> bool:
"""Allowlist check shared by markdown and HTML branches.

A URL is treated as an image only if it has a recognized image
extension in its path (matched before any query string) or its
host/path contains a known image-CDN fragment.
"""
lower = url.lower().split("?", 1)[0].split("#", 1)[0]
image_exts = (".png", ".jpg", ".jpeg", ".gif", ".webp")
if lower.endswith(image_exts):
return True
cdn_fragments = ("fal.media", "fal-cdn", "replicate.delivery")
url_lower = url.lower()
return any(fragment in url_lower for fragment in cdn_fragments)

# Match markdown images: ![alt](url)
md_pattern = r'!\[([^\]]*)\]\((https?://[^\s\)]+)\)'
for match in re.finditer(md_pattern, content):
alt_text = match.group(1)
url = match.group(2)
# Only extract URLs that look like actual images
if any(url.lower().endswith(ext) or ext in url.lower() for ext in
['.png', '.jpg', '.jpeg', '.gif', '.webp', 'fal.media', 'fal-cdn', 'replicate.delivery']):
if _looks_like_image_url(url):
images.append((url, alt_text))

# Match HTML img tags: <img src="url"> or <img src="url"></img> or <img src="url"/>
html_pattern = r'<img\s+src=["\']?(https?://[^\s"\'<>]+)["\']?\s*/?>\s*(?:</img>)?'
for match in re.finditer(html_pattern, content):
url = match.group(1)
images.append((url, ""))
# Apply the same allowlist as the markdown branch so we don't peel
# quoted/example <img> tags and ship them as broken attachments.
if _looks_like_image_url(url):
images.append((url, ""))

# Remove only the matched image tags from content (not all markdown images)
if images:
Expand Down
30 changes: 30 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,14 @@ async def _handle_adapter_fatal_error(self, adapter: BasePlatformAdapter) -> Non
await adapter.disconnect()
finally:
self.adapters.pop(adapter.platform, None)
# Drop from the running-adapter registry too so a stale
# closed instance isn't left pointing at a dead
# connection. See tools/_running_adapters.py.
try:
from tools._running_adapters import clear_running_adapter
clear_running_adapter(adapter.platform.value)
except Exception: # pragma: no cover
logger.exception("Failed to clear %s from running-adapter registry", adapter.platform.value)
self.delivery_router.adapters = self.adapters

# Queue retryable failures for background reconnection
Expand Down Expand Up @@ -3521,6 +3529,20 @@ async def start(self) -> bool:
success = await self._connect_adapter_with_timeout(adapter, platform)
if success:
self.adapters[platform] = adapter
# Publish to the module-level running-adapter registry
# so outbound code paths in webhook-receive platforms
# (Teams Bot Framework, future Webex/Zoom/Google Chat)
# can reach the *live* instance — not a fresh one.
# Stateless REST adapters don't strictly need this but
# registering them anyway keeps the API uniform and
# opens the door to "send via running adapter" patterns
# for any future use case. See
# tools/_running_adapters.py for the rationale.
try:
from tools._running_adapters import set_running_adapter
set_running_adapter(platform.value, adapter)
except Exception: # pragma: no cover — registry must never block connect
logger.exception("Failed to publish %s to running-adapter registry", platform.value)
self._sync_voice_mode_state_to_adapter(adapter)
connected_count += 1
self._update_platform_runtime_status(
Expand Down Expand Up @@ -4793,6 +4815,14 @@ async def _platform_reconnect_watcher(self) -> None:
success = await self._connect_adapter_with_timeout(adapter, platform)
if success:
self.adapters[platform] = adapter
# Mirror the connect-time registry publish so the
# reconnect path keeps the running-adapter registry
# in sync. See tools/_running_adapters.py.
try:
from tools._running_adapters import set_running_adapter
set_running_adapter(platform.value, adapter)
except Exception: # pragma: no cover
logger.exception("Failed to publish %s to running-adapter registry", platform.value)
self._sync_voice_mode_state_to_adapter(adapter)
self.delivery_router.adapters = self.adapters
del self._failed_platforms[platform]
Expand Down
Loading
Loading