docs(spec): V2.0.0 unified image + token-based env routing
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>
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user