a1110c8ecb
#2 Env switch safety: - mcp_common/environment.py: nuova consistency_check() che previene switch accidentali a mainnet. Solleva EnvironmentMismatchError se resolved=mainnet senza creds["environment"]="mainnet" esplicito, o se declared/resolved mismatch. Override via STRICT_MAINNET=false. - Wirato in app_factory.run_exchange_main al boot. - 6 nuovi test consistency. #3 Audit log persistence: - mcp_common/audit.py: TimedRotatingFileHandler aggiuntivo se env AUDIT_LOG_FILE settato. Rotation midnight UTC, retention 30gg default (AUDIT_LOG_BACKUP_DAYS). Format JSONL con SecretsFilter. - docker-compose.prod.yml: bind mount /var/log/cerbero-mcp + env AUDIT_LOG_FILE per i 4 servizi exchange (write endpoints). - 2 nuovi test file sink. #1 Deploy script: - scripts/deploy.sh: idempotente, fa docker login + clone/pull repo + copia secrets chmod 600 + crea .env + setup audit dir + pull image + up + smoke test pubblico HTTPS. - DEPLOYMENT.md aggiornato: sezioni 2 (script), 3 (safety mainnet), 4 (audit log query), renumber sezioni successive. Test: 488/488 verdi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
2.8 KiB
Python
96 lines
2.8 KiB
Python
"""App factory comune per i servizi mcp-{exchange}.
|
|
|
|
Centralizza il boilerplate dei `__main__.py`:
|
|
- configure_root_logging (JSON)
|
|
- fail_fast_if_missing su env mandatory
|
|
- summarize env
|
|
- load creds JSON
|
|
- resolve_environment con default URLs
|
|
- load token store
|
|
- delega creazione client + app a callback per-servizio
|
|
- uvicorn.run
|
|
|
|
Ogni servizio invoca `run_exchange_main(spec)` con uno spec dichiarativo.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import uvicorn
|
|
|
|
from mcp_common.auth import load_token_store_from_files
|
|
from mcp_common.env_validation import fail_fast_if_missing, require_env, summarize
|
|
from mcp_common.environment import (
|
|
EnvironmentInfo,
|
|
consistency_check,
|
|
resolve_environment,
|
|
)
|
|
from mcp_common.logging import configure_root_logging
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExchangeAppSpec:
|
|
exchange: str
|
|
creds_env_var: str
|
|
env_var: str # es. "BYBIT_TESTNET", "ALPACA_PAPER"
|
|
flag_key: str # campo nel secret JSON ("testnet" o "paper")
|
|
default_base_url_live: str
|
|
default_base_url_testnet: str
|
|
default_port: int
|
|
build_client: Callable[[dict, EnvironmentInfo], Any]
|
|
build_app: Callable[..., Any]
|
|
extra_summarize_envs: tuple[str, ...] = ()
|
|
|
|
|
|
def run_exchange_main(spec: ExchangeAppSpec) -> None:
|
|
configure_root_logging()
|
|
|
|
fail_fast_if_missing([spec.creds_env_var])
|
|
summarize([
|
|
spec.creds_env_var,
|
|
"CORE_TOKEN_FILE",
|
|
"OBSERVER_TOKEN_FILE",
|
|
"PORT",
|
|
"HOST",
|
|
spec.env_var,
|
|
*spec.extra_summarize_envs,
|
|
])
|
|
|
|
creds_file = require_env(spec.creds_env_var, f"{spec.exchange} credentials JSON path")
|
|
with open(creds_file) as f:
|
|
creds = json.load(f)
|
|
|
|
env_info = resolve_environment(
|
|
creds,
|
|
env_var=spec.env_var,
|
|
flag_key=spec.flag_key,
|
|
exchange=spec.exchange,
|
|
default_base_url_live=spec.default_base_url_live,
|
|
default_base_url_testnet=spec.default_base_url_testnet,
|
|
)
|
|
|
|
# Safety: previene switch accidentali a mainnet senza conferma esplicita
|
|
# nel secret. Solleva EnvironmentMismatchError → boot abort se mismatch.
|
|
strict_mainnet = os.environ.get("STRICT_MAINNET", "true").lower() not in ("0", "false", "no")
|
|
consistency_check(env_info, creds, strict_mainnet=strict_mainnet)
|
|
|
|
client = spec.build_client(creds, env_info)
|
|
|
|
token_store = load_token_store_from_files(
|
|
core_token_file=os.environ.get("CORE_TOKEN_FILE"),
|
|
observer_token_file=os.environ.get("OBSERVER_TOKEN_FILE"),
|
|
)
|
|
|
|
app = spec.build_app(client=client, token_store=token_store, creds=creds, env_info=env_info)
|
|
|
|
uvicorn.run(
|
|
app,
|
|
log_config=None,
|
|
host=os.environ.get("HOST", "0.0.0.0"),
|
|
port=int(os.environ.get("PORT", str(spec.default_port))),
|
|
)
|