Skip to content

fix(desktop): credential the sidecar when its service listens on a non-loopback address - #60525

Open
Hona wants to merge 1 commit into
devfrom
sidecar-remote-host
Open

Hona wants to merge 1 commit into
devfrom
sidecar-remote-host

Conversation

@Hona

@Hona Hona commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

When the background service is configured to listen on a non-loopback address (hostname in ~/.config/opencode/service.json, for example a Tailscale IP to share it), the desktop app on that machine fails every request to its own server with 401:

  • BackgroundService publishes the sidecar as http://100.87.251.43:45278.
  • The renderer leaves sidecar requests uncredentialed. Main adds Authorization in onBeforeSendHeaders, but that filter only matched 127.0.0.1 and localhost.
  • The event stream fails, asks main for the service again, and retries. On one machine this ran every 1–2 s for almost two hours: 3,823 starting v2 background service log lines, and a 401 on the server for every /api/event, /api/info and session read.

Fix

wireRendererHeaders now builds its filter from the published sidecar endpoint. It registers again on every SidecarCredentials change, because the window opens before the service is ready.

flowchart LR
  S["service.json<br/>hostname: 100.87.251.43"] --> B["BackgroundService<br/>SidecarCredentials.set(url)"]
  B -- onChange --> F["onBeforeSendHeaders<br/>127.0.0.1, localhost, 100.87.251.43"]
  R["renderer fetch<br/>http://100.87.251.43:45278/api/event"] --> F
  F -- "renderer frame + sidecar origin" --> A["Authorization: Basic …"]
Loading
const follow = () =>
  session.webRequest.onBeforeSendHeaders({ urls: sidecarUrls(SidecarCredentials.get()) }, credential)

follow()
SidecarCredentials.onChange(follow)
  • SidecarCredentials.authorization still adds the header only for the exact sidecar origin, from the renderer's top-level frame.
  • A match pattern without a port covers every port, so a restart on a new port needs no new pattern.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none">
  <rect x="8" y="22" width="60" height="28" stroke="currentColor" opacity=".5" />
  <rect x="172" y="22" width="60" height="28" stroke="currentColor" opacity=".5" />
  <rect x="18" y="32" width="40" height="8" fill="currentColor" opacity=".3" />
  <rect x="182" y="32" width="40" height="8" fill="currentColor" opacity=".3" />
  <line x1="72" y1="36" x2="168" y2="36" stroke="currentColor" opacity=".2" />
  <g fill="currentColor">
    <rect x="72" y="32" width="14" height="8" opacity=".8">
      <animate attributeName="x" values="72;154;154" keyTimes="0;.6;1" dur="2s" repeatCount="indefinite" />
    </rect>
    <circle cx="120" cy="18" r="3" opacity=".6" />
    <rect x="117" y="20" width="6" height="6" opacity=".6" />
  </g>
</svg>

Added by the opencode bot (AI-generated).

Fixes #51075

Copilot AI balanced review requested due to automatic review settings October 30, 2026 07:27
@Hona
Hona requested a review from Brendonovich as a code owner October 30, 2026 07:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencodejr opencodejr Bot 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.

The fix looks correct. Building the onBeforeSendHeaders filter from the published sidecar host, and registering it again on each SidecarCredentials change, keeps the exact-origin and top-level-renderer checks in SidecarCredentials.authorization. So a wider filter doesn't send the credential anywhere new.

I couldn't run the desktop app here. The new test passes with this change and fails on the base for the Tailscale and LAN-name cases. tsc -b for packages/desktop passes. Its typecheck script fails with "Script not found" before checking anything.

Public API: unchanged (internal desktop main-process code only).

One small point inline: the onChange subscription added for each window is never removed.

Fixes #51075

session.webRequest.onBeforeSendHeaders({ urls: sidecarUrls(SidecarCredentials.get()) }, credential)

follow()
SidecarCredentials.onChange(follow)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

wireRendererHeaders runs for every new window, and this subscription is never removed. Each window you open adds another follow listener that lives for the life of the app, and every listener registers the same session filter again. Could you keep the stop function that onChange returns and call it when the window is closed?

This branch has not been deployed

No deployments
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.

desktop: local service on Tailscale receives unauthenticated requests

2 participants