Browser Use is stored as a capability flag rather than an entry in `tools[]`,
so the Tools row rendered nothing for it - an agent could have Browser Use
enabled and still look like it had no tools at all.
It now appears as a chip alongside the other tools. Clicking it reopens the
tools picker, which is the only place it can be turned back off, since there
is no per-tool config to show.
Running an agent against a real model surfaced two failures that unit tests
and typechecking could not.
Anthropic rejected the whole request with "tools.19.custom.input_schema.type:
Field required". Three browser tools (browser_scroll, browser_cookies,
browser_storage) are discriminated unions, which convert to a top-level
anyOf; providers require an object at the root and refuse anyOf/oneOf/allOf
there. Because the rejection is per-request rather than per-tool, one such
schema took the entire agent down. They are now flattened into a single
object - union of every branch's properties, requiring only what all branches
share, with the discriminator collapsed into an enum so no mode is hidden
from the model. The real zod schema still validates inside callTool, so a bad
combination returns a correctable error instead of breaking the conversation.
A tool whose schema cannot be expressed is dropped with a warning rather than
poisoning every request.
The suspension then reached the frontend but rendered nothing: the chat only
built interactive cards for approval-shaped payloads and chat_action, so the
run sat suspended with no way to resume. Adds a third interactive variant for
the connect card. The approval card was not reusable - it is hard-coded to
"Approval required" with Approve/Deny, which is the wrong question here.
One suspend payload now serves both surfaces: `components` is what a channel
turns into Block Kit, `type` and `setupUrl` are what the in-app chat matches
on. Neither minds the other's fields.
Verified in the preview against Claude Sonnet 4.5: the agent calls
browser_tab_open, the card renders with a working setup link, and clicking
"I've connected" resumes the run - which then reports no browser and explains
it instead of looping.
Lets an agent drive the chatting user's own browser. When the agent reaches
for the browser and none is connected, the tool suspends and shows a card
with a link to a public setup page; once the extension connects, the run
resumes and the agent can open tabs, navigate, click, type and read pages.
Reuses the instance-ai Browser Use stack wholesale - the @n8n/mcp-browser
toolkit, the published Chrome extension and the existing CDP relay - rather
than adding a second one. The agents module borrows
InstanceAiBrowserSessionService and degrades by simply not registering the
tools when that module is off.
Two constraints shaped the design:
- The agent runtime is cached and shared across every end user of a
published agent, so no session identity can be captured in the tool
closure. It is resolved per call from ctx.persistence.resourceId, the same
way the channel integration tools resolve their message context.
- The session key becomes a tmpdir() path segment inside a service that does
not sanitise it, and resourceId is attacker-influenced on channel runs, so
the key is hashed.
The setup page is deliberately unauthenticated: on a channel like Slack the
person being asked to connect may have no n8n account. The short-lived
setup token is the capability.
Excluded from the agent tool set: browser_connect/browser_disconnect (the
extension handshake owns session lifecycle) and the credential tools (they
resolve the session key against the user table).