Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/deployment-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ opencane config check --strict

- `hardware.auth.enabled`
- `hardware.auth.token`
- `tools.exec.enable`(关闭后不注册 shell `exec` 工具)
- `tools.restrictToWorkspace`(限制工具只能访问工作区)
- `safety.*`
- `interaction.*`

Expand Down
23 changes: 12 additions & 11 deletions opencane/agent/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
11 changes: 6 additions & 5 deletions opencane/agent/subagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
1 change: 1 addition & 0 deletions opencane/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ class WebToolsConfig(BaseModel):

class ExecToolConfig(BaseModel):
"""Shell exec tool configuration."""
enable: bool = True
timeout: int = 60


Expand Down
48 changes: 48 additions & 0 deletions tests/test_subagent_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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(
Expand All @@ -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
13 changes: 13 additions & 0 deletions tests/test_tool_domain_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Loading