Files
ely-eye/backend/src/ely_eye/cache_fabric.py
T
ZacharyZhang-NY 83dfc14daa Let the server decide whose memory a caller may read
A holder is now somebody who can prove it. Schema v11 stores a sha256 of a
per-holder token beside the directory row, so a copied database cannot replay
it and the plaintext is shown exactly once. The operator token mints and
revokes those tokens; a holder token reads that holder's own memories and
nothing else; naming somebody else without the operator token is refused with
both ids in the message.

The visibility rule was leaking before this: browse had none at all, and the
search rules short-circuited to the whole library whenever no holder was
named. The rule is now written out in five copies with one parameter carried
four times, and a test compares the copies so they cannot drift — semgrep
rejects an interpolated constant, so the copies are not a choice.

Eyes carry three states on purpose. NULL means the whole library, which is
what a process that owns the store is entitled to; a holder id means that
holder; the empty string means nobody, which is what a tokenless HTTP caller
is. Making NULL mean nobody instead would have forced every internal caller —
CLI, proofs, the compiler, the bench harnesses — to name eyes, contradicting
the rule that a local process reads as its own trusted principal.

The capability card answers "can this store answer my kind of question" before
anything is sent. It is derived from what the store holds, so it cannot
promise a modality the store lacks, and it carries no memory text, so it is
safe to read before trusting the store. HTTP, CLI and one MCP tool all serve
it without a token.
2026-10-09 14:15:56 -04:00

73 lines
3.2 KiB
Python

from __future__ import annotations
import hashlib
import json
from datetime import datetime
from typing import Any
from .config import Settings, get_settings
from .db import Database
from .status_schemas import CacheLayerStatus
SEMANTIC_LAYER = "semantic"
class CacheFabric:
def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None:
self.settings = settings or get_settings()
self.db = database or Database(self.settings)
def semantic_key(self, question: str, profile: str, store_revision: int, holder: str = "") -> str:
"""A compiled context is only true for the corpus it was compiled from and the eyes that were allowed to see it, so both belong in its name."""
return hashlib.sha256(f"compiler-v2\n{profile}\n{store_revision}\n{holder}\n{question}".encode()).hexdigest()
def record_hit(self, layer: str, cache_key: str, bytes_estimate: int = 0, metadata: dict[str, Any] | None = None) -> None:
self.db.log_cache_event(layer, cache_key, "hit", bytes_estimate, metadata)
def record_miss(self, layer: str, cache_key: str, bytes_estimate: int = 0, metadata: dict[str, Any] | None = None) -> None:
self.db.log_cache_event(layer, cache_key, "miss", bytes_estimate, metadata)
def write_semantic_cache(self, cache_key: str, payload: dict[str, Any]) -> None:
path = self.settings.cache_dir / "semantic" / f"{cache_key}.json"
path.parent.mkdir(parents=True, exist_ok=True)
data = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
path.write_bytes(data)
self.db.log_cache_event(SEMANTIC_LAYER, cache_key, "write", len(data), {"path": str(path)})
def read_semantic_cache(self, cache_key: str) -> dict[str, Any] | None:
"""Return the cached payload, or None. The caller traces the event: one compile, one event."""
path = self.settings.cache_dir / "semantic" / f"{cache_key}.json"
if not path.exists():
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"{path}: the semantic cache entry is unreadable ({error})") from error
def drop_semantic_cache(self) -> int:
"""Delete every compiled context: each one may quote a memory that no longer exists."""
directory = self.settings.cache_dir / "semantic"
if not directory.exists():
return 0
entries = list(directory.glob("*.json"))
for path in entries:
path.unlink()
return len(entries)
def statuses(self) -> list[CacheLayerStatus]:
rows = self.db.cache_layer_rows()
statuses: list[CacheLayerStatus] = []
for row in rows:
last = row["last_event_at"]
statuses.append(
CacheLayerStatus(
layer=row["layer"],
entries=int(row["entries"] or 0),
bytes_estimate=int(row["bytes_estimate"] or 0),
hit_count=int(row["hit_count"] or 0),
miss_count=int(row["miss_count"] or 0),
last_event_at=datetime.fromisoformat(last) if last else None,
)
)
return statuses