Skip to content

Add trusted manual-host mobile pairing - #7238

Closed
austinywang wants to merge 119 commits into
mainfrom
issue-7230-trusted-manual-host
Closed

austinywang wants to merge 119 commits into
mainfrom
issue-7230-trusted-manual-host

Conversation

@austinywang

@austinywang austinywang commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add an explicit manual-host transport kind and QR grammar support so the Mac can advertise a configured LAN/DNS route when Tailscale is unavailable
  • add iOS per-host manual-route trust approval scoped by host, port, and Stack user before Stack credentials can ride a plaintext LAN/subnet-router route
  • update Mac/iOS pairing UI, settings, route labels, localization, reconnect handling, and behavior tests for manual-host pairing

Demo / Dogfood

  • Tagged Debug build passed with ./scripts/reload.sh --tag issue-7230-trusted on the latest pushed head.
  • App path printed by reload: /Users/austinwang/Library/Developer/Xcode/DerivedData/cmux-issue-7230-trusted/Build/Products/Debug/cmux DEV issue-7230-trusted.app
  • Not launched from this closeout loop; ready for user dogfood.

Review Trigger

Checklist

Local Proof

  • Latest pushed head: a21f6672edb4e37a1f1729c26a33f9784308ff5c
  • Latest regression pair: 08870c0511 adds the cancelled manual-host approval regression; a21f6672ed fixes it
  • Merged current origin/main at a1c5295dd0e14808d299a16d89c947669a265d51 without conflicts; explicit merge-tree check clean
  • arch -arm64 swift test --package-path Packages/iOS/CmuxMobileShell — passed, 407 tests
  • ./scripts/reload.sh --tag issue-7230-trusted — passed
  • git diff --check — passed
  • scripts/check-pbxproj.sh — passed
  • python3 scripts/check-workspace-package-groups.py --check — passed
  • python3 scripts/check-package-resolved-policy.py — passed
  • python3 scripts/swift_file_length_budget.py --base-ref origin/main — passed
  • /Users/austinwang/manaflow/cmuxterm-hq/skills/review/autoreview/scripts/cmux-policy-check --mode local --base origin/main — passed before commit
  • /Users/austinwang/manaflow/cmuxterm-hq/skills/review/autoreview/scripts/cmux-policy-check --mode branch --base origin/main — passed after commit
  • /Users/austinwang/manaflow/cmuxterm-hq/skills/autoreview/scripts/autoreview --mode branch --base origin/main — passed with no accepted/actionable findings; cmux policy clean
  • arch -arm64 swift test --package-path Packages/iOS/CmuxMobileShellUI --filter PairingViewPendingApprovalTests... — not runnable locally because the standalone UI package resolves as macOS 10.13 and conflicts with macOS 14 package dependencies; CI iOS/package jobs are the executable gate for that target

CI / Review Status

  • GitHub Actions/Vercel restarted on a21f6672edb4e37a1f1729c26a33f9784308ff5c
  • Passing: all iOS package/simulator jobs, workflow guard, Swift package tests, aggregate tests, app-host unit tests (1/4)-(4/4), tests-build-and-lag, activation checks, Socket, CodeRabbit, Cursor Bugbot, Greptile Review, Linux preflight, and Vercel preview comments
  • Pending/in progress: release-build, Vercel – cmux, and Vercel – cmux-staging
  • No CI failures are reported on this head as of this PR body update

Localization Audit

  • Latest closeout commits add no new user-facing strings.
  • Earlier manual-host UI/settings copy remains covered by the existing English and Japanese localization entries changed in this PR.

Closes #7230
Related #5379
Related #6700

Summary by CodeRabbit

  • New Features
    • Added manual-host transport support, including manual-host routes in iOS pairing and dynamic pairing-QR grammar v3 (m=) alongside the minimal form.
    • Introduced manual-host trust approvals with a new in-memory/persistent trust model and UI flow (“Trust and Pair”).
    • Added “Manual Host” settings and expanded route reachability display to include manual hosts.
  • Security
    • Manual-host connections require explicit, expiring approval before sending Stack credentials.
  • Bug Fixes
    • Tightened manual-host normalization/validation, loopback rejection, version compatibility, and route compatibility precedence.
  • Tests
    • Expanded QR, routing, trust persistence/expiration, and auth-gating coverage.

Note

High Risk
Changes mobile pairing transport, QR grammar, and when Stack tokens may be sent over untrusted networks—security-critical auth and credential-handling paths with broad shell/RPC surface area.

Overview
Adds explicit manual-host mobile routes (LAN/DNS outside Tailscale) end-to-end: new manual_host transport kind, strict host normalization (CmxManualHost / parser), pairing QR v3 with m= routes (v2 stays Tailscale-only), and manual-entry helpers including IPv6 bracketing.

On iOS, Stack credentials on plaintext manual hosts require per-host, per-user trust approval with persisted, expiring trust. The RPC layer revalidates trust and auth scopes at enqueue and send time (scoped token gates, writer authorization) so revoked trust or account/network boundaries cannot leak tokens on queued writes. The shell drives approval UI, reapproval on network/foreground boundaries, Mac-switch/workspace-open flows, and localized insecureManualRoute errors.

Tailscale and loopback remain the secure default; manual hosts are fail-closed unless explicitly approved.

Reviewed by Cursor Bugbot for commit 769ae4a. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 14, 2026 11:33pm
cmux-staging Building Building Preview, Comment Jul 14, 2026 11:33pm

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds manual-host pairing routes with QR v3 support, normalized host parsing, Mac route advertisement, scoped iOS trust approval, auth-scope-aware RPC sending, reconnect/recovery handling, pairing UI, settings, localization, and extensive tests.

Changes

Manual-host pairing

Layer / File(s) Summary
Shared route and QR contracts
Packages/Shared/CMUXMobileCore/...
Adds the manualHost transport kind, normalized host parsing, IPv6 formatting, route validation, and v3 pairing QR encoding/decoding with m= routes while retaining v2 compatibility.
Route advertisement and pairing presentation
Sources/Mobile/..., Packages/macOS/..., Resources/Localizable.xcstrings
Adds configurable Manual Host settings, validates and advertises manual-host routes, and exposes route reachability and endpoint lines in the pairing UI.
Trust persistence and RPC authorization
Packages/iOS/CmuxMobileShellModel/..., Packages/iOS/CmuxMobileRPC/...
Adds endpoint/account-scoped trust storage, expiration handling, auth scopes, per-scope token gates, and token-bearing send validation.
iOS connection flow and UI
Packages/iOS/CmuxMobileShell/..., Packages/iOS/CmuxMobileShellUI/...
Queues approval before credential-bearing RPCs, resumes approved pairing and reconnect flows, handles network-boundary revocation, and presents trust warnings.
Validation coverage
Packages/**/Tests/..., cmuxTests/..., ios/cmuxPackage/Tests/...
Adds coverage for QR compatibility, route selection, trust isolation and expiration, RPC authorization, pairing cancellation, recovery, and UI behavior.

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

Possibly related issues

  • #8324: Updates overlap the flaky MobilePairingAttemptDeadlineTests.immediatePairingRetryDoesNotStartSecondStuckConnect test.

Possibly related PRs

Suggested reviewers: azooz2003-bit, lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error MobileShellComposite+ManualHostTrust.swift adds production ContinuousClock().sleep(for:) waits for trust expiration, which is timing-based sync in shipped code. Replace the sleeps with a cancellation-aware timer/expiration callback or async signal from the trust store; don’t park runtime tasks on deadline waits.
Cmux Swift Package Boundaries ❌ Error FAIL: Sources/Mobile/MobileRouteResolver.swift adds reusable route-planning logic in the app target, and the pbxproj wires it into the cmux app instead of a SwiftPM package. Move MobileRouteResolver (and MobileHostRouteSnapshot) into Packages/iOS/CmuxMobileTransport or a small new routing package; keep only MobileHostService glue in the app target.
Cmux Swift Logging ❌ Error New MainActor source files add file-scoped Loggers as plain private let (ManualHostConnection, WorkspaceOpening, MobileHostService+NetworkStatus), not nonisolated private let. Change those loggers to nonisolated private let or move them out of MainActor-isolated scope, and keep any logged values redacted/sanitized.
Cmux User-Facing Error Privacy ❌ Error Changed end-user copy includes the upstream vendor name "Iroh" in PairingView/localized strings, which the rule forbids in production user-facing text. Reword the help/warning strings to use cmux/product terms only; keep Iroh-specific wording in developer-only docs or user-configured advanced help.
Cmux Architecture Rethink ❌ Error MobileShellComposite adds pendingNetworkRecoveryTrigger and drain logic beside connectionRecoveryOwner, creating a parallel recovery owner/side channel. Move the queued network-change state into MobileConnectionRecoveryOwner (or remove the queue) so one state machine owns recovery and cancellation.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive placeholder2 need code evidence
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main feature: trusted manual-host mobile pairing.
Description check ✅ Passed The description mostly follows the template, covering summary, testing proof, review trigger, and checklist.
Linked Issues check ✅ Passed The changes implement the manual-host pairing flow, warning approval, scoped trust, and supporting UI/tests requested by #7230.
Out of Scope Changes check ✅ Passed The diff is broadly focused on manual-host pairing and its supporting auth, UI, settings, and tests, with no clear unrelated feature work.
Cmux Browser Automation Off-Main ✅ Passed No diff touches the rule-scoped browser-automation files or related browser command routing, so this check isn’t implicated.
Cmux Expensive Synchronous Load ✅ Passed No changed production code adds a sync agent-history/file-scan load; searches found no RestorableAgentSessionIndex/transcript/trajectory refs, and new main-actor code is just UserDefaults+route nor...
Cmux Cache Substitution Correctness ✅ Passed The new route/public-status caches are event-driven or TTL-checked, and manual-host trust reloads from defaults with expiry validation, so no unhandled cold/stale substitution.
Cmux No Hacky Sleeps ✅ Passed No changed TS/JS/shell/runtime-script files were present; the diff is Swift/resources/pbxproj only, so the no-hacky-sleeps rule is not implicated.
Cmux Algorithmic Complexity ✅ Passed No new nested rescans over scalable collections were introduced; the added scans are on tiny route lists or existing single-pass/batched patterns.
Cmux Swift Concurrency ✅ Passed No new Combine/DispatchQueue/completion-handler APIs were added; new Tasks are either stored lifecycle tasks or callback-boundary hops, not fire-and-forget work.
Cmux Swift @Concurrent ✅ Passed The only new nonisolated async helper uses @concurrent, and the other added async work stays intentionally on @MainActor or inside actors.
Cmux Swiftpm Lockfiles ✅ Passed PASS: pbxproj only adds source-file build entries; there are no Package.swift or Package.resolved diffs, no SwiftPM package-reference changes, and no cmux .gitignore ignores Package.resolved.
Cmux Full Internationalization ✅ Passed All new user-facing strings use localized APIs, and both touched string catalogs have matching translations for every supported locale.
Cmux Swiftui State Layout ✅ Passed Changed SwiftUI views only add manual-host warning UI/callbacks; no new ObservableObject/@published, GeometryReader layout changes, lazy row store refs, or render-time state writes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone cmux-owned windows were added or materially changed; only the main WindowGroup and non-window route/UI code were touched.
Cmux Source Artifacts ✅ Passed Changed paths are all source/tests/localization/config; none are logs, build output, caches, DerivedData, or scratch dirs forbidden by the rule.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new production-source test/debug seam was added; existing DEBUG/ForTesting helpers were pre-existing, and the PR only added product-facing manual-host support plus preview scaffolding.
Cmux No Ambient Global State ✅ Passed No new file-scope API or singleton/namespace-only state was added; new manual-host behavior lives on injectable instance types like actors/classes/extensions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7230-trusted-manual-host

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.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements trusted manual-host mobile pairing as a fallback when Tailscale is unavailable in subnet-router topologies. It introduces a new manualHost transport kind, QR grammar v3 (m= field alongside r=), per-host trust approval scoped by host/port/Stack user, session-bounded expiring trust stored in UserDefaults, and a full UI/settings layer on both Mac and iOS.

  • Trust gate: Stack credentials are blocked on .manualHost routes until the user explicitly approves the host/port for the signed-in Stack user; MobileCoreRPCClient re-checks trust on every authenticated request, and all trust is cleared on network-path changes.
  • Route prioritization: Tailscale routes (WireGuard-encrypted) are always preferred over manual-host routes in MobileShellRouteSelection; manual hosts are only dialled when no Tailscale route succeeds.
  • Backward compatibility: QR v2 codes are still decoded as Tailscale-only (the m= manual-host parameter is rejected on v2), and the loopback rejection is preserved across all paths.

Confidence Score: 4/5

Safe to merge once open threads from the prior review round are addressed; the trust gate, route-priority ordering, and QR grammar changes are all correctly implemented.

The new trust path is correctly implemented end-to-end with double trust checks, network-change invalidation, and session-scoped expiry. Localization is complete across all 20 supported locales. Two prior review thread items remain unaddressed on this head.

Prior review comments on CmxManualHost.swift (bare-colon host validation) and MobileShellComposite.swift (compiler-conditional deinit) are still open.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/UserDefaultsMobileManualHostTrustStore.swift New session-scoped trust store: approvals keyed to a random UUID per app launch with 10-minute in-session expiry. Encoding/unescaping of pipe and percent uses correct ordering. Actor isolation preserved throughout.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellRouteSelection.swift New route-selection helper extracted from MobileShellComposite statics; Tailscale IP literals to MagicDNS to manualHost ordering is correct. Loopback suppression on physical devices preserved.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift Trust re-checked before and after fetching the Stack token, minimizing the TOCTOU window. routeAllowsStackAuth allocates a fresh MobileShellRouteAuthPolicy per call as a struct stack-allocated value.
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift QR v3 adds m= manual-host param; v2 rejects any m= item. Route-ID synthesis is now kind-scoped and occurrence-counted correctly. Loopback rejection preserved for all route kinds.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Large change: injects MobileManualHostTrustStoring, wires trust-warning state, moves route-selection helpers to MobileShellRouteSelection. Compiler-version-conditional deinit pattern from previous review thread is unchanged.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ManualHostTrust.swift New extension handling trust-warning lifecycle: queuing, acceptance, staleness checks, and workspace-open intent binding. Trust is cleared correctly on sign-out and team change.
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileShellRouteAuthPolicy.swift Converted from private-init static-methods-only type to a proper injectable struct. manualHost trust gate correctly requires both routeAllowsStackAuth and manualHostTrusted; loopback excluded even with trust.
Sources/Mobile/MobileHostRouteAdvertisement.swift New MainActor helper managing advertised manual-host state on the Mac side; validates host via CmxManualHost and rejects loopback before advertising.
Resources/Localizable.xcstrings All new manual-host strings translated across all 20 supported locales including Japanese.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant iOS as iOS App
    participant Shell as MobileShellComposite
    participant RPC as MobileCoreRPCClient
    participant Trust as ManualHostTrustStore
    participant Mac as Mac (MobileHostService)

    iOS->>Shell: connectPairingInput() / QR scan
    Shell->>Shell: firstManualHostRouteNeedingApproval()
    alt manualHost route and not trusted
        Shell->>iOS: show manualHostTrustWarning
        iOS->>Shell: acceptManualHostTrustWarning()
        Shell->>Trust: trust(scope)
    end
    Shell->>RPC: connect(ticket, manualHostStackAuthTrustProvider)
    RPC->>Trust: isTrusted(scope) before token fetch
    Trust-->>RPC: true
    RPC->>RPC: fetch stackAccessToken
    RPC->>Trust: isTrusted(scope) after token fetch
    Trust-->>RPC: true
    RPC->>Mac: sendRequest with stack_access_token
    Mac-->>RPC: response
    alt insecureManualRoute error
        RPC-->>Shell: MobileShellConnectionError.insecureManualRoute
        Shell->>iOS: queue re-approval warning
    end
    note over Trust: Network path change removes all trust
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant iOS as iOS App
    participant Shell as MobileShellComposite
    participant RPC as MobileCoreRPCClient
    participant Trust as ManualHostTrustStore
    participant Mac as Mac (MobileHostService)

    iOS->>Shell: connectPairingInput() / QR scan
    Shell->>Shell: firstManualHostRouteNeedingApproval()
    alt manualHost route and not trusted
        Shell->>iOS: show manualHostTrustWarning
        iOS->>Shell: acceptManualHostTrustWarning()
        Shell->>Trust: trust(scope)
    end
    Shell->>RPC: connect(ticket, manualHostStackAuthTrustProvider)
    RPC->>Trust: isTrusted(scope) before token fetch
    Trust-->>RPC: true
    RPC->>RPC: fetch stackAccessToken
    RPC->>Trust: isTrusted(scope) after token fetch
    Trust-->>RPC: true
    RPC->>Mac: sendRequest with stack_access_token
    Mac-->>RPC: response
    alt insecureManualRoute error
        RPC-->>Shell: MobileShellConnectionError.insecureManualRoute
        Shell->>iOS: queue re-approval warning
    end
    note over Trust: Network path change removes all trust
Loading

Reviews (37): Last reviewed commit: "test(ios): cover manual host scope gener..." | Re-trigger Greptile

Comment thread Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxManualHost.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

3 issues found and verified against the latest diff

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxManualHost.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 20 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@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: 4

Caution

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

⚠️ Outside diff range comments (3)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileSyncProtocol.swift (1)

14-20: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Gate manual_host out of legacy attach payloads for older clients.

The v2 QR path already fails closed on newer grammar versions, but the legacy compact/full JSON ticket path still has no compatibility gate; a manual_host route will make older clients throw on decode instead of falling back to the existing update guidance.

🤖 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/MobileSyncProtocol.swift`
around lines 14 - 20, The legacy attach payload path still allows manual_host to
reach older clients and break decoding, so add a compatibility gate in the
ticket generation/encoding flow around CmxAttachTransportKind and the legacy
compact/full JSON path. Ensure manual_host is excluded or downgraded for older
grammar versions so those clients keep falling back to the existing update
guidance, while newer versions can still use the route.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swift (1)

12-34: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the user-facing error to match the new trust-gate failure.

Line 12 now describes an approval failure, but Line 34 still points users at “secure route” advertisement. That is misleading for an unapproved manual-host route; return localized copy that tells the user to trust/approve the manual host before pairing.

As per coding guidelines, “Swift UI, menu, alert, tooltip, error, recovery, and command text must be routed through String(localized:defaultValue:) or an equivalent localized API.”

Proposed fix
         case .insecureManualRoute:
-            return "Manual host did not advertise a secure mobile sync route"
+            return L10n.string(
+                "mobile.pairing.manualHostTrustRequired",
+                defaultValue: "Trust this manual host before pairing."
+            )
🤖 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/MobileShellConnectionError.swift`
around lines 12 - 34, The user-facing copy for the MobileShellConnectionError
case handling the manual-host trust gate is misleading, because the
.insecureManualRoute path in MobileShellConnectionError.errorDescription still
tells users about a secure route advertisement instead of an approval
requirement. Update that branch to use localized text via
String(localized:defaultValue:) (or an equivalent localized API) and make the
message explicitly tell the user to trust/approve the manual host before
pairing. Keep the fix scoped to MobileShellConnectionError and its
errorDescription switch so the new trust-gate failure is reflected consistently.

Source: Coding guidelines

Sources/Mobile/MobileHostService.swift (1)

152-165: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a regression test for the new dedup guard.

The equality-guard skipping .mobileHostStatusDidChange when routes are unchanged is a subtle behavior change (previously every update call posted the notification). A small unit test asserting the notification fires once on route change and not on a repeated identical call would guard against future regressions here.

🤖 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 152 - 165, The new
equality guard in MobileHostPublicStatusCache.update(routes:) changes
notification behavior, so add a regression test that verifies
.mobileHostStatusDidChange is posted when routes change and not posted again
when update(routes:) is called with the same CmxAttachRoute array. Use the
update(routes:) method and the notification name as the main symbols to target
the behavior, and assert the first call fires once while the repeated identical
call is deduplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 5638-5648: The route scan in MobileShellComposite’s manual-host
trust selection stops too early when
MobileShellRouteAuthPolicy.routeAllowsStackAuth(route) is true, which prevents
later .manualHost fallback routes from being considered. Update the route
iteration logic to keep scanning all routes in the ticket, and only return a
prompt when an untrusted manualHost route is actually found, using the existing
manualHostTrustScope(for:) and manualHostTrustStore checks; if a fallback
manualHost route is selected, ensure approval is queued there instead of being
skipped.

In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/UserDefaultsMobileManualHostTrustStore.swift`:
- Around line 24-30: The initializer in UserDefaultsMobileManualHostTrustStore
currently falls back silently to UserDefaults.standard when
UserDefaults(suiteName:) fails, which can split the trust store and hide
misconfiguration. Update the init(suiteName:key:) path to surface this failure
by logging a clear diagnostic, and add a debug assertion or similar fail-loud
signal so the suite resolution issue is visible during development. Keep the
intended suite as the primary store and use the fallback only with an explicit
warning tied to the suiteName/key context.

In
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift`:
- Around line 71-78: The manual host validator in MobileSection’s
isManualHostValid still allows loopback hosts that are later filtered out by
CmxPairingQRCode, so the UI can report a host as valid even though it won’t be
included in the pairing payload. Update the validation to reject loopback
addresses as well, either by reusing the same loopback check used by
CmxPairingQRCode or by extracting a shared validator that both trimmedManualHost
and the QR-code pairing path call.

In `@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxManualHost.swift`:
- Around line 35-43: The CmxManualHost initializer currently accepts an
unbracketed host containing a port suffix as a valid bare host, so update the
validation in CmxManualHost to reject any single colon followed only by digits
unless the value is a bracketed IPv6 literal. Keep the existing checks for
whitespace, control characters, reserved URL characters, and scheme markers, and
add a host-only colon rule that preserves valid IPv6 inputs while rejecting
cases like host:port. Also add a test alongside
manualHostNormalizerRejectsURLsAndAcceptsBracketedIPv6 to cover this host:port
typo.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swift`:
- Around line 12-34: The user-facing copy for the MobileShellConnectionError
case handling the manual-host trust gate is misleading, because the
.insecureManualRoute path in MobileShellConnectionError.errorDescription still
tells users about a secure route advertisement instead of an approval
requirement. Update that branch to use localized text via
String(localized:defaultValue:) (or an equivalent localized API) and make the
message explicitly tell the user to trust/approve the manual host before
pairing. Keep the fix scoped to MobileShellConnectionError and its
errorDescription switch so the new trust-gate failure is reflected consistently.

In
`@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileSyncProtocol.swift`:
- Around line 14-20: The legacy attach payload path still allows manual_host to
reach older clients and break decoding, so add a compatibility gate in the
ticket generation/encoding flow around CmxAttachTransportKind and the legacy
compact/full JSON path. Ensure manual_host is excluded or downgraded for older
grammar versions so those clients keep falling back to the existing update
guidance, while newer versions can still use the route.

In `@Sources/Mobile/MobileHostService.swift`:
- Around line 152-165: The new equality guard in
MobileHostPublicStatusCache.update(routes:) changes notification behavior, so
add a regression test that verifies .mobileHostStatusDidChange is posted when
routes change and not posted again when update(routes:) is called with the same
CmxAttachRoute array. Use the update(routes:) method and the notification name
as the main symbols to target the behavior, and assert the first call fires once
while the repeated identical call is deduplicated.
🪄 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: ce99c1cd-c228-4be4-9ce3-012daa400bff

📥 Commits

Reviewing files that changed from the base of the PR and between fa5e0c6 and 22de8ba.

📒 Files selected for processing (42)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxManualHost.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxTransport.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileSyncProtocol.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxManualPairingEntryTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxTransportTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/CmxAttachTicketInput.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ManualAttachTicket.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/InMemoryMobileManualHostTrustStore.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileManualHostTrustScope.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileManualHostTrustStoring.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileManualHostTrustWarning.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileShellRouteAuthPolicy.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/UserDefaultsMobileManualHostTrustStore.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileShellRouteAuthPolicyTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift
  • Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift
  • Packages/iOS/CmuxMobileTransport/Tests/CmuxMobileTransportTests/CmxNetworkByteTransportTests.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/MobileCatalogSection.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/MobileAttachTicketStore.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/MobileRouteResolver.swift
  • Sources/Mobile/Pairing/MobilePairingModel.swift
  • Sources/Mobile/Pairing/MobilePairingView.swift
  • Sources/SettingsSearchAliases.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • cmuxTests/MobilePairingConnectionTransitionTests.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmux/cmuxApp.swift
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRuntime.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment thread Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Outdated
Comment thread Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxManualHost.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 28 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@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: 2

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/UserDefaultsMobileManualHostTrustStore.swift`:
- Around line 2-4: The file-scope Logger constant is missing the required
nonisolated declaration under Swift 6 MainActor isolation. Update the
manualHostTrustStoreLog declaration in UserDefaultsMobileManualHostTrustStore to
be a nonisolated private let so it can be safely used from the actor context,
including the .error(...) calls in init(suiteName:...). Keep the change limited
to the existing Logger symbol and preserve the current subsystem/category
values.

In
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift`:
- Around line 79-88: Centralize the manual-host advertisability check instead of
duplicating the same CmxManualHost + CmxLoopbackHost logic in multiple places.
Add a shared helper on CmxManualHost in CMUXMobileCore, such as a static
isAdvertisable(_:) or a normalizing initializer, and update
MobileSection.isManualHostAdvertisable, MobileHostService.configuredManualHost,
and MobileRouteResolver.routes(...) to call that single source of truth. Keep
the trimming/empty-input behavior consistent in the shared helper so all callers
use the same validation.
🪄 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: 56488159-2937-4534-bda2-e50c0ef5b856

📥 Commits

Reviewing files that changed from the base of the PR and between 22de8ba and f796928.

📒 Files selected for processing (32)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxManualHost.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxManualHostTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/CmxAttachTicketInput.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileHostStatusResponse.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/CmxAttachTicketInputTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ManualHostTrust.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+RouteSelection.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ReconnectRouteSelectionTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/InMemoryMobileManualHostTrustStore.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileManualHostTrustScope.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileManualHostTrustWarning.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileShellRouteAuthPolicy.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/UserDefaultsMobileManualHostTrustStore.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileShellRouteAuthPolicyTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView+PairingWarnings.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/macOS/CmuxSettingsUI/Package.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Sources/Mobile/MobileAttachTicketStore.swift
  • Sources/Mobile/MobileHostService+ManualHostRoutes.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/MobileRouteResolver.swift
  • Sources/Mobile/Pairing/MobilePairingModel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MobileHostAuthorizationTests.swift
  • cmuxTests/MobileHostServiceSettingsTests.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment thread Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/MobileSection.swift Outdated
@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift`:
- Around line 12-17: The time-driven tests use real wall-clock values instead of
a shared virtual clock. In
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift
lines 12-17, create a TestClock and use it for the LivenessTestRuntime now
closure. In
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift
lines 22-30, derive expiresAt from the injected TestClock; at lines 43-46, use
that same clock for now. Preserve the existing test behavior while ensuring all
time values come from the injected clock.
🪄 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: 4769275c-ae31-4cd3-bcd0-1d9fd8beb0f1

📥 Commits

Reviewing files that changed from the base of the PR and between 769ae4a and fb3fd5e.

📒 Files selected for processing (3)
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingCancellationPreservesForegroundTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift

Comment on lines +12 to +17
let runtime = LivenessTestRuntime(
transportFactory: LivenessTransportFactory(router: router, box: TransportBox()),
now: { Date() },
supportedRouteKinds: [.manualHost],
supportsServerPushEvents: false
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Two new test suites read the real wall clock instead of injecting a virtual clock. Both suites use Date() directly, while the third suite in this stack (MobilePairingCancellationPreservesForegroundTests.swift) consistently injects TestClock() for the same now: parameter — the shared root cause is these two files skipping the virtual-clock convention required for time-driven test behavior.

  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift#L12-L17: replace now: { Date() } with a TestClock() instance (e.g. now: { clock.now }).
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift#L22-L30: build expiresAt from an injected TestClock instead of Date().addingTimeInterval(3_600).
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift#L43-L46: replace now: { Date() } with the same injected TestClock.
📍 Affects 2 files
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift#L12-L17 (this comment)
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift#L22-L30
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift#L43-L46
🤖 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/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift`
around lines 12 - 17, The time-driven tests use real wall-clock values instead
of a shared virtual clock. In
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostTrustExpirationSchedulingTests.swift
lines 12-17, create a TestClock and use it for the LivenessTestRuntime now
closure. In
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PrimaryRouteAuthorizationFallbackTests.swift
lines 22-30, derive expiresAt from the injected TestClock; at lines 43-46, use
that same clock for now. Preserve the existing test behavior while ensuring all
time values come from the injected clock.

Source: Path instructions

…nual-host

# Conflicts:
#	Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
#	Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingAttemptDeadlineTests.swift
#	Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ReconnectRouteSelectionTests.swift
#	Sources/Mobile/MobileAttachTicketStore.swift
#	Sources/Mobile/MobileHostService.swift
#	ios/cmux/Resources/Localizable.xcstrings
austinywang and others added 7 commits July 16, 2026 11:40
… tests

- Add DocC to CmxIrohTCPFirstActivation.start and MobileShellComposite.connectionError
- Move stringParamSelection to a file-scope private func per package-design policy
- Use injected TestClock instead of wall clock in the two new time-driven test suites

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With a MagicDNS name plus numeric Tailscale address discovered, the resolver
emits one tailscale route (priority 10) but priced the manual route off the
unfiltered host count (priority 30). CmxPairingQRCode.encodableRoutes requires
canonical priorities (10, 20, ...), so the mint falls back to the v1 payload
and MobilePairingModel refuses to show the QR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Discovery can prepend hosts (like the MagicDNS name) that the resolver
filters out of the emitted route set. Pricing the manual route off the
unfiltered count produced a 10/30 priority gap, CmxPairingQRCode's
canonical-sequence check rejected the mint, and the pairing QR fell back
to v1 and refused to display.

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

A stored Mac with two untrusted manual-host candidates queues an approval
for the first candidate, then the route loop keeps dialing: the second
candidate rotates the pairing attempt, supersedes the first pending trust,
and finishes the switch attempt. The user's approval then resolves as
.superseded and the switch is stranded.

Also pins the working invariant that an approved manual-host switch leaves
the connected Mac persisted as active via identity recovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… owns the attempt

connectStoredMac's host/port loop discarded connectManualHost's result and
only broke on a live connection, so a second untrusted candidate rotated
the pairing attempt, superseded the first candidate's queued trust warning,
and finished the switch attempt out from under the pending approval. The
user's approval then resolved as .superseded and the switch was stranded.
Continue to the next candidate only on a plain failure.

Also drain a parked network-change recovery when the foreground-liveness
recovery path finishes; it cleared the in-flight flags without running the
coalesced pending recovery.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (6)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift (1)

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

Preserve the existing client when an Iroh switch attempt fails.

connect(...) deliberately retains the active connection when preservesActiveConnection is true, but this catch block then unconditionally marks it disconnected and clears its context. Only clear connection state when no active connection was being preserved.

Proposed fix
-                if !handleAuthorizationFailureIfNeeded(
+                let handledAuthorizationFailure = handleAuthorizationFailureIfNeeded(
                     underlyingError,
                     owner: .connectionAttempt(
                         route: failureRoute,
                         preservingActiveConnection: preservesActiveConnection
                     )
-                ) {
+                )
+                if !handledAuthorizationFailure && !preservesActiveConnection {
                     connectionState = .disconnected
                     macConnectionStatus = .unavailable
                     clearRemoteConnectionContext()
                 }
🤖 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`+ConnectionRecovery.swift
around lines 202 - 233, Update the catch block in the Iroh recovery flow to
conditionally clear connection state: only set connectionState to disconnected,
mark macConnectionStatus unavailable, and call clearRemoteConnectionContext when
preservesActiveConnection is false. Preserve the existing active client and
context when preservesActiveConnection is true, while leaving
authorization-failure handling unchanged.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (3)

1539-1544: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not erase every persisted approval on routine reconnects.

removeAll() revokes trust during foreground recovery, team refreshes, and manual retries—not only network-boundary changes. An approved manual host therefore prompts again instead of reconnecting automatically. Keep invalidation in the explicit network/account boundary paths and remove this unconditional wipe.

As per path instructions, manual-host trust must have one authoritative, explicitly scoped source rather than competing blanket invalidation behavior. <path_instructions>

🤖 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 1539 - 1544, The routine reconnect flow in
reconnectActiveMacIfAvailable should not call manualHostTrustStore.removeAll().
Remove this unconditional trust wipe so approved manual hosts reconnect
automatically, while preserving trust invalidation only in the existing
explicitly scoped network or account-boundary paths.

Sources: Coding guidelines, Path instructions


3672-3674: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail closed when the customization target instance is ambiguous.

first(where:) guesses an app instance when multiple tagged records share macDeviceID, potentially writing customization to the wrong persisted record. Require instanceTag, or only infer it when exactly one authority matches.

As per path instructions, correctness-critical instance identity must use one reliable structured source and must not fall back to a best-effort selection. <path_instructions>

🤖 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 3672 - 3674, Update the target instance resolution in the
customization flow around targetInstanceTag so it does not use
displayPairedMacs.first(where:) as a best-effort fallback. Require the provided
instanceTag, or infer it only when exactly one matching macDeviceID record
exists; otherwise fail closed without writing customization to any persisted
record.

Sources: Coding guidelines, Path instructions


1659-1665: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip a concurrently forgotten candidate instead of aborting all reconnects.

When this Mac becomes forgotten, the combined guard executes break, preventing later saved Macs from being attempted.

Proposed fix
-            guard generation == storedMacReconnectGeneration,
-                  await isScopeCurrent(scope),
-                  await !isForgottenMacDeviceID(
-                      mac.macDeviceID,
-                      instanceTag: mac.instanceTag,
-                      scope: scope
-                  ) else { break }
+            guard generation == storedMacReconnectGeneration,
+                  await isScopeCurrent(scope) else { break }
+            if await isForgottenMacDeviceID(
+                mac.macDeviceID,
+                instanceTag: mac.instanceTag,
+                scope: scope
+            ) {
+                continue
+            }
🤖 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 1659 - 1665, Update the reconnect loop containing the combined
guard for generation, scope, and isForgottenMacDeviceID so a forgotten Mac
candidate is skipped rather than breaking the entire loop. Preserve the existing
abort behavior for stale generations or scopes, while allowing later saved Macs
to continue being attempted.
Sources/HostSettingsActions.swift (1)

27-29: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Reuse the browser extension discovery service.

BrowserWebExtensionDiscoveryService keeps its in-flight task per instance, but constructing it inline here bypasses that deduplication. Concurrent settings requests can therefore start duplicate subprocess/discovery scans. Store or inject one service instance on HostSettingsActions and call it here.

🤖 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/HostSettingsActions.swift` around lines 27 - 29, Update
HostSettingsActions to store or receive a shared
BrowserWebExtensionDiscoveryService instance, then use that instance in
discoverBrowserWebExtensions instead of constructing one inline. Preserve the
existing support guard and discovery result behavior while ensuring concurrent
requests reuse the service’s in-flight task.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift (1)

161-164: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Await the recovery signal instead of yielding a fixed number of times.

The ten Task.yield() calls do not establish that the fire-and-forget recovery task has started loading team-b, so this assertion can race and intermittently fail. Await waitUntilLoadStarted(teamID: "team-b") (or another deadline-bounded completion signal) before asserting.

As per coding guidelines, tests should await a real completion signal or use a deadline-bounded poll rather than fixed scheduling yields.

🤖 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/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift`
around lines 161 - 164, Replace the fixed ten-iteration Task.yield loop in the
stale reconnect test with an await of the real recovery signal, using
pairedStore.waitUntilLoadStarted(teamID: "team-b") or an equivalent
deadline-bounded completion mechanism before asserting didStartLoad.

Sources: Coding guidelines, Path instructions

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

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1539-1544: The routine reconnect flow in
reconnectActiveMacIfAvailable should not call manualHostTrustStore.removeAll().
Remove this unconditional trust wipe so approved manual hosts reconnect
automatically, while preserving trust invalidation only in the existing
explicitly scoped network or account-boundary paths.
- Around line 3672-3674: Update the target instance resolution in the
customization flow around targetInstanceTag so it does not use
displayPairedMacs.first(where:) as a best-effort fallback. Require the provided
instanceTag, or infer it only when exactly one matching macDeviceID record
exists; otherwise fail closed without writing customization to any persisted
record.
- Around line 1659-1665: Update the reconnect loop containing the combined guard
for generation, scope, and isForgottenMacDeviceID so a forgotten Mac candidate
is skipped rather than breaking the entire loop. Preserve the existing abort
behavior for stale generations or scopes, while allowing later saved Macs to
continue being attempted.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ConnectionRecovery.swift:
- Around line 202-233: Update the catch block in the Iroh recovery flow to
conditionally clear connection state: only set connectionState to disconnected,
mark macConnectionStatus unavailable, and call clearRemoteConnectionContext when
preservesActiveConnection is false. Preserve the existing active client and
context when preservesActiveConnection is true, while leaving
authorization-failure handling unchanged.

In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift`:
- Around line 161-164: Replace the fixed ten-iteration Task.yield loop in the
stale reconnect test with an await of the real recovery signal, using
pairedStore.waitUntilLoadStarted(teamID: "team-b") or an equivalent
deadline-bounded completion mechanism before asserting didStartLoad.

In `@Sources/HostSettingsActions.swift`:
- Around line 27-29: Update HostSettingsActions to store or receive a shared
BrowserWebExtensionDiscoveryService instance, then use that instance in
discoverBrowserWebExtensions instead of constructing one inline. Preserve the
existing support guard and discovery result behavior while ensuring concurrent
requests reuse the service’s in-flight task.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8539ab21-1317-43eb-b325-8f24500b92f1

📥 Commits

Reviewing files that changed from the base of the PR and between 836be17 and 3f93301.

📒 Files selected for processing (16)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacAliases.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacCoalescing.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacPersistence.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellRouteSelection.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ManualHostSwitchApprovalRegressionTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileSecondaryInstanceAuthorityTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ReconnectRouteSelectionTests.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/HostSettingsActions.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift`:
- Around line 331-343: Remove caller-supplied auth data before the
authentication decision and rebuild credentials only from the client-owned
authenticated state. Update the request preparation flow associated with
preEnqueueValidator and sendAuthorizer so inbound auth, including
auth.stack_access_token, cannot survive when authenticated.stackAccessToken is
nil; retain the existing validators only for client-owned tokens and fail closed
otherwise.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1515-1517: Move trailing network recovery ownership into
MobileConnectionRecoveryOwner: remove pendingNetworkRecoveryTrigger from
MobileShellComposite.swift; update the network-trigger handling in
MobileShellComposite+ConnectionRecovery.swift at lines 175-180 to atomically
coalesce the trigger against the owner’s active attempt and cancellation
generation; update the idle-transition and validation-completion flow at lines
266-277 to consume the queued trigger only on an authoritative transition to
idle and clear it on cancellation.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+AuthorizationFailure.swift:
- Around line 103-114: Update the .rpcError handling in the
authorization-failure classifier to remove all message-substring checks and rely
only on the typed authorization code normalized at the RPC boundary. Switch
exclusively on the structured code, and return false when that code is absent or
not an explicitly recognized authorization failure.
🪄 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: 83f36186-bb63-4053-b76f-e9489e47d99e

📥 Commits

Reviewing files that changed from the base of the PR and between 3f93301 and 2874bc5.

📒 Files selected for processing (22)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/DiagnosticTaxonomy.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCPendingFailure.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCConnectWaiterTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCHostStatusTokenTimeoutTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCIndependentEventTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCStaleReaderRecoveryTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCStalledWriteRecoveryTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AuthorizationFailure.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ManualAttachTicket.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacCoalescing.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PairedMacPersistence.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.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/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/NetworkRecoveryCoalescingTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TaggedBuildPresenceRouteIsolationTests.swift

Comment on lines +331 to +343
let preEnqueueValidator: MobileCoreRPCSession.PreEnqueueValidator?
let sendAuthorizer: MobileCoreRPCSession.SendAuthorizer?
if authenticated.stackAccessToken != nil {
preEnqueueValidator = { @Sendable [self] in
try await validateTokenBearingRequestBeforeEnqueue()
}
sendAuthorizer = { @Sendable [self] in
try await authorizeTokenBearingSend()
}
} else {
preEnqueueValidator = nil
sendAuthorizer = nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Strip caller-supplied auth before deciding whether to install send gates.

Line 333 only detects tokens minted by requestDataWithAuth. A caller can provide auth.stack_access_token on an otherwise unauthenticated request; that field survives, while both validators remain nil, allowing credentials over an unapproved or revoked manual-host route.

Make authentication client-owned by removing inbound auth before rebuilding it.

Proposed fix
         if transportRequest.authorizationMode == .transportAdmission {
             request.removeValue(forKey: "auth")
             return AuthenticatedRequestPayload(
                 data: try JSONSerialization.data(withJSONObject: request),
                 stackAccessToken: nil
             )
         }
+        // Authentication is exclusively owned and generated by this client.
+        request.removeValue(forKey: "auth")

As per path instructions, correctness-critical credential gating must use one authoritative source and fail closed when trust is absent.

🤖 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/MobileCoreRPCClient.swift`
around lines 331 - 343, Remove caller-supplied auth data before the
authentication decision and rebuild credentials only from the client-owned
authenticated state. Update the request preparation flow associated with
preEnqueueValidator and sendAuthorizer so inbound auth, including
auth.stack_access_token, cannot survive when authenticated.stackAccessToken is
nil; retain the existing validators only for client-owned tokens and fail closed
otherwise.

Source: Path instructions

Comment on lines +1515 to +1517
/// Newest-wins trailing network recovery. A later path callback must rotate
/// trust immediately, but cannot strand the reconnect it just superseded.
var pendingNetworkRecoveryTrigger = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Move trailing network recovery into MobileConnectionRecoveryOwner.

The standalone Boolean splits lifecycle ownership: cancellation can accidentally relaunch recovery, while a successful redial awaiting subscription validation can strand the deferred trigger.

  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L1515-L1517: remove the parallel mutable flag and represent the queued trigger in the recovery owner.
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift#L175-L180: atomically coalesce the network trigger through the owner, tied to its active attempt and cancellation generation.
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift#L266-L277: consume the queued trigger only when the owner authoritatively transitions to idle, including validation completion, and clear it on cancellation.

As per coding guidelines, recovery lifecycle state must retain one explicit owner rather than a mutable side channel.

📍 Affects 2 files
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L1515-L1517 (this comment)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift#L175-L180
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift#L266-L277
🤖 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 1515 - 1517, Move trailing network recovery ownership into
MobileConnectionRecoveryOwner: remove pendingNetworkRecoveryTrigger from
MobileShellComposite.swift; update the network-trigger handling in
MobileShellComposite+ConnectionRecovery.swift at lines 175-180 to atomically
coalesce the trigger against the owner’s active attempt and cancellation
generation; update the idle-transition and validation-completion flow at lines
266-277 to consume the queued trigger only on an authoritative transition to
idle and clear it on cancellation.

Source: Coding guidelines

Comment on lines +103 to +114
case let .rpcError(code, message):
let normalizedCode = code?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if let normalizedCode,
["unauthorized", "forbidden", "invalid_token", "token_expired", "expired_token", "auth_required"].contains(normalizedCode) {
return true
}
let normalizedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalizedMessage.contains("unauthorized")
|| normalizedMessage.contains("forbidden")
|| normalizedMessage.contains("invalid token")
|| normalizedMessage.contains("expired token")
|| normalizedMessage.contains("token expired")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not infer authorization failures from error-message substrings.

Text such as "forbidden" is not authoritative: wording changes can bypass reauthentication, while unrelated messages can trigger it. Normalize upstream responses into typed authorization codes at the RPC boundary and switch only on that structured value; fail closed when it is absent.

As per path instructions, correctness-critical authorization state must come from one reliable structured source, not string heuristics.

🤖 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`+AuthorizationFailure.swift
around lines 103 - 114, Update the .rpcError handling in the
authorization-failure classifier to remove all message-substring checks and rely
only on the typed authorization code normalized at the RPC boundary. Switch
exclusively on the structured code, and return false when that code is absent or
not an explicitly recognized authorization failure.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trusted manual-host mobile pairing for subnet-router setups

3 participants