fix(sync): validate loopback ADE handshake before publishing a port#816
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
📝 WalkthroughWalkthroughThe PR validates ADE loopback ownership before sync discovery and relay forwarding, exposes listener, tailnet, and relay route health through sync status and ChangesSync route validation and health
iOS reconnect endpoint recovery
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@copilot review but do not make fixes |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ade-cli/src/services/sync/syncHostService.ts (1)
5295-5306: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAdd coverage for the shared-listener mismatch throw path. The current tests cover the self-owned listener rejection and the shared-listener success case, but not the
waitUntilListening()branch that throws whensharedListener.getLoopbackValidationStatus()doesn’t match the returned port.🤖 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 `@apps/ade-cli/src/services/sync/syncHostService.ts` around lines 5295 - 5306, Add a test covering the shared-listener branch in waitUntilListening where getLoopbackValidationStatus returns an unvalidated status or a mismatched port. Assert that waitUntilListening rejects with the ADE-validation error and that publishValidatedDiscovery is not invoked, while preserving the existing shared-listener success and self-owned listener tests.Source: Path instructions
🧹 Nitpick comments (2)
apps/ade-cli/src/services/sync/syncTunnelClientService.ts (1)
206-228: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy liftConfirm the relay/phone gets a timely rejection when loopback validation fails during
openTunnel.On
assertAdeLoopbackListenerfailure, the function only logssync_tunnel.loopback_validation_failedand returns — it never opens (or opens-then-closes) thepipesocket back to the relay for thisconnectionId. Unless the relay worker has its own short timeout for un-pipedconnectionIds, the requesting phone could be left waiting on its own client-side timeout instead of getting a fast, explicit rejection, which seems to run counter to the PR's stated "relay fail-fast coverage" goal.Consider having the host briefly open then immediately close the pipe with a specific close code/reason (or send an explicit control-channel rejection) so the relay can propagate a fast failure to the caller.
🤖 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 `@apps/ade-cli/src/services/sync/syncTunnelClientService.ts` around lines 206 - 228, The openTunnel failure path after assertAdeLoopbackListener rejects without notifying the relay. Update openTunnel to briefly open and immediately close the pipe for the current connectionId using the established failure close code/reason mechanism, or send the existing explicit control-channel rejection, while preserving the current logging and return behavior.apps/ade-cli/src/services/sync/sharedSyncListener.ts (1)
327-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared loopback-validation-status update into a helper. Both sites independently rebuild the identical
SyncLoopbackValidationStatussuccess/failure shape right afterassertAdeLoopbackListener, which is one root cause split across two files.
apps/ade-cli/src/services/sync/sharedSyncListener.ts#L327-L392: replace the inlineloopbackValidationStatus = {...}construction inbindOnce'sonListeninghandler with a call to a shared helper (e.g.applyLoopbackProbeOutcome(current, port, resultOrError)exported fromsyncLoopbackProbe.ts).apps/ade-cli/src/services/sync/syncHostService.ts#L2845-L2885: replace the inline construction insidevalidateListeningPortwith the same shared helper.♻️ Suggested shared helper (syncLoopbackProbe.ts)
export function applyLoopbackProbeSuccess( current: SyncLoopbackValidationStatus, port: number, result: SyncLoopbackProbeResult, ): SyncLoopbackValidationStatus { return { port, loopbackAdeValidated: true, lastFailureAt: current.lastFailureAt, reason: null, lastSuccessAt: result.checkedAt, }; } export function applyLoopbackShadowFailure( current: SyncLoopbackValidationStatus, error: LoopbackShadowedError, ): SyncLoopbackValidationStatus { return { port: error.port, loopbackAdeValidated: false, lastFailureAt: error.failedAt, reason: error.message, lastSuccessAt: current.lastSuccessAt, }; }🤖 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 `@apps/ade-cli/src/services/sync/sharedSyncListener.ts` around lines 327 - 392, Extract the duplicated loopback validation status construction into shared helpers in syncLoopbackProbe.ts, covering successful probes and LoopbackShadowedError failures while preserving the existing current-status fields. In apps/ade-cli/src/services/sync/sharedSyncListener.ts lines 327-392, replace bindOnce’s inline assignments in onListening with the shared helper; apply the same change in apps/ade-cli/src/services/sync/syncHostService.ts lines 2845-2885 within validateListeningPort, with no direct changes needed elsewhere.
🤖 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 `@apps/ade-cli/src/services/sync/syncService.ts`:
- Around line 1228-1271: Update the relayReason computation in the route-health
construction to treat relayBridgeValidated === false as unhealthy after relay
control connectivity is confirmed. Add a branch that reports the bridge as not
validated before falling through to tunnelStatus.lastError, so relay.reason
remains non-null until the current sync port has been validated; preserve the
existing disabled, loopback-invalid, unavailable-status, and
disconnected-control branches.
---
Outside diff comments:
In `@apps/ade-cli/src/services/sync/syncHostService.ts`:
- Around line 5295-5306: Add a test covering the shared-listener branch in
waitUntilListening where getLoopbackValidationStatus returns an unvalidated
status or a mismatched port. Assert that waitUntilListening rejects with the
ADE-validation error and that publishValidatedDiscovery is not invoked, while
preserving the existing shared-listener success and self-owned listener tests.
---
Nitpick comments:
In `@apps/ade-cli/src/services/sync/sharedSyncListener.ts`:
- Around line 327-392: Extract the duplicated loopback validation status
construction into shared helpers in syncLoopbackProbe.ts, covering successful
probes and LoopbackShadowedError failures while preserving the existing
current-status fields. In apps/ade-cli/src/services/sync/sharedSyncListener.ts
lines 327-392, replace bindOnce’s inline assignments in onListening with the
shared helper; apply the same change in
apps/ade-cli/src/services/sync/syncHostService.ts lines 2845-2885 within
validateListeningPort, with no direct changes needed elsewhere.
In `@apps/ade-cli/src/services/sync/syncTunnelClientService.ts`:
- Around line 206-228: The openTunnel failure path after
assertAdeLoopbackListener rejects without notifying the relay. Update openTunnel
to briefly open and immediately close the pipe for the current connectionId
using the established failure close code/reason mechanism, or send the existing
explicit control-channel rejection, while preserving the current logging and
return 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: CHILL
Plan: Pro
Run ID: b1b560e3-1105-46ab-b383-48520312bf72
📥 Commits
Reviewing files that changed from the base of the PR and between 4eb8ce8 and 8b0ba382f28eb3b0fc5738e5fe7bc5e24e564e04.
📒 Files selected for processing (14)
apps/ade-cli/src/bootstrap.tsapps/ade-cli/src/cli.test.tsapps/ade-cli/src/cli.tsapps/ade-cli/src/services/sync/sharedSyncListener.tsapps/ade-cli/src/services/sync/syncHostService.test.tsapps/ade-cli/src/services/sync/syncHostService.tsapps/ade-cli/src/services/sync/syncLoopbackCollision.test.tsapps/ade-cli/src/services/sync/syncLoopbackProbe.tsapps/ade-cli/src/services/sync/syncService.tsapps/ade-cli/src/services/sync/syncTunnelClientService.test.tsapps/ade-cli/src/services/sync/syncTunnelClientService.tsapps/desktop/src/shared/types/sync.tsapps/ios/ADE/Services/SyncService.swiftapps/ios/ADETests/ADETests.swift
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
https://github.com/arul28/ADE/blob/d3d96139b695c9e119ba12e4d4e4e5f9d0f2e679/apps/ade-cli/src/services/sync/syncHostService.ts#L5395-L5396
Revalidate before republishing discovery routes
When discovery is toggled back on for an already-bound host, this path calls the raw Bonjour and Tailscale publishers without the new ADE loopback handshake. If 127.0.0.1:<port> becomes shadowed while discovery is disabled, re-enabling discovery republishes the stale port and routes phones/Tailscale Serve back to the foreign listener, which is the collision this change is meant to block. Route re-enable should go through the validated publish path and surface the validation failure instead of publishing.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Host-side fix for the 2026-07-14 loopback port-collision incident: a foreign 127.0.0.1:<port> listener shadowed ADE's 0.0.0.0 bind with no EADDRINUSE, routing Tailscale + relay to the wrong process. A 426 loopback probe now validates each port before it is accepted/published; shadowed ports drift to a validated-free port with atomic route republish; relay validates before forwarding; ade sync status + doctor report per-route health. 343 tests + a real macOS collision repro green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate relayReason on relayBridgeValidated so ade doctor / sync status surface a stale relay bridge (control-connected but not revalidated against the current sync port) instead of reporting the route healthy. Mirrors the tailscaleReason gating so the diagnostics this PR adds don't hide a stale-but-connected bridge. Adds a producer-level route-health regression test (relay bridge not validated => non-null relay.reason) and coverage for the shared-listener waitUntilListening ADE-validation throw path. Addresses CodeRabbit review on #816. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d3d9613 to
3027149
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 302714933a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const ok = statusCode === 426 | ||
| && (statusMessage == null || statusMessage.toLowerCase() === "upgrade required"); |
There was a problem hiding this comment.
Require an ADE-specific loopback probe
This treats any listener that returns 426 Upgrade Required as ADE, but a plain ws WebSocketServer returns the same response to a non-upgrade HTTP GET. If the colliding 127.0.0.1:<port> process is another WebSocket service, startup and relay validation will pass and publish/bridge to the wrong process again; the probe needs an ADE-specific WebSocket exchange or other ADE-only signal rather than only the generic 426 status.
Useful? React with 👍 / 👎.
| publishLanDiscovery(port, { force: options?.forceLan }); | ||
| publishTailnetDiscovery(port, { force: options?.forceTailnet }); |
There was a problem hiding this comment.
Revalidate before publishing refreshed routes
refreshLanDiscovery({ forceTailnet: true }) now calls this helper directly, but the helper republishes Bonjour/Tailscale without re-running assertAdeLoopbackListener; setDiscoveryEnabled(true) also still publishes directly. If a loopback-specific process shadows the already-bound port after startup, a refresh or discovery re-enable can advertise the stale port using the old successful validation, reintroducing the wrong-process route this change is meant to prevent.
Useful? React with 👍 / 👎.
…on gaps
Addresses three P1 review findings (Codex x2, Greptile) on the loopback
port-collision hardening. All security/reliability in the sync host path.
1) Probe was not ADE-specific (Codex P1). probeAdeLoopbackListener treated any
HTTP 426 as ADE, but a bare 'ws' server (a stale/foreign WebSocket process,
incl. a second ADE instance) also answers plain GETs with 426 — so the
'ADE handshake' could be satisfied by any WebSocket listener. Both ADE sync
server sites (syncHostService self-owned host; sharedSyncListener bindOnce)
now front their WebSocketServer with an explicit http.Server whose non-upgrade
426 response carries a marker header 'x-ade-sync-loopback: 1'; the probe
requires status 426 AND that marker. The WS upgrade path is unchanged
(WebSocketServer constructed with { server }). NOTE: the 426 deliberately omits
Upgrade/Connection headers — sending them on a non-upgrade response corrupts
Node's keep-alive state machine (subsequent requests 400).
2) Refresh/re-enable skipped revalidation (Codex P1). refreshLanDiscovery and
setDiscoveryEnabled(true) republished Bonjour/Tailscale without re-running the
loopback check, so a shadow that appeared AFTER startup could re-advertise a
stale port. Both now validateListeningPort(port,{force:true}) before
(re)publishing and fail closed (skip publish, mark route unvalidated) on a
shadow.
3) Ephemeral (port 0) shadow could not retry (Greptile P1). A loopback-shadowed
bind(0) exhausted the single-entry attempt plan and failed instead of asking
the OS for a fresh port. Port 0 now gets bounded PREFERRED_PORT_BIND_ATTEMPTS
re-binds (each a fresh OS port), tracks the RESOLVED shadowed port (not literal
0), and skips the fixed-port retry delay; loopback-shadow errors are now
retryable. Mirrored in the syncService self-owned startup loop.
Incidental: fronting ws with an external http.Server changes close() semantics
(ws.close() drains clients and does not close the http server), so listener
close / candidate cleanup / host dispose now terminate clients and close the
http server (closeAllConnections) to free the port deterministically.
Tests: new syncLoopbackProbe.test.ts (marker required); collision tests for a
markerless-426 foreign listener rejection and ephemeral-shadow retry/termination;
host tests for refresh/re-enable revalidation. tsc clean; sync suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6ba14c669
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const hasAdeMarker = markerValue === SYNC_LOOPBACK_ADE_MARKER_VALUE; | ||
| const statusOk = statusCode === 426 | ||
| && (statusMessage == null || statusMessage.toLowerCase() === "upgrade required"); | ||
| // A foreign/stale bare `ws` server also returns 426, so the status is | ||
| // necessary but not sufficient — the ADE marker must be present too. | ||
| const ok = statusOk && hasAdeMarker; |
There was a problem hiding this comment.
Require a per-listener loopback proof
When the loopback shadow is another ADE process, or any local listener that copies this public x-ade-sync-loopback: 1 marker, this probe accepts the shadow as the current listener even though Tailscale/relay traffic to 127.0.0.1:<port> will go to that other process. The check needs an unforgeable per-listener value or challenge tied to the server instance before publishing the port.
Useful? React with 👍 / 👎.
| loopbackValidationStatus = sharedListener!.getLoopbackValidationStatus(); | ||
| if (!loopbackValidationStatus.loopbackAdeValidated || loopbackValidationStatus.port !== port) { | ||
| throw new Error(`The shared sync listener on 127.0.0.1:${port} was not ADE-validated.`); | ||
| } | ||
| await publishValidatedDiscovery(port); |
There was a problem hiding this comment.
Re-probe the shared listener before publishing
On a hosted-project switch with the shared listener, this branch trusts the cached validation status from the original bind and immediately republishes discovery. If a more-specific 127.0.0.1:<port> shadow appears after that first validation, the self-owned refresh paths would catch it with a forced probe, but this shared-listener startup path reuses the stale success and can advertise a Tailscale/relay route that now targets the shadow.
Useful? React with 👍 / 👎.
…ff re-probe Addresses Codex P1 (require a per-listener loopback proof) and P2 (re-probe the shared listener before publishing) on PR #816. P1: the static public marker 'x-ade-sync-loopback: 1' was forgeable — a stale or second ADE process on 127.0.0.1:<port> emits the same constant, so the probe could not tell OUR listener from another ADE instance shadowing the port. Now each listener generates a fresh 128-bit nonce at construction (generateLoopbackNonce), stamps it as the 426 identity header, and every IN-PROCESS validator compares the probed value against the exact nonce it holds. A shadow is a different process and cannot present this instance's nonce. Threaded through: sharedSyncListener (one nonce/instance, getExpectedLoopbackNonce), self-owned host server (reuses the shared listener's nonce when injected, else its own), the syncService startup loop (via host construction), and the relay tunnel client (nonce provider wired in bootstrap.ts). The relay path fails closed when the identity is unavailable, and relayBridgeValidated now also requires the nonce to still match the current listener (a restarted listener with a new nonce invalidates a stale bridge). P2: SharedSyncListener.revalidateLoopback() force re-probes the current bound port with the instance nonce; the shared-listener branch of waitUntilListening now calls it before republishing LAN/Tailscale discovery, so a shadow that appears AFTER the original bind is caught on a project-host handoff (throws before publish; fails closed). Tests: syncLoopbackProbe.test.ts proves a different-instance ADE listener (and the legacy static '1', and a markerless 426) are rejected while the own-identity passes; collision/host tests cover the handoff re-probe and thread the nonce through the existing shadow/retry/refresh fixtures. tsc clean; sync suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1c307aa33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| resolve({ ...result, port, checkedAt }); | ||
| }; | ||
| const request = http.get({ | ||
| host: "127.0.0.1", |
There was a problem hiding this comment.
Support configured loopback bind hosts
When ADE_SYNC_BIND_HOST is set to ::1 (or to localhost on a system that resolves it to IPv6), the sync listener can bind successfully on IPv6 loopback, but this new validation always probes IPv4 127.0.0.1. In that configuration waitUntilListening/ensureListening will report the ADE listener as shadowed and refuse to publish a port even though the listener is healthy; the probe should use the configured loopback address or try both IPv4 and IPv6 loopback routes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Working as intended for the supported configs — deliberately not changing this.
The probe validates 127.0.0.1 because that is the exact address both published routes forward to: Tailscale Serve uses tcp://127.0.0.1:${port} (syncHostService.ts:2738) and the relay bridge uses ws://127.0.0.1:${port} (syncTunnelClientService.ts:251). The default bind host is 0.0.0.0 (IPv4 wildcard), which 127.0.0.1 reaches.
If ADE_SYNC_BIND_HOST=::1 (IPv6-only), the listener is not reachable at 127.0.0.1 where BOTH routes forward — so the listener is already unreachable end-to-end regardless of the probe, and refusing to publish is correct fail-closed behavior (publishing would advertise a 127.0.0.1 route that nothing answers). Making only the probe try ::1 would desync it from the route targets and could re-open the shadow gap for a mixed IPv4/IPv6 host. ::1 is not a supported bind mode today (only ADE_SYNC_BIND_HOST=127.0.0.1 is documented). Full IPv6-loopback support (bind + probe + Tailscale Serve target + relay forward, consistently) is a separate enhancement, tracked as a follow-up rather than fixed here.
|
Ship complete — merged. Coordinator note on the loopback security-protocol change made during ship: What changed in the loopback validation protocol (P1 spoofing fix):
Findings resolved (mapping to your three classes):
Codex's final P2 (IPv6 |
Sync loopback port-collision fix (host-side)
Fixes the diagnosed 2026-07-14 incident (
docs/features/sync-and-multi-device/loopback-port-collision-incident-2026-07-14.md): a foreign process binding127.0.0.1:<sync-port>shadowed ADE's0.0.0.0listener with noEADDRINUSE, so Tailscale Serve + the relay tunnel silently routed to the wrong process and mobile couldn't reach the machine (only direct-LAN worked). This is the host-side complement to the Phase 1 client-side fix.426 Upgrade Requiredruns inonListening(shared + self-owned hosts) before the port is accepted,lastPortpersisted, or any route published. A shadowed port raisesLoopbackShadowedError→ the scan drifts to a validated-free port.tailscale serve --tcp=<old> offfirst, then republishes Bonjour + Tailscale together).ade sync status+ade doctor(listener / tailscale / relay: bound / validated / published / reachable / control / bridge + timestamps + reason) — enabled ≠ usable.hello_erroradvance to the next candidate, never.connected).Tests: 343 sync/CLI + a real macOS wildcard/foreign-loopback collision repro + relay fail-fast + typechecks + iOS sim build all green. Existing bind/scan/handoff/pairing/DPoP unchanged. Deferred hardening: dedicated private loopback listener / unix-domain socket / in-process relay adoption.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
ade doctornow checks sync readiness and highlights failing routes with recommended diagnostics.Bug Fixes
Greptile Summary
This PR hardens sync route publishing and reconnect behavior around loopback port collisions. The main changes are:
ade sync statusandade doctor.Confidence Score: 5/5
Safe to merge based on the reviewed changes.
The changed paths consistently validate the local listener before publishing or bridging routes, and the previously reported retry and republish issues are addressed.
No files require special attention.
What T-Rex did
Important Files Changed
LoopbackShadowedErrorfor unsafe listener detection.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Host as ADE sync host participant Probe as Loopback probe participant LAN as Bonjour/Tailscale publish participant Relay as Cloud relay tunnel participant Phone as iOS client Host->>Host: Bind candidate sync port Host->>Probe: GET 127.0.0.1:port with expected nonce Probe-->>Host: 426 + ADE loopback marker alt Marker matches Host->>LAN: Publish validated LAN/Tailnet route Relay->>Probe: Validate loopback before opening pipe Probe-->>Relay: Current ADE listener confirmed Phone->>Host: Connect and complete hello else Shadowed or wrong listener Host->>Host: Close candidate and try next port Relay-->>Relay: Refuse bridge and record route-health failure Phone->>Phone: Try next endpoint candidate end%%{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 Host as ADE sync host participant Probe as Loopback probe participant LAN as Bonjour/Tailscale publish participant Relay as Cloud relay tunnel participant Phone as iOS client Host->>Host: Bind candidate sync port Host->>Probe: GET 127.0.0.1:port with expected nonce Probe-->>Host: 426 + ADE loopback marker alt Marker matches Host->>LAN: Publish validated LAN/Tailnet route Relay->>Probe: Validate loopback before opening pipe Probe-->>Relay: Current ADE listener confirmed Phone->>Host: Connect and complete hello else Shadowed or wrong listener Host->>Host: Close candidate and try next port Relay-->>Relay: Refuse bridge and record route-health failure Phone->>Phone: Try next endpoint candidate endComments Outside Diff (1)
apps/ade-cli/src/services/sync/syncHostService.ts, line 5395-5396 (link)setDiscoveryEnabled(true)republishes Bonjour and Tailscale directly, unlikewaitUntilListeningandrefreshLanDiscovery. If discovery is toggled back on after127.0.0.1:<port>becomes shadowed, this path publishes the stale port without the ADE loopback handshake, reintroducing the wrong-process route this PR is meant to block. Route re-enable should go through the validated publish path and surface or log validation failure instead of publishing.Prompt To Fix With AI
Reviews (5): Last reviewed commit: "fix(sync): per-instance loopback identit..." | Re-trigger Greptile