Files
hermes-agent/tests/tools/test_skill_env_passthrough.py
Teknium 388b881b33 fix(gateway,tools): per-profile Yuanbao home, env_passthrough allowlist and Slack ignored-channel guard under multiplex
- gateway/platforms/yuanbao.py::AutoSetHomeMiddleware: the first authorized DM
  to a SECONDARY Yuanbao bot wrote YUANBAO_HOME_CHANNEL into os.environ, making
  that tenant's chat the default profile's cron/notification home. The write
  now only happens unscoped; reads go through the scoped reader + config.
- tools/env_passthrough.py::_config_passthrough: one module slot froze the
  first profile's terminal.env_passthrough for every profile's sandbox children;
  keyed by hermes_home_key().
- gateway/run.py::_slack_ignored_channels_from_gateway_config: the runner-level
  fail-safe only had the DEFAULT profile's GatewayConfig, so a secondary Slack
  bot's traffic was judged by the default's ignored list. It now takes the
  source's routed adapter (whose extra is the secondary's own config) and reads
  the env fallback through the scoped gate reader.
2026-09-11 15:29:15 -07:00

140 lines
5.3 KiB
Python

"""Test that skill_view registers required env vars in the passthrough registry."""
import json
import shutil
from pathlib import Path
from unittest.mock import patch
import pytest
import tools.env_passthrough as _ep_mod
from tools.env_passthrough import clear_env_passthrough, is_env_passthrough
# Real optional AgentMail skill — its CLI-first workflow runs `agentmail` through
# the sandboxed terminal, which strips secrets unless the skill declares them.
_AGENTMAIL_SKILL_SRC = (
Path(__file__).resolve().parents[2] / "optional-skills" / "email" / "agentmail"
)
@pytest.fixture(autouse=True)
def _clean_passthrough():
clear_env_passthrough()
_ep_mod._config_passthrough.clear()
yield
clear_env_passthrough()
_ep_mod._config_passthrough.clear()
def _create_skill(tmp_path, name, frontmatter_extra=""):
"""Create a minimal skill directory with SKILL.md."""
skill_dir = tmp_path / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
f"---\n"
f"name: {name}\n"
f"description: Test skill\n"
f"{frontmatter_extra}"
f"---\n\n"
f"# {name}\n\n"
f"Test content.\n"
)
return skill_dir
class TestSkillViewRegistersPassthrough:
def test_available_env_vars_registered(self, tmp_path, monkeypatch):
"""When a skill declares required_environment_variables and the var IS set,
it should be registered in the passthrough."""
_create_skill(
tmp_path,
"test-skill",
frontmatter_extra=(
"required_environment_variables:\n"
" - name: TENOR_API_KEY\n"
" prompt: Enter your Tenor API key\n"
),
)
monkeypatch.setattr(
"tools.skills_tool.SKILLS_DIR", tmp_path
)
# Set the env var so it's "available"
monkeypatch.setenv("TENOR_API_KEY", "test-value-123")
# Patch the secret capture callback to not prompt
with patch("tools.skills_tool._secret_capture_callback", None):
from tools.skills_tool import skill_view
result = json.loads(skill_view(name="test-skill"))
assert result["success"] is True
assert is_env_passthrough("TENOR_API_KEY")
def test_no_env_vars_skill_no_registration(self, tmp_path, monkeypatch):
"""Skills without required_environment_variables shouldn't register anything."""
_create_skill(tmp_path, "simple-skill")
monkeypatch.setattr(
"tools.skills_tool.SKILLS_DIR", tmp_path
)
with patch("tools.skills_tool._secret_capture_callback", None):
from tools.skills_tool import skill_view
result = json.loads(skill_view(name="simple-skill"))
assert result["success"] is True
from tools.env_passthrough import get_all_passthrough
assert len(get_all_passthrough()) == 0
class TestAgentMailKeyPassthrough:
"""The real AgentMail skill must declare AGENTMAIL_API_KEY so a profile-stored
key reaches the sandboxed terminal, while staying usable (self-signup) without
one."""
def _install_real_skill(self, tmp_path, monkeypatch):
dest = tmp_path / "email" / "agentmail"
shutil.copytree(_AGENTMAIL_SKILL_SRC, dest)
monkeypatch.setattr("tools.skills_tool.SKILLS_DIR", tmp_path)
return dest
def test_agentmail_api_key_registered_for_sandbox_when_set(self, tmp_path, monkeypatch):
"""A profile-stored AGENTMAIL_API_KEY must pass through to the terminal
sandbox — otherwise the documented CLI workflow fails authentication."""
self._install_real_skill(tmp_path, monkeypatch)
monkeypatch.setenv("AGENTMAIL_API_KEY", "am_test_key_123")
with patch("tools.skills_tool._secret_capture_callback", None):
from tools.skills_tool import skill_view
result = json.loads(skill_view(name="agentmail"))
assert result["success"] is True
assert is_env_passthrough("AGENTMAIL_API_KEY"), (
"AGENTMAIL_API_KEY was set but not registered for sandbox passthrough — "
"the skill's frontmatter must declare it under required_environment_variables"
)
def test_agentmail_key_is_optional_so_self_signup_still_works(self, tmp_path, monkeypatch):
"""With no key present the skill must NOT be blocked as setup_needed on the
key — the CLI self-signup flow obtains one without a pre-provisioned key."""
self._install_real_skill(tmp_path, monkeypatch)
monkeypatch.delenv("AGENTMAIL_API_KEY", raising=False)
with patch("tools.skills_tool._secret_capture_callback", None):
from tools.skills_tool import skill_view
result = json.loads(skill_view(name="agentmail"))
assert result["success"] is True
# `optional: true` means a missing key must NOT be reported as a blocking
# requirement — that is what keeps the CLI self-signup path viable.
assert "AGENTMAIL_API_KEY" not in result["missing_required_environment_variables"], (
"AGENTMAIL_API_KEY must be declared optional so a missing key does not "
"block the self-signup path"
)
assert result["setup_needed"] is False, (
"an absent optional key must not force the skill into setup_needed"
)