"""Build short-lived Windows Agent enrollment packages in memory.""" from __future__ import annotations import io import json import os import zipfile from pathlib import Path from urllib.parse import urlsplit WINDOWS_PACKAGE_ROOT = "MinerOpsAgent-Windows" WINDOWS_PACKAGE_FILES = ( "Enroll-MinerOpsAgent.ps1", "install.ps1", "uninstall.ps1", "MinerOpsAgentService.ps1", "MinerOpsAgentTray.ps1", "MinerOpsAgentGUI.ps1", "MinerOpsApi3EnrichmentPoller.ps1", "MinerOpsPoolPoller.ps1", "MinerOpsPoolPollerTask.ps1", "EnrichOneMiner.ps1", "MinerOpsControl.ps1", "VERSION", "README.md", ) DEFAULT_WINDOWS_AGENT_SOURCE = Path( os.environ.get( "MINEROPS_WINDOWS_AGENT_SOURCE", "/app/agents/windows", ) ) class AgentWindowsPackageError(RuntimeError): """Base Windows package construction error.""" class AgentWindowsPackageSourceError(AgentWindowsPackageError): """Canonical Windows Agent source is unavailable or incomplete.""" def normalize_hub_url(value: str) -> str: candidate = value.strip() parsed = urlsplit(candidate) if parsed.scheme not in {"http", "https"}: raise AgentWindowsPackageError( "MinerOps Hub URL must use http or https" ) if not parsed.netloc: raise AgentWindowsPackageError( "MinerOps Hub URL must include a host" ) if parsed.username or parsed.password: raise AgentWindowsPackageError( "MinerOps Hub URL must not contain credentials" ) if parsed.query or parsed.fragment: raise AgentWindowsPackageError( "MinerOps Hub URL must not contain query or fragment data" ) if parsed.path not in {"", "/"}: raise AgentWindowsPackageError( "MinerOps Hub URL must be an origin without a path" ) return f"{parsed.scheme}://{parsed.netloc}" def build_windows_agent_package( *, enrollment_token: str, hub_url: str, source_root: Path | None = None, ) -> bytes: token = enrollment_token.strip() if not 32 <= len(token) <= 256: raise AgentWindowsPackageError( "Enrollment token has an invalid length" ) normalized_hub_url = normalize_hub_url(hub_url) root = ( Path(source_root) if source_root is not None else DEFAULT_WINDOWS_AGENT_SOURCE ) if not root.is_dir(): raise AgentWindowsPackageSourceError( "Canonical Windows Agent source is unavailable" ) missing = [ name for name in WINDOWS_PACKAGE_FILES if not (root / name).is_file() ] if missing: raise AgentWindowsPackageSourceError( "Canonical Windows Agent source is incomplete: " + ", ".join(missing) ) enrollment = { "hub_url": normalized_hub_url, "token": token, } instructions = """MinerOps Windows Agent 1. Extract this ZIP on the target Windows machine. 2. Open Windows PowerShell as Administrator. 3. Change to the extracted MinerOpsAgent-Windows folder. 4. Run: .\\Enroll-MinerOpsAgent.ps1 The enrollment credential is short-lived and single-use. Do not post enrollment.json in chat, tickets, screenshots, or logs. Delete the ZIP and extracted setup folder after successful installation. """ buffer = io.BytesIO() with zipfile.ZipFile( buffer, mode="w", compression=zipfile.ZIP_DEFLATED, ) as archive: for name in WINDOWS_PACKAGE_FILES: archive.write( root / name, arcname=f"{WINDOWS_PACKAGE_ROOT}/{name}", ) archive.writestr( f"{WINDOWS_PACKAGE_ROOT}/enrollment.json", json.dumps( enrollment, indent=2, sort_keys=True, ) + "\n", ) archive.writestr( f"{WINDOWS_PACKAGE_ROOT}/INSTALL.txt", instructions, ) return buffer.getvalue()