# 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.