fix: persist and recover the Agent's real installation_id (heartbeat activation)

POST /v1/agents/enroll now returns installation_id; new installs persist it
to agent.env immediately. Runtime recovery precedence: existing SQLite
installation_identity -> configured INSTALLATION_ID -> authenticated
GET /v1/agents/me/identity. Recovery fails closed on missing/malformed/
mismatched identity rather than inventing a UUID (which the Hub would then
reject as a conflicting Installation on an already-enrolled Agent). An
explicit AGENT_JOBS_ENABLED gate stops the service from claiming a job right
after the first heartbeat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-23 22:13:56 -03:00
co-authored by Claude Sonnet 5
parent a9aded1e49
commit ac7b503c2f
16 changed files with 754 additions and 15 deletions
@@ -0,0 +1,107 @@
# Agent Installation Identity Recovery and First Heartbeat
## Goal and scope
Activate the already-enrolled Agent 2 on this Windows host without creating a
new Agent, enrolling again, or registering a second installation. Recover and
persist its existing Hub `installation_id`, send a first real heartbeat, and
stop before any job is claimed. Firebird stays strictly read-only and the
installation remains `FIREBIRD_WRITES=0` / `FISCAL_MODE=DRY_RUN`.
## Current root cause
`POST /v1/agents/enroll` creates the Hub `Installation` with
`installation_id=enrollment_uuid`, but its response omits that identity.
`installer/setup_main.py` therefore does not persist it. On a host with no
`agent_state.sqlite`, `AgentState.installation_id()` invents a UUID. The Hub
heartbeat route rejects a different UUID for an Agent that already has an
Installation, preventing the first heartbeat.
The service loop also calls `GET /v1/jobs/next` immediately after a successful
heartbeat, so an explicit runtime gate is required for this activation.
## Design
### Identity delivery and recovery
- Return `installation_id` in the successful enrollment response. The value is
the existing `enrollment_uuid` used to create the Hub Installation.
- Persist this non-secret value in `agent.env` for new installs.
- Add authenticated `GET /v1/agents/me/identity`, returning only
`agent_id`, `installation_id`, `hostname`, `version`, and `status`. Keep the
existing `/v1/agents/me` response unchanged for compatibility.
- At runtime, identity precedence is: existing SQLite `installation_identity`,
then configured `INSTALLATION_ID`, then the authenticated identity endpoint.
Recovery must verify the returned `agent_id` equals local `AGENT_ID`; absent,
malformed, or mismatched identity fails closed without generating a UUID or
sending a heartbeat.
- Persist a recovered/configured UUID atomically in the existing SQLite state
before sending heartbeat. Restarts reuse this stored ID. A conflict between
configured and already-persisted values fails closed.
### Job-poll gate
- Add `AGENT_JOBS_ENABLED`, defaulting to `1` to preserve current behavior for
existing deployments.
- When it is `0`, the loop may send heartbeat and sleep, but must not call
`next_job`, claim jobs, classify products, or execute jobs.
- Set `AGENT_JOBS_ENABLED=0` only for this existing installation while
validating the first heartbeat. Leave it disabled after the service is
stopped, until a separate operator action authorizes job processing.
### Deployment boundary
- The identity endpoint is Hub application code only; it needs no DB migration.
Deploy that Hub code without changing the immutable Agent release or the
production SSO/DNS configuration.
- The installed service binary must contain the identity recovery and job gate.
Build only the service-host onedir and update the existing
`GTIFiscalAgentService` payload while the service is stopped. Do not run Setup,
replace `agent.env`/`credentials.bin`, recreate an Agent, or enroll again.
- Keep the published 1.1.1 Setup artifact immutable. The installer source fix
gets regression coverage for a future release; this activation updates only
the installed service runtime needed by the current host.
### Write safety
No Firebird write path is added or enabled. The existing agent configuration
must continue to contain `FIREBIRD_WRITES=0`, `FISCAL_MODE=DRY_RUN`, and an empty
`AUTHORIZED_WRITE_DATABASE_PATH`. The first heartbeat reports metadata only;
this mission must not call `/jobs/next` or a classification endpoint.
## Failure behavior
- Identity endpoint unavailable, no existing installation, or Agent ID mismatch:
fail closed before heartbeat and before job polling.
- SQLite persistence failure: fail closed; do not send a guessed identity.
- Jobs disabled: continue the heartbeat loop without touching the job API.
- If Hub deployment or the service-host update cannot be completed, do not
start the installed service; report the deployment blocker instead.
- Service is stopped immediately after the first successful heartbeat is
observed and verified against Agent 2's existing installation.
## Regression coverage
1. Enrollment response returns the exact `enrollment_uuid` as
`installation_id`; Setup persists it without placing any token in `agent.env`.
2. Authenticated identity endpoint returns only the approved identity fields
and derives hostname/version/status from the existing Agent/Installation.
3. Runtime recovers and persists an ID when local state is missing, validates
Agent ID equality, reuses the ID on restart, and fails closed on mismatch or
missing identity.
4. With `AGENT_JOBS_ENABLED=0`, heartbeat runs but `next_job` is never called;
default-enabled behavior remains covered.
5. Existing write-safety tests continue to prove zero Firebird writes in
`DRY_RUN` / `FIREBIRD_WRITES=0`.
## Acceptance for this activation
- Existing Agent ID remains 2; no enrollment or Agent creation occurs.
- Recovered identity equals the Hub's existing `Installation.installation_id`
and is persisted locally before start.
- Jobs are disabled before service start; service reaches RUNNING, sends one
successful heartbeat for the existing installation, then is stopped.
- Panel shows Agent 2 online with the correct hostname and version 1.1.1; no
duplicate installation is created.
- `FIREBIRD_WRITES=0`, `WRITE_ATTEMPTS=0`, `WRITE_SUCCESSES=0`; no job polling,
classification, or Firebird writes occur.