from __future__ import annotations
import json
import logging
import os
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence, Tuple
from .federate import FEDERATED_TOOLSET
logger = logging.getLogger(__name__)
ToolSpec = Tuple[str, Optional[str], Any]
ListTools = Callable[[], Sequence[ToolSpec]]
Dispatch = Callable[[str, Dict[str, Any]], Awaitable[Any]]
Approve = Callable[[str, Dict[str, Any]], Awaitable[Optional[bool]]]
IsDangerous = Callable[[str], bool]
class LocalToolProvider:
def __init__(
self,
mesh: Any,
list_tools: ListTools,
dispatch: Dispatch,
approve: Approve,
is_dangerous: IsDangerous,
) -> None:
self._mesh = mesh
self._list_tools = list_tools
self._dispatch = dispatch
self._approve = approve
self._is_dangerous = is_dangerous
self._handle: Any = None
def start(self) -> List[str]:
if self._handle is not None:
return self.published
specs: List[Tuple[str, Optional[str], str]] = []
for name, description, schema in self._list_tools():
schema_json = (
schema if isinstance(schema, str) else json.dumps(schema or {"type": "object"})
)
specs.append((name, description, schema_json))
if not specs:
logger.info("net plugin: no local tools to publish to the mesh")
return []
self._handle = self._mesh.publish_tools(specs, self._callback, allow_any_caller=True)
logger.info(
"net plugin: published %d local tools to the mesh (%s)",
len(self.published),
", ".join(self.published),
)
return self.published
def stop(self) -> None:
handle, self._handle = self._handle, None
if handle is not None:
try:
handle.stop()
except Exception: logger.debug("net plugin: local-tool publication stop failed", exc_info=True)
@property
def published(self) -> List[str]:
return list(self._handle.tools) if self._handle is not None else []
async def _callback(self, name: str, args_json: str):
try:
args = json.loads(args_json) if args_json else {}
except (TypeError, ValueError):
args = {}
if not isinstance(args, dict):
args = {}
if self._is_dangerous(name):
try:
decision = await self._approve(name, args)
except Exception as e: logger.warning("net plugin: approval for %s errored: %s", name, e)
return (_deny_body(name, "approval_error", str(e)), True)
if decision is None:
return (
_deny_body(
name,
"approval_unreachable",
"no operator approval surface is reachable on this machine; "
"configure one before invoking dangerous tools remotely",
),
True,
)
if decision is not True:
return (
_deny_body(name, "denied", "the operator declined this invocation"),
True,
)
try:
result = await self._dispatch(name, args)
except Exception as e: logger.warning("net plugin: dispatch of %s failed: %s", name, e)
return (_deny_body(name, "error", str(e)), True)
return result if isinstance(result, str) else json.dumps(result)
def _deny_body(name: str, status: str, message: str) -> str:
return json.dumps({"status": status, "tool": name, "message": message})
_OWN_TOOLSETS = frozenset({"net", "net-pinned", FEDERATED_TOOLSET})
_DANGEROUS_HINTS = (
"terminal",
"shell",
"exec",
"command",
"run",
"process",
"write",
"edit",
"delete",
"remove",
"desktop",
"computer",
"browser",
"click",
"keyboard",
)
def name_looks_dangerous(name: str) -> bool:
low = name.lower()
safe_hints = ("read", "get", "list", "search", "describe", "status", "info")
if any(h in low for h in _DANGEROUS_HINTS):
return True
if any(low.startswith(h) or f"_{h}" in low for h in safe_hints):
return False
return True
def start_local_tool_provider(mesh: Any) -> Optional[LocalToolProvider]:
try:
adapters = _hermes_adapters()
except Exception as e: logger.warning(
"net plugin: local-tool publishing not started — could not wire the "
"Hermes registry/approval adapters (%s). Publishing OWN tools to the "
"mesh needs a real Hermes host; the mesh consume/enroll features are "
"unaffected.",
e,
)
return None
provider = LocalToolProvider(mesh, *adapters)
try:
provider.start()
except Exception as e: logger.warning("net plugin: local-tool publication failed to start: %s", e)
return None
return provider
def _hermes_adapters() -> Tuple[ListTools, Dispatch, Approve, IsDangerous]:
from tools.registry import registry
def list_tools() -> Sequence[ToolSpec]:
specs: List[ToolSpec] = []
for name, entry in dict(getattr(registry, "tools", {})).items():
toolset = getattr(entry, "toolset", None)
if toolset in _OWN_TOOLSETS:
continue
schema = getattr(entry, "schema", None) or {}
params = schema.get("parameters") if isinstance(schema, dict) else None
if not isinstance(params, dict):
params = {"type": "object", "properties": {}, "additionalProperties": True}
description = getattr(entry, "description", None) or (
schema.get("description") if isinstance(schema, dict) else None
)
specs.append((name, description, params))
return specs
async def dispatch(name: str, args: Dict[str, Any]) -> Any:
return await registry.dispatch(name, args)
async def approve(name: str, args: Dict[str, Any]) -> Optional[bool]:
try:
from tools import approval except Exception: return None
request = getattr(approval, "request_operator_approval", None)
if request is None:
return None
return await request(name, args)
def is_dangerous(name: str) -> bool:
entry = dict(getattr(registry, "tools", {})).get(name)
flag = getattr(entry, "requires_approval", None) if entry is not None else None
if isinstance(flag, bool):
return flag
return name_looks_dangerous(name)
return list_tools, dispatch, approve, is_dangerous