9a137563e8
Spec architetturale per V2.0.0: collassa 7 immagini Docker (gateway Caddy + 6 servizi MCP) in una singola immagine multi-router. Switch testnet/mainnet diventa runtime per-request via bearer token (TESTNET_TOKEN / MAINNET_TOKEN). Configurazione consolidata in singolo .env, secret JSON eliminati. Swagger UI esposto a /apidocs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
563 lines
21 KiB
Markdown
563 lines
21 KiB
Markdown
# Cerbero MCP — V2.0.0: Unified Image, Token-Based Environment Routing
|
||
|
||
**Branch:** `V2.0.0`
|
||
**Data spec:** 2026-04-30
|
||
**Autore:** Adriano
|
||
|
||
## Sommario esecutivo
|
||
|
||
Riscrittura architetturale di Cerbero MCP per ridurre superficie operativa e
|
||
semplificare la gestione di testnet/mainnet. Si passa da 7 immagini Docker
|
||
(gateway Caddy + 6 servizi MCP) a una **singola immagine** che ospita tutti
|
||
i router exchange in un unico processo FastAPI. La distinzione fra ambiente
|
||
testnet e mainnet, oggi decisa al boot del container tramite variabili di
|
||
override e flag nei secret JSON, viene spostata **a runtime per-request**:
|
||
il bearer token presentato dal client (`Authorization: Bearer <token>`)
|
||
determina quale endpoint upstream viene contattato per quella specifica
|
||
chiamata. Tutta la configurazione confluisce in un singolo file `.env`,
|
||
eliminando i secret JSON separati. Viene infine esposta documentazione
|
||
OpenAPI interattiva via Swagger UI all'indirizzo `/apidocs`.
|
||
|
||
## Motivazioni
|
||
|
||
- **Operatività**: 7 container, 8 secret file, 4 docker-compose overlay e un
|
||
gateway Caddy con plugin di rate-limit sono troppo per il volume di
|
||
traffico atteso. Un singolo container è sufficiente.
|
||
- **Flessibilità ambiente**: oggi un bot che vuole leggere mainnet e
|
||
scrivere testnet deve coordinare due deploy. Con il routing per-request
|
||
basta scegliere il bearer giusto per ogni chiamata.
|
||
- **Configurazione**: 8 secret JSON + 2 token file + variabili di override
|
||
in 4 compose file = stato distribuito difficile da auditare. Un singolo
|
||
`.env` rende ovvio cosa è configurato.
|
||
- **DX dev**: oggi serve `docker compose up` anche per iterare. V2 punta a
|
||
`uv run cerbero-mcp` diretto su laptop senza Docker.
|
||
- **Discovery API**: senza Swagger l'unica fonte sulle tool è il codice.
|
||
`/apidocs` rende le tool esplorabili dal browser, e `/openapi.json` le
|
||
rende leggibili a LLM senza bisogno del protocollo MCP completo.
|
||
|
||
## Decisioni di design
|
||
|
||
| # | Domanda | Decisione |
|
||
|---|---|---|
|
||
| 1 | Significato di "passare il token alla funzione" | Routing per-request via bearer: lo stesso container serve testnet e mainnet contemporaneamente, decide URL upstream a runtime |
|
||
| 2 | Granularità token | 2 token globali (`TESTNET_TOKEN`, `MAINNET_TOKEN`) validi per tutti gli exchange |
|
||
| 3 | ACL core/observer (read-only) | Eliminata. Protezione write rimane via leverage cap server-side e via firewall (Traefik su VPS) |
|
||
| 4 | Scope "single image" | Un'unica immagine, **un solo container** con multi-router interno (un processo Python) |
|
||
| 5 | Gateway L7 | Eliminato dal repo. Su VPS prod c'è Traefik gestito esternamente. In dev nessun gateway |
|
||
| 6 | Formato configurazione | Tutto in `.env`. Nessun JSON. La porta è in `.env` |
|
||
| 7 | Swagger | `/apidocs` ON, `/openapi.json` ON, `/redoc` OFF, `/docs` OFF |
|
||
| 8a | Dispatch exchange | Path-based: `/mcp-{exchange}/tools/{tool}` (backward-compat con bot V1) |
|
||
| 8b | Lifecycle client exchange | Cache lazy `(exchange, env) → client`, max 8 client (4 exchange × 2 env) + 2 client read-only (macro, sentiment) |
|
||
|
||
Decisioni esplicite anche su:
|
||
|
||
- **Macro e sentiment richiedono token valido**: anche le tool puramente
|
||
read-only passano dal middleware auth. Bearer assente o non riconosciuto
|
||
→ 401. Il valore del token (testnet o mainnet) è ignorato per macro e
|
||
sentiment perché non hanno endpoint testnet, ma uno dei due deve essere
|
||
presente.
|
||
- **Nessuna policy `ALLOW_MAINNET`**: la protezione contro uso accidentale
|
||
di mainnet è demandata a (a) custodia dei token (mainnet token solo a bot
|
||
autorizzati), (b) Traefik IP allowlist sul VPS, (c) leverage cap
|
||
server-side già esistente per ogni exchange.
|
||
- **`docker-compose.yml` minimo** mantenuto per chi vuole usare Docker
|
||
localmente; `docker-compose.{prod,traefik,local}.yml` eliminati.
|
||
|
||
## Architettura
|
||
|
||
### Stack runtime
|
||
|
||
```
|
||
Prod (VPS):
|
||
Traefik (TLS, allowlist) ──▶ container cerbero-mcp:9000
|
||
|
||
Dev (laptop):
|
||
uv run cerbero-mcp ──▶ http://localhost:9000
|
||
```
|
||
|
||
Nessun gateway nel repo. Traefik è gestito fuori da questo progetto, con
|
||
label aggiunte tramite override compose esterno. In dev FastAPI è esposto
|
||
direttamente via uvicorn.
|
||
|
||
### Struttura sorgenti
|
||
|
||
```
|
||
Cerbero_mcp/
|
||
├── pyproject.toml # singolo package "cerbero-mcp"
|
||
├── uv.lock
|
||
├── Dockerfile # multi-stage builder + runtime slim
|
||
├── docker-compose.yml # minimo: 1 servizio, env_file: .env
|
||
├── .env.example # template completo, versionato
|
||
├── .gitignore # .env escluso
|
||
├── README.md # riscritto V2
|
||
│
|
||
├── docs/
|
||
│ └── superpowers/
|
||
│ ├── specs/
|
||
│ │ ├── 2026-04-27-cot-report-design.md (storico)
|
||
│ │ └── 2026-04-30-V2.0.0-unified-image-...-design.md
|
||
│ └── plans/
|
||
│ ├── 2026-04-27-cot-report.md (storico)
|
||
│ └── 2026-04-30-V2.0.0-unified-image-...-plan.md
|
||
│
|
||
├── src/cerbero_mcp/
|
||
│ ├── __init__.py
|
||
│ ├── __main__.py # entrypoint: uvicorn.run(app, ...)
|
||
│ ├── settings.py # Pydantic Settings per .env
|
||
│ ├── auth.py # bearer → environment
|
||
│ ├── server.py # build_app(): FastAPI + middleware + handlers + swagger
|
||
│ ├── client_registry.py # cache lazy {(exchange, env): client}
|
||
│ ├── routers/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── deribit.py
|
||
│ │ ├── bybit.py
|
||
│ │ ├── hyperliquid.py
|
||
│ │ ├── alpaca.py
|
||
│ │ ├── macro.py
|
||
│ │ └── sentiment.py
|
||
│ ├── exchanges/
|
||
│ │ ├── deribit/{client.py, tools.py, leverage_cap.py}
|
||
│ │ ├── bybit/{client.py, tools.py, leverage_cap.py}
|
||
│ │ ├── hyperliquid/{client.py, tools.py, leverage_cap.py}
|
||
│ │ ├── alpaca/{client.py, tools.py, leverage_cap.py}
|
||
│ │ ├── macro/{client.py, tools.py}
|
||
│ │ └── sentiment/{client.py, tools.py}
|
||
│ └── common/
|
||
│ ├── indicators.py
|
||
│ ├── options.py
|
||
│ ├── microstructure.py
|
||
│ ├── stats.py
|
||
│ ├── http.py
|
||
│ ├── audit.py
|
||
│ ├── logging.py
|
||
│ └── errors.py # error envelope
|
||
│
|
||
├── tests/
|
||
│ ├── unit/
|
||
│ ├── integration/
|
||
│ └── smoke/
|
||
│
|
||
└── scripts/
|
||
└── build-push.sh # build di 1 sola immagine
|
||
```
|
||
|
||
**Cosa viene eliminato:**
|
||
|
||
- `services/` (intera struttura monorepo a 7 sub-package, sostituita da
|
||
`src/cerbero_mcp/`)
|
||
- `gateway/` (Caddy + Caddyfile + landing page)
|
||
- `secrets/` (8 file JSON + 2 token file)
|
||
- `docker/` (7 Dockerfile separati, sostituiti da `Dockerfile` in root)
|
||
- `docker-compose.prod.yml`, `docker-compose.local.yml`,
|
||
`docker-compose.traefik.yml`
|
||
- `DEPLOYMENT.md` (i contenuti ancora validi confluiscono in `README.md`)
|
||
- `mcp_common.environment` (resolver boot-time, sostituito da `auth.py`
|
||
runtime)
|
||
- `mcp_common.env_validation` (sostituito da Pydantic Settings)
|
||
- `mcp_common.app_factory` (boilerplate boot, integrato in `server.py`)
|
||
|
||
**Cosa resta uguale:**
|
||
|
||
- Path layout `/mcp-{exchange}/tools/{tool}` (backward-compat con bot V1)
|
||
- Tool MCP individuali: firme, response shape, error envelope, header
|
||
`X-Data-Timestamp` e `X-Duration-Ms`
|
||
- Logica indicatori quantitativi (`indicators`, `options`,
|
||
`microstructure`, `stats`)
|
||
- Healthcheck `/health` (formato identico)
|
||
- Leverage cap server-side per exchange
|
||
- Tool MCP-bridge (se in uso) preservato in `common/mcp_bridge.py`
|
||
|
||
### Flusso request
|
||
|
||
```
|
||
1. Bot HTTP request
|
||
POST /mcp-deribit/tools/place_order
|
||
Authorization: Bearer tk_test_xxx
|
||
{ "symbol":"BTC-PERPETUAL", "side":"buy", "qty": 0.1 }
|
||
|
||
2. Middleware AuthBearer (auth.py)
|
||
- whitelist path: /apidocs, /openapi.json, /health → bypass
|
||
- estrae bearer
|
||
- confronta con settings.testnet_token / settings.mainnet_token
|
||
- match testnet → request.state.environment = "testnet"
|
||
- match mainnet → request.state.environment = "mainnet"
|
||
- nessun match → 401 UNAUTHORIZED
|
||
|
||
3. Router deribit (routers/deribit.py)
|
||
- FastAPI valida body con Pydantic schema
|
||
- dependency get_env(request) -> "testnet"|"mainnet"
|
||
- dependency get_client(env) -> DeribitClient
|
||
|
||
4. Client Registry (client_registry.py)
|
||
- chiave (exchange="deribit", env="testnet")
|
||
- cache hit → return; miss → costruisce client lazy + auth iniziale + cache
|
||
|
||
5. Tool impl (exchanges/deribit/tools.py)
|
||
- leverage_cap.enforce(qty, max_leverage)
|
||
- client.place_order(...)
|
||
- response shape standard con data_timestamp, request_id
|
||
|
||
6. Response middleware
|
||
- X-Duration-Ms header
|
||
- data_timestamp injection se mancante
|
||
|
||
7. 200 JSON
|
||
```
|
||
|
||
### Auth
|
||
|
||
```python
|
||
# src/cerbero_mcp/auth.py (esempio sintetico)
|
||
from fastapi import Request, HTTPException, status
|
||
from typing import Literal
|
||
|
||
Environment = Literal["testnet", "mainnet"]
|
||
|
||
WHITELIST_PATHS = {"/health", "/apidocs", "/openapi.json"}
|
||
|
||
async def auth_middleware(request: Request, call_next):
|
||
if request.url.path in WHITELIST_PATHS:
|
||
return await call_next(request)
|
||
auth = request.headers.get("Authorization", "")
|
||
if not auth.startswith("Bearer "):
|
||
raise HTTPException(401, "missing bearer token")
|
||
token = auth[len("Bearer "):].strip()
|
||
settings = request.app.state.settings
|
||
if token == settings.testnet_token.get_secret_value():
|
||
request.state.environment = "testnet"
|
||
elif token == settings.mainnet_token.get_secret_value():
|
||
request.state.environment = "mainnet"
|
||
else:
|
||
raise HTTPException(401, "invalid token")
|
||
return await call_next(request)
|
||
```
|
||
|
||
Confronto token con `secrets.compare_digest` per evitare timing attack.
|
||
|
||
### Client registry
|
||
|
||
```python
|
||
# src/cerbero_mcp/client_registry.py (sintesi)
|
||
class ClientRegistry:
|
||
def __init__(self, settings):
|
||
self._settings = settings
|
||
self._clients: dict[tuple[str, Environment], Any] = {}
|
||
self._locks: dict[tuple[str, Environment], asyncio.Lock] = defaultdict(asyncio.Lock)
|
||
|
||
async def get(self, exchange: str, env: Environment) -> Any:
|
||
key = (exchange, env)
|
||
if key in self._clients:
|
||
return self._clients[key]
|
||
async with self._locks[key]:
|
||
if key in self._clients:
|
||
return self._clients[key]
|
||
client = await self._build(exchange, env)
|
||
self._clients[key] = client
|
||
return client
|
||
|
||
async def aclose(self):
|
||
for c in self._clients.values():
|
||
await c.aclose()
|
||
```
|
||
|
||
- Costruzione **lazy** al primo uso → boot rapido, no auth verso exchange
|
||
non usati
|
||
- **Lock per chiave** evita doppia istanziazione in caso di race
|
||
- Macro e sentiment usano stesso client per testnet e mainnet (l'env è
|
||
ignorato), ma per uniformità API ricevono ugualmente la chiave
|
||
`(exchange, env)`
|
||
- Lifespan FastAPI: registry creato in `startup`, chiuso in `shutdown`
|
||
(chiude HTTP pool, websocket eventuali, sessioni)
|
||
|
||
### Configurazione: `.env`
|
||
|
||
Singola sorgente di verità, letta da Pydantic Settings al boot. File
|
||
versionato come `.env.example` con placeholder vuoti; `.env` reale
|
||
gitignored.
|
||
|
||
```bash
|
||
# SERVER
|
||
HOST=0.0.0.0
|
||
PORT=9000
|
||
LOG_LEVEL=info
|
||
|
||
# AUTH
|
||
TESTNET_TOKEN=
|
||
MAINNET_TOKEN=
|
||
|
||
# DERIBIT
|
||
DERIBIT_CLIENT_ID=
|
||
DERIBIT_CLIENT_SECRET=
|
||
DERIBIT_URL_LIVE=https://www.deribit.com/api/v2
|
||
DERIBIT_URL_TESTNET=https://test.deribit.com/api/v2
|
||
DERIBIT_MAX_LEVERAGE=3
|
||
|
||
# BYBIT
|
||
BYBIT_API_KEY=
|
||
BYBIT_API_SECRET=
|
||
BYBIT_URL_LIVE=https://api.bybit.com
|
||
BYBIT_URL_TESTNET=https://api-testnet.bybit.com
|
||
BYBIT_MAX_LEVERAGE=3
|
||
|
||
# HYPERLIQUID
|
||
HYPERLIQUID_WALLET_ADDRESS=
|
||
HYPERLIQUID_API_WALLET_ADDRESS=
|
||
HYPERLIQUID_PRIVATE_KEY=
|
||
HYPERLIQUID_URL_LIVE=https://api.hyperliquid.xyz
|
||
HYPERLIQUID_URL_TESTNET=https://api.hyperliquid-testnet.xyz
|
||
HYPERLIQUID_MAX_LEVERAGE=3
|
||
|
||
# ALPACA
|
||
ALPACA_API_KEY_ID=
|
||
ALPACA_SECRET_KEY=
|
||
ALPACA_URL_LIVE=https://api.alpaca.markets
|
||
ALPACA_URL_TESTNET=https://paper-api.alpaca.markets
|
||
ALPACA_MAX_LEVERAGE=1
|
||
|
||
# MACRO
|
||
FRED_API_KEY=
|
||
FINNHUB_API_KEY=
|
||
|
||
# SENTIMENT
|
||
CRYPTOPANIC_KEY=
|
||
LUNARCRUSH_KEY=
|
||
```
|
||
|
||
Pydantic Settings con `SecretStr` per i valori sensibili evita leak nei
|
||
log/repr. `extra="ignore"` ammette env aggiuntive (variabili di sistema,
|
||
Docker) senza crash. Validation fail-fast al boot.
|
||
|
||
### Swagger / OpenAPI
|
||
|
||
```python
|
||
app = FastAPI(
|
||
title="Cerbero MCP",
|
||
version="2.0.0",
|
||
description="Multi-exchange MCP server. Bearer token decides environment (testnet/mainnet).",
|
||
docs_url="/apidocs",
|
||
redoc_url=None,
|
||
openapi_url="/openapi.json",
|
||
swagger_ui_parameters={
|
||
"persistAuthorization": True,
|
||
"displayRequestDuration": True,
|
||
"filter": True,
|
||
"tryItOutEnabled": True,
|
||
"tagsSorter": "alpha",
|
||
"operationsSorter": "alpha",
|
||
},
|
||
)
|
||
```
|
||
|
||
Aggiunto `securityScheme = BearerAuth` su tutti gli endpoint sotto `/mcp-*`.
|
||
Click su Authorize in Swagger → input bearer → tutte le richieste "Try it
|
||
out" mandano il header. Cambio token = cambio ambiente senza ricaricare.
|
||
|
||
Tag organizzati per exchange:
|
||
- `system` → `/health`
|
||
- `deribit`, `bybit`, `hyperliquid`, `alpaca`, `macro`, `sentiment` → tool
|
||
rispettive
|
||
|
||
Ogni request body Pydantic include `examples=[...]` con almeno un esempio
|
||
realistico. Per response shape complesse (gamma profile, orderbook
|
||
imbalance) anche le response Pydantic includono examples.
|
||
|
||
### Dockerfile e immagine
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1.7
|
||
|
||
FROM python:3.11-slim AS builder
|
||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||
build-essential curl && rm -rf /var/lib/apt/lists/*
|
||
RUN pip install --no-cache-dir "uv>=0.5,<0.7"
|
||
WORKDIR /app
|
||
COPY pyproject.toml uv.lock ./
|
||
COPY src ./src
|
||
RUN uv sync --frozen --no-dev
|
||
|
||
FROM python:3.11-slim AS runtime
|
||
LABEL org.opencontainers.image.title="cerbero-mcp" \
|
||
org.opencontainers.image.version="2.0.0"
|
||
WORKDIR /app
|
||
COPY --from=builder /app /app
|
||
ENV PATH="/app/.venv/bin:$PATH" \
|
||
HOST=0.0.0.0 \
|
||
PORT=9000 \
|
||
PYTHONUNBUFFERED=1
|
||
RUN useradd -m -u 1000 app && chown -R app:app /app
|
||
USER app
|
||
EXPOSE 9000
|
||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
|
||
CMD python -c "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"PORT\",\"9000\")}/health', timeout=3).close()"
|
||
CMD ["cerbero-mcp"]
|
||
```
|
||
|
||
- 1 sola immagine `cerbero-mcp:2.0.0` (+ `:latest`)
|
||
- Build attesa: ~2-3 min (vs ~12 min × 7 immagini in V1)
|
||
- Image size attesa: ~200 MB
|
||
- Non-root user `app:1000`
|
||
- Healthcheck legge `PORT` da env (rispetta override `.env`)
|
||
|
||
### docker-compose.yml minimo
|
||
|
||
```yaml
|
||
services:
|
||
cerbero-mcp:
|
||
image: cerbero-mcp:2.0.0
|
||
build: .
|
||
container_name: cerbero-mcp
|
||
ports:
|
||
- "${PORT:-9000}:${PORT:-9000}"
|
||
env_file: .env
|
||
restart: unless-stopped
|
||
healthcheck:
|
||
test: ["CMD", "python", "-c",
|
||
"import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"PORT\",\"9000\")}/health', timeout=3).close()"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 3
|
||
```
|
||
|
||
Su VPS si estende con override esterno per applicare label Traefik (file
|
||
non versionato in questo repo).
|
||
|
||
## Errori
|
||
|
||
| Caso | Status | Code |
|
||
|---|---|---|
|
||
| Manca header Authorization | 401 | UNAUTHORIZED |
|
||
| Token non in `{testnet, mainnet}` | 401 | UNAUTHORIZED |
|
||
| Body invalido (Pydantic) | 422 | INVALID_INPUT |
|
||
| Exchange upstream 5xx | 502 | UPSTREAM_ERROR |
|
||
| Rate limit upstream | 429 | RATE_LIMIT |
|
||
| Eccezione non gestita | 500 | UNHANDLED_EXCEPTION |
|
||
|
||
Error envelope identico a V1: campi `error.{type, code, message,
|
||
retryable, suggested_fix?, details?}`, `request_id`, `data_timestamp`.
|
||
|
||
## Test plan
|
||
|
||
**Unit:**
|
||
|
||
- `auth.py`: 4 casi (no header, header malformato, token testnet, token
|
||
mainnet, token invalido). Verifica che `request.state.environment` sia
|
||
settato correttamente.
|
||
- `auth.py`: confronto token usa `secrets.compare_digest` (verifica con
|
||
test che attivi entrambi i rami).
|
||
- `auth.py`: path whitelist (`/health`, `/apidocs`, `/openapi.json`)
|
||
bypassano il middleware.
|
||
- `client_registry.py`: concorrenza — 10 task `get(deribit, testnet)` in
|
||
parallelo, `_build` chiamato 1 sola volta.
|
||
- `client_registry.py`: chiavi diverse istanziano client diversi
|
||
(deribit/testnet ≠ deribit/mainnet ≠ bybit/testnet).
|
||
- `settings.py`: `.env` valido carica senza errori; campo mandatory
|
||
mancante solleva ValidationError al boot.
|
||
- `common/`: tutti i test esistenti su indicators, options,
|
||
microstructure, stats migrano 1:1.
|
||
- Per ogni `exchanges/{exchange}/tools.py`: tool con stub client
|
||
restituisce response shape attesa (test esistenti V1 migrati).
|
||
|
||
**Integration:**
|
||
|
||
- Stub HTTP che intercetta richieste verso URL deribit:
|
||
- request con `Bearer TESTNET_TOKEN` colpisce `DERIBIT_URL_TESTNET`
|
||
- request con `Bearer MAINNET_TOKEN` colpisce `DERIBIT_URL_LIVE`
|
||
- Stesso pattern per bybit, hyperliquid, alpaca.
|
||
- Macro e sentiment: request con bearer testnet o mainnet entrambe
|
||
funzionanti, request senza bearer → 401.
|
||
- Swagger UI: `GET /apidocs` ritorna HTML con securityScheme BearerAuth
|
||
presente.
|
||
- OpenAPI: `GET /openapi.json` ritorna schema valido OpenAPI 3.1 con tag
|
||
per ogni exchange.
|
||
|
||
**Smoke (post-deploy):**
|
||
|
||
```bash
|
||
curl -s http://localhost:9000/health
|
||
curl -s http://localhost:9000/apidocs | grep -q "Cerbero MCP"
|
||
curl -s -H "Authorization: Bearer $TESTNET_TOKEN" \
|
||
http://localhost:9000/mcp-deribit/tools/get_ticker \
|
||
-d '{"instrument": "BTC-PERPETUAL"}'
|
||
```
|
||
|
||
## Migrazione da V1
|
||
|
||
Per chi è in produzione su V1:
|
||
|
||
1. **Backup**: `cp -r secrets/ secrets.v1.bak/`.
|
||
2. **Genera token V2**:
|
||
```bash
|
||
python -c 'import secrets; print("TESTNET_TOKEN=" + secrets.token_urlsafe(32))'
|
||
python -c 'import secrets; print("MAINNET_TOKEN=" + secrets.token_urlsafe(32))'
|
||
```
|
||
3. **Compila `.env`**: mappa V1 → V2 (i campi nei JSON V1 hanno nomi
|
||
leggermente diversi; vedi tabella in README V2 sezione "Migrazione").
|
||
4. **Aggiorna bot client**:
|
||
- URL invariato (`/mcp-{exchange}/tools/{tool}` non cambia)
|
||
- Bearer cambia: dove prima usavi `core.token` o `observer.token`, ora
|
||
usi `TESTNET_TOKEN` o `MAINNET_TOKEN` a seconda dell'ambiente
|
||
desiderato per quella richiesta.
|
||
5. **Spegni V1**: `docker compose down` (vecchio compose).
|
||
6. **Avvia V2**: `docker compose up -d` (nuovo compose minimo) +
|
||
verifica `curl /health` e `curl /apidocs`.
|
||
7. **Rollback** disponibile pullando i tag immagine V1 (`cerbero-mcp-*:1.x`)
|
||
se necessario, ma .env e secrets/ sono incompatibili tra V1 e V2 —
|
||
tenere backup separati.
|
||
|
||
## Pulizia documentazione
|
||
|
||
| File | V1 | Azione V2 |
|
||
|---|---|---|
|
||
| `README.md` | descrive 6 servizi + Caddy + 8 secret + build push 8 immagini | Riscritto da zero |
|
||
| `DEPLOYMENT.md` | runbook 7 immagini, gateway Caddy, allowlist Caddy, deploy no-clone | **Eliminato**. Contenuti utili in `README.md` |
|
||
| `docs/superpowers/specs/2026-04-27-cot-report-design.md` | spec feature passata | Mantenuto (storico) |
|
||
| `docs/superpowers/plans/2026-04-27-cot-report.md` | plan feature passata | Mantenuto (storico) |
|
||
| `docs/superpowers/specs/2026-04-30-V2.0.0-...-design.md` | (nuovo) | Creato |
|
||
| `docs/superpowers/plans/2026-04-30-V2.0.0-...-plan.md` | (nuovo) | Creato dopo writing-plans |
|
||
|
||
Razionale eliminazione `DEPLOYMENT.md`: 16 KB di doc su build-push 8
|
||
immagini, gateway Caddy, secret mounts, IP allowlist Caddy, deploy
|
||
no-clone — tutto obsoleto in V2. Le 30 righe ancora valide (smoke test,
|
||
rollback Watchtower) sono integrate nella sezione "Deploy" del nuovo
|
||
`README.md`.
|
||
|
||
## Out of scope
|
||
|
||
Per evitare scope creep nello stesso sprint, restano fuori:
|
||
|
||
- **HSTS / security headers** custom: in V1 li gestiva Caddy. In V2 si
|
||
delegano a Traefik su VPS (gestito esternamente). Aggiunta a livello
|
||
applicativo non in V2.
|
||
- **Rate limit applicativo** (`slowapi` o simile): demandato a Traefik.
|
||
- **Metriche Prometheus**: rinviate a iterazione successiva.
|
||
- **Token rotation automatica**: fuori scope V2. Rotation manuale
|
||
modificando `.env` + restart container.
|
||
- **Telemetria audit trail**: `mcp_common.audit` viene preservato, ma
|
||
evoluzioni (struttura log, sink) rinviate.
|
||
- **Multi-account per exchange**: V2 supporta 1 account per exchange. Più
|
||
account = future iterazione.
|
||
|
||
## Rischi e mitigazioni
|
||
|
||
| Rischio | Probabilità | Mitigazione |
|
||
|---|---|---|
|
||
| Bug auth → token testnet finisce su URL mainnet | Bassa, alto impatto | Test integration con stub HTTP per ogni exchange; leverage cap server-side resta secondo livello di difesa |
|
||
| Cache `client_registry` non rilascia connessioni → leak fd | Media | Lifespan FastAPI chiama `aclose()` su shutdown; healthcheck monitora processo |
|
||
| Boot fail per env var mancante in `.env` | Alta in dev | Pydantic Settings fail-fast con messaggio chiaro; `.env.example` versionato |
|
||
| Migrazione V1→V2 disallineata bot | Media | Path API invariato; documentare mapping V1→V2 in README |
|
||
| Concorrenza: prima request mainnet e testnet sullo stesso exchange in parallelo | Media | Lock per chiave nel registry impedisce race su `_build` |
|
||
| Image size cresce inattesa | Bassa | Multi-stage slim, base python:3.11-slim, no dipendenze inutili |
|
||
|
||
## Criteri di successo
|
||
|
||
- ✅ `docker compose up -d` avvia 1 container che risponde su `/health`
|
||
entro 5 secondi
|
||
- ✅ `curl /apidocs` rende Swagger UI navigabile
|
||
- ✅ Bot V1 funziona con cambio bearer e basta (path identici)
|
||
- ✅ Stesso bot può alternare testnet e mainnet su request consecutive
|
||
cambiando solo bearer
|
||
- ✅ Tutti i test V1 migrati passano in V2
|
||
- ✅ Tempo di build immagine ridotto da ~12 min a ~3 min
|
||
- ✅ `services/`, `gateway/`, `secrets/`, `docker/`, `DEPLOYMENT.md`,
|
||
3 docker-compose overlay rimossi dal repo
|