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>
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_idin the successful enrollment response. The value is the existingenrollment_uuidused to create the Hub Installation. - Persist this non-secret value in
agent.envfor new installs. - Add authenticated
GET /v1/agents/me/identity, returning onlyagent_id,installation_id,hostname,version, andstatus. Keep the existing/v1/agents/meresponse unchanged for compatibility. - At runtime, identity precedence is: existing SQLite
installation_identity, then configuredINSTALLATION_ID, then the authenticated identity endpoint. Recovery must verify the returnedagent_idequals localAGENT_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 to1to preserve current behavior for existing deployments. - When it is
0, the loop may send heartbeat and sleep, but must not callnext_job, claim jobs, classify products, or execute jobs. - Set
AGENT_JOBS_ENABLED=0only 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
GTIFiscalAgentServicepayload while the service is stopped. Do not run Setup, replaceagent.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
- Enrollment response returns the exact
enrollment_uuidasinstallation_id; Setup persists it without placing any token inagent.env. - Authenticated identity endpoint returns only the approved identity fields and derives hostname/version/status from the existing Agent/Installation.
- 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.
- With
AGENT_JOBS_ENABLED=0, heartbeat runs butnext_jobis never called; default-enabled behavior remains covered. - 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_idand 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.