From bf0d8fed8e349787dd3a09d37ff2192bca960d5f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:51:08 -0700 Subject: [PATCH] fix(config): v32 migration flips baked-in verify_on_stop=true to false (#54740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first ship of verify-on-stop (config v30) defaulted DEFAULT_CONFIG agent.verify_on_stop to a literal True, and migrate_config persists defaults with strip_defaults=False — so every install that updated through v30 had verify_on_stop: true written into config.yaml as a literal. The v30->v31 migration only flipped missing/'auto' values to false and deliberately preserved an explicit bool, so it skipped that entire population and left verify-on-stop ON for everyone who had updated. A literal true was never a user choice: the feature had no off-switch worth setting it against until v31 introduced one, so a true persisted before v32 is always the old machine default. v32 migration flips a literal true -> false once, for both v30 (skipped v31) and v31 (preserved-by-bug) installs. A true the user sets AFTER v32 is a deliberate opt-in and is never touched. --- hermes_cli/config.py | 30 +++++++++++++++++++++++++++++- tests/hermes_cli/test_config.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index c21b01b74f5..731397ba13a 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3013,7 +3013,7 @@ DEFAULT_CONFIG = { # Config schema version - bump this when adding new required fields - "_config_version": 31, + "_config_version": 32, } # ============================================================================= @@ -5462,6 +5462,34 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A "surface-aware behavior." ) + # ── Version 31 → 32: flip the BAKED-IN literal true to OFF (one-time) ── + # The v30→v31 flip above only caught missing/"auto" values. But the very + # first ship of verify-on-stop (config v30, commit 2f1a47b90) defaulted + # DEFAULT_CONFIG["agent"]["verify_on_stop"] to a literal True, and + # migrate_config persists defaults with strip_defaults=False — so every + # install that updated through v30 got `verify_on_stop: true` written into + # config.yaml as a literal. v31's guard deliberately preserves an explicit + # bool, so it skipped that whole population and left them ON. That literal + # true was never a user choice: the feature had no off-switch worth setting + # it against until v31 introduced one, so a true persisted before v32 is + # always the old machine default. Flip it off once here. A true the user + # sets AFTER v32 (config already at version 32) is never touched. + if current_ver < 32: + config = read_raw_config() + raw_agent = config.get("agent") + if isinstance(raw_agent, dict) and raw_agent.get("verify_on_stop") is True: + raw_agent["verify_on_stop"] = False + config["agent"] = raw_agent + save_config(config, strip_defaults=False) + results["config_added"].append("agent.verify_on_stop=false") + if not quiet: + print( + " ✓ Turned off verify-on-stop (agent.verify_on_stop: false) — " + "the old default was written into your config as a literal " + "true. Set it to true again to re-enable, or \"auto\" for the " + "legacy surface-aware behavior." + ) + # ── Post-migration: disable exfiltration-shaped MCP stdio entries ── # Users can hand-edit mcp_servers, and older installs may already contain a # malicious entry. Preserve the stanza for auditability but mark it diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 266ac26d1fa..b830975e318 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -1365,11 +1365,40 @@ class TestVerifyOnStopMigration: raw = yaml.safe_load((tmp_path / "config.yaml").read_text()) assert raw["agent"]["verify_on_stop"] is False - def test_explicit_true_preserved(self, tmp_path): + def test_pre_v32_literal_true_flipped_to_false(self, tmp_path): + # The first ship of verify-on-stop baked a literal `true` into configs + # as the silent default (config v30). It was never a user choice, so the + # v31→v32 migration flips it off. v31's block preserved it (the bug this + # fixes); v32 catches the whole stranded population. with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}): self._write(tmp_path, "_config_version: 30\nagent:\n verify_on_stop: true\n") migrate_config(interactive=False, quiet=True) raw = yaml.safe_load((tmp_path / "config.yaml").read_text()) + assert raw["agent"]["verify_on_stop"] is False + + def test_v31_literal_true_flipped_to_false(self, tmp_path): + # Teknium's case: a v30 install that already ran the v31 migration kept + # its baked-in literal `true` (v31 preserved explicit bools). v32 flips + # it off. + with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}): + self._write(tmp_path, "_config_version: 31\nagent:\n verify_on_stop: true\n") + migrate_config(interactive=False, quiet=True) + raw = yaml.safe_load((tmp_path / "config.yaml").read_text()) + assert raw["agent"]["verify_on_stop"] is False + + def test_post_v32_explicit_true_preserved(self, tmp_path): + # A `true` the user sets AFTER v32 (config already at current version) is + # a deliberate opt-in and must never be flipped. + from hermes_cli.config import DEFAULT_CONFIG + + with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}): + self._write( + tmp_path, + f"_config_version: {DEFAULT_CONFIG['_config_version']}\n" + "agent:\n verify_on_stop: true\n", + ) + migrate_config(interactive=False, quiet=True) + raw = yaml.safe_load((tmp_path / "config.yaml").read_text()) assert raw["agent"]["verify_on_stop"] is True def test_explicit_false_preserved(self, tmp_path):