6.6 KiB
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
masterin focused commits (repo convention; no remote).
Order of work (TDD front-loaded)
Phase A — Backend install_code (code-first, tests first)
central/app/enrollment.py: add Crockford Base32 install-code module (generate_install_code,normalize_install_code,install_code_digest,valid_install_code). 16 chars, alphabet0123456789ABCDEFGHJKMNPQRSTVWXYZ, groupedXXXX-XXXX-XXXX-XXXX.- Migration
0023_install_codes.pyonagent_enrollments:install_code_hash,install_code_expires_at,install_code_attempts,install_code_created_at. UpdateAgentEnrollmentmodel. 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
enrollbucket AND per-code/installation bucket (addrecord_attempt("enroll_code:<enrollment_uuid>", ip)). install_code_attemptsincrements on each failed attempt.
- Accept
central/app/web.py+ templates:agent_pending_detail.html/agent_detail.html: Código de instalaçãoXXXX-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 (auditINSTALLATION_CODE_CREATED); POST/agents/pending/{uuid}/revokealready exists → also clear code + auditINSTALLATION_CODE_REVOKED. agent_releases.htmlmain button → EXE.
- 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. AuditINSTALLER_DOWNLOADED. central/scripts/publish_agent_release.py: generalize--zipto--artifact(accept.zipor.exe); filename derivation handlesGTIFiscalAgent-Setup-<v>.exe; enforce immutability (refuse replacing a PUBLISHED artifact).published_atfix already in place (commit21c9261).- Tests in
central/tests/test_agent_distribution.py+ newtest_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/testssuite green:python -m pytest central/tests -qandpython -m pytest tests -q(repo-level offline tests) after each phase. - Migration head check:
0023newest; fresh-DB upgrade test green. - Artifacts:
dist/installer/GTIFiscalAgent-Setup-1.1.1.exe+*.sha256. - No
AGENT_TOKENliteral anywhere inagent.env/installed tree.