Ctrl K
ComunidadGuías…

butler/tools/execute_code.py

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

Archivo original ↗Preguntar a KiBot
"""Restricted execute_code tool (Hermes PTC subset, default OFF)."""

from __future__ import annotations

import json
import os
import tempfile
from pathlib import Path

from typing import Any, Callable, cast

from butler.utilities.env_parse import env_truthy
from butler.defaults.env_defaults import (
    EXECUTE_CODE_DEFAULT,
    EXECUTE_CODE_ALLOW_NETWORK_DEFAULT,
)

_MAX_CODE_CHARS = 8000
_DEFAULT_TIMEOUT = 30


def execute_code_enabled() -> bool:
    return bool(env_truthy("BUTLER_EXECUTE_CODE", default=EXECUTE_CODE_DEFAULT))


def execute_code_timeout_seconds() -> int:
    try:
        from butler.utilities.env_parse import int_env

        return int(int_env("BUTLER_EXECUTE_CODE_TIMEOUT", _DEFAULT_TIMEOUT, min=5, max=120))
    except ValueError:
        return _DEFAULT_TIMEOUT


def execute_code_allow_network() -> bool:
    return bool(env_truthy("BUTLER_EXECUTE_CODE_ALLOW_NETWORK", default=EXECUTE_CODE_ALLOW_NETWORK_DEFAULT))


def _workspace_cwd() -> Path:
    from butler.tools.execute_code_ops import resolve_execute_code_workspace_safe

    resolved = resolve_execute_code_workspace_safe()
    return resolved if resolved is not None else Path.cwd().resolve()


def run_execute_code(code: str, *, language: str = "python") -> dict[str, Any]:
    if not execute_code_enabled():
        return {
            "ok": False,
            "error": "execute_code 未启用(设置 BUTLER_EXECUTE_CODE=1 并经安全评审)",
            "code": "EXECUTE_CODE_DISABLED",
        }

    text = (code or "").strip()
    if not text:
        return {"ok": False, "error": "empty code", "code": "EXECUTE_CODE_EMPTY"}
    if len(text) > _MAX_CODE_CHARS:
        return {
            "ok": False,
            "error": f"code exceeds {_MAX_CODE_CHARS} chars",
            "code": "EXECUTE_CODE_TOO_LARGE",
        }

    lang = (language or "python").strip().lower()
    if lang not in ("python", "py"):
        return {"ok": False, "error": f"unsupported language: {language}", "code": "EXECUTE_CODE_LANG"}

    cwd = _workspace_cwd()
    env = {
        "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
        "HOME": os.environ.get("HOME", "/tmp"),
        "LANG": "C.UTF-8",
        "PYTHONNOUSERSITE": "1",
        "PYTHONDONTWRITEBYTECODE": "1",
    }
    if not execute_code_allow_network():
        env["HTTP_PROXY"] = ""
        env["HTTPS_PROXY"] = ""
        env["ALL_PROXY"] = ""
        env["NO_PROXY"] = "*"

    with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as fh:
        fh.write(text)
        script = fh.name

    try:
        from butler.tools.execute_code_ops import run_python_subprocess_safe

        out = run_python_subprocess_safe(
            script=script,
            cwd=cwd,
            timeout=execute_code_timeout_seconds(),
            env=env,
        )
        if out is None:
            return {"ok": False, "error": "execute_code failed", "code": "EXECUTE_CODE_ERROR"}
        return cast(dict[str, Any], out)
    finally:
        try:
            Path(script).unlink(missing_ok=True)
        except OSError:
            pass


def register_execute_code_tool(register_fn: Callable[..., None]) -> None:
    if not execute_code_enabled():
        return

    def _handler(args: dict[str, Any]) -> str:
        out = run_execute_code(
            str(args.get("code") or ""),
            language=str(args.get("language") or "python"),
        )
        return json.dumps(out, ensure_ascii=False)

    register_fn(
        name="execute_code",
        description=(
            "Run short Python snippets in an isolated subprocess (no network by default). "
            "Requires BUTLER_EXECUTE_CODE=1."
        ),
        schema={
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "Python source to execute"},
                "language": {"type": "string", "default": "python"},
            },
            "required": ["code"],
        },
        handler=_handler,
        toolset="sandbox",
    )