diff --git a/src/nexus_sync/client/execute.py b/src/nexus_sync/client/execute.py index 3b766a8..9ee995d 100644 --- a/src/nexus_sync/client/execute.py +++ b/src/nexus_sync/client/execute.py @@ -18,22 +18,28 @@ DEFAULT_PRESET_DESCRIPTIONS = { @dataclass(frozen=True) class CommandAccessPolicy: + """Whitelist controlling which command presets a client may run.""" + allowed_commands: frozenset[str] = field(default_factory=frozenset) full_access: bool = False @classmethod def allow(cls, command_names: Iterable[str]) -> "CommandAccessPolicy": + """Allow exactly the named command presets.""" return cls(allowed_commands=frozenset(command_names)) @classmethod def allow_all(cls) -> "CommandAccessPolicy": + """Allow every command preset (use with care).""" return cls(full_access=True) @classmethod def deny_all(cls) -> "CommandAccessPolicy": + """Deny every command preset.""" return cls() def allows(self, command_name: str) -> bool: + """Return whether ``command_name`` is permitted by this policy.""" return self.full_access or command_name in self.allowed_commands @@ -83,6 +89,11 @@ def execute_command( access_policy: CommandAccessPolicy = DEFAULT_COMMAND_ACCESS_POLICY, output_limit_bytes: int = DEFAULT_OUTPUT_LIMIT_BYTES, ) -> CommandResult: + """Run a command preset safely and return its :class:`CommandResult`. + + Only known, allowed presets are executed (never arbitrary shell); output is + size-limited and each run is bound by ``command.timeout_seconds``. + """ if command.kind != CommandKind.EXEC: return _reject(command, f"unsupported command kind: {command.kind}") diff --git a/src/nexus_sync/client/runtime.py b/src/nexus_sync/client/runtime.py index 281156a..c5fcf3a 100644 --- a/src/nexus_sync/client/runtime.py +++ b/src/nexus_sync/client/runtime.py @@ -81,6 +81,7 @@ def find_client_config_path( cwd: Path | None = None, home: Path | None = None, ) -> Path | None: + """Find the client config (nexus.yml/yaml) in cwd, then XDG, then ~/.config.""" cwd = cwd or Path.cwd() home = home or Path.home() candidates = [ @@ -110,6 +111,7 @@ def load_client_config( *, config_path: Path | str | None = None, ) -> ClientConfig: + """Load and validate the client config, returning a :class:`ClientConfig`.""" path = Path(config_path) if config_path is not None else find_client_config_path(env=env) if path is None: raise ClientConfigError("client config file not found") @@ -145,6 +147,7 @@ def build_heartbeat_request( *, last_command_result: CommandResult | None = None, ) -> HeartbeatRequest: + """Build a heartbeat payload from config and an optional last command result.""" now = datetime.now(UTC) return HeartbeatRequest( client_id=config.client_id, @@ -173,6 +176,7 @@ def list_available_commands( presets: Mapping[str, PresetBuilder] = DEFAULT_PRESETS, descriptions: Mapping[str, str] = DEFAULT_PRESET_DESCRIPTIONS, ) -> list[ClientCommandCapability]: + """Return the capabilities the access policy allows, as advertised to the server.""" return [ ClientCommandCapability( name=name, @@ -189,6 +193,7 @@ def send_heartbeat( *, opener: HeartbeatOpener = urllib.request.urlopen, ) -> HeartbeatResponse: + """POST a heartbeat and return the parsed response; raises :class:`HeartbeatError`.""" payload = heartbeat.model_dump_json().encode() request = urllib.request.Request( f"{config.server_url}{HEARTBEAT_PATH}", @@ -227,6 +232,7 @@ def run_once( ] = send_heartbeat, executor: Callable[..., CommandResult] = execute_command, ) -> CommandResult | None: + """Send one heartbeat and execute the command the server returns, if any.""" response = heartbeat_sender( config, build_heartbeat_request(config, last_command_result=last_command_result), @@ -242,6 +248,7 @@ def run_once( def main(argv: list[str] | None = None) -> int: + """Run the client once: load config, heartbeat, and drain any queued commands.""" _ = argv try: config = load_client_config() diff --git a/src/nexus_sync/common/models.py b/src/nexus_sync/common/models.py index 410cd1d..4fb1b52 100644 --- a/src/nexus_sync/common/models.py +++ b/src/nexus_sync/common/models.py @@ -6,10 +6,14 @@ from pydantic import BaseModel, ConfigDict, Field class StrictBaseModel(BaseModel): + """Base model that rejects unknown fields (``extra='forbid'``).""" + model_config = ConfigDict(extra="forbid") class ClientPlatform(StrEnum): + """Operating-system family reported by a client.""" + LINUX = "linux" DARWIN = "darwin" WINDOWS = "windows" @@ -17,10 +21,14 @@ class ClientPlatform(StrEnum): class CommandKind(StrEnum): + """Type of executor a command targets (only ``exec`` is defined so far).""" + EXEC = "exec" class CommandResultStatus(StrEnum): + """Terminal outcome a client reports for an executed command.""" + SUCCEEDED = "succeeded" FAILED = "failed" TIMED_OUT = "timed_out" @@ -28,6 +36,8 @@ class CommandResultStatus(StrEnum): class CommandStatus(StrEnum): + """Lifecycle state of a command on the server (pending → delivered → terminal).""" + PENDING = "pending" DELIVERED = "delivered" SUCCEEDED = "succeeded" @@ -37,22 +47,30 @@ class CommandStatus(StrEnum): class ClientInfo(StrictBaseModel): + """Identifying details a client reports about itself in a heartbeat.""" + hostname: str platform: ClientPlatform version: str class ClientState(StrictBaseModel): + """Lightweight runtime state a client reports (local time, uptime).""" + local_time: datetime uptime_seconds: int | None = Field(default=None, ge=0) class ClientCommandCapability(StrictBaseModel): + """A command preset a client advertises as runnable (name and description).""" + name: str description: str class CommandResult(StrictBaseModel): + """Outcome of one executed command, sent by the client to the server.""" + command_id: str status: CommandResultStatus started_at: datetime | None = None @@ -63,6 +81,8 @@ class CommandResult(StrictBaseModel): class HeartbeatRequest(StrictBaseModel): + """Payload a client POSTs each poll: identity, state, capabilities, last result.""" + client_id: str observed_at: datetime client: ClientInfo @@ -72,6 +92,8 @@ class HeartbeatRequest(StrictBaseModel): class Command(StrictBaseModel): + """A command the server hands to a client for execution.""" + id: str kind: CommandKind name: str @@ -80,6 +102,8 @@ class Command(StrictBaseModel): class HeartbeatResponse(StrictBaseModel): + """Server reply to a heartbeat: poll interval and an optional command to run.""" + status: Literal["ok"] = "ok" server_time: datetime next_poll_after_seconds: int = Field(ge=0) @@ -87,6 +111,8 @@ class HeartbeatResponse(StrictBaseModel): class ClientRecord(StrictBaseModel): + """Server-side stored view of a client and its last heartbeat.""" + id: str hostname: str platform: ClientPlatform @@ -99,6 +125,8 @@ class ClientRecord(StrictBaseModel): class CommandRecord(StrictBaseModel): + """Server-side stored command with delivery and lifecycle bookkeeping.""" + id: str client_id: str kind: CommandKind @@ -114,6 +142,8 @@ class CommandRecord(StrictBaseModel): class CommandResultRecord(StrictBaseModel): + """Server-side stored result reported by a client for a command.""" + command_id: str client_id: str status: CommandResultStatus @@ -126,6 +156,8 @@ class CommandResultRecord(StrictBaseModel): class AuditLogRecord(StrictBaseModel): + """Audit-trail entry recording who did what to which subject.""" + id: str actor: str action: str diff --git a/src/nexus_sync/server/store.py b/src/nexus_sync/server/store.py index de0c8e8..9215ba3 100644 --- a/src/nexus_sync/server/store.py +++ b/src/nexus_sync/server/store.py @@ -15,29 +15,31 @@ from nexus_sync.common import ( class Store(Protocol): + """Storage interface for clients, commands, and command results.""" + def list_clients(self) -> list[ClientRecord]: - pass + """Return all known clients.""" def get_client(self, client_id: str) -> ClientRecord | None: - pass + """Return the client with ``client_id``, or ``None`` if unknown.""" def upsert_client(self, heartbeat: HeartbeatRequest, now: datetime) -> ClientRecord: - pass + """Create or update a client from a heartbeat and return the stored record.""" def enqueue_command(self, command: Command, client_id: str, now: datetime) -> CommandRecord: - pass + """Queue a pending command for a client and return the stored record.""" def record_command_result(self, heartbeat: HeartbeatRequest, now: datetime) -> None: - pass + """Persist the command result carried by a heartbeat, if one is present.""" def take_next_command(self, client_id: str, now: datetime) -> Command | None: - pass + """Pop the oldest pending command for a client, marking it delivered.""" def get_command(self, command_id: str) -> CommandRecord | None: - pass + """Return the command with ``command_id``, or ``None`` if unknown.""" def get_command_result(self, command_id: str) -> CommandResultRecord | None: - pass + """Return the latest result for ``command_id``, or ``None`` if none reported.""" @dataclass