Saltar al contenido principal

14 publicaciones etiquetados con "agentes"

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 5

La clave que expira: darle a un agente su propia identidad en el gateway

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

La Parte 4 puso control de acceso por grupos sobre la interfaz de administración del gateway y después, al final, llamó a la API desde un shell sin cuenta, sin sesión y sin grupo. Respondió con normalidad. La conclusión fue que el SSO protege un plano de control y que el plano de datos autentica a quien llama por máquina con claves — cosa que tiene que hacer, porque un agente que corre a las 3 de la mañana no puede completar un login en un navegador.

Eso era cierto y era también un punto y aparte, no una respuesta. "Los clientes máquina usan claves" te deja con una credencial que nunca expira, que ningún proveedor de identidad conoce, y que sobrevive a la persona que la creó. La Parte 4 lo dijo sin rodeos: sacar a alguien de un grupo no revoca sus claves, una clave filtrada no se ve afectada por la identidad en absoluto, y las claves sobreviven a las personas.

Esta parte le da a la máquina una identidad en lugar de una clave.

authentik le emite al agente un token con el flujo de client credentials — sin navegador, sin pantalla de consentimiento, sin humano. El token va firmado, lleva un scope y expira a los cinco minutos. El gateway lo verifica localmente contra las claves públicas de authentik y rechaza cualquier cosa sin el scope correcto. Al final, tres códigos de estado enseñan el límite aguantando.

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 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 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.

intermediatePart 7

Trazado a nivel de componente: depuración de llamadas a herramientas del agente

· 13 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 agente que llama a herramientas hace dos cosas muy diferentes: razona ("Debería buscar en la documentación") y actúa (realmente llama a la herramienta). Cuando algo sale mal, la primera pregunta siempre es qué capa falló — ¿pensó mal o se rompió la acción? Un registro plano no puede responder eso. Un trazado puede.

En este tutorial, construirás un pequeño agente que llama a herramientas en WEC Inference, lo instrumentarás con Langfuse para que cada paso de razonamiento y cada llamada a la herramienta se conviertan en un nodo inspeccionable, y luego depurarás dos fallos reales del árbol de trazado — incluyendo el peor tipo: una respuesta incorrecta y confiada que nunca lanza un error. Cada comando, error y captura de pantalla a continuación proviene de una ejecución real.

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.