Skip to content

Unbreak cmux-relay against cmux 0.64.16+ (password-mode socket gate)#7

Open
leduckhc wants to merge 3 commits into
NewTurn2017:mainfrom
leduckhc:main
Open

Unbreak cmux-relay against cmux 0.64.16+ (password-mode socket gate)#7
leduckhc wants to merge 3 commits into
NewTurn2017:mainfrom
leduckhc:main

Conversation

@leduckhc

@leduckhc leduckhc commented Jun 24, 2026

Copy link
Copy Markdown

Three commits that unbreak cmux Remote against modern cmux (0.64.16+).

Why

cmux 0.64.16 changed the default automation.socketControlMode to "cmuxOnly" — only cmux-descendant processes can bind the UDS. A launchd-spawned cmux-relay has launchd as PID 1, not cmux, so it gets

ERROR: Access denied — only processes started inside cmux can connect

and the events-stream supervisor pegs at the 8 s reconnect cap. The intended fix is to flip cmux to socketControlMode = "password" and share the secret with the relay, but two things blocked that path:

  1. cmuxSocketPassword() only checked the legacy ~/Library/Application Support/cmux/socket-control-password path. cmux 0.64.16 writes the password sidecar to $XDG_STATE_HOME/cmux/socket-control-password (default ~/.local/state/cmux/socket-control-password), so the relay couldn't find it even after cmux ingested it.
  2. scripts/install-launchd.sh and scripts/relay.plist.tmpl had no slot for CMUX_SOCKET_PASSWORD, forcing operators to plutil -insert the env var by hand.

Plus, cmux 0.64.16 strips automation.socketPassword from cmux.json after ingesting it on startup (privacy hardening — plaintext doesn't sit in config forever). Side-effect: every cmux restart wipes the server-side password and the relay needs the password re-injected. Mechanical, but easy to forget.

What

  • Follow cmux's relocated socket-control-password filecmuxSocketPassword() now follows CMUX_SOCKET_PASSWORD env → $XDG_STATE_HOME/cmux/socket-control-password → legacy app-support path. Mirrors the tiering that cmuxSocketPath() already uses (matching the convention from 55e2d45). Adds two new tests covering the XDG tier and XDG_STATE_HOME override; updates the existing 3 password tests to inject an empty state directory so the host's real ~/.local/state/cmux/socket-control-password cannot shadow fixtures.
  • Surface CMUX_SOCKET_PASSWORD in the launchd installer — adds CMUX_SOCKET_PASSWORD to the plist template's EnvironmentVariables block (placeholder __SOCKET_PASSWORD__); install-launchd.sh reads CMUX_SOCKET_PASSWORD from its own env and renders it via sed. Usage help and dry-run notes document the knob and mask the value in [install-launchd] notes (<set via CMUX_SOCKET_PASSWORD env, N chars>); the literal password is never echoed by the script. README gets a new cmux 0.64.16+ socket-control 비밀번호 section walking through the recipe.
  • Add scripts/refresh-cmux-password.sh for cmux-restart recovery — idempotent helper that reads the canonical secret out of the launchd plist, re-injects it into cmux.json, runs cmux reload-config, kickstarts the relay, and confirms the events stream attached. Safe to re-run any time. Uses python3 (default on macOS) for the JSON edit so the secret never crosses argv.

Verification

  • swift build -c release clean (the pre-existing Sendable warning at HTTPServer.swift:246 is unrelated and tracked separately).
  • The XDG tier behavior was unit-tested via a standalone harness (the SharedKitTests target imports the swift-testing Testing module which the package doesn't declare, blocking swift test end-to-end on stock toolchains — pre-existing, not introduced by this PR; a small package consuming CMUXClient exercised the 6 cases of the new resolution order and all passed).
  • Live end-to-end on cmux 0.64.16 + iPhone over Tailscale: pairing works, terminal mirroring works, cmux-event-driven Inbox notifications work, events.stream attached: 1, unavailable: 0 after restart + refresh-cmux-password.sh.

Out of scope (mentioned for context, not in this PR)

  • The Tests/SharedKitTests/*.swift files import the swift-testing Testing module which the package doesn't declare, blocking swift test. Pre-existing.
  • WSProtocolMachine forwards arbitrary cmux RPC method names without an allow-list, and PerDeviceRateLimiter is defined but never instantiated in production — both flagged in a separate security review I shared with the maintainer.

Summary by CodeRabbit

  • New Features

    • Added support for socket-password-based relay authentication, including automatic password propagation and refresh handling.
    • Added a helper workflow to restore relay connectivity after cmux restarts.
  • Bug Fixes

    • Improved password lookup so it checks environment, state storage, and legacy app-support locations in the right order.
    • Ignored blank or whitespace-only password values to avoid accidental misconfiguration.
  • Documentation

    • Expanded troubleshooting guidance with step-by-step setup and recovery instructions for password mode.

leduckhc added 3 commits June 24, 2026 12:11
cmux 0.64.16 moved automation state to $XDG_STATE_HOME/cmux (default
~/.local/state/cmux/), mirroring the socket-path move addressed in
55e2d45. cmuxSocketPassword() still only checked the legacy
~/Library/Application Support/cmux/socket-control-password, so a relay
running against cmux 0.64.16+ in password mode could not find the
secret cmux had just written and fell back to no auth.

Resolution order now mirrors cmuxSocketPath():

  1. CMUX_SOCKET_PASSWORD env (matches cmux --help precedence)
  2. $XDG_STATE_HOME/cmux/socket-control-password (default
     ~/.local/state/cmux/socket-control-password)
  3. legacy ~/Library/Application Support/cmux/socket-control-password

A new optional stateDirectory: parameter mirrors the cmuxSocketPath
test surface so password tests can isolate the new tier; the existing
three password tests inject an empty state dir so the host's real
~/.local/state/cmux/socket-control-password cannot shadow fixtures.
Adds two new tests covering the XDG tier and XDG_STATE_HOME override.
cmux 0.64.16 changed the default automation.socketControlMode to
'cmuxOnly' — only cmux-descendant processes may bind the UDS. A
launchd-spawned relay has launchd as PID 1, not cmux, so it is denied
with 'ERROR: Access denied — only processes started inside cmux can
connect' and the events-stream supervisor pegs at the 8 s reconnect
backoff cap.

The fix is to flip cmux to socketControlMode='password' and share the
secret with the relay. Until now the install script and plist template
had no slot for it, forcing operators to plutil-insert the env var by
hand.

  - scripts/relay.plist.tmpl: add CMUX_SOCKET_PASSWORD entry to
    EnvironmentVariables (rendered token __SOCKET_PASSWORD__).
  - scripts/install-launchd.sh: SOCKET_PASSWORD passthrough from
    CMUX_SOCKET_PASSWORD env, rendered via sed alongside the existing
    knobs. Usage help and dry-run notes document the knob and mask the
    value (only the length is printed; the literal password is never
    echoed in [install-launchd] notes).
  - README.md: add a 'cmux 0.64.16+ socket-control 비밀번호' section
    walking through the cmux-settings → install-launchd.sh → cmux
    restart sequence, and explain why a one-time cmux restart is
    required (socketControlMode is read at app startup; cmux
    reload-config does not re-init SocketControlServer).
cmux 0.64.16 reads automation.socketPassword from cmux.json once on
startup and then strips the field as a privacy hardening step. Every
cmux restart therefore wipes the server-side password and a launchd-
spawned cmux-relay starts emitting:

  ERROR: auth_unconfigured — Password mode is enabled but no socket
  password is configured in Settings.

The recovery is mechanical (re-write the password into cmux.json,
reload-config, kickstart the relay) but easy to forget across cmux
upgrades. Bake it into a single idempotent script so the answer to
'I just restarted cmux, now what' is one command.

The script:

  1. Reads CMUX_SOCKET_PASSWORD from the relay's launchd plist (the
     canonical secret install-launchd.sh staged there). Never asks the
     user to type or paste it.
  2. Re-injects it into cmux.json under automation.socketPassword via
     a python3 in-place rewrite that preserves every other top-level
     section. Drops a once-per-day cmux.json.YYYYMMDD.bak so the user
     can audit edits without flooding the config dir.
  3. Runs cmux reload-config so cmux ingests the password and re-emits
     ~/.local/state/cmux/socket-control-password.
  4. launchctl kickstart -k on the relay so its events.stream
     subscriber re-auths.
  5. Tails the relay log briefly and reports attached / unavailable
     counts. Exits non-zero with a targeted hint when auth still
     fails (signals a plist/cmux password mismatch — re-run
     install-launchd.sh with the matching CMUX_SOCKET_PASSWORD).

Also documented in README.md alongside the install steps.
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02f35f9f-67b7-4b4d-97d0-9c68c0190870

📥 Commits

Reviewing files that changed from the base of the PR and between 9f805eb and 6662df0.

📒 Files selected for processing (6)
  • README.md
  • Sources/CMUXClient/UnixSocketChannel.swift
  • Tests/CMUXClientTests/CmuxSocketPathTests.swift
  • scripts/install-launchd.sh
  • scripts/refresh-cmux-password.sh
  • scripts/relay.plist.tmpl
 __________________________________
< Pretty fly for a code review AI. >
 ----------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

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