14 KiB
GTI FISCAL WINDOWS INSTALLER EXE V1 — Design
- Date: 2026-09-23
- Status: APPROVED_WITH_ADJUSTMENTS (operator 2026-09-23)
- Related: previous mission "Distribuição ZIP V1" shipped release 1.1.0.
Objective
Replace the ZIP download UX with a real Windows .EXE installer
(GTIFiscalAgent-Setup-<version>.exe) plus an installation/enrollment code per
installation. One universal installer per version. Publish release 1.1.1 in the
production Hub, run a REAL Windows E2E, and stop before the first client.
Acceptance (operator visible): Fiscal → Agents → Nova instalação → Criar →
Baixar Agent → browser downloads GTIFiscalAgent-Setup-1.1.1.exe directly (an
executable, not a ZIP). Run the EXE on Windows, type the code, install/enroll.
ZIP as the main flow = FAIL.
Approved adjustments (operator)
- Setup.exe uses
requireAdministrator/--uac-adminmanifest. NO hybrid asInvoker + self-elevation flow. - Setup.exe is self-contained: embeds the Service host + runtime + Uninstall.exe. It never downloads a second ZIP during install.
install_codealone resolves the Installation on the Hub. User types only the code. If bothinstall_codeandenrollment_tokenare present, the request is rejected.- Production: the permanent credential NEVER goes into
agent.env. It lives only incredentials.bin, protected by DPAPI LocalMachine + ACL SYSTEM/Administrators.agent.envholds only non-secret configuration plusFIREBIRD_WRITES=0. - "Reproducible build" = an automated, repeatable build process and an immutable release — NOT byte-identical hashes across rebuilds. After PUBLISHED, 1.1.1 can never be replaced/overwritten.
Also fixed in this design:
- 16 Crockford Base32 chars = 80 bits of entropy for the install code.
- Enrollment rate limit keyed by IP AND by code/installation.
- Install code is normalized (hyphens/case) before hashing.
- Release 1.1.0 stays untouched.
FIRST_REAL_CLIENT_AGENT_INSTALLED=falsethis mission.
Installer technology
Chosen: PyInstaller (all-Python). No Inno Setup.
Reasoning (INSTALLER_TECHNOLOGY / INSTALLER_REASON):
- PyInstaller 6.21 is already installed on the Windows dev machine; Inno Setup is
not. A fully automated, fail-fast build is achievable today with
pip-managed tools. - The enrollment wizard already needs HTTPS (httpx) and DPAPI (pywin32 win32crypt); those are Python layers, so the wizard must be Python regardless. Adding Inno Setup would add a second tool without removing the Python wizard requirement.
- pywin32 gives the battle-tested Windows Service wrapper, and PyInstaller's own runtime sulution replaces a venv in Program Files, so no Python/pip is needed on clients.
- The previous mission already validated the service wrapper +
sc.exe failurerecovery path in the pilot checklist. - Reproducible build is achieved with a dedicated PowerShell build script (clean output dir, official VERSION, SHA256, metadata, fail on missing dep); exact byte-identical output is NOT required.
Artifacts (universal; built on the Windows dev machine)
| Artifact | PyInstaller mode | Contents / role |
|---|---|---|
GTIFiscalAgent-Setup-<v>.exe |
onefile, --uac-admin |
GUI wizard (tkinter). Embeds Agent runtime sources + service host + Uninstall.exe as bundled data. Enroll → write config → DPAPI credential → extract Service host → register service → preflight. Main download artifact. |
GTIFiscalAgentService.exe (+ _internal/) |
onedir | The Windows Service host (bundled inside Setup.exe, extracted to C:\Program Files\GTI\FiscalAgent\). Reuses gti_agent_windows_service.py logic; token comes from credentials.bin. |
Uninstall.exe |
onefile | Stops/removes the service, removes Program Files tree + ARP entry. Invoked from Settings → Apps → GT1 Fiscal Agent → Uninstall. |
Setup.exe behavior:
- Program runs elevated (requireAdministrator manifest). If not elevated, Windows UAC prompts.
- tkinter window: title "GTI Fiscal Agent", a single install-code field, Hub
URL fixed at
https://gtifiscal.gtigrupo.com.br(no prompt; dev override via env / CLI flag only). - "Instalar" → POST
/v1/agents/enroll{"install_code": "XXXX-XXXX-XXXX-XXXX"}. - On success (agent_id, agent_token, hub_url, tenant, release info):
- Write
agent.env(non-secret): GTI_HUB_URL, AGENT_ID, COMPANY_ID/UF (optional), FIREBIRD_DATABASE_PATH (blank), FB_USER/FB_PASSWORD blank, FISCAL_MODE=DRY_RUN, FIREBIRD_WRITES=0, AGENT_VERSION, POLL_INTERVAL_SECONDS, AGENT_LOG_PATH, AGENT_STATE_DB, EXPECTED_SCHEMA_FINGERPRINT. - Write
credentials.binvia DPAPI LocalMachine encryption (win32crypt.CryptProtectData(..., CRYPTPROTECT_LOCAL_MACHINE)); contains AGENT_TOKEN (and optional secrets). Apply ACL: SYSTEM/Administrators only (icacls).agent.envnever holds the token. - Extract bundled Service host onedir to
C:\Program Files\GTI\FiscalAgent\. - Install service
GTIFiscalAgent(Automatic/Delayed), set recoverysc.exe failure "GTIFiscalAgent" reset= 86400 actions= restart/60000/restart/60000/restart/60000.
- Write
- Preflight: Firebird path not configured → report "serviço instalado; configure o arquivo de banco para ativar" and do NOT start the service (no error loop). If fully configured → optionally start.
- Write ARP uninstall key (DisplayName "GTI Fiscal Agent", UninstallString = Uninstall.exe).
WINDOWS_INSTALL_COMPLETEDaudit (best effort) once install succeeds.
The installer never stores the enrollment code after success, never embeds any permanent credential, and never enables Firebird/AI writes (no UI to do so).
Installation code (install_code)
- Format: 16 chars, Crockford Base32 alphabet
(
0123456789ABCDEFGHJKMNPQRSTVWXYZ— excludes I, L, O, U), groupedXXXX-XXXX-XXXX-XXXX. 16 chars × 5 bits = 80 bits entropy. - Generation:
secrets.randbelow/random pick of 16 chars; return formatted. - Normalization before hashing: uppercase, strip hyphens/spaces.
- Storage: only SHA-256 of the normalized code. Never the plaintext.
- Columns on
agent_enrollments(migration0023):install_code_hash(str 64),install_code_expires_at(timestamptz),install_code_attempts(int, default 0),install_code_created_at(timestamptz). - TTL:
AGENT_ENROLLMENT_TTL_SECONDS(default 86400) reused. - One active code per installation at a time; "Gerar novo código" rotates it (old code invalidated, previous hash kept for audit but unusable).
- Revocation (
Revogar) invalidates the code and, when an agent exists, deactivates it. - Rate limit:
rate_limit.record_attempt(bucket="enroll_code:"+normalized_code_hash or agent_enrollment_id, origin=IP)in addition to per-IP bucket.
Enrollment endpoint (POST /v1/agents/enroll)
- Body:
{"install_code": "..."}OR legacy{"installation_id": ..., "enrollment_token": ...}. - If both
install_codeandenrollment_tokenare provided → 400,ENROLLMENT_REJECTED(reason INVALID_REQUEST). - With
install_code(primary):- Resolve the Installation by normalized-code hash. No installation_id needed.
- Validate: exists, status=PENDING (not REVOKED), release PUBLISHED, code not
expired, one-time.
install_code_attemptsguard + rate limit. - On success: digest the code (mark used), set installation status ENROLLED,
create the Agent with a fresh permanent token, return
{agent_id, agent_token, installation_id, hub_url, ...}. ENROLLMENT_STARTED / SUCCEEDED / REJECTEDaudit; never log the code.
- Legacy
enrollment_tokenpath kept for backward compatibility (published 1.1.0 bundles) but is no longer produced for new installations.
Downloads
/agents/releasesmain button + pending detail + temp link: serve the SAME universal EXE (GTIFiscalAgent-Setup-<v>.exe) stored as the release artifact.- Filename/querystring carry no code/token.
INSTALLER_DOWNLOADEDaudit event on download.
Release model
- New release row:
version=1.1.1,platform=windows,architecture=x64,filename=GTIFiscalAgent-Setup-1.1.1.exe,status=PUBLISHED,published_at(per the prior fix),sha256,size_bytes. - Storage path generalized (EXE artifact, not hardcoded
.zip). The internal ZIP remains a build/debug artifact only (kept under dist/, not the main flow). - Release 1.1.0 row and artifact remain untouched.
- After PUBLISHED, the artifact is immutable: publish script refuses to replace an existing PUBLISHED filename for the same version+platform+arch.
Agent runtime changes (FIREBIRD_WRITES=0, DPAPI credential)
- New module
gti_fiscal_agent/credential_store.py:read_credential(path)— decrypt DPAPI LocalMachine blob (fallback: if the file is a plaintext token, accept in dev only).write_credential(path, payload)(installer side; reused by Setup.exe) — encrypt + set ACL.install_env_from_credentials(path, env)— fillAGENT_TOKENfromcredentials.bin;agent.envstays non-secret.
Config/main/service wrapper read the token viaAGENT_TOKENif set, else look forGTI_CREDENTIAL_FILE(defaultC:\Program Files\GTI\FiscalAgent\credentials.bin) and decrypt.FIREBIRD_WRITES=0enforced (already fail-closed inConfig.firebird_writesandopen_write_connection); drift test asserts the installer template sets 0 and no UI/flag enables it.
Panel UI changes
agent_pending_detail.html+agent_detail.html: show- Versão, Status,
- Código de instalação
XXXX-XXXX-XXXX-XXXX+ [Copiar] - Expira em (formatted TTL deadline)
- [Baixar instalador (.EXE)] → EXE
- [Gerar novo código] → rotates code (audit INSTALLATION_CODE_CREATED)
- [Revogar] → revokes (audit INSTALLATION_CODE_REVOKED / INSTALLATION_REVOKED)
/agents/releases: download button → EXE (main flow).- Never show raw enrollment tokens; the install code is shown to the operator who created the installation only (SSO-authenticated panel).
Build script
scripts/build_windows_installer.ps1- Reads official
release/VERSION(1.1.1). - Cleans
dist/installeroutput. - Builds service host (onedir) and Setup.exe (onefile,
--uac-admin) and Uninstall.exe with PyInstaller from the bundled sources; Setup embeds the service host dir + Uninstall.exe as data. - Computes sha256, writes
GTIFiscalAgent-Setup-<v>.exe.sha256. - Fails on missing dependency/tool.
- Deterministic inputs; byte-identical output not required.
- Reads official
Tests (TDD)
Central (central/tests, pytest):
- install code generation/normalization/hash round trip (format, alphabet, uppercase, hyphen removal, no ambiguous chars).
- enroll with
install_coderesolves installation by code alone; success creates enrolled agent + credential issuance;install_code_attemptsincrements; expiry/revoked/used-one-time → REJECTED; code+token together → 400; rate limit by IP and by code; duplicate/rotation invalidation. - migration 0023 up/down.
- publish script refuses to overwrite a PUBLISHED artifact and stamps
published_at. - downloads serve EXE with
INSTALLER_DOWNLOADEDaudit. - panel pages render code/button actions without leaking tokens.
Agent (agent/tests + root tests): credential_store encrypt/decrypt, DRY_RUN /
FIREBIRD_WRITES=0 enforcement, agent.env has no token, service wrapper reads
credential.
Build verification: PowerShell script dry-run on dev machine produces EXE + sha256; PyInstaller starts the setup binary (elevation triggered) or we verify artifact layout.
E2E plan
- Local: build 1.1.1 EXE; launch against the PRODUCTION Hub once the backend is deployed (or against a local Hub in a scratch container for the UI pass).
- Production Hub (real, at
https://gtifiscal.gtigrupo.com.br):- Create an E2E installation via the panel.
- Generate an install code.
- Download the real EXE (assert filename, MIME application/octet-stream / executable, not ZIP).
- Run on the real Windows dev machine, enter code, complete enrollment
(real
POST /v1/agents/enroll), verify:- Installation → ENROLLED, Agent created.
credentials.binexists (DPAPI-encrypted, not plaintext token).agent.envhas no token, FIREBIRD_WRITES=0, blank Firebird path.- Service
GTIFiscalAgentregistered, Automatic/Delayed, recovery set, NOT started (Firebird not configured) — or started if configured. - Windows firewall/dir ACL ok (SYSTEM/Administrators).
- Uninstall via Uninstall.exe → service removed, Program Files cleaned.
- Revoke/deactivate the E2E agent in the panel; keep audit trail.
- No real Firebird database is ever connected.
- Rejections E2E: invalid code, expired code, replayed/used code (each audited ENROLLMENT_REJECTED), code+token together → 400.
Deploy sequence
- Backup production: PostgreSQL logical dump (
pg_dump) ofgti_fiscal+ copy of release storage volume (via container) to the backup location. - Build image with migration
0023;alembic upgrade head(fresh-DB and head checks). - Deploy Hub (overlay, docker compose up -d --build), postgres intact.
- Publish release 1.1.1 EXE (immutable).
- Production smoke: health endpoint, panel pages, download EXE.
- Run production E2E (above).
- Validate restore: restore EXE artifact + PG metadata from backup copy.
Constraints (non-negotiables)
- FIREBIRD_WRITES=0; AI_WRITE_ALLOWED=false; no UI enabling either.
- No real client install this mission.
- No SSO/NPM/DNS/hardware/credentials changes.
- Release 1.1.0 not touched. 1.1.1 immutable once PUBLISHED.
- CODE_SIGNED=false. SmartScreen warning acceptable in the internal pilot.
- Credentials never logged/committed;
GTI_SSH_PASSWORDenv var only.
Report (final)
Populate the mission's exact field list (STATUS, INSTALLER_TECHNOLOGY=PyInstaller, INSTALLER_REASON, ..., PANEL_DOWNLOAD_FILENAME=GTIFiscalAgent-Setup-1.1.1.exe, DPAPI=TRUE, ACL=system+administrators, FIREBIRD_WRITES=0, WINDOWS_E2E=..., etc.).