Optional add-on

Optional: Browser

Let the agent read public web pages through a managed, sandboxed headless browser.

25 minAdvanced
Alice’s ask

The “What Are Competitors Doing?” request

Before a board meeting Alice asks: “What does this vendor say on their product page?” The database can't answer that. AgentCore Browser gives the agent a managed headless Chromium in the cloud, isolated from your runtime.

Platform

What the platform provisions for you

.env
ENABLE_BROWSER=true
terminal
uv run bootcamp.py deploy
  • Session permissions for your agent role on the AWS-managed aws.browser.v1
  • The browse_web tool, handed to your orchestrator automatically

Flags default to false and need stage 4 or higher. uv run bootcamp.py status shows CODE_INTERPRETER_ENABLED and BROWSER_ENABLED.

You

What you do as a developer

  1. Read the tool

    phase2/app/agent/sandbox_tools.py
    @tool
    def browse_web(url: str) -> str:
        """Open a public web page in a secure AgentCore Browser sandbox and return its title, final URL and visible text.
    
        Args:
            url: Full http(s) URL of the page to open.
        """
        if urlparse(url).scheme not in {"http", "https"}:
            return "Only http(s) URLs are supported."
        with browser_session(REGION, identifier=BROWSER_ID) as client:
            ws_url, headers = client.generate_ws_headers()
            signed = {name: value for name, value in headers.items() if name in SIGNED_HEADERS}
            with connect(ws_url, additional_headers=signed, max_size=None, open_timeout=CDP_TIMEOUT_SECONDS) as socket:
                page = read_page(CdpConnection(socket), url)
        return f"Title: {page['title']}\nURL: {page['url']}\n\n{page['text']}"

    No Playwright: the tool talks Chrome DevTools Protocol over the browser's SigV4-signed websocket (four CDP commands: open a tab, navigate, wait for load, read title and text). It returns the title, the final URL and the first 4,000 characters of visible text.

    terminal
    uv run bootcamp.py invoke "Open https://example.com and tell me the page title." --actor alice-chen
  2. Add a research specialist

    Challenge

    In phase2/app/agent/agent.py, add a research_agent specialist that reads public pages with browse_web and summarises them with the source URL, and register it with the orchestrator.

    Where: edit phase2/app/agent/agent.py in place, run uv run bootcamp.py deploy (~1.5 min), then check with uv run bootcamp.py invoke "Summarise https://aws.amazon.com/bedrock/agentcore/ in three bullets" --actor alice-chen --stream: the tool line shows [calling research_agent], not browse_web.

    Hint 1

    Same pattern as weather_agent, different tool. AgentCore Browser docs.

    Hint 2

    Import browse_web from sandbox_tools, pass tools=[browse_web], and put guardrails in the prompt: cite the URL, don't guess content the page didn't show.

    Solution
    phase2/app/agent/agent.py (adds research_agent) · replace build_specialists()
    def build_specialists(gateway: MCPClient, results: list[str]) -> list:
        """The data and weather specialists as tools; each searches the Gateway for its tools, then records its answer."""
    
        @tool
        def data_agent(query: str) -> str:
            """Query and analyze the DataStream Corp database (employees, departments, projects).
    
            Args:
                query: A database or data analysis question.
            """
            agent = Agent(
                model=make_model(),
                system_prompt=f"{DATA_PROMPT}\n{guardrail.prompt_hint()}",
                tools=search_tools(gateway, DATA_TOOLS),
                hooks=[ReadOnlyGuardHook(), *guardrail.hooks()],
                callback_handler=None,
            )
            return consult(agent, query, "data")
    
        @tool
        def weather_agent(query: str) -> str:
            """Get the weather forecast for a US location (National Weather Service); says so for non-US places.
    
            Args:
                query: A weather-related question, including the place.
            """
            agent = Agent(
                model=make_model(),
                system_prompt=nws_weather.WEATHER_PROMPT,
                hooks=guardrail.hooks(),
                tools=search_tools(gateway, WEATHER_TOOLS),
                callback_handler=None,
            )
            return consult(agent, query, "weather")
    
        @tool
        def research_agent(question: str) -> str:
            """Research public web pages (product pages, news, docs) and summarise them with their URL.
    
            Args:
                question: What to find out, including the URL if known.
            """
            from sandbox_tools import browse_web
    
            agent = Agent(
                model=make_model(),
                system_prompt="You research public web pages with browse_web. Cite the URL you used. Never guess content.",
                tools=[browse_web],
                hooks=guardrail.hooks(),
                callback_handler=None,
            )
            return consult(agent, question, "research")
    
        def consult(agent: Agent, query: str, specialist: str) -> str:
            """Ask a specialist; an exception (e.g. the gateway's 502 after its retries) becomes an explicit failure."""
            try:
                answer = str(agent(query))
            except Exception as error:
                answer = SPECIALIST_FAILED.format(specialist=specialist, reason=f"{type(error).__name__}: {error}")
            return remember(answer, specialist)
    
        def remember(answer: str, specialist: str = "requested") -> str:
            """Record a specialist's answer for `final_answer`; an empty one is recorded as an explicit failure."""
            answer = answer.strip() or SPECIALIST_FAILED.format(specialist=specialist, reason="it returned an empty answer")
            results.append(answer)
            return answer
    
        return [data_agent, weather_agent, research_agent]

    If you already added analysis_agent (Code Interpreter page), keep it: paste only the new research_agent function and add it to the returned list. Then add a routing line to orchestrator_prompt, e.g. "Route questions about public web pages to research_agent and keep the URL it cites in your answer.\n".

    One more change, or your specialist never runs: with ENABLE_BROWSER=true the orchestrator itself also gets browse_web (plus a prompt hint to use it), and a model given the tool and a specialist picks the tool. Take browse_web away from the orchestrator in build_orchestrator; research_agent imports it directly, and the hint goes with it.

    phase2/app/agent/agent.py (browse_web only in research_agent) · replace build_orchestrator()
    def build_orchestrator(gateway: MCPClient, session_id: str, actor_id: str, specialist_results: list[str]) -> Agent:
        """The orchestrator with both specialists, any enabled sandbox tools and this actor's memory."""
        sandbox_tools = [t for t in optional_tools() if t.tool_name != "browse_web"]  # research_agent has it
        return Agent(
            name="Executive Assistant",
            model=make_model(),
            system_prompt=orchestrator_prompt(actor_id, sandbox_tools),
            hooks=guardrail.hooks(),
            tools=[*build_specialists(gateway, specialist_results), *sandbox_tools],
            session_manager=memory_session_manager(session_id, actor_id),
            callback_handler=None,
        )

    Check with invoke --stream (or traces) that research_agent is the tool called, not browse_web.

  3. Deploy and research

    terminal
    uv run bootcamp.py deploy
    uv run bootcamp.py invoke "Summarise https://aws.amazon.com/bedrock/agentcore/ in three bullets" --actor alice-chen
  4. Experiments

    • Compare browse_web with the weather agent's get_us_forecast (one purpose-built API call): which pages need a real browser (JavaScript-rendered content)?
    • Only http(s) URLs are accepted. What else would you restrict (domains, page size) and where: prompt, tool or policy?
    • Check the cost of a browsing question with uv run bootcamp.py llm.

    See it in the console: AgentCore → Observability: the trace of a research question: the research_agent span with its browse_web call inside.

Check your work

With the flag on, test --only 4 adds a browser tool check. It sends exactly this prompt:

terminal · the check's prompt, to try by hand
uv run bootcamp.py invoke "DataStream's network team needs the current AWS IP ranges version: use the browse_web tool (directly or through your research specialist) to open https://ip-ranges.amazonaws.com/ip-ranges.json and reply with the exact value of its syncToken field." --actor alice-chen

It passes when the answer contains the live syncToken of that file: a ~10-digit number such as 1791300000 that AWS changes several times a week, so the model can't know it without loading the page. The check fetches the file itself before and after the agent runs and accepts either value.

terminal
uv run bootcamp.py test --only 4
expected output (abridged)read only
[PASS] stage 4 ...
[PASS] stage 4 browser tool: The syncToken is 1791300000.

The check asks as a fresh actor in a fresh session and retries once on an empty answer. The prompt is framed as DataStream work so a Task 4 “decline off-topic requests” prompt keeps it; if your prompt still refuses it, loosen the scope rule.

This check depends on the model's wording, which varies from run to run. If it fails, rerun it once; check with uv run bootcamp.py traces --args which tool ran (the trace, not the wording, says whether the agent did the work); if the shipped agent does the same (git stash, deploy, rerun, git stash pop, deploy), it's the model, not your change.

Under the hood

Both tools use the AWS-managed built-ins aws.codeinterpreter.v1 and aws.browser.v1. Every call starts a fresh, isolated sandbox session and stops it afterwards, so nothing leaks between participants or requests. When a flag is on, your agent role gets only the session permissions it needs, in the inline policy awsworkshop-<you>-sandbox-tools; the platform sets CODE_INTERPRETER_ENABLED / BROWSER_ENABLED on the runtime, and optional_tools() plus prompt_hints() hand the tools to the orchestrator with a one-line usage hint each.

phase2/app/agent/sandbox_tools.py
def optional_tools() -> list:
    """The sandbox tools this runtime is configured for (empty when both capabilities are off)."""
    tools = []
    if capability_enabled("CODE_INTERPRETER_ENABLED"):
        tools.append(run_python)
    if capability_enabled("BROWSER_ENABLED"):
        tools.append(browse_web)
    return tools