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
@@ -128,6 +128,7 @@ def test_enroll_with_install_code_alone_creates_agent(hub, release_id):
assert enrolled.status_code == 200
body = enrolled.json()
assert body["agent_id"] and body["agent_token"] and body["hostname_mismatch"] is False
assert body["installation_id"] == uuid
with factory() as session:
agent = session.get(Agent, body["agent_id"])
@@ -416,4 +417,4 @@ def test_revoke_clears_code_and_is_audited(hub, tmp_path):
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid))
assert enr.status == "REVOKED" and enr.install_code_hash is None
events = {e.action for e in audits(factory)}
assert "INSTALLATION_CODE_CREATED" in events and "INSTALLATION_CODE_REVOKED" in events and "INSTALLATION_REVOKED" in events
assert "INSTALLATION_CODE_CREATED" in events and "INSTALLATION_CODE_REVOKED" in events and "INSTALLATION_REVOKED" in events
+58 -1
View File
@@ -5,7 +5,7 @@ from sqlalchemy.pool import StaticPool
from central.app.jobs import claim_next_job, complete_job, create_job, fail_job
from central.app.main import create_app
from central.app.models import Agent, Base, BlockedProduct, Job, Tenant
from central.app.models import Agent, Base, BlockedProduct, Installation, Job, Tenant
def _make_agent(session):
@@ -118,6 +118,63 @@ def test_agent_status_derives_online_offline():
assert "company_id" in body
def test_agent_identity_endpoint_returns_existing_installation_identity_only():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
installation_id = "00000000-0000-4000-8000-000000000222"
heartbeat = client.post(
"/v1/agents/heartbeat",
json={
"installation_id": installation_id,
"hostname": "DESKTOP-B0HL02I",
"agent_version": "1.1.1",
"status": "IDLE",
},
headers=headers,
)
assert heartbeat.status_code == 200
response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
assert response.json() == {
"agent_id": heartbeat.json()["agent_id"],
"installation_id": installation_id,
"hostname": "DESKTOP-B0HL02I",
"version": "1.1.1",
"status": "ONLINE",
}
def test_agent_identity_endpoint_returns_null_installation_without_creating_one():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
body = response.json()
assert body == {
"agent_id": body["agent_id"],
"installation_id": None,
"hostname": None,
"version": None,
"status": "OFFLINE",
}
assert isinstance(body["agent_id"], int) and body["agent_id"] > 0
with Session(engine) as session:
assert session.query(Installation).count() == 0
def test_agent_identity_endpoint_requires_agent_authentication():
client, _ = _client()
response = client.get("/v1/agents/me/identity")
assert response.status_code == 401
def test_job_creation_rejects_unknown_kind():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}