initial commit after one day coding agent session

This commit is contained in:
Hans Aschauer 2026-02-07 07:45:57 +01:00
commit 372af75b90
88 changed files with 22694 additions and 0 deletions

View file

@ -0,0 +1,433 @@
"""
Tests for Secure Container Manager.
Following TDD approach - these tests are written before implementation.
Tests cover all requirements from todo.md section 1.3.2.
All tests use mocked Podman client (no actual containers needed).
"""
import pytest
from datetime import datetime, timedelta
from unittest.mock import MagicMock, Mock, patch, call
from pathlib import Path
def test_create_container_with_valid_params_succeeds():
"""Test that container creation with valid params succeeds."""
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger, AuditEventType
from mcp_forge.security.resource_limits import ResourceLimits
from mcp_forge.config.schema import SecurityConfig
# Setup mocks
mock_podman_client = MagicMock(spec=PodmanClient)
mock_container = MagicMock()
mock_container.id = "abc123"
mock_podman_client.client.containers.create.return_value = mock_container
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
config = ContainerConfig(
image="mcp-forge/python:3.11",
command=["python", "-c", "print('hello')"],
resource_limits=ResourceLimits(memory="512m", storage="1g", cpu_quota=100000)
)
container_id = manager.create_container(config, session_id="test-session-1")
assert container_id == "abc123"
mock_podman_client.client.containers.create.assert_called_once()
def test_create_container_with_forbidden_params_raises_security_error():
"""Test that forbidden parameters raise SecurityError."""
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator, SecurityError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
config = ContainerConfig(
image="mcp-forge/python:3.11",
command=["python", "-c", "print('hello')"]
)
# Try to override security params (should be caught in to_podman_params or validation)
with pytest.raises(SecurityError):
# This should fail validation
manager.create_container(config, session_id="test-session", privileged=True)
def test_create_container_with_invalid_image_raises_security_error():
"""Test that invalid/disallowed images raise SecurityError."""
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator, SecurityError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
config = ContainerConfig(
image="evil/malicious:latest",
command=["python", "-c", "print('hello')"]
)
with pytest.raises(SecurityError) as exc_info:
manager.create_container(config, session_id="test-session")
assert "image" in str(exc_info.value).lower()
def test_create_container_enforces_required_parameters():
"""Test that required security parameters are enforced."""
from mcp_forge.podman.containers import ContainerConfig
config = ContainerConfig(
image="mcp-forge/python:3.11",
command=["python", "-c", "print('hello')"]
)
params = config.to_podman_params()
# Check required security parameters
assert params["network_mode"] == "none"
assert params["read_only"] is True
assert "no-new-privileges" in params["security_opt"]
assert params["user"] == "1000:1000"
def test_resource_limits_are_applied_correctly():
"""Test that resource limits are correctly applied."""
from mcp_forge.podman.containers import ContainerConfig
from mcp_forge.security.resource_limits import ResourceLimits
limits = ResourceLimits(
memory="1g",
storage="2g",
cpu_quota=200000
)
config = ContainerConfig(
image="mcp-forge/python:3.11",
resource_limits=limits
)
params = config.to_podman_params()
assert params["mem_limit"] == "1073741824" # 1GB in bytes as string
assert params["cpu_quota"] == 200000
assert params["storage_opt"]["size"] == "2147483648" # 2GB in bytes as string
def test_volume_mounts_are_validated():
"""Test that volume mounts are validated against allowlist."""
from mcp_forge.podman.containers import ContainerConfig, SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator, SecurityError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
# Valid mount (session path)
config = ContainerConfig(
image="mcp-forge/python:3.11",
volumes={"/mcp-forge/sessions/test-session-1/workspace": {"bind": "/workspace", "mode": "rw"}}
)
# This should succeed (valid session path)
mock_container = MagicMock()
mock_container.id = "abc123"
mock_podman_client.client.containers.create.return_value = mock_container
container_id = manager.create_container(config, session_id="test-session-1")
assert container_id == "abc123"
# Invalid mount (forbidden path)
config_bad = ContainerConfig(
image="mcp-forge/python:3.11",
volumes={"/etc/passwd": {"bind": "/tmp/passwd", "mode": "r"}}
)
with pytest.raises(SecurityError):
manager.create_container(config_bad, session_id="test-session-1")
def test_start_container_on_session_container_succeeds():
"""Test that starting a session container succeeds."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
mock_container = MagicMock()
mock_podman_client.client.containers.get.return_value = mock_container
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
# Register container with validator
validator.register_session_container("abc123")
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
manager.start_container("abc123")
mock_container.start.assert_called_once()
def test_start_container_on_non_session_container_raises_security_error():
"""Test that starting a non-session container raises SecurityError."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator, SecurityError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
# Try to start container not registered with validator
with pytest.raises(SecurityError):
manager.start_container("unknown123")
def test_stop_container_works():
"""Test that stopping a container works."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
mock_container = MagicMock()
mock_podman_client.client.containers.get.return_value = mock_container
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
# Register container
validator.register_session_container("abc123")
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
manager.stop_container("abc123", timeout=10)
mock_container.stop.assert_called_once_with(timeout=10)
def test_remove_container_works():
"""Test that removing a container works."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
mock_container = MagicMock()
mock_podman_client.client.containers.get.return_value = mock_container
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
# Register container
validator.register_session_container("abc123")
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
manager.remove_container("abc123", force=True)
mock_container.remove.assert_called_once_with(force=True)
def test_cleanup_old_containers():
"""Test cleanup of old containers."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
# Create mock old and new containers
old_container = MagicMock()
old_container.id = "old123"
old_container.attrs = {
"Created": (datetime.now() - timedelta(hours=25)).isoformat(),
"Labels": {"mcp-forge.session": "old-session"}
}
new_container = MagicMock()
new_container.id = "new123"
new_container.attrs = {
"Created": datetime.now().isoformat(),
"Labels": {"mcp-forge.session": "new-session"}
}
mock_podman_client.client.containers.list.return_value = [old_container, new_container]
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
count = manager.cleanup_old_containers(max_age=timedelta(hours=24))
assert count == 1
old_container.remove.assert_called_once_with(force=True)
new_container.remove.assert_not_called()
def test_get_container_logs():
"""Test getting container logs."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
mock_container = MagicMock()
mock_container.logs.return_value = b"stdout output\nstderr output"
mock_podman_client.client.containers.get.return_value = mock_container
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
# Register container
validator.register_session_container("abc123")
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
stdout, stderr = manager.get_container_logs("abc123", tail=100)
assert "output" in stdout or "output" in stderr
mock_container.logs.assert_called()
def test_wait_for_container():
"""Test waiting for container to exit."""
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.security.audit import AuditLogger
from mcp_forge.config.schema import SecurityConfig
mock_podman_client = MagicMock(spec=PodmanClient)
mock_container = MagicMock()
mock_container.wait.return_value = {"StatusCode": 0}
mock_podman_client.client.containers.get.return_value = mock_container
audit_logger = AuditLogger(Path("/tmp/test_audit.log"))
validator = OperationValidator(SecurityConfig(audit_log=Path("/tmp/test_audit.log")))
# Register container
validator.register_session_container("abc123")
manager = SecureContainerManager(
podman_client=mock_podman_client,
validator=validator,
audit_logger=audit_logger
)
exit_code = manager.wait_for_container("abc123", timeout=300)
assert exit_code == 0
mock_container.wait.assert_called_once_with(timeout=300)
def test_container_config_to_podman_params_includes_all_security_settings():
"""Test that ContainerConfig.to_podman_params includes all required settings."""
from mcp_forge.podman.containers import ContainerConfig
from mcp_forge.security.resource_limits import ResourceLimits
config = ContainerConfig(
image="mcp-forge/python:3.11",
command=["python", "-m", "test"],
environment={"VAR1": "value1"},
volumes={"/mcp-forge/sessions/sess-1/work": {"bind": "/workspace", "mode": "rw"}},
resource_limits=ResourceLimits(memory="512m", storage="1g", cpu_quota=100000),
working_dir="/workspace",
user="1000:1000"
)
params = config.to_podman_params()
# Required security settings
assert params["network_mode"] == "none"
assert params["read_only"] is True
assert "no-new-privileges" in params["security_opt"]
assert params["user"] == "1000:1000"
# Configuration passthrough
assert params["image"] == "mcp-forge/python:3.11"
assert params["command"] == ["python", "-m", "test"]
assert params["environment"] == {"VAR1": "value1"}
assert params["working_dir"] == "/workspace"
# Resource limits
assert "mem_limit" in params
assert "cpu_quota" in params

View file

@ -0,0 +1,324 @@
"""
Tests for Podman client wrapper.
Following TDD approach - these tests are written before implementation.
Tests cover all requirements from todo.md section 1.3.1.
All tests use mocked Podman client (no actual Podman needed).
"""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
def test_connection_to_podman_socket_succeeds(tmp_path):
"""Test that connection to Podman socket succeeds."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch() # Create fake socket file
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client_instance = MagicMock()
mock_client_instance.ping.return_value = "OK"
mock_podman.return_value = mock_client_instance
client.connect()
assert client._client is not None
mock_podman.assert_called_once_with(base_url=f"unix://{socket_path}")
def test_connection_failure_raises_clear_error(tmp_path):
"""Test that connection failure raises clear error."""
from mcp_forge.podman.client import PodmanClient, PodmanConnectionError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "nonexistent.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient', side_effect=Exception("Connection failed")):
with pytest.raises(PodmanConnectionError) as exc_info:
client.connect()
assert "connection" in str(exc_info.value).lower() or "failed" in str(exc_info.value).lower()
def test_socket_path_validation(tmp_path):
"""Test that socket path is validated before connecting."""
from mcp_forge.podman.client import PodmanClient, PodmanConnectionError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
nonexistent_socket = tmp_path / "nonexistent.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=nonexistent_socket,
validator=validator,
audit_logger=audit_logger
)
with pytest.raises(PodmanConnectionError) as exc_info:
client.verify_socket_access()
assert "not found" in str(exc_info.value).lower() or "does not exist" in str(exc_info.value).lower()
def test_socket_permissions_check(tmp_path):
"""Test that socket permissions are checked."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
socket_path.chmod(0o000) # Remove all permissions
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
# verify_socket_access should check readability
# Depending on implementation, might raise error or just warn
try:
client.verify_socket_access()
except Exception as e:
# Should mention permissions or access
assert "permission" in str(e).lower() or "access" in str(e).lower() or "readable" in str(e).lower()
finally:
socket_path.chmod(0o644) # Restore for cleanup
def test_api_version_compatibility_check(tmp_path):
"""Test that API version is checked."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.version.return_value = {
"Version": "4.5.0",
"ApiVersion": "4.5.0"
}
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
client.connect()
version_info = client.check_api_version()
assert "Version" in version_info or "ApiVersion" in version_info
def test_ping_health_check(tmp_path):
"""Test that ping/health check works."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
client.connect()
result = client.ping()
assert result is True or result == "OK"
def test_lazy_connection(tmp_path):
"""Test that connection is lazy (only connects when needed)."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
# Creating client should not connect
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
assert client._client is None # Not connected yet
# Accessing client property should trigger connection
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client_instance = MagicMock()
mock_client_instance.ping.return_value = "OK"
mock_podman.return_value = mock_client_instance
_ = client.client
assert client._client is not None
def test_disconnect_cleanup(tmp_path):
"""Test that disconnect cleans up properly."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
client.connect()
assert client._client is not None
client.disconnect()
assert client._client is None
def test_connection_error_includes_socket_path(tmp_path):
"""Test that connection errors include the socket path for debugging."""
from mcp_forge.podman.client import PodmanClient, PodmanConnectionError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "test.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with pytest.raises(PodmanConnectionError) as exc_info:
client.verify_socket_access()
assert str(socket_path) in str(exc_info.value) or socket_path.name in str(exc_info.value)
def test_client_property_auto_connects(tmp_path):
"""Test that accessing client property auto-connects if not connected."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
# First access should trigger connect
_ = client.client
assert mock_podman.called
# Second access should reuse connection
mock_podman.reset_mock()
_ = client.client
assert not mock_podman.called # Should not connect again
def test_validator_and_audit_logger_stored(tmp_path):
"""Test that validator and audit logger are stored for later use."""
from mcp_forge.podman.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
assert client.validator is validator
assert client.audit_logger is audit_logger