"""Shared helpers for the Windows installer trio (Setup.exe / service host / Uninstall.exe). Kept dependency-light (stdlib + httpx only) so each frozen binary stays small and self-contained; the DPAPI credential write reuses the agent's `gti_fiscal_agent.credential_store` module. Policy enforced here (mirrors CLAUDE.md): - `agent.env` NEVER holds AGENT_TOKEN or any secret — only non-secret config. - `credentials.bin` is DPAPI LocalMachine-encrypted with a SYSTEM/Administrators-only ACL (fail closed if DPAPI/icacls is unavailable). - FIREBIRD_WRITES=0 and FISCAL_MODE=DRY_RUN are always written; no UI or flag ever enables writes or AI write. - The install code is sent once to the Hub and is never persisted locally. """ from __future__ import annotations import os import subprocess import sys import winreg from pathlib import Path import httpx PROD_HUB_URL = "https://gtifiscal.gtigrupo.com.br" SERVICE_NAME = "GTIFiscalAgent" SERVICE_DISPLAY = "GTI Fiscal Agent" SERVICE_FAILURE = "restart/60000/restart/60000/restart/60000" UNINSTALL_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\GTI Fiscal Agent" def install_dir() -> Path: override = os.environ.get("GTI_INSTALL_DIR") if override: return Path(override).resolve() return Path(os.environ.get("PROGRAMFILES", r"C:\Program Files")) / "GTI" / "FiscalAgent" def hub_url() -> str: return (os.environ.get("GTI_HUB_URL") or PROD_HUB_URL).rstrip("/") def machine_fingerprint(max_len: int = 128) -> str: try: with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key: value, _ = winreg.QueryValueEx(key, "MachineGuid") return str(value)[:max_len] except OSError: return os.environ.get("COMPUTERNAME", "")[:max_len] def enroll(install_code: str, *, hub: str | None = None) -> dict: """POST /v1/agents/enroll with the standalone install code. Returns the parsed JSON on success; raises httpx.HTTPStatusError with the Hub's reason in the response body otherwise.""" url = (hub or hub_url()).rstrip("/") + "/v1/agents/enroll" payload = { "install_code": install_code, "hostname": os.environ.get("COMPUTERNAME", ""), "agent_version": os.environ.get("AGENT_VERSION", ""), "machine_fingerprint": machine_fingerprint(), } response = httpx.post(url, json=payload, timeout=30) response.raise_for_status() return response.json() def write_agent_env(install_dir_path: Path, enroll_result: dict) -> None: """Write the non-secret agent.env. The permanent credential goes to credentials.bin (DPAPI), never here. FIREBIRD_WRITES=0 / DRY_RUN always.""" from gti_fiscal_agent.runner import REFERENCE_SCHEMA_V1 install = Path(install_dir_path) lines = [ (f"GTI_HUB_URL={enroll_result.get('hub_url') or hub_url()}"), (f"AGENT_ID={enroll_result.get('agent_id') or ''}"), (f"INSTALLATION_ID={enroll_result.get('installation_id') or ''}"), ("COMPANY_ID="), ("COMPANY_UF="), ("FIREBIRD_DATABASE_PATH="), ("FB_USER="), ("FB_PASSWORD="), ("FISCAL_MODE=DRY_RUN"), ("FIREBIRD_WRITES=0"), (f"AGENT_VERSION={os.environ.get('AGENT_VERSION', '')}"), ("POLL_INTERVAL_SECONDS=15"), (f"AGENT_LOG_PATH={install / 'logs' / 'agent.log'}"), (f"AGENT_STATE_DB={install / 'agent_state.sqlite'}"), (f"EXPECTED_SCHEMA_FINGERPRINT={REFERENCE_SCHEMA_V1}"), (f"GTI_WRITE_EXECUTOR_PATH={install / 'scripts' / 'firebird_write_executor.py'}"), (f"AGENT_HOSTNAME={os.environ.get('COMPUTERNAME', '')}"), ("AUTHORIZED_WRITE_DATABASE_PATH="), ] install.mkdir(parents=True, exist_ok=True) (install / "agent.env").write_text("\n".join(lines) + "\n", encoding="utf-8") def write_credential(install_dir_path: Path, agent_token: str) -> None: """Write credentials.bin via DPAPI LocalMachine + ACL (fail closed).""" from gti_fiscal_agent.credential_store import write_credential as _write install = Path(install_dir_path) install.mkdir(parents=True, exist_ok=True) _write(install / "credentials.bin", agent_token) def _sc(args: list[str]) -> subprocess.CompletedProcess[str]: return subprocess.run(["sc.exe", *args], capture_output=True, text=True) def install_service(install_dir_path: Path) -> None: """Register the service (Delayed Auto) and set its failure-recovery policy. The frozen onedir host lives in /GTIFiscalAgentService/ ; SCM's binPath points at that executable (its `_internal/` dir travels with it).""" exe = Path(install_dir_path) / "GTIFiscalAgentService" / "GTIFiscalAgentService.exe" bin_path = f"\"{exe}\"" result = _sc(["create", SERVICE_NAME, "binPath=", bin_path, "start=", "auto", "DisplayName=", SERVICE_DISPLAY]) if result.returncode != 0 and "already exists" not in (result.stdout + result.stderr): raise RuntimeError(f"sc create failed: {result.stdout} {result.stderr}".strip()) _sc(["config", SERVICE_NAME, "start=", "delayed-auto"]) _sc(["config", SERVICE_NAME, "obj=", "LocalSystem"]) failure = _sc(["failure", SERVICE_NAME, "reset=", "86400", "actions=", SERVICE_FAILURE]) if failure.returncode != 0: raise RuntimeError(f"sc failure failed: {failure.stdout} {failure.stderr}".strip()) config = _sc(["qc", SERVICE_NAME]) if "DELAYED" not in (config.stdout + config.stderr).upper() and config.returncode == 0: raise RuntimeError("sc config: delayed-auto did not stick") def service_installed() -> bool: result = _sc(["query", SERVICE_NAME]) return result.returncode == 0 and ("SERVICE_NAME" in result.stdout or "RUNNING" in result.stdout or "STOPPED" in result.stdout) def service_running() -> bool: result = _sc(["query", SERVICE_NAME]) return "RUNNING" in result.stdout def start_service() -> None: _sc(["start", SERVICE_NAME]) def stop_service() -> None: _sc(["stop", SERVICE_NAME]) def remove_service() -> None: """Stop then delete the service; both best effort (idempotent).""" stop_service() _sc(["delete", SERVICE_NAME]) def write_arp_uninstall(install_dir_path: Path) -> None: """Write the Add/Remove Programs entry pointing to Uninstall.exe.""" install = Path(install_dir_path) with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, UNINSTALL_KEY, 0, winreg.KEY_WRITE) as key: winreg.SetValueEx(key, "DisplayName", 0, winreg.REG_SZ, SERVICE_DISPLAY) winreg.SetValueEx(key, "DisplayVersion", 0, winreg.REG_SZ, os.environ.get("AGENT_VERSION", "")) winreg.SetValueEx(key, "Publisher", 0, winreg.REG_SZ, "GTI Sistemas") winreg.SetValueEx(key, "InstallLocation", 0, winreg.REG_SZ, str(install)) winreg.SetValueEx(key, "UninstallString", 0, winreg.REG_SZ, str(install / "Uninstall.exe")) winreg.SetValueEx(key, "DisplayIcon", 0, winreg.REG_SZ, str(install / "GTIFiscalAgentService" / "GTIFiscalAgentService.exe")) winreg.SetValueEx(key, "NoModify", 0, winreg.REG_DWORD, 1) winreg.SetValueEx(key, "NoRepair", 0, winreg.REG_DWORD, 1) def remove_arp_entry() -> None: try: winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, UNINSTALL_KEY) except OSError: pass def audit_event(agent_token: str, hub: str, event_type: str, message: str) -> None: """Best-effort authenticated event POST (WINDOWS_INSTALL_COMPLETED etc.). Never sends secrets; ignores failures so a hub hiccup cannot fail an install.""" try: response = httpx.post( (hub or hub_url()).rstrip("/") + "/v1/agents/events", json={"event_type": event_type, "message": message, "context": {"source": "setup.exe", "agent_version": os.environ.get("AGENT_VERSION", "")}}, headers={"Authorization": f"Bearer {agent_token}"}, timeout=15, ) response.raise_for_status() except Exception: # noqa: BLE001 — best effort only return