fix(plugins): defer tool imports for disabled plugins
This commit is contained in:
@@ -138,7 +138,7 @@ def load_plugin(path: Path, enabled_plugins: dict[str, bool]) -> LoadedPlugin |
|
||||
skills = _load_plugin_skills(path / manifest.skills_dir)
|
||||
commands = _load_plugin_commands(path, manifest)
|
||||
agents = _load_plugin_agents(path, manifest)
|
||||
tools = _load_plugin_tools(path, manifest)
|
||||
tools = _load_plugin_tools(path, manifest) if enabled else []
|
||||
hooks = _load_plugin_hooks(path / manifest.hooks_file)
|
||||
hooks_dir_file = path / "hooks" / "hooks.json"
|
||||
if not hooks and hooks_dir_file.exists():
|
||||
|
||||
@@ -4,12 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from openharness.coordinator.agent_definitions import AgentDefinition
|
||||
from openharness.mcp.types import McpServerConfig
|
||||
from openharness.plugins.schemas import PluginManifest
|
||||
from openharness.skills.types import SkillDefinition
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openharness.tools.base import BaseTool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginCommandDefinition:
|
||||
@@ -42,7 +46,7 @@ class LoadedPlugin:
|
||||
skills: list[SkillDefinition] = field(default_factory=list)
|
||||
commands: list[PluginCommandDefinition] = field(default_factory=list)
|
||||
agents: list[AgentDefinition] = field(default_factory=list)
|
||||
tools: list = field(default_factory=list)
|
||||
tools: list[BaseTool] = field(default_factory=list)
|
||||
hooks: dict[str, list] = field(default_factory=dict)
|
||||
mcp_servers: dict[str, McpServerConfig] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -71,6 +71,38 @@ def _write_plugin(root: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _write_tool_plugin(root: Path, *, enabled_by_default: bool = True) -> Path:
|
||||
plugin_dir = root / "tool-plugin"
|
||||
tools_dir = plugin_dir / "tools"
|
||||
tools_dir.mkdir(parents=True)
|
||||
(plugin_dir / "plugin.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"name": "tool-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Example tool plugin",
|
||||
"enabled_by_default": enabled_by_default,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tools_dir / "echo_tool.py").write_text(
|
||||
"from pydantic import BaseModel\n"
|
||||
"from openharness.tools.base import BaseTool, ToolExecutionContext, ToolResult\n\n"
|
||||
"class EchoArgs(BaseModel):\n"
|
||||
" text: str = 'hello'\n\n"
|
||||
"class EchoTool(BaseTool):\n"
|
||||
" name = 'plugin_echo'\n"
|
||||
" description = 'Echo from plugin tool'\n"
|
||||
" input_model = EchoArgs\n\n"
|
||||
" async def execute(self, arguments: EchoArgs, context: ToolExecutionContext) -> ToolResult:\n"
|
||||
" del context\n"
|
||||
" return ToolResult(output=arguments.text)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return plugin_dir
|
||||
|
||||
|
||||
def test_load_plugins_from_project_dir(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
||||
project = tmp_path / "repo"
|
||||
@@ -148,3 +180,42 @@ def test_user_plugins_still_load_when_project_plugins_are_disabled(tmp_path: Pat
|
||||
|
||||
assert len(plugins) == 1
|
||||
assert plugins[0].manifest.name == "example"
|
||||
|
||||
|
||||
def test_enabled_plugin_tools_are_loaded(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
||||
project = tmp_path / "repo"
|
||||
plugins_root = project / ".openharness" / "plugins"
|
||||
plugins_root.mkdir(parents=True)
|
||||
_write_tool_plugin(plugins_root, enabled_by_default=True)
|
||||
|
||||
plugins = load_plugins(Settings(allow_project_plugins=True), project)
|
||||
|
||||
assert len(plugins) == 1
|
||||
plugin = plugins[0]
|
||||
assert plugin.enabled is True
|
||||
assert [tool.name for tool in plugin.tools] == ["plugin_echo"]
|
||||
|
||||
|
||||
def test_disabled_plugin_tools_are_not_imported(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
||||
project = tmp_path / "repo"
|
||||
plugins_root = project / ".openharness" / "plugins"
|
||||
plugins_root.mkdir(parents=True)
|
||||
plugin_dir = _write_tool_plugin(plugins_root, enabled_by_default=False)
|
||||
marker = tmp_path / "tool-imported.txt"
|
||||
tool_file = plugin_dir / "tools" / "echo_tool.py"
|
||||
tool_file.write_text(
|
||||
f"from pathlib import Path\n"
|
||||
f"Path({str(marker)!r}).write_text('loaded', encoding='utf-8')\n"
|
||||
+ tool_file.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
plugins = load_plugins(Settings(allow_project_plugins=True), project)
|
||||
|
||||
assert len(plugins) == 1
|
||||
plugin = plugins[0]
|
||||
assert plugin.enabled is False
|
||||
assert plugin.tools == []
|
||||
assert not marker.exists()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openharness.ui.runtime import build_runtime, close_runtime
|
||||
|
||||
|
||||
class _StaticApiClient:
|
||||
async def stream_message(self, request):
|
||||
del request
|
||||
if False:
|
||||
yield None
|
||||
|
||||
|
||||
def _write_tool_plugin(plugins_root: Path) -> None:
|
||||
plugin_dir = plugins_root / "tool-plugin"
|
||||
tools_dir = plugin_dir / "tools"
|
||||
tools_dir.mkdir(parents=True)
|
||||
(plugin_dir / "plugin.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"name": "tool-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Runtime tool plugin",
|
||||
"enabled_by_default": True,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tools_dir / "echo_tool.py").write_text(
|
||||
"from pydantic import BaseModel\n"
|
||||
"from openharness.tools.base import BaseTool, ToolExecutionContext, ToolResult\n\n"
|
||||
"class EchoArgs(BaseModel):\n"
|
||||
" text: str = 'hello'\n\n"
|
||||
"class EchoTool(BaseTool):\n"
|
||||
" name = 'plugin_echo'\n"
|
||||
" description = 'Echo from plugin tool'\n"
|
||||
" input_model = EchoArgs\n\n"
|
||||
" async def execute(self, arguments: EchoArgs, context: ToolExecutionContext) -> ToolResult:\n"
|
||||
" del context\n"
|
||||
" return ToolResult(output=arguments.text)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_runtime_registers_enabled_plugin_tools(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
|
||||
project = tmp_path / "repo"
|
||||
plugins_root = project / ".openharness" / "plugins"
|
||||
plugins_root.mkdir(parents=True)
|
||||
_write_tool_plugin(plugins_root)
|
||||
|
||||
from openharness.config.settings import Settings
|
||||
|
||||
monkeypatch.setattr("openharness.ui.runtime.load_settings", lambda: Settings(allow_project_plugins=True))
|
||||
|
||||
bundle = await build_runtime(cwd=str(project), api_client=_StaticApiClient())
|
||||
try:
|
||||
tool = bundle.tool_registry.get("plugin_echo")
|
||||
assert tool is not None
|
||||
assert tool.description == "Echo from plugin tool"
|
||||
finally:
|
||||
await close_runtime(bundle)
|
||||
Reference in New Issue
Block a user