fix(agent): harden multimodal tool result flow

Keep multimodal tool outputs on the native content-block path while
restoring redirect SSRF checks for web_fetch image responses. Also share
image block construction, simplify persisted history sanitization, and
add regression tests for image reads and blocked private redirects.

Made-with: Cursor
This commit is contained in:
Xubin Ren
2026-03-21 05:34:56 +00:00
parent 834f1e3a9f
commit 445a96ab55
8 changed files with 133 additions and 46 deletions
+10 -13
View File
@@ -3,10 +3,8 @@
from __future__ import annotations
import asyncio
import base64
import html
import json
import mimetypes
import os
import re
from typing import TYPE_CHECKING, Any
@@ -16,6 +14,7 @@ import httpx
from loguru import logger
from nanobot.agent.tools.base import Tool
from nanobot.utils.helpers import build_image_content_blocks
if TYPE_CHECKING:
from nanobot.config.schema import WebSearchConfig
@@ -245,15 +244,17 @@ class WebFetchTool(Tool):
try:
async with httpx.AsyncClient(proxy=self.proxy, follow_redirects=True, max_redirects=MAX_REDIRECTS, timeout=15.0) as client:
async with client.stream("GET", url, headers={"User-Agent": USER_AGENT}) as r:
from nanobot.security.network import validate_resolved_url
redir_ok, redir_err = validate_resolved_url(str(r.url))
if not redir_ok:
return json.dumps({"error": f"Redirect blocked: {redir_err}", "url": url}, ensure_ascii=False)
ctype = r.headers.get("content-type", "")
if ctype.startswith("image/"):
await r.aread()
r.raise_for_status()
b64 = base64.b64encode(r.content).decode()
return [
{"type": "image_url", "image_url": {"url": f"data:{ctype};base64,{b64}"}, "_meta": {"path": url}},
{"type": "text", "text": f"(Image fetched from: {url})"}
]
raw = await r.aread()
return build_image_content_blocks(raw, ctype, url, f"(Image fetched from: {url})")
except Exception as e:
logger.debug("Pre-fetch image detection failed for {}: {}", url, e)
@@ -319,11 +320,7 @@ class WebFetchTool(Tool):
ctype = r.headers.get("content-type", "")
if ctype.startswith("image/"):
b64 = base64.b64encode(r.content).decode()
return [
{"type": "image_url", "image_url": {"url": f"data:{ctype};base64,{b64}"}, "_meta": {"path": url}},
{"type": "text", "text": f"(Image fetched from: {url})"}
]
return build_image_content_blocks(r.content, ctype, url, f"(Image fetched from: {url})")
if "application/json" in ctype:
text, extractor = json.dumps(r.json(), indent=2, ensure_ascii=False), "json"