diff --git a/docs/deployment-config.md b/docs/deployment-config.md index 93cd9892227..71fdd58c8b6 100644 --- a/docs/deployment-config.md +++ b/docs/deployment-config.md @@ -48,6 +48,8 @@ opencane config check --strict - `hardware.auth.enabled` - `hardware.auth.token` +- `tools.exec.enable`(关闭后不注册 shell `exec` 工具) +- `tools.restrictToWorkspace`(限制工具只能访问工作区) - `safety.*` - `interaction.*` diff --git a/opencane/agent/loop.py b/opencane/agent/loop.py index c0365937d2f..f2b1c632db1 100644 --- a/opencane/agent/loop.py +++ b/opencane/agent/loop.py @@ -311,17 +311,18 @@ def _register_default_tools(self) -> None: ) # Shell tool - self.tools.register(ExecTool( - working_dir=str(self.workspace), - timeout=self.exec_config.timeout, - restrict_to_workspace=self.restrict_to_workspace, - )) - self.tool_domains.register_tool( - "exec", - domain="server_tools", - allowed_channels={"cli"}, - allow_system=False, - ) + if self.exec_config.enable: + self.tools.register(ExecTool( + working_dir=str(self.workspace), + timeout=self.exec_config.timeout, + restrict_to_workspace=self.restrict_to_workspace, + )) + self.tool_domains.register_tool( + "exec", + domain="server_tools", + allowed_channels={"cli"}, + allow_system=False, + ) # Web tools self.tools.register(WebSearchTool(api_key=self.brave_api_key)) diff --git a/opencane/agent/subagent.py b/opencane/agent/subagent.py index 6fdb093c485..9fb7b983ff0 100644 --- a/opencane/agent/subagent.py +++ b/opencane/agent/subagent.py @@ -114,11 +114,12 @@ async def _run_subagent( tools.register(WriteFileTool(workspace=self.workspace, allowed_dir=allowed_dir)) tools.register(EditFileTool(workspace=self.workspace, allowed_dir=allowed_dir)) tools.register(ListDirTool(workspace=self.workspace, allowed_dir=allowed_dir)) - tools.register(ExecTool( - working_dir=str(self.workspace), - timeout=self.exec_config.timeout, - restrict_to_workspace=self.restrict_to_workspace, - )) + if self.exec_config.enable: + tools.register(ExecTool( + working_dir=str(self.workspace), + timeout=self.exec_config.timeout, + restrict_to_workspace=self.restrict_to_workspace, + )) tools.register(WebSearchTool(api_key=self.brave_api_key)) tools.register(WebFetchTool()) diff --git a/opencane/config/schema.py b/opencane/config/schema.py index 238bbb5ee33..90db0cf351a 100644 --- a/opencane/config/schema.py +++ b/opencane/config/schema.py @@ -221,6 +221,7 @@ class WebToolsConfig(BaseModel): class ExecToolConfig(BaseModel): """Shell exec tool configuration.""" + enable: bool = True timeout: int = 60 diff --git a/tests/test_subagent_manager.py b/tests/test_subagent_manager.py index b3bc6b56d83..1868bcdec8e 100644 --- a/tests/test_subagent_manager.py +++ b/tests/test_subagent_manager.py @@ -8,6 +8,7 @@ from opencane.agent.subagent import SubagentManager from opencane.bus.queue import MessageBus +from opencane.config.schema import ExecToolConfig from opencane.providers.base import LLMProvider, LLMResponse @@ -30,6 +31,33 @@ def get_default_model(self) -> str: return "fake-model" +class _ToolCaptureProvider(LLMProvider): + def __init__(self) -> None: + super().__init__(api_key=None, api_base=None) + self.tool_names: list[str] = [] + + async def chat( # type: ignore[override] + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + model: str | None = None, + max_tokens: int = 4096, + temperature: float = 0.7, + ) -> LLMResponse: + del messages, model, max_tokens, temperature + self.tool_names = [] + for item in tools or []: + function = item.get("function") if isinstance(item, dict) else None + if isinstance(function, dict): + name = function.get("name") + if isinstance(name, str): + self.tool_names.append(name) + return LLMResponse(content="done") + + def get_default_model(self) -> str: + return "fake-model" + + @pytest.mark.asyncio async def test_subagent_manager_enforces_running_limit(tmp_path: Path) -> None: manager = SubagentManager( @@ -56,3 +84,23 @@ async def _hold(*args: Any, **kwargs: Any) -> None: gate.set() await asyncio.sleep(0.05) assert manager.get_running_count() == 0 + + +@pytest.mark.asyncio +async def test_subagent_manager_omits_exec_tool_when_disabled(tmp_path: Path) -> None: + provider = _ToolCaptureProvider() + manager = SubagentManager( + provider=provider, + workspace=tmp_path, + bus=MessageBus(), + exec_config=ExecToolConfig(enable=False), + ) + + await manager._run_subagent( + task_id="task-1", + task="check tools", + label="check tools", + origin={"channel": "cli", "chat_id": "direct"}, + ) + + assert "exec" not in provider.tool_names diff --git a/tests/test_tool_domain_manager.py b/tests/test_tool_domain_manager.py index c542e627478..ec9fb88fe2a 100644 --- a/tests/test_tool_domain_manager.py +++ b/tests/test_tool_domain_manager.py @@ -9,6 +9,7 @@ from opencane.agent.tools.base import Tool from opencane.bus.events import InboundMessage from opencane.bus.queue import MessageBus +from opencane.config.schema import ExecToolConfig from opencane.providers.base import LLMProvider, LLMResponse, ToolCallRequest @@ -280,3 +281,15 @@ async def test_subagent_system_message_uses_assistant_role(tmp_path: Path) -> No assert outbound is not None assert provider.last_role == "assistant" + + +def test_agent_loop_can_disable_exec_tool(tmp_path: Path) -> None: + bus = MessageBus() + loop = AgentLoop( + bus=bus, + provider=_SystemRoleProbeProvider(), + workspace=tmp_path, + exec_config=ExecToolConfig(enable=False), + ) + + assert loop.tools.get("exec") is None