Skip to content

Harden reconnect: host cooldown parity, persistent diagnostics, per-device rate limits - #8576

Merged
azooz2003-bit merged 7 commits into
feat-iroh-rl-lockoutfrom
feat-iroh-hardening
Jul 22, 2026
Merged

azooz2003-bit merged 7 commits into
feat-iroh-rl-lockoutfrom
feat-iroh-hardening

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Stacked on #8557. Three hardening legs so the reconnect lockout class cannot recur, plus the diagnostics needed to root-cause the underlying session drops.

  1. Mac host parity (fix(iroh): extend broker cooldown to the Mac host and bare 429s): the host runtime gets the same account-scoped Retry-After cooldown, single-mint activation, and precise diagnostic kinds for the previously-unclassified activation failures. A 429 without a Retry-After header now arms a short default floor on both client and host, so a missing header can never reopen the retry storm.
  2. Diagnostics that survive relaunch (feat(ios): persist diagnostics across launches and add verbose log opt-in): both field reports were young processes — the in-memory ring died with each relaunch, erasing the drop moment. The ring is now archived on scene background and the Share Safe Report export prepends the previous launch's block. A localized (en/ja) Verbose Connection Log toggle additionally enables the durable debug-log file in release builds, with a Share Verbose Log row; terminal contents and credentials are never written to it.
  3. Per-device rate limiting (fix(web): partition relay token rate limit per device): the relay token limiter keyed by account only, so one storming device starved every build on the account. It now keys per account+endpoint and runs after validation; the devices/iroh routes already partition per account/operation/identity.

Verification: CmuxIrohTransport 440 tests green locally (new directive-seconds tests); CMUXMobileCore 237 green locally (new archive tests); CmuxMobileDiagnostics 14 green; web relay-token route tests 7/7 green with the per-device contract asserted, tsc clean; tagged macOS build (irhrd) green; full iOS app build green and installed on an isolated simulator. The new Settings rows are compile- and build-verified; on-device toggle interaction is pending the dogfood pass.

Localization audit: three new keys added to CmuxMobileShellUI Localizable.xcstrings with en and ja translations; no other user-facing strings changed.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Prevents reconnect lockouts and account-wide retry storms while making drops easier to diagnose. Adds host cooldown parity, durable cross-launch diagnostics with lifecycle/reachability attribution, and per-device relay token rate limits.

  • New Features

    • Persist diagnostic reports across launches via CMUXMobileCore DiagnosticReportArchive; exports prepend the previous launch and archive on scene inactive/background (work runs off the main actor).
    • iOS: Add “Verbose Connection Log” toggle in CmuxMobileDiagnostics/CmuxMobileShellUI; persists only if the sink opens the file, supports runtime enable/disable, and adds “Share Verbose Log” (DEBUG always logs; Release only when enabled).
    • Diagnostics attribution: record app lifecycle and reachability changes, and include the recovery trigger in each “recovery started” event so drops read as suspension, network move, or a specific retry cause.
  • Bug Fixes

    • Host parity for broker cooldown in CmuxIrohTransport/mobile host: respect account-scoped Retry-After, default 60s for bare 429 via CmxIrohBrokerCooldown.directiveSeconds, gate re-activation with retryScheduled, clear on success; map offline-policy/binding errors to precise diagnostic kinds; reuse bootstrap relay credential.
    • Client hardening: suppress same-route calls during Retry-After via per-route cooldowns in CmxIrohTrustBrokerClient; allow fresh authenticated discovery when registration is rate-limited; treat Retry-After up to 24h.
    • Web relay token route: rate-limit per account+endpoint (after validation) so a storming device only starves itself; updated web/services/relay/http.ts to key as account:device.
    • Diagnostics persistence: clear the archived report and its cache on sign-out/account erase; archive reads/writes happen in a detached utility task; the Settings toggle reverts if enabling verbose logging fails.

Written for commit 7541d43. Summary will update on new commits.

Review in cubic

Parity hardening on top of the client reconnect fix:

- The Mac host runtime now records the same account-scoped broker cooldown
  when activation legs fail with a rate-limit directive, gates re-activation
  on it (zero broker calls while floored, retryScheduled diagnostic), and
  clears it on successful activation, so a Mac can no longer storm the
  broker or keep a shared rate-limit window exhausted.
- Host activation reuses the bootstrap-minted relay credential
  (refreshWithCredential) instead of minting twice.
- A 429 without a Retry-After header now arms a short default floor via the
  shared CmxIrohBrokerCooldown.directiveSeconds helper (client and host), so
  a missing header can never reopen the retry storm.
- The offline-policy and binding-expectation errors carry diagnostic failure
  kinds instead of decoding as unknown (the b=255 field signature).

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

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 705f565a-c41b-4e86-809f-ecb784397c27

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-iroh-hardening

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 21, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens reconnect handling and adds durable mobile diagnostics. The main changes are:

  • Account-aware broker cooldowns for client and host reconnect paths.
  • Previous-launch diagnostic archives and lifecycle attribution.
  • An opt-in verbose connection log with localized Settings controls.
  • Per-device relay-token rate limiting.

Confidence Score: 4/5

These two state-consistency issues should be fixed before merging.

  • Pending archive tasks can write after sign-out and restore cleared account data.
  • Settings can show verbose logging as enabled when startup failed to open the file.
  • The reconnect cooldown and per-device limiter changes otherwise have no confirmed blocking issue from this review.

MobileIrohRuntimeComposition.swift and MobileIrohSettingsModel.swift

Security Review

A pending archive write can recreate a cleared report after sign-out, allowing the prior account's diagnostic data to appear in a later export.

Important Files Changed

Filename Overview
ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift Adds lifecycle archive persistence and clearing, but pending detached writes can restore cleared data or overwrite newer snapshots.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileIrohSettingsModel.swift Adds verbose-log controls, but startup UI state can disagree with the file sink after an open failure.
Packages/iOS/CmuxMobileDiagnostics/Sources/CmuxMobileDiagnostics/MobileDebugLog.swift Persists runtime toggle changes after sink acceptance, while startup failures are not reflected back into Settings state.
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/DiagnosticReportArchive.swift Adds bounded atomic archive storage whose callers must still coordinate concurrent save and clear operations.

Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'origin/fea..." | Re-trigger Greptile

Comment on lines +156 to +157
private let diagnosticArchive = DiagnosticReportArchive.defaultArchive()
private var previousLaunchDiagnosticReport: DiagnosticReport??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Archive Crosses Account Boundaries

The new archive and its cache are process-scoped, but sign-out only clears the live diagnostic ring. After account A backgrounds and signs out, account B can export A's archived timeline because neither wipeLocalState() nor sign-out preparation clears these values. Clear both values during account erasure or scope the archive by account.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

Comment on lines +50 to +53
public func setFileLogging(enabled: Bool) async -> Bool {
UserDefaults.standard.set(enabled, forKey: Self.verboseLogDefaultsKey)
return await sink.setFileLogging(enabled: enabled)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Failed Enable Persists As Active

This stores true before the sink confirms that it opened the log file, and the Settings caller discards the returned failure. If directory creation, rotation, or opening fails, the toggle remains enabled across the current session and relaunch even though no log is being recorded.

Comment on lines +599 to +609
if previousLaunchDiagnosticReport == nil {
previousLaunchDiagnosticReport = .some(diagnosticArchive?.load())
}
let runtime = runtime
let diagnosticLog = diagnosticLog
let diagnosticArchive = diagnosticArchive
sceneTransitionTask = Task {
// Archive the diagnostic ring while backgrounded so a later
// relaunch keeps the events around a drop exportable.
if let diagnosticLog, let diagnosticArchive {
diagnosticArchive.save(await diagnosticLog.snapshot())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Archive I/O Blocks Main Actor

didEnterBackground() synchronously loads the archive, and its unstructured Task inherits this type's MainActor isolation before performing the synchronous atomic save. A slow filesystem or near-limit archive can stall the UI and consume the app's background suspension window; move archive reads and writes behind an explicit non-main async boundary.

Rule Used: Flag incorrect or missing use of Swift @Concurrent... (source)

cmux reload-cloud and others added 2 commits July 21, 2026 14:08
…t-in

Two export lanes so connection drops stay diagnosable in release builds:

- The privacy-safe diagnostic ring is archived when the scene backgrounds
  (DiagnosticReportArchive, bounded JSON in Application Support) and the
  Share Safe Report export now prepends the previous launch's block, so a
  drop followed by a relaunch no longer erases the evidence.
- A localized Verbose Connection Log toggle in the Iroh settings enables the
  durable debug-log file in release builds (DEBUG keeps logging always-on),
  persisted across launches, with a Share Verbose Log row for the file. The
  sink gained a runtime setFileLogging toggle; terminal contents and
  credentials are never written to this lane.

Localization audit: three new keys added to CmuxMobileShellUI
Localizable.xcstrings with en and ja translations; no other user-facing
strings changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The relay token limiter keyed by account only, so one storming device
exhausted the budget for every phone, simulator, and tagged build on the
account. The check now keys by account plus the validated endpoint id and
runs after body validation, so malformed requests never consume a device's
budget and a misbehaving device only starves itself. The devices/iroh routes
already partition per account, operation, and registration identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmux reload-cloud and others added 3 commits July 21, 2026 14:33
…events

Three export-vocabulary additions so a single Share Safe Report can name a
drop's cause remotely:

- appLifecycleChanged (52): scene phase transitions recorded at the existing
  composition hooks, so a session that closes seconds after backgrounding
  reads as a suspension casualty rather than a network failure. Verified
  live on the simulator (inactive/active/inactive/background sequence in the
  archived report).
- reachabilityChanged (53): the shell's network path observation records
  online state on every transition, correlating drops with WiFi/cellular
  moves.
- recoveryStarted now carries the recovery trigger in its b slot (stable
  append-only codes), so every reconnect cycle names why it began: network
  change, manual retry, presence push, foreground, liveness, stream end,
  subscription failure, write timeout, or backoff expiry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Account erasure and sign-out now clear the archived diagnostic report and
  its in-memory cache, so a second account can never export the previous
  account's connection timeline.
- The verbose-log opt-in persists only when the sink actually opened the
  file, and the Settings toggle reverts when enabling fails, so the UI can
  no longer claim to be recording without a log.
- Archive reads and writes moved behind a detached utility task; scene
  backgrounding no longer spends main-actor time or the suspension window on
  filesystem work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +612 to +617
Task.detached(priority: .utility) { [weak self] in
let previous = needsPreviousLoad ? diagnosticArchive.load() : nil
if needsPreviousLoad {
await self?.cachePreviousLaunchReport(previous)
}
diagnosticArchive.save(await diagnosticLog.snapshot())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Serialize pending archive writes

Each inactive or background transition starts an independent detached task that writes to the same archive. A task already waiting for diagnosticLog.snapshot() can finish after sign-out clears the archive and recreate it with the previous account's events. The next account can then export that report. The tasks can also finish out of order, letting the older inactive snapshot replace the newer background snapshot. Serialize these operations and invalidate pending writes when diagnostics or account state are cleared.

Comment on lines +18 to +20
private(set) var verboseLogEnabled = UserDefaults.standard.bool(
forKey: MobileDebugLog.verboseLogDefaultsKey
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Verify startup logging state

This initializes the toggle from the saved preference, but the sink can fail to open its file during startup and silently disable file logging. After such a failure, Settings still shows logging as enabled while file writes are discarded, and an old file can keep the Share row visible. Reconcile this value with the sink's actual startup state before presenting it as enabled.

…iroh-hardening

# Conflicts:
#	ios/cmuxPackage/Sources/cmuxFeature/MobileIrohRuntimeComposition.swift
Comment on lines +615 to +621
let needsPreviousLoad = previousLaunchDiagnosticReport == nil
Task.detached(priority: .utility) { [weak self] in
let previous = needsPreviousLoad ? diagnosticArchive.load() : nil
if needsPreviousLoad {
await self?.cachePreviousLaunchReport(previous)
}
diagnosticArchive.save(await diagnosticLog.snapshot())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Archive writes remain unordered. Each inactive or background transition starts an independent detached task. Sign-out and report clearing remove the file but do not cancel, invalidate, or await these tasks. A pending task can therefore save after the clear and restore the previous account's report. Two lifecycle tasks can also finish out of order, replacing the newer background snapshot with the older inactive snapshot. Serialize archive operations and invalidate pending writes when account or diagnostic state is cleared.

Comment on lines +18 to +20
private(set) var verboseLogEnabled = UserDefaults.standard.bool(
forKey: MobileDebugLog.verboseLogDefaultsKey
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Startup state stays stale. If the saved preference is enabled but the sink cannot open its file during startup, the sink disables file logging while this property remains true. Settings then shows that verbose logging is active even though writes are discarded, and an existing file may still expose the Share row. Initialize this value from the sink's effective state or reconcile it after startup.

@azooz2003-bit
azooz2003-bit merged commit 2ac26f0 into feat-iroh-rl-lockout Jul 22, 2026
9 checks passed
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