feat: Implement session management and secure container lifecycle management

- Added session management for stateful execution in `sessions.py`, including session creation, state documentation, and cleanup of idle sessions.
- Introduced `SecureContainerManager` in `containers.py` for managing container lifecycle with security enforcement, including creation, starting, stopping, and removal of containers.
- Updated server initialization to use the new session manager.
- Enhanced container configuration to skip resource limits for very high values, indicating no enforcement.
- Improved logging capabilities for container operations in the audit logger.
- Refactored Jupyter backend to integrate with the new session management and resource limits handling.
This commit is contained in:
Hans Aschauer 2026-03-04 22:54:56 +01:00
parent 9ceeaa1eda
commit 7d9efc5a38
10 changed files with 3384 additions and 26 deletions

View file

@ -0,0 +1,263 @@
"""Jupyter backend for stateful code execution."""
from typing import Optional, Dict, List
import hashlib
from mcp_forge.config.schema import ForgeConfig
from mcp_forge.podman.containers import SecureContainerManager
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
from mcp_forge.security.resource_limits import ResourceLimits, parse_memory_string
from mcp_forge.execution.simple.executor import ExecutionResult
from mcp_forge.execution.jupyter.kernel import JupyterKernelManager
from mcp_forge.execution.jupyter.sessions import SessionManager, SessionState, SessionError
class JupyterBackend:
"""Stateful code execution backend using Jupyter kernels."""
def __init__(
self,
config: ForgeConfig,
container_manager: SecureContainerManager,
audit_logger: AuditLogger
):
"""
Initialize Jupyter backend.
Args:
config: Forge configuration
container_manager: Container lifecycle manager
audit_logger: Audit logging instance
"""
self.config = config
self.container_manager = container_manager
self.audit_logger = audit_logger
# Initialize kernel manager
kernel_manager = JupyterKernelManager(
container_manager=container_manager,
image=config.images.jupyter,
resource_limits=self._default_resource_limits()
)
# Initialize session manager
self.session_manager = SessionManager(
config=config.sessions,
kernel_manager=kernel_manager,
audit_logger=audit_logger
)
def execute(
self,
code: str,
session_id: str,
timeout: Optional[int] = None,
memory: Optional[str] = None,
cpu_quota: Optional[int] = None,
custom_image: Optional[str] = None,
volumes: Optional[Dict[str, dict]] = None,
injection_code: Optional[str] = None,
bridge_socket_path: Optional[str] = None
) -> ExecutionResult:
"""
Execute code in stateful session.
Creates session if it doesn't exist, reuses existing session otherwise.
Session maintains namespace state across multiple executions.
Args:
code: Python code to execute
session_id: Unique session identifier
timeout: Max execution time in seconds (uses config default if None)
memory: Memory limit string (uses config default if None)
cpu_quota: CPU quota (uses config default if None)
custom_image: Custom image name (uses config default if None)
volumes: Volume mounts dict
injection_code: Optional MCP tool injection code (executed once at session start)
bridge_socket_path: Optional path to MCP bridge socket for mounting
Returns:
ExecutionResult with execution output and metadata
Raises:
ValueError: If limits exceed configured maximums
SessionError: If session operation fails
"""
# Use defaults from config if not specified
timeout = timeout if timeout is not None else self.config.execution.default_timeout
memory = memory if memory is not None else self.config.execution.default_memory
cpu_quota = cpu_quota if cpu_quota is not None else self.config.execution.default_cpu_quota
# Validate limits against maximums
self._validate_limits(timeout, memory, cpu_quota)
# Log execution (hash code, don't log actual content)
code_hash = hashlib.sha256(code.encode()).hexdigest()
self.audit_logger.log(
event_type=AuditEventType.EXECUTION_REQUEST,
severity=AuditSeverity.INFO,
message="Stateful code execution requested",
session_id=session_id,
details={
"code_hash": code_hash,
"timeout": timeout,
"memory": memory,
"cpu_quota": cpu_quota
}
)
# Check if session exists, create if needed
try:
self.session_manager.get_session(session_id)
except SessionError:
# Session doesn't exist, create it with MCP injection
resource_limits = ResourceLimits(
memory=memory,
cpu_quota=cpu_quota,
storage="1g", # Default storage quota
timeout=timeout
)
self.session_manager.create_session(
session_id=session_id,
resource_limits=resource_limits,
volumes=volumes,
injection_code=injection_code,
bridge_socket_path=bridge_socket_path
)
# Execute in session
result = self.session_manager.execute_in_session(
session_id=session_id,
code=code,
timeout=timeout
)
return result
def document_state(
self,
session_id: str,
variables: Dict[str, str],
note: str = "",
clear: bool = False
) -> dict:
"""
Document important variables in session.
Args:
session_id: Session to document
variables: Dictionary of variable_name -> description
note: Optional note about session state
clear: If True, replace all documented variables; if False, merge
Returns:
Dictionary with updated state info
Raises:
SessionError: If session doesn't exist
"""
self.session_manager.document_state(
session_id=session_id,
variables=variables,
note=note,
clear=clear
)
# Return updated state
state = self.session_manager.get_session_state(session_id)
return state.to_dict()
def get_session_state(self, session_id: str) -> SessionState:
"""
Get documented state for session.
Args:
session_id: Session identifier
Returns:
SessionState object
Raises:
SessionError: If session doesn't exist
"""
return self.session_manager.get_session_state(session_id)
def destroy_session(self, session_id: str) -> None:
"""
Destroy session and cleanup kernel.
Args:
session_id: Session to destroy
Raises:
SessionError: If session doesn't exist
"""
self.session_manager.destroy_session(session_id)
def list_sessions(self) -> List[dict]:
"""
List all active sessions with metadata.
Returns:
List of session dictionaries
"""
return self.session_manager.list_sessions()
def cleanup_idle_sessions(self) -> int:
"""
Cleanup sessions idle beyond configured timeout.
Returns:
Number of sessions cleaned up
"""
return self.session_manager.cleanup_idle_sessions()
def _default_resource_limits(self) -> Optional[ResourceLimits]:
"""
Get default resource limits from config.
Returns:
ResourceLimits with config defaults, or None if enforcement disabled
"""
if not self.config.security.enforce_resource_limits:
return None
return ResourceLimits(
memory=self.config.execution.default_memory,
cpu_quota=self.config.execution.default_cpu_quota,
storage="1g",
timeout=self.config.execution.default_timeout
)
def _validate_limits(self, timeout: int, memory: str, cpu_quota: int) -> None:
"""
Validate resource limits against configured maximums.
Args:
timeout: Timeout in seconds
memory: Memory limit string
cpu_quota: CPU quota value
Raises:
ValueError: If any limit exceeds maximum
"""
# Validate timeout
if timeout > self.config.execution.max_timeout:
raise ValueError(
f"Timeout {timeout} exceeds maximum {self.config.execution.max_timeout}"
)
# Validate memory
memory_bytes = parse_memory_string(memory)
max_memory_bytes = parse_memory_string(self.config.execution.max_memory)
if memory_bytes > max_memory_bytes:
raise ValueError(
f"Memory {memory} exceeds maximum {self.config.execution.max_memory}"
)
# Validate CPU quota
if cpu_quota > self.config.execution.max_cpu_quota:
raise ValueError(
f"CPU quota {cpu_quota} exceeds maximum {self.config.execution.max_cpu_quota}"
)