From b058affa29441a6550b03995fb8ff354a2d7da22 Mon Sep 17 00:00:00 2001 From: gti Date: Wed, 23 Sep 2026 16:02:30 -0300 Subject: [PATCH] docs: windows installer EXE V1 implementation plan --- .../2026-09-23-windows-installer-exe-v1.md | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-23-windows-installer-exe-v1.md diff --git a/docs/superpowers/plans/2026-09-23-windows-installer-exe-v1.md b/docs/superpowers/plans/2026-09-23-windows-installer-exe-v1.md new file mode 100644 index 0000000..d1fc804 --- /dev/null +++ b/docs/superpowers/plans/2026-09-23-windows-installer-exe-v1.md @@ -0,0 +1,114 @@ +# 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. \ No newline at end of file