New request
#2848: Audit other TLS-dependent callers for the #2762 background-thread race
We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.
Follow-up from #2762 / #2827. The maintainer review on #2827 noted:
One thing the PR doesn't touch: existing TLS callers. Other code paths that call
_get_state_db()withoutprofile=(search for the function name across the tree) still rely on TLS-then-process-global. Those callers are correct for request-handler contexts (the cookie middleware sets TLS for them) but would have the same bug if they ever ran on a background thread.
This issue tracks that audit.
Surface
_get_state_db() itself only has internal callers in api/state_sync.py (both now accept profile= after #2827). But the same TLS race shape applies to every caller of get_active_hermes_home() that performs a write on a background thread. A quick grep across api/ turns up:
| File | Lines | What it does | Background-thread risk? |
|---|---|---|---|
api/state_sync.py | 116, 149 | Token usage + title writes | Fixed in #2827 |
api/gateway_watcher.py | 39-53 | _get_state_db_path() returns the DB path for the watcher loop | Likely — watcher runs on its own thread |
api/models.py | 2096, 2734, 3324, 3401 | Model-config writes / reads | Depends on whether any of these run from streaming workers or scheduled jobs |
api/config.py | 266, 2257, 2989 | Config file resolution | Likely request-thread-only, but worth verifying |
api/oauth.py | 92-96, 233, 693, 696 | OAuth credential persistence | Request-thread; safe |
api/onboarding.py | 205-209 | Onboarding state | Request-thread; safe |
What's worth fixing
For each site above, two questions:
- Does it run on a background thread or scheduled job? If yes, the TLS fallback to
get_active_hermes_home()can resolve to the process-global active profile rather than the profile that owns the request — same #2762 shape with different blast radius. - Does the caller have an explicit profile context available? (e.g.
Session.profile,Run.profile, an MCP tool-call context, etc.) If yes, threading it through is the same fix pattern that #2827 used.
Sites that are request-thread-only (cookie middleware sets TLS for them) are correct as-is. No change needed.
Suggested pattern (matches #2827)
def _operation(..., profile: str = None):
if profile is not None:
try:
from api.profiles import (
_resolve_profile_home_for_name,
_PROFILE_ID_RE,
_is_root_profile,
)
if not (_is_root_profile(profile) or _PROFILE_ID_RE.fullmatch(profile)):
logger.warning("op: refusing invalid profile name %r", profile)
return None
hermes_home = Path(_resolve_profile_home_for_name(profile)).expanduser().resolve()
except Exception:
logger.warning("op: could not resolve profile %r — skipping", profile)
return None
else:
try:
from api.profiles import get_active_hermes_home
hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
except Exception:
hermes_home = Path(os.getenv('HERMES_HOME', str(Path.home() / '.hermes')))
The "explicit profile fails loudly + TLS fallback for legacy callers" split keeps the migration backward-compatible (existing callers don't regress; new callers can opt in).
Not in scope here
- Migrating sites that are clearly request-thread-only (oauth.py, onboarding.py, etc.). Those are correct.
- Adding the profile kwarg to public API endpoints — the audit comes first; the migration plan can be discussed once we know which sites actually need it.
How to verify
For each candidate site, after threading profile= through:
- Unit test: pass
profile='X'while TLS is set toY, confirm the write lands in X's home. - Manual test: switch profiles via the cookie selector, trigger the code path, confirm only the active profile's files are touched.
The pattern from tests/test_issue2762_state_sync_profile_kwarg.py (and the parametrized invalid-name regression added in #2827's follow-up commit f1489f50) is reusable.
AI Usage Disclosure
This issue body was drafted with Claude (Opus 4.7) assistance, anchored to the maintainer's review comment on #2827. The audit table is from a fresh grep -rn "get_active_hermes_home" api/ on master at v0.51.124.
Internal audit and hardening of TLS-dependent database callers to prevent the same background-thread race addressed in #2827.
- api/state_sync.py
- api/gateway_watcher.py
- api/models.py
- api/config.py
- api/oauth.py
- api/onboarding.py