Files
hermes-agent/tests/agent/test_command_secret_source.py
ethernet 59624b7ace tests: replace every bare host skipif with platforms()
The OS lanes are marker-driven: list_os_marked_tests.py picks the files
a lane imports from their platforms() specs and the lane selects with
-m platforms. A test gated with skipif(sys.platform != "win32") is
therefore never imported on the Windows lane and skipped everywhere else
— it runs on no host. skipif(sys.platform == "win32") tests were merely
invisible to the lane bookkeeping, but the rule the tree now follows is
one host marker, never a bare skipif.

Mechanical mapping, semantics preserved: skip-on-Windows → "posix",
skip-off-Windows → "windows", skip-off-Linux → "linux", skip-on-macOS →
"not macos". The former skip reasons stay as trailing comments. A
non-host condition (os.geteuid() == 0) stays a separate skipif beside
the marker, spelled getattr(os, "geteuid", ...) so the decorator still
imports on Windows.

Where the conversion would stack two platforms() marks on one test (the
conftest rejects that at collection) the narrower mark wins:
- test_update_wedged_gateway: the class is already platforms("linux");
  its per-test "needs UNIX sockets" marks were redundant and are gone.
- test_process_registry.TestSystemdCgroupIsolation: the class-level
  skip-on-Windows moves onto the 11 methods that had no host mark; the
  11 platforms("linux") methods keep theirs.
- test_file_ops_single_roundtrip: the two fifo tests drop their
  platforms("linux") in favour of the module's "posix" (mkfifo exists on
  macOS; both tests already skip when it does not).
- test_linux_desktop_entry / test_gateway_job_teardown_live: duplicate
  or wider marks removed.
2026-09-21 19:18:15 -04:00

222 lines
7.8 KiB
Python

"""E2E tests for the ``command`` secret source.
These exercise the REAL resolution path: real helper shell scripts written
to a temp dir (chmod +x), real ``/bin/sh -c`` subprocesses, and a real temp
HERMES_HOME with a config.yaml routing ``secrets.provider: command`` through
``hermes_cli.env_loader._apply_external_secret_sources``.
Security invariants under test (ported from the desktop TS provider):
* the requested key travels ONLY via the ``HERMES_SECRET_KEY`` env var —
never interpolated into the shell string (hostile key names are inert);
* hard timeout + degrade-to-empty on every failure mode, never raise;
* failure logging carries structured fields only — never the command
string or any secret value.
NOTE: tests assert on key NAMES, lengths, and presence — never log secret
values themselves.
"""
from __future__ import annotations
import os
import stat
import sys
import time
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from agent.secret_sources.command import ( # noqa: E402
CommandSource,
_run_helper,
unquote_dotenv_value,
)
from agent.secret_sources.base import ( # noqa: E402
reset_source_environment,
set_source_environment,
)
from hermes_cli import env_loader # noqa: E402
pytestmark = pytest.mark.platforms("posix") # the command secret provider is POSIX-only
def _write_helper(tmp_path: Path, body: str, name: str = "helper.sh") -> Path:
"""Write a real executable helper script and return its path."""
script = tmp_path / name
script.write_text("#!/bin/sh\n" + body + "\n", encoding="utf-8")
script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
return script
def test_profile_helper_does_not_inherit_process_secret(monkeypatch):
monkeypatch.setenv("LEAK_CANARY", "global-secret")
token = set_source_environment({"PROFILE_ONLY": "profile-value"})
try:
output = _run_helper(
'printf "%s|%s" "${LEAK_CANARY-unset}" "$PROFILE_ONLY"',
"",
1.0,
1024,
)
finally:
reset_source_environment(token)
assert output == "unset|profile-value"
@pytest.fixture(autouse=True)
def _clean_env(monkeypatch):
"""Each test starts with a clean source map, applied-home guard, and no
leftover test keys in os.environ."""
env_loader._SECRET_SOURCES.clear()
env_loader.reset_secret_source_cache()
for key in ("CMDTEST_API_KEY", "CMDTEST_TOKEN", "CMDTEST_OTHER_KEY"):
monkeypatch.delenv(key, raising=False)
yield
env_loader._SECRET_SOURCES.clear()
env_loader.reset_secret_source_cache()
for key in ("CMDTEST_API_KEY", "CMDTEST_TOKEN", "CMDTEST_OTHER_KEY"):
os.environ.pop(key, None)
# ---------------------------------------------------------------------------
# Parsing semantics (pure functions, mirroring the TS parseSecretOutput)
# ---------------------------------------------------------------------------
def test_unquote_strips_one_layer_of_matching_quotes():
assert unquote_dotenv_value('"abc"') == "abc"
assert unquote_dotenv_value("'abc'") == "abc"
assert unquote_dotenv_value('""') == ""
assert unquote_dotenv_value('"') == '"' # lone quote left intact
assert unquote_dotenv_value(" plain ") == "plain"
# ---------------------------------------------------------------------------
# Real-subprocess resolution
# ---------------------------------------------------------------------------
def test_helper_stdout_is_returned_verbatim(tmp_path):
helper = _write_helper(tmp_path, "printf 'sk-test-bare-12345'")
value = _run_helper(str(helper), "CMDTEST_API_KEY", 3.0, 1024)
assert value == "sk-test-bare-12345"
def test_timeout_kills_hung_helper_and_degrades_to_empty(tmp_path):
helper = _write_helper(tmp_path, "sleep 30")
start = time.monotonic()
value = _run_helper(str(helper), "CMDTEST_API_KEY", 2.0, 1024)
elapsed = time.monotonic() - start
assert value is None
assert elapsed < 6.0, f"helper not killed within the bound (took {elapsed:.1f}s)"
def test_failure_logging_never_leaks_command_or_secret(tmp_path, capfd):
secret_value = "sk-super-secret-value-do-not-log"
helper = _write_helper(
tmp_path,
f"echo '{secret_value}' >&2\nexit 7",
name="my-distinctive-helper-name.sh",
)
value = _run_helper(str(helper), "CMDTEST_API_KEY", 3.0, 1024)
assert value is None
captured = capfd.readouterr()
combined = captured.out + captured.err
# Structured fields only — never the command string, the helper's
# stderr, or any secret value.
assert "my-distinctive-helper-name" not in combined
assert secret_value not in combined
assert "code=7" in combined # the structured field IS logged
def test_fetch_parses_dotenv_blob(tmp_path):
helper = _write_helper(
tmp_path,
"printf 'CMDTEST_API_KEY=sk-applied\\nCMDTEST_TOKEN=tok-applied\\n'",
)
result = CommandSource().fetch({"enabled": True, "command": str(helper)}, tmp_path)
assert result.error is None
assert result.secrets == {"CMDTEST_API_KEY": "sk-applied", "CMDTEST_TOKEN": "tok-applied"}
# ---------------------------------------------------------------------------
# Dispatch E2E through env_loader against a real temp HERMES_HOME
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def _clean_registry():
from agent.secret_sources import registry
registry._reset_registry_for_tests()
yield
registry._reset_registry_for_tests()
def test_registry_command_source_applies_and_records_source(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
helper = _write_helper(
tmp_path, "printf 'CMDTEST_API_KEY=sk-dispatch\\nCMDTEST_TOKEN=tok-dispatch\\n'"
)
(tmp_path / "config.yaml").write_text(
"secrets:\n"
" command:\n"
" enabled: true\n"
f" command: {helper}\n",
encoding="utf-8",
)
env_loader._apply_external_secret_sources(tmp_path)
assert os.environ.get("CMDTEST_API_KEY") == "sk-dispatch"
assert env_loader.get_secret_source("CMDTEST_API_KEY") == "command"
assert env_loader.get_secret_source("CMDTEST_TOKEN") == "command"
assert (
env_loader.format_secret_source_suffix("CMDTEST_API_KEY")
== " (from Command helper)"
)
def test_registry_status_line_printed_once_per_home(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
helper = _write_helper(tmp_path, "printf 'CMDTEST_API_KEY=sk-once\\n'")
(tmp_path / "config.yaml").write_text(
"secrets:\n command:\n enabled: true\n"
f" command: {helper}\n",
encoding="utf-8",
)
for _ in range(3): # idempotency guard: only the first call does work
env_loader._apply_external_secret_sources(tmp_path)
err = capsys.readouterr().err
assert err.count("Command helper: applied 1 secret") == 1
def test_registry_failing_helper_does_not_block_startup(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text(
"secrets:\n command:\n enabled: true\n command: exit 9\n",
encoding="utf-8",
)
# Must not raise — config/helper errors never block startup.
env_loader._apply_external_secret_sources(tmp_path)
assert env_loader.get_secret_source("CMDTEST_API_KEY") is None
def test_registry_helper_error_prints_remediation(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text(
"secrets:\n command:\n enabled: true\n command: ''\n",
encoding="utf-8",
)
env_loader._apply_external_secret_sources(tmp_path)
err = capsys.readouterr().err
assert "secrets.command.command" in err