Skip to content

Authenticate plugin-owned OpenCode servers; support secured external servers (#27)#35

Merged
TheRealDinghyDog merged 1 commit into
fix/issue-26-write-permissionsfrom
fix/issue-27-server-auth
Jul 10, 2026
Merged

Authenticate plugin-owned OpenCode servers; support secured external servers (#27)#35
TheRealDinghyDog merged 1 commit into
fix/issue-26-write-permissionsfrom
fix/issue-27-server-auth

Conversation

@TheRealDinghyDog

Copy link
Copy Markdown
Owner

Fixes #27 (review finding H-02).

Note

Stacked on #34 (base branch fix/issue-26-write-permissions) because both touch the same files; GitHub will retarget this PR to opencode-conversion automatically when #34 merges.

Problem

opencode serve was spawned without OPENCODE_SERVER_PASSWORD, so any local process that could reach the loopback port had full API access (file content, sessions, lifecycle) — the server itself warns about this. And the client sent no Authorization header anywhere, so a user who had secured their server got a plugin that could no longer connect.

Fix

  • Owned servers: every spawn generates a per-server random password (crypto.randomBytes(24), base64url), passed only via the child environment (never argv, which is visible in the process list), with OPENCODE_SERVER_USERNAME pinned to opencode so an ambient value cannot desynchronize server and client. The credential is recorded in server.json, now written owner-only (0600).
  • Client: OpencodeServerClient sends HTTP Basic auth — matching OpenCode's own convention (username defaults to opencode) — on all four paths: normal requests, fresh-connection requests, health checks, and the SSE event stream. Verified live on 1.17.15 that every route, including /global/health and /event, requires the header once a password is set.
  • External servers (OPENCODE_COMPANION_SERVER_URL): credentials come from the same OPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAME variables the user already exports for a secured server. A 401 now yields a precise diagnostic (missing vs. rejected credentials) instead of a generic "not healthy".
  • Cancel/teardown: later processes re-derive credentials from the persisted server session or the ambient variables; teardown authenticates its dispose call, with a comment documenting that dispose only cleans instance state — PID termination remains the actual shutdown (observed live).
  • Redaction: the password lives only in the child env and the 0600 server.json; status output and logs never contain it (asserted in tests).

Tests

  • The fake fixture now enforces Basic auth on every route whenever a password is in its environment — so the entire existing suite runs against a password-protected server and would fail if any client path lost its header.
  • New: unauthenticated HTTP and SSE requests are rejected (401) while authenticated ones work; server.json is 0600 and holds the generated credential; status --json output never contains the password; external-server matrix (no credentials → precise error, wrong password → precise error, correct password → connects, health check carries the header); buildBasicAuthHeader unit tests.
  • npm test: 85 passed, 0 failed, 0 skipped.
  • Live smoke test against real OpenCode 1.17.15: owned server starts password-protected (no "unsecured" warning in its log), unauthenticated health → 401, authenticated → 200, server.json mode 600, password absent from logs, teardown actually stops the listener.

🤖 Generated with Claude Code

…servers (#27)

Plugin-owned `opencode serve` processes ran unsecured: any local process
that could reach the loopback port had full API access (file content,
sessions, lifecycle). Conversely, a user who exported
OPENCODE_SERVER_PASSWORD got a server the plugin's header-less client
could no longer talk to.

Every owned server now gets a per-server random password, passed only via
the child environment (never argv) with the username pinned, and recorded
in server.json written owner-only (0600). The client sends HTTP Basic
auth — matching OpenCode's own convention, username defaulting to
"opencode" — on all four paths: normal requests, fresh-connection
requests, health checks, and the SSE event stream. On 1.17.15 every
route including /global/health and /event requires the header, verified
live.

External servers configured via OPENCODE_COMPANION_SERVER_URL read the
same OPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAME variables the
user already exports for a secured server; an unauthenticated 401 now
produces a precise diagnostic instead of a generic health failure.
Cancel processes re-derive credentials from the persisted server session
or the ambient variables, and teardown authenticates its dispose call
(dispose only cleans instance state — the PID termination remains the
actual shutdown).

The fake fixture now enforces Basic auth on every route whenever a
password is present in its environment, so the whole existing suite
exercises authenticated paths; new tests cover unauthenticated HTTP/SSE
rejection, owner-only server.json, credential redaction in status
output, and password-protected external servers (missing, wrong, and
correct credentials).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@TheRealDinghyDog
TheRealDinghyDog merged commit 2940cde into fix/issue-26-write-permissions Jul 10, 2026
1 check passed
TheRealDinghyDog added a commit that referenced this pull request Jul 10, 2026
PR #35 was accidentally merged into its stacked base branch
fix/issue-26-write-permissions instead of opencode-conversion; this
brings the already-reviewed content onto the integration branch.
@TheRealDinghyDog
TheRealDinghyDog deleted the fix/issue-27-server-auth branch July 11, 2026 00:55
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.

1 participant