Bind Agent observations, review, enrichment and telemetry to current Zone
and inventory authority. Preserve immutable MAC ownership and historical
snapshots with stale-result rejection and transactional security audits.
Retire unbound direct/range/scheduled discovery and one/bulk legacy summary
writes in both modes. Require reconciled exact inventory identity or
explicit Zone-scoped IP selection for CSV metadata imports.
Independent Blocker #1 application-security re-audit: PASS on tree
cf863aadc9. Evidence: 141 focused PostgreSQL/HTTP
tests passed; Chromium and PowerShell checks passed. Two documented baseline
source assertions remain unchanged. Production deployment is not approved.
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
"""Typed Agent Zone and check-in contracts."""
|
|
|
|
from ipaddress import ip_network
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
|
|
class AgentZoneCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
site_id: UUID
|
|
name: str = Field(min_length=1, max_length=120)
|
|
description: str | None = Field(default=None, max_length=1000)
|
|
networks: list[str] = Field(default_factory=list, max_length=64)
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def normalize_name(cls, value: str) -> str:
|
|
normalized = value.strip()
|
|
if not normalized:
|
|
raise ValueError("name cannot be blank")
|
|
return normalized
|
|
|
|
@field_validator("description")
|
|
@classmethod
|
|
def normalize_description(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return value.strip() or None
|
|
|
|
@field_validator("networks")
|
|
@classmethod
|
|
def normalize_networks(cls, values: list[str]) -> list[str]:
|
|
normalized: list[str] = []
|
|
for value in values:
|
|
try:
|
|
network = str(ip_network(value.strip(), strict=True))
|
|
except ValueError as exc:
|
|
raise ValueError(f"invalid CIDR network: {value}") from exc
|
|
if network in normalized:
|
|
raise ValueError(f"duplicate CIDR network: {network}")
|
|
normalized.append(network)
|
|
return normalized
|
|
|
|
|
|
class AgentZoneNetwork(BaseModel):
|
|
id: UUID
|
|
network: str
|
|
|
|
|
|
class AgentZoneSummary(BaseModel):
|
|
id: UUID
|
|
site_id: UUID
|
|
name: str
|
|
description: str | None = None
|
|
is_enabled: bool
|
|
assigned_agent_id: str | None = None
|
|
networks: list[AgentZoneNetwork] = Field(default_factory=list)
|
|
|
|
|
|
class AgentZoneListResponse(BaseModel):
|
|
data: list[AgentZoneSummary]
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
class AgentZoneCreateResponse(BaseModel):
|
|
data: AgentZoneSummary
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
class AgentCheckinRequest(BaseModel):
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
agent_id: str | None = Field(default=None, max_length=255)
|
|
site_id: UUID | None = None
|
|
agent_zone_id: UUID | None = None
|
|
site_name: str | None = Field(default=None, max_length=255)
|
|
hostname: str | None = Field(default=None, max_length=255)
|
|
version: str | None = Field(default=None, max_length=120)
|
|
ip_address: str | None = Field(default=None, max_length=255)
|
|
|
|
@model_validator(mode="after")
|
|
def require_site_for_explicit_zone(self) -> "AgentCheckinRequest":
|
|
if self.agent_zone_id is not None and self.site_id is None:
|
|
raise ValueError("site_id is required when agent_zone_id is supplied")
|
|
return self
|
|
|
|
|
|
class AgentCheckinResponse(BaseModel):
|
|
ok: bool = True
|
|
hub_mode: bool = True
|
|
agent_id: str
|
|
site_id: UUID | None = None
|
|
agent_zone_id: UUID | None = None
|
|
routing_available: bool
|
|
routing_reason: str | None = None
|
|
site_name: str
|
|
message: str = "Agent check-in received."
|
|
|
|
|
|
class AgentZoneRoutingContext(BaseModel):
|
|
site_id: UUID | None = None
|
|
agent_zone_id: UUID | None = None
|
|
routing_reason: str | None = None
|
|
|
|
|
|
class AgentDiscoveryMinerMatch(AgentZoneRoutingContext):
|
|
miner_id: UUID | None = None
|
|
matched_by: str | None = None
|
|
|
|
|
|
class MinerZoneAssociationResult(BaseModel):
|
|
agent_zone_id: UUID | None = None
|
|
routing_reason: str | None = None
|
|
outcomes: dict[UUID, str] = Field(default_factory=dict)
|