fixed findings from review

This commit is contained in:
2026-01-18 19:10:14 +01:00
parent f76328b621
commit fa2d278c79
8 changed files with 203 additions and 34 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Lovdata Chat Environment Configuration
# MCP Server Configuration
# URL of the external lovdata MCP server
MCP_SERVER=http://localhost:8001
# LLM API Keys (optional - at least one required)
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GOOGLE_API_KEY=
# Development Settings
# These are configured in docker-compose.yml
# DOCKER_HOST=tcp://docker-daemon:2376
# DOCKER_TLS_VERIFY=1
# DOCKER_CERT_PATH=/certs

View File

@@ -1,40 +1,53 @@
# Lovdata Chat Development Environment # Lovdata Chat Development Environment
This setup creates a container-per-visitor architecture for the Norwegian legal research chat interface. This setup creates a container-per-visitor architecture for the Norwegian legal research chat interface with secure TLS-based Docker communication.
## Quick Start ## Quick Start
1. **Set up environment variables:** 1. **Generate TLS certificates for secure Docker communication:**
```bash ```bash
cp .env.example .env ./generate-certs.sh
# Edit .env with your API keys
``` ```
2. **Start the services:** 2. **Set up environment variables:**
```bash
cp .env.example .env
# Edit .env with your API keys and MCP server URL
```
3. **Start the services:**
```bash ```bash
docker-compose up --build docker-compose up --build
``` ```
3. **Create a session:** 4. **Create a session:**
```bash ```bash
curl http://localhost/api/sessions -X POST curl http://localhost/api/sessions -X POST
``` ```
4. **Access the chat interface:** 5. **Access the chat interface:**
Open the returned URL in your browser Open the returned URL in your browser
## Architecture ## Architecture
- **session-manager**: FastAPI service managing container lifecycles - **session-manager**: FastAPI service managing container lifecycles with TLS-secured Docker communication
- **lovdata-mcp**: Placeholder for Norwegian legal research MCP server - **docker-daemon**: Docker-in-Docker daemon with TLS certificates for secure container management
- **caddy**: Reverse proxy for routing requests to session containers - **lovdata-mcp**: External Norwegian legal research MCP server (configured via MCP_SERVER env var)
- **caddy**: Reverse proxy with dynamic session-based routing
## Security Features
- **TLS-secured Docker communication**: No Docker socket exposure
- **Container isolation**: Each visitor gets dedicated container with resource limits
- **Automatic cleanup**: Sessions expire after 60 minutes of inactivity
- **Resource quotas**: 4GB RAM, 1 CPU core per container, max 3 concurrent sessions
## Development Notes ## Development Notes
- Sessions auto-cleanup after 60 minutes of inactivity
- Limited to 3 concurrent sessions for workstation development
- Each session gets 4GB RAM and 1 CPU core
- Session data persists in ./sessions/ directory - Session data persists in ./sessions/ directory
- TLS certificates auto-generated for development
- External MCP server configured via environment variables
- Health checks ensure service reliability
## API Endpoints ## API Endpoints
@@ -43,4 +56,17 @@ This setup creates a container-per-visitor architecture for the Norwegian legal
- `GET /api/sessions/{id}` - Get session info - `GET /api/sessions/{id}` - Get session info
- `DELETE /api/sessions/{id}` - Delete session - `DELETE /api/sessions/{id}` - Delete session
- `POST /api/cleanup` - Manual cleanup - `POST /api/cleanup` - Manual cleanup
- `GET /api/health` - Health check - `GET /api/health` - Health check
- `/{path}` - Dynamic proxy routing (with X-Session-ID header)
## Environment Variables
```bash
# Required
MCP_SERVER=http://your-lovdata-mcp-server:8001
# Optional LLM API keys
OPENAI_API_KEY=your_key
ANTHROPIC_API_KEY=your_key
GOOGLE_API_KEY=your_key
```

View File

@@ -9,29 +9,34 @@ services:
- "8000:8000" - "8000:8000"
volumes: volumes:
- ./sessions:/app/sessions - ./sessions:/app/sessions
- /var/run/docker.sock:/var/run/docker.sock
environment: environment:
- MCP_SERVER=http://lovdata-mcp:8001 - MCP_SERVER=${MCP_SERVER:-http://localhost:8001}
- OPENAI_API_KEY=${OPENAI_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-} - GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
- DOCKER_HOST=tcp://docker-daemon:2376
- DOCKER_TLS_VERIFY=1
- DOCKER_CERT_PATH=/certs
depends_on: depends_on:
- lovdata-mcp - docker-daemon
networks: networks:
- lovdata-network - lovdata-network
restart: unless-stopped restart: unless-stopped
lovdata-mcp: docker-daemon:
# Placeholder for lovdata MCP server image: docker:dind
# This should be replaced with the actual lovdata MCP server image privileged: true
image: python:3.11-slim environment:
ports: - DOCKER_TLS_CERTDIR=/certs
- "8001:8001" volumes:
- ./docker-certs:/certs
networks: networks:
- lovdata-network - lovdata-network
command: ["python", "-c", "import time; time.sleep(999999)"] # Placeholder command: ["--tlsverify", "--tlscacert=/certs/server/ca.pem", "--tlscert=/certs/server/cert.pem", "--tlskey=/certs/server/key.pem"]
restart: unless-stopped restart: unless-stopped
# lovdata-mcp server is external - configured via MCP_SERVER environment variable
caddy: caddy:
image: caddy:2.7-alpine image: caddy:2.7-alpine
ports: ports:

60
generate-certs.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Generate TLS certificates for secure Docker communication
set -e
CERT_DIR="./docker-certs"
CA_DIR="$CERT_DIR/ca"
SERVER_DIR="$CERT_DIR/server"
CLIENT_DIR="$CERT_DIR/client"
# Create directories
mkdir -p "$CA_DIR" "$SERVER_DIR" "$CLIENT_DIR"
# Generate CA private key
openssl genrsa -out "$CA_DIR/ca-key.pem" 4096
# Generate CA certificate
openssl req -new -x509 -days 365 -key "$CA_DIR/ca-key.pem" -sha256 -out "$CA_DIR/ca.pem" -subj "/C=US/ST=CA/L=San Francisco/O=Docker/CN=docker-ca"
# Generate server private key
openssl genrsa -out "$SERVER_DIR/server-key.pem" 4096
# Generate server certificate signing request
openssl req -subj "/CN=docker-daemon" -new -key "$SERVER_DIR/server-key.pem" -out "$SERVER_DIR/server.csr"
# Create server extensions file
cat > "$SERVER_DIR/server-extfile.cnf" << EOF
subjectAltName = DNS:docker-daemon,IP:127.0.0.1,IP:172.18.0.1
extendedKeyUsage = serverAuth
EOF
# Sign server certificate
openssl x509 -req -days 365 -in "$SERVER_DIR/server.csr" -CA "$CA_DIR/ca.pem" -CAkey "$CA_DIR/ca-key.pem" -CAcreateserial -out "$SERVER_DIR/cert.pem" -extfile "$SERVER_DIR/server-extfile.cnf"
# Generate client private key
openssl genrsa -out "$CLIENT_DIR/key.pem" 4096
# Generate client certificate signing request
openssl req -subj "/CN=docker-client" -new -key "$CLIENT_DIR/key.pem" -out "$CLIENT_DIR/client.csr"
# Create client extensions file
cat > "$CLIENT_DIR/client-extfile.cnf" << EOF
extendedKeyUsage = clientAuth
EOF
# Sign client certificate
openssl x509 -req -days 365 -in "$CLIENT_DIR/client.csr" -CA "$CA_DIR/ca.pem" -CAkey "$CA_DIR/ca-key.pem" -CAcreateserial -out "$CLIENT_DIR/cert.pem" -extfile "$CLIENT_DIR/client-extfile.cnf"
# Copy CA certificate to client and server directories
cp "$CA_DIR/ca.pem" "$CLIENT_DIR/ca.pem"
cp "$CA_DIR/ca.pem" "$SERVER_DIR/ca.pem"
# Set appropriate permissions
chmod 600 "$CA_DIR/ca-key.pem" "$SERVER_DIR/server-key.pem" "$CLIENT_DIR/key.pem"
chmod 644 "$CA_DIR/ca.pem" "$SERVER_DIR/cert.pem" "$CLIENT_DIR/cert.pem"
echo "TLS certificates generated successfully in $CERT_DIR"
echo "CA certificate: $CA_DIR/ca.pem"
echo "Server cert: $SERVER_DIR/cert.pem"
echo "Client cert: $CLIENT_DIR/cert.pem"

View File

@@ -8,14 +8,13 @@ localhost {
reverse_proxy session-manager:8000 reverse_proxy session-manager:8000
} }
# Session-specific routing - dynamically route to containers # Session-specific routing - proxy to session manager for dynamic routing
handle /session/{session_id}* { handle /session/{session_id}* {
# Strip the session prefix and pass to session manager
uri strip_prefix /session/{session_id} uri strip_prefix /session/{session_id}
# This will be handled by dynamic routing in the session manager # Add session ID as header for routing
reverse_proxy session-manager:8000 { header X-Session-ID {session_id}
# The session manager will return the actual container port reverse_proxy session-manager:8000
# This is a simplified version for development
}
} }
# Static files and main interface # Static files and main interface

View File

@@ -2,4 +2,5 @@ fastapi==0.104.1
uvicorn==0.24.0 uvicorn==0.24.0
docker==7.0.0 docker==7.0.0
pydantic==2.5.0 pydantic==2.5.0
python-multipart==0.0.6 python-multipart==0.0.6
httpx==0.25.2

View File

@@ -5,6 +5,7 @@ WORKDIR /app
# Install system dependencies # Install system dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
curl \ curl \
docker.io \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching # Copy requirements first for better caching

View File

@@ -16,10 +16,12 @@ from contextlib import asynccontextmanager
import docker import docker
from docker.errors import DockerException, NotFound from docker.errors import DockerException, NotFound
from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, Response
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
import uvicorn import uvicorn
import httpx
import asyncio
# Configuration # Configuration
@@ -45,7 +47,16 @@ class SessionData(BaseModel):
class SessionManager: class SessionManager:
def __init__(self): def __init__(self):
self.docker_client = docker.from_env() # Use TLS certificates for secure Docker communication
tls_config = docker.tls.TLSConfig(
client_cert=("/certs/client/cert.pem", "/certs/client/key.pem"),
ca_cert="/certs/client/ca.pem",
verify=True,
)
self.docker_client = docker.DockerClient(
base_url=os.getenv("DOCKER_HOST", "tcp://docker-daemon:2376"),
tls=tls_config,
)
self.sessions: Dict[str, SessionData] = {} self.sessions: Dict[str, SessionData] = {}
self._load_sessions() self._load_sessions()
@@ -316,6 +327,56 @@ async def trigger_cleanup():
return {"message": "Cleanup completed"} return {"message": "Cleanup completed"}
@app.api_route(
"/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
)
async def proxy_to_session(request: Request, path: str):
"""Proxy requests to session containers based on X-Session-ID header"""
session_id = request.headers.get("X-Session-ID")
if not session_id:
raise HTTPException(status_code=400, detail="Missing X-Session-ID header")
session = await session_manager.get_session(session_id)
if not session or session.status != "running":
raise HTTPException(status_code=404, detail="Session not found or not running")
# Proxy the request to the container
container_url = f"http://localhost:{session.port}"
# Prepare the request
url = f"{container_url}/{path}"
if request.url.query:
url += f"?{request.url.query}"
# Get request body
body = await request.body()
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.request(
method=request.method,
url=url,
headers={
k: v
for k, v in request.headers.items()
if k.lower() not in ["host", "x-session-id"]
},
content=body,
follow_redirects=False,
)
# Return the proxied response
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
)
except httpx.RequestError as e:
raise HTTPException(
status_code=502, detail=f"Container proxy error: {str(e)}"
)
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint""" """Health check endpoint"""