docs: windows installer EXE V1 implementation plan

This commit is contained in:
gti
2026-09-23 16:02:30 -03:00
parent 701d7dafdd
commit b058affa29
@@ -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:<enrollment_uuid>", 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-<v>.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.