Chat
webrain mcp over stdio, so the model gets
the same 18 webrain_* tools an MCP client would, named webrain-webrain_*.
Nothing is reimplemented for chat — the child inherits your CDP_URL, vault and
engine cache, so it drives whatever browser the CLI would.
Requirements
- A release binary from this repository (
webrain-linux-x86_64,webrain-macos-aarch64,webrain-windows-x86_64.exe, …). Thechatfeature is built into every published target, the two static musl builds included — those vendor OpenSSL 3 and compile it withmusl-gcc, so the result stays a static binary. - Building from source needs Rust 1.94+ and
--features chat:
What each build target carries
CI compiles
pdfium,chat on Linux for every PR, and on Linux, macOS, Windows
and static musl for every push to main — the four shapes a release tag would
otherwise be the first to try.
The musl rows are the reason this table exists at all. chat reaches
native-tls through the SDK’s network-forwarding, which is openssl-sys on
Linux; a static target has no OpenSSL to link, so webrain-cli declares
openssl with the vendored feature for target_env = "musl" only and the
build compiles OpenSSL 3 from source with musl-gcc. The SDK itself supports the
platform: its build script fetches copilot-linuxmusl-x64, not a glibc
package. Building it needs musl-tools, perl, make, pkg-config and
libssl-dev (the last one for the build-host copy of openssl-sys), and about
8 GB of RAM to compile the SDK crate — build with -j 1 if your machine is
tighter than that.
The container image is glibc (Debian) and takes features as a build arg:
Quick start
Provider resolution
First match wins:--provider,--model,--base-url,--key-from.WEBRAIN_CHAT_PROVIDER/_MODEL/_BASE_URL/_API_KEY.- The ambient key chain, in this order:
--provider accepts any alias stem: openrouter, openai, anthropic,
claude, gemini, google, fireworks, dashscope, qwen, groq,
deepseek. A named provider uses only its own variables — naming groq
while OPENAI_API_KEY is set is an error, not a silent mix-up.
A local server needs no key
Any OpenAI-compatible endpoint works. Point the base url at it and a key is optional:Flags
What it cost
The turn ends with three lines on stderr, so the answer on stdout stays pipeable:session.usage.getMetrics), not from counting characters: input_tokens /
output_tokens per model, request count, and the time actually spent in model
API calls — which is what the tok/s figure divides by. --json carries the
same numbers as usage: {input_tokens, output_tokens, requests, api_ms, tokens_per_sec, nano_aiu}.
Two honest limits:
- Cost is
n/aon a BYOK turn.nano_aiuis GitHub’s billing unit and the runtime reports 0 because your key pays the provider, not GitHub. Piping through OpenRouter removes even that (OpenRouter meters its own credits), so the AIU line only means something with GitHub-authenticated models. - Tokens are per model call, not per tool call. A tool call consumes no
tokens by itself; each agent step that follows one does. That is why the live
line reads
· model ·next to the· tool ·lines rather than on them.
--search: live web results, per provider
The vendors do not sell the same thing, so the flag does whatever the provider
actually supports instead of pretending they are interchangeable:
Why the split: this CLI cannot inject a request field into the model call — the
Copilot SDK forwards a fixed provider config, not arbitrary body parameters — so
the search it can switch on is one the vendor exposes as a model variant
(OpenRouter) or as an endpoint it can call itself (DeepSeek, OpenAI, Anthropic,
Gemini, and ours). The vendor row means the vendor’s model plans the queries and
writes the grounded answer; the fourth column is whose index found the pages.
Two things that follow from that shape:
- The vendor call is a second request, billed by the vendor. OpenAI, Anthropic and Google charge per search on top of the turn (Gemini 3+ bills per query the model decides to run, with 5,000 free per month). Their fees do not appear in webrain’s closing cost block — that block reports the runtime’s token totals, which only cover the turn.
- A vendor search that fails falls back to ours, never to a dead turn. A rejected key, a model that cannot use the tool, or an empty answer prints the reason and grounds the turn on webrain’s own results instead.
--search adds one search per turn. The agent can also call webrain_search /
webrain_serp itself, mid-turn, as many times as the question needs — that is
what it does without the flag.
In a browser
webrain_* call, a token meter in the header, and a modal for
every permission request. The page is compiled into the binary (include_str!),
so there is no build step and no second process, and it is the only UI that
opens — nothing is fetched from a CDN.
Four routes, all on loopback:
The event names are the runtime’s own (
assistant.message_delta,
tool.execution_start, session.usage_info, model.call_failure, …) rather
than a translated dialect: the page maps each one to an element, and a name it
does not know is dropped instead of breaking the stream. One event is ours:
webrain.permission, carrying the request id the modal answers.
Two things that matter more than the layout:
- Nothing is invented. If the runtime did not emit it, the pane stays empty.
The composer re-enables on
assistant.idle— the only signal a turn is over. - The modal is the security boundary. Approving in the page is the same
authority as
--yes, so it shows the exact request (the type of action, the command or url) and waits. No answer within 120 s is a deny, and the turn says so rather than hanging.--servedoes not imply--yes.
--serve is loopback-only on purpose: a listener that can approve a shell
command must not be reachable from the LAN. Put a reverse proxy in front of it
and add authentication if you need it elsewhere — the binary will not do that
for you.
Permissions
Without--yes the session approves reads, navigation and MCP tool calls, and
denies shell commands, file writes, and any permission category the SDK does
not model (an unrecognised category is not an invitation). --yes installs an
approve-all handler — say it out loud when you mean it, because the agent can
then drive a logged-in browser.
The key is used in-process to configure the runtime and is never written to
stdout, stderr, or a log. --print-config, --json and every error path print
<redacted> plus the name of the variable that supplied it.
Exit codes
When a turn fails with no answer
The SDK reportsNo response was returned. Send your message again to retry.
whenever the provider produced nothing usable, which can also mean the model
does not support tool calling. See what the runtime actually emitted:
--provider openai) rather than
retrying.
”The prompt was rejected as too long”
That message is the provider refusing the request, not a webrain limit, and both real causes are quotas — neither is the model’s context window:- Tokens per minute. A tool-enabled turn is ~8.4k tokens before your
question: 18 tool schemas (~8.0k) plus a short system prompt (~0.3k). Groq’s
free tier allows 7000 input tokens/minute, so it rejects that outright
(
413,Requested 18102 ... Limit 7000once retries are counted). Use a paid tier or a provider with room. - Output budget against balance. The runtime asks for a 64k-token
completion by default, and some providers reserve the requested maximum
against your balance: OpenRouter answered “you requested up to 64000 tokens,
but can only afford 171”. webrain therefore asks for 4096 by default and
--max-output-tokensonly lowers it further.
WEBRAIN_CHAT_DEBUG example
above).
Isolation from your own Copilot install
A BYOK chat owns its configuration. The client starts in the SDK’sClientMode::Empty — the mode Microsoft documents for “CLI-like ambient
behaviour is unsafe” — which requires an explicit state directory instead of the
implicit ~/.copilot fallback, disables optional features unless asked for, and
keeps the OS keychain out of the runtime’s reach.
So none of your own Copilot setup applies: no ambient MCP servers, no user
skills, no instructions file, no cross-session store, no memory writes, no
session telemetry. This is not politeness — measured on a real turn, inheriting
the machine’s config loaded ~20 unrelated MCP servers (~480 tools,
toolDefinitionsTokens: 137019 against a tokenLimit: 128000) and the turn died
before it reached the provider.
Point the state directory elsewhere with WEBRAIN_CHAT_HOME (default:
webrain-copilot-home under the system temp directory).
What is not removable: the runtime’s built-in github-mcp-server (5 tools)
still loads, and the tool list is filtered separately with the SDK’s ToolSet
so the model can only call webrain-*. Measured, toolDefinitionsTokens stays
8080 either way — the 18 webrain_* schemas are the floor, not the
stragglers.
What chat does not do
No interactive REPL and no--session resume in this version: one prompt, one
turn, one exit code. Everything here is additive to webrain mcp, which keeps
its stdout contract — chat’s own logs go to stderr for that reason.
No GUI either. The runtime already emits everything a conversation window needs
(streamed deltas, tool calls, permission requests, context and cost meters) and
the CLI flattens all of it into stderr lines; ADR-004
maps each event to the element it should become and specifies webrain chat --serve as the smallest interface that keeps the binary self-contained —
including which SDK handler slots a UI has to fill, because today they are
policy, not a person.
