Skip to content

iOS: Tailscale connection method opt-in (Settings + onboarding) with QR-authorized pairing - #9247

Merged
azooz2003-bit merged 5 commits into
mainfrom
feat-ios-tailscale-optin
Jul 31, 2026
Merged

azooz2003-bit merged 5 commits into
mainfrom
feat-ios-tailscale-optin

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

What

Users can now opt into Tailscale over iroh on iOS, from two surfaces:

  • Settings → Connection Method: an Auto-Connect / Tailscale picker. Choosing Tailscale shows a "Scan Pairing Code" button and a footer explaining the Mac-side QR (cmux Settings → Pair iPhone).
  • Last onboarding page: an Auto-Connect (recommended) vs Tailscale choice under the connection preview. Picking Tailscale flips the primary button to "Scan Pairing Code", which opens the existing onboarding scanner.

Why this is more than UI

On current builds a brand-new Tailscale pairing is impossible: .tailscale routes only dial with a device-local CmxLegacyTailscaleAuthorizationEvidence grant that exists solely via the v8 store migration (pre-iroh pairings), and route selection pins exclusively to iroh whenever an iroh route exists. Scanning the Mac's compatibility QR today decodes fine and then fails as hostUnreachable.

How it works

  1. Code entry = authorization event. A new CmxTransportAuthorizationMode.userAuthorizedTailscalePairing(CmxUserTailscalePairingAuthorization) lets a user-entered compatibility code (the pairing window's tokenless v1 ticket or the bare-route v2 grammar) dial exactly the numeric tailscale host:port it named. The authorization anchors on the destination alone — a device identity a code claims is self-reported and grants nothing — and it is minted only for codes entered in the in-app pairing UI (scanner or paste). External URL opens (onOpenURL, injected attach) never mint it, so a malicious deeplink cannot direct a bearer to an attacker address. The interface-bound Tailscale route proof (single utun, numeric peer, per-write revalidation, local-device rejection) applies unchanged, and the value never persists or outlives the pairing dial.
  2. Persisted user grant. After the Mac authenticates (host status binds macDeviceID), the store records the entered destinations in legacy_tailscale_route_grants with a new origin='user' column (v9 migration). Migration-origin grants keep their existing lifecycle (deleted forever once iroh persists); user-origin grants survive iroh arrival because the user chose Tailscale deliberately. Grants stay device-local and never back up; a deliberate re-scan upgrades a migration grant to user origin.
  3. Preference reorders, never authorizes. MobileConnectionMethodStore (UserDefaults) feeds route selection: with Tailscale selected, granted tailscale routes dial first and iroh remains the fallback instead of being exclusive (storedReconnectRoutes(tailscalePreference:), supportedRoutes in connect). Flipping the preference without a grant changes nothing — the iroh pin stays.
  4. Mac label. The pairing window's legacy toggle is now "Use Tailscale Pairing Code" (EN+JA) so the iOS copy matches what users see on the Mac.

Scope notes

  • Preference is prefer, not require: if the tailnet is unreachable, iroh still connects. A strict "Tailscale only" mode is a possible follow-up.
  • Secondary/background multi-Mac clients keep the iroh pin (read-only control plane).
  • Architecture doc (docs/iroh-app-transport-architecture.md) describes Tailscale TCP as migration-frozen; this PR deliberately re-opens explicit user-driven Tailscale pairing as a product decision, using the already-hardened path. Happy to update the doc in this PR or a follow-up.

Tests

  • CMUXMobileCore 320 ✓ (new: CmxUserTailscalePairingAuthorizationTests — canonicalization, non-tailnet rejection, substitution refusal)
  • CmuxMobileTransport 42 ✓ (new: user-authorized mode builds the interface-proof transport for its exact destination regardless of the claimed identity; host/port substitution fails closed; stack-bearer stays fail-closed)
  • CmuxMobilePairedMac 33 ✓ (new: user grant mint, survives iroh publication, requires existing row, re-scan upgrades migration grant)
  • CmuxMobileShellModel 186 ✓ (new: preference store round-trip)
  • CmuxMobileShell new route-ordering tests ✓; full-suite failures reproduced identically on clean main (pre-existing terminal-replay/render-grid flakes)
  • CmuxSyncStore 49 ✓

Localization audit: 12 new iOS keys + 2 macOS keys, EN+JA, in ios/cmux/Resources/Localizable.xcstrings (app catalog, wins at runtime) and Resources/Localizable.xcstrings.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Auto-Connect and Tailscale connection options in onboarding and Settings.
    • Added Tailscale pairing-code scanning with secure, exact-destination authorization.
    • Saved Tailscale preferences and authorized routes for future connections.
    • Prioritized authorized Tailscale routes while retaining secure fallback connections.
    • Added localized English and Japanese guidance for connection methods and pairing codes.
  • Bug Fixes

    • Blocked unauthorized or mismatched Tailscale destinations.
    • Preserved user-authorized routes when connection details refresh.
    • Improved compatibility with older Tailscale pairing codes.

…airing

Adds an Auto-Connect vs Tailscale connection-method choice to iOS Settings
and the last onboarding page. Choosing Tailscale reorders dialing to put
authorized Tailscale routes ahead of the iroh pin (iroh stays as fallback)
and routes the user to the Mac's compatibility QR scanner.

A scanned/pasted v2 compatibility code becomes the authorization event: a
new .userAuthorizedTailscalePairing transport mode dials only the exact
host:port the user entered, only while the peer is unidentified, and only
from explicit in-app code entry (external URL opens never mint it). After
the Mac authenticates, a device-local 'user'-origin grant row persists so
reconnects use the existing evidence path. v9 schema adds grant origin;
migration-origin grants keep dying on iroh arrival, user-origin grants
survive because the user chose Tailscale deliberately.

Mac pairing window's legacy toggle is relabeled "Use Tailscale Pairing
Code" (EN+JA) to match the iOS copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fb78bc22-4b2a-4e9e-b76c-aedf19e689ca

📥 Commits

Reviewing files that changed from the base of the PR and between d26ec57 and 08b8965.

📒 Files selected for processing (3)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/IOSBuildScopedPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/TeamScopedPairedMacStore.swift
  • Resources/Localizable.xcstrings

📝 Walkthrough

Walkthrough

Adds user-selected Tailscale pairing to iOS. Exact host-port authorization flows through transport validation, route selection, paired-Mac persistence, reconnect ordering, and onboarding/settings UI with persisted connection-method preferences.

Changes

Tailscale pairing authorization

Layer / File(s) Summary
Authorization contracts and transport validation
Packages/Shared/CMUXMobileCore/..., Packages/iOS/CmuxMobileTransport/..., Packages/iOS/CmuxMobileRPC/...
Adds exact host-port pairing evidence and validates it for identity-free Tailscale routes while preserving legacy authorization.
Persisted route authorization
Packages/iOS/CmuxMobilePairedMac/..., Packages/iOS/CmuxMobileShell/...
Adds schema v9 route origins, stores user-authorized grants, preserves them across Iroh updates, and forwards authorization through scoped store wrappers.
Pairing flow and route selection
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/...
Carries explicit pairing-code state through connection attempts, persists authorized routes, and prioritizes matching Tailscale routes before Iroh fallbacks.
Connection-method state and UI
Packages/iOS/CmuxMobileShellModel/..., Packages/iOS/CmuxMobileShellUI/..., ios/..., Resources/...
Adds persisted Automatic/Tailscale selection, onboarding and settings controls, app wiring, scanner behavior, and localized guidance.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MobileHostPickerView
  participant MobileShellComposite
  participant MobileCoreRPCClient
  participant CmxTailscaleRouteProof
  participant MobilePairedMacStore
  MobileHostPickerView->>MobileShellComposite: submit user-entered pairing code
  MobileShellComposite->>MobileCoreRPCClient: provide exact host-port authorization
  MobileCoreRPCClient->>CmxTailscaleRouteProof: validate identity-free Tailscale route
  MobileShellComposite->>MobilePairedMacStore: persist authorized routes
  MobileShellComposite->>MobileCoreRPCClient: reconnect using authorized Tailscale route
Loading

Possibly related PRs

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error Changed Resources/Localizable.xcstrings keys mobile.pairing.codeMode.legacyDetail and useLegacy include only en/ja, but the catalog supports 20 locales. Add translated values for ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant in Resources/Localizable.xcstrings.
Docstring Coverage ⚠️ Warning Docstring coverage is 26.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the opt-in Tailscale connection method, its Settings and onboarding surfaces, and QR-authorized pairing.
Description check ✅ Passed The description explains the feature, rationale, implementation, scope, and testing in detail, although it omits several template sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No new actor-isolation violation found: the UI store is explicitly @MainActor, all store accesses are from MainActor shell/UI boundaries, and the async Sendable storage protocol is not MainActor-is...
Cmux Swift Blocking Runtime ✅ Passed Full PR diff adds no semaphore, blocking wait, sleep, delayed dispatch, main sync, timer, polling loop, or manual lock; new store serialization uses the existing PairedMacMutationGate actor.
Cmux Browser Automation Off-Main ✅ Passed The PR changes 42 mobile/Tailscale paths only; no browser automation paths or added browser commands, and both execution-policy source and tests are unchanged.
Cmux Expensive Synchronous Load ✅ Passed Added production lines contain no agent-history loads, transcript/JSONL parsing, directory scans, file reads, or synchronous load calls; SQLite grant work stays in MobilePairedMacStore actor.
Cmux Cache Substitution Correctness ✅ Passed No fresh authoritative read is replaced: reconnect ordering uses loaded paired-Mac data, while persistence still reads and writes SQLite; the new preference store loads UserDefaults at init and upd...
Cmux No Hacky Sleeps ✅ Passed The PR changes only Swift files and localization catalogs. It introduces no covered TypeScript, JavaScript, shell, or non-Swift build/runtime delay or sleep code.
Cmux Algorithmic Complexity ✅ Passed New scans operate on per-Mac route lists, not workspace-scale records; scanned pairing routes have an explicit maximum of 8, and no batch per-target rescan was introduced.
Cmux Swift Concurrency ✅ Passed The full PR diff adds no background queues, DispatchGroup, Combine, completion handlers, continuations, or new Task blocks; new state uses @Observable and store APIs use async throws.
Cmux Swift @Concurrent ✅ Passed No changed Swift declaration adds nonisolated async work or misuses @concurrent; SQLite writes remain actor-isolated, and UI pairing adds only small validation before existing async hops.
Cmux Swift Package Boundaries ✅ Passed All new domain, transport, persistence, preference, and route logic is in existing SwiftPM targets with dedicated tests. Non-package edits are UI copy or app-lifecycle composition glue.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes no Package.swift, Package.resolved, .gitignore, workflow, or Xcode project package-reference files, so the lockfile policy is not triggered.
Cmux Swift Logging ✅ Passed The diff adds one runtime log via the existing OSLog Logger; it adds no print/debugPrint/dump/NSLog or ad hoc output, and logs only a storage error, not secrets or personal data.
Cmux User-Facing Error Privacy ✅ Passed Added Tailscale names appear in explicit Tailscale selection and pairing UI; the only new raw error interpolation is confined to an internal logger, not user-facing output.
Cmux Swiftui State Layout ✅ Passed New state uses @Observable; the diff adds no ObservableObject, @Published, GeometryReader, lazy-stack, or render-time state mutation. Existing List rows receive only incidental pairing changes.
Cmux Architecture Rethink ✅ Passed No prohibited timing, blocking, observer, or side-channel repair was added; one injected connection-method store owns preference state, shared pairing actions remain centralized, and the paired-Mac...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The branch adds no standalone window or close-shortcut ownership change; it only wires an existing iOS WindowGroup, and scripts/lint_auxiliary_window_close_shortcuts.py passes.
Cmux Source Artifacts ✅ Passed All 42 changed paths are Swift source/tests or localization catalogs; no artifact-like directories, binary blobs, logs, caches, build output, or scratch paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed production Swift adds product APIs/helpers and #if os(iOS) UI guards; no new DEBUG/test seam or test/debug-named member was found, and new helpers have production callers.
Cmux No Ambient Global State ✅ Passed New state is held by injectable MobileConnectionMethodStore and passed through AppCompositionRoot; static helpers belong to stateful MobileShellComposite, with no new global mutable vars or free-fu...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-tailscale-optin

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.

The Mac pairing window's Tailscale code is the tokenless v1 compatibility
ticket, which carries a self-reported macDeviceID. Gating the user-entered
authorization on an empty ticket identity would reject exactly the code
users scan. The claimed identity adds no authority at first dial, so the
authorization now anchors on the exact user-entered host:port alone; the
in-app entry gate and the interface-bound route proof are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift (1)

65-92: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Consolidate the triplicated Tailscale authorization-mode switch.

Three files independently re-implement the same "does this authorizationMode authorize dialing this exact host/port given this expected peer device ID" decision, and this PR extended all three identically for the new userAuthorizedTailscalePairing case. A single shared helper (e.g. CmxTransportAuthorizationMode.authorizesTailscaleDial(host:port:expectedPeerDeviceID:) -> Bool, or an equivalent method placed on the enum in CMUXMobileCore) would let each call site map the result to its own error type while guaranteeing the three checks can never silently diverge.

  • Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift#L65-L92: replace the inline switch request.authorizationMode { case .legacyTailscaleBearer... case .userAuthorizedTailscalePairing... } with a call to the shared helper, throwing tailscaleAuthorizationUnavailable on false.
  • Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxTailscaleRouteProof.swift#L153-L171: replace the equivalent switch with the same shared helper, throwing authorizationEvidenceMismatch/unsupportedAuthorizationMode on false.
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift#L588-L625: replace the .legacyTailscaleBearer/.userAuthorizedTailscalePairing arms of canSendStackBearer with a call to the same shared helper (still handling .stackBearer/.transportAdmission locally since those two arms differ in intent from the route-authorization checks).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift`
around lines 65 - 92, Introduce one shared authorization helper on
CmxTransportAuthorizationMode (or the equivalent CMUXMobileCore enum) that
validates Tailscale host, port, and expected peer device ID for both legacy
bearer and user-authorized pairing modes. Replace the duplicated checks in
Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift
lines 65-92 and
Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxTailscaleRouteProof.swift
lines 153-171, mapping false to each existing local error; update
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift lines
588-625 to use the helper for those two modes while retaining its local
stackBearer and transportAdmission handling.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/IOSBuildScopedPairedMacStore.swift`:
- Around line 496-511: The authorizeUserTailscaleRoutes implementations must
resolve the actual paired-Mac row scope before forwarding. In
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/IOSBuildScopedPairedMacStore.swift:496-511,
mirror setCustomizationUnlocked by checking scopedRows(stackUserID:teamID:) and
selecting scopedTeamID(teamID) or scopedTeamID(nil) for the matching fallback
row. In
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/TeamScopedPairedMacStore.swift:320-335,
mirror setCustomization(instanceTag:...) and remove(instanceTag:...) by calling
visibleScope(...) and forwarding scope.stackUserID and scope.teamID.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 8339-8364: The Tailscale-preference ordering algorithm is
duplicated and uses drifting authorization predicates. In
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L8339-L8364,
extract a shared static helper such as orderRoutesPreferringAuthorizedTailscale
that accepts ordered routes, Iroh routes, and an authorization closure, then
call it with the combined legacy and user-pairing authorization predicate. In
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift#L135-L171,
replace the local ordering logic with the same helper using the legacy
authorization predicate via TailscaleRoutePreference.

In `@Resources/Localizable.xcstrings`:
- Around line 132186-132218: Add localization entries for every supported locale
missing from mobile.pairing.codeMode.legacyDetail and
mobile.pairing.codeMode.useLegacy, preserving the existing en and ja entries and
matching the locale set used by Resources/Localizable.xcstrings: ar, bs, da, de,
es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift`:
- Around line 65-92: Introduce one shared authorization helper on
CmxTransportAuthorizationMode (or the equivalent CMUXMobileCore enum) that
validates Tailscale host, port, and expected peer device ID for both legacy
bearer and user-authorized pairing modes. Replace the duplicated checks in
Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift
lines 65-92 and
Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxTailscaleRouteProof.swift
lines 153-171, mapping false to each existing local error; update
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift lines
588-625 to use the helper for those two modes while retaining its local
stackBearer and transportAdmission handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 37202665-8882-4b2b-8203-ef48f2f8e77e

📥 Commits

Reviewing files that changed from the base of the PR and between 9112fe2 and b77b87d.

📒 Files selected for processing (42)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxByteTransportRequest.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxUserTailscalePairingAuthorization.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxUserTailscalePairingAuthorizationTests.swift
  • Packages/Shared/CmuxSyncStore/Tests/CmuxSyncStoreTests/CmuxSyncStoreTests.swift
  • Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift
  • Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift
  • Packages/iOS/CmuxMobilePairedMac/Tests/CmuxMobilePairedMacTests/MobilePairedMacStoreTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/HideComputersVerifierPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/IOSBuildScopedPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacCompatiblePairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacPersistence.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/TeamScopedPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/DelayedTeamPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/GatedUpsertStore.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeForgetWildcardBreadthTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PairedMacBackupTeamRoutingTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ReconnectRouteSelectionTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileConnectionMethodStore.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileConnectionMethodStoreTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileConnectionMethodSection.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileHostPickerView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingConnectionMethodPicker.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingConnectionView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingFlowView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingSceneChrome.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceMacSelectionPairedMacStore.swift
  • Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransportFactory.swift
  • Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxTailscaleRouteProof.swift
  • Packages/iOS/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportFactorySecurityTests.swift
  • Resources/Localizable.xcstrings
  • Sources/Mobile/Pairing/MobilePairingView.swift
  • ios/cmux/AppCompositionRoot.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmux/cmuxApp.swift
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift

Comment on lines +8339 to 8364
// The user's explicit Tailscale method relaxes only the Iroh pin's
// ORDER: authorized Tailscale routes dial first and Iroh remains the
// fallback. Routes without a grant or a user-entered code stay
// undialable regardless of the preference.
if connectionMethodStore?.method == .tailscale {
let authorizedTailscale = supportedRoutes.filter { route in
Self.legacyTailscaleAuthorizationEvidence(
for: route,
macDeviceID: ticket.macDeviceID,
persistedRoutes: legacyTailscaleRoutes
) != nil
|| Self.userTailscalePairingAuthorization(
for: route,
ticket: ticket,
authorizations: userTailscalePairingAuthorizations
) != nil
}
if !authorizedTailscale.isEmpty {
let rest = supportedRoutes.filter { route in
route.kind != .iroh && route.kind != .tailscale
}
return authorizedTailscale + irohRoutes + rest
}
}
return irohRoutes.isEmpty ? supportedRoutes : irohRoutes
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate Tailscale-preference route-ordering logic across two files. Both sites implement the same "authorized Tailscale routes dial first, Iroh stays as fallback, otherwise keep the exclusive Iroh pin" algorithm independently, with slightly different (overlapping) authorization predicates. This is authorization-critical routing logic; keeping two hand-synchronized copies risks the predicates drifting apart as the grant model evolves.

  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L8339-L8364: extract the if !authorizedTailscale.isEmpty { return authorizedTailscale + irohRoutes + rest } else { return irohRoutes.isEmpty ? supportedRoutes : irohRoutes } template into a shared static helper (e.g. taking ordered:, irohRoutes:, and an isAuthorizedTailscale: (CmxAttachRoute) -> Bool closure) and call it here with a predicate combining legacyTailscaleAuthorizationEvidence and userTailscalePairingAuthorization.
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift#L135-L171: call the same shared helper from storedReconnectRoutes, passing a predicate built from legacyTailscaleAuthorizationEvidence only (via TailscaleRoutePreference), so both ordering algorithms stay byte-for-byte identical going forward.
♻️ Suggested shared-helper shape
static func orderRoutesPreferringAuthorizedTailscale(
    _ ordered: [CmxAttachRoute],
    irohRoutes: [CmxAttachRoute],
    isAuthorizedTailscale: (CmxAttachRoute) -> Bool
) -> [CmxAttachRoute] {
    let authorizedTailscale = ordered.filter(isAuthorizedTailscale)
    guard !authorizedTailscale.isEmpty else {
        return irohRoutes.isEmpty ? ordered : irohRoutes
    }
    let rest = ordered.filter { $0.kind != .iroh && $0.kind != .tailscale }
    return authorizedTailscale + irohRoutes + rest
}
📍 Affects 2 files
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L8339-L8364 (this comment)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift#L135-L171
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 8339 - 8364, The Tailscale-preference ordering algorithm is
duplicated and uses drifting authorization predicates. In
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L8339-L8364,
extract a shared static helper such as orderRoutesPreferringAuthorizedTailscale
that accepts ordered routes, Iroh routes, and an authorization closure, then
call it with the combined legacy and user-pairing authorization predicate. In
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift#L135-L171,
replace the local ordering logic with the same helper using the legacy
authorization predicate via TailscaleRoutePreference.

Comment on lines +132186 to +132218
"mobile.pairing.codeMode.legacyDetail": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Tailscale code: for the Tailscale connection method and older iPhone apps. The iPhone must be on the same Tailscale network."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Tailscaleコード: Tailscale接続方法および旧バージョンのiPhoneアプリ用。iPhoneが同じTailscaleネットワークに接続されている必要があります。"
}
}
}
},
"mobile.pairing.codeMode.useLegacy": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Use Tailscale Pairing Code"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Tailscaleペアリングコードを使用"
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(Path("Resources/Localizable.xcstrings").read_text())
keys = ["mobile.pairing.codeMode.legacyDetail", "mobile.pairing.codeMode.useLegacy"]

for key in keys:
    print(key, sorted(data["strings"][key]["localizations"]))

locales = sorted({
    locale
    for entry in data["strings"].values()
    for locale in entry.get("localizations", {})
})
print("catalog locales:", locales)
PY

Repository: manaflow-ai/cmux

Length of output: 402


Add localization entries for the supported xcstrings locales.

mobile.pairing.codeMode.legacyDetail and mobile.pairing.codeMode.useLegacy only include en and ja, but Resources/Localizable.xcstrings uses these locales: ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant. Add matching entries for the missing supported locales.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/Localizable.xcstrings` around lines 132186 - 132218, Add
localization entries for every supported locale missing from
mobile.pairing.codeMode.legacyDetail and mobile.pairing.codeMode.useLegacy,
preserving the existing en and ja entries and matching the locale set used by
Resources/Localizable.xcstrings: ar, bs, da, de, es, fr, it, km, ko, nb, pl,
pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant.

Sources: Path instructions, Learnings

The body and primary button already switch to the Tailscale flow; the title
kept claiming automatic connection. Title now reads "Connect over Tailscale"
(EN+JA) while the method is selected and the Mac is not yet connected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit and others added 2 commits July 30, 2026 21:01
The scoped-store decorators forwarded the selected team verbatim, but the
base store's grant write requires an exact existing row and silently no-ops
otherwise, so a Mac whose row still lives in the team-less fallback scope
would drop the user-entered grant. Mirror the sibling exact-instance writes
(visibleScope / setCustomizationUnlocked) in both decorators.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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