Skip to content

feat(web): say when the agent can search, and mean it - #81

Merged
adityak74 merged 1 commit into
mainfrom
feat/web-search-indicator
Aug 22, 2026
Merged

feat(web): say when the agent can search, and mean it#81
adityak74 merged 1 commit into
mainfrom
feat/web-search-indicator

Conversation

@adityak74

Copy link
Copy Markdown
Contributor

What this is

A read-only indicator in the chat UI that says when the agent can search
the web, plus the two things that had to exist first for it to ever say
yes.

Before this, zorp-web depended on zorp-agent with no features, so
web_search could not register under the browser however the environment
was set up. An indicator reporting the state as it stood would have been
permanently off.

Three pieces:

  1. zorp-web gets an opt-in search feature
    (search = ["zorp-agent/search"]), not on by default. search is the
    only built-in that sends anything off the machine, it is opt-in on
    zorp-agent for that reason, and research deliberately does not pull
    it in either. Starting a local web UI should not acquire an egress path
    by side effect. Run cargo run -p zorp-web --features search when you
    want it.
  2. GET /api/capabilities answers
    {"web_search": {"available": bool, "detail": string}}.
  3. A pill in the topbar when the answer is yes, and nothing at all
    when it is no. The detail is its tooltip.

How availability is observed rather than guessed

Three separate things decide whether the tool exists, and the browser can
see none of them: the compile-time feature, the policy, and whether the
search provider found ZORP_TAVILY_API_KEY. The last of those can change
without a restart, so the question is answered per request rather than
cached at startup.

The answer is not a hand-written copy of those conditions:

  • Registration and the availability query share one function.
    register_builtins_filtered now calls web_search_tool(), and
    web_search_availability() calls the same function and throws the tool
    away. They cannot drift.
  • The policy is read by calling the real Policy::decide, and the policy
    it reads is the one a turn uses: turn::policy(own_port) was factored
    out of run_agent so the endpoint and a real turn ask the same one.
  • The test that matters, in zorp-agent/src/agent.rs, asserts the
    reported answer against tool_names(), which is registration itself,
    and against the real Policy rather than a copy of its reasoning. It
    passes in a build with the feature and in one without.

A hand-written copy would have been right the day it was written and
silently wrong afterwards, and the failure mode is the worst one
available here: a page saying nothing leaves this machine while something
does.

What it does not claim. That the tool is registered, not that
searching will work. Whether Tavily accepts the key is only knowable by
spending a search, and this question gets asked on page loads. The tests
say so out loud.

Why a separate route, not a field on /api/settings

Settings are things a person chose and can PUT back. Nothing here is
choosable from a browser: it is a fact about the build, the code, and the
server's environment. Putting it beside the settings would invite an
attempt to set it. The pill is read-only for the same reason, and it is a
div rather than a button so nothing invites clicking it. There is a
test asserting that.

The route sits behind the same token gate as everything else, with a test
for it: it reports what the build has and what the environment holds,
which is not for an unauthenticated caller when the server is reachable
off this machine.

Tests added

Rust, zorp-web/tests/capabilities.rs:

  • without_the_search_feature_web_search_is_unavailable (default build,
    and the reason names the feature)
  • with_the_feature_but_no_key_web_search_is_unavailable (the reason
    names ZORP_TAVILY_API_KEY)
  • with_the_feature_and_a_key_web_search_is_available
  • capabilities_is_behind_the_token_gate

Rust, zorp-agent/src/agent.rs:

  • web_search_availability_agrees_with_the_gates_it_reports_on

TypeScript, web/test/search-indicator.test.ts, 8 cases: nothing drawn
with no answer, nothing drawn when unavailable, drawn with the label and
the detail when available, taken back down when availability goes away, a
hostile detail cannot become markup, the [hidden] stylesheet rule exists
(the display: inline-flex trap the context meter documents), the markup
is in index.html and starts hidden, and the indicator is not a control.

Written failing first in both languages, and watched fail: the Rust one
on a 404 from the missing route, the TypeScript one on the missing module.

Commands run

Command Result
cargo build --workspace ok
cargo test --workspace ok, no failures (zorp-web: 75 unit, capabilities 2, and every other suite green)
cargo test -p zorp-web --features search ok, capabilities 3 passed
cargo test -p zorp-agent --features search ok, 480 passed
cargo test -p zorp-agent --features research ok, no failures
cargo clippy --workspace --exclude zorp-track --all-targets --locked clean
cargo clippy -p zorp-web --features search --all-targets --locked clean
cargo fmt --all --check clean
web/: npm run check clean
web/: npm test 157 passed, 0 failed
web/: npm run build ok

Also checked by hand, on spare ports, against a temporary config file:

  • --features search with a key set: the endpoint answers
    available: true, and the page draws the pill with its tooltip.
  • default build: the endpoint answers available: false naming the
    missing feature, and the topbar has no pill at all.

Also in here

docs/DECISIONS.md gets an entry (2026-08-21), and the zorp-search
bullet in CLAUDE.md and AGENTS.md gains the browser half. README.md
and web/README.md document the feature and the route.

https://claude.ai/code/session_01KGPVQ8wUG7h36zashWYCp4

The browser had no way to know whether `web_search` was there, and it
never was: `zorp-web` depended on `zorp-agent` with no features, so the
tool could not register under the browser however the environment was
set up. An indicator reporting the state as it stood would have been
permanently off.

So three pieces. `zorp-web` gets its own opt-in `search` feature, off by
default for the reason it is off on `zorp-agent`: starting a local web UI
should not acquire an egress path by side effect. `GET /api/capabilities`
reports whether the tool is really there. And the chat UI draws a small
pill in the topbar when the answer is yes, and nothing at all when it is
no.

The answer is observed rather than re-derived. Registration and the
availability query share one function in `zorp-agent`, the policy is read
from the real `Policy` rather than from a copy of its reasoning, and the
test that matters asserts the reported answer against `tool_names()`,
which is registration itself. A hand-written copy of the three conditions
would have been right the day it was written and silently wrong after
that, and the failure mode is the worst one available here: a page saying
nothing leaves this machine while something does.

What it does not claim is that searching will work. Whether Tavily
accepts the key is only knowable by spending a search, and this question
gets asked on page loads.

A separate route rather than another field on `/api/settings`, which is a
PUT-able resource of things a person chose. Nothing here is choosable
from a browser. The pill is a report and not a switch for the same
reason.

Claude-Session: https://claude.ai/code/session_01KGPVQ8wUG7h36zashWYCp4
@adityak74
adityak74 merged commit 99b1311 into main Aug 22, 2026
7 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