fix(plugins): only prompt for declared capabilities on enable

This commit is contained in:
funky-xamarin
2026-09-20 02:40:26 +08:00
committed by Teknium
parent 969913bb96
commit 09bcf17801
2 changed files with 51 additions and 6 deletions

View File

@@ -1087,8 +1087,9 @@ def _set_plugin_entry_flag(plugin_id: str, key: str, value: bool) -> None:
def cmd_enable(name: str, allow_tool_override: Optional[bool] = None) -> None:
"""Add a plugin to the enabled allow-list (and remove it from disabled).
Non-bundled plugins are asked about the privileged ``allow_tool_override`` grant;
tri-state: ``True``/``False`` skip the prompt, ``None`` asks. Bundled plugins are trusted.
Non-bundled plugins request consent for declared capabilities. The legacy
``allow_tool_override`` grant changes only with an explicit True/False flag;
None leaves it unchanged. Bundled plugins are trusted.
"""
from hermes_cli.relay_plugin_cutover import LEGACY_RELAY_PLUGIN_KEYS, RELAY_PLUGINS_CONFIG_ENV
console = _console()
@@ -1132,10 +1133,10 @@ def cmd_enable(name: str, allow_tool_override: Optional[bool] = None) -> None:
declared_caps = _declared_capabilities_for_key(key)
if declared_caps:
_run_capability_consent(console, key, declared_caps, context="enable")
if allow_tool_override is not None:
_resolve_tool_override_grant(console, key, allow_tool_override)
return
_resolve_tool_override_grant(console, key, allow_tool_override)
# Enabling a plugin is not a request for undeclared privileges. Keep existing
# grants unchanged unless the operator explicitly grants or revokes one.
if allow_tool_override is not None:
_resolve_tool_override_grant(console, key, allow_tool_override)
# ── Capability consent flow ──────────────────────────────────────────────────

View File

@@ -0,0 +1,44 @@
"""Enable asks for declared privileges, not an unsolicited override grant."""
from unittest.mock import MagicMock
import pytest
import yaml
@pytest.mark.parametrize(
"caps,flag,existing,answer,expected,prompts",
[
(None, None, None, "n", None, 0),
([], None, None, "n", None, 0),
(None, True, None, "n", True, 0),
(None, False, True, "n", False, 0),
(None, None, True, "n", True, 0),
(["tools.override"], None, None, "y", True, 1),
],
)
def test_enable_consent(tmp_path, monkeypatch, caps, flag, existing, answer, expected, prompts):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
plugin = tmp_path / "plugins" / "hook-only-probe"
plugin.mkdir(parents=True)
manifest = {"name": "hook-only-probe", "version": "0.1.0", "hooks": ["transform_llm_output"]}
if caps is not None:
manifest["capabilities"] = caps
(plugin / "plugin.yaml").write_text(yaml.safe_dump(manifest), encoding="utf-8")
(plugin / "__init__.py").write_text("def register(ctx):\n pass\n", encoding="utf-8")
initial = {} if existing is None else {
"plugins": {"entries": {"hook-only-probe": {"allow_tool_override": existing}}}
}
(tmp_path / "config.yaml").write_text(yaml.safe_dump(initial), encoding="utf-8")
from hermes_cli import plugins_cmd
console = MagicMock()
console.input.return_value = answer
monkeypatch.setattr(plugins_cmd, "_console", lambda: console)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
plugins_cmd.cmd_enable("hook-only-probe", allow_tool_override=flag)
assert console.input.call_count == prompts
config = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
assert "hook-only-probe" in config["plugins"]["enabled"]
entry = config["plugins"].get("entries", {}).get("hook-only-probe", {})
assert entry.get("allow_tool_override") is expected