agent/sandbox/sandbox_spec.md
The goal of this design specification is to enable RAGFlow to support multiple Sandbox deployment modes:
Defines a unified SandboxProvider interface, and is located at agent/sandbox/providers/.
# agent/sandbox/providers/base.py
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class SandboxInstance:
instance_id: str
provider: str
status: str # running, stopped, error
metadata: Dict[str, Any]
@dataclass
class ExecutionResult:
stdout: str
stderr: str
exit_code: int
execution_time: float
metadata: Dict[str, Any]
class SandboxProvider(ABC):
"""Base interface for all sandbox providers"""
@abstractmethod
def initialize(self, config: Dict[str, Any]) -> bool:
"""Initialize provider with configuration"""
pass
@abstractmethod
def create_instance(self, template: str = "python") -> SandboxInstance:
"""Create a new sandbox instance"""
pass
@abstractmethod
def execute_code(
self,
instance_id: str,
code: str,
language: str,
timeout: int = 10
) -> ExecutionResult:
"""Execute code in the sandbox"""
pass
@abstractmethod
def destroy_instance(self, instance_id: str) -> bool:
"""Destroy a sandbox instance"""
pass
@abstractmethod
def health_check(self) -> bool:
"""Check if provider is healthy"""
pass
@abstractmethod
def get_supported_languages(self) -> list[str]:
"""Get list of supported programming languages"""
pass
@staticmethod
def get_config_schema() -> Dict[str, Dict]:
"""
Return configuration schema for this provider.
Returns a dictionary mapping field names to their schema definitions,
including type, required status, validation rules, labels, and descriptions.
"""
pass
def validate_config(self, config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
"""
Validate provider-specific configuration.
This method allows providers to implement custom validation logic beyond
the basic schema validation. Override this method to add provider-specific
checks like URL format validation, API key format validation, etc.
Args:
config: Configuration dictionary to validate
Returns:
Tuple of (is_valid, error_message):
- is_valid: True if configuration is valid, False otherwise
- error_message: Error message if invalid, None if valid
"""
# Default implementation: no custom validation
return True, None
Wraps the existing executor_manager implementation. The implementation file is located at agent/sandbox/providers/self_managed.py.
Prerequisites:
go install gvisor.dev/gvisor/runsc@latest
sudo cp ~/go/bin/runsc /usr/local/bin/
runsc --version
docker pull infiniflow/sandbox-base-python:latest
docker pull infiniflow/sandbox-base-nodejs:latest
Configuration: Docker API endpoint, pool size, resource limits:
endpoint: HTTP endpoint (default: "http://localhost:9385")timeout: Request timeout in seconds (default: 30)max_retries: Maximum retry attempts (default: 3)pool_size: Container pool size (default: 10)Languages:
Security:
Advantages:
arguments parameter for passing data to main() functionLimitations:
Common issues:
"Container pool is busy": Increase SANDBOX_EXECUTOR_MANAGER_POOL_SIZE (default: 1 in .env, should be 5+)Container creation fails: Ensure gVisor is installed and accessible at /usr/local/bin/runscFile: agent/sandbox/providers/aliyun_codeinterpreter.py
SaaS integration with Aliyun Function Compute Code Interpreter service using the official agentrun-sdk.
Official Resources:
Implementation:
agentrun-sdk packageServerError exceptionsConfiguration:
access_key_id: Aliyun AccessKey IDaccess_key_secret: Aliyun AccessKey Secretaccount_id: Aliyun primary account ID - Required for API callsregion: Region (cn-hangzhou, cn-beijing, cn-shanghai, cn-shenzhen, cn-guangzhou)template_name: Optional sandbox template name for pre-configured environmentstimeout: Execution timeout (max 30 seconds - hard limit)Languages: Python, JavaScript
Security: Serverless microVM isolation, 30-second hard timeout limit
Advantages:
Limitations:
Setup instructions - Creating a RAM user with minimal privileges:
⚠️ Security warning: Never use your Aliyun primary account (root account) AccessKey for SDK operations. Primary accounts have full resource permissions, and leaked credentials pose significant security risks.
Step 1: Create a RAM user
ragflow-sandbox-userRAGFlow Sandbox Service AccountStep 2: Create a custom authorization policy
Navigate to Permissions → Policies → Create Policy → Custom Policy → Configuration Script (JSON)
Choose one of the following policy options based on your security requirements:
Option A: Minimal privilege policy (Recommended)
Grants only the permissions required by the AgentRun SDK:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"agentrun:CreateTemplate",
"agentrun:GetTemplate",
"agentrun:UpdateTemplate",
"agentrun:DeleteTemplate",
"agentrun:ListTemplates",
"agentrun:CreateSandbox",
"agentrun:GetSandbox",
"agentrun:DeleteSandbox",
"agentrun:StopSandbox",
"agentrun:ListSandboxes",
"agentrun:CreateContext",
"agentrun:ExecuteCode",
"agentrun:DeleteContext",
"agentrun:ListContexts",
"agentrun:CreateFile",
"agentrun:GetFile",
"agentrun:DeleteFile",
"agentrun:ListFiles",
"agentrun:CreateProcess",
"agentrun:GetProcess",
"agentrun:KillProcess",
"agentrun:ListProcesses",
"agentrun:CreateRecording",
"agentrun:GetRecording",
"agentrun:DeleteRecording",
"agentrun:ListRecordings",
"agentrun:CheckHealth"
],
"Resource": [
"acs:agentrun:*:{account_id}:template/*",
"acs:agentrun:*:{account_id}:sandbox/*"
]
}
]
}
Replace
{account_id}with your Aliyun primary account ID
Option B: Resource-Level privilege control (most secure)
Limits access to specific resource prefixes:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"agentrun:CreateTemplate",
"agentrun:GetTemplate",
"agentrun:UpdateTemplate",
"agentrun:DeleteTemplate",
"agentrun:ListTemplates"
],
"Resource": "acs:agentrun:*:{account_id}:template/ragflow-*"
},
{
"Effect": "Allow",
"Action": [
"agentrun:CreateSandbox",
"agentrun:GetSandbox",
"agentrun:DeleteSandbox",
"agentrun:StopSandbox",
"agentrun:ListSandboxes",
"agentrun:CheckHealth"
],
"Resource": "acs:agentrun:*:{account_id}:sandbox/*"
},
{
"Effect": "Allow",
"Action": ["agentrun:*"],
"Resource": "acs:agentrun:*:{account_id}:sandbox/*/context/*"
},
{
"Effect": "Allow",
"Action": ["agentrun:*"],
"Resource": "acs:agentrun:*:{account_id}:sandbox/*/file/*"
},
{
"Effect": "Allow",
"Action": ["agentrun:*"],
"Resource": "acs:agentrun:*:{account_id}:sandbox/*/process/*"
},
{
"Effect": "Allow",
"Action": ["agentrun:*"],
"Resource": "acs:agentrun:*:{account_id}:sandbox/*/recording/*"
}
]
}
This limits template creation to only those prefixed with
ragflow-*
Option C: Full access (not recommended for production)
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": "agentrun:*",
"Resource": "*"
}
]
}
Step 3: Authorize the RAM user
ragflow-sandbox-user)Step 4: Configure RAGFlow with the RAM User credentials
After creating the RAM user and obtaining the AccessKey, configure it in RAGFlow's admin settings or environment variables:
# Method 1: Environment variables (for development/testing)
export AGENTRUN_ACCESS_KEY_ID="LTAI5t..." # RAM user's AccessKey ID
export AGENTRUN_ACCESS_KEY_SECRET="xxx..." # RAM user's AccessKey Secret
export AGENTRUN_ACCOUNT_ID="123456789..." # Your primary account ID
export AGENTRUN_REGION="cn-hangzhou"
Or via Admin UI (recommended for production):
access_key_id: RAM user's AccessKey IDaccess_key_secret: RAM user's AccessKey Secretaccount_id: Your primary account IDregion: e.g., cn-hangzhouStep 5: Verify permissions
Test if the RAM user permissions are correctly configured:
from agentrun.sandbox import Sandbox, TemplateInput, TemplateType
try:
# Test template creation
template = Sandbox.create_template(
input=TemplateInput(
template_name="ragflow-permission-test",
template_type=TemplateType.CODE_INTERPRETER
)
)
print("✅ RAM user permissions are correctly configured")
except Exception as e:
print(f"❌ Permission test failed: {e}")
finally:
# Cleanup test resources
try:
Sandbox.delete_template("ragflow-permission-test")
except:
pass
Security best practices:
References:
The file is located at agent/sandbox/providers/e2b.py.
SaaS integration with E2B Cloud.
The file is located at agent/sandbox/providers/manager.py.
Since we only use one active provider at a time (configured globally), the provider management is simplified:
class ProviderManager:
"""Manages the currently active sandbox provider"""
def __init__(self):
self.current_provider: Optional[SandboxProvider] = None
self.current_provider_name: Optional[str] = None
def set_provider(self, name: str, provider: SandboxProvider):
"""Set the active provider"""
self.current_provider = provider
self.current_provider_name = name
def get_provider(self) -> Optional[SandboxProvider]:
"""Get the active provider"""
return self.current_provider
def get_provider_name(self) -> Optional[str]:
"""Get the active provider name"""
return self.current_provider_name
Rationale: With global configuration, there's only one active provider at a time. The provider manager simply holds a reference to the currently active provider, making it a thin wrapper rather than a complex multi-provider manager.
Use the existing SystemSettings table for global sandbox configuration:
# In api/db/db_models.py
class SystemSettings(DataBaseModel):
name = CharField(max_length=128, primary_key=True)
source = CharField(max_length=32, null=False, index=False)
data_type = CharField(max_length=32, null=False, index=False)
value = CharField(max_length=1024, null=False, index=False)
Rationale: Sandbox manager is a system-level service shared by all tenants:
SettingsMgr in admin interfaceStorage Strategy: Each provider's configuration stored as a single JSON object:
sandbox.provider_type - Active provider selection ("self_managed", "aliyun_codeinterpreter", "e2b")sandbox.self_managed - JSON config for self-managed providersandbox.aliyun_codeinterpreter - JSON config for Aliyun Code Interpreter providersandbox.e2b - JSON config for E2B providerNote: The value field has a 1024 character limit, which should be sufficient for typical sandbox configurations. If larger configs are needed, consider using a TextField or a separate configuration table.
Each provider's configuration is stored as a single JSON object in the value field:
{
"name": "sandbox.self_managed",
"source": "variable",
"data_type": "json",
"value": "{\"endpoint\": \"http://localhost:9385\", \"pool_size\": 10, \"max_memory\": \"256m\", \"timeout\": 30}"
}
{
"name": "sandbox.aliyun_codeinterpreter",
"source": "variable",
"data_type": "json",
"value": "{\"access_key_id\": \"LTAI5t...\", \"access_key_secret\": \"xxxxx\", \"account_id\": \"1234567890...\", \"region\": \"cn-hangzhou\", \"timeout\": 30}"
}
{
"name": "sandbox.e2b",
"source": "variable",
"data_type": "json",
"value": "{\"api_key\": \"e2b_sk_...\", \"region\": \"us\", \"timeout\": 30}"
}
{
"name": "sandbox.provider_type",
"source": "variable",
"data_type": "string",
"value": "self_managed"
}
Each provider class implements a static method to describe its configuration schema:
# agent/sandbox/providers/base.py
class SandboxProvider(ABC):
"""Base interface for all sandbox providers"""
@abstractmethod
def initialize(self, config: Dict[str, Any]) -> bool:
"""Initialize provider with configuration"""
pass
@abstractmethod
def create_instance(self, template: str = "python") -> SandboxInstance:
"""Create a new sandbox instance"""
pass
@abstractmethod
def execute_code(
self,
instance_id: str,
code: str,
language: str,
timeout: int = 10
) -> ExecutionResult:
"""Execute code in the sandbox"""
pass
@abstractmethod
def destroy_instance(self, instance_id: str) -> bool:
"""Destroy a sandbox instance"""
pass
@abstractmethod
def health_check(self) -> bool:
"""Check if provider is healthy"""
pass
@abstractmethod
def get_supported_languages(self) -> list[str]:
"""Get list of supported programming languages"""
pass
@staticmethod
def get_config_schema() -> Dict[str, Dict]:
"""Return configuration schema for this provider"""
return {}
Example implementation:
# agent/sandbox/providers/self_managed.py
class SelfManagedProvider(SandboxProvider):
@staticmethod
def get_config_schema() -> Dict[str, Dict]:
return {
"endpoint": {
"type": "string",
"required": True,
"label": "API Endpoint",
"placeholder": "http://localhost:9385"
},
"pool_size": {
"type": "integer",
"default": 10,
"label": "Container Pool Size",
"min": 1,
"max": 100
},
"max_memory": {
"type": "string",
"default": "256m",
"label": "Max Memory per Container",
"options": ["128m", "256m", "512m", "1g"]
},
"timeout": {
"type": "integer",
"default": 30,
"label": "Execution Timeout (seconds)",
"min": 5,
"max": 300
}
}
# agent/sandbox/providers/aliyun_codeinterpreter.py
class AliyunCodeInterpreterProvider(SandboxProvider):
@staticmethod
def get_config_schema() -> Dict[str, Dict]:
return {
"access_key_id": {
"type": "string",
"required": True,
"secret": True,
"label": "Access Key ID",
"description": "Aliyun AccessKey ID for authentication"
},
"access_key_secret": {
"type": "string",
"required": True,
"secret": True,
"label": "Access Key Secret",
"description": "Aliyun AccessKey Secret for authentication"
},
"account_id": {
"type": "string",
"required": True,
"label": "Account ID",
"description": "Aliyun primary account ID, required for API calls"
},
"region": {
"type": "string",
"default": "cn-hangzhou",
"label": "Region",
"options": ["cn-hangzhou", "cn-beijing", "cn-shanghai", "cn-shenzhen", "cn-guangzhou"],
"description": "Aliyun region for Code Interpreter service"
},
"template_name": {
"type": "string",
"required": False,
"label": "Template Name",
"description": "Optional sandbox template name for pre-configured environments"
},
"timeout": {
"type": "integer",
"default": 30,
"label": "Execution Timeout (seconds)",
"min": 1,
"max": 30,
"description": "Code execution timeout (max 30 seconds - hard limit)"
}
}
# agent/sandbox/providers/e2b.py
class E2BProvider(SandboxProvider):
@staticmethod
def get_config_schema() -> Dict[str, Dict]:
return {
"api_key": {
"type": "string",
"required": True,
"secret": True,
"label": "API Key"
},
"region": {
"type": "string",
"default": "us",
"label": "Region",
"options": ["us", "eu"]
},
"timeout": {
"type": "integer",
"default": 30,
"label": "Execution Timeout (seconds)",
"min": 5,
"max": 300
}
}
Benefits of Self-describing providers:
Follow existing pattern in admin/server/routes.py and use SettingsMgr:
# admin/server/routes.py (add new endpoints)
from flask import request, jsonify
import json
from api.db.services.system_settings_service import SystemSettingsService
from agent.agent.sandbox.providers.self_managed import SelfManagedProvider
from agent.agent.sandbox.providers.aliyun_codeinterpreter import AliyunCodeInterpreterProvider
from agent.agent.sandbox.providers.e2b import E2BProvider
from admin.server.services import SettingsMgr
# Map provider IDs to their classes
PROVIDER_CLASSES = {
"self_managed": SelfManagedProvider,
"aliyun_codeinterpreter": AliyunCodeInterpreterProvider,
"e2b": E2BProvider,
}
@admin_bp.route('/api/admin/sandbox/providers', methods=['GET'])
def list_sandbox_providers():
"""List available sandbox providers with their schemas"""
providers = []
for provider_id, provider_class in PROVIDER_CLASSES.items():
schema = provider_class.get_config_schema()
providers.append({
"id": provider_id,
"name": provider_id.replace("_", " ").title(),
"config_schema": schema
})
return jsonify({"data": providers})
@admin_bp.route('/api/admin/sandbox/config', methods=['GET'])
def get_sandbox_config():
"""Get current sandbox configuration"""
# Get active provider
active_provider_setting = SystemSettingsService.get_by_name("sandbox.provider_type")
active_provider = active_provider_setting[0].value if active_provider_setting else None
config = {"active": active_provider}
# Load all provider configs
for provider_id in PROVIDER_CLASSES.keys():
setting = SystemSettingsService.get_by_name(f"sandbox.{provider_id}")
if setting:
try:
config[provider_id] = json.loads(setting[0].value)
except json.JSONDecodeError:
config[provider_id] = {}
else:
# Return default values from schema
provider_class = PROVIDER_CLASSES[provider_id]
schema = provider_class.get_config_schema()
config[provider_id] = {
key: field_def.get("default", "")
for key, field_def in schema.items()
}
return jsonify({"data": config})
@admin_bp.route('/api/admin/sandbox/config', methods=['POST'])
def set_sandbox_config():
"""
Update sandbox provider configuration.
Request Parameters:
- provider_type: Provider identifier (e.g., "self_managed", "e2b")
- config: Provider configuration dictionary
- set_active: (optional) If True, also set this provider as active.
Default: True for backward compatibility.
Set to False to update config without switching providers.
- test_connection: (optional) If True, test connection before saving
Response: Success message
"""
req = request.json
provider_type = req.get('provider_type')
config = req.get('config')
set_active = req.get('set_active', True) # Default to True
# Validate provider exists
if provider_type not in PROVIDER_CLASSES:
return jsonify({"error": "Unknown provider"}), 400
# Validate configuration against schema
provider_class = PROVIDER_CLASSES[provider_type]
schema = provider_class.get_config_schema()
validation_result = validate_config(config, schema)
if not validation_result.valid:
return jsonify({"error": "Invalid config", "details": validation_result.errors}), 400
# Test connection if requested
if req.get('test_connection'):
test_result = test_provider_connection(provider_type, config)
if not test_result.success:
return jsonify({"error": "Connection failed", "details": test_result.error}), 400
# Store entire config as a single JSON record
config_json = json.dumps(config)
setting_name = f"sandbox.{provider_type}"
existing = SystemSettingsService.get_by_name(setting_name)
if existing:
SettingsMgr.update_by_name(setting_name, config_json)
else:
SystemSettingsService.save(
name=setting_name,
source="variable",
data_type="json",
value=config_json
)
# Set as active provider if requested (default: True)
if set_active:
SettingsMgr.update_by_name("sandbox.provider_type", provider_type)
return jsonify({"message": "Configuration saved"})
@admin_bp.route('/api/admin/sandbox/test', methods=['POST'])
def test_sandbox_connection():
"""Test connection to sandbox provider"""
provider_type = request.json.get('provider_type')
config = request.json.get('config')
test_result = test_provider_connection(provider_type, config)
return jsonify({
"success": test_result.success,
"message": test_result.message,
"latency_ms": test_result.latency_ms
})
@admin_bp.route('/api/admin/sandbox/active', methods=['PUT'])
def set_active_sandbox_provider():
"""Set active sandbox provider"""
provider_name = request.json.get('provider')
if provider_name not in PROVIDER_CLASSES:
return jsonify({"error": "Unknown provider"}), 400
# Check if provider is configured
provider_setting = SystemSettingsService.get_by_name(f"sandbox.{provider_name}")
if not provider_setting:
return jsonify({"error": "Provider not configured"}), 400
SettingsMgr.update_by_name("sandbox.provider_type", provider_name)
return jsonify({"message": "Active provider updated"})
Location: web/src/pages/SandboxSettings/index.tsx
import { Form, Select, Input, Button, Card, Space, Tag, message } from 'antd';
import { listSandboxProviders, getSandboxConfig, setSandboxConfig, testSandboxConnection } from '@/utils/api';
const SandboxSettings: React.FC = () => {
const [providers, setProviders] = useState<Provider[]>([]);
const [configs, setConfigs] = useState<Config[]>([]);
const [selectedProvider, setSelectedProvider] = useState<string>('');
const [testing, setTesting] = useState(false);
const providerSchema = providers.find(p => p.id === selectedProvider);
const renderConfigForm = () => {
if (!providerSchema) return null;
return (
<Form layout="vertical">
{Object.entries(providerSchema.config_schema).map(([key, schema]) => (
<Form.Item
key={key}
name={key}
label={schema.label}
rules={[{ required: schema.required }]}
>
{schema.secret ? (
<Input.Password placeholder={schema.placeholder} />
) : schema.type === 'integer' ? (
<InputNumber min={schema.min} max={schema.max} />
) : schema.options ? (
<Select>
{schema.options.map((opt: string) => (
<Option key={opt} value={opt}>{opt}</Option>
))}
</Select>
) : (
<Input placeholder={schema.placeholder} />
)}
</Form.Item>
))}
</Form>
);
};
return (
<Card title="Sandbox Provider Configuration">
<Space direction="vertical" style={{ width: '100%' }}>
<Form.Item label="Select Provider">
<Select
style={{ width: '100%' }}
onChange={setSelectedProvider}
value={selectedProvider}
>
{providers.map(provider => (
<Option key={provider.id} value={provider.id}>
<Space>
<Icon type={provider.icon} />
{provider.name}
{provider.tags.map(tag => (
<Tag key={tag}>{tag}</Tag>
))}
</Space>
</Option>
))}
</Select>
</Form.Item>
{renderConfigForm()}
<Space>
<Button type="primary" onClick={handleSave}>
Save Configuration
</Button>
<Button onClick={handleTest} loading={testing}>
Test Connection
</Button>
</Space>
</Space>
</Card>
);
};
File: web/src/utils/api.ts
export async function listSandboxProviders() {
return request<{ data: Provider[] }>('/api/admin/sandbox/providers');
}
export async function getSandboxConfig() {
return request<{ data: SandboxConfig }>('/api/admin/sandbox/config');
}
export async function setSandboxConfig(config: SandboxConfigRequest) {
return request('/api/admin/sandbox/config', {
method: 'POST',
data: config,
});
}
export async function testSandboxConnection(provider: string, config: any) {
return request('/api/admin/sandbox/test', {
method: 'POST',
data: { provider, config },
});
}
export async function setActiveSandboxProvider(provider: string) {
return request('/api/admin/sandbox/active', {
method: 'PUT',
data: { provider },
});
}
File: web/src/types/sandbox.ts
interface Provider {
id: string;
name: string;
description: string;
icon: string;
tags: string[];
config_schema: Record<string, ConfigField>;
supported_languages: string[];
}
interface ConfigField {
type: 'string' | 'integer' | 'boolean';
required: boolean;
secret?: boolean;
label: string;
placeholder?: string;
default?: any;
options?: string[];
min?: number;
max?: number;
}
// Configuration response grouped by provider
interface SandboxConfig {
active: string; // Currently active provider
self_managed?: Record<string, string>;
aliyun_codeinterpreter?: Record<string, string>;
e2b?: Record<string, string>;
// Add more providers as needed
}
// Request to update provider configuration
interface SandboxConfigRequest {
provider_type: string;
config: Record<string, string | number | boolean>;
test_connection?: boolean;
set_active?: boolean;
}
The agent system will use the sandbox through the simplified provider manager, loading global configuration from SystemSettings:
# In agent/components/code_executor.py
import json
from agent.agent.sandbox.providers.manager import ProviderManager
from agent.agent.sandbox.providers.self_managed import SelfManagedProvider
from agent.agent.sandbox.providers.aliyun_codeinterpreter import AliyunCodeInterpreterProvider
from agent.agent.sandbox.providers.e2b import E2BProvider
from api.db.services.system_settings_service import SystemSettingsService
# Map provider IDs to their classes
PROVIDER_CLASSES = {
"self_managed": SelfManagedProvider,
"aliyun_codeinterpreter": AliyunCodeInterpreterProvider,
"e2b": E2BProvider,
}
class CodeExecutorComponent:
def __init__(self):
self.provider_manager = ProviderManager()
self._load_active_provider()
def _load_active_provider(self):
"""Load the active provider from system settings"""
# Get active provider
active_setting = SystemSettingsService.get_by_name("sandbox.provider_type")
if not active_setting:
raise RuntimeError("No sandbox provider configured")
active_provider = active_setting[0].value
# Load configuration for active provider (single JSON record)
provider_setting = SystemSettingsService.get_by_name(f"sandbox.{active_provider}")
if not provider_setting:
raise RuntimeError(f"Sandbox provider {active_provider} not configured")
# Parse JSON configuration
try:
config = json.loads(provider_setting[0].value)
except json.JSONDecodeError as e:
raise RuntimeError(f"Invalid sandbox configuration for {active_provider}: {e}")
# Get provider class
provider_class = PROVIDER_CLASSES.get(active_provider)
if not provider_class:
raise RuntimeError(f"Unknown provider: {active_provider}")
# Initialize provider
provider = provider_class()
provider.initialize(config)
# Set as active provider in manager
self.provider_manager.set_provider(active_provider, provider)
def execute(self, code: str, language: str) -> ExecutionResult:
"""Execute code using the active provider"""
provider = self.provider_manager.get_provider()
if not provider:
raise RuntimeError("No sandbox provider configured")
# Create instance
instance = provider.create_instance(template=language)
try:
# Execute code
result = provider.execute_code(
instance_id=instance.instance_id,
code=code,
language=language
)
return result
finally:
# Always cleanup
provider.destroy_instance(instance.instance_id)
{tenant_id}:{session_id}:{instance_id}Common metrics (all providers):
Self-managed specific:
SaaS specific:
Structured logging for all provider operations:
{
"timestamp": "2025-01-26T10:00:00Z",
"tenant_id": "tenant_123",
"provider": "aliyun_codeinterpreter",
"operation": "execute_code",
"instance_id": "inst_xyz",
"language": "python",
"code_hash": "sha256:...",
"duration_ms": 1234,
"status": "success",
"exit_code": 0,
"memory_used_mb": 64,
"region": "cn-hangzhou"
}
Critical alerts:
Warning alerts:
Goals: Extract current implementation into provider pattern
Tasks:
agent/sandbox/providers/base.py with SandboxProvider interfaceagent/sandbox/providers/self_managed.py wrapping executor_manageragent/sandbox/providers/manager.py for provider managementDeliverables:
Goals: Add sandbox configuration to admin system
Tasks:
conf/system_settings.json initialization fileSettingsMgr in admin/server/services.py with sandbox-specific methodsadmin/server/routes.pyDeliverables:
/api/admin/sandbox/*)Goals: Build admin settings interface
Tasks:
web/src/pages/SandboxSettings/index.tsxDeliverables:
Goals: Implement Aliyun Code Interpreter and E2B providers
Tasks:
agent/sandbox/providers/aliyun_codeinterpreter.pyagent/sandbox/providers/e2b.pyDeliverables:
Goals: Update agent components to use new provider system
Tasks:
agent/components/code_executor.py to use ProviderManagerDeliverables:
Goals: Add observability and complete documentation
Tasks:
Deliverables:
Provider tests (test/agent/sandbox/providers/test_*.py):
class TestSelfManagedProvider:
def test_initialize_with_config():
provider = SelfManagedProvider()
assert provider.initialize({"endpoint": "http://localhost:9385"})
def test_create_python_instance():
provider = SelfManagedProvider()
provider.initialize(test_config)
instance = provider.create_instance("python")
assert instance.status == "running"
def test_execute_code():
provider = SelfManagedProvider()
result = provider.execute_code(instance_id, "print('hello')", "python")
assert result.exit_code == 0
assert "hello" in result.stdout
Configuration tests:
Provider Switching:
Multi-Tenant Isolation:
Admin API Tests:
Complete flow tests:
def test_sandbox_execution_flow():
# 1. Configure provider via admin API
setSandboxConfig(provider="self_managed", config={...})
# 2. Create agent task with code execution
task = create_agent_task(code="print('test')")
# 3. Execute task
result = execute_agent_task(task.id)
# 4. Verify result
assert result.status == "success"
assert "test" in result.output
# 5. Verify sandbox cleanup
assert get_active_instances() == 0
Admin UI tests:
Load Testing:
Latency Testing:
Infrastructure:
Pros:
Cons:
Aliyun Code Interpreter (estimated):
E2B (estimated):
Pros:
Cons:
Recommendations:
The architecture supports easy addition of new providers:
Step 1: Implement provider class with schema
# agent/sandbox/providers/new_provider.py
from .base import SandboxProvider
class NewProvider(SandboxProvider):
@staticmethod
def get_config_schema() -> Dict[str, Dict]:
return {
"api_key": {
"type": "string",
"required": True,
"secret": True,
"label": "API Key"
},
"region": {
"type": "string",
"default": "us-east-1",
"label": "Region"
}
}
def initialize(self, config: Dict[str, Any]) -> bool:
self.api_key = config.get("api_key")
self.region = config.get("region", "us-east-1")
# Initialize client
return True
# Implement other abstract methods...
Step 2: Register in provider mapping
# In api/apps/sandbox_app.py or wherever providers are listed
from agent.agent.sandbox.providers.new_provider import NewProvider
PROVIDER_CLASSES = {
"self_managed": SelfManagedProvider,
"aliyun_codeinterpreter": AliyunCodeInterpreterProvider,
"e2b": E2BProvider,
"new_provider": NewProvider, # Add here
}
No central registry to update - just import and add to the mapping!
Feature pooling:
Feature multi-region:
Feature hybrid execution:
SystemSettings initialization file (conf/system_settings.json - add these entries):
{
"system_settings": [
{
"name": "sandbox.provider_type",
"source": "variable",
"data_type": "string",
"value": "self_managed"
},
{
"name": "sandbox.self_managed",
"source": "variable",
"data_type": "json",
"value": "{\"endpoint\": \"http://sandbox-internal:9385\", \"pool_size\": 20, \"max_memory\": \"512m\", \"timeout\": 60, \"enable_seccomp\": true, \"enable_ast_analysis\": true}"
},
{
"name": "sandbox.aliyun_codeinterpreter",
"source": "variable",
"data_type": "json",
"value": "{\"access_key_id\": \"\", \"access_key_secret\": \"\", \"account_id\": \"\", \"region\": \"cn-hangzhou\", \"template_name\": \"\", \"timeout\": 30}"
},
{
"name": "sandbox.e2b",
"source": "variable",
"data_type": "json",
"value": "{\"api_key\": \"\", \"region\": \"us\", \"timeout\": 30}"
}
]
}
Admin API request example (POST to /api/admin/sandbox/config):
{
"provider_type": "self_managed",
"config": {
"endpoint": "http://sandbox-internal:9385",
"pool_size": 20,
"max_memory": "512m",
"timeout": 60,
"enable_seccomp": true,
"enable_ast_analysis": true
},
"test_connection": true,
"set_active": true
}
Note: The config object in the request is a plain JSON object. The API will serialize it to a JSON string before storing in SystemSettings.
Admin API response example (GET from /api/admin/sandbox/config):
{
"data": {
"active": "self_managed",
"self_managed": {
"endpoint": "http://sandbox-internal:9385",
"pool_size": 20,
"max_memory": "512m",
"timeout": 60,
"enable_seccomp": true,
"enable_ast_analysis": true
},
"aliyun_codeinterpreter": {
"access_key_id": "",
"access_key_secret": "",
"region": "cn-hangzhou",
"workspace_id": ""
},
"e2b": {
"api_key": "",
"region": "us",
"timeout": 30
}
}
}
Note: The response deserializes the JSON strings back to objects for easier frontend consumption.
| Code | Description | Resolution |
|---|---|---|
| SB001 | Provider not initialized | Configure provider in admin |
| SB002 | Invalid configuration | Check configuration values |
| SB003 | Connection failed | Check network and credentials |
| SB004 | Instance creation failed | Check provider capacity |
| SB005 | Execution timeout | Increase timeout or optimize code |
| SB006 | Out of memory | Reduce memory usage or increase limits |
| SB007 | Code blocked by security policy | Remove blocked imports/operations |
| SB008 | Rate limit exceeded | Reduce concurrency or upgrade plan |
| SB009 | Provider unavailable | Check provider status or use fallback |
Document version: 1.0 Last updated: 2026-01-26 Author: RAGFlow Team Status: Design Specification - Ready for Review
value field as TextField (unlimited length)CharField(1024) to TextFieldvalidate_config() methodEach provider's configuration is stored as JSON in SystemSettings.value:
sandbox.provider_type: Active provider selectionsandbox.self_managed: Self-managed provider JSON configsandbox.aliyun_codeinterpreter: Aliyun provider JSON configsandbox.e2b: E2B provider JSON configProvider configuration requires restart: When switching sandbox providers in the admin panel, the ragflow service must be restarted for changes to take effect.
Reason:
get_provider_manager() function caches the provider globallyImpact:
self_managed → aliyun_codeinterpreter requires ragflow restartWorkarounds:
Production: Restart ragflow service after configuration changes:
cd docker
docker compose restart ragflow-server
Development: Use the reload_provider() function in code:
from agent.sandbox.client import reload_provider
reload_provider() # Reloads from MySQL settings
Future enhancement: To support hot reload without restart, implement configuration change detection:
# In agent/sandbox/client.py
_config_timestamp: Optional[int] = None
def get_provider_manager() -> ProviderManager:
global _provider_manager, _config_timestamp
# Check if configuration has changed
setting = SystemSettingsService.get_by_name("sandbox.provider_type")
current_timestamp = setting[0].update_time if setting else 0
if _config_timestamp is None or current_timestamp > _config_timestamp:
# Configuration changed, reload provider
_provider_manager = None
_load_provider_from_settings()
_config_timestamp = current_timestamp
return _provider_manager
However, this adds overhead on every execute_code() call. For production use, explicit restart is preferred for simplicity and reliability.
All sandbox providers support passing arguments to the main() function in user code. This enables dynamic parameter injection for code execution.
Base interface:
# agent/sandbox/providers/base.py
@abstractmethod
def execute_code(
self,
instance_id: str,
code: str,
language: str,
timeout: int = 10,
arguments: Optional[Dict[str, Any]] = None
) -> ExecutionResult:
"""
Execute code in the sandbox.
The code should contain a main() function that will be called with:
- Python: main(**arguments) if arguments provided, else main()
- JavaScript: main(arguments) if arguments provided, else main()
"""
pass
Provider implementations:
Self-managed provider (self_managed.py:164):
"arguments": arguments or {}args.json into the per-task workspaceargs.jsonmain(**args) and JavaScript runner calls main(args)Aliyun Code Interpreter (aliyun_codeinterpreter.py:260-275):
main(**arguments) or main() if no argumentsif arguments:
wrapped_code = f'''{code}
if __name__ == "__main__":
import json
result = main(**{json.dumps(arguments)})
print(json.dumps(result) if isinstance(result, dict) else result)
'''
if arguments:
wrapped_code = f'''{code}
const result = main({json.dumps(arguments)});
console.log(typeof result === 'object' ? JSON.stringify(result) : String(result));
'''
Client layer (client.py:138-190):
def execute_code(
code: str,
language: str = "python",
timeout: int = 30,
arguments: Optional[Dict[str, Any]] = None
) -> ExecutionResult:
provider_manager = get_provider_manager()
provider = provider_manager.get_provider()
instance = provider.create_instance(template=language)
try:
result = provider.execute_code(
instance_id=instance.instance_id,
code=code,
language=language,
timeout=timeout,
arguments=arguments # Passed through to provider
)
return result
finally:
provider.destroy_instance(instance.instance_id)
CodeExec tool integration (code_exec.py:136-165):
def _execute_code(self, language: str, code: str, arguments: dict):
# ... collect arguments from component configuration
result = sandbox_execute_code(
code=code,
language=language,
timeout=int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)),
arguments=arguments # Passed through to sandbox client
)
Python code with arguments:
# User code
def main(name: str, count: int) -> dict:
"""Generate greeting"""
return {"message": f"Hello {name}!" * count}
# Called with: arguments={"name": "World", "count": 3}
# Result: {"message": "Hello World!Hello World!Hello World!"}
JavaScript code with arguments:
// User code
function main(args) {
const { name, count } = args;
return `Hello ${name}!`.repeat(count);
}
// Called with: arguments={"name": "World", "count": 3}
// Result: "Hello World!Hello World!Hello World!"
Function signature: Code MUST define a main() function
def main(**kwargs) or def main() if no argumentsfunction main(args) or function main() if no argumentsType consistency: Arguments are passed as JSON, so types are preserved:
Return value: Return value is serialized as JSON for parsing
print(json.dumps(result)) if dictconsole.log(JSON.stringify(result)) if objectProvider alignment: All providers (self_managed, aliyun_codeinterpreter, e2b) implement arguments passing consistently