Files
GTI_GERENCIADOR_FISCAL/docs/superpowers/plans/2026-09-23-agent-installation-identity-heartbeat.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

14 KiB

Agent Installation Identity Recovery and First Heartbeat Implementation Plan

For agentic workers: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.

Goal: Recover and persist Agent 2's existing Hub installation_id, send exactly the first safe heartbeat, and stop without claiming jobs or writing Firebird.

Architecture: The Hub exposes the already-linked installation identity over an authenticated, read-only endpoint. New enrollments return that same ID so Setup can persist it; legacy installations recover it at runtime and commit it into the existing SQLite identity table before heartbeat. AGENT_JOBS_ENABLED=0 skips the job API entirely during activation.

Tech Stack: FastAPI, SQLAlchemy, pytest, Python fdb, SQLite local state, PyInstaller onedir service host, Windows SCM / PowerShell.

Global Constraints

  • Keep Agent ID 2; do not create an Agent, reenroll, or create a second Installation.
  • Never generate a fallback installation_id for an already-registered Agent.
  • Do not call /v1/jobs/next, classification endpoints, or any job execution path during activation.
  • Preserve FIREBIRD_WRITES=0, FISCAL_MODE=DRY_RUN, and empty AUTHORIZED_WRITE_DATABASE_PATH.
  • All Firebird connections used by preflight remain read-only; issue no Firebird DML/DDL.
  • Do not rerun Setup.exe or replace agent.env / credentials.bin during service-host update.
  • Keep published GTIFiscalAgent-Setup-1.1.1.exe immutable; update only the installed service runtime.
  • Never print or log FB_PASSWORD, the Agent bearer token, or credential-file contents.
  • Do not commit unless the operator explicitly requests a commit.

Task 1: Hub installation identity response

Files:

  • Modify: central/app/api.py
  • Test: central/tests/test_jobs.py

Interfaces:

  • Add authenticated GET /v1/agents/me/identity.

  • Return exactly {agent_id, installation_id, hostname, version, status}; installation_id and hostname are None if the Agent has no Installation.

  • Do not change the existing GET /v1/agents/me response.

  • Step 1: Write the failing identity endpoint tests

Add tests that create an Agent with one Installation (installation_id fixed UUID, hostname DESKTOP-B0HL02I, version 1.1.1) and assert:

response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
assert response.json() == {
    "agent_id": agent.id,
    "installation_id": installation.installation_id,
    "hostname": "DESKTOP-B0HL02I",
    "version": "1.1.1",
    "status": "OFFLINE",
}

Also assert unauthenticated requests return 401 and an Agent without an Installation returns HTTP 200 with nullable identity fields, without creating an Installation.

  • Step 2: Run the focused tests and verify failure

Run: python -m pytest central/tests/test_jobs.py -q Expected: the identity route returns 404 before implementation.

  • Step 3: Implement the authenticated endpoint

Use the existing principal dependency, query Installation by agent.id, and derive status with the same online-threshold rule as /v1/agents/me. The route must not commit or mutate database state.

  • Step 4: Run the focused tests

Run: python -m pytest central/tests/test_jobs.py -q Expected: all tests pass; existing /v1/agents/me tests remain unchanged and green.

Task 2: Return and persist identity during new enrollment

Files:

  • Modify: central/app/agent_distribution.py
  • Test: central/tests/test_install_code_enrollment.py
  • Modify: installer/install_common.py
  • Test: tests/test_installer_artifacts.py

Interfaces:

  • Successful /v1/agents/enroll response adds installation_id=enr.enrollment_uuid.

  • write_agent_env(install_dir_path, enroll_result) writes INSTALLATION_ID as a non-secret field. It never writes agent_token or AGENT_TOKEN.

  • Step 1: Add failing enrollment and Setup-env assertions

In the existing install-code happy-path test, assert body["installation_id"] == uuid. In the installer artifact test, pass a fixed ID such as 00000000-0000-4000-8000-000000000222 and assert the env includes it while the token is absent.

  • Step 2: Run focused tests and verify failure

Run: python -m pytest central/tests/test_install_code_enrollment.py::test_enroll_with_install_code_alone_creates_agent tests/test_installer_artifacts.py -q Expected: failures show the response/env currently omit installation_id.

  • Step 3: Add the returned ID and env serialization

Add "installation_id": enr.enrollment_uuid to the successful enrollment JSON. Add INSTALLATION_ID to write_agent_env; do not change credential storage or Firebird guard values.

  • Step 4: Run focused tests

Run the same command as Step 2. Expected: enrollment and installer artifact tests pass; agent.env remains token-free with FIREBIRD_WRITES=0 and FISCAL_MODE=DRY_RUN.

Task 3: Persist or recover identity before heartbeat

Files:

  • Modify: agent/src/gti_fiscal_agent/state.py
  • Modify: agent/src/gti_fiscal_agent/client.py
  • Modify: agent/scripts/run_agent_service.py
  • Test: agent/tests/test_state.py
  • Test: agent/tests/test_run_agent_service.py

Interfaces:

  • AgentState.installation_id(candidate: str | None = None) -> str returns the persisted UUID; if no UUID is stored, it persists a valid candidate; if neither exists, it raises instead of generating a random UUID. A different candidate from an already-persisted UUID raises.

  • HubClient.identity() -> dict calls authenticated GET /v1/agents/me/identity.

  • resolve_installation_id(config, env, state, client) -> str selects persisted state, then INSTALLATION_ID, then authenticated identity; verifies returned agent_id equals config.agent_id; persists before returning.

  • Step 1: Write failing state identity tests

Cover: empty store plus valid candidate persists and returns it; second call returns the same ID; conflicting candidate raises; missing candidate does not generate; malformed UUID raises.

  • Step 2: Run state tests and verify failure

Run: python -m pytest agent/tests/test_state.py -q Expected: the missing identity case currently generates a UUID and the persistence API is absent.

  • Step 3: Implement deterministic SQLite identity persistence

Validate candidate values with uuid.UUID. Within a SQLite transaction, read the existing row, compare any candidate, insert only when absent, and return the single stored canonical UUID. Remove random UUID creation from the registered-agent path.

  • Step 4: Write failing runtime bootstrap tests

Use a fake Hub identity response and fake state. Assert missing local identity calls identity() once, verifies matching Agent ID, persists the returned UUID, and uses it in heartbeat. Assert a mismatched Agent ID, null ID, or identity request error fails before heartbeat. Assert an already-persisted ID avoids the identity request.

  • Step 5: Run runtime tests and verify failure

Run: python -m pytest agent/tests/test_run_agent_service.py -q Expected: current loop calls the random-UUID-producing state method and has no authenticated identity bootstrap.

  • Step 6: Implement identity bootstrap before the heartbeat loop

Add HubClient.identity(), implement resolve_installation_id, and invoke it after constructing the Hub client/state but before entering the loop. An identity failure must exit before heartbeat and before job polling.

  • Step 7: Run focused state/runtime tests

Run: python -m pytest agent/tests/test_state.py agent/tests/test_run_agent_service.py -q Expected: persisted/recovered identity tests pass and mismatches fail closed.

Task 4: Disable job polling without disabling heartbeat

Files:

  • Modify: agent/scripts/run_agent_service.py
  • Test: agent/tests/test_run_agent_service.py
  • Modify on this host only: C:\Program Files\GTI\FiscalAgent\agent.env

Interfaces:

  • Config.agent_jobs_enabled parses AGENT_JOBS_ENABLED, default True.

  • When false, the loop sleeps for poll_interval_seconds and continues after heartbeat without calling HubClient.next_job().

  • Step 1: Add the failing no-poll test

Use a fake Hub whose next_job() raises if called. Make its first heartbeat set the external stop flag. Run one loop cycle with AGENT_JOBS_ENABLED=0 and assert one heartbeat, zero next_job calls, and zero job/classification calls.

  • Step 2: Run the focused test and verify failure

Run: python -m pytest agent/tests/test_run_agent_service.py -q Expected: the current loop calls next_job() immediately after heartbeat.

  • Step 3: Implement the gate with a backward-compatible default

Parse AGENT_JOBS_ENABLED as enabled unless its stripped, uppercased value is 0, FALSE, or NO. After successful heartbeat, if disabled, sleep once and continue before constructing or calling the job endpoint.

  • Step 4: Run focused runtime tests

Run: python -m pytest agent/tests/test_run_agent_service.py -q Expected: disabled polling makes no next_job request; default configuration retains the existing job behavior.

Task 5: Full regressions and guard review

Files:

  • Verify: central/tests/, agent/tests/, tests/

  • Step 1: Run Hub and Agent regressions

Run: python -m pytest central/tests -q and python -m pytest agent/tests -q. Expected: both suites pass.

  • Step 2: Run repository offline regressions

Run: python -m pytest tests -q. Expected: all tests pass with only the repository's pre-existing skip.

  • Step 3: Verify write and secret invariants

Assert tests and source still guarantee FIREBIRD_WRITES=0, FISCAL_MODE=DRY_RUN, empty AUTHORIZED_WRITE_DATABASE_PATH, no token in agent.env, and identity endpoint contains only the approved fields. Do not connect to Firebird for this check.

Task 6: Deploy the API and update only the installed service runtime

Files/artifacts:

  • Deploy Hub source: central/app/api.py, central/app/agent_distribution.py

  • Build artifact: dist/installer/GTIFiscalAgentService/ only

  • Existing install: C:\Program Files\GTI\FiscalAgent\GTIFiscalAgentService\

  • Step 1: Deploy the Hub code without a migration

Use the approved production deployment path and preserve the existing Hub .env, PostgreSQL volume, release volume, SSO, and DNS. Build/restart only fiscal-hub; verify /health, /version, and authenticated /v1/agents/me/identity return Agent 2's already-existing identity. Do not query the database directly to retrieve the ID.

  • Step 2: Build only the service-host onedir

Record SHA-256 of dist/installer/GTIFiscalAgent-Setup-1.1.1.exe before building. Verify no unversioned GTIFiscalAgent-Setup.exe exists, then run scripts/build_windows_installer.ps1 -SkipSetup -SkipUninstall. Confirm the versioned Setup hash is unchanged and no Setup.exe is run.

  • Step 3: Back up and replace only the service runtime

With GTIFiscalAgent stopped, copy the current installed GTIFiscalAgentService directory to a timestamped local backup. Replace only that directory with the newly built service onedir. Leave agent.env, credentials.bin, Uninstall.exe, registry/ARP entries, and state files untouched.

  • Step 4: Prepare the existing installation for heartbeat-only operation

Elevated-edit agent.env to set AGENT_JOBS_ENABLED=0, preserving FIREBIRD_WRITES=0, FISCAL_MODE=DRY_RUN, the existing Agent ID, Firebird configuration, and SYSTEM/Administrators ACL. Do not put the bearer token in this file.

  • Step 5: Verify recovery before SCM start

Run the installed runtime's read-only identity bootstrap/preflight with the existing DPAPI credential. Confirm recovered agent_id=2, installation_id equals the endpoint value, the SQLite state contains that same ID, and jobs are disabled. If any check fails, leave the service stopped.

Task 7: First heartbeat only, then stop

Files:

  • Existing Windows service: GTIFiscalAgent

  • Existing Hub identity endpoint: GET /v1/agents/me/identity

  • Step 1: Capture a baseline

Before service start, record the existing local ipi_write_log row count and confirm service state is STOPPED. Do not call /v1/jobs/next or submit classification requests.

  • Step 2: Start the existing service

Run sc.exe start GTIFiscalAgent; wait for SCM state RUNNING and verify the identity endpoint shows agent_id=2, the same stored installation_id, correct hostname, version 1.1.1, and status ONLINE.

  • Step 3: Stop immediately after first heartbeat

Run sc.exe stop GTIFiscalAgent; verify STOPPED. Keep AGENT_JOBS_ENABLED=0 in agent.env.

  • Step 4: Verify zero jobs and writes

Compare local ipi_write_log count with baseline; assert unchanged and equal to zero. Confirm no classification request or job was sent, FIREBIRD_WRITES=0, and WRITE_ATTEMPTS=0 / WRITE_SUCCESSES=0. Verify authenticated identity still maps to Agent 2 and the original installation ID; do not create or enroll anything.

  • Step 5: Verify user-visible status if panel access is available

Confirm Agent 2 displays ONLINE with the host name and version 1.1.1. If the panel requires unavailable human authentication, report the authenticated identity endpoint evidence and leave the panel UI item as a blocker; do not bypass authentication.

Rollback and stop gates

  • If Hub identity response is absent, mismatched, or does not name Agent 2, stop before writing the local identity or starting SCM.
  • If Hub deployment or service-host replacement fails, restore the backed-up service directory and leave the service stopped.
  • If AGENT_JOBS_ENABLED=0 is not verified, do not start the service.
  • If preflight, identity, hostname, version, or write guards fail, stop and report; do not retry enrollment, generate another UUID, run a job, or write Firebird.