Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 59 additions & 49 deletions app/agent/sandbox_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
from app.agent.browser import BrowserContextHelper
from app.agent.toolcall import ToolCallAgent
from app.config import config
from app.daytona.sandbox import create_sandbox, delete_sandbox
from app.daytona.tool_base import SandboxToolsBase
from app.logger import logger
from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
from app.sandbox.providers import SandboxProvider, create_sandbox_provider
from app.tool import Terminate, ToolCollection
from app.tool.ask_human import AskHuman
from app.tool.mcp import MCPClients, MCPClientTool
from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool
from app.tool.sandbox.sb_browser_tool import (
SANDBOX_BROWSER_TOOL_NAME,
SandboxBrowserTool,
)
from app.tool.sandbox.sb_computer_tool import SandboxComputerTool
from app.tool.sandbox.sb_files_tool import SandboxFilesTool
from app.tool.sandbox.sb_mobile_tool import SandboxMobileTool
from app.tool.sandbox.sb_shell_tool import SandboxShellTool
from app.tool.sandbox.sb_vision_tool import SandboxVisionTool

Expand Down Expand Up @@ -53,6 +57,7 @@ class SandboxManus(ToolCallAgent):
) # server_id -> url/command
_initialized: bool = False
sandbox_link: Optional[dict[str, dict[str, str]]] = Field(default_factory=dict)
sandbox_provider: Optional[SandboxProvider] = Field(default=None, exclude=True)

@model_validator(mode="after")
def initialize_helper(self) -> "SandboxManus":
Expand All @@ -69,42 +74,45 @@ async def create(cls, **kwargs) -> "SandboxManus":
instance._initialized = True
return instance

async def initialize_sandbox_tools(
self,
password: str = config.daytona.VNC_password,
) -> None:
async def initialize_sandbox_tools(self) -> None:
try:
# 创建新沙箱
if password:
sandbox = create_sandbox(password=password)
self.sandbox = sandbox
else:
raise ValueError("password must be provided")
vnc_link = sandbox.get_preview_link(6080)
website_link = sandbox.get_preview_link(8080)
vnc_url = vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link)
website_url = (
website_link.url if hasattr(website_link, "url") else str(website_link)
provider = create_sandbox_provider()
await provider.initialize()
self.sandbox_provider = provider

metadata = provider.metadata()
link_key = (
metadata.extra.get("sandbox_id")
if metadata.extra.get("sandbox_id")
else metadata.provider
)

# Get the actual sandbox_id from the created sandbox
actual_sandbox_id = sandbox.id if hasattr(sandbox, "id") else "new_sandbox"
if not self.sandbox_link:
self.sandbox_link = {}
self.sandbox_link[actual_sandbox_id] = {
"vnc": vnc_url,
"website": website_url,
}
logger.info(f"VNC URL: {vnc_url}")
logger.info(f"Website URL: {website_url}")
SandboxToolsBase._urls_printed = True
sb_tools = [
SandboxBrowserTool(sandbox),
SandboxFilesTool(sandbox),
SandboxShellTool(sandbox),
SandboxVisionTool(sandbox),
if metadata.links:
self.sandbox_link[link_key] = metadata.links
for name, url in metadata.links.items():
logger.info(f"Sandbox {name} link: {url}")

tools = [
SandboxShellTool(provider.shell_service()),
SandboxFilesTool(provider.file_service()),
]
self.available_tools.add_tools(*sb_tools)

browser_service = provider.browser_service()
if browser_service:
tools.append(SandboxBrowserTool(browser_service))

computer_service = provider.computer_service()
if computer_service:
tools.append(SandboxComputerTool(computer_service))

mobile_service = provider.mobile_service()
if mobile_service:
tools.append(SandboxMobileTool(mobile_service))

vision_service = provider.vision_service()
if vision_service:
tools.append(SandboxVisionTool(vision_service))

self.available_tools.add_tools(*tools)

except Exception as e:
logger.error(f"Error initializing sandbox tools: {e}")
Expand Down Expand Up @@ -174,25 +182,19 @@ async def disconnect_mcp_server(self, server_id: str = "") -> None:
self.available_tools = ToolCollection(*base_tools)
self.available_tools.add_tools(*self.mcp_clients.tools)

async def delete_sandbox(self, sandbox_id: str) -> None:
"""Delete a sandbox by ID."""
try:
await delete_sandbox(sandbox_id)
logger.info(f"Sandbox {sandbox_id} deleted successfully")
if sandbox_id in self.sandbox_link:
del self.sandbox_link[sandbox_id]
except Exception as e:
logger.error(f"Error deleting sandbox {sandbox_id}: {e}")
raise e

async def cleanup(self):
"""Clean up Manus agent resources."""
if self.browser_context_helper:
await self.browser_context_helper.cleanup_browser()
# Disconnect from all MCP servers only if we were initialized
if self._initialized:
await self.disconnect_mcp_server()
await self.delete_sandbox(self.sandbox.id if self.sandbox else "unknown")
if self.sandbox_provider:
try:
await self.sandbox_provider.cleanup()
except Exception:
logger.warning("Failed to cleanup sandbox provider", exc_info=True)
self.sandbox_provider = None
self._initialized = False

async def think(self) -> bool:
Expand All @@ -203,8 +205,16 @@ async def think(self) -> bool:

original_prompt = self.next_step_prompt
recent_messages = self.memory.messages[-3:] if self.memory.messages else []
browser_tool_names = {
tool.name
for tool in self.available_tools.tools
if isinstance(tool, SandboxBrowserTool)
}
if not browser_tool_names:
browser_tool_names = {SANDBOX_BROWSER_TOOL_NAME}

browser_in_use = any(
tc.function.name == SandboxBrowserTool().name
tc.function.name in browser_tool_names
for msg in recent_messages
if msg.tool_calls
for tc in msg.tool_calls
Expand Down
79 changes: 77 additions & 2 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,56 @@ class BrowserSettings(BaseModel):
)


class AgentBaySessionDefaults(BaseModel):
"""Default parameters when creating AgentBay sessions."""

image_id: Optional[str] = Field(
None, description="Default image ID to use when creating sessions"
)
is_vpc: bool = Field(
False, description="Whether to create AgentBay sessions with VPC resources"
)


class AgentBaySettings(BaseModel):
"""Configuration for AgentBay based sandbox provider."""

api_key: Optional[str] = Field(
None, description="AgentBay API key (falls back to environment if missing)"
)
endpoint: str = Field(
"wuyingai.cn-shanghai.aliyuncs.com", description="AgentBay API endpoint"
)
timeout_ms: int = Field(
60000, description="AgentBay API timeout in milliseconds (connect/read)"
)
env_file: Optional[str] = Field(
None, description="Path to .env file for AgentBay credentials"
)
desktop_image_id: Optional[str] = Field(
None,
description="Override image ID for desktop-capable sessions (e.g., linux_latest)",
)
browser_image_id: Optional[str] = Field(
None,
description="Override image ID for browser-focused sessions (e.g., browser_latest)",
)
mobile_image_id: Optional[str] = Field(
None,
description="Override image ID for mobile automation sessions (e.g., android_latest)",
)
session_defaults: AgentBaySessionDefaults = Field(
default_factory=AgentBaySessionDefaults,
description="Default session creation parameters",
)


class SandboxSettings(BaseModel):
"""Configuration for the execution sandbox"""
"""Configuration for the execution sandbox and provider selection"""

provider: str = Field(
"daytona", description="Sandbox provider to use (e.g., daytona, agentbay)"
)
use_sandbox: bool = Field(False, description="Whether to use the sandbox")
image: str = Field("python:3.12-slim", description="Base image")
work_dir: str = Field("/workspace", description="Container working directory")
Expand All @@ -103,6 +150,9 @@ class SandboxSettings(BaseModel):
network_enabled: bool = Field(
False, description="Whether network access is allowed"
)
agentbay: Optional[AgentBaySettings] = Field(
default=None, description="AgentBay specific sandbox settings"
)


class DaytonaSettings(BaseModel):
Expand Down Expand Up @@ -189,6 +239,9 @@ class AppConfig(BaseModel):
daytona_config: Optional[DaytonaSettings] = Field(
None, description="Daytona configuration"
)
agentbay_config: Optional[AgentBaySettings] = Field(
None, description="AgentBay configuration"
)

class Config:
arbitrary_types_allowed = True
Expand Down Expand Up @@ -287,7 +340,13 @@ def _load_initial_config(self):
search_settings = SearchSettings(**search_config)
sandbox_config = raw_config.get("sandbox", {})
if sandbox_config:
sandbox_settings = SandboxSettings(**sandbox_config)
sandbox_agentbay = sandbox_config.get("agentbay") or {}
sandbox_settings = SandboxSettings(
**{k: v for k, v in sandbox_config.items() if k != "agentbay"},
agentbay=AgentBaySettings(**sandbox_agentbay)
if sandbox_agentbay
else None,
)
else:
sandbox_settings = SandboxSettings()
daytona_config = raw_config.get("daytona", {})
Expand All @@ -310,6 +369,17 @@ def _load_initial_config(self):
run_flow_settings = RunflowSettings(**run_flow_config)
else:
run_flow_settings = RunflowSettings()
agentbay_config = raw_config.get("agentbay", {})
if agentbay_config:
agentbay_settings = AgentBaySettings(**agentbay_config)
else:
# fall back to sandbox nested config if present
agentbay_settings = (
sandbox_settings.agentbay
if sandbox_settings and sandbox_settings.agentbay
else AgentBaySettings()
)

config_dict = {
"llm": {
"default": default_settings,
Expand All @@ -324,6 +394,7 @@ def _load_initial_config(self):
"mcp_config": mcp_settings,
"run_flow_config": run_flow_settings,
"daytona_config": daytona_settings,
"agentbay_config": agentbay_settings,
}

self._config = AppConfig(**config_dict)
Expand All @@ -340,6 +411,10 @@ def sandbox(self) -> SandboxSettings:
def daytona(self) -> DaytonaSettings:
return self._config.daytona_config

@property
def agentbay(self) -> AgentBaySettings:
return self._config.agentbay_config

@property
def browser_config(self) -> Optional[BrowserSettings]:
return self._config.browser_config
Expand Down
48 changes: 48 additions & 0 deletions app/sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Sandbox Providers Overview

This directory hosts the sandbox integration layer for OpenManus. The sandbox is responsible for executing high‑risk operations (shell, browser automation, desktop control, mobile actions, etc.) in isolated environments. Two providers are currently supported:

| Provider | Capabilities | Key files | Notes |
|----------|--------------|-----------|-------|
| **Daytona** | Shell, File, Browser, Vision | `app/daytona/` and `app/sandbox/providers/daytona_provider.py` | Requires a Daytona account and API key. |
| **AgentBay** | Shell, File, Browser, Computer (desktop), Mobile | `app/sandbox/providers/agentbay_provider.py` | Requires access to AgentBay cloud resources. |

## How it works

1. `app/sandbox/providers/base.py` defines common service interfaces (`ShellService`, `BrowserService`, `ComputerService`, etc.) and the `SandboxProvider` base class.
2. `app/sandbox/providers/factory.py` reads `config/config.toml` to instantiate the correct provider.
3. `SandboxManus` (`app/agent/sandbox_agent.py`) requests the provider and injects the provider-specific tools (e.g., `sandbox_shell`, `sandbox_browser`, `sandbox_mobile`) into the agent.
4. Cleanup is unified through `SandboxProvider.cleanup()`, ensuring remote sessions are released when the agent stops.

## Choosing a provider

Set the provider in `config/config.toml`:

```toml
[sandbox]
provider = "agentbay" # or "daytona"
use_sandbox = true
```

### AgentBay setup

1. Install dependencies (already declared in `requirements.txt`, including `wuying-agentbay-sdk`).
2. Create an AgentBay API key by following the official guide: https://help.aliyun.com/zh/agentbay/user-guide/service-management. The service provides a limited trial quota after the key is created—make sure you finish the console steps before running the agent.
3. Copy `config/config.example-agentbay.toml` to your working config and fill in the `[sandbox.agentbay]` section with your API key and image IDs.
4. Run `python sandbox_main.py`. The agent will register shell, file, browser, desktop, and mobile tools backed by AgentBay.
5. Watch the logs for session links to inspect the remote desktop or device.

### Daytona setup

1. Follow the instructions in `app/daytona/README.md` to configure your Daytona API key and sandbox image.
2. Ensure `provider = "daytona"` in `config/config.toml`.
3. Launch `python sandbox_main.py` to use the Daytona-backed tools (shell, file, browser, vision).

## Adding new providers

1. Implement a new provider class in `app/sandbox/providers/` that inherits from `SandboxProvider`.
2. Provide concrete service implementations for any capabilities you support.
3. Register the provider name in `app/sandbox/providers/factory.py`.
4. Update this README and the configuration examples to document the new option.

Keeping the provider abstraction consistent allows the agents and tools to remain agnostic about the underlying execution environment.
30 changes: 4 additions & 26 deletions app/sandbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
"""
Docker Sandbox Module
Sandbox package exports.

Provides secure containerized execution environment with resource limits
and isolation for running untrusted code.
The default imports were trimmed to avoid pulling heavy dependencies when
only provider abstractions are required. Import modules directly as needed.
"""
from app.sandbox.client import (
BaseSandboxClient,
LocalSandboxClient,
create_sandbox_client,
)
from app.sandbox.core.exceptions import (
SandboxError,
SandboxResourceError,
SandboxTimeoutError,
)
from app.sandbox.core.manager import SandboxManager
from app.sandbox.core.sandbox import DockerSandbox


__all__ = [
"DockerSandbox",
"SandboxManager",
"BaseSandboxClient",
"LocalSandboxClient",
"create_sandbox_client",
"SandboxError",
"SandboxTimeoutError",
"SandboxResourceError",
]
__all__ = []
Loading