* feat(community): add Browserless web_capture screenshot tool Add a web_capture tool that renders a page via Browserless /screenshot and presents it through the artifact system, alongside the existing Browserless web_fetch provider. Hardening: - SSRF guard: reject URLs resolving to private/loopback/link-local (incl. the 169.254.169.254 cloud-metadata endpoint)/reserved/multicast/unspecified addresses; opt out via allow_private_addresses for internal targets. - Surface a warning when Browserless renders a target page that itself responded with a non-2xx/3xx status (X-Response-Code), so an error/anti-bot page is not mistaken for valid visual evidence. - Dedupe colliding output filenames instead of silently overwriting prior captures. Docs: comment out token: $BROWSERLESS_TOKEN in tool examples (an unset $VAR fails AppConfig startup) and document allow_private_addresses. * fix(community): format web_capture guard + document local Browserless startup Address PR #3881 review: fix the lint-backend failure (ruff format on browserless/tools.py) and add local Browserless startup instructions to CONFIGURATION.md so reviewers can run the service to try web_fetch/web_capture.
9.3 KiB
Community Browserless Web Capture Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:test-driven-development for code changes. Commit and push steps are intentionally deferred because this local branch must stay uncommitted until user review.
Goal: Add an opt-in community web_capture tool backed by Browserless /screenshot that renders a URL in headless Chrome, saves a PNG/JPEG/WebP screenshot into the current thread outputs directory, and exposes the file as a DeerFlow artifact path.
Architecture: Keep the integration inside the existing deerflow.community.browserless package. BrowserlessClient owns HTTP request construction and binary response handling; tools.py owns DeerFlow runtime/config resolution, output filename generation, file persistence, and artifact-state updates. The feature uses existing thread-data outputs semantics rather than inventing a new artifact channel.
Tech Stack: Python 3.12, LangChain tool decorators, LangGraph Command, httpx.AsyncClient, Browserless REST /screenshot, pytest.
Technical Design
User-Facing Tool Contract
Add a new configured tool:
tools:
- name: web_capture
group: web
use: deerflow.community.browserless.tools:web_capture_tool
base_url: http://localhost:3032
timeout_s: 30
output_format: png
full_page: true
viewport_width: 1280
viewport_height: 720
# token: $BROWSERLESS_TOKEN
# wait_for_selector: main
# wait_for_selector_timeout_ms: 5000
# wait_for_timeout_ms: 1000
# best_attempt: true
The model-callable arguments should stay narrow:
async def web_capture_tool(
runtime: Runtime,
url: str,
tool_call_id: Annotated[str, InjectedToolCallId],
filename: str | None = None,
full_page: bool | None = None,
output_format: str | None = None,
viewport_width: int | None = None,
viewport_height: int | None = None,
) -> Command:
Behavior:
- Accept only explicit
http://orhttps://URLs. - Save the screenshot under
runtime.state["thread_data"]["outputs_path"]. - Return a
Commandthat appends the virtual artifact path/mnt/user-data/outputs/<filename>and a shortToolMessage. - Use tool config defaults when optional callable args are omitted.
- Read Browserless credentials from the configured
tokenvalue or theBROWSERLESS_TOKENenvironment variable.
Browserless API Mapping
Official Browserless docs describe POST /screenshot with JSON body:
{
"url": "https://example.com/",
"options": {
"fullPage": true,
"type": "png"
}
}
Browserless returns binary image content with Content-Type: image/png, image/jpeg, or image/webp depending on options.type. Shared request fields such as waitForSelector, waitForTimeout, bestAttempt, gotoOptions, rejectResourceTypes, and rejectRequestPattern are top-level fields.
This PR supports:
urloptions.fullPageoptions.typeoptions.qualityfor JPEG/WebP only when configuredviewportfrom config/callable width and heightwaitForSelectorfrom configwaitForTimeoutfrom configbestAttemptfrom config
This PR intentionally does not support:
- inline
html addScriptTagaddStyleTag- authenticated profiles
- arbitrary launch parameters
- proxy parameters
Those omitted fields are useful, but exposing them directly to an agent increases the chance of unexpected side effects or credential/session leakage. They can be added later behind explicit config if maintainers want them.
File and Artifact Boundary
The tool writes to the host-side outputs path that ThreadDataMiddleware provides, with filesystem writes offloaded through asyncio.to_thread:
thread_data = runtime.state.get("thread_data") or {}
outputs_path = thread_data.get("outputs_path")
The final user-facing artifact path is always:
/mnt/user-data/outputs/<safe-filename>.<ext>
Filename handling:
- If
filenameis provided, strip directories and normalize it to a safe stem. - If omitted, derive a readable stem from the URL host/path and append a UTC timestamp.
- Enforce extension from
output_format. - Use only ASCII letters, digits, dot, dash, and underscore in the final basename.
Error Handling
BrowserlessClient.capture_screenshot() sends token as a query parameter, matching the current Browserless /screenshot documentation, and returns a small result object instead of raising expected API/network errors:
@dataclass(frozen=True)
class BrowserlessScreenshotResult:
content: bytes
content_type: str
target_status_code: str
target_status: str
final_url: str
Expected failures return strings beginning with Error: from the client, matching existing fetch_html() behavior:
- non-200 Browserless response
- empty image response
- timeout
- request error
The tool converts any error string into a ToolMessage and does not mutate artifacts. Unexpected exceptions are logged and returned as Error: ....
Tests
Use TDD with backend/tests/test_browserless_client.py.
Client tests:
capture_screenshotposts to/screenshotwithurl,options.fullPage,options.type,viewport, and wait fields.- Token is sent as the Browserless
tokenquery parameter. - Non-200 status returns an error string with status and response snippet.
- Empty binary content returns a clear error.
Tool tests:
- Successful
web_capture_toolwrites bytes tooutputs_pathand returns artifact path inCommand.update["artifacts"]. - Tool reads
web_captureconfig block, notweb_fetch. - Invalid URL is rejected before calling Browserless.
- Missing runtime thread outputs returns a clear error and writes no file.
- Unsafe filename is sanitized to a basename under outputs.
Documentation Updates
Update:
config.example.yaml: add commentedweb_captureBrowserless block.backend/docs/CONFIGURATION.md: includeweb_capturein tool list and mention Browserless.frontend/src/content/en/harness/tools.mdx: add Browserless tab for web fetch and a web capture section.frontend/src/content/zh/harness/tools.mdx: mirror the English documentation.scripts/doctor.py: recognizeweb_captureandbrowserlesstoken/config status.backend/packages/harness/deerflow/community/browserless/__init__.py: exportweb_capture_tool.
No README.md update is planned because this is a provider-specific opt-in community tool, and existing provider additions in this area document through config/docs pages.
Execution Tasks
Task 1: Write Failing Client Tests
Files:
- Modify:
backend/tests/test_browserless_client.py
Steps:
- Add tests for
BrowserlessClient.capture_screenshot(). - Run
cd backend && uv run pytest tests/test_browserless_client.py::TestBrowserlessClient -q. - Expected red state:
AttributeError: 'BrowserlessClient' object has no attribute 'capture_screenshot'.
Task 2: Implement Browserless Screenshot Client
Files:
- Modify:
backend/packages/harness/deerflow/community/browserless/browserless_client.py - Modify:
backend/tests/test_browserless_client.py
Steps:
- Add
BrowserlessScreenshotResult. - Add
capture_screenshot()usinghttpx.AsyncClient.post(f"{base_url}/screenshot", ...). - Preserve existing
fetch_html()behavior. - Run client tests until green.
Task 3: Write Failing Tool Tests
Files:
- Modify:
backend/tests/test_browserless_client.py
Steps:
- Add tests for
web_capture_tool. - Use a fake runtime with
state={"thread_data": {"outputs_path": str(tmp_path)}}. - Patch
_get_browserless_client()and_get_tool_config(). - Run the new tool tests.
- Expected red state:
AttributeErroror missingweb_capture_tool.
Task 4: Implement web_capture_tool
Files:
- Modify:
backend/packages/harness/deerflow/community/browserless/tools.py - Modify:
backend/packages/harness/deerflow/community/browserless/__init__.py
Steps:
- Add helpers for config lookup, URL validation, format validation, filename sanitization, and virtual artifact path generation.
- Add
@tool("web_capture", parse_docstring=True). - Write the screenshot bytes to outputs.
- Return
Command(update={"artifacts": [virtual_path], "messages": [ToolMessage(...)]}). - Run Browserless tests until green.
Task 5: Update Config, Doctor, and Docs
Files:
- Modify:
config.example.yaml - Modify:
scripts/doctor.py - Modify:
backend/docs/CONFIGURATION.md - Modify:
frontend/src/content/en/harness/tools.mdx - Modify:
frontend/src/content/zh/harness/tools.mdx
Steps:
- Add commented config block for
web_capture. - Extend doctor provider map.
- Update docs with concise config snippets.
- Run targeted tests and lint/format checks.
Task 6: Verification and Self-Review
Run:
cd backend && uv run pytest tests/test_browserless_client.py -q
cd backend && uv run ruff check packages/harness/deerflow/community/browserless tests/test_browserless_client.py
cd backend && uv run ruff format --check packages/harness/deerflow/community/browserless tests/test_browserless_client.py
Manual review checklist:
- No tracked changes outside the scoped files above.
- No commit created.
web_capturereads theweb_captureconfig block.- Tool does not expose inline HTML, script injection, style injection, or Browserless profile.
- Tool writes only under current thread outputs path.
- Errors do not mutate artifacts.