Skip to content

feat: full OpenCode v2 support with enforced in-sandbox server - #11

Merged
fityannugroho merged 17 commits into
mainfrom
fix/shim-launcher-relative-path
Oct 20, 2026
Merged

fityannugroho merged 17 commits into
mainfrom
fix/shim-launcher-relative-path

Conversation

@fityannugroho

@fityannugroho fityannugroho commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

PR Checklist

  • I have read the documentation.
  • I have read and followed the Contributing Guidelines.
  • I have included a pull request description of my changes.
  • I have included the necessary changes to the documentation.
  • I have added tests to cover my changes.

PR Type

  • Bug fix
  • Feature

What is the current behavior?

Issue Number: N/A

opencodebox fails immediately when opencode is installed by mise's npm backend (aube):

/opt/opencode/opencode: 15: exec: /opt/opencode/../.mise/@opencode+cli@2.0.32/node_modules/@opencode/cli/bin/opencode.exe: not found

command -v opencode resolves 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 only dirname(opencode) (the .bin directory) at /opt/opencode, so inside the sandbox the shim's .. becomes /opt and the .mise store is missing.

Two more pre-existing failures surfaced during verification:

  • A fresh host (opencode never run there) fails before exec because bwrap cannot bind the missing ~/.config/opencode; even with those directories present, every start (OpenCode 1 and 2) dies with EROFS: read-only file system, mkdir ~/.cache/opencode/bin.
  • OpenCode 2's opencode --version already prints opencode v<version>, so opencodebox --version printed opencode 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 opencode lives in a <...>/node_modules/.bin directory (npm, mise/aube, bun, pnpm shims), the sandbox mounts that node_modules directory read-only and execs the shim at /opt/opencode/.bin/opencode, so its relative target resolves. The relocation is gated on the parent being node_modules and on the sensitive-path guard, and the path is normalised lexically first so . or .. in PATH cannot change what gets mounted. Other .bin layouts (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 shims directory — keeps the previous mount (--bind of 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 bin directory), 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/opencode is mounted read-only). opencodebox --version prints 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):

  • Self-upgrading or installing plugins from inside the sandbox is not supported for the read-only layout: opencode upgrade writes to the install tree, and opencode plugin add updates the global configuration under ~/.config/opencode (mounted read-only) — do both on the host instead (e.g. mise upgrade opencode).
  • Only node_modules/.bin shims are relocated. Global bin shims from npm|pnpm|bun install -g and symlinks whose target lies outside the mounted roots still fail with not found (exit 127).
  • OpenCode 2 runs fully isolated: every command gets a private in-sandbox server — the launcher overlays a generated {"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 --help lists every refused command.

@fityannugroho fityannugroho added the bug Something isn't working label Oct 18, 2026
@fityannugroho fityannugroho changed the title fix: keep npm-style opencode shim layout inside sandbox fix: support npm-style opencode v2 installs and fresh hosts Oct 18, 2026
@fityannugroho fityannugroho changed the title fix: support npm-style opencode v2 installs and fresh hosts feat: full OpenCode v2 support with enforced in-sandbox server Oct 19, 2026
@fityannugroho fityannugroho added the enhancement New feature or request label Oct 20, 2026
@fityannugroho
fityannugroho merged commit 98f9193 into main Oct 20, 2026
2 checks passed
@fityannugroho
fityannugroho deleted the fix/shim-launcher-relative-path branch October 20, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant