Skip to content

TUI default in-process transport makes PluginInput.serverUrl report a fabricated localhost:4096 (plugins can't attach) #39561

Closed as not planned

Description

@rgillinlz

Summary

When the TUI runs with the default in-process transport (no --port / --hostname / --mdns), OpenCode does not bind any TCP listener, but PluginInput.serverUrl still reports a fabricated http://localhost:4096. Plugins that consume ctx.serverUrl as a reachable server (for example to drive the TUI via opencode attach <serverUrl> --session <id>) are handed a dead URL and fail silently.

Root cause

Two connected spots:

  1. Default TUI never binds. packages/opencode/src/cli/cmd/tui.ts (≈ L233–249):

    const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true
    const transport = external
      ? { url: (await client.call("server", network)).url, ... }   // real TCP listener
      : { url: "http://opencode.internal", fetch: createWorkerFetch(client), ... }  // in-process RPC, no bind

    With no network flags, external is false, so Server.listen() is never called and there is no socket.

  2. PluginInput.serverUrl fabricates a live-looking URL anyway. packages/opencode/src/plugin/index.ts (≈ L141–161):

    const serverUrl = Server.url                       // undefined when not bound
    baseUrl: serverUrl?.toString() ?? "http://localhost:4096",
    ...
    get serverUrl(): URL {
      return Server.url ?? new URL("http://localhost:4096")   // nothing listening here
    }

    When Server.url is undefined (no listener), both the client baseUrl and the public serverUrl getter fall back to http://localhost:4096, which implies a reachable TCP server. There isn't one.

Reproduction

# 1. Start the TUI with no network flags (default).
# 2. Confirm it binds nothing:
lsof -nP -i TCP -sTCP:LISTEN -p <tui-pid>        # → empty

# Compare:
opencode --port 4096   # binds 127.0.0.1:4096 (reachable)

Real-world impact: the @mspiegel31/opencode-cmux subagent-viewer does opencode attach ${serverUrl} --session ${id}. With the fabricated http://localhost:4096 (and the plugin author's documented port 0 workaround), there is no port to attach to, so the viewer aborts silently.

Expected behavior

PluginInput.serverUrl should reflect reality. When no HTTP listener is bound, it should surface the in-process transport placeholder (http://opencode.internal, already used in tui.ts and run.ts) rather than a fabricated localhost:4096, so plugins can detect there is no external TCP server.

Proposed fix

In plugin/index.ts, change both ?? "http://localhost:4096" / ?? new URL("http://localhost:4096") fallbacks to http://opencode.internal. This is behavior-neutral for the plugin client (the in-process transport routes via Server.Default().app.fetch regardless of the baseUrl host) and only makes the reported URL honest.

I have a PR ready that implements this.

Environment

  • opencode 1.18.9 (also present on dev)
  • macOS (darwin/arm64)

Activity

  1. github-actions commented on Jul 29, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Both issues stem from the same code path you've identified in plugin/index.ts. It may be worth coordinating with those threads, especially since your proposed fix (changing the fallback to http://opencode.internal) is complementary to the solutions discussed there.

  2. rgillinlz commented on Jul 29, 2026

    @rgillinlz
    Author

    Attaching a ready-to-apply patch implementing the proposed fix, in case it's useful before a PR is up. Apply with:

    git am < plugin-serverurl-inprocess.patch

    Summary of the change (in packages/opencode/src/plugin/index.ts):

    • baseUrl fallback: "http://localhost:4096" → "http://opencode.internal"
    • serverUrl getter fallback: new URL("http://localhost:4096") → new URL("http://opencode.internal")

    Behavior-neutral for the plugin client (the in-process transport routes via Server.Default().app.fetch regardless of baseUrl host); it only makes the reported serverUrl honest so plugins can detect there's no external TCP server.

    plugin-serverurl-inprocess.patch
    From 6659527c98a067691684a188c292434f215eb690 Mon Sep 17 00:00:00 2001
    From: Robert Gillin <[email protected]>
    Date: Wed, 29 Jul 2026 13:16:53 -0700
    Subject: [PATCH] fix(plugin): report in-process placeholder in serverUrl when
     no HTTP listener is bound
    
    When the TUI runs with the default in-process transport (no --port/--hostname/--mdns),
    Server.url is undefined and PluginInput.serverUrl fell back to a fabricated
    http://localhost:4096, implying a reachable TCP server that does not exist. Plugins
    that dial ctx.serverUrl (e.g. cmux subagent-viewer's 'opencode attach') silently fail.
    
    Report the in-process placeholder http://opencode.internal instead, matching tui.ts
    and run.ts, so plugins can detect there is no external server. The in-process client
    routes via Server.Default().app.fetch regardless of baseUrl host, so this is
    behavior-neutral for the plugin client and only makes the reported URL honest.
    ---
     packages/opencode/src/plugin/index.ts | 11 +++++++++--
     1 file changed, 9 insertions(+), 2 deletions(-)
    
    diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
    index 1558c70..2b0a895 100644
    --- a/packages/opencode/src/plugin/index.ts
    +++ b/packages/opencode/src/plugin/index.ts
    @@ -139,8 +139,12 @@ const layer = Layer.effect(
             const { Server } = yield* Effect.promise(() => import("../server/server"))
     
             const serverUrl = Server.url
    +        // When no HTTP listener is bound (the default in-process TUI transport),
    +        // there is no reachable loopback URL. Report the in-process placeholder
    +        // ("http://opencode.internal") rather than a fabricated localhost:4096,
    +        // which would falsely imply a live TCP server that plugins can dial.
             const client = createOpencodeClient({
    -          baseUrl: serverUrl?.toString() ?? "http://localhost:4096",
    +          baseUrl: serverUrl?.toString() ?? "http://opencode.internal",
               directory: ctx.directory,
               headers: ServerAuth.headers(),
               ...(serverUrl ? {} : { fetch: async (...args) => Server.Default().app.fetch(...args) }),
    @@ -157,7 +161,10 @@ const layer = Layer.effect(
                 },
               },
               get serverUrl(): URL {
    -            return Server.url ?? new URL("http://localhost:4096")
    +            // Reflect the bound listener when one exists; otherwise surface the
    +            // in-process placeholder so plugins can detect there is no external
    +            // TCP server instead of being handed a dead localhost:4096 URL.
    +            return Server.url ?? new URL("http://opencode.internal")
               },
               // @ts-expect-error
               $: typeof Bun === "undefined" ? undefined : Bun.$,
    -- 
    2.50.1 (Apple Git-155)
    
  3. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions