Originally created by: imshaikot
Closes [#33].
OpenCode (opencode, Anomaly, MIT) as the eighth agent the side panel can run on, in beta.
OpenCode takes its whole config as JSON in OPENCODE_CONFIG_CONTENT, so the run's MCP server, its tool rules and the agent they belong to exist only for the one process. Nothing is written into the user's own config.
opencode run --format json --pure --agent browsentic-contained [--session <id>] [--model provider/model] [--variant <effort>] -- <instruction>
{"*": "deny", "browsentic_<tool>": "allow", …}, each browser tool allowed by name from the daemon's live list, plus webfetch/websearch on a research run. OpenCode applies the last matching rule and puts an agent's rules after the user's global ones, so every other tool — built-in, custom, another MCP server's, or one a later release adds — is denied, and a tool denied outright is never offered to the model at all. Measured: under a user config with "*": "allow" and bash: "allow", the model was offered the browser tools alone.--pure keeps plugins out (they run in-process and can answer permission prompts — local plugin files included, measured), and OPENCODE_DISABLE_PROJECT_CONFIG, OPENCODE_DISABLE_CLAUDE_CODE and OPENCODE_DISABLE_SHARE are set in the run's environment.vetPlan can now read containment out of an environment variable. A new envContains requirement, the fileContains twin, requires "*":"deny" and "share":"disabled" in the config, and a task's "mcp":{}. 14 new tampering and sealing cases in spawn.test.ts.OPENCODE_ only. OpenCode reads a dozen providers' keys, and keeping them all would undo sealing for it; opencode auth login keeps logins in a file instead.{env:…} and {file:…} anywhere in its config, and a { escape does not stop it — {file:/etc/hosts} came back inlined. The run's system prompt carries page text (focused element, fetched data, file reports), so a page could have read any file into it. The prompt is a file named in the config's instructions, which OpenCode reads verbatim; a test pins that no {file:/{env: reaches the config.browsentic, a user agent of that name with {"*": "allow", "bash": "allow"} kept its "*" first and put bash after our deny — a shell command ran. The agent is now browsentic-contained, and the reader fails AGENT_UNSAFE on any tool_use outside the browser that completed. That run is kept as the shell-ran fixture.opencode run waits for stdin to close and appends it to the message. drive.ts already spawns with it ignored; with an open pipe it hangs at init with no output, which looks exactly like a network stall.Defaults overridden for a browsing run: MCP calls time out at 60 s (now 30 min — an approval card waits on the user), tool results are cut at 50 KB into a file the model is told to Read (now 100 KB, Claude Code's 25k-token ceiling, and the prompt says the saved copy can't be opened), a title model call per new session (disabled — the panel makes its own), and every non-git folder shares one session list with the user (runs go into OPENCODE_DB under ~/.browsentic).
One-shots may read only their scratch folder. OpenCode matches a read against its path relative to the project root — / outside git, the repo when ~ is one — so it is allowed as seen from every ancestor, but never from the workspace itself, where tmp/* under / would be the machine's /tmp.
OpenCode Zen's free models answer only requests carrying OpenCode's own built-in tools: 403 FreeTierError — free tier can only be used from within OpenCode, recorded from the real endpoint as the free-tier fixture. Working around it would mean loosening containment, so instead check() reports needs setup (opencode auth login) until OpenCode has a login in auth.json, a Zen key, or a provider declared in its config (a local model needs no login), and that error gets its own message. The curated models are paid providers only.
Proven against the real opencode 1.18.32, through the runner's own plan and drive.ts with vetting and env sealing live, with the model provider replaced by a local OpenAI-compatible stand-in that records each request:
webfetch, a 401, an unknown model, scoped one-shot reads (/etc/hosts, ../ traversal and ~/.ssh refused);browsentic mcp server: OpenCode offered the model all 50 of its run tools with their schemas (page_injectCode/page_runCode stay hidden without Live tool, as elsewhere), and a call reached the daemon.Not proven: a real model driving a real page. The free models refuse (above), and no paid provider was signed in on the machine this was built on. The fixtures are real CLI output with scripted model words, as fixtures/README.md now says. Beta in the picker and the docs until one instruction and a follow-up turn have been through it — the same bar [#8], [#9] and [#26] were held to.
1618 tests pass (41 in opencode.test.ts, 14 new in spawn.test.ts beside the per-agent loops that pick OpenCode up automatically), coverage floors hold, yarn compile and yarn daemon:compile clean.
public/icons/opencode.svg first, or it is a broken image.browsentic stop run with a scratch BROWSENTIC_HOME whose daemon never bound a port (8765–8767 all taken) stopped the real daemon on 8767. stop should act only on the daemon its own lockfile names.🤖 Generated with Claude Code
Tickets: #26
Tickets: #33
Tickets: #36
Tickets: #38
Tickets: #8
Tickets: #9
Ticket changed by: imshaikot