fix(kanban-dashboard): use context-local board pin in specify/decompose endpoints

The dashboard specify and decompose endpoints run as sync FastAPI threadpool
handlers and pinned the active board by mutating the process-global
HERMES_KANBAN_BOARD env var. Two concurrent requests for different boards
race on that shared global and cross-write — the same bug class as the CLI
path (#38323), now using the scoped_current_board() contextvar introduced by
the CLI fix.
This commit is contained in:
teknium
2026-06-04 06:13:56 -07:00
committed by Teknium
parent 081694c111
commit 62f0cfd902

View File

@ -38,7 +38,6 @@ from __future__ import annotations
import asyncio
import json
import logging
import os
import sqlite3
import time
from dataclasses import asdict
@ -1620,10 +1619,12 @@ def specify_task_endpoint(
"""
board = _resolve_board(board)
# Pin the board for the duration of this call so the specifier module
# (which calls ``kb.connect()`` with no args) hits the right DB.
prev_env = os.environ.get("HERMES_KANBAN_BOARD")
try:
os.environ["HERMES_KANBAN_BOARD"] = board or kanban_db.DEFAULT_BOARD
# (which calls ``kb.connect()`` with no args) hits the right DB. Use a
# context-local override rather than mutating the process-global
# HERMES_KANBAN_BOARD env var — this endpoint runs in FastAPI's
# threadpool, so two concurrent requests for different boards would
# otherwise race on the shared env var and cross-write (issue #38323).
with kanban_db.scoped_current_board(board or kanban_db.DEFAULT_BOARD):
# Import lazily so a missing auxiliary client at import time
# doesn't break plugin load.
from hermes_cli import kanban_specify # noqa: WPS433 (intentional)
@ -1632,11 +1633,6 @@ def specify_task_endpoint(
task_id,
author=(payload.author or None),
)
finally:
if prev_env is None:
os.environ.pop("HERMES_KANBAN_BOARD", None)
else:
os.environ["HERMES_KANBAN_BOARD"] = prev_env
return {
"ok": bool(outcome.ok),
@ -2233,19 +2229,16 @@ def decompose_task_endpoint(
can take minutes on reasoning models.
"""
board = _resolve_board(board)
prev_env = os.environ.get("HERMES_KANBAN_BOARD")
try:
os.environ["HERMES_KANBAN_BOARD"] = board or kanban_db.DEFAULT_BOARD
# Context-local board pin (see specify endpoint above): this sync
# endpoint runs in FastAPI's threadpool, so mutating the process-global
# HERMES_KANBAN_BOARD env var would let concurrent requests for
# different boards race and cross-write (issue #38323).
with kanban_db.scoped_current_board(board or kanban_db.DEFAULT_BOARD):
from hermes_cli import kanban_decompose # noqa: WPS433 (intentional)
outcome = kanban_decompose.decompose_task(
task_id,
author=(payload.author or None),
)
finally:
if prev_env is None:
os.environ.pop("HERMES_KANBAN_BOARD", None)
else:
os.environ["HERMES_KANBAN_BOARD"] = prev_env
return {
"ok": bool(outcome.ok),