From cfe6b4c25f70af5f926dd3a10cc43ff88cc47ca0 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 16 Jun 2026 22:53:13 -0400 Subject: [PATCH] =?UTF-8?q?fix(host=5Fagent):=20deploy/provision=20500=20?= =?UTF-8?q?=E2=80=94=20double-begin=20on=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch_scope_targets() runs a SELECT that autobegins the session transaction (SQLAlchemy 2.0), so the following `async with db.begin()` raised "A transaction is already begun on this Session" → 500 on both /plugins/host_agent/deploy and /provision. Mint registrations directly into the autobegun transaction, build the inventory while the ORM objects are still live, then await db.commit(). Escaped CI because unit tests use a mock db_sessionmaker; no integration test exercises these routes (follow-up noted in Scribe issue #884). Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/host_agent/routes.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/plugins/host_agent/routes.py b/plugins/host_agent/routes.py index ab88034..0519873 100644 --- a/plugins/host_agent/routes.py +++ b/plugins/host_agent/routes.py @@ -731,17 +731,19 @@ async def deploy_via_ansible(): targets = await fetch_scope_targets(db, scope) if not targets: return _error(400, "no_targets", "No Ansible targets in that scope") + # The read above autobegins the session transaction (SQLAlchemy 2.0), so + # mint into it directly and commit — a nested db.begin() would double-begin. tokens: dict[str, str] = {} - async with db.begin(): - for t in targets: - host = await _ensure_host_for_target(db, t) - tokens[t.name] = await _mint_registration_token(db, host) - inv = generate_inventory(targets) + for t in targets: + host = await _ensure_host_for_target(db, t) + tokens[t.name] = await _mint_registration_token(db, host) + inv = generate_inventory(targets) # built while ORM objects are still live for name, tok in tokens.items(): hv = inv["_meta"]["hostvars"].setdefault(name, {}) hv["steward_url"] = url hv["steward_token"] = tok inventory_content = _json.dumps(inv) + await db.commit() actor_role = UserRole(session.get("user_role", "viewer")) run, _source, err = await invoke_capability( @@ -805,12 +807,13 @@ async def provision_via_ansible(): targets = await fetch_scope_targets(db, scope) if not targets: return _error(400, "no_targets", "No Ansible targets in that scope") + # The read above autobegins the session transaction (SQLAlchemy 2.0), so + # mint into it directly and commit — a nested db.begin() would double-begin. tokens: dict[str, str] = {} - async with db.begin(): - for t in targets: - host = await _ensure_host_for_target(db, t) - tokens[t.name] = await _mint_registration_token(db, host) - inv = generate_inventory(targets) + for t in targets: + host = await _ensure_host_for_target(db, t) + tokens[t.name] = await _mint_registration_token(db, host) + inv = generate_inventory(targets) # built while ORM objects are still live for name, tok in tokens.items(): hv = inv["_meta"]["hostvars"].setdefault(name, {}) hv["steward_url"] = url @@ -821,6 +824,7 @@ async def provision_via_ansible(): hv["steward_pubkey"] = pubkey hv["steward_user"] = steward_user inventory_content = _json.dumps(inv) + await db.commit() actor_role = UserRole(session.get("user_role", "viewer")) run, _source, err = await invoke_capability(