# GTI FISCAL WINDOWS INSTALLER EXE V1 — Implementation Plan - Date: 2026-09-23 - Spec: `docs/superpowers/specs/2026-09-23-windows-installer-exe-v1-design.md` - Branch strategy: work on `master` in focused commits (repo convention; no remote). ## Order of work (TDD front-loaded) Phase A — Backend install_code (code-first, tests first) 1. `central/app/enrollment.py`: add Crockford Base32 install-code module (`generate_install_code`, `normalize_install_code`, `install_code_digest`, `valid_install_code`). 16 chars, alphabet `0123456789ABCDEFGHJKMNPQRSTVWXYZ`, grouped `XXXX-XXXX-XXXX-XXXX`. 2. Migration `0023_install_codes.py` on `agent_enrollments`: `install_code_hash`, `install_code_expires_at`, `install_code_attempts`, `install_code_created_at`. Update `AgentEnrollment` model. 3. `central/app/agent_distribution.py` — `POST /v1/agents/enroll`: - Accept `install_code` (primary) OR legacy (`installation_id` + `enrollment_token`). - If both bodies present → 400 `INVALID_REQUEST` (ENROLLMENT_REJECTED). - Install-code path resolves Installation by hash alone; validates status, expiry, one-time, attempts, release PUBLISHED; mints agent+installation, marks code used (hash cleared after success → replay-proof), audits `ENROLLMENT_STARTED/SUCCEEDED/REJECTED` (never the code). - Rate limit: keep per-IP `enroll` bucket AND per-code/installation bucket (add `record_attempt("enroll_code:", ip)`). - `install_code_attempts` increments on each failed attempt. 4. `central/app/web.py` + templates: - `agent_pending_detail.html` / `agent_detail.html`: Código de instalação `XXXX-XXXX-XXXX-XXXX` + [Copiar], Expira em, [Baixar instalador (.EXE)], [Gerar novo código], [Revogar]. No raw tokens. - POST `/agents/pending/{uuid}/code` → generate/rotate code (audit `INSTALLATION_CODE_CREATED`); POST `/agents/pending/{uuid}/revoke` already exists → also clear code + audit `INSTALLATION_CODE_REVOKED`. - `agent_releases.html` main button → EXE. 5. Downloads serve universal EXE: `build_and_serve` → when release artifact is an EXE, serve the stored EXE bytes (no bundle zip / no enrollment.json for the EXE flow). Filename for EXE = stored release filename. Audit `INSTALLER_DOWNLOADED`. 6. `central/scripts/publish_agent_release.py`: generalize `--zip` to `--artifact` (accept `.zip` or `.exe`); filename derivation handles `GTIFiscalAgent-Setup-.exe`; enforce immutability (refuse replacing a PUBLISHED artifact). `published_at` fix already in place (commit 21c9261). 7. Tests in `central/tests/test_agent_distribution.py` + new `test_install_codes.py` (RED→GREEN). Full suite green. Phase B — Agent runtime credential isolation 8. `agent/src/gti_fiscal_agent/credential_store.py`: DPAPI LocalMachine encrypt/decrypt (`win32crypt.CryptProtectData`/`CryptUnprotectData` with `CRYPTPROTECT_LOCAL_MACHINE`), ACL enforcing (SYSTEM/Administrators), plaintext-token fallback only in dev (env `GTI_ALLOW_PLAINTEXT_CREDENTIAL=1`). 9. `run_agent_service.py` Config: read AGENT_TOKEN from env OR decrypt from `credentials.bin` (`GTI_CREDENTIAL_FILE`, default Program Files path); keep `agent.env` non-secret; enforce FIREBIRD_WRITES=0 (already fail-closed). 10. Tests: credential_store round-trip + no-write enforcement. Phase C — Installer (Windows build host = this dev machine) 11. `installer/` package (self-contained sources bundled by PyInstaller): - `installer/setup_main.py` (uac-admin entitlement via PyInstaller `--uac-admin`): tkinter UI with single code field; fixed prod Hub URL (dev override only via `GTI_HUB_URL` env/CLI); POST enroll; write `agent.env` (non-secret) + `credentials.bin` (DPAPI LocalMachine + ACL); extract bundled Service host + Uninstall.exe into Program Files; register service + recovery `sc.exe failure ...`; report preflight; write ARP key; best-effort `WINDOWS_INSTALL_COMPLETED`. - `installer/service_main.py`: minimal Windows Service host entry reusing `gti_agent_windows_service` logic (token from credential_store). - `installer/uninstall_main.py`: stop/remove service, remove Program Files tree + ARP, remove credentials. 12. `scripts/build_windows_installer.ps1`: clean dist/installer, read `release/VERSION` (1.1.1), PyInstaller builds (Setup onefile --uac-admin, service host onedir, uninstall onefile), embed service+uninstaller as data in Setup, write `.sha256`. Fails on missing tool/dep. Reproducibility = automated/repeatable + immutable release (not byte-identical hashes). 13. Local build + smoke: produce EXE; validate layout, sha256, no credential in `agent.env`, FIREBIRD_WRITES=0. Phase D — Local E2E (this Windows dev machine) 14. Run Setup against a LOCAL Hub (control-plane image, scratch postgres or SQLite test env) for the UI/install flow: invalid code → clear error; valid code → enroll → verify utils/env/credential/service registration; reinstall idempotence; uninstall. (Real production E2E happens after deploy, Phase F.) Phase E — Production deploy + release 15. Backup: pg_dump of `gti_fiscal` + release storage volume copy (via container) to backup location; validate restore (EXE + metadata) from the backup copy. 16. Build + deploy Hub image (migration 0023 via `alembic upgrade head`); postgres intact; smoke: health, panel, downloads. 17. Publish release 1.1.1 EXE (immutable), `AGEING NOT overwritten`: 1.1.0 zip untouched. Phase F — Production E2E (REAL Windows) 18. Create E2E installation in production panel; generate code; download real EXE from `https://gtifiscal.gtigrupo.com.br`; run on this real Windows host, enter code, enroll. Verify Installation→ENROLLED, Agent created, `credentials.bin` DPAPI-protected (ACL), `agent.env` no token, service registered (Automatic/Delayed, recovery, NOT started since Firebird blank), WINDOWS_INSTALL_COMPLETED audit. 19. Rejection E2E: invalid code, expired code, replayed/used code, code+token together → 400. Each audited. 20. Uninstall via Uninstall.exe; revoke/deactivate E2E agent in panel; audit preserved. Phase G — Report 21. Final report in the mission's exact field list + COMMITS + GIT_STATUS. ## Verification gates - Entire `central/tests` suite green: `python -m pytest central/tests -q` and `python -m pytest tests -q` (repo-level offline tests) after each phase. - Migration head check: `0023` newest; fresh-DB upgrade test green. - Artifacts: `dist/installer/GTIFiscalAgent-Setup-1.1.1.exe` + `*.sha256`. - No `AGENT_TOKEN` literal anywhere in `agent.env`/installed tree.