Skip to content

Fix iOS discovery of sibling Mac builds - #9403

Closed
azooz2003-bit wants to merge 3 commits into
mainfrom
feat-ios-build-instances
Closed

azooz2003-bit wants to merge 3 commits into
mainfrom
feat-ios-build-instances

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary:

  • retain every zero-touch Iroh candidate after the foreground build connects
  • authenticate sibling build identity before persisting it as an inactive pairing
  • preserve the selected foreground build while secondary aggregation connects siblings

Tests:

  • regression test fails on the test-only commit and passes with the fix
  • swift test --package-path Packages/iOS/CmuxMobileShell --filter IrohZeroTouchDiscoveryTests
  • swift build --package-path Packages/iOS/CmuxMobileShell

Hosted evidence:

The fixed hosted run reaches unrelated current-main failures in package-conventions-lint and WorkspaceListTableCoordinatorDropTests. The full shell-package process also hits existing concurrent timing failures and does not exit before the job limit; the isolated changed suite passes.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes iOS zero-touch discovery so sibling Mac builds (e.g., stable + nightly) are authenticated and saved as secondary pairings without taking over the active connection. This stabilizes multi-Mac aggregation and prevents missing siblings.

  • Bug Fixes
    • Retain all zero-touch Iroh candidates except the foreground winner; track by pairing ID within CmuxMobileShell.
    • Authenticate each candidate via RPC; verify device/build and use the authenticated instance tag and display name before persisting.
    • Persist siblings as inactive (markActive: false) and preserve already-active pairings on upsert so the foreground build stays active.
    • Retry on transient failures; discard incompatible, hidden, or permanent failures; clear staged candidates on sign-out, scope change, or after aggregation completes.
    • Run authentication during secondary aggregation when new connections are allowed; reload paired Macs after successful persists.
    • Add markActive to paired-mac persistence and authenticatedDisplayName to SecondaryClientHandle.
    • Add tests, including clean-install coverage that persists multiple authenticated sibling builds.

Written for commit ce91b32. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added zero-touch authentication for eligible secondary connections discovered during reconnection.
    • Preserves authenticated display names for secondary connections.
    • Supports retaining multiple authenticated sibling connections.
  • Bug Fixes

    • Prevents already-active connections from being incorrectly deactivated during persistence.
    • Defers saving discovered connections until authentication and compatibility checks succeed.
    • Keeps temporary authentication failures eligible for retry.
  • Tests

    • Added coverage for multiple authenticated connections and route-specific discovery scenarios.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds deferred zero-touch Iroh candidate authentication during secondary aggregation. Validated candidates are persisted with controlled activation, authenticated display names are retained, and tests cover multiple sibling builds and route-specific transport behavior.

Changes

Zero-touch Iroh pairing

Layer / File(s) Summary
Controlled paired-Mac activation
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacPersistence.swift
persistPairedMacFromTicket accepts markActive. Upserts preserve existing active records and use the computed activation state.
Candidate staging and authentication
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ZeroTouchIroh.swift
Stored-Mac reconnects stage scoped candidates. Secondary aggregation authenticates them in order, handles cancellation and scope changes, retries transient failures, and discards invalid candidates.
Authenticated display propagation and coverage
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/SecondaryClientHandle.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift
Secondary handles retain authenticated display names. Tests cover multiple sibling builds, route-specific hosts, routers, and cleanup.

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

Sequence Diagram(s)

sequenceDiagram
  participant StoredMac
  participant MobileShellComposite
  participant ZeroTouchIroh
  participant PairedMacPersistence
  StoredMac->>MobileShellComposite: report discovered candidates
  MobileShellComposite->>MobileShellComposite: stage candidates by scope
  MobileShellComposite->>ZeroTouchIroh: authenticate candidates
  ZeroTouchIroh->>PairedMacPersistence: submit validated ticket
  MobileShellComposite->>MobileShellComposite: retain authenticated display name
Loading

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error ZeroTouchIroh.swift:125-205 loops pending candidates and calls persistPairedMacFromTicket; each call loads all paired rows at PairedMacPersistence.swift:44, yielding O(K·N) rescans with no bound on... Load one scoped snapshot into a pairing-ID dictionary and reuse it, or add a batch/indexed persistence operation; also enforce a bound on pending candidates or provide benchmark evidence.
Cmux Swift @Concurrent ❌ Error New @MainActor authenticateZeroTouchIrohCandidate performs background RPC authentication via makeSecondaryClient without @concurrent or an explicit off-actor hop. Move RPC/authentication work behind a nonisolated @concurrent or detached boundary, then return to @MainActor only for staged-state and persistence updates.
Cmux Swift Package Boundaries ❌ Error The diff adds a 163-line zero-touch discovery/authentication/persistence workstream to the app-facing CmuxMobileShell target, coupled to composite state and lifecycle methods. Extract the candidate policy and authentication seam into a small CmuxMobileZeroTouchIroh target; expose ZeroTouchIrohCoordinator (or its authentication protocol), and keep only lifecycle wiring in MobileShellComposite.
Cmux Cache Substitution Correctness ❓ Inconclusive Investigation is still in progress; no verdict submitted yet. Inspect the changed Swift paths and their authoritative persistence sources.
✅ Passed checks (21 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 New state and zero-touch methods are MainActor-isolated; aggregation uses Task { @MainActor }, and the added handle/model values introduce no shared mutable Sendable reference or service protocol.
Cmux Swift Blocking Runtime ✅ Passed Production diff adds no semaphores, blocking waits, sleeps, delayed dispatch, polling, main-queue sync, or locks; the only pollUntil addition is test-only, and retry uses the existing cancellable c...
Cmux Browser Automation Off-Main ✅ Passed The PR changes only iOS CmuxMobileShell files; rule-scoped browser socket files and worker-policy identifiers are unchanged.
Cmux Expensive Synchronous Load ✅ Passed The production diff adds no agent-history, transcript, JSONL, directory, or large-JSON load; its only new read awaits actor-isolated MobilePairedMacStore.loadAll, while loadPairedMacs is already as...
Cmux No Hacky Sleeps ✅ Passed The PR diff contains five Swift files and no non-Swift files. The configured rule explicitly scopes TypeScript, JavaScript, shell, and non-Swift runtime scripts.
Cmux Swift Concurrency ✅ Passed No new legacy async pattern appears in cmux production code; the flow uses async methods and an existing tracked aggregation task. The only unowned Task is moved test cleanup.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes only Swift source and tests; it changes no Package.swift, Package.resolved, .gitignore, workflow, or Xcode package-reference files, so no lockfile rule is triggered.
Cmux Swift Logging ✅ Passed The PR diff adds no print, debugPrint, dump, NSLog, ad hoc file/stdout logging, Logger declarations, or diagnostic data logging.
Cmux User-Facing Error Privacy ✅ Passed The PR diff adds no user-facing error, alert, command output, API error body, or recovery copy; production additions are internal state/logic, and tests are allowed.
Cmux Full Internationalization ✅ Passed The PR adds no user-facing copy, localization keys, catalogs, or web messages; production changes only persistence, connection state, and authenticated host display-name plumbing.
Cmux Swiftui State Layout ✅ Passed The PR changes only @Observable model/aggregation logic and tests; no new SwiftUI view, ObservableObject state, GeometryReader, lazy-row store reference, or render-time state write appears in the d...
Cmux Architecture Rethink ✅ Passed The diff adds no production sleeps, delayed dispatch, polling, locks, or observers; pending candidates stay under one MainActor owner with scope guards and teardown.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The complete PR diff changes only MobileShell persistence/aggregation and Iroh test fixtures; it adds no NSWindow, NSPanel, WindowGroup, identifier, or close-shortcut code.
Cmux Source Artifacts ✅ Passed All five changed paths are Swift source or test files; no artifact directories, logs, media, archives, caches, or build outputs appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The production diff adds no DEBUG/test guards or test-named members; new pending state, authentication methods, and metadata have production callers in aggregation and persistence flows.
Cmux No Ambient Global State ✅ Passed The production diff adds only MobileShellComposite instance methods/state, a member property, and a private outcome enum with cases; no new free API, mutable file global, static namespace, or singl...
Title check ✅ Passed The title clearly identifies the main change: fixing iOS discovery of sibling Mac builds.
Description check ✅ Passed The description explains the change, testing, hosted evidence, and known unrelated failures, but it omits the template checklist and demo video.
✨ 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-build-instances

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 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: 1

🤖 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/MobileShellComposite`+ZeroTouchIroh.swift:
- Around line 125-148: The candidate loop around
authenticateZeroTouchIrohCandidate must not return before trailing cleanup
executes. Label the loop, replace the Task.isCancelled/current-scope and
.superseded returns with breaks from that loop, and preserve the existing
cleanup afterward for clearing pendingZeroTouchIrohCandidateScope, loading
paired Macs when didPersistCandidate is true, and scheduling retries for
transientFailureMacIDs.
🪄 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: b5bf74d4-4104-4de7-b577-fdaa56585211

📥 Commits

Reviewing files that changed from the base of the PR and between 175127e and faf67ef.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacPersistence.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ZeroTouchIroh.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/SecondaryClientHandle.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift

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

Caution

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

⚠️ Outside diff range comments (2)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ZeroTouchIroh.swift (2)

102-106: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not treat a store-read failure as an empty store.

try? ... ?? [] makes storedPairingIDs empty when pairedMacStore.loadAll fails. The loop then treats every candidate as new and may persist a pairing without confirming the authoritative store state. Preserve the candidates and route the read failure through the transient retry path.

As per coding guidelines: correctness-critical state must use one reliable source and fail closed when that source is unavailable.

🤖 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`+ZeroTouchIroh.swift
around lines 102 - 106, Update the storedPairingIDs loading in the zero-touch
pairing flow to distinguish a successful empty result from a failed
pairedMacStore.loadAll read. Preserve the authoritative store-read error and
route it through the existing transient retry path, preventing candidate
persistence when the store state is unavailable; only evaluate new candidates
after a successful load.

Sources: Coding guidelines, Path instructions


194-201: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use the authenticated MAC identity for the final hidden check.

candidate.macDeviceID and candidate.instanceTag are broker discovery data, but this path already relies on handle.ticket/handle.authenticatedInstanceTag for build compatibility and persistence. Use the authenticated identity for isHiddenMacDeviceID; if it is unavailable or mismatches the dial hint, reject the candidate instead of applying hidden filtering on stale discovery data.

🤖 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`+ZeroTouchIroh.swift
around lines 194 - 201, Update the guard in the zero-touch candidate validation
flow to call isHiddenMacDeviceID with the authenticated MAC identity from
handle.ticket and handle.authenticatedInstanceTag, rather than
candidate.macDeviceID and candidate.instanceTag. Reject the candidate when the
authenticated identity is unavailable or does not match the dial hint, while
preserving the existing hidden-device filtering behavior for valid authenticated
identities.

Source: Path instructions

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

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ZeroTouchIroh.swift:
- Around line 102-106: Update the storedPairingIDs loading in the zero-touch
pairing flow to distinguish a successful empty result from a failed
pairedMacStore.loadAll read. Preserve the authoritative store-read error and
route it through the existing transient retry path, preventing candidate
persistence when the store state is unavailable; only evaluate new candidates
after a successful load.
- Around line 194-201: Update the guard in the zero-touch candidate validation
flow to call isHiddenMacDeviceID with the authenticated MAC identity from
handle.ticket and handle.authenticatedInstanceTag, rather than
candidate.macDeviceID and candidate.instanceTag. Reject the candidate when the
authenticated identity is unavailable or does not match the dial hint, while
preserving the existing hidden-device filtering behavior for valid authenticated
identities.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 337a34f6-5f80-4c77-8b74-26aaf26f9048

📥 Commits

Reviewing files that changed from the base of the PR and between faf67ef and ce91b32.

📒 Files selected for processing (1)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ZeroTouchIroh.swift

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants