Restringir acceso a herramientas en Python: Ejemplo completo

Ejemplo runnable completo con tool allowlist, action allowlist y bloqueo de llamadas prohibidas.
En esta página
  1. Que demuestra este ejemplo
  2. Estructura del proyecto
  3. Como ejecutar
  4. Que construimos en codigo
  5. Codigo
  6. tools.py — herramientas reales
  7. gateway.py — policy gateway (capa clave)
  8. llm.py — modelo y tool schemas
  9. main.py — ciclo del agente con verificacion de politicas
  10. requirements.txt
  11. Ejemplo de salida
  12. Por que este es un enfoque de produccion
  13. Donde profundizar despues
  14. Codigo completo en GitHub

Esta es la implementacion completa del ejemplo del articulo Como restringir el acceso a herramientas.

Si aun no leiste el articulo, empieza por ahi. Aqui el foco es el codigo: como funcionan exactamente las restricciones en runtime.


Que demuestra este ejemplo

  • Nivel 1 (tool access): que herramientas puede invocar el agente en general
  • Nivel 2 (action access): que acciones estan permitidas dentro de una herramienta accesible
  • Policy gateway: verificacion centralizada de solicitudes antes de ejecutar
  • Comportamiento de fallback: el agente recibe un error y elige el siguiente paso seguro

Estructura del proyecto

TEXT
foundations/
└── tool-calling/
    └── python/
        ├── main.py           # agent loop
        ├── llm.py            # model + tool schemas
        ├── gateway.py        # policy checks + execution
        ├── tools.py          # tools (system actions)
        └── requirements.txt

Esta separacion es importante: el modelo propone una accion, y gateway.py decide si esa accion se ejecuta o no.


Como ejecutar

1. Clona el repositorio y entra a la carpeta:

BASH
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd foundations/tool-calling/python

2. Instala dependencias:

BASH
pip install -r requirements.txt

3. Configura la API key:

BASH
export OPENAI_API_KEY="sk-..."

4. Ejecuta:

BASH
python main.py

Que construimos en codigo

Construimos un robot prudente al que no se le permite hacer todo sin control.

  • La IA puede pedir cualquier accion
  • un "guardian" especial (gateway) verifica si esta permitida
  • si esta prohibida, el robot no hace nada peligroso y explica una alternativa segura

Es como una puerta con cerradura: sin permiso, el comando no pasa.


Codigo

tools.py — herramientas reales

PYTHON
from typing import Any

CUSTOMERS = {
    101: {"id": 101, "name": "Anna", "tier": "free", "email": "anna@gmail.com"},
    202: {"id": 202, "name": "Max", "tier": "pro", "email": "max@company.local"},
}


def customer_db(action: str, customer_id: int, new_tier: str | None = None) -> dict[str, Any]:
    customer = CUSTOMERS.get(customer_id)
    if not customer:
        return {"ok": False, "error": f"customer {customer_id} not found"}

    if action == "read":
        return {"ok": True, "customer": customer}

    if action == "update_tier":
        if not new_tier:
            return {"ok": False, "error": "new_tier is required"}
        customer["tier"] = new_tier
        return {"ok": True, "customer": customer}

    return {"ok": False, "error": f"unknown action '{action}'"}


def email_service(to: str, subject: str, body: str) -> dict[str, Any]:
    return {
        "ok": True,
        "status": "queued",
        "to": to,
        "subject": subject,
        "preview": body[:80],
    }

Son funciones Python normales. El riesgo empieza cuando el agente puede invocarlas sin control.


gateway.py — policy gateway (capa clave)

PYTHON
import json
from typing import Any

from tools import customer_db, email_service

TOOL_REGISTRY = {
    "customer_db": customer_db,
    "email_service": email_service,
}

# Level 1: which tools are visible to the agent
ALLOWED_TOOLS = {"customer_db"}

# Level 2: which actions are allowed inside each tool
ALLOWED_ACTIONS = {
    "customer_db": {"read"},  # update_tier is blocked
}


def execute_tool_call(tool_name: str, arguments_json: str) -> dict[str, Any]:
    if tool_name not in ALLOWED_TOOLS:
        return {"ok": False, "error": f"tool '{tool_name}' is not allowed"}

    tool = TOOL_REGISTRY.get(tool_name)
    if tool is None:
        return {"ok": False, "error": f"tool '{tool_name}' not found"}

    try:
        args = json.loads(arguments_json or "{}")
    except json.JSONDecodeError:
        return {"ok": False, "error": "invalid JSON arguments"}

    if tool_name == "customer_db":
        action = args.get("action")
        if action not in ALLOWED_ACTIONS["customer_db"]:
            return {
                "ok": False,
                "error": f"action '{action}' is not allowed for tool '{tool_name}'",
            }

    try:
        result = tool(**args)
    except TypeError as exc:
        return {"ok": False, "error": f"invalid arguments: {exc}"}

    return {"ok": True, "tool": tool_name, "result": result}

Aqui es donde se enforcean las reglas. El modelo no puede saltarse esta capa via prompt.


llm.py — modelo y tool schemas

PYTHON
import os
from openai import OpenAI

api_key = os.environ.get("OPENAI_API_KEY")

if not api_key:
    raise EnvironmentError(
        "OPENAI_API_KEY is not set.\n"
        "Run: export OPENAI_API_KEY='sk-...'"
    )

client = OpenAI(api_key=api_key)

SYSTEM_PROMPT = """
You are a support agent.
Use tools when data is missing.
If a tool or action is blocked, do not argue; suggest a safe manual next step.
Reply briefly in English.
""".strip()

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "customer_db",
            "description": "Customer data operations: read or update tier",
            "parameters": {
                "type": "object",
                "properties": {
                    "action": {"type": "string", "enum": ["read", "update_tier"]},
                    "customer_id": {"type": "integer"},
                    "new_tier": {"type": "string"},
                },
                "required": ["action", "customer_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "email_service",
            "description": "Sends an email to the customer",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {"type": "string"},
                    "subject": {"type": "string"},
                    "body": {"type": "string"},
                },
                "required": ["to", "subject", "body"],
            },
        },
    },
]


def ask_model(messages: list[dict]):
    completion = client.chat.completions.create(
        model="gpt-4.1-mini",
        messages=[{"role": "system", "content": SYSTEM_PROMPT}, *messages],
        tools=TOOLS,
        tool_choice="auto",
    )
    return completion.choices[0].message

Ojo: llm.py puede mostrarle mas herramientas al modelo, pero gateway.py igual bloquea las no permitidas.


main.py — ciclo del agente con verificacion de politicas

PYTHON
import json

from gateway import execute_tool_call
from llm import ask_model

MAX_STEPS = 6

TASK = (
    "For customer_id=101, check the profile, upgrade tier to pro, "
    "and send a confirmation email to anna@gmail.com. "
    "If any action is blocked, explain the safe manual next step."
)


def to_assistant_message(message) -> dict:
    tool_calls = []
    for tc in message.tool_calls or []:
        tool_calls.append(
            {
                "id": tc.id,
                "type": "function",
                "function": {
                    "name": tc.function.name,
                    "arguments": tc.function.arguments,
                },
            }
        )

    return {
        "role": "assistant",
        "content": message.content or "",
        "tool_calls": tool_calls,
    }


def run():
    messages: list[dict] = [{"role": "user", "content": TASK}]

    for step in range(1, MAX_STEPS + 1):
        print(f"\n=== STEP {step} ===")
        assistant = ask_model(messages)
        messages.append(to_assistant_message(assistant))

        if assistant.content and assistant.content.strip():
            print("Assistant:", assistant.content.strip())

        tool_calls = assistant.tool_calls or []
        if not tool_calls:
            print("\nDone: model finished the task.")
            return

        for tc in tool_calls:
            print(f"Tool call: {tc.function.name}({tc.function.arguments})")
            execution = execute_tool_call(
                tool_name=tc.function.name,
                arguments_json=tc.function.arguments,
            )
            print("Gateway result:", execution)

            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(execution, ensure_ascii=False),
                }
            )

    print("\nStop: MAX_STEPS reached.")


if __name__ == "__main__":
    run()

Aqui se ve bien el boundary: el modelo propone, gateway decide si la accion ocurre o no.


requirements.txt

TEXT
openai>=1.0.0

Ejemplo de salida

TEXT
=== STEP 1 ===
Tool call: customer_db({"action":"read","customer_id":101})
Gateway result: {'ok': True, 'tool': 'customer_db', 'result': {'ok': True, 'customer': {'id': 101, 'name': 'Anna', 'tier': 'free', 'email': 'anna@gmail.com'}}}

=== STEP 2 ===
Tool call: customer_db({"action":"update_tier","customer_id":101,"new_tier":"pro"})
Gateway result: {'ok': False, 'error': "action 'update_tier' is not allowed for tool 'customer_db'"}

=== STEP 3 ===
Tool call: email_service({"to":"anna@gmail.com","subject":"Tier updated","body":"..."})
Gateway result: {'ok': False, 'error': "tool 'email_service' is not allowed"}

=== STEP 4 ===
Assistant: I can only read the profile. Tier updates and email sending require manual operator action.

Done: model finished the task.

Nota: la cantidad de STEP puede variar entre ejecuciones.
En un STEP, el modelo puede devolver varios tool_calls, por eso a veces veras 2 pasos y a veces 4.
Esta es una no determinacion normal del LLM. Lo importante es que el policy gateway bloquea de forma estable update_tier y email_service.


Por que este es un enfoque de produccion

Tool calling ingenuoCon policy gateway
El modelo decide todo por si solo
Hay control de acceso centralizado
Se pueden separar acciones read/write
Los errores se convierten en fallback controlado

Donde profundizar despues

  • Agrega ALLOWED_TOOLS_BY_ROLE (por ejemplo viewer, operator, admin)
  • Agrega un approval-flow para update_tier en lugar de bloqueo total
  • Agrega max_tool_calls y max_cost junto a MAX_STEPS
  • Registra tool_name, args_hash, decision, reason para auditoria

Codigo completo en GitHub

En el repositorio esta la version completa de esta demo: tool loop, verificaciones allowlist y fallback controlado.

Ver codigo completo en GitHub ↗
⏱️ 7 min de lecturaActualizado 2 de marzo de 2026Dificultad: ★★☆
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.

Autor

Nick — ingeniero que construye infraestructura para agentes de IA en producción.

Enfoque: patrones de agentes, modos de fallo, control del runtime y fiabilidad del sistema.

🔗 GitHub: https://github.com/mykolademyanov


Nota editorial

Esta documentación está asistida por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

El contenido se basa en fallos reales, post-mortems e incidentes operativos en sistemas de agentes de IA desplegados.