fix(config): block generic Copilot ACP controls

This commit is contained in:
unsupportedpastels
2026-08-21 15:13:54 +00:00
committed by Teknium
parent 5425ba14f2
commit c4f376c19a
3 changed files with 81 additions and 3 deletions

View File

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

View File

@@ -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")

View File

@@ -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"]