fix(memhub): resolve a caller's Hermes root through the grant's own helper

The overlap check resolved the caller-supplied root a second time in new
code. Resolve it with the same helper the grant uses (`resolved_source_root`
in access.py); the Hermes check now compares an already-resolved root and
only resolves other users' stored grant roots itself. A root that cannot be
resolved is left to the grant that follows, which refuses it with its own
code; the `profiles` container is still refused by name before that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvinfeiandClaude Opus 5.5 committed 2026-11-02 16:32:17 +08:00
1 parent 576d9bf408
commit ce9801b449
3 files changed
+32 -15

No files matched your search

+14 -3
View File
@@ -130,6 +130,7 @@ from one_memory.hy_sdk_types import (
from one_memory.imports.access import (
AuthorizedSourceRoot,
SourceAccessError,
resolved_source_root,
root_identity_matches,
source_root_identity,
)
@@ -172,6 +173,7 @@ from one_memory.imports.sources.common import SourceInstallState
from one_memory.imports.sources.hermes import (
HERMES_ROOT_IS_PROFILES_CONTAINER,
hermes_root_conflict,
hermes_root_is_profiles_container,
)
from one_memory.imports.sources.registry import (
RootCandidateKind,
@@ -3217,17 +3219,26 @@ def create_app(
"""
if source_id not in MULTI_USER_SOURCES:
return
if hermes_root_is_profiles_container(root):
raise HTTPException(status_code=422, detail=HERMES_ROOT_IS_PROFILES_CONTAINER)
def others() -> list[Path]:
return [
def conflict_with_others() -> str | None:
try:
mine = resolved_source_root(root)
except SourceAccessError:
# Unreadable or missing: the grant that follows refuses it
# with its own code.
return None
others = [
Path(path)
for receipt, path in _grant_store(app).grants()
if receipt.source_id == source_id
and receipt.user_id != user_id
and receipt.revoked_at is None
]
return hermes_root_conflict(mine, others)
conflict = hermes_root_conflict(root, await asyncio.to_thread(others))
conflict = await asyncio.to_thread(conflict_with_others)
if conflict is not None:
raise HTTPException(
status_code=422 if conflict == HERMES_ROOT_IS_PROFILES_CONTAINER else 409,
@@ -436,6 +436,11 @@ def root_identity_matches(root: Path, expected: str) -> bool:
return _root_identity_matches_details(resolved, details, expected)
def resolved_source_root(root: Path) -> Path:
"""The real directory a root names (symlinks followed), as a grant records it."""
return _root_details(root)[0]
def _check_root_identity(root: Path, expected: str | None) -> None:
if expected is not None and not root_identity_matches(root, expected):
raise SourceAccessError(SourceAccessErrorCode.SOURCE_IDENTITY_CHANGED)
@@ -268,20 +268,21 @@ def _is_hermes_target(relative_path: str) -> bool:
return path.name == "state.db" or (path.suffix == ".jsonl" and "sessions" in path.parts)
def hermes_root_conflict(root: Path, other_users_roots: Iterable[Path]) -> str | None:
"""Why `root` may not be granted beside other users' Hermes roots, or None.
def hermes_root_is_profiles_container(root: Path) -> bool:
"""`profiles` itself holds everyone's profiles; it is never one person's history."""
return root.name == HERMES_PROFILES_DIRECTORY
Paths are compared resolved (symlinks followed). `profiles` itself is the
container of everyone's profiles, never one person's history. Two roots
that are equal or nest would read the same files for two users, except
when one is exactly the other's `profiles/<name>`: the scan above never
enters `profiles/`, so the two stay apart (`local` on `~/.hermes`, a user
on `~/.hermes/profiles/<name>`).
def hermes_root_conflict(mine: Path, other_users_roots: Iterable[Path]) -> str | None:
"""Why the resolved root `mine` may not be granted beside other users' Hermes roots.
Paths are compared resolved (symlinks followed; the other users' roots are
resolved here). Two roots that are equal or nest would read the same files
for two users, except when one is exactly the other's `profiles/<name>`:
the scan above never enters `profiles/`, so the two stay apart (`local` on
`~/.hermes`, a user on `~/.hermes/profiles/<name>`). None when it may.
"""
if root.name == HERMES_PROFILES_DIRECTORY:
return HERMES_ROOT_IS_PROFILES_CONTAINER
mine = _real(root)
if mine.name == HERMES_PROFILES_DIRECTORY:
if hermes_root_is_profiles_container(mine):
return HERMES_ROOT_IS_PROFILES_CONTAINER
for other in other_users_roots:
theirs = _real(other)