v0.20.0: at-rest encryption (AES-256-GCM) for all query-derived text

- crypto.py: AES-256-GCM encrypt/decrypt + ensure_key()
- Key auto-generated on first boot, stored as heartbeat_interval_ms in settings
- All 12 storage paths wired (SQLite + Qdrant)
- memory.py: FTS5 search replaced with Python-side matching
- 200/200 tests pass
This commit is contained in:
gramps
2026-07-14 13:22:32 -07:00
parent f6b01ec9ac
commit 30deb0db64
18 changed files with 176 additions and 79 deletions
+19 -28
View File
@@ -7,6 +7,7 @@ import re
from datetime import datetime, timezone
from typing import Optional
from crypto import encrypt_text, decrypt_text
from db import get_db
from config import MAX_MEMORY_FACT_CHARS
@@ -119,7 +120,7 @@ def add_memory(fact: str, topic: str = "general", source: str = "explicit") -> O
now = datetime.now(timezone.utc).isoformat()
cur = db.execute(
"INSERT INTO memories (fact, topic, source, created_at) VALUES (?, ?, ?, ?)",
(fact, topic, source, now),
(encrypt_text(fact), topic, source, now),
)
db.commit()
rowid = cur.lastrowid
@@ -131,31 +132,16 @@ def add_memory(fact: str, topic: str = "general", source: str = "explicit") -> O
def search_memories(query: str, limit: int = 5) -> list:
if not query.strip():
return []
db = get_db()
words = re.findall(r"[A-Za-z0-9_]+", query)
if not words:
db.close()
return []
escaped = []
for word in words[:10]:
if word.upper() in {"AND", "OR", "NOT", "NEAR"}:
escaped.append(f'"{word}"*')
else:
escaped.append(word + "*")
safe_query = " OR ".join(escaped)
try:
rows = db.execute(
"SELECT rowid, fact, topic, source, created_at, bm25(memories) AS rank "
"FROM memories WHERE memories MATCH ? ORDER BY rank LIMIT ?",
(safe_query, limit),
).fetchall()
results = [dict(row) for row in rows]
log.debug(f"Memory search '{query}' returned {len(results)} results")
except Exception as e:
log.warning(f"Memory search error: {e}")
results = []
db.close()
return results
all_mems = get_all_memories()
words = set(re.findall(r"[A-Za-z0-9_]+", query.lower()))
scored = []
for m in all_mems:
fact_lower = m["fact"].lower()
score = sum(1 for w in words if w in fact_lower)
if score > 0:
scored.append((score, m))
scored.sort(key=lambda x: -x[0])
return [m for _, m in scored[:limit]]
def get_all_memories(topic: Optional[str] = None) -> list:
@@ -167,7 +153,12 @@ def get_all_memories(topic: Optional[str] = None) -> list:
else:
rows = db.execute("SELECT rowid, * FROM memories ORDER BY created_at DESC").fetchall()
db.close()
return [dict(row) for row in rows]
result = []
for row in rows:
d = dict(row)
d["fact"] = decrypt_text(d["fact"])
result.append(d)
return result
def delete_memory(rowid: int) -> bool:
@@ -183,7 +174,7 @@ def delete_memory(rowid: int) -> bool:
def update_memory(rowid: int, fact: str) -> bool:
db = get_db()
cur = db.execute("UPDATE memories SET fact = ? WHERE rowid = ?", (fact, rowid))
cur = db.execute("UPDATE memories SET fact = ? WHERE rowid = ?", (encrypt_text(fact), rowid))
db.commit()
updated = cur.rowcount > 0
db.close()