From e6395b908a20b87bac75b7ef7506867b8874ba77 Mon Sep 17 00:00:00 2001 From: obasilakis Date: Tue, 6 Oct 2026 10:08:01 +0200 Subject: [PATCH] fix(setup): deploy the system agent once /setup has created the admin (#3237) The system agent's only deploy attempt ran at backend startup. On a fresh install that is before /setup creates the admin it is owned by, so it failed with "Admin user 'admin' not found" and stayed missing until the backend next restarted. /setup now schedules system_agent_service.ensure_deployed() as a background task after the first-run seed pass. It never raises (setup has already succeeded; the next start retries), and ensure_deployed is idempotent, so the startup path stays a no-op afterwards. Verified on an AWS instance from the v1.0.0-aws.2 AMI: with agent-trinity-system removed, the task recreated it (NanoCpus capped to the host's 2 CPUs). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/routers/setup.py | 18 +++++++ .../test_3237_system_agent_after_setup.py | 51 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/unit/test_3237_system_agent_after_setup.py diff --git a/src/backend/routers/setup.py b/src/backend/routers/setup.py index 3f7dd9ea68..447427ddb0 100644 --- a/src/backend/routers/setup.py +++ b/src/backend/routers/setup.py @@ -67,6 +67,7 @@ from database import db from dependencies import hash_password from services.system_seed_service import ensure_first_run_seeded +from services.system_agent_service import system_agent_service from services.operator_intake_service import submit_operator_intake from utils.password_validation import validate_password_strength, PASSWORD_REQUIREMENTS_MESSAGE from utils.admin_identity import admin_username, is_usable_password_hash @@ -159,6 +160,16 @@ async def get_setup_status(): } +async def _deploy_system_agent() -> None: + """Deploy the system agent after setup (#3237). Never raises: setup has + already succeeded, and the next backend start retries.""" + try: + result = await system_agent_service.ensure_deployed() + logger.info("System agent after setup: %s - %s", result.get("action"), result.get("message")) + except Exception as e: # noqa: BLE001 + logger.error("System agent deploy after setup failed: %s", e) + + @router.post("/admin-password") async def set_admin_password( data: SetAdminPasswordRequest, request: Request, background_tasks: BackgroundTasks @@ -327,6 +338,13 @@ async def set_admin_password( # never double-provision or surprise an established fleet. background_tasks.add_task(ensure_first_run_seeded) + # #3237: the system agent needs the admin as its owner. Its only other + # deploy attempt runs at backend startup, which on a fresh install is before + # this endpoint, so without this it stayed missing until the next restart. + # Same background-task rule as the seed pass above; ensure_deployed is + # idempotent, so a later restart stays a no-op. + background_tasks.add_task(_deploy_system_agent) + # Operator intake (trinity-enterprise#38): only on affirmative consent. # Scheduled as a background task so it runs AFTER the response is sent — it # can never delay or break setup. The service is idempotent (once-per-install) diff --git a/tests/unit/test_3237_system_agent_after_setup.py b/tests/unit/test_3237_system_agent_after_setup.py new file mode 100644 index 0000000000..6205e15761 --- /dev/null +++ b/tests/unit/test_3237_system_agent_after_setup.py @@ -0,0 +1,51 @@ +""" +#3237: the system agent is deployed once /setup has created the admin. + +The only other deploy attempt runs at backend startup, which on a fresh install +is before the admin exists, so it fails with "Admin user 'admin' not found" and +nothing retried it until the backend next restarted. + +Module: src/backend/routers/setup.py +""" + +import asyncio + +import pytest +from fastapi import BackgroundTasks + +from test_setup_operator_profile import _get_setup, _req, _run, _task_funcs, patched # noqa: F401 + +pytestmark = pytest.mark.unit + + +def test_setup_schedules_system_agent_deploy(patched): + bg = BackgroundTasks() + _run(_req(), bg) + setup = _get_setup() + funcs = _task_funcs(bg) + assert setup._deploy_system_agent in funcs + # After the seed pass: Cornelius and the starter agents come first. + assert funcs.index(setup.ensure_first_run_seeded) < funcs.index(setup._deploy_system_agent) + + +def test_system_agent_deploy_failure_never_raises(monkeypatch): + setup = _get_setup() + + async def boom(): + raise RuntimeError("docker unreachable") + + monkeypatch.setattr(setup.system_agent_service, "ensure_deployed", boom) + asyncio.run(setup._deploy_system_agent()) + + +def test_system_agent_deploy_calls_ensure_deployed(monkeypatch): + setup = _get_setup() + calls = [] + + async def ok(): + calls.append(1) + return {"action": "created", "status": "success", "message": "ok"} + + monkeypatch.setattr(setup.system_agent_service, "ensure_deployed", ok) + asyncio.run(setup._deploy_system_agent()) + assert calls == [1]