Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ COPY processing/ ./processing/
COPY retrieval/ ./retrieval/
COPY rag/ ./rag/
COPY scripts/ ./scripts/
COPY utils/ ./utils/
COPY configs/ ./configs/

# Copy built UI from builder stage
Expand All @@ -48,11 +47,39 @@ COPY --from=ui-builder /ui/dist /usr/share/nginx/html
RUN rm -f /etc/nginx/sites-enabled/default \
/etc/nginx/sites-available/default

# ── Non-root runtime user ──────────────────────────────────────────────────
# Runtime writes this container actually performs, and what each needs:
# /app/data/faiss_index/ -- VectorStore.save() (index.faiss, metadata.pkl),
# written by scripts/build_index.py on first boot
# /app/logs/, /app/cache/ -- reserved for app use (currently unwritten, but
# pre-created + owned so a future write doesn't
# silently hit a permission error)
# /var/log/nginx/ -- access.log / error.log (nginx opens these at
# startup, not lazily -- must be writable then)
# /var/lib/nginx/ -- client_body/proxy/fastcgi/... temp dirs, created
# by nginx workers on first request
# /etc/nginx/conf.d/ -- devhub.conf is generated here by the CMD script
# at container start (embeds the runtime JWT_SECRET)
# The stock nginx.conf also sets `user www-data;` and `pid /run/nginx.pid;`,
# both of which assume a root master process (setuid to www-data, write to
# /run which is root:root here) -- since the master now runs as appuser,
# the `user` directive is dropped (nginx just runs workers as whoever
# started it) and the pid file is redirected to /tmp (world-writable).
RUN groupadd --gid 10001 appuser \
&& useradd --uid 10001 --gid appuser --no-create-home --shell /usr/sbin/nologin appuser \
&& mkdir -p /app/data/faiss_index /app/logs /app/cache \
&& chown -R appuser:appuser /app \
&& chown -R appuser:appuser /var/log/nginx /var/lib/nginx /etc/nginx/conf.d \
&& sed -i '/^user www-data;$/d' /etc/nginx/nginx.conf \
&& sed -i 's#pid /run/nginx.pid;#pid /tmp/nginx.pid;#' /etc/nginx/nginx.conf

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
REPO_BASE=/repos

USER appuser

EXPOSE 8082 5173

# Startup sequence:
Expand Down
21 changes: 14 additions & 7 deletions api/routes/rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pydantic import BaseModel
from typing import Optional
import json
import os
import traceback

from rag.control_plane import CONTROL_PLANE
Expand All @@ -11,6 +12,12 @@
router = APIRouter()


def _debug_tracebacks_enabled() -> bool:
# Same shared-secret-style convention as api/auth.py's _auth_enabled():
# a no-op (tracebacks hidden) unless explicitly turned on.
return os.getenv("DEBUG_TRACEBACKS", "").strip().lower() == "true"


# =========================================================
# REQUEST MODEL
# =========================================================
Expand Down Expand Up @@ -61,13 +68,13 @@ def query(req: QueryRequest, actor: str = Depends(require_auth)):
}

except Exception as e:
raise HTTPException(
status_code=500,
detail={
"error": str(e),
"trace": traceback.format_exc()
}
)
detail = {"error": str(e)}
# Stack traces can leak file paths, internals, and other sensitive
# detail into the HTTP response -- only include one when a deployer
# has explicitly opted in via DEBUG_TRACEBACKS=true.
if _debug_tracebacks_enabled():
detail["trace"] = traceback.format_exc()
raise HTTPException(status_code=500, detail=detail)


# =========================================================
Expand Down
21 changes: 18 additions & 3 deletions tests/test_rag_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,34 @@ def test_query_endpoint_success(client, mock_control_plane):
assert data["answer"] == "test answer"
assert data["api_version"] == "v6"

def test_query_endpoint_failure(client, mock_control_plane):
def test_query_endpoint_failure_traceback_enabled(client, mock_control_plane, monkeypatch):
monkeypatch.setenv("DEBUG_TRACEBACKS", "true")
mock_engine = MagicMock()
mock_engine.query.side_effect = Exception("Query Failed")
mock_control_plane.get_engine.return_value = mock_engine

response = client.post("/query", json={"query": "hello"})

assert response.status_code == 500
data = response.json()
assert "detail" in data
assert data["detail"]["error"] == "Query Failed"
assert "trace" in data["detail"]

def test_query_endpoint_failure_traceback_disabled(client, mock_control_plane, monkeypatch):
monkeypatch.delenv("DEBUG_TRACEBACKS", raising=False)
mock_engine = MagicMock()
mock_engine.query.side_effect = Exception("Query Failed")
mock_control_plane.get_engine.return_value = mock_engine

response = client.post("/query", json={"query": "hello"})

assert response.status_code == 500
data = response.json()
assert "detail" in data
assert data["detail"]["error"] == "Query Failed"
assert "trace" not in data["detail"]


# =========================================================
# ENDPOINT TESTS: /stream
Expand Down
Loading