Files
Teknium 76f2fdb794 fix(cache): a rotated key, token or same-size edit is never served stale from a file cache
Every cache keyed on a file's stat signature had the bug the config caches had: a same-size
in-place rewrite in one coarse timestamp tick keeps mtime, ctime, size and inode, so the older
parse was served for the rest of the process. Move the helper to utils.file_cache_version
(file_signature + content digest while the file is under 2 s old; always_digest for signatures
compared much later, where the racy digest going empty would read as a change) and key every
cache that can hold a secret, token, pairing/allowlist, route or config on it:

- auth.py: global auth-store memo, Nous auth-status memo (was mtime only)
- gateway/pairing.py approved-users cache (a revocation kept access)
- run_profile_reconcile: served-profile signature (always_digest: a false change reconnects
  adapters) and the hook-consent signature
- webhook dynamic routes (route HMAC secrets), Feishu comment pairing/rules cache (mtime only)
- MCP OAuth tokens-file reload check (mtime only; always_digest: a reload tears the session down)
- config parse-failure record (always_digest), named-custom-provider memo, model aliases,
  gateway.standalone memo, profile model read, MoA preset, agent raw config,
  tui_gateway raw config cache, onboarding due stamp, free-tier inventory stamp,
  provider model-list credential fingerprint, git safe.directory key, skills prompt
  manifest

config_cache_version takes its stat through config_version, so its errors and seams are unchanged.
Left alone: the tool-definition cache key and the platform manifest stamp (a safe worker must
never open config.yaml or a plugin file; the MoA preset cache is skipped in one instead) and the
CLI MCP watcher seed (pinned to file_signature by a main test).
2026-10-27 12:57:09 -07:00

46 lines
1.8 KiB
Python

"""Heal mixed ``sys.modules`` after an in-place checkout update.
Pre-reexec updaters (Hermes ≤ v2026.9.14) purged only package prefixes
(``hermes_cli``, ``gateway``, ``tools``, ``tui_gateway``, ``agent``) and left
root modules like ``utils`` cached in the updater process. The post-pull
gateway-restart phase then imports new ``hermes_cli.gateway`` /
``hermes_cli.config`` into that process; those need symbols the stale
``utils`` lacks (``file_signature``), and ``hermes update`` exits 1 with
``gateway auto-restart failed: cannot import name 'file_signature' from
'utils'``.
Post-swap hand-off (``hermes_cli.update_handoff``) makes this class dead for
updaters that already include it. This module is the bridge for the one
upgrade from a pre-handoff release onto a tree that needs new root symbols:
freshly imported ``hermes_cli`` code drops the incomplete cache before
importing ``utils``.
"""
from __future__ import annotations
import sys
from typing import Mapping, Sequence
# Root modules the narrow purge left behind, keyed by attributes that must
# exist on the on-disk copy after this release. Extend when a new root-level
# symbol would otherwise break the pre-handoff upgrade path.
_ROOT_MODULE_REQUIRED_ATTRS: dict[str, tuple[str, ...]] = {
"utils": ("file_signature", "file_cache_version"),
}
def drop_stale_root_modules(
required: Mapping[str, Sequence[str]] | None = None,
) -> list[str]:
"""Drop cached root modules missing required attrs. Returns dropped names."""
checks = _ROOT_MODULE_REQUIRED_ATTRS if required is None else required
dropped: list[str] = []
for name, attrs in checks.items():
mod = sys.modules.get(name)
if mod is None:
continue
if any(not hasattr(mod, attr) for attr in attrs):
sys.modules.pop(name, None)
dropped.append(name)
return dropped