mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 16:38:49 +00:00
181 lines
5.0 KiB
Python
181 lines
5.0 KiB
Python
from pathlib import Path
|
|
|
|
from nanobot.agent.tools.registry import ToolRegistry
|
|
from nanobot.command.router import CommandRouter
|
|
from nanobot.extensions import ExtensionManifest
|
|
from nanobot.extensions.registry import (
|
|
ExtensionCandidate,
|
|
ExtensionSnapshot,
|
|
)
|
|
from nanobot.extensions.runtime import ExtensionRuntimeManager
|
|
|
|
|
|
def _candidate(root: Path, extension_id: str = "test.python") -> ExtensionCandidate:
|
|
return ExtensionCandidate(
|
|
ExtensionManifest(
|
|
id=extension_id,
|
|
name="Python extension",
|
|
version="1.0.0",
|
|
entry="extension:register",
|
|
),
|
|
location=root,
|
|
trusted=True,
|
|
)
|
|
|
|
|
|
def _snapshot(*candidates: ExtensionCandidate) -> ExtensionSnapshot:
|
|
return ExtensionSnapshot(candidates, ())
|
|
|
|
|
|
def _write_extension(root: Path, result: str = "ok") -> None:
|
|
(root / "extension.py").write_text(
|
|
f"""
|
|
from nanobot.agent.hook import AgentHook
|
|
from nanobot.agent.tools.base import Tool
|
|
|
|
class ExtensionTool(Tool):
|
|
@property
|
|
def name(self):
|
|
return "extension_echo"
|
|
|
|
@property
|
|
def description(self):
|
|
return "Echo from an extension"
|
|
|
|
@property
|
|
def parameters(self):
|
|
return {{"type": "object", "properties": {{}}}}
|
|
|
|
async def execute(self):
|
|
return "{result}"
|
|
|
|
async def command(_context):
|
|
return None
|
|
|
|
def hook_factory(_context):
|
|
return AgentHook()
|
|
|
|
def register(api):
|
|
api.register_tool(ExtensionTool())
|
|
api.register_command("extension", command)
|
|
api.register_hook_factory(hook_factory)
|
|
"""
|
|
)
|
|
|
|
|
|
async def test_python_extension_registers_and_removes_owned_capabilities(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_write_extension(tmp_path)
|
|
tools = ToolRegistry()
|
|
commands = CommandRouter()
|
|
hooks = []
|
|
manager = ExtensionRuntimeManager(
|
|
tools=tools,
|
|
commands=commands,
|
|
hook_factories=hooks,
|
|
)
|
|
|
|
result = await manager.activate(_snapshot(_candidate(tmp_path)))
|
|
|
|
assert result.diagnostics == ()
|
|
assert tools.owner("extension_echo") == "test.python"
|
|
assert commands.owner("exact", "/extension") == "test.python"
|
|
assert len(hooks) == 1
|
|
assert await tools.get("extension_echo").execute() == "ok"
|
|
|
|
await manager.close()
|
|
|
|
assert tools.owner("extension_echo") is None
|
|
assert commands.owner("exact", "/extension") is None
|
|
assert hooks == []
|
|
|
|
|
|
async def test_failed_registration_rolls_back_partial_state(tmp_path: Path) -> None:
|
|
(tmp_path / "extension.py").write_text(
|
|
"""
|
|
from nanobot.agent.tools.base import Tool
|
|
|
|
class DuplicateTool(Tool):
|
|
name = "duplicate"
|
|
description = "Duplicate"
|
|
parameters = {"type": "object", "properties": {}}
|
|
async def execute(self):
|
|
return "ok"
|
|
|
|
async def command(_context):
|
|
return None
|
|
|
|
def register(api):
|
|
api.register_tool(DuplicateTool())
|
|
api.register_command("duplicate", command)
|
|
"""
|
|
)
|
|
tools = ToolRegistry()
|
|
commands = CommandRouter()
|
|
|
|
async def core_handler(_context):
|
|
return None
|
|
|
|
commands.exact("/duplicate", core_handler)
|
|
manager = ExtensionRuntimeManager(
|
|
tools=tools,
|
|
commands=commands,
|
|
)
|
|
|
|
result = await manager.activate(_snapshot(_candidate(tmp_path)))
|
|
|
|
assert result.extensions == ()
|
|
assert result.diagnostics[0].code == "activation_failed"
|
|
assert tools.get("duplicate") is None
|
|
assert commands.owner("exact", "/duplicate") == "nanobot.core"
|
|
|
|
|
|
async def test_python_extension_reloads_updated_source(tmp_path: Path) -> None:
|
|
tools = ToolRegistry()
|
|
candidate = _candidate(tmp_path)
|
|
_write_extension(tmp_path, "first")
|
|
first = ExtensionRuntimeManager(tools=tools, commands=CommandRouter())
|
|
|
|
await first.activate(_snapshot(candidate))
|
|
assert await tools.get("extension_echo").execute() == "first"
|
|
await first.close()
|
|
|
|
_write_extension(tmp_path, "later")
|
|
second = ExtensionRuntimeManager(tools=tools, commands=CommandRouter())
|
|
await second.activate(_snapshot(candidate))
|
|
|
|
assert await tools.get("extension_echo").execute() == "later"
|
|
await second.close()
|
|
|
|
|
|
async def test_extensions_with_the_same_entry_module_are_isolated(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
tools = ToolRegistry()
|
|
manager = ExtensionRuntimeManager(tools=tools, commands=CommandRouter())
|
|
first_root = tmp_path / "first"
|
|
second_root = tmp_path / "second"
|
|
first_root.mkdir()
|
|
second_root.mkdir()
|
|
_write_extension(first_root, "first")
|
|
_write_extension(second_root, "second")
|
|
second_source = (second_root / "extension.py").read_text()
|
|
(second_root / "extension.py").write_text(
|
|
second_source
|
|
.replace("extension_echo", "second_echo")
|
|
.replace('register_command("extension"', 'register_command("second"')
|
|
)
|
|
|
|
result = await manager.activate(
|
|
_snapshot(
|
|
_candidate(first_root, "test.first"),
|
|
_candidate(second_root, "test.second"),
|
|
)
|
|
)
|
|
|
|
assert result.diagnostics == ()
|
|
assert await tools.get("extension_echo").execute() == "first"
|
|
assert await tools.get("second_echo").execute() == "second"
|
|
await manager.close()
|