Day 64: The Open Web, Closed Carefully
Web Search merged at 9:04 this morning — PR #127, the branch that has been in flight since Day 59. It's the second integration in Pinchy's connection system, after Odoo, and the first one where "what the agent can touch" isn't a nameable list of records. It's the entire public internet, subtracted back to what the admin is willing to allow.
Brave, Not Google
The search backend is Brave Search. The reasoning is boring: Brave has a clean paid API, no AI-result-rewriting layer, no consent-screen drama, a pricing model that doesn't require a sales call, and — this matters for our audience — no business reason to care about the query volume of a self-hosted AI deployment. Pinchy ships two tools out of the box: pinchy_web_search (Brave's web-results endpoint) and pinchy_web_fetch (a URL reader for the pages the search returns). Both live in the pinchy-web plugin.
The integration-connection flow is the simplified cousin of the Odoo wizard. No multi-step setup, no schema sync. One form: paste the API key, click Test, the server probes the Brave search endpoint, save the connection. If another admin tries to add a second Brave connection to the same workspace, the server returns a 409; Web Search is a singleton connection type, unlike Google where you legitimately want multiple mailboxes. The Add integration dialog greys the Brave option out once one is already present.
Agent-Level Permissions Without a "Trust the Internet" Toggle
The trickier half was the agent-level permission UI. The web is not a safe default target for an agent that also has access to customer data, so the permission section was designed assuming the admin will want to restrict it.
The first draft had two separate lists — Allowed domains and Excluded domains — which are technically correct and practically annoying. Every time you added a domain you had to decide whether it belonged on the yes-list or the no-list, and the combination rules weren't obvious from the UI. The redesigned version, which is what shipped, is one list with a per-chip Include/Exclude toggle. +example.com allows the whole domain; -blog.example.com carves a subtree back out. Empty list means unrestricted. That last part gets a banner saying so, so "nothing configured" can't be mistaken for "nothing allowed."
Advanced options sit in a collapsible group — freshness window, language, region — because most admins won't touch them. Language and region get searchable comboboxes backed by iso-639-1 (184 languages) and i18n-iso-countries (249 countries), because hardcoding a dropdown of "the 20 languages we thought you'd want" is the kind of decision that ages badly the first time a Bulgarian customer signs up.
One other thing the permission section does: if the agent has both web tools enabled and access to sensitive data (a file directory, an Odoo connection), the section shows a contextual warning. That combination is the classic data-exfiltration shape — sensitive input on one side, untrusted output channel on the other. The warning doesn't block anything. It just makes the admin check the box deliberately.
The SSRF Work
The pinchy_web_fetch tool turns any URL the agent names into an HTTP GET from the Pinchy host. That's a server-side request forger's dream if you're not careful. The guard rails, in order of how embarrassing their absence would have been:
Hostnames get resolved and checked against a deny-list — RFC 1918 private ranges, loopback, link-local, the AWS metadata address at 169.254.169.254, and a few other well-known traps — before the fetch begins. Hostnames are normalised (lowercased, trailing dot stripped) before comparison, so mixed-case and FQDN forms can't slip through. Redirects use redirect: "manual" instead of "follow", which means every hop gets re-validated through the same deny-list; the naïve version would validate public.com, follow its redirect to 169.254.169.254, and cheerfully return AWS metadata. Maximum five redirect hops.
The docs acknowledge one known limitation openly: DNS-rebinding TOCTOU. Between the moment we resolve a hostname and the moment we make the request, a sufficiently determined DNS server can flip what the name points at. The proper fix — resolve once, pin the IP, use it directly — is tracked as a separate issue. Documenting the gap is not a fix, but it's better than pretending it's closed.
The Refactor Nobody Will Notice
Before the web plugin could land, agents.pluginConfig had to stop being flat. The old shape was { allowed_paths: [...] } — a single plugin's config glued onto the agent row with no namespace. That worked when pinchy-files was the only plugin with agent-level config. The moment pinchy-web needed its own fields, the flat shape becomes ambiguous.
The migration restructures it into { "pinchy-files": {...}, "pinchy-web": {...} }. Old rows get rewritten in place. Every code path that reads or writes pluginConfig moves through the namespaced shape. It's not glamorous — the commit log literally has a line that says "refactor: namespace AgentPluginConfig by plugin ID" — but it's the kind of refactor that costs very little now and saves the same amount ten times over as more plugins get per-agent config.
Day 64
Web Search is the first integration that makes Pinchy useful for work that isn't "answer questions about data the company already has." It's also the first integration where the permission design mattered more than the API. Most admins won't touch the advanced options. That's fine. The ones who care about what a customer-support agent is allowed to fetch get a UI that takes their caring seriously.