feat: add safe miner control foundation

This commit is contained in:
lussierj committed 2026-08-24 20:49:28 +00:00
1 parent 6cd876b4b3
commit f5965af914
39 files changed
+2952 -491

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""Typed miner control domain."""
@@ -0,0 +1,8 @@
"""Dependency construction for miner control."""
from app.miner_control.repository import MinerControlRepository
from app.miner_control.service import MinerControlService
def get_miner_control_service() -> MinerControlService:
return MinerControlService(MinerControlRepository())
+21
View File
@@ -0,0 +1,21 @@
"""Miner control domain exceptions."""
class MinerControlError(Exception):
"""Base miner control error."""
class ControlledMinerNotFound(MinerControlError):
"""The durable miner UUID is unknown."""
class MinerControlUnavailable(MinerControlError):
"""The requested action cannot be safely queued."""
class MinerControlConflict(MinerControlError):
"""Another live control operation already exists."""
class MinerControlActorRequired(MinerControlError):
"""An authenticated actor UUID is required."""
+85
View File
@@ -0,0 +1,85 @@
"""Typed contracts for miner control capabilities and operations."""
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
ControlAction = Literal["suspend", "resume", "performance_mode"]
CapabilityState = Literal["supported", "unavailable", "unknown"]
QueueStatus = Literal["pending", "claimed", "completed", "failed", "cancelled"]
VerificationStatus = Literal[
"confirmed", "verification_failed", "unconfirmed", "unavailable"
]
PerformanceMode = Literal["low", "normal", "high"]
class EmptyControlRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
class PerformanceModeRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
mode: PerformanceMode
class ControlCapability(BaseModel):
state: CapabilityState
reason: str | None = None
class ControlAgentAvailability(BaseModel):
state: CapabilityState
reason: str | None = None
last_checkin_at: AwareDatetime | None = None
version: str | None = None
class MinerControlOperation(BaseModel):
operation_id: str
miner_id: UUID
action: ControlAction
requested_value: str | None = None
status: QueueStatus
verification_status: VerificationStatus | None = None
command_accepted: bool | None = None
observed_performance_mode: PerformanceMode | None = None
failure_code: str | None = None
created_at: AwareDatetime
claimed_at: AwareDatetime | None = None
completed_at: AwareDatetime | None = None
class MinerControlProjection(BaseModel):
miner_id: UUID
runtime_state: str | None = None
performance_mode: PerformanceMode | None = None
agent: ControlAgentAvailability
service_control: ControlCapability
performance_mode_control: ControlCapability
available_service_action: Literal["suspend", "resume"] | None = None
recent_operations: list[MinerControlOperation]
class MinerControlResponse(BaseModel):
data: MinerControlProjection
meta: dict[str, object] = Field(default_factory=dict)
error: None = None
class MinerControlAccepted(BaseModel):
operation_id: str
miner_id: UUID
action: ControlAction
requested_value: str | None = None
status: Literal["pending"]
created_at: datetime
class MinerControlAcceptedResponse(BaseModel):
data: MinerControlAccepted
meta: dict[str, object] = Field(default_factory=dict)
error: None = None
+135
View File
@@ -0,0 +1,135 @@
"""PostgreSQL persistence for typed miner control operations."""
from collections.abc import Callable
from typing import Any
from uuid import UUID
import psycopg
from psycopg.errors import UniqueViolation
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from app.config import settings
from app.miner_control.exceptions import MinerControlConflict
ConnectionFactory = Callable[[], psycopg.Connection]
LIVE_CONTROL_CONSTRAINT = "uq_agent_queue_live_miner_control"
def default_connection_factory() -> psycopg.Connection:
return psycopg.connect(
host=settings.postgres_host,
port=settings.postgres_port,
dbname=settings.postgres_db,
user=settings.postgres_user,
password=settings.postgres_password,
connect_timeout=5,
)
class MinerControlRepository:
"""Resolve control facts and persist queue/audit rows atomically."""
def __init__(self, connection_factory: ConnectionFactory | None = None):
self._connection_factory = connection_factory or default_connection_factory
def get_control_context(self, miner_id: UUID) -> dict | None:
connection = self._connection_factory()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT miners.id, miners.site_id, host(miners.current_ip) AS current_ip,
miners.status AS runtime_state,
miners.configured_performance_mode,
miners.api3_supported, miners.api3_api_switch,
agents.agent_id, agents.last_checkin_at,
agents.version AS agent_version, agents.last_payload
FROM miners
LEFT JOIN minerops_site_agents AS agents
ON agents.site_id = miners.site_id
WHERE miners.id = %s
""",
(miner_id,),
)
row = cur.fetchone()
return dict(row) if row else None
finally:
connection.close()
def list_recent(self, miner_id: UUID, limit: int = 5) -> list[dict]:
connection = self._connection_factory()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT id AS operation_id, command_status AS status,
request_payload, result_payload, created_at,
claimed_at, completed_at
FROM minerops_agent_command_queue
WHERE command_type = 'miner_control'
AND request_payload ->> 'miner_id' = %s
AND request_payload ->> 'schema_version' = '1'
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
(str(miner_id), limit),
)
return [dict(row) for row in cur.fetchall()]
finally:
connection.close()
def enqueue_with_audit(
self,
*,
operation_id: str,
miner_id: UUID,
agent_id: str,
actor_user_id: UUID,
actor_username: str,
request_payload: dict[str, Any],
audit_payload: dict[str, Any],
) -> dict:
connection = self._connection_factory()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
INSERT INTO minerops_agent_command_queue (
id, agent_id, command_type, command_status,
requested_by_user_id, requested_by_username, request_payload
)
VALUES (%s, %s, 'miner_control', 'pending', %s, %s, %s)
RETURNING id AS operation_id, command_status AS status, created_at
""",
(
operation_id, agent_id, str(actor_user_id), actor_username,
Jsonb(request_payload),
),
)
operation = dict(cur.fetchone())
cur.execute(
"""
INSERT INTO audit_logs (
actor_user_id, action, entity_type, entity_id,
new_value_json, related_miner_asset_id
)
VALUES (%s, 'miner.control.queued', 'miner', %s, %s, %s)
""",
(actor_user_id, miner_id, Jsonb(audit_payload), miner_id),
)
connection.commit()
return operation
except UniqueViolation as exc:
connection.rollback()
if exc.diag.constraint_name == LIVE_CONTROL_CONSTRAINT:
raise MinerControlConflict(
"A pending or running control operation already exists for this miner"
) from exc
raise
except Exception:
connection.rollback()
raise
finally:
connection.close()
+129
View File
@@ -0,0 +1,129 @@
"""HTTP mapping for typed miner controls."""
from typing import Annotated
from urllib.parse import urlsplit
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request, status
from app.identity import require_permission
from app.miner_control.dependencies import get_miner_control_service
from app.miner_control.exceptions import (
ControlledMinerNotFound,
MinerControlConflict,
MinerControlUnavailable,
)
from app.miner_control.models import (
EmptyControlRequest,
MinerControlAcceptedResponse,
MinerControlResponse,
PerformanceModeRequest,
)
from app.miner_control.service import MinerControlService
router = APIRouter(prefix="/api/miners", tags=["miner-controls"])
ControlService = Annotated[MinerControlService, Depends(get_miner_control_service)]
ControlViewer = Annotated[dict, Depends(require_permission("miners.view"))]
ControlUser = Annotated[dict, Depends(require_permission("miners.control"))]
def require_same_origin(request: Request) -> None:
if request.headers.get("sec-fetch-site", "").lower() == "cross-site":
raise HTTPException(status_code=403, detail="Cross-site control request rejected")
origin = request.headers.get("origin")
if origin:
parsed = urlsplit(origin)
if parsed.scheme not in {"http", "https"} or parsed.netloc != request.url.netloc:
raise HTTPException(status_code=403, detail="Cross-site control request rejected")
def _actor(current_user: dict) -> tuple[UUID, str]:
return UUID(str(current_user["id"])), str(
current_user.get("username") or current_user.get("email") or "unknown"
)
def _queue(
service: MinerControlService,
miner_id: UUID,
action: str,
requested_value: str | None,
current_user: dict,
) -> MinerControlAcceptedResponse:
actor_id, username = _actor(current_user)
try:
operation = service.queue_control(
miner_id,
action,
requested_value=requested_value,
actor_user_id=actor_id,
actor_username=username,
)
except ControlledMinerNotFound as exc:
raise HTTPException(status_code=404, detail="Miner not found") from exc
except MinerControlUnavailable as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except MinerControlConflict as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return MinerControlAcceptedResponse(data=operation)
@router.get("/{miner_id}/controls", response_model=MinerControlResponse)
def get_miner_controls(
miner_id: UUID,
current_user: ControlViewer,
service: ControlService,
) -> MinerControlResponse:
del current_user
try:
return MinerControlResponse(data=service.get_controls(miner_id))
except ControlledMinerNotFound as exc:
raise HTTPException(status_code=404, detail="Miner not found") from exc
@router.post(
"/{miner_id}/controls/suspend",
response_model=MinerControlAcceptedResponse,
status_code=status.HTTP_202_ACCEPTED,
dependencies=[Depends(require_same_origin)],
)
def suspend_miner(
miner_id: UUID,
payload: EmptyControlRequest,
current_user: ControlUser,
service: ControlService,
) -> MinerControlAcceptedResponse:
del payload
return _queue(service, miner_id, "suspend", None, current_user)
@router.post(
"/{miner_id}/controls/resume",
response_model=MinerControlAcceptedResponse,
status_code=status.HTTP_202_ACCEPTED,
dependencies=[Depends(require_same_origin)],
)
def resume_miner(
miner_id: UUID,
payload: EmptyControlRequest,
current_user: ControlUser,
service: ControlService,
) -> MinerControlAcceptedResponse:
del payload
return _queue(service, miner_id, "resume", None, current_user)
@router.post(
"/{miner_id}/controls/performance-mode",
response_model=MinerControlAcceptedResponse,
status_code=status.HTTP_202_ACCEPTED,
dependencies=[Depends(require_same_origin)],
)
def set_performance_mode(
miner_id: UUID,
payload: PerformanceModeRequest,
current_user: ControlUser,
service: ControlService,
) -> MinerControlAcceptedResponse:
return _queue(service, miner_id, "performance_mode", payload.mode, current_user)
+165
View File
@@ -0,0 +1,165 @@
"""Business rules for typed, Agent-routed miner controls."""
from typing import Any
from uuid import UUID, uuid4
from app.miner_control.exceptions import (
ControlledMinerNotFound,
MinerControlActorRequired,
MinerControlUnavailable,
)
from app.miner_control.models import (
ControlAgentAvailability,
ControlCapability,
MinerControlAccepted,
MinerControlOperation,
MinerControlProjection,
)
from app.miner_control.repository import MinerControlRepository
KNOWN_MODES = {"low", "normal", "high"}
RUNNING_STATES = {"online", "running"}
STOPPED_STATES = {"suspended", "stopped"}
class MinerControlService:
def __init__(self, repository: MinerControlRepository):
self.repository = repository
@staticmethod
def _capability(context: dict) -> ControlCapability:
if not context.get("site_id"):
return ControlCapability(state="unavailable", reason="site_unassigned")
if not context.get("agent_id"):
return ControlCapability(state="unavailable", reason="agent_unassigned")
if not context.get("current_ip"):
return ControlCapability(state="unavailable", reason="current_ip_missing")
if context.get("api3_supported") is False:
return ControlCapability(state="unavailable", reason="api3_unsupported")
if context.get("api3_supported") is not True:
return ControlCapability(state="unknown", reason="api3_support_unknown")
payload = context.get("last_payload") or {}
if payload.get("control_available") is False:
reason = payload.get("control_unavailable_reason")
return ControlCapability(
state="unavailable",
reason=reason if reason == "credentials_missing" else "agent_unavailable",
)
if payload.get("control_available") is not True:
return ControlCapability(
state="unknown", reason="agent_capability_unknown"
)
return ControlCapability(state="supported")
@staticmethod
def _operation(miner_id: UUID, row: dict) -> MinerControlOperation:
request = row.get("request_payload") or {}
result = row.get("result_payload") or {}
mode = result.get("observed_performance_mode")
return MinerControlOperation(
operation_id=row["operation_id"],
miner_id=miner_id,
action=request.get("action"),
requested_value=request.get("requested_value"),
status=row["status"],
verification_status=result.get("verification_status"),
command_accepted=result.get("command_accepted"),
observed_performance_mode=mode if mode in KNOWN_MODES else None,
failure_code=result.get("failure_code"),
created_at=row["created_at"],
claimed_at=row.get("claimed_at"),
completed_at=row.get("completed_at"),
)
def get_controls(self, miner_id: UUID) -> MinerControlProjection:
context = self.repository.get_control_context(miner_id)
if not context:
raise ControlledMinerNotFound("Miner not found")
capability = self._capability(context)
runtime = str(context.get("runtime_state") or "").strip().lower() or None
service_action = None
if capability.state == "supported":
if runtime in RUNNING_STATES:
service_action = "suspend"
elif runtime in STOPPED_STATES:
service_action = "resume"
configured_mode = str(context.get("configured_performance_mode") or "").lower()
return MinerControlProjection(
miner_id=miner_id,
runtime_state=runtime,
performance_mode=(configured_mode if configured_mode in KNOWN_MODES else None),
agent=ControlAgentAvailability(
state=capability.state,
reason=capability.reason,
last_checkin_at=context.get("last_checkin_at"),
version=context.get("agent_version"),
),
service_control=capability,
performance_mode_control=capability,
available_service_action=service_action,
recent_operations=[
self._operation(miner_id, row)
for row in self.repository.list_recent(miner_id, limit=5)
],
)
def queue_control(
self,
miner_id: UUID,
action: str,
*,
requested_value: str | None,
actor_user_id: UUID,
actor_username: str,
) -> MinerControlAccepted:
if not actor_user_id:
raise MinerControlActorRequired("Authenticated actor UUID is required")
context = self.repository.get_control_context(miner_id)
if not context:
raise ControlledMinerNotFound("Miner not found")
capability = self._capability(context)
if capability.state != "supported":
raise MinerControlUnavailable(
capability.reason or "control_capability_not_supported"
)
runtime = str(context.get("runtime_state") or "").strip().lower()
if action == "suspend" and runtime not in RUNNING_STATES:
raise MinerControlUnavailable("miner_not_factually_running")
if action == "resume" and runtime not in STOPPED_STATES:
raise MinerControlUnavailable("miner_not_factually_stopped")
if action == "performance_mode" and requested_value not in KNOWN_MODES:
raise MinerControlUnavailable("invalid_performance_mode")
operation_id = str(uuid4())
payload: dict[str, Any] = {
"schema_version": 1,
"miner_id": str(miner_id),
"action": action,
"requested_value": requested_value,
"ip_address": context["current_ip"],
}
operation = self.repository.enqueue_with_audit(
operation_id=operation_id,
miner_id=miner_id,
agent_id=context["agent_id"],
actor_user_id=actor_user_id,
actor_username=actor_username,
request_payload=payload,
audit_payload={
"operation_id": operation_id,
"miner_id": str(miner_id),
"action": action,
"requested_value": requested_value,
},
)
return MinerControlAccepted(
operation_id=operation["operation_id"],
miner_id=miner_id,
action=action,
requested_value=requested_value,
status=operation["status"],
created_at=operation["created_at"],
)