You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 8, 2026. It is now read-only.
Repository navigation
Commit fb3136d
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/reference/specs/web-tools.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,7 +10,7 @@ Agents can read a web page the user links and search the web for current informa
10
10
1.**URL reading is provider-agnostic and in-process** (Option B): `web_fetch` fetches an http(s) URL and returns its text (HTML is stripped to readable text), running in the bot process via an injected `web` capability — no Executor, no workspace. A no-repo agent can use it.
11
11
2.**web_fetch is SSRF-hardened, connect-time**: only `http`/`https` schemes; literal internal addresses are refused synchronously — including every IPv6 form of an internal address (loopback, `10/8`, `172.16/12`, `192.168/16`, link-local `169.254/16` incl. cloud metadata, ULA, and IPv4-mapped/-compat/NAT64 IPv6 like `::ffff:169.254.169.254`, expanded fully rather than string-matched) — and internal hostnames (`localhost`, `*.local`, `*.internal`). For hostnames, the production fetch uses an undici dispatcher whose `lookup` resolves and refuses internal IPs **at connect time and connects to exactly that address** — so the validated IP is the connected IP (no DNS-rebinding TOCTOU). Redirect targets are re-validated on every hop (sync guard) and re-connect through the same dispatcher. Responses are size-capped (~1 MB) and time-bounded (~12 s); blocks/timeouts/HTTP errors return a clear message, never a crash.
12
12
- **Binary links reach the model as vision/document input (M1b)**: when the response `content-type` (parameters stripped, case-folded) is `image/jpeg|png|gif|webp` or `application/pdf`, `web_fetch` returns a parts list — a text header plus an `image` / `document` content part (base64) — instead of text, so the model *sees* the picture or reads the PDF. This is the second producer of image/document parts after Slack attachments ([slack-channel.md](slack-channel.md)), and the reason `tool_result.content` is `string | ToolResultPart[]` (`src/core/chatMessage.ts`). Per-file caps match the attachment path (5 MiB image, 10 MiB PDF); an over-cap binary is refused with a message (never truncated — a cut image is garbage), and other `image/*` types (e.g. SVG) are named as unsupported rather than sent to a model that would reject them. Text URLs are unchanged. The parts reach the model through the relay ([harness-pi.md](harness-pi.md) item 7): an image as pi's image block inside the tool result, a document as its descriptor (name, type, size), pi's own adapters carrying them to the provider. Run-visibility summaries render binary parts as `[image image/png, N bytes]` — the base64 never enters the stream.
13
-
3.**web_search is a seam with ≥2 implementations**: `WebSearch` has a real `BraveWebSearch` adapter (keyed by `BRAVE_SEARCH_API_KEY`) and a `NullWebSearch`. `makeWebCapability` selects Brave when the key is present, Null otherwise. With no key, `web_search` returns a clear "not configured" message (and notes URL reading still works) — it never breaks the [run](../vocabulary.md#run).
13
+
3. **web_search is a seam with ≥2 implementations**: `WebSearch` has a real `BraveWebSearch` adapter (keyed by `BRAVE_SEARCH_API_KEY`) and a `NullWebSearch`. `makeWebCapability` selects Brave when the key is present, Null otherwise. With no key, `web_search` returns a clear "not configured" message (and notes URL reading still works) — it never breaks the [run](../vocabulary.md#run). A failed Brave response keeps its observed HTTP status, validated `ErrorResponse` code and request ID, bounded rate-limit window values, and one redacted, capped detail line in the tool result. An unparseable body keeps status without inventing provider fields. A successful response with a null or missing web section yields no results; other invalid web shapes yield a decode failure. A transport failure has no HTTP status or remote message. Neither failure retries the request or exposes the search URL, key, raw body or headers. The adapter follows [Brave's error shape](https://api-dashboard.search.brave.com/api-reference/web/search/post) and [rate-limit headers](https://api-dashboard.search.brave.com/documentation/guides/rate-limiting).
14
14
4.**Enablement by toolset**: `web_fetch` (URL reading) is in the `full` (coding), `readonly` (review), `assistant` (general) and `explore` toolsets — broadly available; `web_search` is gated to the `web` toolset, held by the research-capable agent, and the `explore` toolset, whose long investigation reads the web beside the repository it runs ([agent-explore.md](agent-explore.md) item 2). `general` reads a linked URL but does not search ([agent-general.md](agent-general.md) item 1).
15
15
5.**A dedicated `research` agent** (`toolset: "web"`, `machine: "none"`) answers research/URL questions with search + fetch and no workspace; since [github-tools.md](github-tools.md) item 6 its toolset also holds the GitHub READ tools, so a question about one of our repos is answered from the repo over the App credential (a github.com URL of ours is read with `github_file`/`github_tree`, not `web_fetch`), and its prompt forbids concluding a repo is inaccessible from a public-web 404. `general` directs web-research asks to `agent:research`.
16
16
6.**The capability is injected, not global**: the dispatcher builds `web` from `process.env` per run; when absent (e.g., a run that didn't inject it), the tools report themselves unavailable rather than throwing.
@@ -34,6 +34,6 @@ Agents can read a web page the user links and search the web for current informa
34
34
| Live: `agent:research` describes an image link / summarizes a PDF link end-to-end |`[agent]` In Slack: `@switchboard agent:research what does https://<public>.png show?` → the answer describes the picture; `… summarize https://<public>.pdf` → the answer cites the PDF's content. |
| web_search formats seam results; missing key degrades gracefully |`[unit]``::web_search tool::formats results from the search seam`, `::degrades gracefully when search is not configured`, `::handles empty query and missing capability`, `::reports a generic search failure`|
37
-
| Brave adapter calls the API correctly and parses results; ≥2 impls selectable |`[unit]``::BraveWebSearch adapter::calls the Brave API with the key header and parses results`, `::throws on non-200`; `::makeWebCapability::selects Brave when a key is present, Null otherwise`, `::NullWebSearch throws WebSearchUnavailableError`|
37
+
| Brave adapter calls the API correctly and parses results; null web results are empty; a failed response keeps safe observed details and a failed transport stays status-free; ≥2 impls selectable |`[unit]``src/tools/web.test.ts::BraveWebSearch adapter::calls the Brave API with the key header and parses results`, `::treats a null Brave web section as no results`, `::throws on non-200`, `::keeps a Brave 429 code, request ID, rate limits and redacted detail on the tool result`, `::reports Brave transport failure without inventing an HTTP response or exposing the request URL`, `::uses the observed status and omits invalid Brave fields while capping the detail`, `::keeps HTTP status when Brave sends an unparseable error body`, `::reports a malformed successful Brave response as a decode failure`; `::makeWebCapability::selects Brave when a key is present, Null otherwise`, `::NullWebSearch throws WebSearchUnavailableError`|
38
38
| Enablement: web_fetch broad (incl. `assistant`), web_search gated to the `web` and `explore` toolsets; general holds `assistant` without web_search or bash |`[unit]``src/tools/web.test.ts::toolset + agent wiring::*`|
39
39
| Live: `agent:research` answers a question / summarizes a URL end-to-end |`[agent]` In Slack: `@switchboard agent:research <a question needing current sources>` → the answer cites sources found via `web_search`; `… summarize <public URL>` → the answer reflects that page's content. The `web_search` half requires `BRAVE_SEARCH_API_KEY` to be provisioned on the deployment; without it the run must still answer, saying search is not configured. |
0 commit comments