22Configuration loader for MCP session.
33
44This module provides functionality to load MCP configuration from JSON files.
5+
6+ Security: Sandbox is enforced by default for all stdio-based MCP servers.
7+ Unsandboxed execution requires explicit OPENSPACE_ALLOW_UNSANDBOXED=1 env var.
58"""
69
10+ import os
711from typing import Any , Optional
812
913from openspace .grounding .core .types import SandboxOptions
2630 E2BSandbox = None
2731 E2B_AVAILABLE = False
2832
33+
34+ # Trusted sandbox config keys that may be sourced from config/env
35+ _TRUSTED_SANDBOX_KEYS = frozenset ({
36+ "timeout" , "sse_read_timeout" , "supergateway_command" , "port" ,
37+ "sandbox_template_id" ,
38+ })
39+
40+
41+ def _build_trusted_sandbox_options (
42+ caller_options : SandboxOptions | None ,
43+ default_timeout : float ,
44+ default_sse_timeout : float ,
45+ ) -> dict [str , Any ]:
46+ """Build sandbox options from trusted sources only.
47+
48+ Strips caller-supplied api_key (must come from env E2B_API_KEY).
49+ Only allows known config keys through; ignores anything else.
50+ """
51+ base : dict [str , Any ] = {
52+ "timeout" : default_timeout ,
53+ "sse_read_timeout" : default_sse_timeout ,
54+ }
55+ if caller_options :
56+ for key in _TRUSTED_SANDBOX_KEYS :
57+ if key in caller_options :
58+ base [key ] = caller_options [key ]
59+ return base
60+
61+
2962async def create_connector_from_config (
3063 server_config : dict [str , Any ],
3164 server_name : str = "unknown" ,
32- sandbox : bool = False ,
65+ sandbox : bool = True ,
3366 sandbox_options : SandboxOptions | None = None ,
3467 timeout : float = 30.0 ,
3568 sse_read_timeout : float = 300.0 ,
@@ -40,10 +73,15 @@ async def create_connector_from_config(
4073) -> MCPBaseConnector :
4174 """Create a connector based on server configuration.
4275
76+ For stdio-based servers, sandbox is ENFORCED. Unsandboxed stdio execution
77+ is denied by default. Set OPENSPACE_ALLOW_UNSANDBOXED=1 to explicitly
78+ opt out (development/testing only — NOT recommended for production).
79+
4380 Args:
4481 server_config: The server configuration section
4582 server_name: Name of the MCP server (for display purposes)
4683 sandbox: Whether to use sandboxed execution mode for running MCP servers.
84+ Defaults to True (enforced).
4785 sandbox_options: Optional sandbox configuration options.
4886 timeout: Timeout for operations in seconds (default: 30.0)
4987 sse_read_timeout: SSE read timeout in seconds (default: 300.0)
@@ -56,13 +94,40 @@ async def create_connector_from_config(
5694 A configured connector instance
5795
5896 Raises:
59- RuntimeError: If dependencies are not installed and user declines installation
97+ RuntimeError: If sandbox is required but not available, or if
98+ dependencies are not installed and user declines installation
6099 """
61100
62101 # Get original command and args from config
63102 original_command = get_config_value (server_config , "command" )
64103 original_args = get_config_value (server_config , "args" , [])
65104
105+ # --- Sandbox enforcement BEFORE any host-side operations ---
106+ # Reject unsandboxed stdio early, before ensure_dependencies runs
107+ # npm/pip install on the host. This prevents a malicious server config
108+ # from triggering host-side package installs before being denied.
109+ if is_stdio_server (server_config ) and not sandbox :
110+ allow_unsandboxed = os .environ .get ("OPENSPACE_ALLOW_UNSANDBOXED" , "" ).strip ()
111+ if allow_unsandboxed != "1" :
112+ raise RuntimeError (
113+ f"Unsandboxed stdio execution denied for server '{ server_name } '. "
114+ "Sandbox is required for all stdio-based MCP servers. "
115+ "Set OPENSPACE_ALLOW_UNSANDBOXED=1 to override (NOT recommended)."
116+ )
117+ import logging
118+ logging .getLogger (__name__ ).warning (
119+ "SECURITY: Running server '%s' WITHOUT sandbox (OPENSPACE_ALLOW_UNSANDBOXED=1). "
120+ "This is NOT recommended for production use." ,
121+ server_name ,
122+ )
123+
124+ if is_stdio_server (server_config ) and sandbox and not E2B_AVAILABLE :
125+ raise ImportError (
126+ "E2B sandbox support not available. Please install e2b-code-interpreter: "
127+ "'pip install e2b-code-interpreter'"
128+ )
129+
130+ # --- Host-side operations (only after sandbox enforcement passes) ---
66131 # Check and install dependencies if needed (only for stdio servers)
67132 if is_stdio_server (server_config ) and check_dependencies :
68133 # Use provided installer or get global instance
@@ -73,27 +138,23 @@ async def create_connector_from_config(
73138 # Ensure dependencies are installed (using original command/args)
74139 await installer .ensure_dependencies (server_name , original_command , original_args )
75140
76- # Stdio connector (command-based )
141+ # Stdio connector — unsandboxed (only reachable with explicit opt-out )
77142 if is_stdio_server (server_config ) and not sandbox :
78143 return StdioConnector (
79144 command = get_config_value (server_config , "command" ),
80145 args = get_config_value (server_config , "args" ),
81146 env = get_config_value (server_config , "env" , None ),
82147 )
83148
84- # Sandboxed connector
85- elif is_stdio_server (server_config ) and sandbox :
86- if not E2B_AVAILABLE :
87- raise ImportError (
88- "E2B sandbox support not available. Please install e2b-code-interpreter: "
89- "'pip install e2b-code-interpreter'"
90- )
91-
92- # Create E2B sandbox instance
93- _sandbox_options = sandbox_options or {}
149+ # Sandboxed connector (E2B_AVAILABLE already verified above)
150+ elif is_stdio_server (server_config ):
151+ # Build sandbox options from trusted config/env only (never user input)
152+ _sandbox_options = _build_trusted_sandbox_options (
153+ sandbox_options , timeout , sse_read_timeout
154+ )
94155 e2b_sandbox = E2BSandbox (_sandbox_options )
95156
96- # Extract timeout values from sandbox_options or use defaults
157+ # Extract timeout values from trusted options
97158 connector_timeout = _sandbox_options .get ("timeout" , timeout )
98159 connector_sse_timeout = _sandbox_options .get ("sse_read_timeout" , sse_read_timeout )
99160
0 commit comments