mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-10 22:38:40 +03:00
refactor(agent): clarify plugin module boundary
This commit is contained in:
@@ -480,7 +480,7 @@ class AgentLoop:
|
|||||||
config,
|
config,
|
||||||
provider_snapshot_loader,
|
provider_snapshot_loader,
|
||||||
)
|
)
|
||||||
from nanobot.agent.agent_plugins import agent_plugin_mcp_servers
|
from nanobot.agent.plugins import agent_plugin_mcp_servers
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
bus=bus,
|
bus=bus,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -44,12 +43,7 @@ _MCP_SERVER_FIELDS = {
|
|||||||
}
|
}
|
||||||
_SETUP_ENV = {"HOME", "LANG", "LC_ALL", "LOGNAME", "PATH", "SHELL", "TMPDIR", "USER"}
|
_SETUP_ENV = {"HOME", "LANG", "LC_ALL", "LOGNAME", "PATH", "SHELL", "TMPDIR", "USER"}
|
||||||
_SETUP_TIMEOUT_SECONDS = 600
|
_SETUP_TIMEOUT_SECONDS = 600
|
||||||
_LOGO_MIME_TYPES = {
|
_LOGO_SUFFIXES = {".jpeg", ".jpg", ".png", ".webp"}
|
||||||
".jpeg": "image/jpeg",
|
|
||||||
".jpg": "image/jpeg",
|
|
||||||
".png": "image/png",
|
|
||||||
".webp": "image/webp",
|
|
||||||
}
|
|
||||||
_MAX_LOGO_BYTES = 256 * 1024
|
_MAX_LOGO_BYTES = 256 * 1024
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +73,16 @@ class AgentPlugin:
|
|||||||
install_command: tuple[str, ...]
|
install_command: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AgentPluginState:
|
||||||
|
"""Runtime state for one discovered Agent Plugin."""
|
||||||
|
|
||||||
|
plugin: AgentPlugin
|
||||||
|
mcp_servers: tuple[str, ...]
|
||||||
|
enabled: bool
|
||||||
|
setup_required: bool
|
||||||
|
|
||||||
|
|
||||||
def discover_agent_plugins(workspace: Path) -> list[AgentPlugin]:
|
def discover_agent_plugins(workspace: Path) -> list[AgentPlugin]:
|
||||||
"""Return valid packages from ``<workspace>/plugins/*``."""
|
"""Return valid packages from ``<workspace>/plugins/*``."""
|
||||||
workspace = workspace.expanduser().resolve()
|
workspace = workspace.expanduser().resolve()
|
||||||
@@ -205,37 +209,23 @@ def agent_plugin_mcp_servers(
|
|||||||
return servers
|
return servers
|
||||||
|
|
||||||
|
|
||||||
def agent_plugins_payload(workspace: Path) -> dict[str, Any]:
|
def discover_agent_plugin_states(workspace: Path) -> list[AgentPluginState]:
|
||||||
"""Return installed Agent Plugins for the WebUI Apps surface."""
|
"""Return component and lifecycle state for discovered plugins."""
|
||||||
plugins: list[dict[str, Any]] = []
|
states: list[AgentPluginState] = []
|
||||||
enabled_count = 0
|
|
||||||
for plugin in discover_agent_plugins(workspace):
|
for plugin in discover_agent_plugins(workspace):
|
||||||
mcp_servers = sorted(_plugin_mcp_servers(workspace, plugin))
|
states.append(
|
||||||
if not mcp_servers and not plugin.install_command:
|
AgentPluginState(
|
||||||
continue
|
plugin=plugin,
|
||||||
enabled = _enabled(workspace, plugin.name)
|
mcp_servers=tuple(sorted(_plugin_mcp_servers(workspace, plugin))),
|
||||||
enabled_count += int(enabled)
|
enabled=_enabled(workspace, plugin.name),
|
||||||
plugins.append(
|
setup_required=bool(plugin.install_command)
|
||||||
{
|
|
||||||
"name": plugin.name,
|
|
||||||
"display_name": plugin.display_name,
|
|
||||||
"version": plugin.version,
|
|
||||||
"description": plugin.description,
|
|
||||||
"category": plugin.category,
|
|
||||||
"repository": plugin.repository,
|
|
||||||
"accent_color": plugin.accent_color,
|
|
||||||
"logo_url": _plugin_logo_data_url(plugin.logo),
|
|
||||||
"permissions": list(plugin.permissions),
|
|
||||||
"mcp_servers": mcp_servers,
|
|
||||||
"enabled": enabled,
|
|
||||||
"setup_required": bool(plugin.install_command)
|
|
||||||
and _setup_version(workspace, plugin.name) != (plugin.version or "unknown"),
|
and _setup_version(workspace, plugin.name) != (plugin.version or "unknown"),
|
||||||
}
|
)
|
||||||
)
|
)
|
||||||
return {"plugins": plugins, "enabled_count": enabled_count}
|
return states
|
||||||
|
|
||||||
|
|
||||||
def set_agent_plugin_enabled(workspace: Path, name: str, enabled: bool) -> dict[str, Any]:
|
def set_agent_plugin_enabled(workspace: Path, name: str, enabled: bool) -> AgentPlugin:
|
||||||
"""Enable or disable one installed plugin."""
|
"""Enable or disable one installed plugin."""
|
||||||
plugin = next((item for item in discover_agent_plugins(workspace) if item.name == name), None)
|
plugin = next((item for item in discover_agent_plugins(workspace) if item.name == name), None)
|
||||||
if plugin is None:
|
if plugin is None:
|
||||||
@@ -251,12 +241,7 @@ def set_agent_plugin_enabled(workspace: Path, name: str, enabled: bool) -> dict[
|
|||||||
_write_state(data / "enabled", "1")
|
_write_state(data / "enabled", "1")
|
||||||
else:
|
else:
|
||||||
(data / "enabled").unlink(missing_ok=True)
|
(data / "enabled").unlink(missing_ok=True)
|
||||||
payload = agent_plugins_payload(workspace)
|
return plugin
|
||||||
payload["last_action"] = {
|
|
||||||
"ok": True,
|
|
||||||
"message": f"{plugin.display_name} {'enabled' if enabled else 'disabled'}.",
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def _valid_optional_fields(payload: dict[str, Any]) -> bool:
|
def _valid_optional_fields(payload: dict[str, Any]) -> bool:
|
||||||
@@ -302,7 +287,7 @@ def _plugin_logo(value: object, plugin_root: Path) -> Path | None:
|
|||||||
logger.warning("Ignoring invalid Agent Plugin logo in '{}'", plugin_root)
|
logger.warning("Ignoring invalid Agent Plugin logo in '{}'", plugin_root)
|
||||||
return None
|
return None
|
||||||
logo = _contained_file(plugin_root / value[2:], plugin_root)
|
logo = _contained_file(plugin_root / value[2:], plugin_root)
|
||||||
if logo is None or logo.suffix.lower() not in _LOGO_MIME_TYPES:
|
if logo is None or logo.suffix.lower() not in _LOGO_SUFFIXES:
|
||||||
logger.warning("Ignoring invalid Agent Plugin logo in '{}'", plugin_root)
|
logger.warning("Ignoring invalid Agent Plugin logo in '{}'", plugin_root)
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
@@ -314,26 +299,6 @@ def _plugin_logo(value: object, plugin_root: Path) -> Path | None:
|
|||||||
return logo
|
return logo
|
||||||
|
|
||||||
|
|
||||||
def _plugin_logo_data_url(path: Path | None) -> str | None:
|
|
||||||
if path is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
data = path.read_bytes()
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
suffix = path.suffix.lower()
|
|
||||||
valid = (
|
|
||||||
suffix == ".png" and data.startswith(b"\x89PNG\r\n\x1a\n")
|
|
||||||
or suffix in {".jpg", ".jpeg"} and data.startswith(b"\xff\xd8\xff")
|
|
||||||
or suffix == ".webp" and data.startswith(b"RIFF") and data[8:12] == b"WEBP"
|
|
||||||
)
|
|
||||||
if not valid:
|
|
||||||
logger.warning("Ignoring malformed Agent Plugin logo '{}'", path)
|
|
||||||
return None
|
|
||||||
encoded = base64.b64encode(data).decode("ascii")
|
|
||||||
return f"data:{_LOGO_MIME_TYPES[suffix]};base64,{encoded}"
|
|
||||||
|
|
||||||
|
|
||||||
def _install_command(value: object, plugin_root: Path) -> tuple[str, ...]:
|
def _install_command(value: object, plugin_root: Path) -> tuple[str, ...]:
|
||||||
"""Validate nanobot's optional, shell-free setup command extension."""
|
"""Validate nanobot's optional, shell-free setup command extension."""
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
@@ -60,7 +60,7 @@ class SkillsLoader:
|
|||||||
Returns:
|
Returns:
|
||||||
List of skill info dicts with 'name', 'path', 'source'.
|
List of skill info dicts with 'name', 'path', 'source'.
|
||||||
"""
|
"""
|
||||||
from nanobot.agent.agent_plugins import enabled_agent_plugin_skills
|
from nanobot.agent.plugins import enabled_agent_plugin_skills
|
||||||
|
|
||||||
plugin_skills = enabled_agent_plugin_skills(self.workspace)
|
plugin_skills = enabled_agent_plugin_skills(self.workspace)
|
||||||
skills = self._skill_entries_from_dir(self.workspace_skills, "workspace")
|
skills = self._skill_entries_from_dir(self.workspace_skills, "workspace")
|
||||||
@@ -102,7 +102,7 @@ class SkillsLoader:
|
|||||||
workspace_path = self.workspace_skills / name / "SKILL.md"
|
workspace_path = self.workspace_skills / name / "SKILL.md"
|
||||||
if workspace_path.exists():
|
if workspace_path.exists():
|
||||||
return workspace_path.read_text(encoding="utf-8")
|
return workspace_path.read_text(encoding="utf-8")
|
||||||
from nanobot.agent.agent_plugins import enabled_agent_plugin_skills
|
from nanobot.agent.plugins import enabled_agent_plugin_skills
|
||||||
|
|
||||||
for plugin_skill in enabled_agent_plugin_skills(self.workspace):
|
for plugin_skill in enabled_agent_plugin_skills(self.workspace):
|
||||||
if plugin_skill.name == name and plugin_skill.path.is_file():
|
if plugin_skill.name == name and plugin_skill.path.is_file():
|
||||||
|
|||||||
@@ -1286,7 +1286,7 @@ async def reload_servers(state: Any, registry: ToolRegistry) -> dict[str, Any]:
|
|||||||
"requires_restart": True,
|
"requires_restart": True,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
from nanobot.agent.agent_plugins import agent_plugin_mcp_servers
|
from nanobot.agent.plugins import agent_plugin_mcp_servers
|
||||||
from nanobot.config.loader import load_config, resolve_config_env_vars
|
from nanobot.config.loader import load_config, resolve_config_env_vars
|
||||||
|
|
||||||
config = resolve_config_env_vars(load_config())
|
config = resolve_config_env_vars(load_config())
|
||||||
|
|||||||
@@ -1149,7 +1149,7 @@ Use the `run_cli_app` tool with `name="{name}"` for command execution. Do not in
|
|||||||
shutil.rmtree(legacy_dir)
|
shutil.rmtree(legacy_dir)
|
||||||
|
|
||||||
def _record_installed(self, app: dict[str, Any]) -> dict[str, Any]:
|
def _record_installed(self, app: dict[str, Any]) -> dict[str, Any]:
|
||||||
from nanobot.agent.agent_plugins import set_agent_plugin_enabled
|
from nanobot.agent.plugins import set_agent_plugin_enabled
|
||||||
|
|
||||||
installed = self._load_installed()
|
installed = self._load_installed()
|
||||||
entry = self._installed_entry(app)
|
entry = self._installed_entry(app)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -16,7 +17,11 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, Mapping, cast
|
from typing import Any, Literal, Mapping, cast
|
||||||
|
|
||||||
from nanobot.agent.agent_plugins import agent_plugins_payload, set_agent_plugin_enabled
|
from nanobot.agent.plugins import (
|
||||||
|
AgentPluginState,
|
||||||
|
discover_agent_plugin_states,
|
||||||
|
set_agent_plugin_enabled,
|
||||||
|
)
|
||||||
from nanobot.agent.tools.registry import ToolRegistry
|
from nanobot.agent.tools.registry import ToolRegistry
|
||||||
from nanobot.apps.protocol import app_manifest, compact_dict
|
from nanobot.apps.protocol import app_manifest, compact_dict
|
||||||
from nanobot.config.loader import load_config, resolve_config_env_vars, save_config
|
from nanobot.config.loader import load_config, resolve_config_env_vars, save_config
|
||||||
@@ -49,6 +54,12 @@ _MAX_TEST_TOOLS = 16
|
|||||||
_DEFAULT_TEST_TIMEOUT = 20
|
_DEFAULT_TEST_TIMEOUT = 20
|
||||||
_DEFAULT_CUSTOM_TIMEOUT = 30
|
_DEFAULT_CUSTOM_TIMEOUT = 30
|
||||||
_CUSTOM_ACTIONS = {"custom", "import", "import-cursor", "tools"}
|
_CUSTOM_ACTIONS = {"custom", "import", "import-cursor", "tools"}
|
||||||
|
_PLUGIN_LOGO_MIME_TYPES = {
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".png": "image/png",
|
||||||
|
".webp": "image/webp",
|
||||||
|
}
|
||||||
|
|
||||||
McpReload = Callable[[], Awaitable[dict[str, Any]]]
|
McpReload = Callable[[], Awaitable[dict[str, Any]]]
|
||||||
|
|
||||||
@@ -838,38 +849,45 @@ def _custom_payload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _agent_plugin_payload(plugin: Mapping[str, Any]) -> dict[str, Any]:
|
def _plugin_logo_data_url(path: Path | None) -> str | None:
|
||||||
enabled = bool(plugin.get("enabled"))
|
if path is None:
|
||||||
permissions = plugin.get("permissions")
|
return None
|
||||||
mcp_servers = plugin.get("mcp_servers")
|
try:
|
||||||
permission_names = (
|
data = path.read_bytes()
|
||||||
[str(item) for item in cast(list[object], permissions)]
|
except OSError:
|
||||||
if isinstance(permissions, list)
|
return None
|
||||||
else []
|
suffix = path.suffix.lower()
|
||||||
)
|
valid = (
|
||||||
server_names = (
|
suffix == ".png" and data.startswith(b"\x89PNG\r\n\x1a\n")
|
||||||
[str(item) for item in cast(list[object], mcp_servers)]
|
or suffix in {".jpg", ".jpeg"} and data.startswith(b"\xff\xd8\xff")
|
||||||
if isinstance(mcp_servers, list)
|
or suffix == ".webp" and data.startswith(b"RIFF") and data[8:12] == b"WEBP"
|
||||||
else []
|
|
||||||
)
|
)
|
||||||
|
if not valid:
|
||||||
|
return None
|
||||||
|
encoded = base64.b64encode(data).decode("ascii")
|
||||||
|
return f"data:{_PLUGIN_LOGO_MIME_TYPES[suffix]};base64,{encoded}"
|
||||||
|
|
||||||
|
|
||||||
|
def _agent_plugin_payload(state: AgentPluginState) -> dict[str, Any]:
|
||||||
|
plugin = state.plugin
|
||||||
return {
|
return {
|
||||||
"name": f"plugin-{plugin['name']}",
|
"name": f"plugin-{plugin.name}",
|
||||||
"display_name": str(plugin.get("display_name") or plugin["name"]),
|
"display_name": plugin.display_name,
|
||||||
"category": str(plugin.get("category") or "plugin"),
|
"category": plugin.category,
|
||||||
"description": str(plugin.get("description") or "Agent Plugin"),
|
"description": plugin.description or "Agent Plugin",
|
||||||
"docs_url": str(plugin.get("repository") or ""),
|
"docs_url": plugin.repository,
|
||||||
"transport": "stdio",
|
"transport": "stdio",
|
||||||
"requires": ", ".join(permission_names),
|
"requires": ", ".join(plugin.permissions),
|
||||||
"note": "",
|
"note": "",
|
||||||
"install_supported": True,
|
"install_supported": True,
|
||||||
"installed": True,
|
"installed": True,
|
||||||
"configured": enabled,
|
"configured": state.enabled,
|
||||||
"available": enabled,
|
"available": state.enabled,
|
||||||
"status": "configured" if enabled else "not_installed",
|
"status": "configured" if state.enabled else "not_installed",
|
||||||
"logo_url": plugin.get("logo_url"),
|
"logo_url": _plugin_logo_data_url(plugin.logo),
|
||||||
"brand_color": plugin.get("accent_color"),
|
"brand_color": plugin.accent_color,
|
||||||
"required_fields": [],
|
"required_fields": [],
|
||||||
"connection_summary": ", ".join(server_names),
|
"connection_summary": ", ".join(state.mcp_servers),
|
||||||
"enabled_tools": ["*"],
|
"enabled_tools": ["*"],
|
||||||
"tool_names": [],
|
"tool_names": [],
|
||||||
"source": "agent-plugin",
|
"source": "agent-plugin",
|
||||||
@@ -893,12 +911,16 @@ def mcp_presets_payload(
|
|||||||
for name, cfg in sorted(config.tools.mcp_servers.items())
|
for name, cfg in sorted(config.tools.mcp_servers.items())
|
||||||
if name not in known
|
if name not in known
|
||||||
]
|
]
|
||||||
plugin_state = agent_plugins_payload(config.workspace_path)
|
plugin_states = [
|
||||||
|
state
|
||||||
|
for state in discover_agent_plugin_states(config.workspace_path)
|
||||||
|
if state.mcp_servers or state.plugin.install_command
|
||||||
|
]
|
||||||
existing_names = {str(row["name"]) for row in (*preset_rows, *custom_rows)}
|
existing_names = {str(row["name"]) for row in (*preset_rows, *custom_rows)}
|
||||||
plugin_rows = [
|
plugin_rows = [
|
||||||
row
|
row
|
||||||
for plugin in plugin_state["plugins"]
|
for state in plugin_states
|
||||||
if (row := _agent_plugin_payload(plugin))["name"] not in existing_names
|
if (row := _agent_plugin_payload(state))["name"] not in existing_names
|
||||||
]
|
]
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"presets": [*preset_rows, *custom_rows, *plugin_rows],
|
"presets": [*preset_rows, *custom_rows, *plugin_rows],
|
||||||
@@ -1395,19 +1417,23 @@ async def mcp_presets_settings_action(
|
|||||||
config = load_config()
|
config = load_config()
|
||||||
plugin_name = name.removeprefix("plugin-")
|
plugin_name = name.removeprefix("plugin-")
|
||||||
installed = {
|
installed = {
|
||||||
str(plugin["name"])
|
state.plugin.name
|
||||||
for plugin in agent_plugins_payload(config.workspace_path)["plugins"]
|
for state in discover_agent_plugin_states(config.workspace_path)
|
||||||
|
if state.mcp_servers or state.plugin.install_command
|
||||||
}
|
}
|
||||||
if name not in config.tools.mcp_servers and plugin_name in installed:
|
if name not in config.tools.mcp_servers and plugin_name in installed:
|
||||||
if action not in {"enable", "remove"}:
|
if action not in {"enable", "remove"}:
|
||||||
raise McpPresetError("Agent Plugins support enable and disable actions only")
|
raise McpPresetError("Agent Plugins support enable and disable actions only")
|
||||||
state = await asyncio.to_thread(
|
plugin = await asyncio.to_thread(
|
||||||
set_agent_plugin_enabled,
|
set_agent_plugin_enabled,
|
||||||
config.workspace_path,
|
config.workspace_path,
|
||||||
plugin_name,
|
plugin_name,
|
||||||
action == "enable",
|
action == "enable",
|
||||||
)
|
)
|
||||||
payload = mcp_presets_payload(last_action=state.get("last_action"))
|
verb = "enabled" if action == "enable" else "disabled"
|
||||||
|
payload = mcp_presets_payload(
|
||||||
|
last_action={"ok": True, "message": f"{plugin.display_name} {verb}."}
|
||||||
|
)
|
||||||
if reload_mcp is not None:
|
if reload_mcp is not None:
|
||||||
payload = attach_mcp_hot_reload_result(payload, await reload_mcp())
|
payload = attach_mcp_hot_reload_result(payload, await reload_mcp())
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from urllib.parse import unquote
|
|||||||
from websockets.http11 import Request as WsRequest
|
from websockets.http11 import Request as WsRequest
|
||||||
from websockets.http11 import Response
|
from websockets.http11 import Response
|
||||||
|
|
||||||
from nanobot.agent.agent_plugins import agent_plugins_payload
|
from nanobot.agent.plugins import discover_agent_plugin_states
|
||||||
from nanobot.agent.tools.image_generation import request_image_generation_reload
|
from nanobot.agent.tools.image_generation import request_image_generation_reload
|
||||||
from nanobot.agent.tools.mcp import request_mcp_reload
|
from nanobot.agent.tools.mcp import request_mcp_reload
|
||||||
from nanobot.api.runtime import ApiRuntime, ApiStartOptions, api_runtime_paths
|
from nanobot.api.runtime import ApiRuntime, ApiStartOptions, api_runtime_paths
|
||||||
@@ -1157,8 +1157,9 @@ class WebUISettingsRouter:
|
|||||||
if action == "enable" and name.startswith("plugin-"):
|
if action == "enable" and name.startswith("plugin-"):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
plugin_names = {
|
plugin_names = {
|
||||||
f"plugin-{plugin['name']}"
|
f"plugin-{state.plugin.name}"
|
||||||
for plugin in agent_plugins_payload(config.workspace_path)["plugins"]
|
for state in discover_agent_plugin_states(config.workspace_path)
|
||||||
|
if state.mcp_servers or state.plugin.install_command
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
name not in config.tools.mcp_servers
|
name not in config.tools.mcp_servers
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ from typing import Any, cast
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent import agent_plugins
|
from nanobot.agent import plugins as agent_plugins
|
||||||
from nanobot.agent.agent_plugins import (
|
from nanobot.agent.plugins import (
|
||||||
AGENT_PLUGIN_MCP_SCHEMA,
|
AGENT_PLUGIN_MCP_SCHEMA,
|
||||||
AGENT_PLUGIN_SCHEMA,
|
AGENT_PLUGIN_SCHEMA,
|
||||||
agent_plugin_mcp_servers,
|
agent_plugin_mcp_servers,
|
||||||
agent_plugins_payload,
|
|
||||||
discover_agent_plugin_skills,
|
discover_agent_plugin_skills,
|
||||||
|
discover_agent_plugin_states,
|
||||||
set_agent_plugin_enabled,
|
set_agent_plugin_enabled,
|
||||||
)
|
)
|
||||||
from nanobot.agent.skills import SkillsLoader
|
from nanobot.agent.skills import SkillsLoader
|
||||||
@@ -142,7 +142,7 @@ def test_unknown_manifest_fields_and_non_object_extensions_are_ignored(tmp_path:
|
|||||||
assert [skill.name for skill in discover_agent_plugin_skills(tmp_path)] == ["example"]
|
assert [skill.name for skill in discover_agent_plugin_skills(tmp_path)] == ["example"]
|
||||||
|
|
||||||
|
|
||||||
def test_agent_plugin_payload_embeds_contained_raster_logo(tmp_path: Path) -> None:
|
def test_agent_plugin_discovers_contained_raster_logo(tmp_path: Path) -> None:
|
||||||
plugin = _write_plugin(
|
plugin = _write_plugin(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
"demo",
|
"demo",
|
||||||
@@ -155,22 +155,7 @@ def test_agent_plugin_payload_embeds_contained_raster_logo(tmp_path: Path) -> No
|
|||||||
assets = plugin / "assets"
|
assets = plugin / "assets"
|
||||||
assets.mkdir()
|
assets.mkdir()
|
||||||
(assets / "icon.png").write_bytes(b"\x89PNG\r\n\x1a\nlogo")
|
(assets / "icon.png").write_bytes(b"\x89PNG\r\n\x1a\nlogo")
|
||||||
executable = plugin / "bin" / "server"
|
assert agent_plugins.discover_agent_plugins(tmp_path)[0].logo == assets / "icon.png"
|
||||||
executable.parent.mkdir()
|
|
||||||
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
||||||
(plugin / "mcp.json").write_text(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"$schema": AGENT_PLUGIN_MCP_SCHEMA,
|
|
||||||
"mcpServers": {"demo": {"type": "stdio", "command": "./bin/server"}},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
logo_url = agent_plugins_payload(tmp_path)["plugins"][0]["logo_url"]
|
|
||||||
|
|
||||||
assert logo_url == "data:image/png;base64,iVBORw0KGgpsb2dv"
|
|
||||||
|
|
||||||
|
|
||||||
def test_agent_plugin_logo_cannot_escape_package(tmp_path: Path) -> None:
|
def test_agent_plugin_logo_cannot_escape_package(tmp_path: Path) -> None:
|
||||||
@@ -351,7 +336,7 @@ def test_plugin_setup_command_runs_once_per_version(
|
|||||||
assert calls[0][0] == (str(executable),)
|
assert calls[0][0] == (str(executable),)
|
||||||
assert calls[0][1]["PLUGIN_ROOT"] == str(plugin)
|
assert calls[0][1]["PLUGIN_ROOT"] == str(plugin)
|
||||||
assert "NANOBOT_TEST_SECRET" not in calls[0][1]
|
assert "NANOBOT_TEST_SECRET" not in calls[0][1]
|
||||||
assert agent_plugins_payload(tmp_path)["plugins"][0]["setup_required"] is False
|
assert discover_agent_plugin_states(tmp_path)[0].setup_required is False
|
||||||
|
|
||||||
|
|
||||||
def test_concurrent_plugin_enable_runs_setup_once(
|
def test_concurrent_plugin_enable_runs_setup_once(
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent import agent_plugins
|
from nanobot.agent import plugins as agent_plugins
|
||||||
from nanobot.agent.agent_plugins import discover_agent_plugin_skills
|
from nanobot.agent.plugins import discover_agent_plugin_skills
|
||||||
from nanobot.agent.skills import SkillsLoader
|
from nanobot.agent.skills import SkillsLoader
|
||||||
from nanobot.apps.cli.service import CliAppError, CliAppManager, CliAppsRuntimeConfig
|
from nanobot.apps.cli.service import CliAppError, CliAppManager, CliAppsRuntimeConfig
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent.agent_plugins import AGENT_PLUGIN_MCP_SCHEMA, AGENT_PLUGIN_SCHEMA
|
from nanobot.agent.plugins import AGENT_PLUGIN_MCP_SCHEMA, AGENT_PLUGIN_SCHEMA
|
||||||
from nanobot.config.loader import load_config
|
from nanobot.config.loader import load_config
|
||||||
from nanobot.webui.mcp_presets_api import (
|
from nanobot.webui.mcp_presets_api import (
|
||||||
McpPresetError,
|
McpPresetError,
|
||||||
|
|||||||
Reference in New Issue
Block a user