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

+28 -7
View File
@@ -13,6 +13,7 @@ from pydantic import BaseModel
from . import __version__
from .adapters import plan as adapter_plan
from .api_auth import require_write
from .api_cartridges import register as register_cartridge_routes
from .api_holders import register as register_holder_routes
from .browse import browse, picture
@@ -76,9 +77,10 @@ def settings_dep() -> Settings:
def create_app() -> FastAPI:
app = FastAPI(title="Ely-Eye", version=__version__)
app.state.write_settings = settings_dep
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_origins=settings_dep().allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -116,6 +118,7 @@ def create_app() -> FastAPI:
def ingest_path(
request: PathIngestRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
):
try:
return IngestionService(settings).ingest_path(request.path, request.cartridge_name)
@@ -126,6 +129,7 @@ def create_app() -> FastAPI:
def ingest_upload(
metadata: Annotated[UploadIngestRequest, Depends()],
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
files: Annotated[list[UploadFile], File()],
):
upload_root = settings.data_dir / "uploads"
@@ -144,7 +148,7 @@ def create_app() -> FastAPI:
service = IngestionService(settings)
aggregate = None
for path in saved_paths:
result = service.ingest_path(path, None)
result = service.ingest_path(path, None, origin=MemoryOrigin.third_party)
if aggregate is None:
aggregate = result
else:
@@ -162,7 +166,11 @@ def create_app() -> FastAPI:
return aggregate
@app.post("/api/chat", response_model=AnswerResponse)
def chat(request: ChatRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> AnswerResponse:
def chat(
request: ChatRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> AnswerResponse:
db = Database(settings)
compiler = ContextCompiler(settings, db)
context = compiler.compile(request.question, request.profile)
@@ -200,7 +208,9 @@ def create_app() -> FastAPI:
@app.post("/api/memory", response_model=EvidenceAtom)
def remember(
request: RememberRequest, settings: Annotated[Settings, Depends(settings_dep)]
request: RememberRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> EvidenceAtom:
try:
return MemoryService(settings).remember(
@@ -218,7 +228,9 @@ def create_app() -> FastAPI:
@app.post("/api/memory/supersede", response_model=EvidenceAtom)
def supersede(
request: SupersedeRequest, settings: Annotated[Settings, Depends(settings_dep)]
request: SupersedeRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> EvidenceAtom:
try:
return MemoryService(settings).supersede(
@@ -234,7 +246,11 @@ def create_app() -> FastAPI:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@app.post("/api/memory/forget", response_model=ForgetReport)
def forget(request: ForgetRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> ForgetReport:
def forget(
request: ForgetRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> ForgetReport:
try:
return MemoryService(settings).forget(
atom_id=request.atom_id, source_id=request.source_id, reason=request.reason
@@ -288,7 +304,11 @@ def create_app() -> FastAPI:
return adapter_plan(settings, Database(settings))
@app.post("/api/hashhop", response_model=HashHopProof)
def hashhop(request: HashHopRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> HashHopProof:
def hashhop(
request: HashHopRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> HashHopProof:
evaluator = HashHopEvaluator(settings)
if request.kind == "text":
return evaluator.generate_text_proof(request.hops, request.token_equivalent)
@@ -300,6 +320,7 @@ def create_app() -> FastAPI:
def proof_suite(
request: ProofSuiteRequest,
settings: Annotated[Settings, Depends(settings_dep)],
_: Annotated[None, Depends(require_write)],
) -> ProofSuiteReport:
try:
return PRDProofSuite(settings).run(request.cartridge_id)