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:
@@ -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.
|
||||
Reference in New Issue
Block a user