refactor(mcp_common): remove risk_guard, models, env_validation, storage

This commit is contained in:
AdrianoDev
2026-04-27 17:38:44 +02:00
parent e888fc373d
commit 888a3cde84
9 changed files with 1 additions and 571 deletions
+1 -19
View File
@@ -1,19 +1 @@
from mcp_common.models import (
Event,
EventPriority,
EventType,
L1State,
L2Entry,
L3Entry,
UserInstruction,
)
__all__ = [
"L1State",
"L2Entry",
"L3Entry",
"Event",
"EventPriority",
"EventType",
"UserInstruction",
]
__all__ = []
@@ -1,80 +0,0 @@
"""CER-P5-010: env validation policy — fail-fast per mandatory, soft per optional.
Usage al boot di ogni mcp `__main__.py`:
from mcp_common.env_validation import require_env, optional_env, summarize
creds_file = require_env("CREDENTIALS_FILE", "deribit credentials JSON path")
host = optional_env("HOST", default="0.0.0.0")
summarize(["CREDENTIALS_FILE", "HOST", "PORT"])
"""
from __future__ import annotations
import logging
import os
import sys
logger = logging.getLogger(__name__)
class MissingEnvError(RuntimeError):
"""Mandatory env var absent or empty."""
def require_env(name: str, description: str = "") -> str:
"""Fail-fast: raise MissingEnvError se name non presente o vuoto.
Uscita dal processo con codice 2 se chiamato dal main(). Comporta
logging chiaro del missing var prima dell'exit.
"""
val = (os.environ.get(name) or "").strip()
if not val:
msg = f"missing mandatory env var: {name}"
if description:
msg += f" ({description})"
logger.error(msg)
raise MissingEnvError(msg)
return val
def optional_env(name: str, *, default: str = "") -> str:
"""Soft: ritorna env o default. Log INFO se default usato."""
val = (os.environ.get(name) or "").strip()
if not val:
if default:
logger.info("env %s not set, using default=%r", name, default)
return default
return val
def summarize(names: list[str]) -> None:
"""Log INFO di tutti gli env rilevanti con presenza (mask se SECRET/KEY/TOKEN)."""
sensitive_tokens = ("SECRET", "KEY", "TOKEN", "PASSWORD", "CREDENTIAL", "WALLET")
for n in names:
val = os.environ.get(n)
if val is None:
logger.info("env[%s]: <unset>", n)
continue
if any(t in n.upper() for t in sensitive_tokens):
logger.info("env[%s]: <set, %d chars>", n, len(val))
else:
logger.info("env[%s]: %s", n, val)
def fail_fast_if_missing(names: list[str]) -> None:
"""Verifica lista di nomi mandatory al boot. Exit 2 se uno solo manca.
Uso preferito: early call in main() per bloccare boot se config incompleta.
"""
missing: list[str] = []
for n in names:
if not (os.environ.get(n) or "").strip():
missing.append(n)
if missing:
logger.error("boot aborted: missing mandatory env vars: %s", missing)
print(
f"FATAL: missing mandatory env vars: {missing}",
file=sys.stderr,
)
sys.exit(2)
-98
View File
@@ -1,98 +0,0 @@
from __future__ import annotations
from enum import StrEnum
from functools import total_ordering
from typing import Any
from pydantic import BaseModel, Field
@total_ordering
class EventPriority(StrEnum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
CRITICAL = "critical"
def _rank(self) -> int:
return ["low", "normal", "high", "critical"].index(self.value)
def __lt__(self, other: EventPriority) -> bool:
return self._rank() < other._rank()
class EventType(StrEnum):
ALERT = "alert"
USER_INSTRUCTION = "user_instruction"
SYSTEM = "system"
class L1State(BaseModel):
"""Singleton row with current operational state."""
updated_at: str
equity_total: float | None = None
equity_by_exchange: dict[str, float] = Field(default_factory=dict)
bias: str | None = None
pnl_day: float | None = None
pnl_total: float | None = None
capital: float | None = None
open_positions_count: int = 0
greeks_aggregate: dict[str, float] = Field(default_factory=dict)
notes: str | None = None
class L2Entry(BaseModel):
"""Reasoning entry — schema matches system_prompt v2.
`authored_by_model`: identifica l'LLM che ha generato la entry (es.
"google/gemini-3-flash-preview" per core, "anthropic/claude-haiku-4-5"
per worker). None se scritto da operatore umano via UI.
"""
timestamp: str
setup: str
tesi: str | None = None
tesi_check: str | None = None
invalidation: str | None = None
esito: str
scostamento: str | None = None
scostamento_sigma: float | None = None
lezione: str | None = None
sizing_note: str | None = None
run_id: str | None = None
user_instruction_id: int | None = None
authored_by_model: str | None = None
class L3Entry(BaseModel):
"""Compacted pattern from L2 batch."""
created_at: str
category: str # "pattern_errore" | "pattern_vincente" | "correlazione"
summary: str
source_l2_ids: list[int]
class Event(BaseModel):
id: int | None = None
created_at: str
expires_at: str
type: EventType
source: str
priority: EventPriority
payload: dict[str, Any]
acked_at: str | None = None
ack_outcome: str | None = None
ack_notes: str | None = None
class UserInstruction(BaseModel):
id: int | None = None
created_at: str
text: str
priority: EventPriority
require_ack: bool = True
source: str = "observer"
acked_at: str | None = None
ack_outcome: str | None = None
@@ -1,92 +0,0 @@
"""CER-016 hard guard server-side su place_order.
Caps configurabili via env (default safety-first, mirati a ~200 EUR single,
1000 EUR aggregato, 3x max leverage).
Thresholds sono numerici semplici — l'operatore stabilisce unità (EUR/USD)
via env; il server compara su un unico campo `notional` in valore monetario.
"""
from __future__ import annotations
import os
from fastapi import HTTPException
def _env_float(name: str, default: float) -> float:
raw = os.environ.get(name)
if not raw:
return default
try:
return float(raw)
except (TypeError, ValueError):
return default
def _env_int(name: str, default: int) -> int:
raw = os.environ.get(name)
if not raw:
return default
try:
return int(raw)
except (TypeError, ValueError):
return default
def max_notional() -> float:
return _env_float("CERBERO_MAX_NOTIONAL", 200.0)
def max_aggregate() -> float:
return _env_float("CERBERO_MAX_AGGREGATE", 1000.0)
def max_leverage() -> int:
return _env_int("CERBERO_MAX_LEVERAGE", 3)
def _hard_reject(reason: str) -> None:
raise HTTPException(
status_code=403,
detail={
"error": "HARD_PROHIBITION",
"message": reason,
"caps": {
"max_notional": max_notional(),
"max_aggregate": max_aggregate(),
"max_leverage": max_leverage(),
},
},
)
def enforce_leverage(leverage: int | float | None) -> int:
"""Ritorna leverage applicabile. Default 3x se None. Reject se > cap."""
cap = max_leverage()
if leverage is None:
return cap
lev = int(leverage)
if lev < 1:
_hard_reject(f"leverage must be >= 1 (got {lev})")
if lev > cap:
_hard_reject(f"leverage {lev}x exceeds hard cap {cap}x")
return lev
def enforce_single_notional(notional: float, *, exchange: str, instrument: str) -> None:
cap = max_notional()
if notional > cap:
_hard_reject(
f"{exchange}.{instrument} notional {notional:.2f} exceeds single trade cap {cap:.2f}"
)
def enforce_aggregate(current_total: float, new_notional: float) -> None:
cap = max_aggregate()
total = current_total + new_notional
if total > cap:
_hard_reject(
f"aggregate notional {total:.2f} (current {current_total:.2f} + new "
f"{new_notional:.2f}) exceeds cap {cap:.2f}"
)
-43
View File
@@ -1,43 +0,0 @@
from __future__ import annotations
import sqlite3
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Database:
path: Path
conn: sqlite3.Connection | None = None
def connect(self) -> sqlite3.Connection:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.conn = sqlite3.connect(
str(self.path),
isolation_level=None,
check_same_thread=False,
)
self.conn.row_factory = sqlite3.Row
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA synchronous=NORMAL")
self.conn.execute("PRAGMA foreign_keys=ON")
return self.conn
def close(self) -> None:
if self.conn is not None:
self.conn.close()
self.conn = None
def run_migrations(conn: sqlite3.Connection, migrations: dict[int, str]) -> None:
"""Idempotent migrations. `migrations` keys are monotonic version numbers."""
conn.execute(
"CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER PRIMARY KEY)"
)
cur = conn.execute("SELECT COALESCE(MAX(version), 0) FROM _schema_version")
current = cur.fetchone()[0]
for version in sorted(migrations):
if version <= current:
continue
conn.executescript(migrations[version])
conn.execute("INSERT INTO _schema_version (version) VALUES (?)", (version,))