Skip to content

Make pairing and adoption survive a flaky network - #952

Merged
arul28 merged 6 commits into
mainfrom
ade/pr4-pairing-adoption-e5aafbb4
Jul 29, 2026
Merged

Make pairing and adoption survive a flaky network#952
arul28 merged 6 commits into
mainfrom
ade/pr4-pairing-adoption-e5aafbb4

Conversation

@arul28

@arul28 arul28 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Why

Three defects made first-connect and re-pair fragile, all found during a live mobile-connection audit:

  1. A failed re-pair destroyed a working pairing. The host rotated the pairing secret ~2 round trips before the phone persisted it, so a drop in that window left the host holding credentials the phone never saved — the pairing was dead until a manual re-pair. On a flaky relay, retrying was the thing that broke you.
  2. First-connect was serial. Account adoption walked lan → tailnet → relay one route at a time at 5s socket open + 3s challenge each, so cellular users waited 10-20s before relay was even attempted.
  3. Any adoption failure latched auto-reconnect off, persisted in UserDefaults across relaunch — which is why the app opened on a dead "Disconnected" screen with a manual Reconnect button.

What changed

  • Staged pairing rotation with explicit commit/ack. The host keeps both old and new secrets valid until the client acknowledges the replacement, bound to the exact staged secret so a stale socket can't promote a newer rotation. Legacy clients keep hello-as-commit semantics. A dropped retry can no longer brick a pairing.
  • Adoption routes now race on the unified transport machinery from fix(ios): race every transport at once and stop replacing healthy connections #948 instead of a sequential for-loop.
  • autoReconnectPausedByUser only latches on an explicit user disconnect, with a one-time migration that clears failure-fallout pauses.
  • PIN limiting is per-device/per-address, not one global bucket — five wrong PINs on one phone used to lock pairing and account adoption for every device in the house. Thresholds tuned for onboarding: 10 tries, 2-minute cooldown (a 6-digit code against a 2-minute cooldown is still ~7 guesses/min against a 1e6 space).
  • Typed auth errors. "Sync authentication failed." covered fourteen distinct causes with fourteen different fixes; each now says what to do.
  • Unknown AEAD name is a compatibility failure for that route, not a security abort of all routes.

Verification

  • Host suites pass (sync host service, pairing store, pair-failure tracker), including new coverage for flaky re-pairs, commit expiry/races, per-device limiting, and typed errors.
  • iOS: BUILD SUCCEEDED, 1053 tests run locally on iPhone 17 Pro sim. The only failures are 2 pre-existing testDeepLinkRouter* cases that fail identically on clean main (tracked separately).
  • Not on-device verified; the 3-phase phone walkthrough is the acceptance gate.

🤖 Generated with Claude Code

ADE   Open in ADE  ·  ade/pr4-pairing-adoption-e5aafbb4 branch  ·  PR #952

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 29, 2026 9:01pm

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@arul28, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9a1173a-e5db-44e1-a6f0-0d14240eee28

📥 Commits

Reviewing files that changed from the base of the PR and between 7c4a8b8 and 375e021.

📒 Files selected for processing (12)
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ade-cli/src/services/sync/syncDpop.ts
  • apps/ade-cli/src/services/sync/syncHostService.test.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ade-cli/src/services/sync/syncPairFailureTracker.test.ts
  • apps/ade-cli/src/services/sync/syncPairFailureTracker.ts
  • apps/ade-cli/src/services/sync/syncPairingStore.test.ts
  • apps/ade-cli/src/services/sync/syncPairingStore.ts
  • apps/desktop/src/main/services/remoteRuntime/syncPairedMachineStore.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADETests/ADETests.swift

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.

arul28 and others added 5 commits July 29, 2026 16:52
A re-pair overwrote the device's working secret the moment the host
answered, two round trips before the device could persist the reply. A
drop in that gap left the host holding credentials the phone never saw,
and the only way back was another PIN typed at the Mac -- so on a flaky
network, retrying was what broke you.

A re-pair now stages: the committed secret stays live and the
replacement waits in a pending slot for 10 minutes, promoted by the
first hello that authenticates with it. Proof of possession IS the
commit, so no new wire message is needed and clients that predate this
get the protection unchanged. `pairing_result` carries an advisory
`rotation` field for diagnosis only. Only PIN pairing stages; account
adoption returns its secret inside hello_ok with no acknowledged
follow-up, and the local OS/SSH path hands it back in-process.

Also:
- Bucket failed PIN attempts by device as well as address, and raise the
  global breaker to 25. Five fumbled PINs on one phone used to block
  pairing and account adoption for every device in the house.
- Give each of the collapsed hello rejections its own actionable
  message; "Sync authentication failed." covered fourteen distinct
  causes with fourteen different fixes.
- Fold two copies of the pair-failure limiter into one shared tracker.

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

Account adoption walked [lan, tailnet, relay] strictly in order, paying a
socket open plus a 3s identity challenge per route. On cellular, where no
direct route can ever succeed, that is 10-20s of dead time before relay
is even dialed. It now races every route at once through the same
machinery the paired reconnect path uses (#948): one plan, relay joining
behind the leading direct candidate rather than after it, one 10s
budget. Each candidate owns its socket and mailbox, so only the winner
touches app state; the challenge crypto is unchanged.

Any adoption failure also latched `autoReconnectPausedByUser`, which is
persisted and blocks reconnecting forever -- so one bad connect left the
app opening on a dead "Disconnected" screen for good. It is now written
only by an explicit user action, which stamps a source alongside it; a
paused flag with no source is provably fallout from an older build and
is cleared once.

Also:
- A host naming an adoption cipher this build does not implement now
  fails that route instead of aborting the attempt with an identity
  verification error. The cipher is still never used.
- Persist a PIN pairing secret before the hello, so a lost hello_ok
  cannot strand the phone on a secret the Mac already retired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Negotiate explicit pairing commits for clients that persist replacement credentials before hello. Keep legacy hello-as-commit behavior for older clients and bind acknowledgements to the exact staged secret so stale sockets cannot promote a newer rotation.

Extend host coverage for flaky re-pairs, commit expiry and races, per-device limiting, and actionable authentication errors. Add pure iOS coverage for persistence ordering and commit negotiation without running an iOS build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten tries and a two-minute cooldown per device instead of five tries and
ten minutes. Fumbling a 6-digit code while setting up a phone is the
common case, not an attack; a two-minute wait still caps a LAN brute
force at roughly seven codes a minute against a million-code space.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two suites disagreed about a staged re-pair. The desktop copy asserted a
renamed device shows its new name straight away; the new ade-cli coverage
asserted the whole staged record stays hidden. Splitting them by what is
actually a credential settles it: the secret, its DPoP binding, runtime-host
grant, and account ownership wait for the acknowledgement, while peerName,
platform, and device type apply immediately. A user who renames a phone
should not wait on a handshake they cannot see.

Also key the two PIN-limit loops off PAIR_FAILURE_THRESHOLD instead of
hardcoding the old value, so retuning the threshold cannot silently break
them again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28
arul28 force-pushed the ade/pr4-pairing-adoption-e5aafbb4 branch from a81d608 to 0171833 Compare July 29, 2026 20:56
Staging protected more than the secret. A PIN re-pair is supposed to
declassify an account-owned pairing, but staging that left the committed
record account-owned, so the next account switch revoked the pairing the
user had just re-established at the Mac. A withdrawn runtime-host grant
had the same shape: authority the re-pair removed stayed readable until an
acknowledgement that may never arrive.

Split by direction instead of by field: elevations wait for proof,
reductions land at once. Costs at most one re-grant; the alternative is a
privilege leak and a destroyed pairing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28
arul28 merged commit 5f83ff7 into main Jul 29, 2026
3 checks passed
@arul28
arul28 deleted the ade/pr4-pairing-adoption-e5aafbb4 branch July 31, 2026 04:53
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