S11: make presence/discovery state honest on apply failure

SetPresenceMode and the Discoverable time-box auto-revert both committed
the new presence_mode to local state *before* apply_discovery and only
log_msg'd on failure, so a failed off-transition could leave the n0 DNS
PkarrPublisher running while the UI showed not-discoverable (privacy /
reality mismatch — security-open-handoff S11, from the W7 P7 review).

Fix (Codex, senior-reviewed):
- discovery.rs: pure resolve_presence_transition(prev, requested, apply_ok)
  -> (mode, Option<error>) seam — on failure keep the previous (truthful)
  mode and surface a message. +4 unit tests.
- apply_discovery now builds the replacement resolver/publisher services
  BEFORE clearing the service set, so a builder failure leaves the old
  posture fully intact (no partial state) — "keep previous mode" is then
  provably truthful.
- Both SetPresenceMode and the time-box revert apply discovery first, route
  through the seam, commit only the truthful mode, and surface failures via
  the existing PresenceModeReverted (corrects the picker) + UiEvent::Error.
  No new wire/event variant.
- A failed off-transition stays Discoverable and arms a 60s retry
  (DISCOVERY_REVERT_RETRY) so the beacon never stands stuck.
- P3 notes documented: relay-resolve exposes n0 query metadata (by design);
  no explicit iroh unpublish API exists, so the bounded ~30s pkarr TTL
  linger is documented, not behavior-changed; DirectOnly stays no-n0.

306 lib tests / clippy --all-targets / release all green (re-run by senior).
Runtime publish-stop behavior still wants a 2-machine / packet-capture check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 03:32:38 -04:00
co-authored by Claude Opus 4.8
parent d56c2c90b2
commit 319d0c5e29
4 changed files with 259 additions and 57 deletions
+7 -5
View File
@@ -955,13 +955,15 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
state.friend_presence.insert(id, presence);
}
UiEvent::PresenceModeReverted { mode } => {
// The Discoverable time-box elapsed; core dropped us back to
// `mode` (Normal) and stopped publishing. Mirror + persist so the
// presence picker reflects it, and tell the user why it changed.
// Core corrected the committed presence mode. Mirror + persist so
// the picker reflects the discovery state the endpoint actually has.
state.config.presence_mode = mode;
state.config.save();
state.status_message =
"Discoverable timed out — back to Normal".to_string();
state.status_message = if mode == PresenceMode::Normal {
"Discoverable timed out — back to Normal".to_string()
} else {
format!("Presence mode stayed {mode}")
};
}
UiEvent::ShutdownComplete => {
if state.closing {