Skip to content

Add privacy-safe mobile dial diagnostics - #8264

Closed
x90skysn3k wants to merge 6 commits into
manaflow-ai:mainfrom
x90skysn3k:feat-iroh-main-rebase
Closed

x90skysn3k wants to merge 6 commits into
manaflow-ai:mainfrom
x90skysn3k:feat-iroh-main-rebase

Conversation

@x90skysn3k

@x90skysn3k x90skysn3k commented Jul 16, 2026 •

Copy link
Copy Markdown

Summary

  • add ordered mobile.dial.attempt, mobile.dial.connected, and mobile.dial.failed diagnostics around the underlying transport connect lifecycle
  • report route order, redacted endpoint shape, stable failure kind, and elapsed time without logging host addresses, full EndpointIds, relay URLs/tokens, or raw transport errors
  • suppress failure diagnostics for every cancelled connect, including production-shaped cases where closing the transport causes connect() to finish with a domain error instead of CancellationError
  • make the terminal input/resync replay regression deterministic by freezing ticket time and explicitly sequencing subscription, replay, input, and replay acknowledgement
  • preserve main's authoritative authenticated CmuxIrohTransport implementation from Run cmux iOS over authenticated Iroh transport #7908; an authenticated Iroh route remains fail-closed and never downgrades to raw Tailscale

The earlier duplicate native-FFI transport implementation was dropped during the rebase because #7908 now owns production Iroh transport, broker admission, relay policy, and authenticated path-hint fallback.

Regression evidence

  • test-only commit baa62fdfb4: callerCancellationClosedErrorDoesNotEmitFailedAndAllowsRetry failed exactly because the cancelled closed connect emitted one .failed event
  • fix commit 8fb97e2648: normalizes Task.isCancelled before observer notification; the new regression and existing cancellation retry test both pass

Verification

  • swift test --package-path Packages/iOS/CmuxMobileShell --filter MobileDialDiagnosticsTests — 2 tests passed
  • focused MobileRPC cancellation run — 2 tests passed, including the production-shaped closed-error regression
  • xcodebuild test ... -only-testing:cmuxFeatureTests on an iOS Simulator at 051310aebb — 188 tests passed, including terminalInputResyncsOutputWhenMacSequenceIsAhead; final commits touch only MobileRPC cancellation diagnostics and their focused tests
  • scripts/reload.sh --tag iroh-rebase --launch at 8fb97e2648 — tagged macOS build succeeded; tag-bound CLI workspace smoke test passed
  • Release iPhone build at 8fb97e2648 succeeded, was signed, and installed on a physical iPhone 16 Pro Max; automatic launch awaits device unlock
  • phone metadata verified CMUXAuthEnvironment=production, display name cmux BETA Iroh, and Stack OAuth callback scheme stack-auth-mobile-oauth-url

Review

  • Greptile final review: 5/5, safe to merge, no files requiring attention on the reduced diagnostics diff
  • CodeRabbit and Socket Security checks completed successfully on the prior stable head
  • fresh internal integration review of origin/main...8fb97e2648 found no concrete blockers
  • no host publication, pairing persistence/backup, reconnect-route, endpoint-pinning, or settings paths are changed

@cursor

cursor Bot commented Jul 16, 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.

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

@x90skysn3k is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds Iroh as the primary mobile transport with Tailscale/LAN fallbacks. It introduces a Rust/C FFI and xcframework pipeline, shared Swift transports, Mac host lifecycle integration, iOS route selection and endpoint trust pinning, pairing QR v3 support, transport diagnostics, persistence, UI updates, tests, and CI provisioning.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (11 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error MobileHostService adds production Task.sleep timeouts and a 500ms polling accept loop; the rule forbids these non-test sync patterns in shipped Swift. Replace the timed accept loop and readiness sleeps with explicit callback/state-transition signals or a cancellable async primitive; keep locks only in low-level FFI bridges.
Cmux Cache Substitution Correctness ❌ Error MobileHostService.currentIrohRoute() falls back to cached irohRoute when routeJSON() fails, so a stale route can replace the authoritative FFI read in the snapshot path. Return nil or re-read from an authoritative source on lookup failure; if caching is kept, invalidate it by endpoint generation/freshness so stale routes cannot persist.
Cmux No Hacky Sleeps ❌ Error scripts/ensure-cmux-iroh.sh adds a polling mkdir lock with sleep 1 to wait for cache publication, which is a shared-state synchronization delay in a build/runtime script. Replace the lock polling/sleep with an OS-backed advisory lock or atomic publication flow that waits on a real owner event instead of elapsed time.
Cmux Swift Package Boundaries ❌ Error Reusable Iroh/route/pairing logic landed in app-target Sources/Mobile (MobileRouteResolver, MobileHostIrohFFI, MobilePairingModel) instead of a small SwiftPM package. Extract those domain seams into a package target (e.g. CmuxMobileHost/CmuxMobilePairing) and keep Sources/ limited to composition-root glue and lifecycle wiring.
Cmux Swiftpm Lockfiles ❌ Error cmux.xcodeproj adds XCLocalSwiftPackageReference CmuxMobileTransport/CmuxIrohC, but the root cmux.xcodeproj Package.resolved has no diff; only ios/cmux.xcworkspace lockfile changed. Add the matching root cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff for the new package reference; the ios workspace lockfile alone is insufficient.
Cmux Swift Logging ❌ Error MobileHostService.info now emits the full iroh endpoint ID with .public privacy in production, exposing a trust/pairing identifier without redaction. Redact or omit the endpoint ID (or shorten it) in the info log; keep only sanitized summaries, and use a nonisolated private logger if the file is MainActor-isolated.
Cmux User-Facing Error Privacy ❌ Error Production pairing guidance still exposes ios/scripts/reload.sh --prod-auth, an internal script path/flag in user-facing copy. Replace script paths/flags with product-level wording (e.g. 'rebuild with production auth') and keep build steps in docs/internal help.
Cmux Full Internationalization ❌ Error New iOS web copy is only in web/messages/en.json and ja.json while routing.ts lists 20 locales; new xcstrings mobile keys also only have en/ja localizations. Add matching translations for every locale in web/messages/*.json and fill all supported locales in Resources/Localizable.xcstrings for the new/changed keys.
Cmux Architecture Rethink ❌ Error MobileCoreRPCSession adds a connect observer that suspends before connectionTask is set, so concurrent callers can spawn duplicate dials and the in-flight connect loses its single owner. Move observer emission and makeTransport() into the detached task after the lease/task is installed, so one in-flight connect owns the transition and all callers join it.
Cmux No Test Or Debug Seam In Production Source ❌ Error Sources/Mobile/MobileHostService.swift adds a #if DEBUG extension with debugWaitForIrohStartupForTesting() and other *ForTesting helpers, which are production-source test seams. Move the needed observation into the test target via @testable import/internal access, or isolate any genuine debug-only API in a dedicated debug file or folder.
Cmux No Ambient Global State ❌ Error NetworkReachability.swift:26 adds public static let shared = ..., a new process-wide runtime singleton; the rule forbids new shared/default singletons for owned state. Remove the shared shim; construct NetworkReachability/ReachabilityService in the app seam and inject any ReachabilityProviding into callers that need reachability state.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
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 Changed Swift types are either actors/@mainactor UI types or @unchecked Sendable wrappers with locks/comments; I found no new implicit-MainActor or unsafe Sendable regression.
Cmux Browser Automation Off-Main ✅ Passed The only control-socket change is adding irohLaneState to MobileHostService.publicStatusPayload in TerminalController.swift; no browser.* routing or WebKit wait handling changed.
Cmux Expensive Synchronous Load ✅ Passed No changed production Swift file introduces RestorableAgentSessionIndex.load(), agent-history JSON/transcript parsing, or other heavy synchronous loads on @MainActor/interactive paths.
Cmux Algorithmic Complexity ✅ Passed The edited production code adds no new scalable nested scans or repeated sort/filter loops; the only new loops are fixed-size build steps or unchanged behavior.
Cmux Swift Concurrency ✅ Passed New async work is boundary- or lifecycle-managed: NWPathMonitor/dispatch queues, stored connection tasks, and no Combine or app-state callbacks were introduced.
Cmux Swift @Concurrent ✅ Passed No changed code adds @concurrent misuse; the new nonisolated async helpers are request-handling/off-main work and explicitly hop via Task.detached or an awaited background actor call.
Cmux Swiftui State Layout ✅ Passed Changed SwiftUI views stay snapshot-based: list rows use value models/closures, no new ObservableObject/@published, no GeometryReader/layout mutation, and no render-time state writes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No PR changes introduce or modify standalone NSWindow/NSPanel/WindowGroup code; the touched files are transport/UI plumbing and tests, not auxiliary windows.
Cmux Source Artifacts ✅ Passed Changed artifact-like paths are intentional: a packaged xcframework symlink, lockfile/workspace configs, and a governance TSV update for the package move.
Title check ✅ Passed The title matches a real, important part of the change: adding privacy-safe mobile dial diagnostics.
Description check ✅ Passed It covers the main summary, verification, and review details, but omits the template's Demo Video, Review Trigger, and Checklist sections.
✨ 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 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: 29

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.swift (1)

2149-2159: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Short-circuit the already-connected Iroh route.

This fast path only runs when activeRoute is .hostPort. With Iroh as the default, selecting the current device/instance unnecessarily enters the destructive reconnect loop. Compare peer routes against activeRoute too, ideally through one route-equivalence helper.

🤖 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 2149 - 2159, Update the existing fast path around the
connection-state check to also recognize an already-active Iroh/peer route, not
only .hostPort endpoints. Add or reuse a route-equivalence helper to compare
each candidate route with activeRoute, preserving the current host normalization
behavior, and return before the destructive reconnect loop when the selected
device and instance are already connected.
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift (1)

238-336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared tailscale-route-parsing and ticket-construction logic.

decodeLegacyTailscaleOnly and decodeCurrent duplicate the same parseHostPort → loopback-check → CmxAttachRoute synthesis loop, and an almost identical CmxAttachTicket construction block that differs only by the routes array. Any future grammar/ticket-field change now needs to be kept in sync across both functions.

♻️ Proposed extraction
 private extension CmxPairingQRCode {
+    func synthesizedTailscaleRoutes(from rawRoutes: [String]) throws -> [CmxAttachRoute] {
+        try rawRoutes.enumerated().map { index, rawRoute -> CmxAttachRoute in
+            let (host, port) = try parseHostPort(rawRoute)
+            guard !CmxLoopbackHost().matches(host) else {
+                throw MobileSyncPairingPayloadError.loopbackRouteRejected
+            }
+            return try CmxAttachRoute(
+                id: synthesizedRouteID(index: index),
+                kind: .tailscale,
+                endpoint: .hostPort(host: host, port: port),
+                priority: synthesizedRoutePriority(index: index)
+            )
+        }
+    }
+
+    func makeDecodedTicket(routes: [CmxAttachRoute], components: URLComponents) throws -> CmxAttachTicket {
+        let ticket = try CmxAttachTicket(
+            workspaceID: "",
+            terminalID: nil,
+            macDeviceID: "",
+            macDisplayName: nil,
+            macUserEmail: queryValue(named: "e", in: components),
+            macUserID: queryValue(named: "ub", in: components),
+            macPairingCompatibilityVersion: queryInt(named: "pc", in: components) ?? 0,
+            macAppVersion: queryValue(named: "av", in: components),
+            macAppBuild: queryValue(named: "ab", in: components),
+            routes: routes,
+            expiresAt: nil,
+            authToken: nil
+        )
+        try ticket.validate()
+        return ticket
+    }
+
     func decodeLegacyTailscaleOnly(_ components: URLComponents) throws -> CmxAttachTicket {
         let rawRoutes = (components.queryItems ?? [])
             .filter { $0.name == "r" }
             .compactMap(\.value)
         guard !rawRoutes.isEmpty, rawRoutes.count <= Self.maximumRouteCount else {
             throw MobileSyncPairingPayloadError.invalidURL
         }
-        let routes = try rawRoutes.enumerated().map { ... }
-        let ticket = try CmxAttachTicket(...)
-        try ticket.validate()
-        return ticket
+        return try makeDecodedTicket(routes: try synthesizedTailscaleRoutes(from: rawRoutes), components: components)
     }

Similarly, decodeCurrent would build the iroh route, call synthesizedTailscaleRoutes(from:) for the r= items, concatenate, and call makeDecodedTicket(routes:components:).

🤖 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/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift`
around lines 238 - 336, Extract the duplicated tailscale route mapping from
decodeLegacyTailscaleOnly and decodeCurrent into a shared
synthesizedTailscaleRoutes(from:) helper that preserves host/port parsing,
loopback rejection, route IDs, kinds, endpoints, and priorities. Extract the
shared CmxAttachTicket construction and validation into
makeDecodedTicket(routes:components:), then have both decoders call it with
their respective route arrays while preserving the existing iroh route handling
in decodeCurrent.
🤖 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 @.github/workflows/ios-testflight.yml:
- Around line 369-374: Split the “Provision cmux-iroh FFI” workflow step into
separate steps for running install-rust-ci.sh and ensure-cmux-iroh.sh. Remove
the manual PATH export, relying on install-rust-ci.sh to configure GITHUB_PATH
for subsequent steps, and retain the existing execution order.

In @.github/workflows/perf-activation.yml:
- Around line 174-177: Add a Rust build cache step before the “Provision
cmux-iroh FFI” step, caching ~/.cargo/registry, ~/.cargo/git, and
native/cmux-iroh/target with a key that varies by runner and relevant
dependency/build configuration. Apply the same cache setup to other workflows
that invoke ensure-cmux-iroh.sh.

In `@docs/ios-swift-mobile-plan.md`:
- Around line 43-44: Renumber the milestones in the documented rollout plan
sequentially: keep the auth/reconnect milestone as 6, then place the Iroh
default and transport diagnostics milestone after it as milestone 7.

In `@ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift`:
- Around line 349-389: Change the validation flow around pairedMacStore.loadAll
and the currentUserID() lookup so it requires non-empty authenticated user and
team scope, without falling back to ticket.macUserID. Require an existing paired
record with a non-empty authoritative irohEndpointID and explicit trust
validation, rejecting missing or mismatched IDs and unavailable pinned routes.
Remove the upsert paths that create or populate trust from ticket.routes; handle
enrollment/re-trust only through a separate explicit user-approved flow.

In `@ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift`:
- Line 27: Update the initializer and default handling for
irohEndpointTrustValidator so omitted injection uses a throwing fail-closed
validator instead of a no-op. Preserve successful validation for explicitly
supplied validators, and move any no-op behavior behind an explicitly named
insecure test/helper path.

In `@ios/scripts/cloud-testflight.sh`:
- Line 235: Update the ensure-cmux-iroh.sh invocation in the archive preparation
flow to stop suppressing failures: remove the unconditional `|| true` while
preserving the existing executable check and repository-root working directory.
A failure from ensure-cmux-iroh.sh must immediately fail the production archive
rather than allowing a stale XCFramework to be packaged.

In `@native/cmux-iroh/src/lib.rs`:
- Around line 63-71: Update runtime() and its exported extern "C" callers to
make Tokio runtime creation fallible instead of using expect. Propagate
initialization failures as CmuxIrohError with ErrorKind::Internal before
crossing the FFI boundary, while preserving the existing successful runtime
reuse behavior.

In `@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift`:
- Around line 209-247: Move the asynchronous .attempt observer call and
makeTransport() setup from the actor-isolated connection path into the detached
task created by the connection method. Assign connectionTask synchronously
immediately after acquiring the connect lease, ensuring concurrent callers join
the same task; preserve failure reporting, lease cleanup, cancellation handling,
and connectionID setup using the captured state.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ReconnectRoutes.swift:
- Around line 135-139: Update freshReconnectRoutesAfterLocalFailure to return
[CmxAttachRoute] instead of an optional, returning [] when refreshed routes are
empty. Adjust every caller to check isEmpty rather than optional binding,
preserving the existing behavior for non-empty fresh routes.
- Around line 282-291: Unify peer-route identity between reconnectRouteKey and
mergedReconnectRoutes’ local endpointKey by extracting one shared key-building
function and reusing it in both paths. Ensure the shared key applies the same
field set for .peer routes, including or excluding relayHint consistently, so
deduplication and freshReconnectRoutesAfterLocalFailure comparisons cannot
disagree.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift`:
- Around line 41-45: Update the activeTransportKind argument in
WorkspaceDetailContainer’s WorkspaceDetailView call to use the transport kind
from workspace’s per-Mac connection snapshot, rather than store.activeRoute.
Keep the workspace-specific connection status and ensure the displayed pill and
toolbar subtitle reflect the Mac that owns workspace.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView`+DerivedState.swift:
- Around line 11-18: Update selectedToolbarSubtitle to derive the terminal name
from the existing selectedTerminal property instead of resolving
store.selectedTerminalID and searching workspace.terminals directly. Preserve
the current connection-status and activeTransportKind behavior while ensuring
nil or stale selections use the same visible first-terminal fallback.

In
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift`:
- Around line 148-181: Add the missing localization catalog entries in
Localizable.xcstrings for every key used by irohTransportRow and
publishTailscaleRoutesRow, including both labels and on/off subtitles. Provide
entries for all supported locales using the catalog’s existing format and
translation conventions.

In
`@Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohByteTransport.swift`:
- Around line 136-144: Move the boundEndpointBeforeDeadline() call into the
existing error-mapping do block so any CmxIrohFailure is converted through the
bindFailed helper into CmxIrohByteTransportError.endpointBindFailed. Preserve
the elapsed-time check and timeout behavior, while ensuring callers do not
receive the raw FFI failure.
- Around line 125-135: Update CmxIrohByteTransport.connect() to track an
in-flight connection using a .connecting state or stored task, preventing
concurrent connect() and close() operations from producing duplicate or stale
.ready state. Ensure cancellation and close transition through the same
lifecycle path, and update receive() cancellation handling to set state to
.closed when releasing the FFI handle. Guarantee the handle is released exactly
once.

In
`@Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohFFIClient.swift`:
- Around line 6-16: Update CmxIrohEndpointReference and the corresponding
connection wrapper to add an idempotent deinitialization fallback that releases
the native handle when Swift ownership ends, while preserving explicit close()
behavior and synchronization with active uses. Ensure destruction cannot release
either handle more than once.

In
`@Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift`:
- Around line 48-55: Replace String-based associated values in
CmxNetworkByteTransport’s connectionFailed, receiveFailed, and sendFailed cases
with typed, localizable failure values. In CmxNetworkByteTransport.swift lines
693-709, map Network.framework failures to those typed cases without hard-coded
English or raw descriptions. In CmxNetworkRoutePinger.swift lines 54-56 and
90-92, map unexpected TCP and Iroh errors to safe typed failures while retaining
upstream details only in sanitized diagnostics.
- Around line 565-572: Update cancelSend to leave the matching sendContinuation
occupied until the underlying NWConnection.send completion callback finishes;
mark the operation canceled and resume its waiting continuation, but do not
clear the slot there. Adjust the completion handling to recognize the canceled
operation, clear the slot only after the callback, and preserve any connection
error rather than discarding it when the operation ID matches the canceled send.

In
`@Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift`:
- Around line 143-149: Update the xcframeworkURL construction in
CmxIrohByteTransportTests to target the package-local
CmuxIrohFFIBinary.xcframework path used by Package.swift, rather than the
repository-root CmuxIrohFFI.xcframework location. Keep the existing
file-existence guard and native loopback test flow unchanged once it resolves
the packaged framework correctly.

In
`@Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift`:
- Around line 68-73: Replace the scheduler-dependent synchronization in the
concurrent transport tests with explicit event signals: in
Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift:68-73,
await a receive-start signal before calling transport.close; in
Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift:220-233,
await the send-completion event directly and remove the race against the 250 ms
sleep. Keep each test’s existing transport behavior and assertions unchanged.

In `@reports.md`:
- Around line 68-70: Add a blank line after the TailscaleStatusTests.swift test
heading staleEvaluationCannotOverwriteFresherRefresh in reports.md, separating
the heading from the following list.

In `@Resources/Localizable.xcstrings`:
- Around line 123525-123537: Update the localized value for the
mobile.pairing.req.route.missing key to mention LAN alongside Iroh and
Tailscale, using wording such as “No Iroh, Tailscale, or LAN route is
available.” Apply the equivalent meaning consistently to the affected
localization.

In `@scripts/ensure-cmux-iroh.sh`:
- Around line 115-123: Replace the unbounded mkdir/sleep polling loop around
LOCK_DIR with an OS-backed advisory lock or race-safe independent temporary
build strategy followed by atomic publication. Ensure stale locks cannot block
future builds indefinitely, while preserving the cached XCFramework fast path
through link_local_xcframework and the existing successful-build behavior.
- Around line 50-62: Update hash_sources in ensure-cmux-iroh.sh to include the
build environment in the BUILD_KEY: hash Rust and Xcode toolchain versions,
configured target list, SDK versions, and effective IPHONEOS_DEPLOYMENT_TARGET
and MACOSX_DEPLOYMENT_TARGET values before hashing the source files. Reuse the
script’s existing environment and toolchain variables where available, and
preserve the current source-content hashing behavior.

In `@Sources/Mobile/MobileHostIrohSecretKeyStore.swift`:
- Around line 21-29: Make secret-key creation atomic in secretKey() by using the
store-if-absent operation and returning the persisted winner rather than
allowing concurrent generated keys to replace each other. Update the
duplicate-item handling in the persistence helper at
Sources/Mobile/MobileHostIrohSecretKeyStore.swift lines 67-87 to load and return
the established key on errSecDuplicateItem; keep replacement available only
through an explicit rotation path. Both affected sites are in the same file:
lines 21-29 require the atomic creation flow, and lines 67-87 require duplicate
handling to preserve the single persisted identity.

In `@Sources/Mobile/MobileHostService.swift`:
- Around line 1854-1861: Remove the DEBUG-only
debugWaitForIrohStartupForTesting() accessor from MobileHostService in
production Sources. Update the tests to use an internal lifecycle signal
accessible via `@testable` import or observe the existing status transition
instead, while preserving the startup-wait behavior.
- Around line 1009-1024: Use one authoritative, normalized route source for Iroh
availability: in finishIrohStartup, require a valid route before setting the
lane active or starting the accept loop; if absent, close the endpoint and fail
closed. In Sources/Mobile/MobileHostService.swift lines 1103-1112, clear and
withdraw the cached route when the current FFI lookup fails instead of returning
stale route data.
- Around line 1060-1090: Update the iroh accept loop in the service owner around
irohAcceptTask and acceptByteConnectionOffMain to use a cancellable,
close-unblocked accept operation instead of the 500 ms timeout polling. Route
endpoint closure and persistent accept failures through a generation-checked
MobileHostService transition that withdraws the active route and marks or
restarts the lane, ensuring only the owner performs lifecycle state changes and
preventing immediate error-spin log flooding.

In `@web/messages/en.json`:
- Around line 4-15: Add the iOS translation keys metaDescription, subtitle, and
byoNetwork to every locale file under web/messages, preserving each file’s
existing locale structure and providing translated values where applicable.
Ensure all locales define these keys so they do not fall back to English.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 2149-2159: Update the existing fast path around the
connection-state check to also recognize an already-active Iroh/peer route, not
only .hostPort endpoints. Add or reuse a route-equivalence helper to compare
each candidate route with activeRoute, preserving the current host normalization
behavior, and return before the destructive reconnect loop when the selected
device and instance are already connected.

In
`@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift`:
- Around line 238-336: Extract the duplicated tailscale route mapping from
decodeLegacyTailscaleOnly and decodeCurrent into a shared
synthesizedTailscaleRoutes(from:) helper that preserves host/port parsing,
loopback rejection, route IDs, kinds, endpoints, and priorities. Extract the
shared CmxAttachTicket construction and validation into
makeDecodedTicket(routes:components:), then have both decoders call it with
their respective route arrays while preserving the existing iroh route handling
in decodeCurrent.
🪄 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

Run ID: 479d205c-ae76-4410-a9dc-2fcf09bfcad9

📥 Commits

Reviewing files that changed from the base of the PR and between 25dc913 and 1ac66cd.

⛔ Files ignored due to path filters (4)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
  • cmux.xcworkspace/contents.xcworkspacedata is excluded by !**/*.xcworkspace/contents.xcworkspacedata
  • ios/cmux.xcworkspace/xcshareddata/swiftpm/Package.resolved is excluded by !**/Package.resolved
  • native/cmux-iroh/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (142)
  • .github/workflows/ci-macos-compat.yml
  • .github/workflows/ci.yml
  • .github/workflows/ios-app-store.yml
  • .github/workflows/ios-testflight.yml
  • .github/workflows/nightly.yml
  • .github/workflows/perf-activation.yml
  • .github/workflows/release.yml
  • .github/workflows/reload-build.yml
  • .github/workflows/test-depot.yml
  • .github/workflows/test-e2e.yml
  • .github/workflows/test-ios.yml
  • .github/workflows/tmux-corpus.yml
  • .gitignore
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/Shared/CmuxMobileTransport/CmuxIrohFFIBinary.xcframework
  • Packages/Shared/CmuxMobileTransport/Package.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxIrohC/CmuxIrohC.c
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxIrohC/include/CmuxIrohC.h
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohByteTransport.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohByteTransportError.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohEndpointManager.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohFFIClient.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohFailure.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohKeychainSecretStore.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohSecretKeyStore.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkRoutePinger.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/MobileIrohTransportFlag.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/NetworkInterfaceAddress.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/NetworkInterfaceAddressProviding.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/NetworkReachability.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/ReachabilityProviding.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/ReachabilityService.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/SystemNetworkInterfaceAddressProvider.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/TailscaleStatus.swift
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/TailscaleStatusMonitor.swift
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxRoutePingTests.swift
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/PingTestListener.swift
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/ReachabilityServiceTests.swift
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/TailscaleStatusTests.swift
  • Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMac.swift
  • Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore+IrohPinning.swift
  • Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore+Records.swift
  • Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift
  • Packages/iOS/CmuxMobilePairedMac/Tests/CmuxMobilePairedMacTests/MobilePairedMacStoreIrohPinTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileRPCTransportConnectEvent.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncRuntime.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientIrohTrustTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/TransportTestDoubles.swift
  • Packages/iOS/CmuxMobileShell/Package.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileChatEventSource.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure+Iroh.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AuthorizationFailure.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ManualAttachTicket.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalReplayLifecycle.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceCreateRequest.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellMacAvailabilityFailureClassifier.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/PairedMacBackupRecord.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/PairedMacBackupRecordWire.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/DialDiagnosticFailingTransport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/DialDiagnosticRecorder.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/DialDiagnosticTransportFactory.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileDialDiagnosticsTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingAttemptDeadlineTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingFailureTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ReconnectRouteSelectionTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CmxAttachTransportKind+MobileDisplay.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileMacConnectionStatus+Display.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileMacConnectionStatusPill.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileMacConnectionStatusRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingPage.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SetupHelpGateContent.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SetupHelpView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TailscaleInactiveCallout.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+DerivedState.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceListConnectionChromeTests.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/MobileCatalogSection.swift
  • Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/SettingCatalogTests.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Environment/MobilePairingRoute.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Environment/MobilePairingStatusSnapshot.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/HostSettingsActions.swift
  • Sources/Mobile/MobileHostByteConnection.swift
  • Sources/Mobile/MobileHostIrohConnectionAdapter.swift
  • Sources/Mobile/MobileHostIrohFFI.swift
  • Sources/Mobile/MobileHostIrohFlag.swift
  • Sources/Mobile/MobileHostIrohRoute.swift
  • Sources/Mobile/MobileHostIrohSecretKeyStore.swift
  • Sources/Mobile/MobileHostNWConnectionAdapter.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/MobileRouteResolver.swift
  • Sources/Mobile/Pairing/MobilePairingModel.swift
  • Sources/Mobile/Pairing/MobilePairingView.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MobileHostAuthorizationTests.swift
  • cmuxTests/MobileHostIrohSecretKeyTests.swift
  • cmuxTests/MobilePairingConnectionTransitionTests.swift
  • docs/ios-swift-mobile-plan.md
  • docs/pro-badge-options.html
  • ios/cmux-ios.xcodeproj/project.pbxproj
  • ios/cmux/AppCompositionRoot.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmux/cmuxApp.swift
  • ios/cmuxPackage/Package.swift
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
  • ios/scripts/cloud-testflight.sh
  • ios/scripts/reload.sh
  • native/cmux-iroh/.gitignore
  • native/cmux-iroh/Cargo.toml
  • native/cmux-iroh/include/cmux_iroh_ffi.h
  • native/cmux-iroh/include/module.modulemap
  • native/cmux-iroh/src/lib.rs
  • plans/feat-ios-iroh/DESIGN.md
  • reports.md
  • scripts/check-package-resolved-policy.py
  • scripts/ensure-cmux-iroh.sh
  • scripts/install-rust-ci.sh
  • scripts/reload.sh
  • scripts/setup.sh
  • web/messages/en.json
  • web/messages/ja.json

@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

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (29)
.github/workflows/ios-testflight.yml (1)

369-374: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Separate Rust installation and FFI provisioning into distinct steps.

For consistency with other workflows (such as ci.yml and nightly.yml), consider separating the Rust installation and the cmux-iroh provisioning into distinct steps. Assuming install-rust-ci.sh configures $GITHUB_PATH, this also removes the need to manually export the PATH within the same step.

♻️ Proposed refactor
-      - name: Provision cmux-iroh FFI
-        run: |
-          ./scripts/install-rust-ci.sh
-          export PATH="$HOME/.cargo/bin:$PATH"
-          ./scripts/ensure-cmux-iroh.sh
+      - name: Install Rust
+        run: |
+          ./scripts/install-rust-ci.sh
+
+      - name: Provision cmux-iroh FFI
+        run: |
+          ./scripts/ensure-cmux-iroh.sh
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      - name: Install Rust
        run: |
          ./scripts/install-rust-ci.sh

      - name: Provision cmux-iroh FFI
        run: |
          ./scripts/ensure-cmux-iroh.sh
🤖 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 @.github/workflows/ios-testflight.yml around lines 369 - 374, Split the
“Provision cmux-iroh FFI” workflow step into separate steps for running
install-rust-ci.sh and ensure-cmux-iroh.sh. Remove the manual PATH export,
relying on install-rust-ci.sh to configure GITHUB_PATH for subsequent steps, and
retain the existing execution order.
.github/workflows/perf-activation.yml (1)

174-177: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider caching the Rust build artifacts.

ensure-cmux-iroh.sh compiles the Rust crate for four different targets (aarch64-apple-darwin, x86_64-apple-darwin, aarch64-apple-ios, aarch64-apple-ios-sim). Since this step runs on every CI execution, compiling from scratch adds significant overhead.

Consider adding an actions/cache step (or using Swatinem/rust-cache) for ~/.cargo/registry, ~/.cargo/git, and native/cmux-iroh/target prior to provisioning the FFI to speed up CI runs. This applies to the other workflows utilizing this script as well.

🤖 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 @.github/workflows/perf-activation.yml around lines 174 - 177, Add a Rust
build cache step before the “Provision cmux-iroh FFI” step, caching
~/.cargo/registry, ~/.cargo/git, and native/cmux-iroh/target with a key that
varies by runner and relevant dependency/build configuration. Apply the same
cache setup to other workflows that invoke ensure-cmux-iroh.sh.
docs/ios-swift-mobile-plan.md (1)

43-44: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Renumber the milestones sequentially.

Milestone 7 appears before milestone 6. Move the rollout-defaults item after milestone 6 and number it 7.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 43-43: Ordered list item prefix
Expected: 6; Actual: 7; Style: 1/2/3

(MD029, ol-prefix)


[warning] 44-44: Ordered list item prefix
Expected: 7; Actual: 6; Style: 1/2/3

(MD029, ol-prefix)

🤖 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 `@docs/ios-swift-mobile-plan.md` around lines 43 - 44, Renumber the milestones
in the documented rollout plan sequentially: keep the auth/reconnect milestone
as 6, then place the Iroh default and transport diagnostics milestone after it
as milestone 7.

Source: Linters/SAST tools

ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift (1)

349-389: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not bootstrap endpoint trust from the route being validated.

Empty IDs currently succeed, the user scope can fall back to ticket.macUserID, and missing pins are silently created from ticket.routes. This lets pairing/reconnect input become its own trust authority before credentials are sent.

Separate explicit user-approved pairing/re-trust enrollment from validation. Background validation should require the authenticated scope and an authoritative existing pin, failing closed otherwise.

As per path instructions, “the pinned iroh EndpointId / iroh-route availability decision comes from the authoritative pinned record + explicit trust validation,” and missing signals must fail closed.

🤖 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 `@ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift` around lines
349 - 389, Change the validation flow around pairedMacStore.loadAll and the
currentUserID() lookup so it requires non-empty authenticated user and team
scope, without falling back to ticket.macUserID. Require an existing paired
record with a non-empty authoritative irohEndpointID and explicit trust
validation, rejecting missing or mismatched IDs and unavailable pinned routes.
Remove the upsert paths that create or populate trust from ticket.routes; handle
enrollment/re-trust only through a separate explicit user-approved flow.

Source: Path instructions

ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift (1)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all CMUXMobileRuntime construction sites to check whether iroh-capable
# call sites always set irohEndpointTrustValidator explicitly.
rg -nP -C3 'CMUXMobileRuntime\(' --type=swift
rg -nP -C3 '\.irohEndpointTrustValidator\s*=' --type=swift

Repository: manaflow-ai/cmux

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the runtime and RPC files first.
git ls-files 'ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift' \
             'ios/cmuxPackage/Sources/cmuxFeature/MobileCoreRPCClient.swift' \
             'ios/cmuxPackage/Sources/cmuxFeature/MobileCoreRPCSession.swift' \
             'ios/cmuxPackage/Sources/cmuxFeature' \
             | sed 's#^`#FILE` #'

echo '--- OUTLINE CMUXMobileRuntime.swift ---'
ast-grep outline ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift --view expanded || true

echo '--- OUTLINE MobileCoreRPCClient.swift ---'
ast-grep outline ios/cmuxPackage/Sources/cmuxFeature/MobileCoreRPCClient.swift --view expanded || true

echo '--- OUTLINE MobileCoreRPCSession.swift ---'
ast-grep outline ios/cmuxPackage/Sources/cmuxFeature/MobileCoreRPCSession.swift --view expanded || true

echo '--- SEARCH CMUXMobileRuntime construction sites ---'
rg -n -C3 'CMUXMobileRuntime\(' ios/cmuxPackage/Sources || true

echo '--- SEARCH trust validator assignments/usages ---'
rg -n -C3 'irohEndpointTrustValidator|supportedRouteKinds|supportedKinds|route.kind == \.iroh' ios/cmuxPackage/Sources || true

Repository: manaflow-ai/cmux

Length of output: 10430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant source sections with line numbers.
sed -n '1,260p' ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift | cat -n
echo '---'
sed -n '1,260p' ios/cmuxPackage/Sources/cmuxFeature/MobileCoreRPCClient.swift | cat -n
echo '---'
sed -n '1,260p' ios/cmuxPackage/Sources/cmuxFeature/MobileCoreRPCSession.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 11616


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the runtime file itself and any declared initializers around the validator default.
sed -n '1,220p' ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift | cat -n

echo '---'
rg -n -C4 'init\(|irohEndpointTrustValidator|supportedRouteKinds|supportedKinds' ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift || true

echo '--- ALL REFERENCES ---'
rg -n -C4 'irohEndpointTrustValidator|supportedRouteKinds|supportedKinds|route.kind == \.iroh' ios/cmuxPackage/Sources || true

Repository: manaflow-ai/cmux

Length of output: 27535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- CMUXMobileRuntime construction sites (repo-wide) ---'
rg -n -C3 'CMUXMobileRuntime\(' . || true

echo '--- explicit irohEndpointTrustValidator assignments (repo-wide) ---'
rg -n -C3 'irohEndpointTrustValidator\s*=' . || true

echo '--- route-aware transport factories / iroh support mentions ---'
rg -n -C3 'supportedKinds|CmxRouteAwareByteTransportFactory|\.iroh|iroh' ios cmuxPackage .github || true

Repository: manaflow-ai/cmux

Length of output: 47566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for path in Path('.').rglob('*.swift'):
    try:
        text = path.read_text(errors='ignore')
    except Exception:
        continue
    if 'CMUXMobileRuntime(' in text or 'irohEndpointTrustValidator' in text or '.iroh' in text:
        print(path)
PY

Repository: manaflow-ai/cmux

Length of output: 4693


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show all Swift files that mention the runtime or validator, then the matching lines.
python3 - <<'PY'
from pathlib import Path
hits = []
for path in Path('ios').rglob('*.swift'):
    try:
        text = path.read_text(errors='ignore')
    except Exception:
        continue
    if 'CMUXMobileRuntime(' in text or 'irohEndpointTrustValidator' in text or '.iroh' in text:
        hits.append(str(path))
for p in sorted(set(hits)):
    print(p)
PY

echo '--- matching lines ---'
rg -n -C2 'CMUXMobileRuntime\(|irohEndpointTrustValidator|\.iroh' ios || true

Repository: manaflow-ai/cmux

Length of output: 15276


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the runtime initializer call paths in the iOS feature tree.
rg -n -C4 'CMUXMobileRuntime\(' ios/cmuxPackage/Sources ios/cmuxPackage/Tests || true

echo '---'
rg -n -C4 'irohEndpointTrustValidator\s*=\s*Self\.makeIrohEndpointTrustValidator|irohEndpointTrustValidator\s*=' ios/cmuxPackage/Sources ios/cmuxPackage/Tests || true

Repository: manaflow-ai/cmux

Length of output: 5198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- MobileCoreRPCClient outline ---'
ast-grep outline Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift --view expanded || true

echo '--- MobileCoreRPCClient iroh/trust lines ---'
rg -n -C4 'route\.kind == \.iroh|irohEndpointTrustValidator|supportedRouteKinds|supportedKinds|send credentials|token' Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift || true

echo '--- CMUXMobileRuntime call site in app ---'
sed -n '1,120p' ios/cmux/cmuxApp.swift | cat -n

echo '--- CMUXMobileRootScene iroh validator setup ---'
sed -n '270,380p' ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 20194


Default irohEndpointTrustValidator to fail closed.
The public initializer still falls back to a no-op, so any iroh-capable caller that misses this injection will silently skip endpoint-trust checks and send credentials. Make the default throwing, and move the no-op behind an explicit insecure test/helper path.

🤖 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 `@ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift` at line 27,
Update the initializer and default handling for irohEndpointTrustValidator so
omitted injection uses a throwing fail-closed validator instead of a no-op.
Preserve successful validation for explicitly supplied validators, and move any
no-op behavior behind an explicitly named insecure test/helper path.

Source: Path instructions

ios/scripts/cloud-testflight.sh (1)

235-235: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not suppress Iroh build failures before archiving.

|| true lets the production archive continue when ensure-cmux-iroh.sh fails, potentially packaging a stale XCFramework. Skip only when intentionally supported; otherwise fail immediately.

Proposed fix
-  [[ -x "$REPO_ROOT/scripts/ensure-cmux-iroh.sh" ]] && ( cd "$REPO_ROOT" && ./scripts/ensure-cmux-iroh.sh ) || true
+  [[ -x "$REPO_ROOT/scripts/ensure-cmux-iroh.sh" ]] \
+    || die "missing scripts/ensure-cmux-iroh.sh"
+  ( cd "$REPO_ROOT" && ./scripts/ensure-cmux-iroh.sh ) \
+    || die "failed to prepare cmux Iroh FFI"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  [[ -x "$REPO_ROOT/scripts/ensure-cmux-iroh.sh" ]] \
    || die "missing scripts/ensure-cmux-iroh.sh"
  ( cd "$REPO_ROOT" && ./scripts/ensure-cmux-iroh.sh ) \
    || die "failed to prepare cmux Iroh FFI"
🤖 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 `@ios/scripts/cloud-testflight.sh` at line 235, Update the ensure-cmux-iroh.sh
invocation in the archive preparation flow to stop suppressing failures: remove
the unconditional `|| true` while preserving the existing executable check and
repository-root working directory. A failure from ensure-cmux-iroh.sh must
immediately fail the production archive rather than allowing a stale XCFramework
to be packaged.
native/cmux-iroh/src/lib.rs (1)

63-71: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files native/cmux-iroh/src/lib.rs native/cmux-iroh -z | tr '\0' '\n'

printf '\n== line count ==\n'
wc -l native/cmux-iroh/src/lib.rs

printf '\n== relevant excerpt ==\n'
sed -n '1,220p' native/cmux-iroh/src/lib.rs

Repository: manaflow-ai/cmux

Length of output: 6713


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file count ==\n'
git ls-files native/cmux-iroh/src/lib.rs | wc -l

printf '\n== excerpt lines 1-260 ==\n'
nl -ba native/cmux-iroh/src/lib.rs | sed -n '1,260p'

Repository: manaflow-ai/cmux

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail
nl -ba native/cmux-iroh/src/lib.rs | sed -n '1,260p'

Repository: manaflow-ai/cmux

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
text = Path('native/cmux-iroh/src/lib.rs').read_text()
for needle in ['runtime()', 'CmuxIrohError', 'set_error(', 'clear_error(', 'catch_unwind', 'panic']:
    print(f'\n== {needle} ==')
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            print(f'{i}: {line}')
PY

Repository: manaflow-ai/cmux

Length of output: 2738


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
lines = Path('native/cmux-iroh/src/lib.rs').read_text().splitlines()
for i,l in enumerate(lines,1):
    if 'runtime()' in l or 'fn set_error' in l or 'fn clear_error' in l or 'enum ErrorKind' in l:
        start = max(1, i-8)
        end = min(len(lines), i+25)
        print(f'\n--- lines {start}-{end} around {i} ---')
        for j in range(start, end+1):
            print(f'{j}: {lines[j-1]}')
PY

Repository: manaflow-ai/cmux

Length of output: 15580


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

lines = Path('native/cmux-iroh/src/lib.rs').read_text().splitlines()

for target in ['fn runtime()', 'fn set_error(', 'fn clear_error(']:
    print(f'\n== {target} ==')
    for i, line in enumerate(lines, 1):
        if target in line:
            start = max(1, i - 5)
            end = min(len(lines), i + 35)
            for j in range(start, end + 1):
                print(f'{j}: {lines[j-1]}')
            break

print('\n== runtime() call sites ==')
for i, line in enumerate(lines, 1):
    if 'runtime()' in line:
        print(f'{i}: {line}')
PY

Repository: manaflow-ai/cmux

Length of output: 4533


Handle Tokio runtime init failures before crossing the FFI boundary (native/cmux-iroh/src/lib.rs:63-71)

runtime() is used by several exported extern "C" functions, so expect("tokio runtime should build") can terminate the host process before CmuxIrohError is set. Make runtime creation fallible and propagate ErrorKind::Internal instead.

🤖 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 `@native/cmux-iroh/src/lib.rs` around lines 63 - 71, Update runtime() and its
exported extern "C" callers to make Tokio runtime creation fallible instead of
using expect. Propagate initialization failures as CmuxIrohError with
ErrorKind::Internal before crossing the FFI boundary, while preserving the
existing successful runtime reuse behavior.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift (1)

209-247: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Fix actor reentrancy: prevent redundant connections by moving async setup into the detached task.

Adding await transportConnectObserver?(.attempt) introduces a new actor suspension point before connectionTask is populated. If multiple RPC requests arrive concurrently while no transport exists, they will all pass the if let existing = connectionTask check, suspend at this observer, and then spawn redundant makeTransport() dials to the Mac.

Although the end of the method cleans up losing races, this still causes redundant dials and connection churn. To eliminate the race window and guarantee all concurrent callers join the exact same connection attempt, move the observer and makeTransport() inside the detached Task so you can synchronously assign connectionTask immediately after acquiring the lease.

🔒️ Proposed fix to securely capture state and run setup within the Task
-            let connectStartedAt = ContinuousClock.now
-            await transportConnectObserver?(.attempt)
-            let candidate: any CmxByteTransport
-            do {
-                candidate = try makeTransport()
-            } catch {
-                await transportConnectObserver?(
-                    .failed(
-                        error: error,
-                        elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
-                    )
-                )
-                await connectAttemptRegistry.clearFinishedConnect(lease: connectLease)
-                throw error
-            }
             connectionID = UUID()
             let transportConnectObserver = transportConnectObserver
+            let makeTransport = self.makeTransport
+            let registry = self.connectAttemptRegistry
+            let capturedLease = connectLease
             task = Task.detached {
+                let connectStartedAt = ContinuousClock.now
+                await transportConnectObserver?(.attempt)
+                let candidate: any CmxByteTransport
+                do {
+                    candidate = try makeTransport()
+                } catch {
+                    await transportConnectObserver?(
+                        .failed(
+                            error: error,
+                            elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
+                        )
+                    )
+                    await registry.clearFinishedConnect(lease: capturedLease)
+                    throw error
+                }
+
                 do {
                     let connected = try await withTaskCancellationHandler {
                         try await candidate.connect()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            connectionID = UUID()
            let transportConnectObserver = transportConnectObserver
            let makeTransport = self.makeTransport
            let registry = self.connectAttemptRegistry
            let capturedLease = connectLease
            task = Task.detached {
                let connectStartedAt = ContinuousClock.now
                await transportConnectObserver?(.attempt)
                let candidate: any CmxByteTransport
                do {
                    candidate = try makeTransport()
                } catch {
                    await transportConnectObserver?(
                        .failed(
                            error: error,
                            elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
                        )
                    )
                    await registry.clearFinishedConnect(lease: capturedLease)
                    throw error
                }

                do {
                    let connected = try await withTaskCancellationHandler {
                        try await candidate.connect()
                        return candidate
                    } onCancel: {
                        Task {
                            await candidate.close()
                        }
                    }
                    await transportConnectObserver?(
                        .connected(elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt))
                    )
                    return connected
                } catch {
                    await transportConnectObserver?(
                        .failed(
                            error: error,
                            elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
                        )
                    )
                    throw error
🤖 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/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift`
around lines 209 - 247, Move the asynchronous .attempt observer call and
makeTransport() setup from the actor-isolated connection path into the detached
task created by the connection method. Assign connectionTask synchronously
immediately after acquiring the connect lease, ensuring concurrent callers join
the same task; preserve failure reporting, lease cleanup, cancellation handling,
and connectionID setup using the captured state.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift (2)

135-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SwiftLint: prefer an empty array over [CmxAttachRoute]? for freshReconnectRoutesAfterLocalFailure.

The optional return here only ever distinguishes "nothing to try" (nil) from "a non-empty list of fresh routes" (the guard !refreshed.isEmpty else { return nil } a few lines below guarantees the non-nil case is never empty). Returning [] instead of nil and having the caller check isEmpty would remove the discouraged optional-collection pattern flagged by SwiftLint without losing any information.

🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 139-139: Prefer empty collection over optional collection

(discouraged_optional_collection)

🤖 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`+ReconnectRoutes.swift
around lines 135 - 139, Update freshReconnectRoutesAfterLocalFailure to return
[CmxAttachRoute] instead of an optional, returning [] when refreshed routes are
empty. Adjust every caller to check isEmpty rather than optional binding,
preserving the existing behavior for non-empty fresh routes.

Source: Linters/SAST tools


282-291: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Two near-duplicate route-key builders disagree on which fields identify a peer route.

reconnectRouteKey (used by reconnectRoutes's dedup and by freshReconnectRoutesAfterLocalFailure's tried-vs-fresh comparison) includes relayHint in the .peer key, while mergedReconnectRoutes's local endpointKey (used when merging ticket + stored routes) intentionally drops relayHint from the same case. Two peer routes that differ only in relayHint are therefore treated as duplicates by one dedup path and as distinct routes by the other, so a route already collapsed by mergedReconnectRoutes can reappear as two separate reconnect candidates via reconnectRoutes, and freshReconnectRoutesAfterLocalFailure's "did anything really change" comparison can spuriously report a change.

Extract a single shared key function (e.g. keep mergedReconnectRoutes's narrower field set, or intentionally include relayHint in both) and have both call sites use it.

Also applies to: 328-337

🤖 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`+ReconnectRoutes.swift
around lines 282 - 291, Unify peer-route identity between reconnectRouteKey and
mergedReconnectRoutes’ local endpointKey by extracting one shared key-building
function and reusing it in both paths. Ensure the shared key applies the same
field set for .peer routes, including or excluding relayHint consistently, so
deduplication and freshReconnectRoutesAfterLocalFailure comparisons cannot
disagree.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift (1)

41-45: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use the route owned by this workspace’s Mac.

workspace may belong to an aggregated secondary Mac, while store.activeRoute is the foreground route. Combining workspace-specific status with the foreground transport mislabels the pill and toolbar subtitle. Resolve the transport kind from the workspace’s per-Mac connection snapshot instead.

🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift`
around lines 41 - 45, Update the activeTransportKind argument in
WorkspaceDetailContainer’s WorkspaceDetailView call to use the transport kind
from workspace’s per-Mac connection snapshot, rather than store.activeRoute.
Keep the workspace-specific connection status and ensure the displayed pill and
toolbar subtitle reflect the Mac that owns workspace.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+DerivedState.swift (1)

11-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Base the subtitle on the visible terminal fallback.

Use selectedTerminal rather than independently resolving store.selectedTerminalID; nil or stale IDs currently make the subtitle disagree with the displayed first terminal.

Based on learnings, terminal UI selection must consistently use the same first-terminal fallback as selectedTerminal.

Proposed fix
 var selectedToolbarSubtitle: String? {
-    guard let selectedTerminalID = store.selectedTerminalID else { return nil }
-    let terminalName = workspace.terminals.first { $0.id == selectedTerminalID }?.name
+    guard let terminalName = selectedTerminal?.name else { return nil }
     guard connectionStatus == .connected,
           let activeTransportKind else {
         return terminalName
     }
     return activeTransportKind.mobileToolbarSubtitle(terminalName: terminalName)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    var selectedToolbarSubtitle: String? {
        guard let terminalName = selectedTerminal?.name else { return nil }
        guard connectionStatus == .connected,
              let activeTransportKind else {
            return terminalName
        }
        return activeTransportKind.mobileToolbarSubtitle(terminalName: terminalName)
🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView`+DerivedState.swift
around lines 11 - 18, Update selectedToolbarSubtitle to derive the terminal name
from the existing selectedTerminal property instead of resolving
store.selectedTerminalID and searching workspace.terminals directly. Preserve
the current connection-status and activeTransportKind behavior while ensuring
nil or stale selections use the same visible first-terminal fallback.

Source: Learnings

Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift (1)

148-181: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify new localization keys have translations for all supported locales in Localizable.xcstrings.

KEYS=(
  "settings.mobile.iOSPairingIrohTransport"
  "settings.mobile.iOSPairingIrohTransport.subtitleOn"
  "settings.mobile.iOSPairingIrohTransport.subtitleOff"
  "settings.mobile.iOSPairingPublishesTailscaleRoutes"
  "settings.mobile.iOSPairingPublishesTailscaleRoutes.subtitleOn"
  "settings.mobile.iOSPairingPublishesTailscaleRoutes.subtitleOff"
  "settings.mobile.activeTransport"
  "settings.mobile.activeTransport.subtitle"
  "settings.mobile.activeTransport.none"
  "settings.mobile.routes.empty"
  "settings.mobile.routes.title"
)

if [ ! -f "Resources/Localizable.xcstrings" ]; then
  echo "Resources/Localizable.xcstrings not found in the repository root."
  exit 0
fi

# Extract all locales defined in the catalog
LOCALES=$(jq -r '.strings | to_entries | .[0].value.localizations | keys | .[]?' Resources/Localizable.xcstrings 2>/dev/null || echo "")

if [ -z "$LOCALES" ]; then
  echo "Could not parse locales from Localizable.xcstrings."
  exit 0
fi

echo "Verifying keys against locales: $LOCALES"
for key in "${KEYS[@]}"; do
  for locale in $LOCALES; do
    HAS_LOCALE=$(jq -r ".strings[\"$key\"].localizations[\"$locale\"] // empty" Resources/Localizable.xcstrings 2>/dev/null)
    if [ -z "$HAS_LOCALE" ]; then
      echo "Missing translation for '$key' in locale '$locale'"
    fi
  done
done

Repository: manaflow-ai/cmux

Length of output: 16345


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate localization catalogs and inspect the relevant keys/locale structure.
fd -a 'Localizable.xcstrings' . || true
echo '---'
CATALOG=$(fd -a 'Localizable.xcstrings' . | head -n 1)
if [ -z "${CATALOG:-}" ]; then
  exit 0
fi

python3 - <<'PY' "$CATALOG"
import json, sys
from pathlib import Path

path = Path(sys.argv[1])
data = json.loads(path.read_text(encoding="utf-8"))
strings = data.get("strings", {})
keys = [
    "settings.mobile.iOSPairingIrohTransport",
    "settings.mobile.iOSPairingIrohTransport.subtitleOn",
    "settings.mobile.iOSPairingIrohTransport.subtitleOff",
    "settings.mobile.iOSPairingPublishesTailscaleRoutes",
    "settings.mobile.iOSPairingPublishesTailscaleRoutes.subtitleOn",
    "settings.mobile.iOSPairingPublishesTailscaleRoutes.subtitleOff",
    "settings.mobile.activeTransport",
    "settings.mobile.activeTransport.subtitle",
    "settings.mobile.activeTransport.none",
    "settings.mobile.routes.empty",
    "settings.mobile.routes.title",
]

# Collect locales from every string entry.
locales = sorted({loc for entry in strings.values() for loc in entry.get("localizations", {}).keys()})
print("Locales:", ", ".join(locales))
print()

for key in keys:
    entry = strings.get(key)
    if not entry:
        print(f"MISSING KEY: {key}")
        continue
    locs = entry.get("localizations", {})
    missing = [loc for loc in locales if loc not in locs]
    print(f"{key}: present={len(locs)}/{len(locales)} missing={len(missing)}")
    if missing:
        print("  missing:", ", ".join(missing))
    # show state for each localization if present
    for loc in locales:
        if loc in locs:
            v = locs[loc]
            state = v.get("state")
            value = v.get("stringUnit", {}).get("value")
            if value is None:
                value = v.get("variations", {})
            print(f"  {loc}: state={state!r} value={value!r}")
    print()
PY

Repository: manaflow-ai/cmux

Length of output: 1959


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact structure for one representative key to determine whether
# the earlier check is missing nested translation data or simply reading the wrong fields.
CATALOG=$(fd -a 'Localizable.xcstrings' . | head -n 1)
[ -n "${CATALOG:-}" ]

python3 - <<'PY' "$CATALOG"
import json, sys
from pathlib import Path

path = Path(sys.argv[1])
data = json.loads(path.read_text(encoding="utf-8"))
key = "settings.mobile.iOSPairingIrohTransport"
entry = data["strings"].get(key)
print(json.dumps(entry, indent=2, ensure_ascii=False)[:12000])
PY

Repository: manaflow-ai/cmux

Length of output: 159


Add the missing localization entries for these settings strings. Resources/Localizable.xcstrings does not contain the new pairing/transport keys, so they have no cataloged translations yet; add them for the catalog’s 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
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift`
around lines 148 - 181, Add the missing localization catalog entries in
Localizable.xcstrings for every key used by irohTransportRow and
publishTailscaleRoutesRow, including both labels and on/off subtitles. Provide
entries for all supported locales using the catalog’s existing format and
translation conventions.

Source: Path instructions

Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohByteTransport.swift (2)

125-135: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make connect() own the in-flight lifecycle.

connect() still suspends after the initial state check, so another connect() or close() can interleave and leave the actor with duplicate or stale .ready state. receive() also closes the FFI handle on cancellation without transitioning state to .closed, so later calls can observe a live state backed by a dead connection.

Add a .connecting state or stored in-flight task, and route cancellation/close through the same transition so the handle is released exactly once.

🤖 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/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohByteTransport.swift`
around lines 125 - 135, Update CmxIrohByteTransport.connect() to track an
in-flight connection using a .connecting state or stored task, preventing
concurrent connect() and close() operations from producing duplicate or stale
.ready state. Ensure cancellation and close transition through the same
lifecycle path, and update receive() cancellation handling to set state to
.closed when releasing the FFI handle. Guarantee the handle is released exactly
once.

136-144: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Map endpoint-binding failures into CmxIrohByteTransportError.

boundEndpointBeforeDeadline() can throw CmxIrohFailure, but this call sits outside the mapping do block, so callers receive the raw FFI failure instead of .endpointBindFailed. The existing bindFailed helper is never applied.

Proposed fix
-        let endpoint = try await boundEndpointBeforeDeadline()
+        let endpoint: CmxIrohEndpointReference
+        do {
+            endpoint = try await boundEndpointBeforeDeadline()
+        } catch let failure as CmxIrohFailure {
+            throw CmxIrohByteTransportError.bindFailed(failure)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let connectStartedAt = DispatchTime.now().uptimeNanoseconds
        let endpoint: CmxIrohEndpointReference
        do {
            endpoint = try await boundEndpointBeforeDeadline()
        } catch let failure as CmxIrohFailure {
            throw CmxIrohByteTransportError.bindFailed(failure)
        }
        let elapsed = DispatchTime.now().uptimeNanoseconds - connectStartedAt
        guard elapsed < connectTimeoutNanoseconds else {
            throw CmxIrohByteTransportError.endpointBindFailed(
                "iroh endpoint key load timed out",
                .timedOut
            )
        }
🤖 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/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohByteTransport.swift`
around lines 136 - 144, Move the boundEndpointBeforeDeadline() call into the
existing error-mapping do block so any CmxIrohFailure is converted through the
bindFailed helper into CmxIrohByteTransportError.endpointBindFailed. Preserve
the elapsed-time check and timeout behavior, while ensuring callers do not
receive the raw FFI failure.
Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohFFIClient.swift (1)

6-16: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Release native handles when Swift ownership ends.

Both wrappers release only through explicit close(). Dropping an unclosed reference leaks its native endpoint or connection. Add an idempotent destruction fallback.

Proposed fix
 final class CmxIrohEndpointReference: `@unchecked` Sendable {
+    deinit {
+        close()
+    }
 }

 final class CmxIrohConnectionReference: `@unchecked` Sendable {
+    deinit {
+        close()
+    }
 }

Also applies to: 68-78

🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 6-6: Classes should have an explicit deinit method

(required_deinit)

🤖 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/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxIrohFFIClient.swift`
around lines 6 - 16, Update CmxIrohEndpointReference and the corresponding
connection wrapper to add an idempotent deinitialization fallback that releases
the native handle when Swift ownership ends, while preserving explicit close()
behavior and synchronization with active uses. Ensure destruction cannot release
either handle more than once.
Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift (2)

48-55: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Keep implementation diagnostics behind a typed, localized error boundary.

Transport-generated English and raw error descriptions currently flow into UI-facing ping results, preventing complete localization and risking disclosure of FFI/provider internals.

  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift#L48-L55: keep public failure contracts typed rather than carrying presentation copy.
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift#L693-L709: remove hard-coded user-facing English from the Network.framework mapping.
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkRoutePinger.swift#L54-L56: map unexpected TCP errors to a safe typed failure.
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkRoutePinger.swift#L90-L92: map unexpected Iroh errors similarly and retain details only in sanitized diagnostics.

As per coding guidelines and path instructions, user-facing errors must be localized and must not expose raw upstream or internal details.

📍 Affects 2 files
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift#L48-L55 (this comment)
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift#L693-L709
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkRoutePinger.swift#L54-L56
  • Packages/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkRoutePinger.swift#L90-L92
🤖 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/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift`
around lines 48 - 55, Replace String-based associated values in
CmxNetworkByteTransport’s connectionFailed, receiveFailed, and sendFailed cases
with typed, localizable failure values. In CmxNetworkByteTransport.swift lines
693-709, map Network.framework failures to those typed cases without hard-coded
English or raw descriptions. In CmxNetworkRoutePinger.swift lines 54-56 and
90-92, map unexpected TCP and Iroh errors to safe typed failures while retaining
upstream details only in sanitized diagnostics.

Sources: Coding guidelines, Path instructions


565-572: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the send slot occupied until the canceled write completes.

Cancellation clears sendContinuation, but it does not cancel the underlying NWConnection.send. A second send can therefore overlap it, and the first callback—including a connection error—is discarded because its operation ID no longer matches.

Proposed fix
 private func cancelSend(operationID: UUID) {
     if let pending = sendContinuation, pending.id == operationID {
-        sendContinuation = nil
-        cancelledOperationIDs.insert(operationID)
+        // Keep the slot occupied until Network.framework completes the write.
+        sendContinuation = (id: pending.id, continuation: nil)
         pending.continuation?.resume(throwing: CancellationError())
     } else {
         cancelledOperationIDs.insert(operationID)
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    private func cancelSend(operationID: UUID) {
        if let pending = sendContinuation, pending.id == operationID {
            // Keep the slot occupied until Network.framework completes the write.
            sendContinuation = (id: pending.id, continuation: nil)
            pending.continuation?.resume(throwing: CancellationError())
        } else {
            cancelledOperationIDs.insert(operationID)
        }
    }
🤖 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/Shared/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift`
around lines 565 - 572, Update cancelSend to leave the matching sendContinuation
occupied until the underlying NWConnection.send completion callback finishes;
mark the operation canceled and resume its waiting continuation, but do not
clear the slot there. Adjust the completion handling to recognize the canceled
operation, clear the slot only after the callback, and preserve any connection
error rather than discarding it when the operation ID matches the canceled send.
Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift (1)

143-149: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check the packaged XCFramework path instead of silently skipping this test.

Package.swift uses package-local CmuxIrohFFIBinary.xcframework, but this checks repository-root CmuxIrohFFI.xcframework. The guard therefore bypasses the native loopback test.

Proposed fix
-        let xcframeworkURL = packageURL.appending(path: "../../../CmuxIrohFFI.xcframework").standardizedFileURL
+        let xcframeworkURL = packageURL.appending(path: "CmuxIrohFFIBinary.xcframework")
         guard FileManager.default.fileExists(atPath: xcframeworkURL.path) else {
+            Issue.record("Missing packaged Iroh XCFramework at \(xcframeworkURL.path)")
             return
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let packageURL = URL(filePath: `#filePath`)
            .deletingLastPathComponent()
            .deletingLastPathComponent()
            .deletingLastPathComponent()
        let xcframeworkURL = packageURL.appending(path: "CmuxIrohFFIBinary.xcframework")
        guard FileManager.default.fileExists(atPath: xcframeworkURL.path) else {
            Issue.record("Missing packaged Iroh XCFramework at \(xcframeworkURL.path)")
            return
🤖 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/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift`
around lines 143 - 149, Update the xcframeworkURL construction in
CmxIrohByteTransportTests to target the package-local
CmuxIrohFFIBinary.xcframework path used by Package.swift, rather than the
repository-root CmuxIrohFFI.xcframework location. Keep the existing
file-existence guard and native loopback test flow unchanged once it resolves
the packaged framework correctly.
Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift (1)

68-73: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make concurrent transport tests event-driven rather than scheduler-dependent.

Both tests can pass or fail based on task scheduling instead of the intended transport invariant.

  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift#L68-L73: await an explicit receive-start signal before closing.
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift#L220-L233: await the send event without racing it against a 250 ms sleep.

As per coding guidelines, tests must use real completion signals rather than fixed-duration or scheduler-dependent waits.

📍 Affects 2 files
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift#L68-L73 (this comment)
  • Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift#L220-L233
🤖 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/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift`
around lines 68 - 73, Replace the scheduler-dependent synchronization in the
concurrent transport tests with explicit event signals: in
Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift:68-73,
await a receive-start signal before calling transport.close; in
Packages/Shared/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxIrohByteTransportTests.swift:220-233,
await the send-completion event directly and remove the race against the 250 ms
sleep. Keep each test’s existing transport behavior and assertions unchanged.

Source: Coding guidelines

reports.md (1)

68-70: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a blank line after the test heading.

Insert an empty line after Line 70 so the Markdown heading is separated from the following list.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 70-70: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 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 `@reports.md` around lines 68 - 70, Add a blank line after the
TailscaleStatusTests.swift test heading
staleEvaluationCannotOverwriteFresherRefresh in reports.md, separating the
heading from the following list.

Source: Linters/SAST tools

Resources/Localizable.xcstrings (1)

123525-123537: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include LAN in the missing-route message.

The product supports Tailscale/LAN fallback routes, but this string says no route is available when only an LAN route may be present. Use “No Iroh, Tailscale, or LAN route is available” or a generic “No fallback route is available” wording.

🤖 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 123525 - 123537, Update the
localized value for the mobile.pairing.req.route.missing key to mention LAN
alongside Iroh and Tailscale, using wording such as “No Iroh, Tailscale, or LAN
route is available.” Apply the equivalent meaning consistently to the affected
localization.
scripts/ensure-cmux-iroh.sh (2)

50-62: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include the build environment in BUILD_KEY.

The cache currently survives Rust/Xcode/SDK or deployment-target changes, allowing an incompatible static archive to be reused. Hash the toolchain versions, target list, SDK versions, and effective IPHONEOS_DEPLOYMENT_TARGET/MACOSX_DEPLOYMENT_TARGET.

🤖 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 `@scripts/ensure-cmux-iroh.sh` around lines 50 - 62, Update hash_sources in
ensure-cmux-iroh.sh to include the build environment in the BUILD_KEY: hash Rust
and Xcode toolchain versions, configured target list, SDK versions, and
effective IPHONEOS_DEPLOYMENT_TARGET and MACOSX_DEPLOYMENT_TARGET values before
hashing the source files. Reuse the script’s existing environment and toolchain
variables where available, and preserve the current source-content hashing
behavior.

115-123: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Replace the unbounded polling lock.

If a builder is killed before cleanup, LOCK_DIR remains and every future build sleeps forever. Use an OS-backed advisory lock or race-safe independent temporary builds with atomic publication.

As per path instructions, “For production runtime, script, and build changes, flag fixed sleeps, timers, delayed dispatch, polling, or wall-clock waits used as synchronization.”

🤖 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 `@scripts/ensure-cmux-iroh.sh` around lines 115 - 123, Replace the unbounded
mkdir/sleep polling loop around LOCK_DIR with an OS-backed advisory lock or
race-safe independent temporary build strategy followed by atomic publication.
Ensure stale locks cannot block future builds indefinitely, while preserving the
cached XCFramework fast path through link_local_xcframework and the existing
successful-build behavior.

Source: Path instructions

Sources/Mobile/MobileHostIrohSecretKeyStore.swift (1)

21-29: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make secret-key creation atomic so the persisted EndpointId cannot change during concurrent startup.

Two callers can both load nil, generate different keys, and return different values because the duplicate-add path overwrites the first key. A detached startup surviving cancellation can therefore replace the key backing an already-bound endpoint and invalidate client pins.

  • Sources/Mobile/MobileHostIrohSecretKeyStore.swift#L21-L29: use an atomic store-if-absent operation that returns the persisted winner.
  • Sources/Mobile/MobileHostIrohSecretKeyStore.swift#L67-L87: on errSecDuplicateItem, load and return the established key; reserve replacement for an explicit rotation path.

As per coding guidelines, persisted identity must have one owner and invalid shared-state combinations must not be representable.

📍 Affects 1 file
  • Sources/Mobile/MobileHostIrohSecretKeyStore.swift#L21-L29 (this comment)
  • Sources/Mobile/MobileHostIrohSecretKeyStore.swift#L67-L87
🤖 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 `@Sources/Mobile/MobileHostIrohSecretKeyStore.swift` around lines 21 - 29, Make
secret-key creation atomic in secretKey() by using the store-if-absent operation
and returning the persisted winner rather than allowing concurrent generated
keys to replace each other. Update the duplicate-item handling in the
persistence helper at Sources/Mobile/MobileHostIrohSecretKeyStore.swift lines
67-87 to load and return the established key on errSecDuplicateItem; keep
replacement available only through an explicit rotation path. Both affected
sites are in the same file: lines 21-29 require the atomic creation flow, and
lines 67-87 require duplicate handling to preserve the single persisted
identity.

Source: Coding guidelines

Sources/Mobile/MobileHostService.swift (3)

1009-1024: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Derive the active lane and advertised route from one authoritative current route.

Startup marks the lane active even when no route was obtained, while later lookup returns stale cached route data after an authoritative FFI lookup failure.

  • Sources/Mobile/MobileHostService.swift#L1009-L1024: require a valid normalized route before transitioning to .active; otherwise close the endpoint and fail closed.
  • Sources/Mobile/MobileHostService.swift#L1103-L1112: clear and withdraw the cached route when current lookup fails instead of returning the previous value.

As per path instructions, Iroh route availability must use one authoritative source and fail closed when that source is absent.

📍 Affects 1 file
  • Sources/Mobile/MobileHostService.swift#L1009-L1024 (this comment)
  • Sources/Mobile/MobileHostService.swift#L1103-L1112
🤖 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 `@Sources/Mobile/MobileHostService.swift` around lines 1009 - 1024, Use one
authoritative, normalized route source for Iroh availability: in
finishIrohStartup, require a valid route before setting the lane active or
starting the accept loop; if absent, close the endpoint and fail closed. In
Sources/Mobile/MobileHostService.swift lines 1103-1112, clear and withdraw the
cached route when the current FFI lookup fails instead of returning stale route
data.

Source: Path instructions


1060-1090: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Route accept-loop termination through the service owner instead of polling.

The 500 ms timed accept continuously polls. Worse, .endpointClosed exits while the service still advertises an active endpoint, and persistent errors immediately spin and flood logs. Use a cancellable/close-unblocked accept contract and a generation-checked owner transition that withdraws the route and marks or restarts the lane.

As per coding guidelines, runtime lifecycle synchronization must not rely on polling, and state transitions need one explicit owner.

🤖 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 `@Sources/Mobile/MobileHostService.swift` around lines 1060 - 1090, Update the
iroh accept loop in the service owner around irohAcceptTask and
acceptByteConnectionOffMain to use a cancellable, close-unblocked accept
operation instead of the 500 ms timeout polling. Route endpoint closure and
persistent accept failures through a generation-checked MobileHostService
transition that withdraws the active route and marks or restarts the lane,
ensuring only the owner performs lifecycle state changes and preventing
immediate error-spin log flooding.

Sources: Coding guidelines, Path instructions


1854-1861: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the new test-only lifecycle seam from production Sources.

debugWaitForIrohStartupForTesting() adds a DEBUG-only, test-named production API. Expose an internal lifecycle signal through @testable import, or observe the existing status transition from the test target.

As per path instructions, production Sources/ files must not add test/debug-only accessors.

🤖 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 `@Sources/Mobile/MobileHostService.swift` around lines 1854 - 1861, Remove the
DEBUG-only debugWaitForIrohStartupForTesting() accessor from MobileHostService
in production Sources. Update the tests to use an internal lifecycle signal
accessible via `@testable` import or observe the existing status transition
instead, while preserving the startup-wait behavior.

Sources: Coding guidelines, Path instructions

web/messages/en.json (1)

4-15: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if updated keys in en.json exist in other locale files.

# Extract a sample of updated keys (e.g., ios.metaDescription, ios.subtitle, ios.byoNetwork)
KEYS=("metaDescription" "subtitle" "byoNetwork")

for locale_file in web/messages/*.json; do
  if [[ "$locale_file" == "web/messages/en.json" ]]; then continue; fi
  echo "Checking $locale_file..."
  for key in "${KEYS[@]}"; do
    if ! jq -e ".ios.\"$key\"" "$locale_file" >/dev/null 2>&1; then
      echo "Missing key: ios.$key in $locale_file"
    fi
  done
done

Repository: manaflow-ai/cmux

Length of output: 3679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map supported locales and inspect the ios namespace in the English catalog.
python3 - <<'PY'
import json, pathlib

routing = json.loads(pathlib.Path("web/i18n/routing.ts").read_text())
print("routing.ts exists:", pathlib.Path("web/i18n/routing.ts").exists())
PY

echo "--- supported locales ---"
sed -n '1,220p' web/i18n/routing.ts

echo "--- en.json ios keys ---"
python3 - <<'PY'
import json, pathlib
data = json.loads(pathlib.Path("web/messages/en.json").read_text())
ios = data.get("ios", {})
for k in ios.keys():
    print(k)
PY

echo "--- sample locale structure (ja.json) ---"
python3 - <<'PY'
import json, pathlib
data = json.loads(pathlib.Path("web/messages/ja.json").read_text())
ios = data.get("ios", {})
print("ios keys:", list(ios.keys())[:20])
print("has metaDescription:", "metaDescription" in ios)
print("has subtitle:", "subtitle" in ios)
print("has byoNetwork:", "byoNetwork" in ios)
PY

Repository: manaflow-ai/cmux

Length of output: 754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- supported locales ---"
sed -n '1,220p' web/i18n/routing.ts

echo "--- en.json ios keys ---"
python3 - <<'PY'
import json, pathlib
data = json.loads(pathlib.Path("web/messages/en.json").read_text())
ios = data.get("ios", {})
for k in ios.keys():
    print(k)
PY

echo "--- sample locale structure (ja.json) ---"
python3 - <<'PY'
import json, pathlib
data = json.loads(pathlib.Path("web/messages/ja.json").read_text())
ios = data.get("ios", {})
print("ios keys:", list(ios.keys())[:20])
print("has metaDescription:", "metaDescription" in ios)
print("has subtitle:", "subtitle" in ios)
print("has byoNetwork:", "byoNetwork" in ios)
PY

Repository: manaflow-ai/cmux

Length of output: 1962


Add the new iOS strings to every locale file.
ios.metaDescription, ios.subtitle, and ios.byoNetwork are missing from most web/messages/*.json locale files, so those locales will fall back to English.

🤖 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 `@web/messages/en.json` around lines 4 - 15, Add the iOS translation keys
metaDescription, subtitle, and byoNetwork to every locale file under
web/messages, preserving each file’s existing locale structure and providing
translated values where applicable. Ensure all locales define these keys so they
do not fall back to English.

Source: Path instructions

@x90skysn3k
x90skysn3k force-pushed the feat-iroh-main-rebase branch from 1ac66cd to 4a50a3c Compare July 16, 2026 11:02
@cursor

cursor Bot commented Jul 16, 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.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds ordered mobile.dial.attempt / mobile.dial.connected / mobile.dial.failed diagnostics around the transport connect lifecycle, with privacy-safe endpoint summaries and stable failure-kind strings. It also replaces two Task.sleep polling loops in the terminal-resync regression test with proper async stream iteration and deterministic sequencing.

  • MobileRPCTransportConnectEvent + MobileCoreRPCSession changes: the connect task now emits .attempt unconditionally, then either .connected or .failed; cancellation (direct CancellationError or non-cancellation error thrown while Task.isCancelled is set) correctly skips the .failed observer, resolving the spurious failure counts described in the previous thread.
  • MobileShellComposite changes: injects a dialLog closure (defaults to MobileDebugLog.anchormux) and wires a per-route transportConnectObserver that logs route index, redacted endpoint shape, stable failure kind, and elapsed milliseconds — without exposing host addresses, full endpoint IDs, relay tokens, or raw error messages.
  • cmuxFeatureTests change: removes all Task.sleep synchronization from terminalInputResyncsOutputWhenMacSequenceIsAhead, freezes ticket time at test start, and uses the async output iterator with explicit replay acknowledgement to produce a fully deterministic sequence.

Confidence Score: 5/5

Safe to merge; the diagnostic plumbing is purely additive and the cancellation fix is well-covered by new tests.

The cancellation guard — which was the one correctness concern raised in the previous round — is now handled by two independent checks: a direct catch is CancellationError rethrow and a Task.isCancelled guard in the general catch. Both paths are exercised by the new callerCancellationClosedErrorDoesNotEmitFailedAndAllowsRetry test. The dial-diagnostic log lines are built from pre-captured, privacy-scrubbed locals rather than from the raw transport error or host address. The Task.sleep removal in the feature test is a strict improvement with no production-code side effects.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileRPCTransportConnectEvent.swift New Sendable enum for transport lifecycle events; clean, minimal public API surface with no issues.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift Rewires connect task to emit attempt/connected/failed events; correctly skips .failed for both direct CancellationError and errors thrown when Task.isCancelled is set; elapsed-time calculation is correct.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift Plumbs transportConnectObserver through the public init to the session; no issues.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds injected dialLog defaulting to MobileDebugLog.anchormux; attaches observer per route iteration with privacy-safe endpoint summary; both mobileDialEndpointSummary and mobileDialFailureKind are nonisolated static helpers with no global state.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileDialDiagnosticsTests.swift Two focused integration tests verifying fail-closed Iroh behavior and fallback route ordering diagnostics; no timing sleeps, properly uses async iteration.
Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCAbandonedConnectTests.swift Extends existing cancellation tests and adds callerCancellationClosedErrorDoesNotEmitFailedAndAllowsRetry covering the subtle cancel-then-close path; verifies no spurious .failed events and successful retry.
ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift Replaces two polling Task.sleep loops with proper async stream iteration and explicit acknowledgement sequencing; removes TerminalOutputCollector and makes the test deterministic.

Reviews (3): Last reviewed commit: "fix(mobile-rpc): suppress cancelled conn..." | Re-trigger Greptile

Comment on lines +256 to 265
} catch {
await transportConnectObserver?(
.failed(
error: error,
elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
)
)
throw error
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Cancellation reported as transport failure in dial diagnostics

When tearDown cancels the in-flight connection task, Task.checkCancellation() or candidate.connect() throws CancellationError, which falls into the catch block and calls transportConnectObserver?(.failed(error: CancellationError, ...)). At the consumer side, mobileDialFailureKind has no CancellationError branch, so every intentional cancellation is logged as failure_kind=transport_error:CancellationError. This inflates observed transport-failure counts in the dial log whenever the user navigates away or a session tears down mid-connect. The fix is to rethrow without calling the observer when the error is a cancellation, so .attempt without a corresponding .connected or .failed implicitly signals that the attempt was abandoned.

Suggested change
} catch {
await transportConnectObserver?(
.failed(
error: error,
elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
)
)
throw error
}
}
} catch {
if !(error is CancellationError) {
await transportConnectObserver?(
.failed(
error: error,
elapsedMilliseconds: Self.elapsedMilliseconds(since: connectStartedAt)
)
)
}
throw error
}

@x90skysn3k x90skysn3k changed the title Enable production Iroh transport Add privacy-safe mobile dial diagnostics Jul 16, 2026
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.

2 participants