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:
gti
2026-09-23 22:13:56 -03:00
co-authored by Claude Sonnet 5
parent a9aded1e49
commit ac7b503c2f
16 changed files with 754 additions and 15 deletions
+28 -1
View File
@@ -94,6 +94,8 @@ class Config:
self.fiscal_mode = (env.get("FISCAL_MODE") or ("AUTO_APPLY" if requested else "DRY_RUN")).strip().upper()
self.requested_writes = requested
self.firebird_writes = requested and self.fiscal_mode == "AUTO_APPLY"
jobs_enabled = (env.get("AGENT_JOBS_ENABLED", "1") or "1").strip().upper()
self.agent_jobs_enabled = jobs_enabled not in ("0", "FALSE", "NO")
self.poll_interval_seconds = float(env.get("POLL_INTERVAL_SECONDS", "5"))
self.agent_version = env.get("AGENT_VERSION", "1.1.1")
self.schema_fingerprint = env.get("SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1)
@@ -221,6 +223,27 @@ def execute_job(job, config, executor, state, env):
return None, f"{type(error).__name__}: {error}"
def resolve_installation_id(config, env, state, client):
"""Recover and persist the Hub-bound identity; never invent one for an Agent."""
persisted = state.stored_installation_id()
configured = (env.get("INSTALLATION_ID") or "").strip() or None
if persisted is not None:
return state.installation_id(configured)
if configured is not None:
return state.installation_id(configured)
configured_agent_id = str(env.get("AGENT_ID") or config.agent_id or "").strip()
if not configured_agent_id:
raise RuntimeError("AGENT_ID is required to recover installation identity")
identity = client.identity()
if str(identity.get("agent_id") or "") != configured_agent_id:
raise RuntimeError("authenticated identity does not match configured Agent")
installation_id = identity.get("installation_id")
if not installation_id:
raise RuntimeError("authenticated identity response has no installation_id")
return state.installation_id(str(installation_id))
def run(config, env, *, max_iterations=None, stopper=None):
"""`stopper` defaults to a signal-based GracefulStop (SIGINT/SIGTERM), which
only works from the main thread. A host that manages its own lifecycle
@@ -230,7 +253,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
executor = load_executor()
state = AgentState(config.state_db)
client = HubClient(config.hub_url, config.agent_token)
installation_id = state.installation_id() if hasattr(state, "installation_id") else (config.agent_id or "legacy-installation")
installation_id = resolve_installation_id(config, env, state, client)
stopper = stopper if stopper is not None else GracefulStop()
backoff = 1
@@ -266,6 +289,10 @@ def run(config, env, *, max_iterations=None, stopper=None):
backoff = min(backoff * 2, _MAX_BACKOFF_SECONDS)
continue
if not config.agent_jobs_enabled:
time.sleep(config.poll_interval_seconds)
continue
try:
job = client.next_job()
except Exception as error: