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. :::