88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
"""Agent command transport coordination and result validation."""
|
|
|
|
import re
|
|
|
|
from pydantic import ValidationError
|
|
|
|
from app.agent_commands.exceptions import (
|
|
AgentCommandConflict,
|
|
AgentCommandNotFound,
|
|
AgentResultValidationError,
|
|
)
|
|
from app.agent_commands.models import (
|
|
AgentCommand,
|
|
AgentCommandResultRequest,
|
|
MinerControlResultPayload,
|
|
)
|
|
from app.agent_commands.repository import AgentCommandRepository
|
|
|
|
|
|
class AgentCommandService:
|
|
def __init__(self, repository: AgentCommandRepository):
|
|
self.repository = repository
|
|
|
|
def claim(self, agent_id: str) -> list[AgentCommand]:
|
|
return [
|
|
AgentCommand.model_validate(row)
|
|
for row in self.repository.claim_pending(agent_id)
|
|
]
|
|
|
|
@staticmethod
|
|
def _legacy_error(value: str | None) -> str | None:
|
|
if not value:
|
|
return None
|
|
sanitized = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "[address]", value)
|
|
return sanitized[:255]
|
|
|
|
def submit_result(
|
|
self, agent_id: str, request: AgentCommandResultRequest
|
|
) -> None:
|
|
command = self.repository.get_command(request.command_id)
|
|
if not command:
|
|
raise AgentCommandNotFound("Agent command not found")
|
|
if command["agent_id"] != agent_id:
|
|
raise AgentCommandConflict("Agent command belongs to another Agent")
|
|
if command["command_status"] != "claimed":
|
|
raise AgentCommandConflict(
|
|
f"Agent command is {command['command_status']}, not claimed"
|
|
)
|
|
|
|
result_payload = request.result_payload
|
|
error_message = self._legacy_error(request.error_message)
|
|
if command["command_type"] == "miner_control":
|
|
try:
|
|
result = MinerControlResultPayload.model_validate(request.result_payload)
|
|
except ValidationError as exc:
|
|
raise AgentResultValidationError(
|
|
"Invalid typed miner-control result"
|
|
) from exc
|
|
if request.status == "completed" and not result.command_accepted:
|
|
raise AgentResultValidationError(
|
|
"Completed miner-control results must be accepted"
|
|
)
|
|
if request.status == "failed" and result.command_accepted:
|
|
raise AgentResultValidationError(
|
|
"Failed miner-control results cannot be accepted"
|
|
)
|
|
action = (command.get("request_payload") or {}).get("action")
|
|
if (
|
|
action in {"suspend", "resume"}
|
|
and result.command_accepted
|
|
and result.verification_status != "unconfirmed"
|
|
):
|
|
raise AgentResultValidationError(
|
|
"Accepted suspend and resume results must remain unconfirmed"
|
|
)
|
|
result_payload = result.model_dump(mode="json")
|
|
error_message = (
|
|
result.failure_code.replace("_", " ") if result.failure_code else None
|
|
)
|
|
|
|
self.repository.finish_command(
|
|
request.command_id,
|
|
agent_id,
|
|
request.status,
|
|
result_payload,
|
|
error_message,
|
|
)
|