Merge pull request #960 from kkarimi/fix/mac-node-bridge-tunnel-865

macOS: prefer bridge tunnel port in remote mode
This commit is contained in:
Peter Steinberger
2026-01-16 02:00:23 +00:00
committed by GitHub
53 changed files with 409 additions and 345 deletions

View File

@@ -86,7 +86,28 @@
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. - UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank. - TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states. - TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
- TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`.
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). - Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
#### Agents / Auth / Tools / Sandbox
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
- Logging: tolerate `EIO` from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.
- Sandbox: restore `docker.binds` config validation and preserve configured PATH for `docker exec`. (#873) — thanks @akonyer.
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
#### macOS / Apps
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
- macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
- macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
- macOS: reuse launchd gateway auth and skip wizard when gateway config already exists. (#917)
- macOS: prefer the default bridge tunnel port in remote mode for node bridge connectivity; document macOS remote control + bridge tunnels. (#960, fixes #865) — thanks @kkarimi.
- Apps: use canonical main session keys from gateway defaults across macOS/iOS/Android to avoid creating bare `main` sessions.
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis. - macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver. - Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
- Telegram: split long captions into media + follow-up text messages. (#907) - thanks @jalehman. - Telegram: split long captions into media + follow-up text messages. (#907) - thanks @jalehman.

View File

@@ -103,6 +103,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
extension MenuSessionsInjector { extension MenuSessionsInjector {
// MARK: - Injection // MARK: - Injection
private var mainSessionKey: String { WorkActivityStore.shared.mainSessionKey } private var mainSessionKey: String { WorkActivityStore.shared.mainSessionKey }
private func inject(into menu: NSMenu) { private func inject(into menu: NSMenu) {

View File

@@ -312,9 +312,23 @@ final class MacNodeModeCoordinator {
} }
let remotePort = Self.remoteBridgePort() let remotePort = Self.remoteBridgePort()
let preferredLocalPort = Self.loopbackBridgePort()
if let preferredLocalPort {
self.logger.info(
"mac node bridge tunnel starting " +
"preferredLocalPort=\(preferredLocalPort, privacy: .public) " +
"remotePort=\(remotePort, privacy: .public)")
} else {
self.logger.info(
"mac node bridge tunnel starting " +
"preferredLocalPort=none " +
"remotePort=\(remotePort, privacy: .public)")
}
self.tunnel = try await RemotePortTunnel.create( self.tunnel = try await RemotePortTunnel.create(
remotePort: remotePort, remotePort: remotePort,
allowRemoteUrlOverride: false) preferredLocalPort: preferredLocalPort,
allowRemoteUrlOverride: false,
allowRandomLocalPort: true)
if let localPort = self.tunnel?.localPort, if let localPort = self.tunnel?.localPort,
let port = NWEndpoint.Port(rawValue: localPort) let port = NWEndpoint.Port(rawValue: localPort)
{ {

View File

@@ -110,6 +110,32 @@ Tip: compare against `pnpm clawdbot gateway discover --json` to see whether the
macOS apps discovery pipeline (NWBrowser + tailnet DNSSD fallback) differs from macOS apps discovery pipeline (NWBrowser + tailnet DNSSD fallback) differs from
the Node CLIs `dns-sd` based discovery. the Node CLIs `dns-sd` based discovery.
## Remote connection plumbing (SSH tunnels)
When the macOS app runs in **Remote** mode, it opens SSH tunnels so local UI
components can talk to a remote Gateway as if it were on localhost. There are
two independent tunnels:
### Control tunnel (Gateway control/WebSocket port)
- **Purpose:** health checks, status, Web Chat, config, and other control-plane calls.
- **Local port:** the Gateway port (default `18789`), always stable.
- **Remote port:** the same Gateway port on the remote host.
- **Behavior:** no random local port; the app reuses an existing healthy tunnel
or restarts it if needed.
- **SSH shape:** `ssh -N -L <local>:127.0.0.1:<remote>` with BatchMode +
ExitOnForwardFailure + keepalive options.
### Node bridge tunnel (macOS node mode)
- **Purpose:** connect the macOS node to the Gateway **Bridge** protocol (TCP JSONL).
- **Remote port:** `gatewayPort + 1` (default `18790`), derived from the Gateway port.
- **Local port preference:** `CLAWDBOT_BRIDGE_PORT` or the default `18790`.
- **Behavior:** prefer the default bridge port for consistency; fall back to a
random local port if the preferred one is busy. The node then connects to the
resolved local port.
For setup steps, see [macOS remote access](/platforms/mac/remote). For protocol
details, see [Bridge protocol](/gateway/bridge-protocol).
## Related docs ## Related docs
- [Gateway runbook](/gateway) - [Gateway runbook](/gateway)

View File

@@ -9,6 +9,9 @@ import { pollUntil } from "../../../test/helpers/poll.js";
import { approveNodePairing, listNodePairing } from "../node-pairing.js"; import { approveNodePairing, listNodePairing } from "../node-pairing.js";
import { configureNodeBridgeSocket, startNodeBridgeServer } from "./server.js"; import { configureNodeBridgeSocket, startNodeBridgeServer } from "./server.js";
const pairingTimeoutMs = process.platform === "win32" ? 8000 : 3000;
const suiteTimeoutMs = process.platform === "win32" ? 20000 : 10000;
function createLineReader(socket: net.Socket) { function createLineReader(socket: net.Socket) {
let buffer = ""; let buffer = "";
const pending: Array<(line: string) => void> = []; const pending: Array<(line: string) => void> = [];
@@ -55,9 +58,8 @@ async function waitForSocketConnect(socket: net.Socket) {
}); });
} }
describe("node bridge server", () => { describe("node bridge server", { timeout: suiteTimeoutMs }, () => {
let baseDir = ""; let baseDir = "";
const pairingTimeoutMs = process.platform === "win32" ? 8000 : 3000;
const pickNonLoopbackIPv4 = () => { const pickNonLoopbackIPv4 = () => {
const ifaces = os.networkInterfaces(); const ifaces = os.networkInterfaces();