Skip to content

feat(tools): browse_web — drive a real browser via AgentCore Browser - #1010

Merged
philmerrell merged 2 commits into
developfrom
feature/agentcore-browser-tool
Sep 9, 2026
Merged

feat(tools): browse_web — drive a real browser via AgentCore Browser#1010
philmerrell merged 2 commits into
developfrom
feature/agentcore-browser-tool

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

What

A browse_web tool backed by the AgentCore Browser. Actions: navigate, extract_text, extract_links, click, type, evaluate, screenshot, live_view, close. The browser session persists across calls within a conversation.

The infra was already deployed and idle — AgentCoreBrowserConstruct provisions the CfnBrowserCustom, the runtime role has the browser IAM actions scoped to it, and BROWSER_ID is in the container env. The only thing reading it was a startup log line (inference_api/main.py:61). No CDK change needed.

Zero new dependencies

websockets==16.0 is already in the image, so the CDP layer is hand-rolled (cdp_client.py, ~230 lines) instead of adding Playwright and its bundled Node driver to the inference-api container. What Playwright would buy is auto-waiting and a selector engine we mostly don't use — JS evaluated in the page does the same job, and it's what AWS's own guidance recommends for extraction. cdp_client.py is the seam if frames / real input events / file chooser are ever needed.

Cost posture

A browsing transcript is the textbook unbounded per-turn payload, so:

  • every action's output is capped before it reaches the model (MAX_TEXT_CHARS, MAX_EVAL_CHARS, MAX_LINKS, all env-tunable) with a truncation note that tells the model to use evaluate instead
  • screenshots are never automatic — vision tokens are the most expensive thing this tool can emit, so the model has to ask, and the description steers it to extract_text/evaluate first
  • seeded enabledByDefault=False, gated by BROWSER_TOOL_ENABLED (default on, kill switch)
  • session TTL defaults to 15 min, not AgentCore's 8h max — an abandoned session bills until TTL

Session lifecycle

Follows the "one session can be served by more than one agent" rule: the browser session id lives on Strands agent.state (same pattern as app_context_dispatch), while the live socket is a process-local lookup keyed by that id. A second cached agent — or a container that restarted — reads the same id and reconnects to the same remote session rather than starting and billing a second browser.

WebMCP hook

add_init_script (CDP Page.addScriptToEvaluateOnNewDocument) is implemented and covered by the probe but unused. It's the exact hook a future WebMCP driver needs, per docs/specs/webmcp-host-spa-tools.md (#1004) — included now so the seam exists, not because anything calls it.

Testing

21 unit tests, no AWS: the CDP layer runs against a fake websocket that speaks the protocol (id framing, Target.* browser-scoped vs page commands sessionId-scoped, page exceptions surfacing as CdpError, idempotent close), and the tool layer against a fake CDP session (truncation budgets, missing-selector errors, SSRF-ish URL refusals, kill switch). Full suite: 1476 passed.

Not verified against live AWS. The dev-ai SSO token was expired and the device grant needs an interactive session, so the CDP handshake against AgentCore's automation endpoint is unproven. backend/scripts/probe_agentcore_browser.py runs the five checks unit tests can't:

cd backend && AWS_PROFILE=dev-ai uv run python scripts/probe_agentcore_browser.py --discover

The riskiest assumption it checks is that the automation endpoint exposes a browser-level CDP surface where Target.getTargets / attachToTarget work. If AgentCore hands back an already-page-scoped socket, _attach_to_page is where it fails and the fix is local to that method.

To use it in dev

Re-run the bootstrap seeder (idempotent — skips tools that already exist) to create the catalog row, then grant browse_web to a role and enable it per user.

🤖 Generated with Claude Code

philmerrell and others added 2 commits September 8, 2026 18:39
Adds a `browse_web` tool backed by the AgentCore Browser resource that
PlatformStack already deploys but nothing consumed: the runtime role has
the browser IAM actions and `BROWSER_ID` in env, and the only reader was a
startup log line.

Zero new dependencies. `websockets==16.0` is already in the image, so the
CDP layer is hand-rolled rather than adding Playwright and its bundled Node
driver to the inference-api container for auto-waiting and a selector engine
we mostly don't use — JS evaluated in the page does the same job.
`cdp_client.py` is the seam if richer interaction is ever needed.

Cost posture, since a browsing transcript is the classic unbounded per-turn
payload: every action's output is capped before it reaches the model, and a
screenshot is only ever taken when the model explicitly asks — vision tokens
are the most expensive thing this tool can emit, so it is never automatic.
Seeded `enabledByDefault=False` and gated by `BROWSER_TOOL_ENABLED`.

Session lifecycle follows the "never cache session state on an agent
instance" rule: the browser session *id* lives on Strands `agent.state`
(as `app_context_dispatch` does), while the live socket is process-local
keyed by that id — so a second cached agent for the same conversation
reconnects to the same remote session instead of starting a second one.

`add_init_script` (CDP `Page.addScriptToEvaluateOnNewDocument`) is included
and probed but unused: it is the hook a future WebMCP driver would need,
per docs/specs/webmcp-host-spa-tools.md.

Not verified against live AWS — the dev-ai SSO token was expired and the
device grant needs an interactive session. `scripts/probe_agentcore_browser.py`
runs the five checks that unit tests cannot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding browse_web to the bootstrap seed broke three assertions in
TestSeedDefaultTools that hardcoded "9 tools". The counts now come from
len(DEFAULT_TOOLS), so the invariant under test is the real one — every
entry in the seed list gets created or skipped — instead of a literal that
has to be bumped by hand whenever a tool is added.

Also pins browse_web's seeded fields the way the other tools are pinned,
including enabledByDefault=False: that flag is the cost guard, not a
default worth drifting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 6f248fb into develop Sep 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant