fix(gateway): scope pairing platform discovery to the profile dir

The per-profile pairing isolation added self._dir and scoped every
per-file path helper (_pending_path, _approved_path) to it, but
_all_platforms still enumerated the module-global PAIRING_DIR. For a
profile-scoped PairingStore, list_approved/list_pending/clear_pending
therefore operated on the GLOBAL platform set while loading each
platform's file from the PROFILE dir — so list_approved() returned []
for a user that is_approved() confirmed as approved, a silent divergence
between the authz surface and the list/inspect/clear surface.

Route discovery through self._dir. Byte-identical for the global store
(self._dir == PAIRING_DIR when no profile is set); only the buggy
profile-scoped case changes. self._dir is guaranteed to exist (__init__
mkdirs it).
This commit is contained in:
briandevans 2026-07-07 16:44:43 -07:00 committed by Brooklyn Nicholson
parent d063684b3f
commit 4cbd46545e
2 changed files with 29 additions and 1 deletions

View file

@ -727,7 +727,7 @@ class PairingStore:
def _all_platforms(self, suffix: str) -> list:
"""List all platforms that have data files of a given suffix."""
platforms = []
for f in PAIRING_DIR.iterdir():
for f in self._dir.iterdir():
if f.name.endswith(f"-{suffix}.json"):
platform = f.name.replace(f"-{suffix}.json", "")
if not platform.startswith("_"):

View file

@ -46,6 +46,34 @@ class TestSplitPairingDirMigration:
assert "ou_user" in migrated
class TestProfileScopedDiscovery:
def test_list_approved_scopes_platform_discovery_to_profile_dir(self, tmp_path):
# A profile-scoped store must enumerate platforms from its own
# per-profile directory (self._dir), not the module-global PAIRING_DIR.
# Regression: _all_platforms iterated PAIRING_DIR while every per-file
# path helper routed through self._dir, so a profile store confirmed a
# user via is_approved() (reads self._dir) yet returned [] from
# list_approved() (scanned the empty global dir).
home = tmp_path / "home"
global_dir = tmp_path / "global-pairing"
global_dir.mkdir(parents=True)
with patch("gateway.pairing.PAIRING_DIR", global_dir), patch(
"gateway.pairing.get_hermes_home", return_value=home
):
store = PairingStore(profile="alice")
# Store lives under the profile dir, provably distinct from PAIRING_DIR.
assert store._dir != global_dir
with store._lock:
store._approve_user("telegram", "tg-456", "Bob")
assert store.is_approved("telegram", "tg-456") is True
approved = store.list_approved()
assert [r["user_id"] for r in approved] == ["tg-456"]
assert approved[0]["platform"] == "telegram"
# ---------------------------------------------------------------------------
# _secure_write
# ---------------------------------------------------------------------------