From 2e89c5da48de5551efef7ccf97cd09e147fa37f4 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:56:29 +0530 Subject: [PATCH] fix(mcp-oauth): keep the refresh-fence sidecar when removing token state flock is bound to an inode, not a path. Unlinking `.json.refresh.lock` from `remove()` while a peer still holds the fence lets the next acquirer open and lock a brand-new inode, so two processes hold "the" fence at once and the single-use refresh token can be consumed twice. On Windows the unlink of a locked file raises PermissionError straight out of `remove()`/`restore()`. A 0-byte 0600 sidecar in a 0700 directory is harmless, so leave it in place. It stays out of `_state_paths()` so `restore(only_if_absent=True)` still keys off real token state only. --- tools/mcp_oauth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/mcp_oauth.py b/tools/mcp_oauth.py index c824478e64..6e17a8e821 100644 --- a/tools/mcp_oauth.py +++ b/tools/mcp_oauth.py @@ -566,7 +566,9 @@ class HermesTokenStorage: def remove(self) -> None: """Delete all stored OAuth state for this server.""" - for p in (*self._state_paths(), self._cimd_rejected_path(), _refresh_lock_path(self._tokens_path())): + # The ``.refresh.lock`` sidecar is deliberately kept: flock is inode-bound, so unlinking it + # while a peer holds the fence would let the next acquirer lock a fresh inode (two holders). + for p in (*self._state_paths(), self._cimd_rejected_path()): p.unlink(missing_ok=True) def snapshot(self) -> dict[str, bytes]: