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:
21
CHANGELOG.md
21
CHANGELOG.md
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,6 +110,32 @@ Tip: compare against `pnpm clawdbot gateway discover --json` to see whether the
|
|||||||
macOS app’s discovery pipeline (NWBrowser + tailnet DNS‑SD fallback) differs from
|
macOS app’s discovery pipeline (NWBrowser + tailnet DNS‑SD fallback) differs from
|
||||||
the Node CLI’s `dns-sd` based discovery.
|
the Node CLI’s `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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user