← Engineering blog

Put OpenCode’s whole workbench in the sandbox

A coding agent reads a file, edits it, and runs the tests. Put only its shell in a remote sandbox and those three steps can disagree about which file they mean. The test runner may be checking a perfectly good copy of the code the agent never edited.

Our first Mainbrella integration for OpenCode moves its coding server into the sandbox, so reading, editing, searching, and running commands share the guest checkout. The interface stays local. This uses the existing workspace adapter mechanism and Mainbrella’s existing command and protected-preview APIs, with no backend changes or new dependencies.

We’ve opened draft PR #53943. As of October 8, it’s an experimental proposal, not an upstream release, and live cloud validation is still outstanding. The interesting result so far is how much of the integration fits at an existing boundary—and how an expiring connection makes that boundary move.

One path should mean one file

OpenCode is an open source coding agent with a terminal interface, desktop app, and IDE integration. It can inspect a repository, change files, and run commands through tools. Its permission rules determine whether a tool operation is allowed, denied, or needs an answer from you.

Issue #48411 asks for more accessible permission controls, sandboxed execution, and an optional model to review approvals. We picked the execution part for a first PR. A remote working environment gives approved operations somewhere else to happen; it doesn’t decide whether to approve them.

Consider a hypothetical fix to src/parser.ts. If the editor changes the laptop’s checkout while a replacement bash tool runs npm test in Mainbrella, the agent sees a test result for another tree. Keeping two copies synchronized would make every file operation part of a replication protocol. The same path string doesn’t establish that the bytes are the same.

In the hypothetical shell-only design, read and edit point to the laptop checkout while test points to a separate guest checkout. In our design, the local interface connects to a guest OpenCode server, whose three tools point to one guest checkout.
The top placement is a tempting hypothetical shortcut. The bottom is the PR’s design. Following the arrows shows which checkout each tool actually touches.

OpenCode already has a useful separation: opencode serve runs the headless server, and a client can connect to it. In the source we built against, experimental workspace adapters can supply a remote server target. Our adapter joins that mechanism instead of inventing another remote version of each file tool.

The agent runtime, project tooling, and provider authentication belong to the guest server. That keeps the coding operations together. It also makes the security boundary easier to describe: those tools operate on the prepared guest, and the adapter never falls back to running them on the host when that guest is unavailable.

Borrow a machine; own a server

For this first version, you prepare an owned, running Node sandbox, install the same OpenCode build, check out your project, and configure its model credentials. Prefer Small or larger for the server and project tooling. The adapter requires opencode, tmux, and curl on the guest’s PATH. It does not copy your laptop’s files, SSH keys, provider credentials, or environment into the machine.

You supply both the container slot and its exact creation timestamp. A reused slot such as small is a name, not continuity: another machine can occupy it later. The adapter checks for the particular running generation you selected and includes that generation in its command and preview requests. A missing machine is an error to resolve explicitly.

Starting a long-running server through a short-lived command API presents another lifetime problem. The adapter submits a command with a 20-second timeout, but starts OpenCode in a detached tmux session. A dedicated socket and an ownership file associate that server with one OpenCode workspace. The launch checks its version and waits for a healthy endpoint; it refuses another workspace’s ownership marker or an occupied HTTP port.

Tmux detaches the process from the launch request. It doesn’t buy the machine more time. The sandbox still has its existing idle and hard lease deadlines, and a quiet event stream doesn’t by itself keep it alive. That distinction matters if you expect to leave a coding session open while you go to lunch.

Delete the OpenCode workspace: what happens?
ResourceAdapter’s responsibility
Its tmux serverStop it after checking ownership.
Its known preview grantRevoke it for the selected generation.
The prepared sandboxLeave it running. You control save and stop.
Unexported guest filesNo automatic snapshot. Save or export before the machine stops.

Leaving preparation explicit gives the PR a smaller promise to fulfill. We can ask upstream whether this workspace integration belongs before coupling it to image installation, repository transfer, or a new machine lifecycle. Those conveniences can follow an accepted boundary.

The server stays put; its address expires

Mainbrella’s protected preview supplies access to the guest server’s HTTP port. The returned URL carries a bearer token in its hostname: possession of the address grants access. The account API key is used to request that grant from the configured Mainbrella API origin; it is never sent to the guest server or preview.

The adapter requests a one-hour grant, clipped to the machine’s hard deadline. It checks the returned destination and server health, then keeps the connection receipt in private local state rather than public workspace metadata. On Unix, that directory is mode 0700 and its files are 0600. The adapter source contains those checks and the server launch script.

When the recorded grant expires, resolving the workspace target can request a new one. That means the same OpenCode workspace can acquire a different URL without acquiring a different server or filesystem.

A single guest server continues across the expiry of preview A. The reconnect resolves the target again and uses preview B. Both preview access and server lifetime end at the machine’s hard deadline.
A schematic renewal while the machine remains alive. Changing the access grant doesn’t extend the lease. An idle deadline can stop the machine earlier.

The existing event synchronizer resolved its target before entering its reconnect loop. Reconnecting diligently to an expired URL would never use the adapter’s renewed address. We moved target resolution into each connection attempt and added a regression test where the first stream closes and the adapter returns a different route. The next connection reaches the new event stream and synchronizes history there.

The URL’s role as a credential also changed what should cross the proxy or appear in logs. Forwarded workspace requests now drop the local server’s Authorization header, cookies, and auth_token query parameter. Adapter-supplied remote headers and a remote terminal’s ticket remain available. Transport error logs report status and error category without recording the credential-bearing destination or response body.

The preview gateway itself strips Authorization, so this guest server starts without OpenCode’s Basic authentication. Its preview grant is the ingress credential; exposing that port through some other route would need its own access control. The guest’s model credentials remain accessible to guest processes. This PR adds neither guest network filtering nor a reviewer model.

When issuing the address has an uncertain result

Renewal has a less pleasant case: the grant was created, but the response never reached us. Preview issuance currently has no idempotency key. Another POST can create another grant, and listing existing grants returns metadata rather than their secret URLs. The adapter can’t reconstruct the missing credential from that list.

Blindly retry the POST

A second grant can be issued while the first remains active and unrecorded. Successful retry hasn’t accounted for the earlier access.

Record uncertainty before issuing

The adapter writes a pending marker first. An ambiguous response leaves that marker in place and blocks another issuance until the uncertain grant is reconciled.

Recovery is manual in this draft: inspect and revoke the uncertain grant for that exact generation, then reset the pending marker as described in the adapter README. Failed cleanup retains the private receipt even if OpenCode removes the workspace row. That is an awkward user experience, but it preserves the evidence needed to clean up.

This is a concrete candidate for later backend work. A way to reconcile a particular issuance would let the adapter recover with less intervention. For the first PR, we kept the uncertainty visible rather than changing the provider contract to make the integration appear effortless.

What you can try today

The implementation is in our fork at this commit. Use that build on both sides; a stock release doesn’t include this adapter. If both development builds report version local, use the same source commit too. Once the guest project and provider authentication are prepared, supply the account key through your normal secret environment mechanism and set the public binding:

# MAINBRELLA_API_KEY is already set securely.
export OPENCODE_EXPERIMENTAL_WORKSPACES=true
export MAINBRELLA_CONTAINER_ID=small
export MAINBRELLA_CREATED_AT='2026-10-08T12:00:00.000Z'
export MAINBRELLA_DIRECTORY=/workspace/my-project
opencode

The slot and timestamp above are examples: replace them with the complete values returned for your running machine. Create a workspace and choose Mainbrella in the experimental picker. The setup guide covers optional ports, a local API origin, and recovery.

Our verification included 86 passing targeted tests across the adapter, plugin registration, workspace lifecycle and routing, HTTP proxy, and WebSocket suites. Package typechecking and the repository’s pre-push typechecks passed. The HTTP boundary tests use a local contract server; one runtime test launches the real OpenCode source server through real tmux, reads a file from a directory containing spaces and an apostrophe, and checks shutdown. That runtime test skips where tmux is unavailable; it ran here.

Those checks establish local behavior, not a completed hosted agent run. Live cloud transport validation is the next check. An externally revoked preview also isn’t automatically reminted before its recorded expiry, and the adapter neither supervises a crashed server nor replays model work into a replacement machine.

For a first experiment, change one harmless guest file and verify that the test runner sees that edit. Then delete the OpenCode workspace and verify that its server is gone while the prepared machine is still yours. Those two observations exercise the promise this PR is trying to make: one place for the work, and cleanup that knows what it owns.