Ctrl K
ComunidadGuías…

butler/tools/registry_tools.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Registry tools: search, propose install, and Owner-gated direct install."""

from __future__ import annotations

import json
from typing import Any, Callable, cast

from butler.registry.skill_service import SkillRegistryService
from butler.tools.registry_tools_ops import (
    default_registry_tenant_id,
    install_skill_confirmed_json,
)


def register_registry_tools(register: Callable[..., None]) -> None:
    register(
        name="registry_search_skills",
        description=(
            "【catalog·read-only】Query skill indexes (bundled/github/marketplace); "
            "returns identifier hits. Zero download, zero filesystem writes."
        ),
        schema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query or URL"},
                "source": {
                    "type": "string",
                    "description": "all | bundled | project | github | url | clawhub | marketplace | lobehub",
                    "default": "all",
                },
            },
            "required": ["query"],
        },
        handler=_tool_registry_search_skills,
        toolset="registry",
    )

    register(
        name="registry_propose_skill_install",
        description=(
            "【owner-hint·text】Compose slash/WeChat install instructions for an identifier. "
            "Returns command text only; does not copy files."
        ),
        schema={
            "type": "object",
            "properties": {
                "identifier": {"type": "string", "description": "Skill identifier"},
            },
            "required": ["identifier"],
        },
        handler=_tool_registry_propose_skill_install,
        toolset="registry",
    )

    register(
        name="registry_install_skill",
        description=(
            "【install·mutation】Copy skill package into tenant store after Owner gate. "
            "Requires identifier; performs download/write."
        ),
        schema={
            "type": "object",
            "properties": {
                "identifier": {"type": "string", "description": "Skill identifier from search results"},
                "source": {"type": "string", "default": "", "description": "Source hint (github/clawhub/lobehub)"},
            },
            "required": ["identifier"],
        },
        handler=_tool_registry_install_skill,
        toolset="registry",
    )

    register(
        name="registry_search_mcp",
        description="Search curated MCP server catalog (read-only). Owner installs via butler mcp add or /mcp 安装.",
        schema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "default": ""},
            },
        },
        handler=_tool_registry_search_mcp,
        toolset="registry",
    )


def _tool_registry_search_skills(query: str, source: str = "all", **_: Any) -> str:
    tenant = default_registry_tenant_id()
    svc = SkillRegistryService(tenant_id=tenant)
    hits = svc.search(query, source_filter=source or "all")
    payload = [
        {
            "name": h.name,
            "identifier": h.identifier,
            "source": h.source,
            "trust": h.trust,
            "description": h.description[:300],
        }
        for h in hits
    ]
    return json.dumps({"skills": payload}, ensure_ascii=False)


def _tool_registry_propose_skill_install(identifier: str, **_: Any) -> str:
    svc = SkillRegistryService()
    return json.dumps(
        {
            "action": "propose_only",
            "message": svc.propose_install_command(identifier),
        },
        ensure_ascii=False,
    )


def _tool_registry_install_skill(identifier: str, source: str = "", **_: Any) -> str:
    """Install a skill — requires Owner confirmation.

    Sprint 19-1 SEC-19-A-1: 旧实现 import `butler.human_gate.is_owner_context`
    但该符号根本不存在, try/except ImportError 静默吞掉, owner gate 完全 no-op.
    改用 is_gateway_owner 真源 (Sprint 18-1 单一真源), tool 上下文从
    get_current_session_key() 解析 chat_id, 无 session_key 时 fail-closed.

    R1-10: route through ``is_current_turn_owner`` / ``owner_required_message``
    in ``butler.execution_context`` so tools → gateway stays a one-way dependency.
    """
    from butler.execution_context import (
        get_current_session_key,
        is_current_turn_owner,
        owner_required_message,
    )
    from butler.session.keys import chat_id_from_session_key

    sk = get_current_session_key() or ""
    cid = chat_id_from_session_key(sk) if sk else ""
    if not is_current_turn_owner(platform="wechat", external_id=cid, session_key=sk):
        return json.dumps({
            "error": owner_required_message(),
            "hint": f"请主人通过 /技能 安装 {identifier} 触发 (微信 owner 账号)",
        }, ensure_ascii=False)

    svc = SkillRegistryService()
    return cast(str, install_skill_confirmed_json(svc, identifier))


def _tool_registry_search_mcp(query: str = "", **_: Any) -> str:
    from butler.registry.mcp_catalog import McpCatalogService

    svc = McpCatalogService()
    entries = svc.search(query or "")
    payload = [
        {
            "id": e.id,
            "title": e.title,
            "description": e.description[:200],
            "transport": e.transport,
        }
        for e in entries
    ]
    return json.dumps({"mcp_catalog": payload}, ensure_ascii=False)