Skip to content

iOS/macOS transport: survive wake-time broker 401s without endpoint teardown - #9263

Closed
azooz2003-bit wants to merge 5 commits into
mainfrom
feat-iroh-wake-auth
Closed

azooz2003-bit wants to merge 5 commits into
mainfrom
feat-iroh-wake-auth

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Problem

Field cmuxdiag rings from build 20260731034828 (Aziz's phone, 2026-07-31) show the dominant reconnect-flakiness mechanism at app wake: the relay-policy refresh fires with a credential pair that another lane (the RPC wake force-refresh) rotated between capture and server validation. The broker answers 401. Today that single 401:

  • fails endpoint activation (endpointFailed authorizationFailed) or, on warm wakes, tears down the whole verified runtime including routes and the offline policy cache (CmxIrohClientRuntime+PolicyRefresh), and
  • schedules a flat 30s+jitter retry (retryScheduled ms=32331/36124/36683 in the rings).

Every dial in that window then fails policyUnavailable/endpointUnavailable/noRoute; observed outages ran 30s to 2.5 minutes per wake. Ring stats: 39 dials, 10 connected, 29 recovery attempts in a 19-minute trace.

The rings also show every recovery dialing the target Mac twice: recovery nils foregroundMacDeviceID, which makes the recovering Mac eligible for secondary (background-control) aggregation; the warm dial then has to be drained by the foreground redial (controlOwnerReleased + fresh dial, plus up to the 3s handoff-drain wait).

Fix

  • CmxIrohTrustBrokerClient recovers exactly once from a 401. CmxIrohBrokerTokenSource gains an optional recoveredCredentialPair closure invoked with the rejected pair; the client retries the request once with the recovered pair. The iOS composition and the Mac host wire it to re-capture the session snapshot (force-minting via AuthCoordinator.forceRefreshAccessToken() only when the rejected access token is unchanged, so concurrent rejections cannot stampede the minter). Frozen pinned sources (sign-out revocation flows) keep the default nil closure and never switch credentials.
  • Auth rejections stop destroying verified state. preservesVerifiedPolicyDuringRefresh now includes 401/403, retriesInitialActivation includes 401, and resolvePolicy falls back to the verified offline bootstrap on auth rejections exactly as it already did for connectivity failures. LAN and cached-relay dials keep working while auth settles; a genuinely dead session still exits through the auth coordinator's state clear, which owns runtime teardown.
  • Cause-aware backoff. The relay-policy refresh loop retries authorization failures on a 2s→120s ladder (relayPolicyRetrySchedule(for:)) instead of the flat 30s+jitter schedule.
  • No duplicate dial during recovery. secondaryAggregationCandidateMacs excludes the in-flight recovery target (recoveryTargetMacDeviceID/recoveryTargetInstanceTag, gated on isReconnectingStoredMac || connectionRecoveryOwner.isActive) the same way it excludes a live foreground.

Regression structure

  • Commit 1 adds the failing regressions only (classifier behavior + aggregation exclusion) — CI red.
  • Commit 2 lands the fix plus the new-API tests (401 retry semantics, cached-policy recovery classifier, backoff schedule) — CI green.

Relationship to in-flight PRs

Complementary to #9256 (pooled-session wake validation, deferred recovery, dial gating) and #9250 (health-gated recovery, discovery freshness): neither touches the broker auth lane or the aggregation exclusion. The zombie/corpse-dial signatures in the same rings are owned by #9256.

Test plan

  • swift test --package-path Packages/Shared/CmuxIrohTransport (full suite)
  • swift test --package-path Packages/iOS/CmuxMobileShell --filter MobileMacConnectionPoolTests (75/75)
  • cmuxFeatureTests.relayPolicyRetryScheduleShortensAuthorizationFailures runs in the hosted iOS-simulator package job (package does not build for macOS)
  • Tagged cloud builds (macOS + iOS, tag wkauth) + iPhone dogfood

🤖 Generated with Claude Code


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


Summary by cubic

Fixes wake-time broker 401s without tearing down the endpoint, preserving verified state so LAN and existing routes stay online while auth settles. Also prevents duplicate background-control dials during recovery and shortens auth-failure backoff.

  • Bug Fixes
    • CmxIrohTrustBrokerClient retries a 401 once with a recovered pair via CmxIrohBrokerTokenSource.recoveredCredentialPair (live sources re-capture/force-mint as needed; frozen sources opt out).
    • 401/403 preserve verified policy during refresh; 401 retries initial activation. Cached/offline fallbacks remain availability-only — 401/403 never unlock cached grants or offline stores.
    • Cause-aware backoff: authorization failures use a 2s→120s ladder instead of the flat 30s+jitter.
    • MobileShellComposite excludes the in-flight recovery target to prevent a duplicate background-control dial during reconnect.
    • Discovery/registry fallbacks now key on availability-only classification (isAvailabilityFailure); removed a leftover recoversWithCachedPolicy reference.
    • Tests updated: move terminal foreground failure from 401 to 400; add coverage for 401 recovery, preserved-policy on 401/403, availability-only cached-policy fallback, backoff schedule, aggregation exclusion, and auth classifiers.

Written for commit 7063d0f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added automatic credential recovery and a one-time retry for unauthorized broker requests.
    • Added faster retry scheduling for authorization failures.
  • Bug Fixes
    • Prevented authenticated denials from incorrectly falling back to cached policies.
    • Improved policy preservation during temporary authorization failures.
    • Prevented devices reconnecting in the foreground from being selected for secondary aggregation.
  • Tests
    • Added coverage for credential recovery, authorization handling, retry timing, policy fallback, and device eligibility.

azooz2003-bit and others added 2 commits July 30, 2026 22:14
A broker 401 at app wake (token pair rotated by another lane between
capture and server validation) must not tear down the verified iroh
runtime, and the Mac being redialed must not be dialed a second time as
a background-control aggregation candidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At app wake the relay-policy refresh races the RPC lane's force token
refresh: the pair captured coherently a moment earlier reaches the
broker after rotation and gets a 401. That single 401 used to fail the
endpoint, clear routes and the offline cache (or tear down the whole
runtime on warm wakes), and nap 30-36s of flat backoff, turning a
seconds-long token race into the 30s-2.5min reconnect outages visible
in every wake ring.

Four changes:
- CmxIrohTrustBrokerClient recovers exactly once from a 401: the token
  source re-captures (force-minting only when the rejected access token
  is unchanged) and the request retries with the recovered pair. Frozen
  pinned sources (sign-out revocation) opt out by default.
- 401/403 now preserve verified policy during refresh, and 401 retries
  initial activation; resolvePolicy falls back to the verified offline
  bootstrap on auth rejections like it already did for connectivity, so
  LAN and cached-relay dials keep working while auth settles.
- The relay-policy refresh loop retries authorization failures on a
  2s..120s ladder instead of the flat 30s+jitter schedule.
- The Mac being redialed is excluded from secondary aggregation while a
  stored-Mac reconnect is in flight, removing the duplicate
  background-control dial (and its drain wait) from every recovery.

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

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Trust Broker now recovers rejected credentials and retries unauthorized requests once. Policy fallback distinguishes availability failures from authenticated denials. Relay retries use authorization-specific delays. Mac aggregation excludes the active recovery target until reconnection completes.

Changes

Trust Broker recovery

Layer / File(s) Summary
Broker credential retry
Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohTrustBrokerClient.swift, Packages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohTrustBrokerClientAuthRecoveryTests.swift
CmxIrohBrokerTokenSource accepts a credential-recovery callback and retries HTTP 401 requests once with recovered credentials. Tests cover recovery, propagation, repeated rejection, and HTTP 403 behavior.
Authorization classification and cached policy
Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohTrustBrokerClientError.swift, Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohClientRuntime+Policy.swift, Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohHostRuntime+PolicyRefresh.swift, Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohRegistryContextProvider.swift, Packages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohTrustBrokerClientAuthClassifierTests.swift, Packages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohClientRuntimeTests.swift
HTTP 401 and 403 responses preserve verified policy. Availability classification excludes authenticated denials. Cached-policy bootstrap and fallback use the updated predicates.
Mobile credential and relay retry integration
Sources/Mobile/MobileHostIrohRuntime+Activation.swift, ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift, ios/cmuxPackage/Tests/cmuxFeatureTests/MobileIrohRuntimeCompositionTests.swift
Mobile token sources reuse rotated sessions or force-refresh credentials while preserving account checks. Relay policy refresh uses shorter authorization retry delays and retains the default connectivity schedule.

Recovery-aware Mac aggregation

Layer / File(s) Summary
Mac recovery candidate filtering
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacConnectionPoolTests.swift
Secondary aggregation excludes the retained foreground recovery target during stored-Mac reconnection and restores candidate eligibility after reconnection completes.

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

Sequence Diagram(s)

sequenceDiagram
  participant BrokerTokenSource
  participant TrustBroker
  participant CredentialRecovery
  BrokerTokenSource->>TrustBroker: Send authenticated request
  TrustBroker-->>BrokerTokenSource: Return HTTP 401
  BrokerTokenSource->>CredentialRecovery: Request recovered credentials
  CredentialRecovery-->>BrokerTokenSource: Return account-pinned credentials
  BrokerTokenSource->>TrustBroker: Retry request once
Loading

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production retry behavior now routes authorization failures to a new 2-second ladder, executed by the relay refresh loop's Task.sleep(for:) backoff. Use the repository's cancellation-aware scheduler, timer abstraction, or explicit state signal for relay retry timing instead of Task.sleep in production.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Cache Substitution Correctness ❓ Inconclusive Investigation is still in progress; no verdict evidence submitted yet. Continue inspecting the production diff for cache substitution in persistence, history, undo, or snapshot paths.
✅ Passed checks (22 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 UI coordinators are explicitly @MainActor, transport code uses actors or Sendable values, and recovery closures await AuthCoordinator's MainActor APIs.
Cmux Browser Automation Off-Main ✅ Passed The PR changes 13 transport/runtime files only; both rule target files are unchanged, and the diff adds no browser socket routing, WebKit waits, worker commands, or policy tests.
Cmux Expensive Synchronous Load ✅ Passed The production diff adds no agent-history loader, transcript/JSONL parsing, directory scan, or per-record syscall; changes use async broker calls and bounded in-memory policy/retry logic.
Cmux No Hacky Sleeps ✅ Passed The changed paths are all Swift files. The rule explicitly scopes TypeScript, JavaScript, shell, and non-Swift build/runtime scripts, so this check is not applicable.
Cmux Algorithmic Complexity ✅ Passed The production diff adds only bounded one-time request recovery, scalar classifiers, and state selection; the aggregation path adds no collection scan, and existing linear filtering is unchanged.
Cmux Swift Concurrency ✅ Passed The diff adds async/await credential recovery and actor-based tests; no new Dispatch, Combine, completion-handler, or fire-and-forget Task patterns appear in production additions.
Cmux Swift @Concurrent ✅ Passed Added network work remains in actor-isolated CmxIrohTrustBrokerClient; relayPolicyRetrySchedule is synchronous nonisolated; recovery closures explicitly await @MainActor AuthCoordinator APIs.
Cmux Swift Package Boundaries ✅ Passed Transport auth logic and aggregation logic are in SwiftPM targets with package tests; the only root-app change is AuthCoordinator-bound composition glue supplying the package recovery callback.
Cmux Swiftpm Lockfiles ✅ Passed The PR diff changes only Swift source and test files; it contains no Package.swift, Package.resolved, .gitignore, workflow, or Xcode project changes, so the lockfile rule is not triggered.
Cmux Swift Logging ✅ Passed The PR adds no print, debugPrint, dump, NSLog, ad hoc output, or secret-bearing logs; changed diagnostics use the existing cmux diagnostic log, and Logger declarations are unchanged.
Cmux User-Facing Error Privacy ✅ Passed The production diff adds no user-facing error, alert, output, or recovery copy; it changes internal retry/classification logic, and diagnostics remain generic while tests/comments are allowed.
Cmux Full Internationalization ✅ Passed The PR adds only Swift transport/runtime logic, tests, and developer comments; it adds no user-facing text or changes to catalogs, plists, web messages, or locale files.
Cmux Swiftui State Layout ✅ Passed The PR adds no SwiftUI view/layout patterns. Its only UI-adjacent hunk changes local filtering in the existing @Observable MobileShellComposite, with no new state, GeometryReader, lazy-row store, o...
Cmux Architecture Rethink ✅ Passed The diff adds no sleeps, dispatch delays, polling, locks, observers, or new mutable state; broker recovery delegates to AuthCoordinator, and aggregation uses existing recovery-owner state with focu...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR adds no standalone window or close-shortcut code; changed lines cover transport and aggregation only. scripts/lint_auxiliary_window_close_shortcuts.py passes.
Cmux Source Artifacts ✅ Passed The PR changes only Swift source and test files under Sources and Tests; no artifact directories, logs, media, caches, build output, or dependency checkouts are added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The production diff adds no DEBUG/TESTING guards or test/debug-named members; the new recovery callback and retry/classifier helpers have live production callers.
Cmux No Ambient Global State ✅ Passed The production diff adds only members on existing types: an injectable token callback, private client methods, error classifiers, and a runtime schedule helper; it adds no top-level API, mutable gl...
Title check ✅ Passed The title clearly summarizes the primary change: preventing wake-time broker 401s from tearing down the endpoint.
Description check ✅ Passed The description clearly explains the problem, fix, regression coverage, related work, and test plan, but omits the template checklist and demo video.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-iroh-wake-auth

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.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Heads-up: this composes semantically with #9259 (transient token-miss → connectivity) — 9259 covers the launch/revalidation-window half of the fail=15 signature, this PR covers the server-401-after-rotation half plus teardown/backoff/double-dial. They textually conflict in CmxIrohTrustBrokerClient.swift, MobileIrohRuntimeComposition.swift, and MobileHostIrohRuntime+Activation.swift (9259 makes credentialPair throwing; this PR adds recoveredCredentialPair). Whichever merges second needs a mechanical rebase: keep the recovery closure non-throwing (a failed recovery returns nil) and add try to the pair captures.

@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 `@ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift`:
- Around line 2473-2476: Update the macOS host relay-policy refresh retry-delay
calculation in the settings-control flow to use the same cause-aware
authorization backoff as MobileIrohRuntimeComposition’s
Self.relayPolicyRetrySchedule(for:) helper. Preserve the existing failureCount
and retryAfterSeconds inputs, ensuring authorization failures follow the shared
2–120 second schedule instead of only CmxIrohRetrySchedule().

In
`@Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohClientRuntime`+Lifecycle.swift:
- Around line 109-127: Update CmxIrohClientRuntime.recoversWithCachedPolicy to
return true only for connectivity or other explicitly supported transient
failures, and remove the 401/403 CmxIrohTrustBrokerClientError rejection
fallback. Ensure rejected broker responses continue through the existing
failure-closed lifecycle/sign-out path.

In `@Sources/Mobile/MobileHostIrohRuntime`+Activation.swift:
- Around line 126-149: Extract the duplicated account-pinned credential recovery
sequence into one shared helper or static factory near CmxIrohBrokerTokenSource,
preserving session re-capture, access-token rotation reuse, one force-refresh
fallback, account-ID validation, and fail-closed behavior. In
Sources/Mobile/MobileHostIrohRuntime+Activation.swift lines 126-149 and
ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift lines
2879-2902, replace each recoveredCredentialPair closure body with calls to that
shared implementation, passing auth/session snapshot, expected account ID, and
force-refresh behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7c9748d9-195b-4800-9381-d68ce513f219

📥 Commits

Reviewing files that changed from the base of the PR and between b5294c4 and 562fc11.

📒 Files selected for processing (11)
  • Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohClientRuntime+Lifecycle.swift
  • Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohClientRuntime+Policy.swift
  • Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohTrustBrokerClient.swift
  • Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohTrustBrokerClientError.swift
  • Packages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohTrustBrokerClientAuthClassifierTests.swift
  • Packages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohTrustBrokerClientAuthRecoveryTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacConnectionPoolTests.swift
  • Sources/Mobile/MobileHostIrohRuntime+Activation.swift
  • ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/MobileIrohRuntimeCompositionTests.swift

Comment on lines +2473 to 2476
let retryDelay = Self.relayPolicyRetrySchedule(for: error).delay(
failureCount: failureCount,
retryAfterSeconds: (error as? any CmxRetryAfterProviding)?
.retryAfterSeconds,

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every scheduleRelayPolicyRefresh implementation and check
# whether each uses a cause-aware retry schedule for authorization failures.
rg -n --type=swift -B3 -A5 'func scheduleRelayPolicyRefresh'
rg -n --type=swift 'relayPolicyRetrySchedule\('

Repository: manaflow-ai/cmux

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -u

echo "Swift files matching target patterns:"
git ls-files | rg '(^|/)(MobileIrohRuntimeComposition\.swift|MobileHostIrohRuntime\+Activation\.swift)$' || true

echo
echo "Search scheduleRelayPolicyRefresh by name string:"
rg -n -S 'scheduleRelayPolicyRefresh' . || true

echo
echo "Search relayPolicyRetrySchedule by name string:"
rg -n -S 'relayPolicyRetrySchedule' . || true

echo
echo "Search CmxIrohRetrySchedule:"
rg -n -S 'CmxIrohRetrySchedule|RetrySchedule|retryAfterSeconds|authorizationFailed' . || true

Repository: manaflow-ai/cmux

Length of output: 41828


🏁 Script executed:

#!/bin/bash
set -u

echo "MobileHostIrohRuntime+SettingsControl scheduleRelayPolicyRefresh:"
sed -n '320,370p' Sources/Mobile/MobileHostIrohRuntime+SettingsControl.swift

echo
echo "MobileHostIrohRuntime+Activation activate scheduleRelayPolicyRefresh call:"
sed -n '460,510p' Sources/Mobile/MobileHostIrohRuntime+Activation.swift

echo
echo "MobileHostIrohRuntime policy schedule property:"
sed -n '100,140p' Sources/Mobile/MobileHostIrohRuntime.swift

echo
echo "Shared host runtime policy refresh retry delay:"
sed -n '1,80p' Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohHostRuntime+PolicyRefresh.swift

echo
echo "Shared host runtime initializer:"
sed -n '60,150p' Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohHostRuntime.swift

Repository: manaflow-ai/cmux

Length of output: 14153


🏁 Script executed:

#!/bin/bash
set -u

echo "MobileIrohRuntimeComposition scheduleRelayPolicyRefresh definition:"
sed -n '2350,2520p' ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift

echo
echo "MobileHostIrohRuntime+Lifecycle scheduleRelayPolicyRefresh implementation:"
rg -n -A120 -B20 'private func scheduleRelayPolicyRefresh|func scheduleRelayPolicyRefresh\(' Sources/Mobile/MobileHostIrohRuntime+Lifecycle.swift Sources/Mobile/MobileHostIrohRuntime+SettingsControl.swift Sources/Mobile/MobileHostIrohRuntime+Activation.swift

echo
echo "Any relayPolicyRetrySchedule usage outside iOS cmuxFeature:"
rg -n -S 'relayPolicyRetrySchedule|diagnosticFailureKind\(' Sources/Mobile Packages/Shared ios/cmuxPackage/Sources | grep -v 'Tests/cmuxFeatureTests' | head -n 120

Repository: manaflow-ai/cmux

Length of output: 25251


Apply the authorization backoff to the macOS host relay-policy refresh.

Sources/Mobile/MobileHostIrohRuntime+SettingsControl.swift:347-351 still computes the retry delay with CmxIrohRetrySchedule() only, while ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift:2491-2507 has the cause-aware helper. Use the same authorization-aware schedule here so iOS and macOS get the same 2s-120s retry path.

Also applies to: 2491-2509

🤖 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/MobileIrohRuntimeComposition.swift`
around lines 2473 - 2476, Update the macOS host relay-policy refresh retry-delay
calculation in the settings-control flow to use the same cause-aware
authorization backoff as MobileIrohRuntimeComposition’s
Self.relayPolicyRetrySchedule(for:) helper. Preserve the existing failureCount
and retryAfterSeconds inputs, ensuring authorization failures follow the shared
2–120 second schedule instead of only CmxIrohRetrySchedule().

Comment on lines +126 to 149
},
recoveredCredentialPair: { [weak auth] rejected in
guard let auth else { return nil }
// Re-capture first: when another lane already rotated the
// session, the fresh snapshot differs from the rejected
// pair and no extra mint is needed. Only an unchanged
// access token forces a mint; the SDK store dedups
// concurrent refreshes.
if let session = try? await auth.authenticatedSessionSnapshot(),
session.accountID == accountID,
session.accessToken != rejected.accessToken {
return CmxIrohBrokerCredentials(
accessToken: session.accessToken,
refreshToken: session.refreshToken
)
}
guard (try? await auth.forceRefreshAccessToken()) != nil,
let session = try? await auth.authenticatedSessionSnapshot(),
session.accountID == accountID else { return nil }
return CmxIrohBrokerCredentials(
accessToken: session.accessToken,
refreshToken: session.refreshToken
)
}

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 | 🟠 Major | 🏗️ Heavy lift

Extract the duplicated account-pinned credential recovery logic into one shared helper.

Both sites implement the identical exactly-once recovery sequence: re-capture the session and reuse it if the access token already rotated, otherwise force-refresh and re-capture, failing closed on any account mismatch. This is security-relevant authentication logic; duplicating it across the macOS host runtime and the iOS client runtime means a future correction to one path can silently miss the other.

  • Sources/Mobile/MobileHostIrohRuntime+Activation.swift#L126-L149: replace this closure body with a call to a shared helper that takes auth, the expected account ID, and the rejected pair.
  • ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift#L2879-L2902: replace this closure body with the same shared helper call.

Consider adding a static factory such as CmxIrohBrokerTokenSource.accountPinned(expectedAccountID:sessionSnapshot:forceRefresh:) in CmuxIrohTransport, or a helper in the shared auth package, so both platforms call one implementation.

♻️ Sketch of a shared extraction
extension CmxIrohBrokerTokenSource {
    /// Account-pinned token source with exactly-once 401 recovery: reuses an
    /// already-rotated session, otherwise force-refreshes once. Fails closed
    /// on any account mismatch.
    static func accountPinned(
        expectedAccountID: String,
        sessionSnapshot: `@escaping` `@Sendable` () async -> (
            accountID: String, accessToken: String, refreshToken: String
        )?,
        forceRefresh: `@escaping` `@Sendable` () async -> Bool
    ) -> CmxIrohBrokerTokenSource {
        func pair(_ session: (accountID: String, accessToken: String, refreshToken: String)) -> CmxIrohBrokerCredentials {
            CmxIrohBrokerCredentials(accessToken: session.accessToken, refreshToken: session.refreshToken)
        }
        return CmxIrohBrokerTokenSource(
            credentialPair: {
                guard let session = await sessionSnapshot(),
                      session.accountID == expectedAccountID else { return nil }
                return pair(session)
            },
            recoveredCredentialPair: { rejected in
                if let session = await sessionSnapshot(),
                   session.accountID == expectedAccountID,
                   session.accessToken != rejected.accessToken {
                    return pair(session)
                }
                guard await forceRefresh(),
                      let session = await sessionSnapshot(),
                      session.accountID == expectedAccountID else { return nil }
                return pair(session)
            }
        )
    }
}
📍 Affects 2 files
  • Sources/Mobile/MobileHostIrohRuntime+Activation.swift#L126-L149 (this comment)
  • ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift#L2879-L2902
🤖 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/MobileHostIrohRuntime`+Activation.swift around lines 126 -
149, Extract the duplicated account-pinned credential recovery sequence into one
shared helper or static factory near CmxIrohBrokerTokenSource, preserving
session re-capture, access-token rotation reuse, one force-refresh fallback,
account-ID validation, and fail-closed behavior. In
Sources/Mobile/MobileHostIrohRuntime+Activation.swift lines 126-149 and
ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift lines
2879-2902, replace each recoveredCredentialPair closure body with calls to that
shared implementation, passing auth/session snapshot, expected account ID, and
force-refresh behavior.

azooz2003-bit and others added 3 commits July 31, 2026 02:58
foregroundTerminalBrokerFailureRevokesLocalPolicy used a 401 as its
terminal failure; auth rejections are deliberately no longer terminal,
so the revocation case now uses a genuinely terminal 400 and 401/403
join the parameterized preserved-set coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The preserved-policy widening must not cross the security boundary the
never-consults-offline tests pin: an authenticated denial (401/403)
keeps already-verified in-memory state during a refresh, but it must
never unlock the offline policy store or dial-time cached grants — a
revoked account or binding stops dialing at the next dial, not at grant
expiry. resolvePolicy's offline bootstrap stays connectivity-only, and
the registry context provider and host cached-policy fallbacks now key
on a strict isAvailabilityFailure classifier instead of the widened
preserve set.

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

Copy link
Copy Markdown
Collaborator Author

Superseded by #9344: same fixes redesigned as a single credential authority on post-connectivity-v2 main (this branch conflicted after the v2 merge). The backoff-schedule piece landed separately with the v2 closeout.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant