Skip to content

Commit 464c972

Browse files
安辰claude
andcommitted
feat: integrate OpenSandbox for isolated code execution
Replace direct exec()/subprocess calls with OpenSandbox's containerized execution when sandbox is enabled in config. All 4 execution paths (RunCode, ExecuteNbCode, Terminal, shell_execute) now route through SandboxExecutor which wraps the opensandbox + code-interpreter SDKs. Falls back to local execution when sandbox is disabled or on failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 11cdf46 commit 464c972

9 files changed

Lines changed: 675 additions & 13 deletions

File tree

config/config2.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,19 @@ llm:
55
api_type: "openai" # or azure / ollama / groq etc.
66
model: "gpt-4-turbo" # or gpt-3.5-turbo
77
base_url: "https://api.openai.com/v1" # or forward url / other llm url
8-
api_key: "YOUR_API_KEY"
8+
api_key: "YOUR_API_KEY"
9+
10+
## Sandbox Configuration (Optional)
11+
## Enables isolated code execution via OpenSandbox.
12+
## Requires: pip install opensandbox opensandbox-code-interpreter
13+
# sandbox:
14+
# enabled: true
15+
# domain: "localhost:8080"
16+
# api_key: "YOUR_SANDBOX_API_KEY"
17+
# image: "opensandbox/code-interpreter:v1.0.2"
18+
# timeout: 600
19+
# resource:
20+
# cpu: "1"
21+
# memory: "2Gi"
22+
# env:
23+
# PYTHON_VERSION: "3.11"

metagpt/actions/di/execute_nb_code.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from metagpt.actions import Action
2828
from metagpt.logs import logger
29+
from metagpt.tools.libs.sandbox_executor import get_sandbox_executor
2930
from metagpt.utils.report import NotebookReporter
3031

3132
INSTALL_KEEPLEN = 500
@@ -243,6 +244,30 @@ async def run_cell(self, cell: NotebookNode, cell_index: int) -> Tuple[bool, str
243244
except Exception:
244245
return self.parse_outputs(self.nb.cells[-1].outputs)
245246

247+
def _sandbox_enabled(self) -> bool:
248+
"""Check if sandbox execution is enabled in config."""
249+
try:
250+
return self.config.sandbox.enabled
251+
except (AttributeError, Exception):
252+
return False
253+
254+
async def _run_code_in_sandbox(self, code: str) -> Tuple[bool, str]:
255+
"""Execute code in the OpenSandbox via CodeInterpreter."""
256+
executor = await get_sandbox_executor(self.config.sandbox)
257+
stdout, stderr = await executor.run_code(code, language="python")
258+
259+
if stderr:
260+
output = stderr
261+
output = remove_escape_and_color_codes(output)
262+
output = output[-5000:]
263+
return False, output
264+
else:
265+
output = stdout
266+
output = remove_escape_and_color_codes(output)
267+
output = remove_log_and_warning_lines(output)
268+
output = output[:5000]
269+
return True, output
270+
246271
async def run(self, code: str, language: Literal["python", "markdown"] = "python") -> Tuple[str, bool]:
247272
"""
248273
return the output of code execution, and a success indicator (bool) of code execution.
@@ -254,12 +279,17 @@ async def run(self, code: str, language: Literal["python", "markdown"] = "python
254279
# add code to the notebook
255280
self.add_code_cell(code=code)
256281

257-
# build code executor
258-
await self.build()
282+
if self._sandbox_enabled():
283+
success, outputs = await self._run_code_in_sandbox(code)
284+
# Store output in notebook cell for consistency
285+
self.add_output_to_cell(self.nb.cells[-1], outputs)
286+
else:
287+
# build code executor
288+
await self.build()
259289

260-
# run code
261-
cell_index = len(self.nb.cells) - 1
262-
success, outputs = await self.run_cell(self.nb.cells[-1], cell_index)
290+
# run code
291+
cell_index = len(self.nb.cells) - 1
292+
success, outputs = await self.run_cell(self.nb.cells[-1], cell_index)
263293

264294
if "!pip" in code:
265295
success = False

metagpt/actions/run_code.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from metagpt.actions.action import Action
2525
from metagpt.logs import logger
2626
from metagpt.schema import RunCodeContext, RunCodeResult
27+
from metagpt.tools.libs.sandbox_executor import get_sandbox_executor
2728
from metagpt.utils.exceptions import handle_exception
2829

2930
PROMPT_TEMPLATE = """
@@ -79,8 +80,21 @@ class RunCode(Action):
7980
name: str = "RunCode"
8081
i_context: RunCodeContext = Field(default_factory=RunCodeContext)
8182

83+
def _sandbox_enabled(self) -> bool:
84+
"""Check if sandbox execution is enabled in config."""
85+
try:
86+
return self.context.config.sandbox.enabled
87+
except (AttributeError, Exception):
88+
return False
89+
8290
@classmethod
83-
async def run_text(cls, code) -> Tuple[str, str]:
91+
async def run_text(cls, code, sandbox_config=None) -> Tuple[str, str]:
92+
if sandbox_config and sandbox_config.enabled:
93+
try:
94+
executor = await get_sandbox_executor(sandbox_config)
95+
return await executor.run_code(code, language="python")
96+
except Exception as e:
97+
logger.warning(f"Sandbox execution failed, falling back to local: {e}")
8498
try:
8599
# We will document_store the result in this dictionary
86100
namespace = {}
@@ -93,6 +107,9 @@ async def run_script(self, working_directory, additional_python_paths=[], comman
93107
working_directory = str(working_directory)
94108
additional_python_paths = [str(path) for path in additional_python_paths]
95109

110+
if self._sandbox_enabled():
111+
return await self._run_script_in_sandbox(working_directory, additional_python_paths, command)
112+
96113
# Copy the current environment variables
97114
env = self.context.new_environ()
98115

@@ -117,6 +134,32 @@ async def run_script(self, working_directory, additional_python_paths=[], comman
117134
stdout, stderr = process.communicate()
118135
return stdout.decode("utf-8"), stderr.decode("utf-8")
119136

137+
async def _run_script_in_sandbox(self, working_directory, additional_python_paths, command) -> Tuple[str, str]:
138+
"""Execute a script inside the OpenSandbox."""
139+
executor = await get_sandbox_executor(self.context.config.sandbox)
140+
sandbox_workdir = "/workspace"
141+
142+
# Upload source files and requirements to sandbox
143+
work_path = Path(working_directory)
144+
if work_path.exists():
145+
for f in work_path.iterdir():
146+
if f.is_file() and f.suffix in (".py", ".txt", ".json", ".yaml", ".yml"):
147+
await executor.upload_file(str(f), f"{sandbox_workdir}/{f.name}")
148+
149+
# Install dependencies inside sandbox
150+
req_file = work_path / "requirements.txt"
151+
if req_file.exists() and req_file.stat().st_size > 0:
152+
await executor.run_command(
153+
f"pip install -r {sandbox_workdir}/requirements.txt", working_directory=sandbox_workdir
154+
)
155+
await executor.run_command("pip install pytest", working_directory=sandbox_workdir)
156+
157+
# Build and run the command inside sandbox
158+
cmd_str = " ".join(command)
159+
logger.info(f"Running in sandbox: {cmd_str}")
160+
stdout, stderr, _ = await executor.run_command(cmd_str, working_directory=sandbox_workdir)
161+
return stdout, stderr
162+
120163
async def run(self, *args, **kwargs) -> RunCodeResult:
121164
logger.info(f"Running {' '.join(self.i_context.command)}")
122165
if self.i_context.mode == "script":
@@ -126,7 +169,8 @@ async def run(self, *args, **kwargs) -> RunCodeResult:
126169
additional_python_paths=self.i_context.additional_python_paths,
127170
)
128171
elif self.i_context.mode == "text":
129-
outs, errs = await self.run_text(code=self.i_context.code)
172+
sandbox_config = self.context.config.sandbox if self._sandbox_enabled() else None
173+
outs, errs = await self.run_text(code=self.i_context.code, sandbox_config=sandbox_config)
130174

131175
logger.info(f"{outs=}")
132176
logger.info(f"{errs=}")

metagpt/config2.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from metagpt.configs.role_custom_config import RoleCustomConfig
2222
from metagpt.configs.role_zero_config import RoleZeroConfig
2323
from metagpt.configs.s3_config import S3Config
24+
from metagpt.configs.sandbox_config import SandboxConfig
2425
from metagpt.configs.search_config import SearchConfig
2526
from metagpt.configs.workspace_config import WorkspaceConfig
2627
from metagpt.const import CONFIG_ROOT, METAGPT_ROOT
@@ -67,6 +68,9 @@ class Config(CLIParams, YamlModel):
6768
browser: BrowserConfig = BrowserConfig()
6869
mermaid: MermaidConfig = MermaidConfig()
6970

71+
# Sandbox Parameters
72+
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
73+
7074
# Storage Parameters
7175
s3: Optional[S3Config] = None
7276
redis: Optional[RedisConfig] = None

metagpt/configs/sandbox_config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
"""
4+
@Time : 2024/3/18
5+
@File : sandbox_config.py
6+
"""
7+
from typing import Dict, List, Optional
8+
9+
from metagpt.utils.yaml_model import YamlModel
10+
11+
12+
class SandboxConfig(YamlModel):
13+
"""Config for OpenSandbox integration."""
14+
15+
enabled: bool = False
16+
domain: str = ""
17+
api_key: str = ""
18+
image: str = "opensandbox/code-interpreter:v1.0.2"
19+
entrypoint: List[str] = ["/opt/opensandbox/code-interpreter.sh"]
20+
timeout: int = 600
21+
resource: Dict[str, str] = {"cpu": "1", "memory": "2Gi"}
22+
env: Dict[str, str] = {}
23+
network_policy: Optional[Dict] = None

0 commit comments

Comments
 (0)