- 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.
140 lines
5.3 KiB
Python
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"
|
|
)
|