Saltar al contenido principal

25 publicaciones etiquetados con "autoalojamiento"

Ver Todas las Etiquetas
intermediatePart 10

Dale a cada herramienta MCP su propio ámbito y devuelve un rechazo sobre el que el cliente pueda actuar

· 12 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
authentik+Model Context Protocol

La parte 6 puso required_scopes=["mcp:invoke"] en el verificador de JWT y dio por autorizado el servidor. Lo está, en el sentido de que quien llama sin autenticarse no obtiene nada. No lo está, en el sentido que importa: mcp:invoke abre todas las herramientas del servidor. El mismo token que deja a un agente ejecutar find_customer le deja ejecutar issue_refund.

Ese es todo el hueco. Un agente de solo lectura y un agente que emite reembolsos tienen credenciales idénticas, y lo único que se interpone entre "busca a Maria" y "reembolsa la factura 2" es que nadie lo pidió.

Este post lo cierra, primero mal y luego bien, porque el "mal" es lo que la mayoría despliega y la diferencia solo se nota en lo que el cliente puede hacer ante un rechazo.

Ambas versiones rechazan la misma llamada — un token de solo lectura pidiendo issue_refund. Se diferencian en dónde se comprueba el ámbito, y esa única decisión determina lo que recibe el cliente:

Requisitos previos​

Las partes 5 y 6, en concreto:

  • Authentik emitiendo tokens de client-credentials a un proveedor agent-tools (parte 5)
  • Un servidor FastMCP verificando esos tokens contra el endpoint JWKS (parte 6)
  • ~/mcp-auth/token.sh, que recibe una cadena de ámbitos y devuelve un token de acceso

El servidor de la parte 6 se queda en :8770 todo el rato. Las dos versiones de abajo corren en :8771 y :8772 para que puedas comparar las tres sin parar nada.

Paso 1 — Dos ámbitos nuevos​

Los ámbitos son Property Mappings en Authentik. Customization → Property Mappings → New Property Mapping → Scope Mapping, dos veces:

NombreNombre del ámbitoDescripción
office-readoffice:readConsultar clientes y facturas
office-refundoffice:refundEmitir reembolsos

Dos nuevos mapeos de ámbito junto a los existentes gateway-invoke y mcp-invoke

Crearlos no basta. Un proveedor solo emitirá un ámbito que se le haya asignado, así que abre Applications → Providers → agent-tools → Edit y mueve ambos a Selected Scopes.

El proveedor agent-tools con mcp-invoke, office-read y office-refund seleccionados

Fíjate en la línea bajo el selector: "Select which scopes can be used by the client. The client still has to specify the scope to access the data." Las dos mitades importan. Seleccionar un ámbito aquí no lo mete en todos los tokens — permite que el cliente lo pida. Un cliente que no pide nada no obtiene nada, y por eso cada llamada a token.sh de abajo pasa una cadena de ámbitos explícita.

Confirma que el token lleva de verdad lo que pediste:

~/mcp-auth/token.sh "mcp:invoke office:read" \
| cut -d. -f2 | base64 -d 2>/dev/null | jq .scope
"mcp:invoke office:read"

Si eso vuelve sin office:read, el ámbito no está en el proveedor — arréglalo antes de escribir una línea de código de servidor, o te pasarás una hora depurando una aplicación de permisos que funciona perfectamente sobre un token al que nunca se le asignó el ámbito.

Paso 2 — La implementación obvia​

Mantén required_scopes=["mcp:invoke"] en el verificador como precio de entrada, y luego pregunta por herramienta si quien llama tiene lo que esa operación necesita. FastMCP expone el token ya verificado a través de get_access_token(), así que un decorador puede leer los claims que el verificador ya comprobó:

from fastmcp.exceptions import ToolError
from fastmcp.server.dependencies import get_access_token

def requires(scope: str):
def decorate(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
token = get_access_token()
held = set(getattr(token, "scopes", None) or [])
if scope not in held:
raise ToolError(
f'insufficient_scope: this tool requires "{scope}"; '
f'token carries {sorted(held) or "nothing"}'
)
return await fn(*args, **kwargs)
return wrapper
return decorate

Y luego una línea por herramienta:

@mcp.tool
@requires("office:read")
async def find_customer(query: str) -> list[dict]: ...

@mcp.tool
@requires("office:refund")
async def issue_refund(invoice_id: int) -> str: ...

Lánzalo en :8771 y prueba ambas herramientas con ambos tokens:

Dos tokens, dos herramientas: cada token permite una y rechaza la otra

--- token: mcp:invoke + office:read ---
find_customer : ALLOWED -> [{'id': 1, 'name': 'Maria Alvarez', ...}]
issue_refund : REFUSED -> insufficient_scope: this tool requires "office:refund"

--- token: mcp:invoke + office:refund ---
find_customer : REFUSED -> insufficient_scope: this tool requires "office:read"
issue_refund : ALLOWED -> Refunded 80.00 on invoice 2.

Eso es aplicación real de permisos. El token de reembolso no puede leer, el de lectura no puede reembolsar, y el rechazo nombra el ámbito que falta. Para muchos despliegues, aquí es donde se para.

Paso 3 — Por qué ese rechazo no basta​

Mira la capa HTTP en lugar de la librería cliente.

TOK=$(~/mcp-auth/token.sh "mcp:invoke office:read")
curl -sS -i -X POST http://127.0.0.1:8771/mcp \
-H "Authorization: Bearer $TOK" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_refund","arguments":{"invoice_id":2}}}'

La comprobación en el cuerpo de la herramienta devuelve HTTP 200 con el rechazo dentro del cuerpo

HTTP/1.1 200 OK
content-type: text/event-stream

data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"insufficient_scope: this tool
requires \"office:refund\"; token carries ['mcp:invoke', 'office:read']","type":"text"},
"isError":true}}

HTTP 200. La petición tuvo éxito; la que declinó fue la herramienta. Esa es la semántica correcta de JSON-RPC y es inútil para un cliente OAuth.

Un cliente OAuth que quiere escalar — volver al servidor de autorización y pedir office:refund — está atento a un 401 o 403 con una cabecera WWW-Authenticate que le diga qué solicitar. No se pone a extraer inglés de un resultado de herramienta. Así que el rechazo es legible para un humano que lee logs e invisible para la maquinaria diseñada para manejar justo este caso.

Y además llega tarde. El cuerpo de la herramienta corre después del enrutado, después de montar la sesión, después de que el servidor se ha comprometido a una respuesta con éxito. Para entonces ya no queda línea de estado que cambiar.

Paso 4 — El desafío que la especificación realmente quiere​

Conseguir un 403 significa comprobar los ámbitos antes de que responda la capa JSON-RPC, lo que significa un middleware ASGI envolviendo la app de FastMCP:

class ScopeChallengeMiddleware:
def __init__(self, app):
self.app = app

async def __call__(self, scope, receive, send):
if scope["type"] != "http" or scope["method"] != "POST":
return await self.app(scope, receive, send)

# Buffer the body so we can inspect it and still pass it on.
chunks, more = [], True
while more:
msg = await receive()
chunks.append(msg.get("body", b""))
more = msg.get("more_body", False)
body = b"".join(chunks)

needed = None
try:
rpc = json.loads(body)
if rpc.get("method") == "tools/call":
needed = TOOL_SCOPES.get((rpc.get("params") or {}).get("name"))
except Exception:
pass

if needed and needed not in token_scopes(dict(scope.get("headers") or [])):
challenge = (
f'Bearer error="insufficient_scope", scope="{needed}", '
f'resource_metadata="{RESOURCE_METADATA}", '
f'error_description="This operation requires the {needed} scope"'
)
await send({"type": "http.response.start", "status": 403,
"headers": [(b"content-type", b"application/json"),
(b"www-authenticate", challenge.encode())]})
await send({"type": "http.response.body",
"body": json.dumps({"error": "insufficient_scope",
"scope": needed}).encode()})
return

# Replay the buffered body downstream.
replayed = False
async def replay():
nonlocal replayed
if not replayed:
replayed = True
return {"type": "http.request", "body": body, "more_body": False}
return await receive()

await self.app(scope, replay, send)

app = ScopeChallengeMiddleware(mcp.http_app(stateless_http=True))

La misma petición, contra :8772:

El middleware devuelve 403 con un desafío WWW-Authenticate que nombra el ámbito que falta

HTTP/1.1 403 Forbidden
www-authenticate: Bearer error="insufficient_scope", scope="office:refund",
resource_metadata="http://127.0.0.1:8772/.well-known/oauth-protected-resource",
error_description="This operation requires the office:refund scope"

Ahora el cliente tiene algo sobre lo que actuar: el estado dice rechazado, scope= dice qué pedir, y resource_metadata dice dónde averiguar cómo. Eso es autorización escalada como protocolo, no como mensaje de log.

Paso 5 — Lo que costó​

El middleware funciona. También es peor código que el decorador, de tres formas concretas, y fingir lo contrario sería deshonesto.

No sabe qué es una herramienta. El middleware ASGI ve bytes y cabeceras. Para averiguar qué herramienta se está llamando, parsea él mismo el sobre JSON-RPC y busca el nombre en una tabla que tiene que mantener:

TOOL_SCOPES = {
"find_customer": "office:read",
"open_invoices": "office:read",
"issue_refund": "office:refund",
}

Esa tabla es una segunda fuente de verdad. Añade una herramienta y olvida la entrada y queda sin proteger — en silencio, porque el middleware simplemente deja pasar cualquier cosa que no reconoce. El decorador no podía tener ese fallo: el requisito estaba sobre la función.

Decodifica el token sin verificarlo. El middleware corre antes del verificador, así que los claims verificados todavía no existen. Parte el JWT y decodifica en base64 el payload sin comprobar la firma:

payload = auth.split(None, 1)[1].split(".")[1]
claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))

Esto no es el agujero que parece — el verificador sigue corriendo después y sigue rechazando un token falsificado, así que nada llega a una herramienta con una firma mala. Pero la decisión de ámbito se toma sobre bytes no autenticados, y la única razón por la que eso es sobrevivible es la segunda comprobación detrás. Es una verruga, no una vulnerabilidad, y es de esas cosas que vale la pena dejar escritas antes de que alguien más tarde quite el verificador "redundante".

Almacena en memoria cada cuerpo de petición. Para leer el sobre y aun así pasarlo hacia abajo, el middleware vacía receive() en memoria y lo reproduce. Bien para llamadas JSON-RPC; piénsalo mejor antes de poner esto delante de subidas grandes.

Resolución de problemas — los errores que esta ejecución produjo de verdad​

insufficient_scope en una herramienta que sí concediste​

Revisa el token, no el servidor:

~/mcp-auth/token.sh "mcp:invoke office:refund" | cut -d. -f2 | base64 -d 2>/dev/null | jq .scope

Si falta office:refund, el ámbito existe como Property Mapping pero nunca se movió a los Selected Scopes del proveedor. Authentik descarta en silencio los ámbitos que un cliente no tiene permitido solicitar en lugar de dar error, así que el token vuelve válido y corto.

El middleware nunca se dispara​

Solo inspecciona POST. Los clientes MCP abren primero un GET para el flujo de eventos, y esa petición no lleva sobre JSON-RPC — si estás mirando la petición equivocada concluirás que el middleware está muerto. Confírmalo con el curl de arriba, que es un único POST.

De repente todas las herramientas devuelven 403​

TOOL_SCOPES está indexado por el nombre registrado de la herramienta, que es el nombre de la función, no la etiqueta decorada. Renombra una función y la tabla deja de coincidir. El caso de paso libre es la dirección peligrosa (sin proteger), pero un error tipográfico en la tabla produce este otro.

El cuerpo del 403 está vacío en algunos clientes​

El desafío vive en la cabecera WWW-Authenticate. Los clientes que solo registran el cuerpo de la respuesta te mostrarán {"error":"insufficient_scope"} y nada sobre qué ámbito. Usa curl -i.

Qué te dio esto y qué no​

Te dio autorización por herramienta de verdad: dos tokens que se diferencian en un ámbito, cada uno capaz de ejecutar exactamente una de dos herramientas, demostrado en la capa HTTP en lugar de afirmado. Y con el middleware, un rechazo del que un cliente puede recuperarse de forma programática.

No te dio un diseño limpio. Ambas implementaciones son concesiones que apuntan en direcciones opuestas:

decorador de herramienta (:8771)middleware ASGI (:8772)
El requisito de ámbito viveen la funciónen una tabla aparte
Lee claims verificadossíno — decodifica sin verificar, el verificador corre después
RechazoHTTP 200, isError: trueHTTP 403 + WWW-Authenticate
El cliente puede escalarnosí
Herramienta nueva sin proteger por defectonosí

El resumen honesto es que el comportamiento correcto del protocolo requiere salir de la abstracción que te da el framework, y la versión ergonómica no puede producirlo. Si tus clientes no implementan escalado — y la mayoría de los clientes de agentes hoy no lo hacen — el decorador es el mejor trato. Si lo hacen, lo pagas con una tabla que tienes que acordarte de actualizar.

Tampoco te dio una autorización que sobreviva a que la herramienta haga otra cosa. issue_refund está protegido por office:refund; nada impide que un futuro find_customer se edite para escribir. Los ámbitos controlan la entrada, no el comportamiento — que es el mismo límite que la parte 4 trazó alrededor de la política-en-la-herramienta.

Qué sigue​

El hueco evidente que queda es que ambas versiones confían en la lista de ámbitos del token y en nada más. Ninguna pregunta quién es quien llama ni sobre qué está actuando — un token con office:refund reembolsa cualquier factura, de cualquier cliente, por cualquier importe. Eso es autorización a nivel de objeto, y no vive en los ámbitos de OAuth en absoluto.

Finished this tutorial?
Mark it complete to earn A scope per tool, and a refusal clients can act on on your skill path.

Más lectura​

intermediatePart 9

Aísla el código que escribe tu agente y comprueba que cada límite se aplicó de verdad

· 12 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+

Todas las partes de esta serie hasta ahora le han dado al modelo herramientas: funciones que escribiste tú, con argumentos que definiste tú. Este post va sobre la otra cosa que hacen los agentes, que es escribir código y después ejecutarlo.

Es un riesgo distinto, y vale la pena ser preciso sobre la diferencia. Una llamada a herramienta es el modelo eligiendo de un menú que tú controlas. Ejecutar código generado es el modelo pasándote algo que nadie ha leído nunca, y que tú ejecutas en tu máquina.

intermediatePart 8

Mueve el demonio de Docker fuera de root para que una fuga del contenedor caiga en un usuario común

· 15 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+

Esta es la parte que nadie dice en voz alta cuando te recomiendan añadirte al grupo docker para dejar de escribir sudo.

Ese grupo es root. No "casi root", ni "root para cosas de Docker". Si puedes ejecutar un contenedor, puedes leer, modificar o borrar cualquier archivo de la máquina — incluido el archivo de contraseñas, los directorios personales de otras personas y los archivos que el administrador apartó deliberadamente de ti.

No hace falta ningún exploit. Es un comando, y tarda unos cuatro segundos.

intermediatePart 7

Limita la salida de un contenedor de agente para que solo alcance una API y nada más

· 23 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+

La parte 4 de orquestación de agentes dividió una caja de herramientas para que el agente de agenda no pudiera emitir reembolsos. La parte 5 le dio al agente una identidad propia en el gateway. La parte 6 puso un verificador de JWT delante del servidor de herramientas para que rechace a quien llama sin identificarse.

Todas ellas controlan qué puede llamar el agente. Ninguna controla adónde puede ir.

Esa distinción es todo este artículo. La exfiltración no necesita una herramienta. Necesita un socket.

intermediatePart 6

El token válido equivocado: autenticar un servidor de herramientas MCP con authentik

· 17 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
authentik+Model Context Protocol

La Parte 5 le dio a un agente su propia identidad en el gateway: un token emitido por authentik con client credentials, con un scope, y que expira a los cinco minutos. Terminó nombrando lo que no había cubierto — el servidor de herramientas MCP de la parte 4 de orquestación de agentes sigue fiándose de cualquier cosa que alcance su puerto, issue_refund incluido.

Aquel post fue explícito sobre el límite de lo que había construido:

The MCP server still trusts everyone. Scoping happens in the client. Anything that can reach 127.0.0.1:8770 can call issue_refund directly, agent or not.

Repartir la caja de herramientas por rol impidió que un agente alcanzara una herramienta que no le tocaba. No hizo nada contra un curl. Esta parte cierra eso.

intermediatePart 4

Repartir una caja de herramientas MCP entre dos agentes para que el de agenda no pueda emitir reembolsos

· 14 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
Model Context Protocol++LiteLLM
0/4
🎯 Skill path0/4 earned
Agent orchestration with LangGraph

La Parte 3 le dio a un agente cinco herramientas por MCP — un calendario, una lista de clientes, facturas y un reembolso — y puso a un humano delante del reembolso. Terminó admitiendo lo evidente: un solo agente tenía las cinco. Nada impedía que el modelo echara mano de issue_refund cuando se le había pedido reservar una cita. La pausa humana era lo único que se interponía, y una pausa solo se dispara si el modelo llega a llamar la herramienta.

Esta parte quita la herramienta en su lugar. El de agenda recibe tres y el de facturación tres, compartiendo una. Pídele al de agenda que reembolse una factura y no puede — no porque se le haya dicho que no, no porque una política lo bloquee, sino porque issue_refund nunca estuvo en la lista que se le dio.

Luego el supervisor de la Parte 2 vuelve encima, y sale a la luz la propiedad interesante: el enrutado decide quién trabaja, el alcance decide qué es posible. Un reembolso mal enrutado sigue sin poder reembolsar.

intermediatePart 3

Reservar citas y reembolsar facturas desde un agente LangGraph con MCP

· 21 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
Model Context Protocol++LiteLLM
0/4
🎯 Skill path0/4 earned
Agent orchestration with LangGraph

La Parte 1 le dio a un agente un estado que sobrevive a un reinicio, y una pausa donde un humano aprueba antes de que continúe. La Parte 2 puso un supervisor delante de un agente de agenda y uno de facturación, y repartió el trabajo entre ellos.

Ninguno de esos agentes agendó ni facturó nada. Lo comentaron. Cada herramienta que tenían era una función que tú escribiste en el mismo proceso, y las interesantes — buscar un cliente, ocupar un hueco, mover dinero — no existían.

Esta parte construye la caja de herramientas que esos dos roles necesitan, por MCP: un calendario, una lista de clientes, facturas y un reembolso. Para reducir piezas móviles apuntamos un agente a las cinco herramientas en lugar de reconstruir el supervisor de la Parte 2 — el agente las descubre en tiempo de ejecución en vez de estar cableado a ellas, las encadena para responder una pregunta, y escribe filas que puedes ir a comprobar en la base de datos después. Luego el reembolso lo detiene en seco y hace que un humano teclee el importe.

Repartir estas herramientas de vuelta entre el agente de agenda y el de facturación — de modo que el de agenda no pueda ver issue_refund en absoluto — es el siguiente paso natural, y la última sección explica cómo.

En julio la especificación de MCP eliminó las sesiones por completo. El día que ejecutamos esto, LangChain publicó el soporte para esa revisión en el paquete principal. Así que esta es una primera ejecución contra un protocolo de cinco semanas y un cliente del mismo día — que es justo por lo que tres cosas de aquí no están todavía en la documentación de nadie.

intermediatePart 3

Un solo inicio de sesión para todo: poniendo authentik delante de una app autoalojada

· 22 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
authentik+

La Parte 1 cerró los puertos que nadie quiso abrir. La Parte 2 impidió que dos contenedores se ejecutaran como root. Ambas trataban sobre la máquina. Ninguna tocó la pregunta que una pila de IA autoalojada responde peor: ¿quién tiene permiso para iniciar sesión, y dónde se decide eso?

La misma caja de antes. El Langfuse que venimos endureciendo desde la Parte 1 — aquel cuyo Postgres, ClickHouse y Redis la Parte 1 encontró correctamente enlazados a localhost, y cuyo worker la Parte 2 bajó de root — se desplegó en Detecta lo que tus pruebas no ven. Dos partes han asegurado ya la máquina por debajo sin tocar ni una vez la puerta de entrada de la propia aplicación. Esta es la primera parte que cambia Langfuse en sí.

Ahora mismo esa decisión se toma en cada app, por separado. Langfuse tiene su propia tabla de correo y contraseña. También la tiene cualquier otra herramienta en la caja. Cada una es un lugar donde una cuenta puede sobrevivir a la persona que la tenía, donde una contraseña puede reutilizarse, y donde "quitarle el acceso a esta persona" significa acordarse de que esa app existe.

Este post mueve esa decisión a un solo lugar. Ese lugar es authentik — un proveedor de identidad de código abierto que ejecutas tú mismo, la contraparte autoalojada de Okta o Auth0. Guarda las cuentas, muestra la pantalla de inicio de sesión, y da fe de quién es alguien ante cualquier app que pregunte. Las apps dejan de almacenar contraseñas y empiezan a preguntarle a authentik.

Es el mismo trabajo que hace Keycloak, y Keycloak es el nombre más conocido. authentik se gana la elección aquí por el coste de puesta en marcha: un archivo compose y un asistente frente a los realms, clients y ajustes de JVM de Keycloak. Para una caja y una app, esa diferencia es toda la decisión.

Lo desplegamos, conectamos Langfuse a él por OIDC, y terminamos con un inicio de sesión que pasa por authentik y vuelve.

intermediatePart 2

Pasar trabajo entre agentes LangGraph sin corromper el estado compartido

· 17 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+PostgreSQL+
0/4
🎯 Skill path0/4 earned
Agent orchestration with LangGraph

Tienes dos agentes. Uno reserva citas. Otro gestiona facturación.

Llega un ticket que necesita los dos: cambia la fecha de mi instalación, y mi factura parece incorrecta. Así que lo envías a los dos.

El primero reserva el martes. El segundo ve que la cuenta está en mora y la congela. Los dos terminan casi en el mismo momento, y los dos guardan lo que decidieron.

Solo se guarda uno de ellos. ¿Cuál? El que terminó primero — que depende de lo lenta que estuviera una llamada de API ese día. Así que reservas una cita en una cuenta congelada, o congelas una cuenta a la que acabas de prometerle un ingeniero. Después parece que se tomó una única decisión limpia.

La Parte 1 construyó un agente que ejecuta sus pasos en un orden fijo. Este post tiene varios: un supervisor que elige quién trabaja en qué, un agente que pasa el trabajo a otro a mitad de camino, y dos agentes puestos en el camino del otro a propósito — para descubrir qué hace LangGraph cuando no están de acuerdo.

intermediatePart 1

Checkpoints de un agente LangGraph en una instancia WEC para que las caídas no te cuesten nada

· 21 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+PostgreSQL+
0/4
🎯 Skill path0/4 earned
Agent orchestration with LangGraph

Tu agente lleva cuarenta segundos trabajando en la petición de un cliente. Ha leído el ticket, ha consultado la cuenta y ha revisado los calendarios de tres ingenieros. Está a punto de reservar la cita.

Entonces el proceso muere. Un despliegue que sale, el host que se queda sin memoria, alguien que reinicia el contenedor — da igual cuál de ellos.

El agente no continúa donde lo dejó, porque no hay nada desde donde continuar. Todo lo que aprendió vivía en variables dentro de un proceso que ya no existe. El cliente sigue esperando. Ejecútalo de nuevo y pagas todo ese trabajo por segunda vez. Y la parte que más debería preocuparte: nadie puede decir si la cita se reservó en el último segundo antes de morir.

Un agente es un modelo dentro de un bucle. Como script de Python normal, ese bucle es exactamente igual de frágil que el proceso que lo contiene.

Aquí construimos uno que guarda su estado en Postgres después de cada paso, para que una caída no cueste nada.

intermediatePart 3

Enmascarar PII en la puerta de enlace: configurar Presidio, además de la línea que los documentos omiten

· 22 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
LiteLLM+Presidio+
0/5
🎯 Skill path0/5 earned
Self-hosting an LLM gateway

La Parte 2 terminó con la puerta de enlace decidiendo qué modelo responde a cada solicitud — y aún así reenvía cada solicitud tal cual, incluyendo las que llevan nombres de clientes, correos electrónicos y números de teléfono.

Esta publicación coloca un filtro de PII en ese camino. No delante del modelo, sin embargo: delante de los registros. El modelo que lee tu solicitud está haciendo su trabajo; el riesgo es lo que se almacena. Así que el texto sin procesar va al modelo y una copia enmascarada va a tus registros.

Eso es lo que la puerta de enlace publicita. Seguir su configuración documentada me dio lo opuesto: un modelo que respondió correctamente y una respuesta que regresó como <LOCATION>. Así es como encontrar eso y cómo solucionarlo.

intermediatePart 2

Enrutamiento de complejidad de LiteLLM: el modelo adecuado para cada solicitud y su costo en latencia

· 13 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
LiteLLM+Qwen+
0/5
🎯 Skill path0/5 earned
Self-hosting an LLM gateway

Parte 1 terminó en un número incómodo. La misma respuesta de tres palabras costó 3 tokens de un modelo pequeño y 200 de un modelo de razonamiento — que gastó los 200 pensando y no devolvió nada en absoluto.

Cada solicitud que envían tus aplicaciones elige un modelo, y en su mayoría esa elección se hace una vez, codificada de forma rígida, y nunca se revisita. Esta publicación pone al gateway a cargo de ello en su lugar: clasifica la solicitud, la enruta a un modelo adecuado para el trabajo. Luego mide lo que cuesta esa decisión, porque no es gratis y la mayoría de los informes omiten esa parte.

intermediatePart 1

Un endpoint, muchos modelos: despliega un gateway LLM en una instancia WEC

· 16 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+LiteLLM+
0/5
🎯 Skill path0/5 earned
Self-hosting an LLM gateway

Así es como suele ir. Una aplicación necesita un modelo, así que pegas la clave API en su .env. Luego una segunda aplicación necesita una. Luego un script. Seis meses después, la misma clave está en cinco lugares, nadie recuerda cuál de ellas sigue funcionando, y no puedes rotarla sin romper algo que solo descubrirás cuando se rompa.

Un gateway es la solución aburrida. Un endpoint frente a cada modelo, un lugar que mantiene la credencial real, y una clave específica por aplicación que puedes revocar por su cuenta. Esta publicación despliega uno en una instancia WEC y lo apunta a la API de Inferencia WEC.

intermediatePart 2

Root por defecto: endurecimiento del privilegio del contenedor en una pila de IA autoalojada

· 9 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+

Parte 1 de esta serie auditó una caja multi-agente en vivo y encontró cada pregunta sobre exposición de red que valía la pena hacer. Una línea de esa auditoría no se siguió: "el Postgres, ClickHouse y Redis detrás de Langfuse estaban publicados en 127.0.0.1 en lugar del mundo — alguien tomó una buena decisión allí. Mantén ese pensamiento."

Aquí está la otra mitad de ese pensamiento: conseguir la red correcta no dice nada sobre lo que sucede después de que alguien ya esté dentro de un contenedor. Si el proceso que se ejecuta allí es root, un compromiso comienza con las llaves de todo el sistema de archivos. Así que verificamos — en la misma caja, la misma pila de Langfuse que la Parte 1 ya alabó — si "red correcta" también significaba "privilegio correcto." No lo era, para dos de los seis contenedores. Aquí está cómo se veía realmente la solución, incluyendo la parte que se rompió.

intermediatePart 1

Tu firewall te está mintiendo: endurecimiento de redes Docker para sistemas multi-agente

· 16 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+

Comienzas con un agente. Luego necesita una base de datos. Después agregas un segundo agente, un puente de mensajería, una pila de observabilidad. Seis meses después, una única instancia de WEC está ejecutando cinco proyectos de compose, veinte y tantos contenedores, y nadie recuerda qué puertos están abiertos al mundo.

Eso no es un hipotético — esa es la caja en la que se escribió este tutorial. Así que en lugar de teorizar, lo investigamos: ¿pueden los contenedores alcanzarse entre sí a través de pilas? ¿Pueden alcanzar las bases de datos? ¿Está el firewall realmente protegiendo algo?

Tres de las respuestas me sorprendieron. Una de ellas fue una base de datos sentada allí sin contraseña. Y el firewall — el firewall estaba mintiendo.

beginnerPart 2

Migrar el Bot de Telegram de OpenClaw a Hermes

· 6 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
Hermes Agent+
0/2
🎯 Skill path0/2 earned
Self-hosting Hermes

En Parte 1 alojaste Hermes con memoria persistente. Esta parte lo conecta al mismo bot de Telegram de la serie OpenClaw — el chat que tus usuarios ya conocen sigue funcionando exactamente como antes, solo que con un agente diferente respondiendo detrás. No hay un nuevo bot que anunciar, ni un canal al que migrar a la gente.

intermediatePart 1

Crea un asistente de IA para WhatsApp desde cero con Evolution y la API de WEC

· 16 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
WhatsApp++Evolution
0/2
🎯 Skill path0/2 earned
WhatsApp automation on WEC
  • 1Self-host a WhatsApp AI bridge
  • 🏆Production delivery via Cloud API

La mayoría de las guías de "autoalbergar un asistente de IA para WhatsApp" se detienen en "el contenedor se inició". Esta va hasta el final: despliegas una puerta de enlace de WhatsApp programable real (API de Evolution), luego escribes el puente tú mismo — las ~50 líneas que convierten un mensaje entrante en una respuesta de LLM y la envían de vuelta. Ese puente (webhook → modelo → respuesta) es el patrón reutilizable detrás de cada integración de chat-AI: SMS, Slack, Telegram, voz — cambia el canal, la forma es idéntica.

Y porque esto es una construcción real, encontramos — y solucionamos — cada detalle: una imagen que movió a los editores, un bucle de versión de Baileys, un bucle de respuesta infinito, spam en grupos de chat, la nueva dirección LID de WhatsApp, y una genuina pared de entrega que la mayoría de los tutoriales pretenden que no existe. Cada comando, error y salida a continuación es de una ejecución real.

beginnerPart 6

Agregar un canal de WhatsApp a OpenClaw

· 6 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
OpenClaw+WhatsApp
0/6
🎯 Skill path0/6 earned
Self-hosting OpenClaw

Telegram (Parte 3) le dio a tu agente un bot. WhatsApp le da una línea telefónica — la aplicación que ~3 mil millones de personas ya usan, accesible sin fricción. Una advertencia que vale la pena entender desde el principio: WhatsApp no tiene cuenta de bot, así que OpenClaw se vincula a un número real como un dispositivo compañero (como WhatsApp Web) y el agente actúa como esa cuenta. Cada comando y error a continuación proviene de una ejecución real.

advancedPart 4

Captura lo que tus pruebas no ven: observa y evalúa tu aplicación WEC en producción con Langfuse

· 20 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
+
0/8
🎯 Skill path0/8 earned
AI evals & observability

Un cliente dice que tu bot de soporte les prometió una política de reembolso que no existe. Tu función hizo dos llamadas LLM: clasificar y luego responder. ¿Cuál de ellas la inventó? Si no puedes responder eso, tu aplicación es una caja negra — y esta guía soluciona exactamente eso.

Ahora puedes probar que un modelo funciona (parte 1), hacer que su salida sea confiable para máquinas (parte 2), y generar un conjunto de pruebas real para verificarlo (parte 3). Pero todo eso se ejecuta fuera de línea, en CI, con entradas que elegiste. La producción no juega de esa manera.

Esta guía cierra la brecha. Autoalojaremos Langfuse — la alternativa de código abierto y autoalojable a LangSmith — rastrearemos cada llamada real, evaluaremos automáticamente el tráfico en vivo con un juez LLM, profundizaremos en el paso exacto que falla y retroalimentaremos los fallos para que tu conjunto de datos de la parte 3 se vuelva más fuerte. La evaluación fuera de línea te dice que funcionó en tu conjunto de pruebas; esto te dice que funciona en el mundo real.

beginnerPart 5

Ejecutar OpenClaw en Modelos WEC

· 8 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
0/6
🎯 Skill path0/6 earned
Self-hosting OpenClaw
OpenClaw+

A través de Partes 1–4 implementaste OpenClaw, lo aseguraste con HTTPS, añadiste Telegram y lo hiciste privado a través de una malla NetBird — todo apuntando a OpenAI. Esta parte intercambia el modelo por debajo: apunta el mismo agente a Modelos WEC — la inferencia compatible con OpenAI de WiLine — y ejecútalo en un modelo de peso abierto, Llama 3.1 8B Instruct. Misma caja, sin reconstrucción — solo un cambio de base-URL, clave y modelo a través de la CLI de OpenClaw.

intermediatePart 1

Autoalojar el Agente Hermes con memoria persistente

· 15 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
Hermes Agent+SQLite
0/2
🎯 Skill path0/2 earned
Self-hosting Hermes

Hermes Agent es el agente de IA de código abierto (MIT) de Nous Research — "el agente que crece contigo." Su característica destacada es memoria persistente: aprende sobre tus proyectos y no olvida entre reinicios. Esta guía lo despliega en la misma Instancia WEC que ya usas para OpenClaw, lo apunta a un modelo y prueba que la memoria sobrevive a un reinicio completo.

intermediatePart 4

Haz que OpenClaw sea privado con una VPN de malla NetBird

· 20 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
OpenClaw+NetBird
0/6
🎯 Skill path0/6 earned
Self-hosting OpenClaw

En Artículo 2 colocamos Caddy frente a OpenClaw para HTTPS, y en Artículo 3 agregamos un canal de Telegram. La puerta de enlace funciona — pero Caddy sigue escuchando en 0.0.0.0, accesible por cualquier cosa que pueda enrutar hacia la caja. Este es el proyecto final de la serie: unimos el servidor y tu laptop a una malla NetBird, redirigimos openclaw.local a la IP de la malla y cerramos los puertos públicos. La misma URL https://openclaw.local/chat sigue funcionando — pero solo para tus dispositivos. Cada comando y error a continuación proviene de la ejecución real.

beginnerPart 3

Agregar un canal de Telegram a OpenClaw

· 7 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
OpenClaw+
0/6
🎯 Skill path0/6 earned
Self-hosting OpenClaw

Has desplegado OpenClaw y lo has asegurado detrás de HTTPS. Ahora hazlo utilizable — habla con tu agente desde tu teléfono a través de Telegram. Creamos un bot, lo conectamos, limpiamos la puerta de emparejamiento de OpenClaw y obtenemos una respuesta real. Cada comando y detalle a continuación proviene de una ejecución real.

intermediatePart 2

Asegurar OpenClaw con un proxy inverso Caddy + HTTPS

· 9 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
OpenClaw+
0/6
🎯 Skill path0/6 earned
Self-hosting OpenClaw

En Artículo 1 hicimos funcionar OpenClaw — pero solo a través de HTTP sin cifrar, con un workaround allowInsecureAuth. Aquí colocamos Caddy frente a él como un proxy inverso: verdadero HTTPS, autenticación emparejada por dispositivo, y los puertos del gateway cerrados para que el proxy sea la única forma de acceso. Cada comando y error a continuación proviene de la implementación real.

beginnerPart 1

Desplegar OpenClaw en una instancia WEC a través de Docker Compose

· 14 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
OpenClaw++
0/6
🎯 Skill path0/6 earned
Self-hosting OpenClaw

Tu asistente de IA no tiene que vivir en la nube de otra persona.

Despliega un agente de IA OpenClaw autoalojado en una instancia WEC con Docker Compose — desde iniciar la VM hasta un agente que realmente responde, usando tu propia clave de API de modelo. Cada comando, versión y error a continuación fue capturado de un despliegue real en una instancia WEC.