We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/tylerburleigh/foundry-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for layered config file loading hierarchy."""
import os
from pathlib import Path
from unittest.mock import patch
import pytest
from foundry_mcp.config.server import ServerConfig
class TestConfigHierarchy:
"""Test layered configuration loading (XDG -> home -> project -> env)."""
@pytest.fixture
def xdg_config_content(self):
"""XDG config directory content (system-wide user defaults)."""
return """
[logging]
level = "NOTSET"
structured = true
[tools]
disabled_tools = ["environment"]
"""
@pytest.fixture
def home_config_content(self):
"""Home directory config content (user defaults)."""
return """
[logging]
level = "DEBUG"
structured = false
[tools]
disabled_tools = ["health"]
"""
@pytest.fixture
def project_config_content(self):
"""Project directory config content (project overrides)."""
return """
[logging]
level = "WARNING"
[workspace]
specs_dir = "./my-specs"
"""
@pytest.fixture
def legacy_config_content(self):
"""Legacy .foundry-mcp.toml content."""
return """
[logging]
level = "ERROR"
[workspace]
specs_dir = "./legacy-specs"
"""
def test_home_config_loaded_as_base_layer(self, tmp_path, home_config_content):
"""Home config is loaded as the base configuration layer."""
home_config = tmp_path / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(os.environ, {}, clear=True):
# Run from a directory without any project config
original_cwd = os.getcwd()
os.chdir(tmp_path)
try:
config = ServerConfig.from_env()
assert config.log_level == "DEBUG"
assert config.structured_logging is False
assert config.disabled_tools == ["health"]
finally:
os.chdir(original_cwd)
def test_project_config_overrides_home_config(self, tmp_path, home_config_content, project_config_content):
"""Project config values override home config values."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Project overrides home
assert config.log_level == "WARNING"
# Home values preserved for unset project values
assert config.structured_logging is False
assert config.disabled_tools == ["health"]
# Project-specific value
assert config.specs_dir == Path("./my-specs")
finally:
os.chdir(original_cwd)
def test_env_vars_override_both_toml_layers(self, tmp_path, home_config_content, project_config_content):
"""Environment variables override both home and project config."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(
os.environ,
{
"FOUNDRY_MCP_LOG_LEVEL": "INFO",
"FOUNDRY_MCP_SPECS_DIR": "/env/specs",
},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Env vars override everything
assert config.log_level == "INFO"
assert config.specs_dir == Path("/env/specs")
finally:
os.chdir(original_cwd)
def test_explicit_config_file_skips_layered_loading(self, tmp_path, home_config_content, project_config_content):
"""Explicit config_file parameter skips layered loading."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
explicit_config = tmp_path / "explicit.toml"
explicit_config.write_text("""
[logging]
level = "CRITICAL"
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env(config_file=str(explicit_config))
# Only explicit config loaded
assert config.log_level == "CRITICAL"
# Defaults used, not home or project values
assert config.structured_logging is True # default
assert config.disabled_tools == [] # default
finally:
os.chdir(original_cwd)
def test_env_var_config_file_skips_layered_loading(self, tmp_path, home_config_content, project_config_content):
"""FOUNDRY_MCP_CONFIG_FILE env var skips layered loading."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
explicit_config = tmp_path / "explicit.toml"
explicit_config.write_text("""
[logging]
level = "CRITICAL"
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_CONFIG_FILE": str(explicit_config)},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Only explicit config loaded
assert config.log_level == "CRITICAL"
assert config.structured_logging is True # default
finally:
os.chdir(original_cwd)
def test_missing_home_config_no_error(self, tmp_path, project_config_content):
"""Missing home config doesn't cause errors."""
home_dir = tmp_path / "home"
home_dir.mkdir()
# No home config file
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Project config loaded, defaults for rest
assert config.log_level == "WARNING"
assert config.structured_logging is True # default
finally:
os.chdir(original_cwd)
def test_missing_project_config_uses_home_only(self, tmp_path, home_config_content):
"""Missing project config uses home config values only."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
# No project config file
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Home config values used
assert config.log_level == "DEBUG"
assert config.structured_logging is False
finally:
os.chdir(original_cwd)
def test_neither_config_exists_uses_defaults(self, tmp_path):
"""When neither config exists, defaults are used."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# All defaults
assert config.log_level == "INFO" # default
assert config.structured_logging is True # default
assert config.disabled_tools == [] # default
finally:
os.chdir(original_cwd)
def test_legacy_config_loaded_as_fallback(self, tmp_path, home_config_content, legacy_config_content):
"""Legacy .foundry-mcp.toml in project dir is loaded as fallback."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
# Only legacy config, no new config
legacy_config = project_dir / ".foundry-mcp.toml"
legacy_config.write_text(legacy_config_content)
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Legacy config overrides home
assert config.log_level == "ERROR"
# Home values preserved for unset legacy values
assert config.structured_logging is False
# Legacy-specific value
assert config.specs_dir == Path("./legacy-specs")
finally:
os.chdir(original_cwd)
def test_new_config_preferred_over_legacy(
self, tmp_path, home_config_content, project_config_content, legacy_config_content
):
"""New foundry-mcp.toml is preferred over legacy .foundry-mcp.toml."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
# Both new and legacy configs exist
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
legacy_config = project_dir / ".foundry-mcp.toml"
legacy_config.write_text(legacy_config_content)
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# New config used, not legacy
assert config.log_level == "WARNING" # from project
assert config.specs_dir == Path("./my-specs") # from project
# Not legacy values
assert config.log_level != "ERROR"
finally:
os.chdir(original_cwd)
def test_partial_override_preserves_unset_values(self, tmp_path):
"""Partial project config preserves unset values from home."""
home_dir = tmp_path / "home"
home_dir.mkdir()
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text("""
[logging]
level = "DEBUG"
structured = false
[tools]
disabled_tools = ["health", "error"]
[research]
default_timeout = 500.0
""")
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
# Only override logging level
project_config.write_text("""
[logging]
level = "INFO"
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Project override
assert config.log_level == "INFO"
# Home values preserved
assert config.structured_logging is False
assert config.research.default_timeout == 500.0
assert set(config.disabled_tools) == {"health", "error"}
finally:
os.chdir(original_cwd)
def test_xdg_config_loaded_as_base_layer(self, tmp_path, xdg_config_content):
"""XDG config (~/.config/foundry-mcp/config.toml) is loaded as the base layer."""
home_dir = tmp_path / "home"
home_dir.mkdir()
xdg_config_dir = home_dir / ".config" / "foundry-mcp"
xdg_config_dir.mkdir(parents=True)
xdg_config = xdg_config_dir / "config.toml"
xdg_config.write_text(xdg_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.log_level == "NOTSET"
assert config.structured_logging is True
assert config.disabled_tools == ["environment"]
finally:
os.chdir(original_cwd)
def test_home_config_overrides_xdg_config(self, tmp_path, xdg_config_content, home_config_content):
"""Home config (~/.foundry-mcp.toml) overrides XDG config."""
home_dir = tmp_path / "home"
home_dir.mkdir()
# XDG config (lowest priority of user configs)
xdg_config_dir = home_dir / ".config" / "foundry-mcp"
xdg_config_dir.mkdir(parents=True)
xdg_config = xdg_config_dir / "config.toml"
xdg_config.write_text(xdg_config_content)
# Home config (overrides XDG)
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Home overrides XDG
assert config.log_level == "DEBUG"
assert config.structured_logging is False
assert config.disabled_tools == ["health"]
finally:
os.chdir(original_cwd)
def test_xdg_config_home_env_var_respected(self, tmp_path, xdg_config_content):
"""XDG_CONFIG_HOME environment variable is respected."""
home_dir = tmp_path / "home"
home_dir.mkdir()
# Custom XDG config location
custom_xdg = tmp_path / "custom-config"
custom_xdg.mkdir()
xdg_config_dir = custom_xdg / "foundry-mcp"
xdg_config_dir.mkdir()
xdg_config = xdg_config_dir / "config.toml"
xdg_config.write_text(xdg_config_content)
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(custom_xdg)}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# XDG config loaded from custom location
assert config.log_level == "NOTSET"
assert config.disabled_tools == ["environment"]
finally:
os.chdir(original_cwd)
def test_full_hierarchy_xdg_home_project_env(
self, tmp_path, xdg_config_content, home_config_content, project_config_content
):
"""Full config hierarchy: XDG < home < project < env."""
home_dir = tmp_path / "home"
home_dir.mkdir()
# XDG config (base layer)
xdg_config_dir = home_dir / ".config" / "foundry-mcp"
xdg_config_dir.mkdir(parents=True)
xdg_config = xdg_config_dir / "config.toml"
xdg_config.write_text(xdg_config_content)
# Home config (overrides XDG)
home_config = home_dir / ".foundry-mcp.toml"
home_config.write_text(home_config_content)
# Project config (overrides home)
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text(project_config_content)
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_LOG_LEVEL": "CRITICAL"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
# Env var overrides everything
assert config.log_level == "CRITICAL"
# Project overrides home
assert config.specs_dir == Path("./my-specs")
# Home overrides XDG (structured=false from home)
assert config.structured_logging is False
# Home value (disabled_tools from home, not XDG)
assert config.disabled_tools == ["health"]
finally:
os.chdir(original_cwd)
def test_autonomy_security_rate_limit_loaded_from_toml(self, tmp_path):
"""Autonomy security rate-limit fields are loaded from TOML."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text("""
[autonomy_security]
rate_limit_max_consecutive_denials = 7
rate_limit_denial_window_seconds = 45
rate_limit_retry_after_seconds = 12
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_security.rate_limit_max_consecutive_denials == 7
assert config.autonomy_security.rate_limit_denial_window_seconds == 45
assert config.autonomy_security.rate_limit_retry_after_seconds == 12
finally:
os.chdir(original_cwd)
def test_autonomy_security_rate_limit_env_overrides(self, tmp_path):
"""Environment variables override autonomy security rate-limit values."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text("""
[autonomy_security]
rate_limit_max_consecutive_denials = 7
rate_limit_denial_window_seconds = 45
rate_limit_retry_after_seconds = 12
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(
os.environ,
{
"FOUNDRY_MCP_AUTONOMY_SECURITY_RATE_LIMIT_MAX_CONSECUTIVE_DENIALS": "3",
"FOUNDRY_MCP_AUTONOMY_SECURITY_RATE_LIMIT_DENIAL_WINDOW_SECONDS": "22",
"FOUNDRY_MCP_AUTONOMY_SECURITY_RATE_LIMIT_RETRY_AFTER_SECONDS": "9",
},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_security.rate_limit_max_consecutive_denials == 3
assert config.autonomy_security.rate_limit_denial_window_seconds == 22
assert config.autonomy_security.rate_limit_retry_after_seconds == 9
finally:
os.chdir(original_cwd)
def test_autonomy_posture_profile_expands_defaults(self, tmp_path):
"""[autonomy_posture] profile applies role/security/session defaults."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text("""
[autonomy_posture]
profile = "unattended"
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_posture.profile == "unattended"
assert config.autonomy_security.role == "autonomy_runner"
assert config.autonomy_security.allow_lock_bypass is False
assert config.autonomy_security.allow_gate_waiver is False
assert config.autonomy_security.enforce_required_phase_gates is True
assert config.autonomy_session_defaults.gate_policy == "strict"
assert config.autonomy_session_defaults.stop_on_phase_completion is True
finally:
os.chdir(original_cwd)
def test_autonomy_posture_supervised_profile_expands_defaults(self, tmp_path):
"""[autonomy_posture] supervised profile keeps guardrails with escape hatches enabled."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text("""
[autonomy_posture]
profile = "supervised"
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_posture.profile == "supervised"
assert config.autonomy_security.role == "maintainer"
assert config.autonomy_security.allow_lock_bypass is True
assert config.autonomy_security.allow_gate_waiver is True
assert config.autonomy_security.enforce_required_phase_gates is True
assert config.autonomy_session_defaults.gate_policy == "strict"
assert config.autonomy_session_defaults.stop_on_phase_completion is True
finally:
os.chdir(original_cwd)
def test_autonomy_posture_allows_direct_override_with_unsafe_warning(self, tmp_path):
"""Direct security/session defaults can override profile and trigger safety warnings."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
project_config = project_dir / "foundry-mcp.toml"
project_config.write_text("""
[autonomy_posture]
profile = "unattended"
[autonomy_security]
role = "maintainer"
allow_lock_bypass = true
allow_gate_waiver = true
enforce_required_phase_gates = false
[autonomy_session_defaults]
gate_policy = "manual"
stop_on_phase_completion = false
""")
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(os.environ, {}, clear=True):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_posture.profile == "unattended"
assert config.autonomy_security.role == "maintainer"
assert config.autonomy_security.allow_lock_bypass is True
assert config.autonomy_security.allow_gate_waiver is True
assert config.autonomy_security.enforce_required_phase_gates is False
assert config.autonomy_session_defaults.gate_policy == "manual"
assert config.autonomy_session_defaults.stop_on_phase_completion is False
assert any(
"UNSAFE unattended posture configuration" in warning for warning in config.startup_warnings
)
finally:
os.chdir(original_cwd)
def test_autonomy_posture_env_applies_debug_defaults(self, tmp_path):
"""FOUNDRY_MCP_AUTONOMY_POSTURE applies debug profile defaults."""
home_dir = tmp_path / "home"
home_dir.mkdir()
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=home_dir):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_AUTONOMY_POSTURE": "debug"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_posture.profile == "debug"
assert config.autonomy_security.role == "maintainer"
assert config.autonomy_security.allow_lock_bypass is True
assert config.autonomy_security.allow_gate_waiver is True
assert config.autonomy_session_defaults.gate_policy == "manual"
assert config.autonomy_session_defaults.stop_on_phase_completion is False
finally:
os.chdir(original_cwd)
class TestConfigProvenanceLogging:
"""H4: Verify security-relevant config settings log their provenance."""
def test_role_env_var_logs_provenance(self, tmp_path, caplog):
"""FOUNDRY_MCP_ROLE env var logs its provenance at INFO level."""
import logging
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_ROLE": "autonomy_runner"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
with caplog.at_level(logging.INFO, logger="foundry_mcp.config"):
config = ServerConfig.from_env()
assert config.autonomy_security.role == "autonomy_runner"
assert any(
"autonomy_security.role = autonomy_runner" in msg and "FOUNDRY_MCP_ROLE" in msg
for msg in caplog.messages
), f"Expected provenance log for role, got: {caplog.messages}"
finally:
os.chdir(original_cwd)
def test_allow_lock_bypass_env_var_logs_provenance(self, tmp_path, caplog):
"""FOUNDRY_MCP_AUTONOMY_SECURITY_ALLOW_LOCK_BYPASS logs provenance."""
import logging
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_AUTONOMY_SECURITY_ALLOW_LOCK_BYPASS": "true"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
with caplog.at_level(logging.INFO, logger="foundry_mcp.config"):
config = ServerConfig.from_env()
assert config.autonomy_security.allow_lock_bypass is True
assert any("allow_lock_bypass" in msg and "env var" in msg for msg in caplog.messages), (
f"Expected provenance log for allow_lock_bypass, got: {caplog.messages}"
)
finally:
os.chdir(original_cwd)
def test_posture_profile_logs_provenance(self, tmp_path, caplog):
"""Posture profile application logs provenance and overrides."""
import logging
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_AUTONOMY_POSTURE": "unattended"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
with caplog.at_level(logging.INFO, logger="foundry_mcp.config"):
config = ServerConfig.from_env()
assert config.autonomy_posture.profile == "unattended"
assert any("autonomy_posture.profile = unattended" in msg for msg in caplog.messages), (
f"Expected posture profile provenance log, got: {caplog.messages}"
)
finally:
os.chdir(original_cwd)
def test_env_var_overrides_posture_and_logs(self, tmp_path, caplog):
"""Env var security setting applied after posture logs its provenance."""
import logging
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{
"FOUNDRY_MCP_AUTONOMY_POSTURE": "unattended",
"FOUNDRY_MCP_ROLE": "maintainer",
},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
with caplog.at_level(logging.INFO, logger="foundry_mcp.config"):
config = ServerConfig.from_env()
# Env var wins over posture default
assert config.autonomy_security.role == "maintainer"
# Both provenance logs should be present
assert any(
"autonomy_security.role = maintainer" in msg and "FOUNDRY_MCP_ROLE" in msg
for msg in caplog.messages
), f"Expected role env var provenance log, got: {caplog.messages}"
finally:
os.chdir(original_cwd)
class TestConfigEnvVarOverridesToml:
"""T5: Verify env vars override TOML values for security-sensitive settings."""
def test_role_env_overrides_toml(self, tmp_path):
"""FOUNDRY_MCP_ROLE env var overrides TOML role = 'maintainer'."""
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "foundry-mcp.toml").write_text('[autonomy_security]\nrole = "maintainer"\n')
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_ROLE": "autonomy_runner"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_security.role == "autonomy_runner"
finally:
os.chdir(original_cwd)
def test_allow_lock_bypass_env_overrides_toml(self, tmp_path):
"""FOUNDRY_MCP_AUTONOMY_SECURITY_ALLOW_LOCK_BYPASS env overrides TOML."""
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "foundry-mcp.toml").write_text("[autonomy_security]\nallow_lock_bypass = true\n")
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_AUTONOMY_SECURITY_ALLOW_LOCK_BYPASS": "false"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
config = ServerConfig.from_env()
assert config.autonomy_security.allow_lock_bypass is False
finally:
os.chdir(original_cwd)
def test_provenance_logged_for_override(self, tmp_path, caplog):
"""Provenance log mentions setting name and source when env overrides TOML."""
import logging
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "foundry-mcp.toml").write_text('[autonomy_security]\nrole = "maintainer"\n')
with patch.object(Path, "home", return_value=tmp_path):
with patch.dict(
os.environ,
{"FOUNDRY_MCP_ROLE": "observer"},
clear=True,
):
original_cwd = os.getcwd()
os.chdir(project_dir)
try:
with caplog.at_level(logging.INFO, logger="foundry_mcp.config"):
config = ServerConfig.from_env()
assert config.autonomy_security.role == "observer"
assert any(
"autonomy_security.role" in msg and "FOUNDRY_MCP_ROLE" in msg for msg in caplog.messages
), f"Expected provenance log for role override, got: {caplog.messages}"
finally:
os.chdir(original_cwd)