fix(config): keep v41 off the unversioned ladder and drop the stamp read guard

v41 walks every profile SOUL.md and deletes any "## Messaging other
agents" section on a bare heading match. A config.yaml with no
_config_version says nothing about where that SOUL text came from, so an
unstamped current config must not authorize rewriting a user-owned file;
the versioned 40->41 path is unchanged. The unversioned regression test
now seeds a user-authored section under that heading and asserts it
survives.

has_version_stamp() loses its bare except: migrate_config() already
called check_config_version(raise_on_parse_error=True) and the docker
script returns early on the (latest, latest) a parse failure yields, so
the raw read cannot fail at this point.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
This commit is contained in:
kshitijk4poor
2026-09-24 13:57:46 +05:30
committed by kshitij
parent a88bef98b2
commit 49aaa52d33
2 changed files with 10 additions and 6 deletions

View File

@@ -39,11 +39,9 @@ def support_floor_message() -> str:
def has_version_stamp() -> bool:
"""Whether config.yaml carries a ``_config_version`` key. ``check_config_version()`` reads a
missing one as 0, but such a file is never-stamped current-schema content, not an ancient
install: the floor must not refuse it and only :data:`LEGACY_KEY_STEPS` may run on it."""
try:
return "_config_version" in _cfg().read_user_config_raw()
except Exception:
return False
install: the floor must not refuse it and only :data:`LEGACY_KEY_STEPS` may run on it.
Callers have already gone through ``check_config_version()``, so the read cannot fail here."""
return "_config_version" in _cfg().read_user_config_raw()
def _cfg():
@@ -772,7 +770,9 @@ MIGRATIONS: Tuple[Tuple[int, Callable[[Dict[str, Any], bool], None]], ...] = (
#: cli-config.yaml.example; targeted writers never stamp), so it gets only these: every other step
#: decides by a value or an absence that, in such a file, is the user's own choice. v13 is left
#: out: it clears OPENAI_MODEL from .env, a generic name Hermes never reads but the user's tools may.
LEGACY_KEY_STEPS = frozenset({12, 14, 16, 17, 29, 33, 38, 39, 41, 42, 43, 46})
#: v41 is left out too: it rewrites profile SOUL.md on a heading match, an artifact whose
#: provenance the config stamp says nothing about.
LEGACY_KEY_STEPS = frozenset({12, 14, 16, 17, 29, 33, 38, 39, 42, 43, 46})
def run_migrations(

View File

@@ -54,6 +54,9 @@ def test_update_keeps_user_values_of_an_unversioned_config_and_migrates_legacy_k
plugin = hermes_home / "plugins" / "notes-helper"
plugin.mkdir(parents=True)
(plugin / "plugin.yaml").write_text("name: notes-helper\nversion: 0.1.0\n", encoding="utf-8")
# User-authored SOUL.md section that happens to carry the v41 heading.
soul = "# Me\n\n## Messaging other agents\nmy own notes\n\n## Prefs\nkeep\n"
(hermes_home / "SOUL.md").write_text(soul, encoding="utf-8")
assert persist_personality("kawaii")
for key, value in USER_CHOICES.items():
set_config_value(key, value)
@@ -66,6 +69,7 @@ def test_update_keeps_user_values_of_an_unversioned_config_and_migrates_legacy_k
assert "summary_model" not in after["compression"]
assert _at(after, "auxiliary.compression.model") == _at(before, "compression.summary_model")
assert after["_config_version"] == DEFAULT_CONFIG["_config_version"]
assert (hermes_home / "SOUL.md").read_text(encoding="utf-8") == soul
def test_config_seeded_from_the_template_reads_as_current(hermes_home):