feat(mcp+runtime): allineamento a Cerbero MCP V2 e flag operativi

Adegua Cerbero Bite alla nuova versione 2.0.0 del server MCP unificato
(testnet/mainnet routing per token, header X-Bot-Tag obbligatorio) e
introduce due interruttori operativi indipendenti per separare la
raccolta dati dall'esecuzione di strategia.

Auth e collegamento MCP
- Token bearer letto dalla nuova variabile CERBERO_BITE_MCP_TOKEN; il
  valore sceglie l'ambiente upstream (testnet vs mainnet) sul server.
  Rimosso il caricamento da file (`secrets/core.token`,
  CERBERO_BITE_CORE_TOKEN_FILE, Docker secret /run/secrets/core_token).
- Aggiunto header X-Bot-Tag (default `BOT__CERBERO_BITE`, override via
  CERBERO_BITE_MCP_BOT_TAG) su ogni call MCP, con validazione lato client
  (non vuoto, ≤ 64 caratteri).
- Cartella `secrets/` rimossa, `.gitignore` ripulito, Dockerfile e
  docker-compose.yml aggiornati con env passthrough e fail-fast quando
  manca il token.

Modalità operativa (RuntimeFlags)
- Nuovo modulo `config/runtime_flags.py` con `RuntimeFlags(
  data_analysis_enabled, strategy_enabled)` e loader che parserizza
  CERBERO_BITE_ENABLE_DATA_ANALYSIS e CERBERO_BITE_ENABLE_STRATEGY
  (true/false/yes/no/on/off/enabled/disabled, case-insensitive).
- L'orchestratore espone i flag, audita e logga la modalità al boot
  (`engine started: env=… data_analysis=… strategy=…`), e in
  `install_scheduler` esclude i job `entry`/`monitor` quando strategy è
  off e il job `market_snapshot` quando data analysis è off. I job di
  infrastruttura (health, backup, manual_actions) restano sempre attivi.
- Default profile = "solo analisi dati" (data_analysis=true,
  strategy=false), pensato per la finestra di soak post-deploy.

GUI saldi
- `gui/live_data.py::_fetch_deribit_currency` riconosce il campo soft
  `error` nel payload V2 (HTTP 200 con `error` valorizzato dal server
  quando l'auth Deribit fallisce) e lo propaga come `BalanceRow.error`,
  evitando di mostrare un fuorviante equity = 0,00.

CLI
- Sostituita l'opzione `--token-file` con `--token` (stringa) sui comandi
  start/dry-run/ping; il default proviene dall'env. Le chiamate al
  builder dell'orchestrator passano anche `bot_tag` e `flags`.

Documentazione
- `docs/04-mcp-integration.md`: descrizione del nuovo flusso di auth V2
  (token = ambiente, X-Bot-Tag nell'audit) e router unificati.
- `docs/06-operational-flow.md`: nuova sezione "Modalità operativa" con
  i tre profili canonici e tabella di gating per ogni job; aggiunto
  `market_snapshot` al cron summary.
- `docs/10-config-spec.md`: nuova sezione "Variabili d'ambiente"
  tabellare con tutti gli env, comprese le bool dei flag operativi.
- `docs/02-architecture.md`: layout del repo aggiornato (`secrets/`
  rimosso, `runtime_flags.py` aggiunto), descrizione di `config/`
  estesa.

Test
- 5 nuovi test su `_fetch_deribit_currency` (soft-error, payload pulito,
  eccezione, error blank, signature parity).
- 7 nuovi test su `load_runtime_flags` (default, override, parsing
  truthy/falsy, blank fallback, valore invalido).
- 4 nuovi test su `HttpToolClient` (X-Bot-Tag default e custom, blank e
  troppo lungo rifiutati).
- 3 nuovi test integration sull'orchestratore (gating dei job in base
  ai flag).
- Test esistenti su token/CLI ping/orchestrator aggiornati al nuovo
  schema. Suite intera: 404 passed, 1 skipped (sqlite3 CLI assente
  sull'host di sviluppo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 17:14:40 +02:00
parent d9454fc996
commit ce158a92dd
23 changed files with 757 additions and 200 deletions
+66 -29
View File
@@ -1,30 +1,40 @@
"""Resolve MCP service URLs and the bearer token.
"""Resolve MCP service URLs, the bearer token and the bot tag.
Cerbero Bite runs in its own Docker container that joins the
``cerbero-suite`` network: every MCP service is reachable by the
container DNS name plus its internal port (``mcp-deribit:9011`` etc.).
Cerbero MCP V2 (a single FastAPI image fronting Deribit, Hyperliquid,
Macro, Sentiment and friends) is deployed on a dedicated VPS and reached
through the public gateway at ``https://cerbero-mcp.tielogic.xyz``. The
server decides the upstream environment (testnet vs mainnet) entirely
from the bearer token attached to each request — Cerbero Bite does not
have to be told which is which: swapping the token in ``.env`` is enough
to switch environments.
The resolver supports two layers of override:
The resolver supports the following layers of override:
1. Per-service environment variables (``CERBERO_BITE_MCP_DERIBIT_URL``,
``CERBERO_BITE_MCP_MACRO_URL``…). Useful for dev when running
outside Docker — point at ``http://localhost:9011`` etc.
2. ``CERBERO_BITE_CORE_TOKEN_FILE`` env var: path to the file that
stores the bearer token (default
``/run/secrets/core_token``). The file is read at boot, the
trailing whitespace is stripped, and the value is *not* logged.
1. Per-service URL env vars (``CERBERO_BITE_MCP_DERIBIT_URL``,
``CERBERO_BITE_MCP_HYPERLIQUID_URL``, ``CERBERO_BITE_MCP_MACRO_URL``,
``CERBERO_BITE_MCP_SENTIMENT_URL``). Useful for local dev when the
bot must talk to a same-host MCP server (``http://localhost:9000``)
instead of the public gateway.
2. ``CERBERO_BITE_MCP_TOKEN`` env var: the bearer token used on every
request. The token's value is *never* logged.
3. ``CERBERO_BITE_MCP_BOT_TAG`` env var: identifier sent on the
``X-Bot-Tag`` header (default ``BOT__CERBERO_BITE``). Must be a
non-empty string of at most 64 characters.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from cerbero_bite.clients._base import DEFAULT_BOT_TAG
__all__ = [
"DEFAULT_BOT_TAG",
"DEFAULT_ENDPOINTS",
"MCP_SERVICES",
"McpEndpoints",
"load_bot_tag",
"load_endpoints",
"load_token",
]
@@ -78,31 +88,58 @@ def load_endpoints(env: dict[str, str] | None = None) -> McpEndpoints:
return McpEndpoints(**resolved)
_DEFAULT_TOKEN_FILE = "/run/secrets/core_token"
_TOKEN_FILE_ENV = "CERBERO_BITE_CORE_TOKEN_FILE"
_TOKEN_ENV = "CERBERO_BITE_MCP_TOKEN"
_BOT_TAG_ENV = "CERBERO_BITE_MCP_BOT_TAG"
_BOT_TAG_MAX_LEN = 64
def load_token(
*,
path: str | Path | None = None,
value: str | None = None,
env: dict[str, str] | None = None,
) -> str:
"""Read the bearer token from disk and return it stripped.
"""Return the MCP bearer token, stripped of surrounding whitespace.
Resolution order:
1. explicit ``path`` argument;
2. ``CERBERO_BITE_CORE_TOKEN_FILE`` env var;
3. ``/run/secrets/core_token`` (Docker secrets default).
1. explicit ``value`` argument (e.g. from a CLI flag);
2. ``CERBERO_BITE_MCP_TOKEN`` env var.
"""
if value is not None:
token = value.strip()
if not token:
raise ValueError("explicit MCP token is empty")
return token
e = env if env is not None else os.environ
target = (
Path(path)
if path is not None
else Path(e.get(_TOKEN_FILE_ENV, _DEFAULT_TOKEN_FILE))
)
if not target.is_file():
raise FileNotFoundError(f"core token file not found: {target}")
token = target.read_text(encoding="utf-8").strip()
raw = e.get(_TOKEN_ENV, "")
token = raw.strip()
if not token:
raise ValueError(f"core token file is empty: {target}")
raise ValueError(
f"{_TOKEN_ENV} is unset or empty; set it in .env to the testnet or "
"mainnet bearer issued by Cerbero MCP"
)
return token
def load_bot_tag(
*,
value: str | None = None,
env: dict[str, str] | None = None,
) -> str:
"""Return the ``X-Bot-Tag`` value, with the project default as fallback.
Resolution order:
1. explicit ``value`` argument;
2. ``CERBERO_BITE_MCP_BOT_TAG`` env var;
3. :data:`DEFAULT_BOT_TAG` (``"BOT__CERBERO_BITE"``).
"""
raw = value if value is not None else (env if env is not None else os.environ).get(
_BOT_TAG_ENV, ""
)
cleaned = raw.strip() if raw else ""
if not cleaned:
return DEFAULT_BOT_TAG
if len(cleaned) > _BOT_TAG_MAX_LEN:
raise ValueError(
f"{_BOT_TAG_ENV} exceeds {_BOT_TAG_MAX_LEN} characters: {cleaned!r}"
)
return cleaned
+78
View File
@@ -0,0 +1,78 @@
"""Operational mode flags read from the environment.
Cerbero Bite supports two independent runtime switches:
* ``CERBERO_BITE_ENABLE_DATA_ANALYSIS`` — when ``true``, the periodic
market-snapshot job is scheduled and writes 15-minute snapshots to
``market_snapshots``; when ``false``, the bot still pings MCP for
health and reconciliation but does not record any market dataset.
* ``CERBERO_BITE_ENABLE_STRATEGY`` — when ``true``, the entry and
monitor cycles are scheduled and may propose/execute trades; when
``false``, no entry or monitor logic runs autonomously (the methods
remain callable from the CLI ``dry-run`` and via manual actions, so
the operator can still test code paths on demand).
The default profile is "analysis only": data analysis on, strategy off.
This is the mode used during the post-deploy soak window where the
team observes data quality before opening any position.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
__all__ = [
"DATA_ANALYSIS_ENV",
"STRATEGY_ENV",
"RuntimeFlags",
"load_runtime_flags",
]
DATA_ANALYSIS_ENV = "CERBERO_BITE_ENABLE_DATA_ANALYSIS"
STRATEGY_ENV = "CERBERO_BITE_ENABLE_STRATEGY"
_TRUE_TOKENS = frozenset({"1", "true", "yes", "on", "enabled"})
_FALSE_TOKENS = frozenset({"0", "false", "no", "off", "disabled"})
@dataclass(frozen=True)
class RuntimeFlags:
"""Boolean switches that gate optional cycles.
Both fields default to the canonical "analysis only" profile.
"""
data_analysis_enabled: bool = True
strategy_enabled: bool = False
def _parse_bool(raw: str, *, var: str, default: bool) -> bool:
cleaned = raw.strip().lower()
if not cleaned:
return default
if cleaned in _TRUE_TOKENS:
return True
if cleaned in _FALSE_TOKENS:
return False
raise ValueError(
f"{var}: expected one of "
f"{sorted(_TRUE_TOKENS | _FALSE_TOKENS)}, got {raw!r}"
)
def load_runtime_flags(env: dict[str, str] | None = None) -> RuntimeFlags:
"""Build a :class:`RuntimeFlags` from environment variables."""
e = env if env is not None else os.environ
return RuntimeFlags(
data_analysis_enabled=_parse_bool(
e.get(DATA_ANALYSIS_ENV, ""),
var=DATA_ANALYSIS_ENV,
default=True,
),
strategy_enabled=_parse_bool(
e.get(STRATEGY_ENV, ""),
var=STRATEGY_ENV,
default=False,
),
)