crypto: auto-heal split-brain device_keys state on for_user #88
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/matrix-mcp!88
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "crypto/auto-heal-device-keys"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause
matrix-rust-sdk's per-account
sharedflag (a.k.a.device_keys_published) latchestrueon the first successful/keys/uploadand is never re-verified against the homeserver. If the upload was actually rejected — e.g. /setup attempting its own unbound bearer, a transient network blip, or a later admin-side device wipe — the SDK keeps operating against a device that doesn't exist server-side. Every subsequent/keys/uploadis short-circuited (keys_for_upload()returnsNonewhileshared == true); the device shows up "Unverified — doesn't support encryption" in Element even though OTKs are uploaded;/setup/recover's self-sign attaches to local keys that never landed on Synapse.Repro'd on 2026-05-21:
MATRIXMCP2had 50 OTKs ine2e_one_time_keys_jsonand zero rows ine2e_device_keys_json. matrix-rust-sdk emitted the canonical log line"Our own device might have been deleted"on every sync but no remediation triggered.What this PR does
MatrixClientCache::for_usernow post-verifies (Client::encryption().get_user_devices(...)) once per cached client. On a negative result it evicts, wipes the per-MXID store subdirectory, and recurses exactly once. A second negative after rebuild surfaces as an error rather than looping. The wipe is scoped to one MXID's hash directory under the store root — never the root itself, never another user's data.src/matrix_client.rs: refactorfor_user→for_user_inner(retry: bool), new helpersverify_or_healandverify_device_keys_published, newwipe_user_storeon the cache, newkeys_verified: AtomicBoolonCachedClient. New tests for the wipe (scope + idempotency) and the metric.src/metrics.rs: newmatrix_mcp_client_cache_self_heal_total{reason="..."}IntCounterVec. Single reason label for now (device_keys_missing); the design leaves room for future heal causes.src/setup.rs: comment refresh — the existingcontains()precondition stays as defense-in-depth; for_user is now the primary guard.Why scoped per-MXID wipe is the right level
sharedback tofalse. The pickledAccountis a single encrypted blob inkv[key="account"]; surgical mutation would require per-SDK-version schema understanding and is more fragile than a scoped rebuild./setup/recoverjust takes 30s longer; no error, no manual intervention.Why not upstream this to matrix-rust-sdk
A public
Encryption::republish_device_keys()or periodic verification of thesharedflag against/keys/querywould make this workaround vestigial. Worth filing eventually; not in scope for this PR.Observability
Paired with argocd PR #551 which adds a Grafana Loki alert on the SDK warning
"Our own device might have been deleted". The combination is: alert fires when the split-brain happens in the wild → the next /mcp tool call for that user auto-heals → metric increments → alert resolves on its own.🤖 Generated with Claude Code
When matrix-rust-sdk's local `device_keys_published` flag is latched to true but Synapse doesn't actually have our device's keys (e.g. an earlier /keys/upload was rejected with the SDK still flipping the flag), subsequent /keys/upload calls are skipped forever and the device stays invisible to the homeserver. The SDK exposes no public API to clear the flag, so the only recovery is to wipe the per-MXID crypto store and let the SDK rebuild from scratch. `MatrixClientCache::for_user` now post-verifies via /keys/query once per cached client. On a negative result it evicts, wipes the per-MXID store directory, and recurses exactly once. A second negative after rebuild errors out instead of looping. The wipe is scoped to one MXID's subdirectory under the store root — never the root itself, never another user's data. A new `matrix_mcp_client_cache_self_heal_total{reason}` counter records each self-heal so we can alert on it in Loki/Grafana. The /setup precondition stays as defense-in-depth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.