Skip to content

feat(bwopencode2): add --serve to run the web UI server in the sandbox - #41

Closed
Stuart B. Wilkins (stuwilkins) wants to merge 2 commits into
NSLS2:mainfrom
stuwilkins:add_bwopencode2_serve
Closed

Stuart B. Wilkins (stuwilkins) wants to merge 2 commits into
NSLS2:mainfrom
stuwilkins:add_bwopencode2_serve

Conversation

@stuwilkins

Copy link
Copy Markdown
Contributor

Summary

Add bwopencode2 --serve, which runs the OpenCode 2 web UI server inside the
sandbox so it can be opened from a browser over an ssh tunnel.

Description

serve, --server and pair stay refused: they reach a shared service that
runs tools outside any sandbox. --serve is the one supported way to run a
server, and it runs opencode2 serve inside the same sandbox, so tools stay
sandboxed.

  • One server per invocation, bound to 127.0.0.1 only.
  • The first free port of a fixed range, 24700-24709 by default
    (BWOPENCODE2_SERVE_PORTS=LO-HI changes it; --serve-port N picks one). The
    range is below the kernel's ephemeral range and clear of 49374, the shared
    service.
  • --serve-ssh-config prints an ssh_config block with one LocalForward per
    port, so a single alias covers every session. The start-up message prints the
    matching ssh -L command with the host from hostname -f.
  • The password is random for each session unless BWOPENCODE2_SERVE_PASSWORD
    is set (at least 12 characters). It reaches the server only as
    OPENCODE_PASSWORD, is never written to a file, and the host's own
    OPENCODE_PASSWORD is still dropped.
  • A port is claimed with a host-side file under
    ~/.local/state/bwopencode2/serve/ (not mounted in the sandbox, no secret),
    so two launchers cannot pick the same port. Claims of dead processes are
    removed.
  • --serve takes no opencode2 arguments and cannot be combined with --exec.

Open questions for reviewers:

  • A sandboxed agent can read its own environment, so it can read its server
    password. A random password limits that to its own session; a fixed
    BWOPENCODE2_SERVE_PASSWORD is shared by every session that uses it. Is the
    fixed-password option worth keeping?
  • The launcher ends in exec, so it cannot retry if another program takes the
    port just before the server binds it.

Documented in docs/bw-wrappers.md.

Testing

On babelfish, with OpenCode 2.2.6:

  • bash -n, shfmt, shellcheck and the other pre-commit hooks pass on the
    changed files.
  • --serve-ssh-config output, and --serve --dry-run (port choice, redacted
    password, the serve --hostname 127.0.0.1 --port P command).
  • The port logic in a scratch script: first free port, a second launcher
    skipping a claimed port, a busy port skipped, a stale claim reused, a busy
    requested port failing, an exhausted range failing, short passwords and bad
    ranges rejected.
  • A throwaway opencode serve with isolated directories, queried with curl: 401
    without the password and 200 with it, the web page served without a
    password, and POST /api/pair giving a one-time link that sets the cookie
    opencode_session_<port>.

Not tested, because it needs a browser or a full launch: a real
bwopencode2 --serve run, the password prompt in the web UI, two sessions open
in one browser, and whether the agent can see OPENCODE_PASSWORD in its
environment.

Checklist

  • Edit the sections above with relevant information
  • Update in-repo documentation, if applicable
  • Run pixi run -e dev lint locally (or wait for CI) — see .github/workflows/lint.yml
  • Test the changes on a representative managed host (or runansible deployment)

Runs opencode2 serve inside the sandbox on 127.0.0.1, so tools stay sandboxed, with one server per invocation on the first free port of a fixed range (24700-24709 by default) so one ssh_config block covers every session. The password is random for each session unless BWOPENCODE2_SERVE_PASSWORD is set, and reaches the server only as OPENCODE_PASSWORD. Ports are claimed with a host-side file so two launchers cannot pick the same one. --serve-ssh-config prints the matching ssh_config block.

Assisted-by: opencode:claude-sonnet-5-5
Copilot AI balanced review requested due to automatic review settings November 8, 2026 21:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Port-claim races can allow concurrent launchers to select the same port, while host-agnostic claims on shared NFS homes can incorrectly delete remote claims or reserve free local ports.

No open issues 🎉

What changed in this PR

Adds bwopencode2 --serve to run the OpenCode web UI inside the existing sandbox, while preserving restrictions on shared, unsandboxed server access.

Changes:

  • Adds loopback-only server startup with configurable port selection and host-side port claims.
  • Adds per-session passwords, optional fixed passwords, and SSH forwarding instructions.
  • Documents server usage, authentication trade-offs, limitations, and verification performed.
File Description
docs/​bw-wrappers.md Documents sandboxed web UI usage, port claims, passwords, and SSH tunnels.
bin/​bwopencode2 Implements serve options, port allocation, password handling, and sandbox launch integration.

Review effort: Balanced

The web UI opens on a page that asks for a pairing link or the password, and opencode pair points at the shared service, which the wrapper refuses. Inside the sandbox a small wrapper now starts the server, waits for it, asks it for a pairing code and prints the link, so the browser can sign in without typing the password. The password reaches curl on stdin, never on a command line. Use password still works after the link expires.

Assisted-by: opencode:claude-sonnet-5-5
Copilot AI balanced review requested due to automatic review settings November 8, 2026 23:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Loopback requests do not force proxy bypass, risking failed readiness checks and disclosure of the server’s Basic-auth password to an inherited HTTP proxy.

No open issues 🎉

Review effort: Balanced

@stuwilkins

Copy link
Copy Markdown
Contributor Author

Closing as now included in #37 with work by Thomas A Caswell (@tacaswell)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants