docs: Hub/Agent independent versioning (Missão 4 / Bloco B.3)

Documents what the code already does (no behavior change): GTI_FISCAL_VERSION
(Hub) and the Agent's own version are separate numbers with separate release
cycles; central.app.installation_identity.reference_agent_version() already
compares an Agent's heartbeat version against the highest PUBLISHED Windows
AgentRelease, never against the Hub's version - verified against
test_version_reference.py. Also documents that there is no real minimum-
version connection gate today (only SCHEMA_FINGERPRINT is enforced) and where
one would go if ever needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-28 11:22:18 -03:00
co-authored by Claude Sonnet 5
parent 0973c41580
commit 8130c4fbdb
+57
View File
@@ -0,0 +1,57 @@
# Versionamento — Hub e Agent (Missão 4 / Bloco B.3)
Hub e Agent têm números de versão **independentes**, cada um com seu próprio ciclo de release.
Um deploy do Hub (`scripts/deploy_hub.ps1`) nunca exige tocar no Agent, e vice-versa. Este
documento só descreve o comportamento já existente no código — nada foi alterado no Agent.
## Fontes de verdade
- **Hub**: `release/VERSION` == `central/app/version.py::GTI_FISCAL_VERSION` ==
`central/pyproject.toml`'s `version` == `RELEASE_MANIFEST.json`'s `GTI_FISCAL_VERSION`/`hub.version`.
Exposto em `GET /version`.
- **Agent**: `agent/pyproject.toml`'s `version` == o default de `AGENT_VERSION` em
`agent/scripts/run_agent_service.py` == `AGENT_VERSION_DEFAULT` em
`agent/src/gti_fiscal_agent/preflight.py`. Em produção, o valor real vem de `.env`
(`AGENT_VERSION=...`), nunca do default do código — o default só importa para uma instalação
legada sem essa variável.
- Os dois só coincidem por coincidência numérica ocasional; **não há relação obrigatória entre
eles**. `tests/test_release_candidate.py` tem dois testes separados que provam isso:
`test_versions_are_coherent_across_hub_and_packages` (só Hub) e
`test_agent_version_is_internally_coherent` (só Agent, consigo mesmo).
## Como o painel decide "Atualização disponível" (já implementado, sem mudança nesta missão)
`central/app/installation_identity.py::reference_agent_version(session)` retorna a maior versão
entre os `AgentRelease` com `status=PUBLISHED` e `platform=windows` — **nunca** a versão do Hub.
Só cai de volta para `settings.agent_server_version` (env `CURRENT_SERVER_VERSION`, um valor de
compatibilidade legado, não a versão do Hub) quando não existe nenhum release Windows publicado.
`compute_version_drift(agent_version, reference)` compara a versão relatada pelo Agent com essa
referência: `CURRENT` se `agent_version >= reference`, `OUTDATED` caso contrário. Exemplo real
verificado nesta missão: Hub em 1.3.11, Agent 2 em 1.3.8, e o último `AgentRelease` Windows
publicado também é 1.3.8 → `compute_version_drift("1.3.8", "1.3.8") == "CURRENT"`, **sem alerta
falso**, mesmo com o Hub numericamente "à frente". Testado em
`central/tests/test_version_reference.py`.
## Versão mínima do Agent aceita pelo Hub
Não existe hoje um bloqueio "Agent versão X não pode nem se conectar" — qualquer Agent
autenticado (token válido) pode enviar heartbeat e reivindicar Jobs, independentemente da
versão. O único controle de compatibilidade real hoje é `SCHEMA_FINGERPRINT` (a impressão do
schema Firebird que o Agent relata), não a versão de software. `api_compat` em `GET /version`
(hoje `"1.0"`) existe como um número de versão de **contrato de API**, mas nenhum código ainda
recusa uma chamada por causa dele — é informativo.
### Matriz de compatibilidade (estado atual, não uma trava de código)
| Hub | Agent | Compatível? | Observação |
|--------|--------|--------------|------------|
| 1.3.11 | 1.3.8 | Sim | Estado real de produção nesta missão. |
| 1.3.9+ | 1.1.0+ | Sim | Capabilities (`RTC_CCLASSTRIB_WRITE_V1`, enriquecimento) são negociadas por presença de campo no heartbeat, não por número de versão. |
| — | < 1.1.0 | Não testado | Antes da distribuição via installer/enrollment; fora do escopo de missões recentes. |
Se um dia for necessário recusar Agents antigos de verdade (ex.: um campo obrigatório novo no
heartbeat), o mecanismo natural é acrescentar uma verificação explícita em
`central/app/api.py` (heartbeat/claim) comparando `agent_version` a um mínimo configurável — isso
NÃO existe hoje e não foi adicionado nesta missão (mudaria o comportamento do Agent/Hub sem
necessidade concreta).