feat(V2): IBKR key rotation admin endpoints + health probe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-03 21:37:29 +00:00
parent bea37fd734
commit 55bfeca88e
+85
View File
@@ -8,6 +8,9 @@ from pathlib import Path
from typing import Any, Literal
from fastapi import APIRouter, HTTPException, Query, Request
from pydantic import BaseModel, SecretStr
from cerbero_mcp.exchanges.ibkr.key_rotation import KeyRotationManager
MAX_RECORDS = 10000
DEFAULT_LIMIT = 1000
@@ -155,4 +158,86 @@ def make_admin_router() -> APIRouter:
},
}
class _IBKRRotateConfirmReq(BaseModel):
new_consumer_key: str
new_access_token: str
new_access_token_secret: str
@r.post("/ibkr/rotate-keys/start")
async def _ibkr_rotate_start(env: str, request: Request):
if env not in ("testnet", "mainnet"):
raise HTTPException(400, detail={"error": "invalid env"})
settings = request.app.state.settings
creds = settings.ibkr.credentials(env)
mgr = KeyRotationManager(
signature_key_path=creds["signature_key_path"],
encryption_key_path=creds["encryption_key_path"],
)
rotations = getattr(request.app.state, "ibkr_rotations", None)
if rotations is None:
rotations = {}
request.app.state.ibkr_rotations = rotations
rotations[env] = mgr
return await mgr.start()
@r.post("/ibkr/rotate-keys/confirm")
async def _ibkr_rotate_confirm(
env: str, body: _IBKRRotateConfirmReq, request: Request,
):
if env not in ("testnet", "mainnet"):
raise HTTPException(400, detail={"error": "invalid env"})
rotations = getattr(request.app.state, "ibkr_rotations", {}) or {}
mgr = rotations.get(env)
if mgr is None:
raise HTTPException(409, detail={"error": "rotation not started"})
settings = request.app.state.settings
if env == "testnet":
settings.ibkr.consumer_key_testnet = body.new_consumer_key
settings.ibkr.access_token_testnet = body.new_access_token
settings.ibkr.access_token_secret_testnet = SecretStr(body.new_access_token_secret)
else:
settings.ibkr.consumer_key_live = body.new_consumer_key
settings.ibkr.access_token_live = body.new_access_token
settings.ibkr.access_token_secret_live = SecretStr(body.new_access_token_secret)
registry = request.app.state.registry
registry._clients.pop(("ibkr", env), None)
async def _validate() -> bool:
try:
client = await registry.get("ibkr", env)
await client._request("GET", "/iserver/auth/status", skip_tickle=True)
return True
except Exception:
return False
try:
return await mgr.confirm(validate=_validate)
finally:
rotations.pop(env, None)
@r.post("/ibkr/rotate-keys/abort")
async def _ibkr_rotate_abort(env: str, request: Request):
rotations = getattr(request.app.state, "ibkr_rotations", {}) or {}
mgr = rotations.pop(env, None)
if mgr is None:
return {"aborted": False, "reason": "no rotation in progress"}
return await mgr.abort()
@r.post("/ibkr/health")
async def _ibkr_health(request: Request):
registry = request.app.state.registry
out: dict[str, Any] = {}
for env in ("testnet", "mainnet"):
try:
client = await registry.get("ibkr", env)
status = await client._request(
"GET", "/iserver/auth/status", skip_tickle=True
)
out[env] = {"healthy": True, "status": status}
except Exception as e:
out[env] = {"healthy": False, "error": str(e)[:200]}
return out
return r