From 7876d183c94f7ba91c731cf7495faa8b2c6a547a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:06:51 -0700 Subject: [PATCH] fix(approval): recover legacy list values without character grants Recover legacy stringified lists with a warning. Reject malformed shapes and nonstring members without admitting approvals or rewriting user config on read. Fixes #104779 Co-authored-by: liuhao1024 --- tests/tools/test_allowlist_legacy_config.py | 21 +++++++++++++++++++++ tools/approval.py | 18 +++++++++++++++++- website/docs/user-guide/security.md | 6 ++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/tools/test_allowlist_legacy_config.py diff --git a/tests/tools/test_allowlist_legacy_config.py b/tests/tools/test_allowlist_legacy_config.py new file mode 100644 index 0000000000..ff91f28c36 --- /dev/null +++ b/tests/tools/test_allowlist_legacy_config.py @@ -0,0 +1,21 @@ +import yaml +from tools import approval + + +def test_legacy_string_allowlist_recovers_only_string_lists(tmp_path, monkeypatch, caplog): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + description = "script execution via -e/-c flag" + for value in ([description], yaml.safe_dump([description])): + (tmp_path / "config.yaml").write_text(yaml.safe_dump({"command_allowlist": value})) + assert approval.load_permanent_allowlist() == {description} + assert approval.is_approved("probe", description) + assert "command_allowlist" in caplog.text + + +def test_malformed_allowlist_does_not_grant_approval(tmp_path, monkeypatch, caplog): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + for value in ("plain text", "[bad", {"not": "a list"}, [True, "candidate"], "[true, candidate]", 42): + (tmp_path / "config.yaml").write_text(yaml.safe_dump({"command_allowlist": value})) + assert approval.load_permanent_allowlist() == set() + assert not approval.is_approved("probe", "candidate") + assert "command_allowlist" in caplog.text diff --git a/tools/approval.py b/tools/approval.py index b9f0dbf606..503ab01d8f 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -330,7 +330,23 @@ def load_permanent_allowlist() -> set: try: from hermes_cli.config import load_config_readonly config = load_config_readonly() - patterns = set(config.get("command_allowlist", []) or []) + raw = config.get("command_allowlist") + legacy = isinstance(raw, str) + if legacy: + # Old config-set versions serialized list values as scalar strings. + import yaml + try: + raw = yaml.safe_load(raw) + except yaml.YAMLError: + raw = False + if raw is None and not legacy: + raw = [] + if not isinstance(raw, list) or any(not isinstance(item, str) for item in raw): + logger.warning("Ignoring malformed command_allowlist; configure a list of strings.") + return set() + if legacy: + logger.warning("Recovered legacy string command_allowlist; re-save it as a list of strings.") + patterns = set(raw) if patterns: load_permanent(patterns) return patterns diff --git a/website/docs/user-guide/security.md b/website/docs/user-guide/security.md index 65dbf32871..c07cbbbfcc 100644 --- a/website/docs/user-guide/security.md +++ b/website/docs/user-guide/security.md @@ -262,6 +262,12 @@ command_allowlist: These patterns are loaded at startup and silently approved in all future sessions. +The setting must be a list of strings. Legacy installs that stored a list as a +quoted YAML/JSON string recover that list at load time and log a warning to +re-save it with `hermes config edit`. Other malformed values are ignored with +a warning; they never become per-character approvals. Loading does not rewrite +your configuration file. + :::tip Use `hermes config edit` to review or remove patterns from your permanent allowlist. :::