refactor(mcp_common): remove risk_guard, models, env_validation, storage
This commit is contained in:
@@ -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)
|
||||
@@ -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}"
|
||||
)
|
||||
@@ -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,))
|
||||
Reference in New Issue
Block a user