Repository navigation
feat: full OpenCode v2 support with enforced in-sandbox server - #11
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
PR Type
What is the current behavior?
Issue Number: N/A
opencodeboxfails immediately whenopencodeis installed by mise's npm backend (aube):command -v opencoderesolves to~/.local/share/mise/installs/opencode/latest/node_modules/.bin/opencode, a shim that execs its target relative to its own location. The launcher mounts onlydirname(opencode)(the.bindirectory) at/opt/opencode, so inside the sandbox the shim's..becomes/optand the.misestore is missing.Two more pre-existing failures surfaced during verification:
~/.config/opencode; even with those directories present, every start (OpenCode 1 and 2) dies withEROFS: read-only file system, mkdir ~/.cache/opencode/bin.opencode --versionalready printsopencode v<version>, soopencodebox --versionprintedopencode vopencode v<version>.Without this change, a sandboxed v2 client with a live host service attaches to it over loopback, so tool execution runs outside the sandbox; with no host service it spawns 'serve --service' inside the sandbox but writes a stale registration (in-sandbox pid) into the real '~/.local/state/opencode/service.json'.
What is the new behavior?
npm-style layouts. When
opencodelives in a<...>/node_modules/.bindirectory (npm, mise/aube, bun, pnpm shims), the sandbox mounts thatnode_modulesdirectory read-only and execs the shim at/opt/opencode/.bin/opencode, so its relative target resolves. The relocation is gated on the parent beingnode_modulesand on the sensitive-path guard, and the path is normalised lexically first so.or..inPATHcannot change what gets mounted. Other.binlayouts (for example$HOME/.bin) keep the old mount, which binds only the shim's own directory, and print a warning.Every other layout — a single binary, the mise
shimsdirectory — keeps the previous mount (--bindof the binary's directory) and exec target (/opt/opencode/opencode); the only payload change is quoting around that path.Fresh hosts and version output. The launcher now creates opencode's config, agents, cache (including the helper
bindirectory), data and state directories when they are missing, so a fresh host no longer fails at the bind step (OpenCode 1 may still need its config seeded on the host, since~/.config/opencodeis mounted read-only).opencodebox --versionprints OpenCode 2's self-prefixed output correctly while keeping OpenCode 1's format unchanged.Enforced in-sandbox server (OpenCode 2 default). Every command now gets a private server inside the bwrap sandbox: the launcher bind-mounts a generated service config (
disabled: true) over '/service.json', 'service-local.json', 'service-prod.json' and any existing 'service-*.json' on every run, so the v2 client can never attach to - or register - a host background service; no host service registration is ever written. The host service registration in the state directory is masked too, so nothing in the sandbox can learn the host server address or password. Commands that inherently need the host service are refused before sandboxing, each error naming the alternative: 'pair', 'service *', 'serve --service'/'--stdio', '--server', 'upgrade', 'update', 'uninstall'. Sessions, credentials, and TUI state stay shared via the read-write data and state binds (the config directory is shared read-only); web UI via 'opencodebox serve --port N' plus the host browser with the printed password.Other information
Documented in the README (
### OpenCode Launcher Mount):opencode upgradewrites to the install tree, andopencode plugin addupdates the global configuration under~/.config/opencode(mounted read-only) — do both on the host instead (e.g.mise upgrade opencode).node_modules/.binshims are relocated. Globalbinshims fromnpm|pnpm|bun install -gand symlinks whose target lies outside the mounted roots still fail withnot found(exit 127).{"disabled": true}service config so the client can never attach to a host service — and masks the host service registration in the state directory, so nothing in the sandbox can read the host server URL/password — while sessions, credentials, and TUI state stay shared (the config directory is shared read-only). Commands needing the host service (pair,service,serve --service/--stdio,--server,upgrade,update,uninstall) are refused; each error names the alternative.Known residual: the OpenCode state directory is mounted read-write so locks and TUI state stay shared; a sandboxed process can still create files there, and the host client reads some of them (including the pty runtime registration). See the README's Security Restrictions section.
Tests: 62 passing (
uv run --frozen --group dev pytest -q), covering shim relocation, fallback/warning, lexical normalisation, fresh-host directory creation, OpenCode 1/2 version formatting, v2 isolation refusals, overlay binds (including prod-channel coverage), config seeding, config-dir precedence, the symlink-safe seeding, the symlink-aware config-dir guard, the state-registration masking, and the dangling-symlink refusal, plus a guard that--helplists every refused command.