github →

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.

Issue
enhancementinvestigationtracking

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() without profile= (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:

FileLinesWhat it doesBackground-thread risk?
api/state_sync.py116, 149Token usage + title writesFixed in #2827
api/gateway_watcher.py39-53_get_state_db_path() returns the DB path for the watcher loopLikely — watcher runs on its own thread
api/models.py2096, 2734, 3324, 3401Model-config writes / readsDepends on whether any of these run from streaming workers or scheduled jobs
api/config.py266, 2257, 2989Config file resolutionLikely request-thread-only, but worth verifying
api/oauth.py92-96, 233, 693, 696OAuth credential persistenceRequest-thread; safe
api/onboarding.py205-209Onboarding stateRequest-thread; safe

What's worth fixing

For each site above, two questions:

  1. 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.
  2. 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 to Y, 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.

Assessmentadvisory
chore●● medium80% confidence

Internal audit and hardening of TLS-dependent database callers to prevent the same background-thread race addressed in #2827.

Likely files
  • api/state_sync.py
  • api/gateway_watcher.py
  • api/models.py
  • api/config.py
  • api/oauth.py
  • api/onboarding.py
Create the request

This opens a fresh agent run and a draft PR for issue #2848.

Cancel