Ctrl K
ComunidadGuías…

butler/tools/config_tools.py

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

Archivo original ↗Preguntar a KiBot
"""Butler config query/set tool — lets the Agent read and modify BUTLER_* settings."""

from __future__ import annotations

import json
import logging
from typing import Any, Callable

logger = logging.getLogger(__name__)


def tool_butler_config(action: str = "list", key: str = "", value: str = "", category: str = "", **_: Any) -> str:
    from butler.configuration.service import config_get, config_list, config_set, config_categories

    action = (action or "list").strip().lower()

    if action == "categories":
        return json.dumps({"categories": config_categories()}, ensure_ascii=False)

    if action == "list":
        items = config_list(category)
        rows = []
        for cv in items:
            row = {"key": cv.key, "value": cv.effective, "source": cv.source}
            if cv.meta:
                row["description"] = cv.meta.description
                row["category"] = cv.meta.category
            rows.append(row)
        return json.dumps({"ok": True, "count": len(rows), "items": rows}, ensure_ascii=False)

    if action == "get":
        if not key:
            return json.dumps({"error": "key is required for action=get"})
        cv = config_get(key)
        result: dict[str, Any] = {"key": cv.key, "value": cv.effective, "source": cv.source}
        if cv.meta:
            result["description"] = cv.meta.description
            result["default"] = cv.meta.default
            result["category"] = cv.meta.category
        return json.dumps(result, ensure_ascii=False)

    if action == "set":
        if not key or not value:
            return json.dumps({"error": "key and value are required for action=set"})
        # Sprint 9 SEC-9.5: set 路径在调 config_set 之前做 owner 二次校验,
        # 防止 prompt 注入 → Agent 调 butler_config 改运行时配置。
        # R1-10: route through ``is_current_turn_owner`` / ``owner_required_message``
        # in ``butler.execution_context`` so tools → gateway stays a one-way
        # dependency. 平台硬编 wechat(CLI / BYPASS 走 BUTLER_PROJECT_CREATE_OPEN)。
        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 = str(get_current_session_key() or "").strip()
        cid = chat_id_from_session_key(sk) if sk else ""
        if not is_current_turn_owner(platform="wechat", external_id=cid, session_key=sk):
            logger.warning(
                "config_set denied: sk=%s cid=%s key=%s", sk, cid, key
            )
            return json.dumps({
                "ok": False,
                "message": owner_required_message(),
                "needs_reset": False,
                "needs_restart": False,
            }, ensure_ascii=False)
        r = config_set(key, value)
        return json.dumps({
            "ok": r.ok, "message": r.message,
            "needs_reset": r.needs_reset, "needs_restart": r.needs_restart,
        }, ensure_ascii=False)

    return json.dumps({"error": f"unknown action: {action}; use list/get/set/categories"})


def register_config_tools(register_fn: Callable[..., None]) -> None:
    register_fn(
        name="butler_config",
        description=(
            "Query or modify Butler system configuration. "
            "action=list to browse settings (optionally filter by category), "
            "action=get to read one key, "
            "action=set to change a key value at runtime."
        ),
        schema={
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "get", "set", "categories"],
                    "description": "Operation: list/get/set/categories",
                },
                "key": {"type": "string", "description": "BUTLER_* variable name"},
                "value": {"type": "string", "description": "New value (for action=set)"},
                "category": {"type": "string", "description": "Filter category (for action=list)"},
            },
            "required": ["action"],
        },
        handler=tool_butler_config,
        toolset="system",
    )


__all__ = ["register_config_tools", "tool_butler_config"]