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:
+9
-5
@@ -10,6 +10,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from config import DEFAULT_MODEL, LLAMA_SERVER_BASE
|
||||
from crypto import encrypt_text, decrypt_text
|
||||
from db import get_db, get_upload_context
|
||||
from memory import process_remember_command, auto_detect_facts, check_fact_conflicts
|
||||
from rag import build_system_prompt, ingest_auto_fact
|
||||
@@ -109,17 +110,20 @@ async def chat(request: Request):
|
||||
conv_id = str(uuid.uuid4())
|
||||
title = user_message[:80] + ("..." if len(user_message) > 80 else "")
|
||||
db.execute("INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(conv_id, title, model, now, now))
|
||||
(conv_id, encrypt_text(title), model, now, now))
|
||||
else:
|
||||
db.execute("UPDATE conversations SET updated_at = ? WHERE id = ?", (now, conv_id))
|
||||
|
||||
db.execute("INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "user", user_message, now))
|
||||
(conv_id, "user", encrypt_text(user_message), now))
|
||||
db.commit()
|
||||
|
||||
history_rows = db.execute(
|
||||
raw_rows = db.execute(
|
||||
"SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY id ASC", (conv_id,)
|
||||
).fetchall()
|
||||
history_rows = []
|
||||
for row in raw_rows:
|
||||
history_rows.append({"role": row["role"], "content": decrypt_text(row["content"])})
|
||||
extra_prompt = preset_prompt
|
||||
if upload_doc:
|
||||
extra_prompt = (extra_prompt + "\n\n" + upload_doc) if extra_prompt else upload_doc
|
||||
@@ -215,7 +219,7 @@ async def chat(request: Request):
|
||||
|
||||
db2 = get_db()
|
||||
db2.execute("INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "assistant", saved_msg, datetime.now(timezone.utc).isoformat()))
|
||||
(conv_id, "assistant", encrypt_text(saved_msg), datetime.now(timezone.utc).isoformat()))
|
||||
db2.commit()
|
||||
db2.close()
|
||||
|
||||
@@ -237,7 +241,7 @@ async def chat(request: Request):
|
||||
|
||||
db2 = get_db()
|
||||
db2.execute("INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "assistant", saved_msg, datetime.now(timezone.utc).isoformat()))
|
||||
(conv_id, "assistant", encrypt_text(saved_msg), datetime.now(timezone.utc).isoformat()))
|
||||
db2.commit()
|
||||
db2.close()
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from config import DEFAULT_MODEL, LLAMA_SERVER_BASE, COMPLETIONS_API_KEY
|
||||
from crypto import encrypt_text
|
||||
from db import get_db
|
||||
from rag import build_system_prompt
|
||||
from routers.chat import parse_llama_stream_chunk
|
||||
@@ -124,7 +125,7 @@ async def chat_completions(request: Request):
|
||||
title = f"[IDE] {user_message[:72]}{'...' if len(user_message) > 72 else ''}"
|
||||
db.execute(
|
||||
"INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(conv_id, title, model, now, now),
|
||||
(conv_id, encrypt_text(title), model, now, now),
|
||||
)
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
@@ -132,7 +133,7 @@ async def chat_completions(request: Request):
|
||||
if role in ("user", "assistant"):
|
||||
db.execute(
|
||||
"INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, role, content, now),
|
||||
(conv_id, role, encrypt_text(content), now),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
@@ -195,7 +196,7 @@ async def _stream_chat(payload: dict, model: str, conv_id: str, request: Request
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "assistant", assistant_msg, datetime.now(timezone.utc).isoformat()),
|
||||
(conv_id, "assistant", encrypt_text(assistant_msg), datetime.now(timezone.utc).isoformat()),
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
@@ -240,7 +241,7 @@ async def _blocking_chat(payload: dict, model: str, conv_id: str, request: Reque
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "assistant", assistant_msg, datetime.now(timezone.utc).isoformat()),
|
||||
(conv_id, "assistant", encrypt_text(assistant_msg), datetime.now(timezone.utc).isoformat()),
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from db import get_db
|
||||
from crypto import encrypt_text, decrypt_text
|
||||
from security import read_json_body, BODY_LIMIT_DEFAULT_BYTES
|
||||
from config import DEFAULT_MODEL, MAX_CONVERSATION_TITLE_CHARS
|
||||
|
||||
@@ -18,6 +19,7 @@ async def list_conversations():
|
||||
result = []
|
||||
for r in rows:
|
||||
c = dict(r)
|
||||
c["title"] = decrypt_text(c["title"])
|
||||
attach_count = db.execute(
|
||||
"SELECT COUNT(*) FROM upload_context WHERE conversation_id = ?", (c["id"],)
|
||||
).fetchone()[0]
|
||||
@@ -36,7 +38,7 @@ async def create_conversation(request: Request):
|
||||
title = str(body.get("title", "New Chat"))[:MAX_CONVERSATION_TITLE_CHARS]
|
||||
db = get_db()
|
||||
db.execute("INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(conv_id, title, model, now, now))
|
||||
(conv_id, encrypt_text(title), model, now, now))
|
||||
db.commit()
|
||||
db.close()
|
||||
return {"id": conv_id, "title": title, "model": model, "created_at": now, "updated_at": now}
|
||||
@@ -51,7 +53,14 @@ async def get_conversation(conv_id: str):
|
||||
raise HTTPException(status_code=404, detail="Conversation not found")
|
||||
messages = db.execute("SELECT * FROM messages WHERE conversation_id = ? ORDER BY id ASC", (conv_id,)).fetchall()
|
||||
db.close()
|
||||
return {"conversation": dict(conv), "messages": [dict(m) for m in messages]}
|
||||
conv_dict = dict(conv)
|
||||
conv_dict["title"] = decrypt_text(conv_dict["title"])
|
||||
msg_list = []
|
||||
for m in messages:
|
||||
md = dict(m)
|
||||
md["content"] = decrypt_text(md["content"])
|
||||
msg_list.append(md)
|
||||
return {"conversation": conv_dict, "messages": msg_list}
|
||||
|
||||
|
||||
@router.put("/api/conversations/{conv_id}")
|
||||
@@ -61,7 +70,7 @@ async def update_conversation(conv_id: str, request: Request):
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
if "title" in body:
|
||||
db.execute("UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?",
|
||||
(str(body["title"])[:MAX_CONVERSATION_TITLE_CHARS], now, conv_id))
|
||||
(encrypt_text(str(body["title"])[:MAX_CONVERSATION_TITLE_CHARS]), now, conv_id))
|
||||
if "model" in body:
|
||||
db.execute("UPDATE conversations SET model = ?, updated_at = ? WHERE id = ?",
|
||||
(body["model"], now, conv_id))
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from config import COMPLETIONS_API_KEY
|
||||
from crypto import encrypt_text
|
||||
from eviction import maybe_evict
|
||||
from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION
|
||||
|
||||
@@ -50,7 +51,7 @@ async def ingest_content(request: Request):
|
||||
continue
|
||||
vector = embed_resp.json()["embedding"]
|
||||
point_id = f"ingest-{source}-{datetime.now(timezone.utc).timestamp()}-{i}"
|
||||
payload = {"text": chunk, "source": source, "ingest_date": datetime.now(timezone.utc).isoformat(), "type": "ingest"}
|
||||
payload = {"text": encrypt_text(chunk), "source": source, "ingest_date": datetime.now(timezone.utc).isoformat(), "type": "ingest"}
|
||||
payload.update(metadata)
|
||||
upsert_resp = await client.put(
|
||||
f"{QDRANT_URL}/collections/{RAG_COLLECTION}/points?wait=true",
|
||||
|
||||
@@ -9,6 +9,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from config import DEFAULT_MODEL, LLAMA_SERVER_BASE, MAX_SEARCH_QUERY_CHARS
|
||||
from crypto import encrypt_text
|
||||
from db import get_db
|
||||
from search import query_searxng, format_search_results
|
||||
from routers.chat import parse_llama_stream_chunk
|
||||
@@ -41,12 +42,12 @@ async def explicit_search(request: Request):
|
||||
conv_id = str(uuid.uuid4())
|
||||
title = query[:70] + "..." if len(query) > 70 else query
|
||||
db.execute("INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(conv_id, title, model, now, now))
|
||||
(conv_id, encrypt_text(title), model, now, now))
|
||||
else:
|
||||
db.execute("UPDATE conversations SET updated_at = ? WHERE id = ?", (now, conv_id))
|
||||
|
||||
db.execute("INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "user", query, now))
|
||||
(conv_id, "user", encrypt_text(query), now))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
@@ -60,7 +61,7 @@ async def explicit_search(request: Request):
|
||||
yield f"data: {json.dumps({'token': error_msg, 'conversation_id': conv_id})}\n\n"
|
||||
db2 = get_db()
|
||||
db2.execute("INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "assistant", error_msg, datetime.now(timezone.utc).isoformat()))
|
||||
(conv_id, "assistant", encrypt_text(error_msg), datetime.now(timezone.utc).isoformat()))
|
||||
db2.commit()
|
||||
db2.close()
|
||||
yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id})}\n\n"
|
||||
@@ -102,7 +103,7 @@ async def explicit_search(request: Request):
|
||||
|
||||
db2 = get_db()
|
||||
db2.execute("INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
(conv_id, "assistant", saved_msg, datetime.now(timezone.utc).isoformat()))
|
||||
(conv_id, "assistant", encrypt_text(saved_msg), datetime.now(timezone.utc).isoformat()))
|
||||
db2.commit()
|
||||
db2.close()
|
||||
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from config import UPLOAD_DIR, MAX_UPLOAD_BYTES, SUPPORTED_UPLOAD_TYPES, UPLOAD_CONTEXT_EXPIRY_HOURS
|
||||
from crypto import encrypt_text
|
||||
from db import get_db, insert_upload_context, list_upload_context_by_conversation, delete_upload_context_by_id
|
||||
from eviction import maybe_evict
|
||||
from rag import chunk_text, QDRANT_URL, EMBED_URL, EMBED_MODEL, RAG_COLLECTION
|
||||
@@ -78,7 +79,7 @@ async def upload_file(
|
||||
"points": [{
|
||||
"id": pid,
|
||||
"vector": vector,
|
||||
"payload": {"text": chunk, "source": file.filename, "upload_date": datetime.now(timezone.utc).isoformat(), "type": "upload"},
|
||||
"payload": {"text": encrypt_text(chunk), "source": file.filename, "upload_date": datetime.now(timezone.utc).isoformat(), "type": "upload"},
|
||||
}]
|
||||
},
|
||||
timeout=30.0,
|
||||
|
||||
Reference in New Issue
Block a user