Files
hermes-webui/tests/test_issue4067_import_cli_cross_profile_guard.py
nesquena-hermes 0378ed4e34 fix(routes): preserve live cards on cross-profile 409 collisions
Gate follow-up for #7710. Three front-end handlers treated EVERY 409 as a
domain-specific stale/expired signal, so the new session_profile_mismatch
response would have:

- messages.js: hidden a LIVE clarify card and mislabelled it expired
- ui.js: retired a LIVE compression-recovery card with a false note
- commands.js: rendered a false 'Compression failed' error state

Each now routes through _sessionProfileMismatchFromError() first and leaves
the card standing. Also captures the status in the import_cli test mock so a
wrong status can no longer pass (the mock swallowed it into *_).
2026-09-22 10:21:22 +00:00

153 lines
5.9 KiB
Python

"""Regression test for the #4067 cross-profile import_cli boundary.
The all-profiles session enumeration (#4067) must NOT relax the cross-profile
isolation boundary on the import_cli refresh path. Both release-gate advisors
(Codex CORE + Opus) reproduced the same leak on PR head b16586f0: the
already-imported branch of `_handle_session_import_cli` forced
`allow_all_profiles=True` whenever the (globally-stored) session carried a
profile, and had no `_session_visible_to_active_profile` gate — so an
unqualified request from profile A could read/refresh a session imported under
profile B's live `state.db` (Codex returned the foreign transcript + a planted
secret on an unqualified request).
This pins the fix: the existing-session branch is profile-scoped exactly like
the /api/session detail and /api/session/export endpoints.
"""
from __future__ import annotations
import io
import api.routes as routes
class _FakeHandler:
def __init__(self, cookie_profile: str | None = None):
self.status = None
self.headers = {}
if cookie_profile is not None:
self.headers["Cookie"] = f"hermes_profile={cookie_profile}"
self.wfile = io.BytesIO()
def send_response(self, status):
self.status = status
def send_header(self, key, value):
self.headers[key] = value
def end_headers(self):
pass
def _capture(monkeypatch):
cap = {}
# #7710: the real ``j`` signature is ``j(handler, payload, status=200,
# extra_headers=None, *, pretty=True)``; the mock has to accept the
# status keyword so the helper's 409 ``session_profile_mismatch``
# payload can be captured here. Capture the STATUS too — swallowing it
# into ``*_`` would let a wrong status (200/404) pass this test.
def _fake_j(h, o, status=200, *_, **__):
cap["ok"] = o
cap["ok_status"] = status
return True
monkeypatch.setattr(routes, "j", _fake_j)
monkeypatch.setattr(
routes,
"bad",
lambda h, m, c=400: (cap.__setitem__("bad", (m, c)), True)[1],
)
return cap
class _FakeSession:
"""Minimal stand-in for a globally-stored Session imported under a profile."""
def __init__(self, sid, profile):
self.session_id = sid
self.profile = profile
self.messages = [{"role": "user", "content": "FOREIGN_SECRET"}]
self.source_tag = "cli"
self.raw_source = "cli"
self.session_source = "cli"
self.source_label = "CLI"
self.parent_session_id = None
def compact(self):
return {"id": self.session_id, "profile": self.profile}
def save(self, touch_updated_at=False):
raise AssertionError("must not save/refresh a foreign-profile session")
def test_import_cli_existing_foreign_profile_unqualified_request_404(monkeypatch):
"""Unqualified request (active profile=default) for a session stored under a
foreign profile must yield 409 ``session_profile_mismatch`` (so the client
can offer to switch to it per #5419) — not 404, which would mask a
legitimate owned-by-other-profile session as missing and trigger a
destructive self-heal in the frontend (#7710)."""
foreign = _FakeSession("foreign_existing_001", "other")
monkeypatch.setattr(routes.Session, "load", staticmethod(lambda sid: foreign))
monkeypatch.setattr(routes, "_get_active_profile_name", lambda: "default")
# If the gate fails open, these would be reached — make them loud.
monkeypatch.setattr(
routes, "get_cli_session_messages",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("foreign read attempted")),
)
cap = _capture(monkeypatch)
routes._handle_session_import_cli(
_FakeHandler(cookie_profile="default"),
{"session_id": "foreign_existing_001"},
)
assert "ok" in cap, f"expected 409 session_profile_mismatch, got {cap}"
assert cap["ok_status"] == 409, f"expected status 409, got {cap.get('ok_status')}"
assert cap["ok"] == {
"error": "Session belongs to a different profile",
"code": "session_profile_mismatch",
"session_id": "foreign_existing_001",
"profile": "other",
}
def test_import_cli_existing_same_profile_still_refreshes(monkeypatch):
"""A session stored under the active profile is still served/refreshed."""
own = _FakeSession("own_001", "default")
own.save = lambda touch_updated_at=False: None # allow refresh
monkeypatch.setattr(routes.Session, "load", staticmethod(lambda sid: own))
monkeypatch.setattr(routes, "_get_active_profile_name", lambda: "default")
monkeypatch.setattr(routes, "_resolve_cli_import_metadata", lambda *a, **k: {})
monkeypatch.setattr(routes, "get_cli_session_messages", lambda *a, **k: [])
cap = _capture(monkeypatch)
routes._handle_session_import_cli(
_FakeHandler(cookie_profile="default"),
{"session_id": "own_001"},
)
assert "ok" in cap, f"expected success, got {cap}"
assert cap["ok"]["session"]["id"] == "own_001"
def test_import_cli_all_profiles_requires_matching_profile(monkeypatch):
"""An explicit all_profiles import for a foreign session must still match the
requested profile — a mismatched profile 404s rather than reading."""
foreign = _FakeSession("foreign_002", "other")
monkeypatch.setattr(routes.Session, "load", staticmethod(lambda sid: foreign))
monkeypatch.setattr(routes, "_get_active_profile_name", lambda: "default")
monkeypatch.setattr(
routes, "get_cli_session_messages",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("foreign read attempted")),
)
cap = _capture(monkeypatch)
# all_profiles=1 but requested profile "haku" != stored "other" → 404.
routes._handle_session_import_cli(
_FakeHandler(cookie_profile="default"),
{"session_id": "foreign_002", "all_profiles": 1, "profile": "haku"},
)
assert "bad" in cap, f"expected 404, got {cap}"
assert cap["bad"][1] == 404