Require a token for writes and quarantine what nobody vouches for

Sixteen mutating routes now refuse a caller who cannot present the token the
server started with; queries stay open, because a question is not a write. The
environment variable is ELY_EYE_AUTH_TOKEN, following the repo's env_prefix
convention — the shorter name I reached for first returned 403 where the smoke
expected 401, and the server printed the reason.

Words from a stranger are quarantined on the way in: an HTTP upload is
third_party by definition, and a sentence carrying an instruction-shaped phrase
is quarantined whoever wrote it. A quarantined atom stays findable and stays out
of the compiled context until a second, live, unquarantined source agrees with
its rarest words. Corroboration is computed at read time because it is a
property of the corpus now, not of the moment a memory was written; it is
term-level, and the docstring says so.

forget now erases the source row and the compiled contexts beside the atoms, so
a forgotten memory leaves nothing that can still speak. The semantic cache key
carries a store revision, which fixes a real staleness: a context compiled
before new evidence arrived kept its old quarantine decision forever.

bench.audit --product still pointed at evals.py, which the p1-6 split moved into
backend/src/ely_eye/proofs/. It failed fast, but the anti-self-justification
rule had quietly stopped auditing the product; it now walks the package, and a
test pins that the package exists and holds no re-roll.

Gates: pytest 306 passed; ruff clean; mypy cold cache 24 errors in 9 files, the
recorded baseline, net zero; pyright 0 errors on the 23 backend files and the 2
bench files; go build/vet/test and gofmt clean; npm run build 221.39 kB with
eslint exit 0; bench/filesize_check clean; bench.audit --product clean over 31
harness files; scripts/mcp_write_smoke.sh PASS with the gate live across two
servers and the MCP child.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 23:05:15 -04:00
1 parent 8b974e6626
commit 9145a23f18
32 files changed
+750 -58

No files matched your search

+10 -2
View File
@@ -13,6 +13,8 @@ from datetime import datetime
from pathlib import Path
from typing import Any
from . import trust
from .cache_fabric import CacheFabric
from .config import Settings, get_settings
from .db import Database
from .schemas import (
@@ -45,6 +47,7 @@ class MemoryService:
self.db = database or Database(self.settings)
self.vectors = vectors or VectorIndex(self.settings, self.db)
self.pictures = VisualIndex(self.settings, self.db)
self.cache = CacheFabric(self.settings, self.db)
def remember(
self,
@@ -114,6 +117,7 @@ class MemoryService:
visibility=visibility or Visibility(),
retention_until=retention_until,
)
trust.stamp([atom], origin=origin)
self.db.upsert_atoms([atom])
self.vectors.sync_atoms([atom.atom_id])
return atom
@@ -169,8 +173,8 @@ class MemoryService:
) -> ForgetReport:
"""Delete a memory and everything derived from it.
The cascade is the whole point: an answer cached from a forgotten memory is still
on screen, and a vector index that still carries it will keep surfacing it.
The cascade is the whole point: a cached answer or a live vector will keep
surfacing a memory the owner asked to erase, so both go with it.
"""
if (atom_id is None) == (source_id is None):
raise ValueError("forget takes exactly one of atom_id or source_id")
@@ -185,6 +189,9 @@ class MemoryService:
edges_deleted = self.db.delete_edges_touching(doomed_ids)
atoms_deleted = self.db.delete_atoms(doomed_ids)
cache_cleared = self.db.clear_cache_events(f"forget:{target_source}")
semantic_cleared = self.cache.drop_semantic_cache()
if self.db.delete_source(target_source) != 1:
raise ValueError(f"{target_source}: the source row vanished before the forget could erase it")
vectors_dropped, visual_dropped = self._drop_derived_vectors(doomed_ids)
return ForgetReport(
source_id=target_source,
@@ -192,6 +199,7 @@ class MemoryService:
atoms_deleted=atoms_deleted,
edges_deleted=edges_deleted,
cache_entries_cleared=cache_cleared,
semantic_cache_cleared=semantic_cleared,
vectors_dropped=vectors_dropped,
visual_index_dropped=visual_dropped,
affected_cartridges=self._cartridges_holding(target_source),