ai.oz0/web
For: tenant administrators and plugin users.
ai.oz0/web provides web search and page retrieval through oz0_web. Both functions are read-only and open-world. Its oz0-web-research agent uses them to answer questions and cite the sources it read.
| Function | Arguments | Returns |
|---|---|---|
oz0_web.search | query, count (1 to 20, default 8), page | The backend's name and each result's title, URL and excerpt |
oz0_web.fetch | url, output (markdown, text or raw), max_chars (default 20 000), max_bytes (default 2 MB, at most 10 MB), and from 1.2.0 view, budget and query | The final URL, status, type, title and content, whether it was cut short, and with a budget the fit note |
Install
Its agent oz0-web-research asks for the tenant's default model alias with tools, never a vendor's model, so it runs on whichever provider the tenant set up first; map it elsewhere with oz0 model alias set default <provider>/<model>.
oz0 plugin install hub:ai.oz0/web
Ask the agent:
oz0 run oz0-web-research "What changed in Python 3.13's free-threaded build?"
It answers with Answer, Sources (each address it used) and Not reached (each address a fetch refused, with
the reason and the command that allows it). Or give your own agent the functions in its definition; from v1.0 its
plugin also lists them under uses: in its manifest (plugin manifest):
tools: [web/oz0_web.search, web/oz0_web.fetch] # web's functions, by their address
Choose a search backend
The backend is two settings of the tenant and, for a backend that takes one,
a key kept as an optional secret: the plugin starts without them, and search says what to set when none is.
| Name | Kind | Value |
|---|---|---|
WEB_SEARCH_BACKEND | setting | searxng or brave. Without it: searxng when only WEB_SEARCH_URL is set, brave when WEB_SEARCH_KEY is |
WEB_SEARCH_URL | setting | A SearXNG instance's address; for Brave, another API address than https://api.search.brave.com |
WEB_SEARCH_KEY | secret | Brave's subscription token (Brave Search API), or a bearer token for a SearXNG behind one |
oz0 setting set WEB_SEARCH_URL https://searx.internal.example
# or
oz0 setting set WEB_SEARCH_BACKEND brave
printf '%s' "$BRAVE_API_KEY" | oz0 secret set WEB_SEARCH_KEY
Settings are shown and never scanned for as secrets, so fetch reaches a page on the same address as the search
backend. Version 1.0.0 read the backend and its address as optional secrets; with 1.1.0 a tenant's old secrets of those
names become these settings by themselves, and stay secrets until you delete them (oz0 setting list notes which).
A SearXNG instance answers JSON only when search.formats in its settings.yml lists json. Changing a setting
restarts the plugin's server on the nodes; nothing else changes. The backend's address is the tenant's own setting, so
search does not check it against the egress allowlist; what it returns are links, and fetching them does.
What fetch reaches
- Only hosts on the egress allowlist: the tenant's (
oz0 tenant egress) and the plugin's, which names only Brave's API. A fresh tenant's list is empty, so allow the sites agents may read:oz0 tenant egress default '*.wikipedia.org' docs.python.org. - Never a private network by name. A name that resolves to a private, loopback, link-local or metadata address is
refused; only an address the tenant's list names is reached, such as an intranet server at
10.0.0.7:8080. The connection goes to the address that was checked, so a name cannot resolve differently in between. - Every redirect is checked again, up to five.
- robots.txt is read per site and kept for ten minutes; a path it disallows for
oz0-webis not fetched. As RFC 9309 says, a robots.txt that answers 4xx allows everything, and one that answers 5xx or not at all disallows everything. - Only text: HTML (converted to Markdown or text, without scripts, styles, navigation, headers, footers and hidden elements), plain text, Markdown, CSV, JSON and XML. Anything else is refused by its type.
Inside a sandbox (a job an outside event started) the tool server has no resolver: fetch matches the name against the lists and sends the request, by name, through the node's egress proxy, which applies the same lists and refuses the node's own, link-local and metadata addresses.
Every request asks the proxy for
public addresses only (the header Oz0-Egress-Public-Only), so there too a name that resolves to a private address is
refused, the tenant's names included, and only an address the tenant's list names is reached. Every refusal is a tool
error that names the reason, so the agent can say so.
Long pages and a budget
From version 1.2.0 fetch follows the convention of results that fit a budget:
with budget (tokens), view or query, it reads the whole page (up to max_bytes) and answers with the biggest
view of its Markdown that fits:
| View | What comes back |
|---|---|
full | The whole page as Markdown |
section | The part under the heading query names, with the headings below it, or the lines of a range such as 40-90 |
summary | The address and title, then each heading of the page with its first sentence |
stub | The address, title and size, and the page's headings; when nothing else fits, with how to ask for more |
The answer's fit field (and the result's _meta under oz0/fit) names the view, the budget, the page's size and the answer's in tokens, and what was left out. The runtime gives every call of an agent's model a budget when the model leaves it out, so an agent gets the biggest view that fits what is left of its context, at most 8,000 tokens. A page without headings (plain text, JSON) is cut into ranges of lines. max_chars, when given, is the bound the view is chosen within, so the fit note describes what came back.
A fitted answer holds at most 50,000 tokens and 200,000 characters whatever the budget, and at most 8 characters for each token of the budget, so a page made of white space is not mistaken for a short one: past a bound, truncated is set and the answer is a smaller view. A call without view, budget or query (a flow step, or the tool called by hand) answers as 1.1.0 did: the first max_chars characters.
{"url": "https://docs.example.com/guide", "budget": 2000, "query": "Install"}