Skip to content

feat(mirrorhost): opencode sessions can use the preview tools - #591

Merged
ZacxDev merged 3 commits into
mainfrom
feat/preview-mcp-opencode
Nov 11, 2026
Merged

ZacxDev merged 3 commits into
mainfrom
feat/preview-mcp-opencode

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Why

Mirrored opencode sessions could not use the preview tools. opencode resolves a remote MCP server's headers once, in its one shared background service. Pane headers would therefore name the service's pane for every opencode session, and every preview would land in the wrong window. The only per-session signal opencode sends is params._meta["ai.opencode/sessionID"] on every tools/call (packages/core/src/mcp/client.ts, read at the pinned opencode revision).

What

/preview-mcp only. /mcp (muster's tools) does not change.

  • A request carrying the static header X-Muster-Client: opencode is an opencode caller. It skips the tmux-server check, since the service runs in no pane. The secret, Origin and POST checks still apply.
  • initialize and tools/list need no session. They return the instructions and the three tools as for any caller.
  • A tools/call is bound to the one window whose active opencode pane shows that session, or the session's nearest ancestor (a subagent's child session calls with its own id). The ancestors come from opencode's session list (OpenCodeSource.Ancestors). The binding must be one muster also sends on: the launcher's record, or a verified title binding.
  • These cases fail closed with no_window: no session id, an id no window shows, an id shown only through an unverified title match, and an id shown in two windows.
  • Pane headers on an opencode-marked request are ignored. They could only be the service's own.
  • A request without the marker resolves by pane exactly as before. If it also carries a _meta session id, the pane still wins.

Security: the session id selects a window. It does not authenticate. It is believed only among this host's mirrored windows, behind the same per-host secret, so a forged id can at most bind a preview to another of the same owner's windows on this host. A Claude Code caller can already do that by naming another pane. An unknown id binds nothing.

Docs: docs/attention.md (new "opencode sessions" subsection with the config entry), docs/previews.md setup step 6, and the opencode non-goal in docs/design/previews.md (now an as-built note).

opencode MCP entry (opencode config, under mcp)

"preview": {
  "type": "remote",
  "url": "http://127.0.0.1:47823/preview-mcp",
  "oauth": false,
  "headers": {
    "Authorization": "Bearer {file:~/.config/muster-mirror/mcp-secret}",
    "X-Muster-Client": "opencode"
  }
}

Tests

  • internal/mirrorhost/mcpproxy_opencode_test.go: session → window (launcher-bound, verified title, child and grandchild); fail-closed cases; the marker vs pane-header precedence; the endpoint end to end (instructions, tools, no_window tool errors, 401 without the secret, nothing forwarded); the marker is not honoured on /mcp, and an unmarked outsider is still an outsider; Ancestors against the opencode fake server.
  • internal/previewhost/mcp_test.go: the _meta id reaches Caller for every tool. A missing id, a non-string id or another key reads as none.
  • Red at the base, green here: at the base, an opencode-marked caller got the outsider's empty tool list and no instructions. With pane headers, its call was bound to the pane's window ("@1", the service's), not its session's.
  • Mutation battery: 15 mutants, all killed, including a known-caught control. They cover the fail-closed branches (empty id, unbound, ambiguous, the no-window fallback), the guard (the marker honoured on /mcp, the marker never skipping), the ancestry walk, the _meta read and trimming. Source checksums were verified after the battery.
  • internal/mirror/runtime_ledger_test.go: MCPProxy.openCodeWindow is added to the runtime ledger on purpose. It names RuntimeOpenCode because the id it resolves is opencode's own _meta key. The first CI run went red on this ledger, and the second commit fixes it.
  • go test -race on ./internal/previewhost ./internal/mirrorhost ./cmd/muster-mirror: ok. Leak scan: clean.

Not verified: a live opencode session calling the tools. That needs the entry above on a host with previews on.

🤖 Generated with Claude Code

ZacxDev and others added 2 commits November 10, 2026 22:20
opencode resolves a remote MCP server's headers once, in its one shared
background service, so pane headers would attribute every opencode
session to the service's pane. The one per-session signal it sends is
params._meta["ai.opencode/sessionID"] on every tools/call.

/preview-mcp now recognises an opencode caller by a static
X-Muster-Client: opencode header. Such a request skips the tmux-server
check (the service runs in no pane; secret, Origin and POST checks are
unchanged), initialize/tools/list answer as for any caller, and a
tools/call is bound to the ONE window whose active opencode pane shows
that session, or its nearest ancestor (a subagent's child session),
through a binding muster sends on (launcher record or verified title
binding). No id, an unknown id, an unverified title match, or two
windows showing it is no_window. Pane headers on an opencode-marked
request are ignored: they could only be the service's own.

The id selects among this host's own mirrored windows behind the same
secret; a forged id can only reach another of the owner's windows on
this host. /mcp (muster's tools) does not honour the header.

Tests: red at the base (an opencode caller gets the outsider's empty
tool list; with pane headers it is bound to the pane's window), green
here; 15 mutants of the resolution, guard, ancestry walk and _meta read
all killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MCPProxy.openCodeWindow matches only an opencode pane's session: the id
it resolves is opencode's own _meta key, so the runtime check is
deliberate and belongs in the ledger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner Author

Round 0 + round 1 on 3cea184: no operator premise, no 🔴 — ladder ends. The secret stays mandatory (Origin → secret → POST → marker), the marker only skips the tmux-server routing check and only on /preview-mcp (/mcp passes opencodeOK=false; mutant caught), lookups are fresh per call and fail closed. Recorded open (🟢, not fixed): no test for the cycle check in Ancestors (depth cap still bounds it); binding needs opencode to be the window's active pane; a launcher-bound pane keeps its original session id. 🟢4 resolved by evidence outside this PR: opencode puts a server's initialize instructions into the system context (pinned opencode 6ec5682: mcp/instructions.ts → session/context.ts:139).

(no deletion candidates — examined: 5 requirements, guard change, ancestor walk, Ancestors/OpenCodeSourceOf, main.go wiring, 3 docs; none removable without breaking the operator's opencode ask or a fail-closed branch)
1. no fixes this round (no 🔴/🟡); 🟢 items recorded open above

@ZacxDev
ZacxDev marked this pull request as ready for review November 11, 2026 05:36
@ZacxDev
ZacxDev merged commit e932b69 into main Nov 11, 2026
19 checks passed
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