Web and vision¶
Reaching beyond the machine: searching and reading the web, driving a browser,
and looking at images. These are the capabilities that bring in outside content,
which is also the main source of prompt injection. secure includes none of
them, and in the others every result passes through
injection screening when the profile has it.
| Capability | Tools | Spec block | Needs |
|---|---|---|---|
| Web | search_web, fetch_web_page |
web None (optional TAVILY_API_KEY) |
|
| Browser | navigate, click, type_text, snapshot, screenshot, … |
browser |
Playwright and Chromium |
| Visual | describe_screenshot |
visual |
a vision model |
Web¶
Search and page extraction, through Tavily or, with no key at all, DuckDuckGo.
search_web(query, max_results?, search_depth?, include_answer?, preferred_domains?)is open search over the public web.preferred_domainsnarrows one query to chosen sites. The model is told to search before guessing about current APIs, versions or errors.fetch_web_page(url, max_chars?, extract_depth?, content_format?)returns a page as markdown or text.
The two are deliberately unequal. Search returns short snippets, so it is open.
Extraction pulls whole pages into context, which is where injected text would
come in, so fetch_web_page only reads allowlisted domains:
web:
search_domains:
- docs.pydantic.dev
- github.com
Absent, a built-in list of documentation sites applies. The page is marked as untrusted data in what the model reads, and the model is told to treat it as a document to quote, never as instructions.
Backends¶
web:
search_backend: auto # auto | tavily | duckduckgo
auto uses Tavily when TAVILY_API_KEY is set and DuckDuckGo otherwise, so the
tools work out of the box. tavily and duckduckgo pin one provider.
search_depth, include_answer, extract_depth and content_format apply to
Tavily only.
With DuckDuckGo the page is fetched and converted on your machine rather than by a service, so the fetch is SSRF-protected: DNS is resolved up front and loopback, private, link-local and cloud-metadata addresses are refused even if the domain is on the allowlist, the resolved address is pinned for the request, and every redirect hop is re-checked against the allowlist. Size and time are capped, and binary content is skipped.
The Tavily key is read from the environment when a tool is called. A missing key or package produces a clear message instead of failing the run.
Browser¶
A real Chromium browser the agent can operate: for sites that need JavaScript, logins you've set up, or checking how a page looks. It's driven with Playwright, headless, and starts on the first browser call (a run that never browses pays nothing).
Mobile by default. The browser presents as an iPhone: a 390×844 touch
viewport, retina scale, and an iPhone user agent, so responsive sites serve
their mobile layout. set_viewport switches profile mid-run.
Tools:
| Navigate | navigate, go_back, go_forward, tabs, wait_for |
| Read | snapshot (the page's accessibility tree), get_text, screenshot, console_messages, network_requests |
| Act | click, type_text, press_key, select_option, hover, scroll, handle_next_dialog |
| Other | execute_js, set_viewport |
With a vision model configured, a screenshot doesn't reach the main model as
pixels: it's stored as a shot_… reference and described by the
Visual capability on request.
Where it can go¶
The browser is locked down by default, with two independent rules and deny wins:
allowed_domainsbounds which hosts it can reach, subdomains included. Navigation is checked, and so are the requests a page's scripts use to send data (fetch, XHR, WebSocket,sendBeacon). Passive assets (images, fonts, scripts) load freely, so an allowed page still renders. The default is loopback only (localhost,127.0.0.1,::1).block_private_addresses(defaulttrue) refuses loopback, LAN and link-local addresses in every frame, resolving names first so a hostname pointing at one is refused too. Cloud metadata (169.254.169.254) is refused always.
The two together mean the default browser reaches nothing: its allowlist
names loopback, and the private-address guard forbids loopback. To browse a
public site, list it. To browse a local development server, list it and set
block_private_addresses: false.
browser:
allowed_domains: [docs.example.com]
Limits¶
- Not a hard boundary. The egress rules don't stop DNS rebinding, because Chromium resolves a name again before it connects. List only hosts you trust an agent to read, and keep browser-enabled agents away from sensitive tools.
- Chromium runs with its own sandbox off. On Ubuntu 23.10 and later, an AppArmor rule stops unprivileged user namespaces, and Chromium's sandbox then fails at launch. paf disables it so the browser starts. That weakens isolation from the page, so enable user namespaces on the host if you can, and treat the browser as exposed to whatever it loads.
- Setup.
playwright install chromiumonce. If Chromium isn't there, the tool says so and the run continues. - No durable execution. A live page can't survive replay, so the browser can't be combined with Temporal-style durability.
Visual¶
Lets a text-only model work with images, by routing them to a vision model.
describe_screenshot(ref, prompt)asks the vision model a specific question ("is the padding right?", "what does the error dialog say?") aboutref: ashot_…from the browser, or animg_…id for an image you sent.
visual:
model: openrouter:xiaomi/mimo-v2.5
With a visual.model, images that reach the conversation (browser captures, or
images you send, with /image <path> [message] in paf chat or an upload in the
web UI) are swapped for short text stubs, so a text-only model never receives
bytes it can't read. The vision model is a contained side-call: image in, text
out, no tools and no conversation history. It is not a subagent.
Without a visual: block the capability does nothing and images pass to the
main model unchanged, which is right if that model can see.