feat: sandbox

This commit is contained in:
Soulter
2026-01-13 19:29:22 +08:00
parent 2e2da4b4ce
commit dca88d8ab8
21 changed files with 190 additions and 106 deletions
@@ -1,24 +0,0 @@
{
"booter": {
"description": "Select booter to initialize the sandbox environment.",
"type": "string",
"options": [
"shipyard-bay"
],
"default": "shipyard-bay"
},
"endpoint": {
"description": "The endpoint URL of the Shipyard server.",
"type": "string",
"condition": {
"booter": "shipyard-bay"
}
},
"access_token": {
"description": "The access token for authenticating with the Shipyard server.",
"type": "string",
"condition": {
"booter": "shipyard-bay"
}
}
}
@@ -1,29 +0,0 @@
import os
import astrbot.api.star as star
from astrbot.api import AstrBotConfig
from .tools.fs import CreateFileTool, FileUploadTool, ReadFileTool
from .tools.python import PythonTool
from .tools.shell import ExecuteShellTool
class Main(star.Star):
"""AstrBot Agent"""
def __init__(self, context: star.Context, config: AstrBotConfig) -> None:
self.context = context
self.config = config
self.endpoint = config.get("endpoint", "http://localhost:8000")
self.access_token = config.get("access_token", "")
os.environ["ASTRBOT_SANDBOX_TYPE"] = config.get("booter", "shipyard-bay")
os.environ["SHIPYARD_ENDPOINT"] = self.endpoint
os.environ["SHIPYARD_ACCESS_TOKEN"] = self.access_token
context.add_llm_tools(
CreateFileTool(),
ExecuteShellTool(),
PythonTool(),
ReadFileTool(),
FileUploadTool(),
)
@@ -1,4 +0,0 @@
name: astrbot-agent
desc: AstrBot Agent
author: Soulter
version: 0.0.1
@@ -1,42 +0,0 @@
import os
import uuid
from typing import Literal
from astrbot.api import logger
from .booters.base import SandboxBooter
class SandboxClient:
session_booter: dict[str, SandboxBooter] = {}
def __init__(
self, booter_type: Literal["shipyard-bay", "boxlite"] | None = None
) -> None:
if booter_type is None:
booter_type = os.getenv("ASTRBOT_SANDBOX_TYPE", "shipyard-bay") # type: ignore
self.booter_type = booter_type
logger.info(f"SandboxClient initialized with booter type: {self.booter_type}")
async def get_booter(self, session_id: str) -> SandboxBooter:
if session_id not in self.session_booter:
uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex
if self.booter_type == "shipyard-bay":
from .booters.shipyard import ShipyardBooter
self.client = ShipyardBooter()
elif self.booter_type == "boxlite":
from .booters.boxlite import ShipyardBooter
self.client = ShipyardBooter()
else:
raise ValueError(f"Unknown booter type: {self.booter_type}")
try:
await self.client.boot(uuid_str)
except Exception as e:
logger.error(f"Error booting sandbox for session {session_id}: {e}")
raise e
self.session_booter[session_id] = self.client
return self.session_booter[session_id]
+44
View File
@@ -113,6 +113,12 @@ DEFAULT_CONFIG = {
"provider": "moonshotai",
"moonshotai_api_key": "",
},
"sandbox": {
"enable": False,
"booter": "shipyard",
"shipyard_endpoint": "",
"shipyard_access_token": {},
},
},
"provider_stt_settings": {
"enable": False,
@@ -2539,6 +2545,44 @@ CONFIG_METADATA_3 = {
# "provider_settings.enable": True,
# },
# },
"sandbox": {
"description": "Agent 沙箱环境",
"type": "object",
"items": {
"provider_settings.sandbox.enable": {
"description": "启用沙箱环境",
"type": "bool",
"hint": "启用后,Agent 可以使用沙箱环境中的工具和资源,如 Python 代码执行、Shell 等。",
},
"provider_settings.sandbox.booter": {
"description": "沙箱环境驱动器",
"type": "string",
"options": ["shipyard"],
"condition": {
"provider_settings.sandbox.enable": True,
},
},
"provider_settings.sandbox.shipyard_endpoint": {
"description": "Shipyard API Endpoint",
"type": "string",
"hint": "Shipyard 服务的 API 访问地址。",
"condition": {
"provider_settings.sandbox.enable": True,
"provider_settings.sandbox.booter": "shipyard",
},
"_special": "check_shipyard_connection"
},
"provider_settings.sandbox.shipyard_api_key": {
"description": "Shipyard API Key",
"type": "string",
"hint": "用于访问 Shipyard 服务的 API 密钥。",
"condition": {
"provider_settings.sandbox.enable": True,
"provider_settings.sandbox.booter": "shipyard",
},
},
},
},
"truncate_and_compress": {
"description": "上下文管理策略",
"type": "object",
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from collections.abc import AsyncGenerator
from astrbot.core import logger
@@ -35,8 +36,13 @@ from .....astr_agent_tool_exec import FunctionToolExecutor
from ....context import PipelineContext, call_event_hook
from ...stage import Stage
from ...utils import (
CREATE_FILE_TOOL,
EXECUTE_SHELL_TOOL,
FILE_UPLOAD_TOOL,
KNOWLEDGE_BASE_QUERY_TOOL,
LLM_SAFETY_MODE_SYSTEM_PROMPT,
PYTHON_TOOL,
READ_FILE_TOOL,
retrieve_knowledge_base,
)
@@ -93,6 +99,8 @@ class InternalAgentSubStage(Stage):
"safety_mode_strategy", "system_prompt"
)
self.sandbox_cfg = settings.get("sandbox", {})
self.conv_manager = ctx.plugin_manager.context.conversation_manager
def _select_provider(self, event: AstrMessageEvent):
@@ -466,6 +474,24 @@ class InternalAgentSubStage(Stage):
f"Unsupported llm_safety_mode strategy: {self.safety_mode_strategy}.",
)
def _apply_sandbox_tools(self, req: ProviderRequest, session_id: str) -> None:
"""Add sandbox tools to the provider request."""
if req.func_tool is None:
req.func_tool = ToolSet()
if self.sandbox_cfg.get("booter") == "shipyard":
ep = self.sandbox_cfg.get("shipyard_endpoint", "")
at = self.sandbox_cfg.get("shipyard_access_token", "")
if not ep or not at:
logger.error("Shipyard sandbox configuration is incomplete.")
return
os.environ["SHIPYARD_ENDPOINT"] = ep
os.environ["SHIPYARD_ACCESS_TOKEN"] = at
req.func_tool.add_tool(CREATE_FILE_TOOL)
req.func_tool.add_tool(READ_FILE_TOOL)
req.func_tool.add_tool(EXECUTE_SHELL_TOOL)
req.func_tool.add_tool(PYTHON_TOOL)
req.func_tool.add_tool(FILE_UPLOAD_TOOL)
async def process(
self, event: AstrMessageEvent, provider_wake_prefix: str
) -> AsyncGenerator[None, None]:
@@ -590,6 +616,10 @@ class InternalAgentSubStage(Stage):
if self.llm_safety_mode:
self._apply_llm_safety_mode(req)
# apply sandbox tools
if self.sandbox_cfg.get("enable", False):
self._apply_sandbox_tools(req, req.session_id)
stream_to_general = (
self.unsupported_streaming_strategy == "turn_off"
and not event.platform_meta.support_streaming_message
@@ -5,6 +5,13 @@ from astrbot.api import logger, sp
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.sandbox.tools import (
CreateFileTool,
ExecuteShellTool,
FileUploadTool,
PythonTool,
ReadFileTool,
)
from astrbot.core.star.context import Context
LLM_SAFETY_MODE_SYSTEM_PROMPT = """You are running in Safe Mode.
@@ -135,3 +142,9 @@ async def retrieve_knowledge_base(
KNOWLEDGE_BASE_QUERY_TOOL = KnowledgeBaseQueryTool()
CREATE_FILE_TOOL = CreateFileTool()
READ_FILE_TOOL = ReadFileTool()
EXECUTE_SHELL_TOOL = ExecuteShellTool()
PYTHON_TOOL = PythonTool()
FILE_UPLOAD_TOOL = FileUploadTool()
@@ -16,5 +16,8 @@ class SandboxBooter:
async def shutdown(self) -> None: ...
async def upload_file(self, path: str, file_name: str) -> dict:
"""Upload file to sandbox"""
"""Upload file to sandbox.
Should return a dict with `success` (bool) and `file_path` (str) keys.
"""
...
@@ -124,7 +124,7 @@ class MockShipyardSandboxClient:
loop -= 1
class ShipyardBooter(SandboxBooter):
class BoxliteBooter(SandboxBooter):
async def boot(self, session_id: str) -> None:
logger.info(
f"Booting(Boxlite) for session: {session_id}, this may take a while..."
+38
View File
@@ -0,0 +1,38 @@
import uuid
from typing import Literal
from astrbot.api import logger
from .booters.base import SandboxBooter
session_booter: dict[str, SandboxBooter] = {}
class SandboxClient:
@classmethod
async def get_booter(
cls,
session_id: str,
booter_type: Literal["shipyard", "boxlite"] = "shipyard",
) -> SandboxBooter:
if session_id not in session_booter:
uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex
if booter_type == "shipyard":
from .booters.shipyard import ShipyardBooter
client = ShipyardBooter()
elif booter_type == "boxlite":
from .booters.boxlite import BoxliteBooter
client = BoxliteBooter()
else:
raise ValueError(f"Unknown booter type: {booter_type}")
try:
await client.boot(uuid_str)
except Exception as e:
logger.error(f"Error booting sandbox for session {session_id}: {e}")
raise e
session_booter[session_id] = client
return session_booter[session_id]
+11
View File
@@ -0,0 +1,11 @@
from .fs import CreateFileTool, FileUploadTool, ReadFileTool
from .python import PythonTool
from .shell import ExecuteShellTool
__all__ = [
"CreateFileTool",
"ReadFileTool",
"FileUploadTool",
"PythonTool",
"ExecuteShellTool",
]
@@ -30,7 +30,7 @@ class CreateFileTool(FunctionTool):
)
async def run(self, event: AstrMessageEvent, path: str, content: str):
sb = await SandboxClient().get_booter(event.unified_msg_origin)
sb = await SandboxClient.get_booter(event.unified_msg_origin)
try:
result = await sb.fs.create_file(path, content)
return json.dumps(result)
@@ -56,7 +56,7 @@ class ReadFileTool(FunctionTool):
)
async def run(self, event: AstrMessageEvent, path: str):
sb = await SandboxClient().get_booter(event.unified_msg_origin)
sb = await SandboxClient.get_booter(event.unified_msg_origin)
try:
result = await sb.fs.read_file(path)
return result
@@ -90,7 +90,7 @@ class FileUploadTool(FunctionTool):
event: AstrMessageEvent,
local_path: str,
):
sb = await SandboxClient().get_booter(event.unified_msg_origin)
sb = await SandboxClient.get_booter(event.unified_msg_origin)
try:
# Check if file exists
if not os.path.exists(local_path):
@@ -31,7 +31,7 @@ class PythonTool(FunctionTool):
)
async def run(self, event: AstrMessageEvent, code: str, silent: bool = False):
sb = await SandboxClient().get_booter(event.unified_msg_origin)
sb = await SandboxClient.get_booter(event.unified_msg_origin)
try:
result = await sb.python.exec(code, silent=silent)
output = result.get("output", {})
@@ -42,7 +42,7 @@ class ExecuteShellTool(FunctionTool):
background: bool = False,
env: dict = {},
):
sb = await SandboxClient().get_booter(event.unified_msg_origin)
sb = await SandboxClient.get_booter(event.unified_msg_origin)
try:
result = await sb.shell.exec(command, background=background, env=env)
return json.dumps(result)
@@ -133,6 +133,28 @@
}
}
},
"sandbox": {
"description": "Agent Sandbox Environment",
"provider_settings": {
"sandbox": {
"enable": {
"description": "Enable Sandbox Environment",
"hint": "When enabled, Agent can use tools and resources in the sandbox environment, such as Python tool, Shell, etc."
},
"booter": {
"description": "Sandbox Environment Driver"
},
"shipyard_endpoint": {
"description": "Shipyard API Endpoint",
"hint": "API access address for Shipyard service."
},
"shipyard_api_key": {
"description": "Shipyard API Key",
"hint": "API key for accessing Shipyard service."
}
}
}
},
"truncate_and_compress": {
"description": "Context Management Strategy",
"provider_settings": {
@@ -133,6 +133,28 @@
}
}
},
"sandbox": {
"description": "Agent 沙箱环境",
"provider_settings": {
"sandbox": {
"enable": {
"description": "启用沙箱环境",
"hint": "启用后,Agent 可以使用沙箱环境中的工具和资源,如 Python 代码执行、Shell 等。"
},
"booter": {
"description": "沙箱环境驱动器"
},
"shipyard_endpoint": {
"description": "Shipyard API Endpoint",
"hint": "Shipyard 服务的 API 访问地址。"
},
"shipyard_api_key": {
"description": "Shipyard API Key",
"hint": "用于访问 Shipyard 服务的 API 密钥。"
}
}
}
},
"truncate_and_compress": {
"description": "上下文管理策略",
"provider_settings": {