Skip to content

feat(copilot): Add browse_web tool for JS-rendered page browsing - #12214

Closed
majdyz wants to merge 5 commits into
devfrom
feat/browsing-capability-copilot
Closed

feat(copilot): Add browse_web tool for JS-rendered page browsing#12214
majdyz wants to merge 5 commits into
devfrom
feat/browsing-capability-copilot

Conversation

@majdyz

@majdyz majdyz commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds browse_web copilot tool backed by Stagehand + Browserbase for real-browser page extraction
  • Handles JS-rendered pages, SPAs, and dynamic content that web_fetch cannot reach
  • Fully ephemeral — no external storage required (content stays in-process, never persisted)
  • Gracefully degrades when STAGEHAND_API_KEY / STAGEHAND_PROJECT_ID are not configured

What changed

  • browse_web.py — new BrowseWebTool: navigates to a URL, runs Stagehand extract() with a natural-language instruction, returns up to 50K chars of content
  • models.py — adds BROWSE_WEB response type and BrowseWebResponse model
  • tools/__init__.py — registers browse_web in TOOL_REGISTRY

Design decisions

  • No R2 / no storage: content returned inline as text — no file upload needed (unlike AutoReviewer which needs embeddable GitHub URLs)
  • Thread-safe signal handling: Stagehand registers OS signal handlers on init which raises in worker threads. Applied same monkey-patch pattern as blocks/stagehand/blocks.py
  • Auth required: requires_auth = True — anonymous users cannot consume Browserbase credits
  • Content cap: 50K chars before truncation to protect LLM context window

Required env vars (new)

STAGEHAND_API_KEY      # Browserbase API key
STAGEHAND_PROJECT_ID   # Browserbase project ID
ANTHROPIC_API_KEY      # LLM key for Stagehand's extraction model

Test plan

  • Set env vars, ask Copilot to browse a JS-heavy page (e.g. a React SPA) and verify content is returned
  • Ask Copilot to browse with a specific extraction instruction (e.g. "get all pricing tiers")
  • Verify graceful error message when env vars are missing
  • Verify 50K char truncation on a large page
  • Confirm web_fetch still works independently for static pages

Adds a new `browse_web` copilot tool backed by Stagehand + Browserbase
that handles JavaScript-rendered pages, SPAs, and dynamic content that
the existing `web_fetch` tool cannot reach.

- BrowseWebTool: navigates URLs with a real browser, extracts content
  via natural language instruction using Stagehand's extract() API
- Ephemeral sessions per call — no external storage needed
- Thread-safe signal handling for the CoPilot executor thread pool
- 50K char content limit to protect LLM context window
- Graceful degradation when Stagehand env vars are not configured
- Registered in TOOL_REGISTRY alongside web_fetch

Requires: STAGEHAND_API_KEY, STAGEHAND_PROJECT_ID, ANTHROPIC_API_KEY
@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new BrowseWebTool that uses Stagehand to navigate and extract JavaScript-rendered page content, updates models and OpenAPI to include a browse_web response type, and includes unit tests and registration of the tool.

Changes

Cohort / File(s) Summary
Backend Tool Implementation
autogpt_platform/backend/backend/copilot/tools/browse_web.py, autogpt_platform/backend/backend/copilot/tools/__init__.py
Introduces BrowseWebTool with Stagehand integration: lazy import, thread-safe signal handler patching, client lifecycle (init/close), navigation and extraction with timeouts, URL/input validation, truncation logic, structured success/error responses, and registers the tool in TOOL_REGISTRY.
Data Models
autogpt_platform/backend/backend/copilot/tools/models.py
Adds ResponseType.BROWSE_WEB and a BrowseWebResponse model (url, content, truncated).
Frontend API Schema
autogpt_platform/frontend/src/app/api/openapi.json
Adds "browse_web" to the ResponseType enum in the OpenAPI schema.
Tests
autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
New comprehensive unit tests covering metadata, input validation, env var checks, Stagehand-missing handling, success flows (timeouts, truncation, session propagation), error paths, cleanup, and thread-safety of the Stagehand patching logic.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BrowseWebTool as "BrowseWebTool"
  participant StagehandAPI as "Stagehand API"
  participant PageModel as "Page / Extraction Model"

  Client->>BrowseWebTool: request(url, instruction)
  BrowseWebTool->>BrowseWebTool: validate input & env
  BrowseWebTool->>StagehandAPI: init client (lazy import)
  StagehandAPI-->>BrowseWebTool: client
  BrowseWebTool->>StagehandAPI: navigate to URL (with timeout)
  StagehandAPI->>PageModel: render page & run extraction model
  PageModel-->>StagehandAPI: extracted content
  StagehandAPI-->>BrowseWebTool: content/result
  BrowseWebTool->>BrowseWebTool: truncate if needed
  BrowseWebTool-->>Client: BrowseWebResponse / ErrorResponse
  BrowseWebTool->>StagehandAPI: close client
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code to fetch the page,
Stagehand helped me act upon the stage,
Threads well-behaved, no signals astray,
Content trimmed tidy to bring back today,
A tiny rabbit, browsing wise and sage.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and specifically describes the main change: adding a browse_web tool for JS-rendered page browsing, which matches the core objective of the changeset.
Description check ✅ Passed Description is detailed and relevant, covering the tool's purpose, design decisions, configuration requirements, and test plan—all directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/browsing-capability-copilot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added platform/backend AutoGPT Platform - Back end size/l labels Feb 26, 2026
@github-actions

github-actions Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

🔍 PR Overlap Detection

This check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early.

🟡 Medium Risk — Some Line Overlap

These PRs have some overlapping changes:

  • feat(copilot): Add agent-browser multi-step browser automation tools #12230 (majdyz · updated 4m ago)
    • autogpt_platform/backend/backend/copilot/tools/__init__.py: L10-16, L50-58
    • autogpt_platform/backend/backend/copilot/tools/browse_web.py: L1-227
    • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py: L1-486
    • autogpt_platform/backend/backend/copilot/tools/models.py: L41-48, L438-454

🟢 Low Risk — File Overlap Only

These PRs touch the same files but different sections (click to expand)

Summary: 0 conflict(s), 1 medium risk, 1 low risk (out of 2 PRs with file overlap)


Auto-generated on push. Ignores: openapi.json, lock files.

Adds browse_web to the ResponseType enum in the OpenAPI schema to match
the new BrowseWebTool response type added to the backend.
@github-actions github-actions Bot added the platform/frontend AutoGPT Platform - Front end label Feb 26, 2026
@majdyz

majdyz commented Feb 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@majdyz majdyz left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline notes on key design decisions in this PR.

"""

import logging
import os

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why monkey-patch instead of importing from blocks/stagehand/blocks.py?

Stagehand registers OS signal handlers on __init__, which raises ValueError in non-main threads (the CoPilot executor thread pool). The same patch is applied in blocks.py but importing it from there would create a copilot/tools → blocks dependency, which is an unusual direction for this codebase. Duplicating the 5-line patch here keeps the tools module self-contained.

Matches the exact same pattern used in blocks/stagehand/blocks.py:38-48.

Comment thread autogpt_platform/backend/backend/copilot/tools/browse_web.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/__init__.py`:
- Line 13: The eager import of BrowseWebTool causes unguarded stagehand imports
in browse_web.py to break tool initialization if stagehand is missing; update
browse_web.py to either move the stagehand imports into the
BrowseWebTool._execute method (so imports occur only at runtime) or guard the
top-level imports with try/except and make TOOL_REGISTRY register BrowseWebTool
conditionally/lazily (e.g., only add BrowseWebTool to TOOL_REGISTRY if the
stagehand import succeeds); ensure references to BrowseWebTool and TOOL_REGISTRY
remain unchanged so other tools register normally.

In `@autogpt_platform/backend/backend/copilot/tools/browse_web.py`:
- Around line 183-186: The truncation logic appends "\n\n[Content truncated]"
after slicing to _MAX_CONTENT_CHARS, which causes the final content to exceed
the 50,000-character cap; adjust the truncation so that when truncated is True
you slice content to _MAX_CONTENT_CHARS minus the length of the suffix (or
otherwise ensure len(content) <= _MAX_CONTENT_CHARS) before appending the
suffix, updating the block that sets truncated and mutates content (variables:
truncated, content, constant: _MAX_CONTENT_CHARS, and the appended suffix
string) so the returned content never exceeds the cap.
- Around line 195-200: The user-facing ErrorResponse currently includes raw
exception text (variable e) in browse_web's exception handler; change it to a
generic message (e.g., "Failed to browse URL") and an appropriate error code
("browse_failed"), log the full exception details internally using
logger.exception or logger.warning(..., exc_info=True) for debugging, and ensure
session_id and other safe fields remain, so no provider/internal exception text
is returned to users in the ErrorResponse.
- Around line 52-60: Remove the redundant context manager _thread_safe_signal
and its usage: delete the _thread_safe_signal generator function definition, and
where the code uses "with _thread_safe_signal():" (the usage wrapping the
worker/thread setup), remove the with line and dedent the inner block so the
wrapped code runs directly; rely on the existing _register_signal_handlers patch
instead of mutating signal.signal from worker threads.
- Around line 176-177: The page navigation and extraction calls use
page.goto(url) and page.extract(instruction) without Stagehand timeouts; update
both calls to pass an explicit timeoutMs value (e.g., page.goto(url,
timeoutMs=...) and page.extract(instruction, timeoutMs=...)) instead of using
asyncio.timeout(), picking the appropriate millisecond value from existing
config/constants or introduce a sensible default constant and reuse it so both
operations have bounded timeouts.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 195b142 and 3fbc695.

📒 Files selected for processing (4)
  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/frontend/src/app/api/openapi.json
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

Files:

  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/backend/**/*.{py,txt}

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use poetry run prefix for all Python commands, including testing, linting, formatting, and migrations

Files:

  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/backend/backend/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings

Files:

  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
🧠 Learnings (2)
📚 Learning: 2026-02-27T07:26:32.993Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12213
File: autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunMCPTool/helpers.tsx:23-24
Timestamp: 2026-02-27T07:26:32.993Z
Learning: In autogpt_platform/frontend/src/app/(platform)/copilot/tools/**/helpers.tsx files, inline TypeScript interfaces for tool response types (e.g., MCPToolsDiscoveredOutput, BlockDetailsResponse) are intentional for SSE stream payloads that don't appear in openapi.json. Only ResponseType enum values are generated. This pattern should not be flagged for replacement with generated types.

Applied to files:

  • autogpt_platform/frontend/src/app/api/openapi.json
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
🧬 Code graph analysis (3)
autogpt_platform/backend/backend/copilot/tools/__init__.py (1)
autogpt_platform/backend/backend/copilot/tools/browse_web.py (1)
  • BrowseWebTool (65-207)
autogpt_platform/backend/backend/copilot/tools/models.py (1)
autogpt_platform/backend/backend/copilot/response_model.py (1)
  • ResponseType (20-44)
autogpt_platform/backend/backend/copilot/tools/browse_web.py (3)
autogpt_platform/backend/backend/copilot/model.py (1)
  • ChatSession (126-302)
autogpt_platform/backend/backend/copilot/tools/base.py (1)
  • BaseTool (16-119)
autogpt_platform/backend/backend/copilot/tools/models.py (3)
  • BrowseWebResponse (443-449)
  • ErrorResponse (205-210)
  • ToolResponseBase (56-61)
🔇 Additional comments (5)
autogpt_platform/backend/backend/copilot/tools/models.py (2)

44-45: ResponseType.BROWSE_WEB is correctly integrated.

The new enum value is consistent with the tool naming and response model usage.


443-450: BrowseWebResponse model shape looks correct.

The response fields (url, content, truncated) and typed discriminator align well with browse_web output expectations.

autogpt_platform/backend/backend/copilot/tools/__init__.py (1)

54-55: browse_web registry wiring is clean.

The registry entry matches the tool name and keeps discovery/execution paths consistent.

autogpt_platform/frontend/src/app/api/openapi.json (1)

11170-11170: ResponseType enum update is aligned with the new tool response.

Adding "browse_web" on Line 11170 keeps the frontend discriminator list in sync with the backend tool response type.

autogpt_platform/backend/backend/copilot/tools/browse_web.py (1)

19-21: The current implementation is correct. stagehand is a required dependency declared in pyproject.toml, not optional. Required dependencies should not be wrapped in try-except; they should fail at import time if missing, which is the correct behavior. This pattern is intentionally used consistently across the codebase—autogpt_platform/backend/backend/blocks/stagehand/blocks.py uses the identical approach without defensive wrapping. The monkey patching of _register_signal_handlers is already defensive via the _safe_register_signal_handlers wrapper, which correctly checks thread context before calling the original handler.

Comment thread autogpt_platform/backend/backend/copilot/tools/__init__.py
Comment thread autogpt_platform/backend/backend/copilot/tools/browse_web.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/tools/browse_web.py Outdated
Comment thread autogpt_platform/backend/backend/copilot/tools/browse_web.py
Comment thread autogpt_platform/backend/backend/copilot/tools/browse_web.py Outdated
@majdyz

majdyz commented Feb 27, 2026

Copy link
Copy Markdown
Contributor Author

Design notes — key decisions in this PR

1. Signal handler monkey-patch (browse_web.py:38-48)

Stagehand calls signal.signal() during __init__, which raises ValueError in non-main threads. The CoPilot executor runs tools in a ThreadPoolExecutor, so this needs the same monkey-patch that blocks/stagehand/blocks.py already applies. Rather than importing from the blocks module (wrong dependency direction for copilot/tools), the 5-line patch is self-contained here.


2. requires_auth = True (browse_web.py:110)

Each browse_web call creates a Browserbase cloud browser session, which costs money. Anonymous users are explicitly blocked to prevent credit abuse. Authenticated users' costs are tied to their account.


3. 50K char content cap (browse_web.py:20)

Stagehand's extract() can return very long pages. Without a cap, a large page could flood the LLM context window and hit the claude_agent_max_buffer_size (currently 10MB). 50K chars ≈ ~12K tokens, leaving plenty of room for the rest of the context.


4. No session/cookie persistence (by design)

Each call creates a fresh ephemeral Browserbase session. Persistent sessions (for sites requiring login) can be added later by storing the Browserbase session_id as a user credential. The current PR intentionally keeps scope minimal — stateless browsing covers the majority of use cases (reading articles, docs, product pages).


5. Overlap with #12213 (run_mcp_tool)

Both PRs add a new entry to models.py ResponseType and TOOL_REGISTRY. This is a low-risk conflict — whoever merges second just needs to rebase. The lines don't interact.

- Lazy-import Stagehand inside _execute with ImportError guard so missing
  stagehand package never breaks other tools in the registry
- Remove _thread_safe_signal context manager (redundant + globally mutates
  signal.signal from worker threads, which is harmful)
- Add _patch_stagehand_once() with double-checked locking — thread-safe
  one-shot monkey-patch identical to the pattern in blocks/stagehand/blocks.py
- Add explicit timeoutMs to page.goto() (30 s) and page.extract() (60 s)
- Fix truncation: compute keep = MAX - len(suffix) to stay within cap
- Catch Exception without binding `e`; log via logger.exception(); return
  generic message to avoid leaking raw exception text to callers
@majdyz

majdyz commented Feb 27, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit review — follow-up (commit 9ae1bda)

All 5 actionable items addressed:

🔴 Critical — unguarded module-level stagehand imports (__init__.py line 13 + browse_web.py)
Fixed. Both import stagehand.main and from stagehand import Stagehand are now lazy-imported inside _execute behind a try/except ImportError guard. _patch_stagehand_once() is called only after the import succeeds. If Stagehand is absent, only browse_web returns an ErrorResponse; all other registered tools are unaffected.

🟠 Major — _thread_safe_signal context manager (redundant + harmful)
Removed entirely. The monkey-patch in _patch_stagehand_once() (double-checked locking, applied once per process) is sufficient. The old context manager was additionally harmful because it globally mutated signal.signal from a worker thread, which is not thread-safe itself.

🟠 Major — no explicit timeouts on page.goto() / page.extract()
Fixed. Added timeoutMs=_GOTO_TIMEOUT_MS (30 000 ms) and timeoutMs=_EXTRACT_TIMEOUT_MS (60 000 ms). Constants are declared at module level and documented.

🟠 Major — truncation exceeded the 50 000-char cap
Fixed. New logic: keep = max(0, _MAX_CONTENT_CHARS - len(suffix)); content = content[:keep] + suffix — the final string is always ≤ _MAX_CONTENT_CHARS.

🟠 Major — raw exception text leaked to user
Fixed. The exception is no longer bound (except Exception: without as e) and the user-facing message is the generic "Failed to browse URL.". Full traceback is logged via logger.exception("[browse_web] Failed for %s", url).

37 tests covering:
- Tool metadata (name, requires_auth, parameter schema, registry registration)
- Input validation (missing/empty/non-HTTP URLs)
- Env var checks (missing STAGEHAND_API_KEY, STAGEHAND_PROJECT_ID, ANTHROPIC_API_KEY)
- Stagehand absent (ImportError → ErrorResponse, other tools unaffected)
- Successful browse (response shape, instruction forwarding, timeout constants,
  session_id propagation, client.close() called)
- Truncation (within limit, over limit, exact limit, empty/None extraction)
- Error handling (generic message, no raw exception leakage, finally close,
  close() failure does not propagate)
- Thread safety (_patch_stagehand_once idempotence, worker-thread no-op)

All tests run without a running server or database via sys.modules injection.
@majdyz
majdyz marked this pull request as ready for review February 27, 2026 13:45
@majdyz
majdyz requested a review from a team as a code owner February 27, 2026 13:45
@majdyz
majdyz requested review from Bentlybro and Pwuts and removed request for a team February 27, 2026 13:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
autogpt_platform/backend/backend/copilot/tools/browse_web_test.py (1)

157-175: Consider parametrizing the URL-scheme rejection cases.

The ftp://, file://, and javascript: tests are structurally identical and can be collapsed into one parametrized test for easier maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/tools/browse_web_test.py` around
lines 157 - 175, The three identical tests for rejected URL schemes
(test_ftp_url_rejected, test_file_url_rejected, test_javascript_url_rejected)
should be collapsed into a single parametrized test: create one async test
(e.g., test_rejected_url_schemes) that uses pytest.mark.parametrize with a list
of URLs ("ftp://example.com/file", "file:///etc/passwd", "javascript:alert(1)")
and calls BrowseWebTool()._execute(user_id="u1", session=make_session(),
url=url) for each case, asserting the result is an ErrorResponse and keeping the
existing scheme-specific assertion (e.g., for ftp assert "http" in
result.message.lower() if still needed) or otherwise asserting a generic error
message; remove the three duplicate test functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/browse_web.py`:
- Around line 136-141: The prefix-only URL check is too weak; update the
validation in the browse_web handler (the code that checks the variable `url`
and currently returns `ErrorResponse`) to parse the URL with
urllib.parse.urlparse, verify that parsed.scheme.lower() is either "http" or
"https", and ensure parsed.netloc is non-empty (reject if empty or missing);
replace the startswith branch with this parsed-scheme + netloc check and return
the same ErrorResponse (error="invalid_url") when validation fails so malformed
or mixed-case schemes are rejected and true HTTP/HTTPS URLs are accepted.

---

Nitpick comments:
In `@autogpt_platform/backend/backend/copilot/tools/browse_web_test.py`:
- Around line 157-175: The three identical tests for rejected URL schemes
(test_ftp_url_rejected, test_file_url_rejected, test_javascript_url_rejected)
should be collapsed into a single parametrized test: create one async test
(e.g., test_rejected_url_schemes) that uses pytest.mark.parametrize with a list
of URLs ("ftp://example.com/file", "file:///etc/passwd", "javascript:alert(1)")
and calls BrowseWebTool()._execute(user_id="u1", session=make_session(),
url=url) for each case, asserting the result is an ErrorResponse and keeping the
existing scheme-specific assertion (e.g., for ftp assert "http" in
result.message.lower() if still needed) or otherwise asserting a generic error
message; remove the three duplicate test functions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3fbc695 and 1c26799.

📒 Files selected for processing (2)
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Seer Code Review
  • GitHub Check: types
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: end-to-end tests
  • GitHub Check: Check PR Status
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (6)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

Files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/backend/**/*.{py,txt}

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use poetry run prefix for all Python commands, including testing, linting, formatting, and migrations

Files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/backend/**/*_test.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

autogpt_platform/backend/**/*_test.py: Always review snapshot changes with git diff before committing when updating snapshots with poetry run pytest --snapshot-update
Use pytest with snapshot testing for API responses in test files
Colocate test files with source files using the *_test.py naming convention

Files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
autogpt_platform/backend/backend/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)

Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings

Files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
autogpt_platform/backend/**/*test*.py

📄 CodeRabbit inference engine (AGENTS.md)

Run poetry run test for backend testing (runs pytest with docker based postgres + prisma)

Files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
🧠 Learnings (2)
📚 Learning: 2026-01-28T18:29:34.362Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: autogpt_platform/frontend/src/tests/CLAUDE.md:0-0
Timestamp: 2026-01-28T18:29:34.362Z
Learning: Applies to autogpt_platform/frontend/src/tests/src/tests/**/*.spec.ts : Use E2E tests (Playwright) for flows requiring real browser APIs (clipboard, downloads) or cross-page navigation

Applied to files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/copilot/tools/browse_web_test.py
  • autogpt_platform/backend/backend/copilot/tools/browse_web.py
🧬 Code graph analysis (2)
autogpt_platform/backend/backend/copilot/tools/browse_web_test.py (2)
autogpt_platform/backend/backend/copilot/tools/browse_web.py (2)
  • BrowseWebTool (68-227)
  • _patch_stagehand_once (44-65)
autogpt_platform/backend/backend/copilot/tools/models.py (1)
  • BrowseWebResponse (443-449)
autogpt_platform/backend/backend/copilot/tools/browse_web.py (3)
autogpt_platform/backend/backend/copilot/model.py (1)
  • ChatSession (126-302)
autogpt_platform/backend/backend/copilot/tools/base.py (1)
  • BaseTool (16-119)
autogpt_platform/backend/backend/copilot/tools/models.py (3)
  • BrowseWebResponse (443-449)
  • ErrorResponse (205-210)
  • ToolResponseBase (56-61)
🔇 Additional comments (4)
autogpt_platform/backend/backend/copilot/tools/browse_web_test.py (2)

250-439: Great execution-path coverage for the tool lifecycle.

These tests strongly cover success, truncation, failure, and cleanup behavior with clear assertions and isolated mocks.


446-485: Thread-safety/idempotence checks are solid.

The focused tests around _patch_stagehand_once() add meaningful protection against regressions in concurrent contexts.

autogpt_platform/backend/backend/copilot/tools/browse_web.py (2)

167-177: Lazy import + graceful Stagehand-absent handling looks good.

Scoping optional dependency failure to this tool keeps unrelated tools operational.


201-206: Truncation cap implementation is correct and safe.

Nice job ensuring the suffix is included without exceeding _MAX_CONTENT_CHARS.

Comment on lines +136 to +141
if not url.startswith(("http://", "https://")):
return ErrorResponse(
message="Only HTTP/HTTPS URLs are supported.",
error="invalid_url",
session_id=session_id,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Strengthen URL validation beyond a raw prefix check.

A startswith check can reject valid mixed-case schemes and accept malformed HTTP URLs without a host. Parse and validate scheme + netloc explicitly.

Suggested fix
 import logging
 import os
 import threading
 from typing import Any
+from urllib.parse import urlparse
@@
-        if not url.startswith(("http://", "https://")):
+        parsed = urlparse(url)
+        if parsed.scheme.lower() not in {"http", "https"} or not parsed.netloc:
             return ErrorResponse(
                 message="Only HTTP/HTTPS URLs are supported.",
                 error="invalid_url",
                 session_id=session_id,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@autogpt_platform/backend/backend/copilot/tools/browse_web.py` around lines
136 - 141, The prefix-only URL check is too weak; update the validation in the
browse_web handler (the code that checks the variable `url` and currently
returns `ErrorResponse`) to parse the URL with urllib.parse.urlparse, verify
that parsed.scheme.lower() is either "http" or "https", and ensure parsed.netloc
is non-empty (reject if empty or missing); replace the startswith branch with
this parsed-scheme + netloc check and return the same ErrorResponse
(error="invalid_url") when validation fails so malformed or mixed-case schemes
are rejected and true HTTP/HTTPS URLs are accepted.

@autogpt-reviewer autogpt-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Automated Review — PR #12214

PR #12214 — feat(copilot): Add browse_web tool for JS-rendered page browsing
Author: majdyz | Files: 5 (+728/-0) | HEAD: 1c267998

🎯 Verdict: APPROVE WITH CONDITIONS

What This PR Does

Adds a new browse_web copilot tool backed by Stagehand + Browserbase that enables the AI copilot to navigate and extract content from JavaScript-rendered pages (SPAs, dynamic content). This complements the existing web_fetch tool which only handles static HTTP pages. Each call creates an ephemeral cloud browser session, navigates to a URL, runs LLM-powered extraction, and returns up to 50K chars of content.

Specialist Findings

🛡️ Security ⚠️ — URL validation uses startswith("http://") which is weaker than the urlparse + SSRF blocklist approach used by web_fetch (backend/util/request.py:validate_url). However, since browsing happens in Browserbase's cloud (not the AutoGPT backend), there is no SSRF risk to AutoGPT infrastructure. Risk is limited to potential abuse of Browserbase's internal network, which is their sandbox responsibility. Auth enforcement (requires_auth=True) correctly implemented. Error messages sanitized — no secret leakage. Signal handler patch thread-safe. Tests explicitly verify no exception text leaks.

🏗️ Architecture ✅ — Perfect adherence to BaseTool pattern. Dependency direction correct (duplicated 5-line signal patch from blocks/stagehand/ rather than creating cross-layer coupling). Clean module structure at 227 lines. Well-differentiated from web_fetch. Overlap with PRs #12213/#12230 is append-only (low conflict risk).

Performance ✅ — Per-call latency (10-30s for cloud browser + LLM extraction) is inherent and acceptable for a heavyweight fallback tool. Timeouts properly configured (30s navigation, 60s extraction). 50K char truncation correctly implemented. No client.init() timeout is a minor gap. Thread pool starvation is theoretical (LLM naturally rate-limits calls). Silent except: pass in cleanup should log a warning.

🧪 Testing ✅ — Excellent. 486 lines of tests covering all code paths: metadata, input validation (5 URL cases), env var checks (3 combos), ImportError handling, happy path (7 tests), truncation edge cases (6 boundary tests including exact-at-limit), error handling (5 tests), cleanup verification, and thread-safety of signal patch. Secret leakage explicitly tested. All CI green (lint, types, test 3.11/3.12/3.13, e2e, integration, CodeQL, Snyk).

📖 Quality ✅ — Well-structured, readable code. Constants well-named with _MS suffix convention. Logging appropriate (no secrets). Type hints complete. CodeRabbit's 5 actionable items from round 1 all properly fixed in commit 9ae1bda. Docstring coverage at 26% (below 80% threshold) but consistent with all other tools in the codebase — this is a repo-wide pattern, not a PR-specific gap.

📦 Product ✅ — Fills a genuine gap (JS-rendered pages invisible to web_fetch). Clean invocation via copilot function-calling. instruction parameter enables targeted extraction. Graceful degradation when env vars missing. Stateless v1 scope is appropriate. Should add cost hint to tool description to guide LLM toward cheaper web_fetch when possible.

📬 Discussion ✅ — 5/5 critical items from CodeRabbit round 1 verified fixed. One minor unresolved item from round 2 (URL validation with urlparse — addressed in conditions below). Author's design notes are thorough and well-reasoned. Zero human reviewers so far.

🔎 QA ✅ — Live testing confirmed: frontend loads cleanly, browse_web tool registered and callable via copilot, graceful not_configured error when Stagehand credentials absent, copilot auto-falls back to web_fetch, no console errors, OpenAPI schema consistent. Build page renders without issues.

Conditions (should fix before merge)

  1. browse_web.py:136 — Strengthen URL validation with urlparse: Replace the startswith check with urllib.parse.urlparse() to verify scheme in ("http", "https") and netloc is non-empty. This prevents malformed URLs like http:// (no host) and handles mixed-case schemes. Consider also blocking obvious private targets (localhost, 169.254.x.x, 10.x.x.x) for defense-in-depth, even though the browser runs in Browserbase's cloud. This was flagged by both Security and CodeRabbit round 2.

  2. browse_web.py:91-95 — Add cost/fallback guidance to tool description: Include a note like "Each call launches a cloud browser session. Prefer web_fetch for static pages." This helps the LLM make cost-aware tool selection decisions.

Should Fix (Follow-up OK)

  1. browse_web.py:200 — Log cleanup failures instead of silent pass: Change except Exception: pass to logger.warning("[browse_web] Failed to close client", exc_info=True) for observability.
  2. browse_web.py:176 — Add timeout on client.init(): Currently only goto and extract have timeouts. If Browserbase is slow, init() can block indefinitely. Wrap in asyncio.wait_for() or add an overall timeout budget.
  3. Shared utility for Stagehand signal patch: The 5-line monkey-patch is duplicated between browse_web.py and blocks/stagehand/blocks.py. Consider extracting to backend/util/stagehand.py.
  4. Add comment above _STAGEHAND_MODEL: Document why this specific model version is pinned and when it should be updated.

Risk Assessment

Merge risk: LOW | Rollback: EASY (pure addition, no existing code modified)

All CI green. Pure additive change — 5 new/modified files, zero lines deleted from existing code. Feature is gated behind env vars (STAGEHAND_API_KEY, STAGEHAND_PROJECT_ID) so it's effectively dormant until configured. requires_auth=True prevents anonymous abuse. Rollback is trivial (revert adds).

QA Screenshots

@ntindle Clean, well-tested copilot tool addition. Two small conditions (URL validation + tool description hint), otherwise ready to merge.

@majdyz

majdyz commented Mar 1, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #12230 which includes this browse_web tool plus the browser_navigate / browser_act / browser_screenshot agent-browser tools, all fixes from reviews, and the full test suite. Closing this in favour of the combined PR.

@majdyz majdyz closed this Mar 1, 2026
@github-project-automation github-project-automation Bot moved this to Done in Frontend Mar 1, 2026
@github-project-automation github-project-automation Bot moved this from 🆕 Needs initial review to ✅ Done in AutoGPT development kanban Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform/backend AutoGPT Platform - Back end platform/frontend AutoGPT Platform - Front end size/l size/xl

Projects

Status: ✅ Done
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants