diff --git a/astrbot/builtin_stars/astrbot_agent/_conf_schema.json b/astrbot/builtin_stars/astrbot_agent/_conf_schema.json deleted file mode 100644 index 5ef1a909e..000000000 --- a/astrbot/builtin_stars/astrbot_agent/_conf_schema.json +++ /dev/null @@ -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" - } - } -} \ No newline at end of file diff --git a/astrbot/builtin_stars/astrbot_agent/main.py b/astrbot/builtin_stars/astrbot_agent/main.py deleted file mode 100644 index ed5daccef..000000000 --- a/astrbot/builtin_stars/astrbot_agent/main.py +++ /dev/null @@ -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(), - ) diff --git a/astrbot/builtin_stars/astrbot_agent/metadata.yaml b/astrbot/builtin_stars/astrbot_agent/metadata.yaml deleted file mode 100644 index 818da0fc2..000000000 --- a/astrbot/builtin_stars/astrbot_agent/metadata.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: astrbot-agent -desc: AstrBot Agent -author: Soulter -version: 0.0.1 \ No newline at end of file diff --git a/astrbot/builtin_stars/astrbot_agent/sandbox_client.py b/astrbot/builtin_stars/astrbot_agent/sandbox_client.py deleted file mode 100644 index e8340cafd..000000000 --- a/astrbot/builtin_stars/astrbot_agent/sandbox_client.py +++ /dev/null @@ -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] diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 4a00dad41..4ff53be6d 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -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", diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index 5ba97bedb..babee3588 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -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 diff --git a/astrbot/core/pipeline/process_stage/utils.py b/astrbot/core/pipeline/process_stage/utils.py index 112238b73..895cdacdc 100644 --- a/astrbot/core/pipeline/process_stage/utils.py +++ b/astrbot/core/pipeline/process_stage/utils.py @@ -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() diff --git a/astrbot/builtin_stars/astrbot_agent/booters/base.py b/astrbot/core/sandbox/booters/base.py similarity index 77% rename from astrbot/builtin_stars/astrbot_agent/booters/base.py rename to astrbot/core/sandbox/booters/base.py index 59ccd7abe..5fd793ee7 100644 --- a/astrbot/builtin_stars/astrbot_agent/booters/base.py +++ b/astrbot/core/sandbox/booters/base.py @@ -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. + """ ... diff --git a/astrbot/builtin_stars/astrbot_agent/booters/boxlite.py b/astrbot/core/sandbox/booters/boxlite.py similarity index 99% rename from astrbot/builtin_stars/astrbot_agent/booters/boxlite.py rename to astrbot/core/sandbox/booters/boxlite.py index 687972c64..4d481c49e 100644 --- a/astrbot/builtin_stars/astrbot_agent/booters/boxlite.py +++ b/astrbot/core/sandbox/booters/boxlite.py @@ -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..." diff --git a/astrbot/builtin_stars/astrbot_agent/booters/shipyard.py b/astrbot/core/sandbox/booters/shipyard.py similarity index 100% rename from astrbot/builtin_stars/astrbot_agent/booters/shipyard.py rename to astrbot/core/sandbox/booters/shipyard.py diff --git a/astrbot/builtin_stars/astrbot_agent/olayer/__init__.py b/astrbot/core/sandbox/olayer/__init__.py similarity index 100% rename from astrbot/builtin_stars/astrbot_agent/olayer/__init__.py rename to astrbot/core/sandbox/olayer/__init__.py diff --git a/astrbot/builtin_stars/astrbot_agent/olayer/filesystem.py b/astrbot/core/sandbox/olayer/filesystem.py similarity index 100% rename from astrbot/builtin_stars/astrbot_agent/olayer/filesystem.py rename to astrbot/core/sandbox/olayer/filesystem.py diff --git a/astrbot/builtin_stars/astrbot_agent/olayer/python.py b/astrbot/core/sandbox/olayer/python.py similarity index 100% rename from astrbot/builtin_stars/astrbot_agent/olayer/python.py rename to astrbot/core/sandbox/olayer/python.py diff --git a/astrbot/builtin_stars/astrbot_agent/olayer/shell.py b/astrbot/core/sandbox/olayer/shell.py similarity index 100% rename from astrbot/builtin_stars/astrbot_agent/olayer/shell.py rename to astrbot/core/sandbox/olayer/shell.py diff --git a/astrbot/core/sandbox/sandbox_client.py b/astrbot/core/sandbox/sandbox_client.py new file mode 100644 index 000000000..bd6ca5ef4 --- /dev/null +++ b/astrbot/core/sandbox/sandbox_client.py @@ -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] diff --git a/astrbot/core/sandbox/tools/__init__.py b/astrbot/core/sandbox/tools/__init__.py new file mode 100644 index 000000000..0ff6d7699 --- /dev/null +++ b/astrbot/core/sandbox/tools/__init__.py @@ -0,0 +1,11 @@ +from .fs import CreateFileTool, FileUploadTool, ReadFileTool +from .python import PythonTool +from .shell import ExecuteShellTool + +__all__ = [ + "CreateFileTool", + "ReadFileTool", + "FileUploadTool", + "PythonTool", + "ExecuteShellTool", +] diff --git a/astrbot/builtin_stars/astrbot_agent/tools/fs.py b/astrbot/core/sandbox/tools/fs.py similarity index 94% rename from astrbot/builtin_stars/astrbot_agent/tools/fs.py rename to astrbot/core/sandbox/tools/fs.py index 1349b841c..5513f82d7 100644 --- a/astrbot/builtin_stars/astrbot_agent/tools/fs.py +++ b/astrbot/core/sandbox/tools/fs.py @@ -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): diff --git a/astrbot/builtin_stars/astrbot_agent/tools/python.py b/astrbot/core/sandbox/tools/python.py similarity index 96% rename from astrbot/builtin_stars/astrbot_agent/tools/python.py rename to astrbot/core/sandbox/tools/python.py index 8dbf4eef3..622377214 100644 --- a/astrbot/builtin_stars/astrbot_agent/tools/python.py +++ b/astrbot/core/sandbox/tools/python.py @@ -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", {}) diff --git a/astrbot/builtin_stars/astrbot_agent/tools/shell.py b/astrbot/core/sandbox/tools/shell.py similarity index 95% rename from astrbot/builtin_stars/astrbot_agent/tools/shell.py rename to astrbot/core/sandbox/tools/shell.py index 4a67079a1..6de5990d3 100644 --- a/astrbot/builtin_stars/astrbot_agent/tools/shell.py +++ b/astrbot/core/sandbox/tools/shell.py @@ -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) diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 987e9baf6..fff27d9a4 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -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": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 352d4b242..3622affa2 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -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": {