Visitar URL original
feat(desktop): pair with nearby phones over mDNS by Brendonovich · Pull Request #63679 · anomalyco/opencode · GitHub
Skip to content

feat(desktop): pair with nearby phones over mDNS - #63679

Open
Brendonovich wants to merge 3 commits into
devfrom
nearby-pairing
Open

Brendonovich wants to merge 3 commits into
devfrom
nearby-pairing

Conversation

@Brendonovich

@Brendonovich Brendonovich commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Lets the OpenCode mobile app pair with a desktop on the same network without scanning a QR code. The desktop announces itself over mDNS (_opencode._tcp), the phone lists it, and tapping it runs a key exchange. Both screens show the same 6-digit code, and only after the user confirms on both devices does the desktop send a pairing code, encrypted for that phone. The phone then redeems it the same way as a scanned QR code. The server doesn't change.

Guarded by the new nearby_pairing flag. Unlike other flags it's on in local, dev and beta builds, so beta testers can try it with the mobile app; stable releases keep it off until it graduates. The desktop main process has no Location config, so it reads the flag from OPENCODE_FLAGS and the build channel only; experimental.nearby_pairing in config doesn't reach it.

How it works

  • Code exchange (exchange.ts, whose header comment is the wire spec): X25519 with a commit/reveal step so an attacker in the middle can't search for matching codes, HKDF-derived keys for each direction, then AES-256-GCM sealed frames with counter nonces.
  • Prompt: a request shows as a toast in every window, wherever the user is (Pair / Decline). If no window is focused, a system notification also shows, at most once every 30 s. Success or failure gets its own toast.
  • Abuse limits (gate.ts): one request at a time; a device can replace its own request at most every 3 s; Snooze This Device (1 h) appears after 3+ requests from one address in 2 min, and Snooze All for 15 Minutes after 3+ addresses. Devices are told apart by network address, not the name the phone sends.
  • mDNS (mdns*.ts): uses the system's own mDNS service. macOS runs dns-sd -R. Linux talks to Avahi over D-Bus (dbus-next). Windows uses DnsServiceRegister in dnsapi.dll, through a hidden PowerShell process. If the system service is missing or refuses, it falls back to bonjour-service. Using the system service avoids the host-name clashes (macbook-2, -3, …) that a second responder causes.
  • Settings › Pairing › Discoverable nearby turns announcing off. It shows only while the flag is on, and turning it off and on again clears snoozes.
  • Remote access: if the server only listens on this machine, the pairing payload turns on remote access and sends the tunnel URL, as the QR dialog does.

The iOS side is in anomalyco/opencode-ios, branch nearby-pairing, behind an off-by-default experiment.

Testing

  • bun run check passes, the 59 pairing tests pass (exchange with fixed test vectors shared with the Swift version, gate, mDNS backend choice and fallback), and the desktop production build bundles the new code.

  • macOS: paired the dev desktop end to end with the iOS app on a phone and in the simulator. After porting onto the refactored pairing plugin, rechecked with a mock phone: discovery starts through dns-sd and the toast shows the code.

  • Linux: Avahi path checked in a Debian container: publish, withdraw on stop, the "Roadmap & Existing Issues #2" rename on a duplicate name, and the fallback when Avahi is gone. Not yet tried on a real Linux machine on the same network as a phone.

  • Windows: the PowerShell script parses and compiles under PowerShell 7 on Linux, but it hasn't run on Windows yet. It still needs a check on real hardware, including what the firewall asks about the pairing port.

  • Settings › Pairing: the Discoverable nearby row renders on the refactored page, and toggling it stops and restarts the announcement.

A screen recording of the desktop and simulator flow is available on request (Brendan has it).


Added by the opencode bot (AI-generated).

Public API: adds the nearby_pairing flag and the experimental.nearby_pairing config key (backwards compatible)

@Brendonovich
Brendonovich marked this pull request as ready for review November 13, 2026 05:25

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

I couldn't run this end to end: it needs the Electron app, a phone and a system mDNS responder. I read the whole diff. The 59 pairing tests pass and tsc -b is clean in packages/gui-plugins.

Public API: the change adds the nearby_pairing flag to ConfigFlag.registry, which also adds the config key experimental.nearby_pairing (backwards compatible). It extends the internal GUI pairing IPC contract and adds the macOS NSLocalNetworkUsageDescription and NSBonjourServices entries. The server API is unchanged.

Concerns, most important first:

  1. Generated files committed by mistake. The PR adds roughly 2,000 lines of generated Astro output under packages/web/.astro/ and packages/posts/.astro/ that have nothing to do with this feature. Please drop them.
  2. An idle connection can block pairing (inline comment on nearby.ts).
  3. The new config key does nothing (inline comment on flag.ts).
  4. Beta rollout on Windows. The flag is on by default in beta, so every beta user opens a listener on all interfaces at startup. The Windows dnsapi path, and the firewall prompt it may raise, haven't been tried on real hardware yet. It's worth checking that before this ships to beta, or keeping the flag off on Windows until then.


const pair = (socket: Socket) => {
const device = socket.remoteAddress ?? "unknown"
const admission = gate.admit(device, state.active)

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 gate gives a connection the single slot (state.active) as soon as the TCP connection opens, before hello/commit. The prompt and its snooze buttons only appear once exchange.code resolves.

So a device on the network can just connect and stay silent, holding the slot for up to two 10 s steps. If it reconnects every 3 s, it keeps getting replaced into the slot indefinitely. Meanwhile every real phone is refused silently, and the user never sees a toast or the snooze offers.

Consider admitting a connection only once its code is ready, or at least not refusing other devices while the active one hasn't reached the code yet.

Related: after a replace, state.prompt keeps the old id and code until the new code arrives. During that window, pressing Pair on the visible toast does nothing.

"Watch Slack threads, GitHub pull requests, issues, commits and Actions runs, and the workspace's MCP servers' events through OpenCode Console, and wake sessions when they change.",
mcp_events:
"Enable experimental MCP Events, a draft MCP extension: declare support to MCP servers and read the events of servers that offer them.",
nearby_pairing:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adding this to registry also adds an experimental.nearby_pairing config key through experimental.ts. That key does nothing, because main never reads config. Users who set it will think they've turned the feature on or off. Can the flag live outside the shared registry, or can the schema leave this key out, until main can read it from config?

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

Thanks for removing the generated .astro files. They're no longer in the diff, which settles the main point from my last review. This push only deletes files, so there was nothing new to run. The other two points are still in their open threads: a silent connection can hold the only pairing slot, and the experimental.nearby_pairing config key has no effect. The one from my summary is also still open: the Windows path should be checked on real hardware, or the flag kept off on Windows in beta until it is.

Public API: unchanged from my last review. The PR adds the nearby_pairing flag, and with it the experimental.nearby_pairing config key (backwards compatible).

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.

1 participant