fixed findings from review
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -44,3 +57,16 @@ This setup creates a container-per-visitor architecture for the Norwegian legal
|
|||||||
- `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
|
||||||
|
```
|
||||||
@@ -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
60
generate-certs.sh
Executable 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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
Reference in New Issue
Block a user