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:
1 parent
576d9bf408
commit
ce9801b449
3 files changed
+32
-15
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user