Files
GTI_GERENCIADOR_FISCAL/docs/superpowers/specs/2026-09-23-agent-installation-identity-heartbeat-design.md
T
gtiandClaude Sonnet 5 ac7b503c2f 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>
2026-09-23 22:13:56 -03:00

5.4 KiB

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.