fix(config): block generic Copilot ACP controls
This commit is contained in:
committed by
Teknium
parent
5425ba14f2
commit
c4f376c19a
@@ -219,6 +219,9 @@ _ENV_VAR_NAME_DENYLIST: frozenset[str] = frozenset({
|
||||
# MCP catalog trust root. Package-manager wrappers may still provide this
|
||||
# in the process environment; only generic persistence writes are blocked.
|
||||
"HERMES_OPTIONAL_MCPS",
|
||||
# Local ACP subprocess selection. Existing operator/package-manager values
|
||||
# remain readable; generic writers cannot acquire executable/argv authority.
|
||||
"HERMES_COPILOT_ACP_COMMAND", "HERMES_COPILOT_ACP_ARGS",
|
||||
# Hermes security policy / approval-routing context. These remain available
|
||||
# through their dedicated CLI/config/session controls, but a generic
|
||||
# credential writer must not persist them for the next process startup.
|
||||
|
||||
@@ -1095,6 +1095,8 @@ class TestEnvWriteDenylist:
|
||||
"HERMES_CONFIG_PATH",
|
||||
"HERMES_ENV_PATH",
|
||||
"HERMES_OPTIONAL_MCPS",
|
||||
"HERMES_COPILOT_ACP_COMMAND",
|
||||
"HERMES_COPILOT_ACP_ARGS",
|
||||
"HERMES_YOLO_MODE",
|
||||
"HERMES_ACCEPT_HOOKS",
|
||||
"HERMES_REDACT_SECRETS",
|
||||
@@ -1133,6 +1135,8 @@ class TestEnvWriteDenylist:
|
||||
("Path", "PATH"),
|
||||
("Hermes_Yolo_Mode", "HERMES_YOLO_MODE"),
|
||||
("Hermes_Optional_Mcps", "HERMES_OPTIONAL_MCPS"),
|
||||
("Hermes_Copilot_Acp_Command", "HERMES_COPILOT_ACP_COMMAND"),
|
||||
("Hermes_Copilot_Acp_Args", "HERMES_COPILOT_ACP_ARGS"),
|
||||
],
|
||||
)
|
||||
def test_windows_policy_names_are_case_insensitive(self, key, expected):
|
||||
@@ -1154,9 +1158,18 @@ class TestEnvWriteDenylist:
|
||||
assert not _env_line_defines_key(line, "PATH", is_windows=False)
|
||||
|
||||
@pytest.mark.windows_only
|
||||
def test_windows_writer_rejects_mixed_case_protected_name(self):
|
||||
@pytest.mark.parametrize(
|
||||
"protected_key",
|
||||
[
|
||||
"Hermes_Yolo_Mode",
|
||||
"Hermes_Optional_Mcps",
|
||||
"Hermes_Copilot_Acp_Command",
|
||||
"Hermes_Copilot_Acp_Args",
|
||||
],
|
||||
)
|
||||
def test_windows_writer_rejects_mixed_case_protected_name(self, protected_key):
|
||||
with pytest.raises(ValueError, match="denylist"):
|
||||
save_env_value("Hermes_Yolo_Mode", "1")
|
||||
save_env_value(protected_key, "1")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,7 +166,12 @@ def test_catalog_accepts_declared_credential(
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"protected_key",
|
||||
["HERMES_YOLO_MODE", "HERMES_OPTIONAL_MCPS"],
|
||||
[
|
||||
"HERMES_YOLO_MODE",
|
||||
"HERMES_OPTIONAL_MCPS",
|
||||
"HERMES_COPILOT_ACP_COMMAND",
|
||||
"HERMES_COPILOT_ACP_ARGS",
|
||||
],
|
||||
)
|
||||
def test_generic_env_endpoint_rejects_protected_key(
|
||||
client: TestClient,
|
||||
@@ -190,3 +195,60 @@ def test_process_supplied_catalog_root_remains_supported(catalog_env: Path):
|
||||
from hermes_cli.mcp_catalog import get_entry
|
||||
|
||||
assert get_entry("demo") is not None
|
||||
|
||||
|
||||
def test_rejected_copilot_controls_do_not_change_live_resolvers(
|
||||
client: TestClient,
|
||||
catalog_env: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from agent.copilot_acp_client import _resolve_args, _resolve_command
|
||||
|
||||
monkeypatch.setenv("HERMES_COPILOT_ACP_COMMAND", "/opt/trusted/copilot")
|
||||
monkeypatch.setenv("HERMES_COPILOT_ACP_ARGS", "--acp --stdio")
|
||||
expected_command = _resolve_command()
|
||||
expected_args = _resolve_args()
|
||||
|
||||
attempts = {
|
||||
"HERMES_COPILOT_ACP_COMMAND": "/tmp/attacker-command",
|
||||
"HERMES_COPILOT_ACP_ARGS": "--malicious-transport",
|
||||
}
|
||||
for key, value in attempts.items():
|
||||
response = client.put(
|
||||
"/api/env",
|
||||
headers=HEADERS,
|
||||
json={"key": key, "value": value},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
assert _resolve_command() == expected_command
|
||||
assert _resolve_args() == expected_args
|
||||
env_path = catalog_env / ".env"
|
||||
if env_path.exists():
|
||||
env_text = env_path.read_text(encoding="utf-8")
|
||||
assert "/tmp/attacker-command" not in env_text
|
||||
assert "--malicious-transport" not in env_text
|
||||
|
||||
|
||||
def test_preexisting_copilot_controls_remain_usable(
|
||||
catalog_env: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from agent.copilot_acp_client import _resolve_args, _resolve_command
|
||||
from hermes_cli.env_loader import load_hermes_dotenv
|
||||
|
||||
monkeypatch.setenv("HERMES_COPILOT_ACP_COMMAND", "parent-placeholder")
|
||||
monkeypatch.setenv("HERMES_COPILOT_ACP_ARGS", "--parent-placeholder")
|
||||
(catalog_env / ".env").write_text(
|
||||
"HERMES_COPILOT_ACP_COMMAND=/opt/operator/copilot\n"
|
||||
"HERMES_COPILOT_ACP_ARGS=--acp --stdio --operator-mode\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
load_hermes_dotenv(
|
||||
hermes_home=catalog_env,
|
||||
load_external_secrets=False,
|
||||
)
|
||||
|
||||
assert _resolve_command() == "/opt/operator/copilot"
|
||||
assert _resolve_args() == ["--acp", "--stdio", "--operator-mode"]
|
||||
|
||||
Reference in New Issue
Block a user