Sesión 2: Tools + Agent Workflows
Duración: 50 minutos | Nivel: .NET Intermedio
Descripción General#
En la arquitectura de agentes, una herramienta (tool) es cualquier capacidad que la IA puede invocar para interactuar con el mundo — leer archivos, ejecutar comandos, obtener páginas web, programar tareas. La diferencia entre un chatbot y un agente es simple: un chatbot genera texto; un agente usa herramientas.
Esta sesión sigue el enfoque "Explicar → Explorar → Extender": explicamos la arquitectura, exploramos el código pre-construido y lo extendemos con pequeños cambios asistidos por Copilot. La seguridad es una preocupación de primera clase — cada herramienta tiene protecciones de defensa en profundidad contra vectores de ataque del mundo real.
Lo Que los Asistentes Entenderán#
- Cómo la capa de abstracción de herramientas habilita la extensibilidad
- Por qué las puertas de seguridad (aprobación, validación, listas de bloqueo) son esenciales
- Cómo el bucle del agente coordina el razonamiento del modelo con la ejecución de herramientas
- La separación entre
IToolRegistry(qué herramientas existen) eIToolExecutor(cómo se ejecutan de forma segura)
Etapa 1: Arquitectura de Herramientas (12 min)#
Conceptos a Explicar#
- Qué diferencia a un agente de un chatbot: ¡El uso de herramientas! Un chatbot genera texto. Un agente decide que necesita hacer algo — leer un archivo, ejecutar un comando, obtener una URL — y solicita una llamada a herramienta. El modelo no ejecuta nada; emite una solicitud estructurada de herramienta que nuestro código ejecuta.
- Interfaz ITool: Cada herramienta implementa
IToolcon cuatro miembros:Name— identificador único (ej.,"file_system","shell")Description— qué hace la herramienta (se envía al LLM)Metadata— esquema de parámetros, requisitos de aprobación, categoría, etiquetasExecuteAsync(ToolInput, CancellationToken)— la ejecución real
- Políticas de aprobación:
IToolApprovalPolicycon dos métodos:RequiresApprovalAsync— ¿esta herramienta necesita aprobación humana?IsApprovedAsync— ¿ha sido aprobada?- Incluido:
AlwaysApprovePolicy(aprobación automática para todo) - ShellTool establece
RequiresApproval = trueen sus metadatos
- Separación de responsabilidades entre IToolRegistry e IToolExecutor:
IToolRegistrygestiona el descubrimiento de herramientas:Register,GetTool,GetAllTools,GetToolManifestIToolExecutorgestiona la ejecución segura: búsqueda → verificación de aprobación → ejecución → registro- ¿Por qué separar? El registro trata sobre qué existe; el ejecutor trata sobre cómo ejecutar de forma segura
Recorrido por el Código#
Tools.Abstractions (7 archivos, 90 LOC)#
Recorrido breve por cada archivo:
-
ITool.cs— La interfaz principal. Cada herramienta del sistema la implementa. Señalar queExecuteAsyncdevuelveToolResult, no cadenas de texto. -
IToolExecutor.cs— Dos métodos:ExecuteAsync(herramienta individual) yExecuteBatchAsync(múltiples herramientas). El ejecutor no conoce herramientas específicas — usa el registro. -
IToolRegistry.cs— Cuatro métodos:Register,GetTool,GetAllTools,GetToolManifest. El manifiesto devuelve solo metadatos (sin capacidad de ejecución) — seguro para exponer al modelo. -
IToolApprovalPolicy.cs— La interfaz de puerta de seguridad. IncluyeAlwaysApprovePolicycomo predeterminada. En producción, implementarías una política que verifique permisos de usuario. -
ToolInput.cs— Envuelve argumentos JSON crudos con métodos auxiliares:GetArgument<T>,GetStringArgument. UsaJsonDocument.Parsepara acceso sin asignación de memoria. -
ToolMetadata.cs— Lo que el LLM ve: Name, Description,ParameterSchema(JSON Schema),RequiresApproval, Category, Tags. -
ToolResult.cs— Éxito/fallo con output, error y duración. Métodos fábrica:ToolResult.Ok(...)yToolResult.Fail(...).
Tools.Core (3 archivos, 101 LOC)#
-
ToolExecutor.cs— El patrón de puerta de aprobación:// 1. Búsqueda var tool = _registry.GetTool(toolName); if (tool is null) return ToolResult.Fail(...); // 2. Verificación de aprobación if (await _approvalPolicy.RequiresApprovalAsync(toolName, arguments) && !await _approvalPolicy.IsApprovedAsync(toolName, arguments)) return ToolResult.Fail(...); // 3. Ejecutar con cronómetro var sw = Stopwatch.StartNew(); var result = await tool.ExecuteAsync(input, cancellationToken);Señalar: cada ejecución se registra con duración. El ejecutor es un punto de control — todas las llamadas a herramientas pasan por él.
-
ToolRegistry.cs— Diccionario seguro para hilos conStringComparer.OrdinalIgnoreCase. Simple pero efectivo — los nombres de herramientas no distinguen mayúsculas. -
ToolsServiceCollectionExtensions.cs— Cableado DI:AddToolFramework()registra Registry (singleton), Executor (scoped), ApprovalPolicy (singleton)AddTool<T>()registra herramientas individuales como singletons
Demo en Vivo#
Mostrar el endpoint de lista de herramientas: GET /api/tools
- Abrir navegador o cliente HTTP
- Navegar a
https://localhost:{port}/api/tools - Mostrar la respuesta JSON — lista de metadatos de herramientas (nombre, descripción, esquema de parámetros)
- Señalar: esto es lo que el modelo ve al decidir qué herramienta llamar
Etapa 2: Herramientas Integradas + Seguridad (15 min)#
Conceptos a Explicar#
Tres amenazas de seguridad del mundo real contra las que las herramientas del agente deben defenderse:
- Recorrido de Ruta (Path Traversal) — Un atacante (o LLM confundido) intenta
../../etc/passwdo..\..\Windows\System32. La herramienta de sistema de archivos debe confinar el acceso al espacio de trabajo. - Inyección de Comandos — El LLM genera
rm -rf /oformat C:. La herramienta de shell debe bloquear comandos peligrosos antes de ejecutarlos. - SSRF (Server-Side Request Forgery) — El LLM obtiene
http://127.0.0.1:8080/adminohttp://169.254.169.254/metadata. La herramienta web debe bloquear solicitudes a redes internas/privadas.
Patrón de defensa: Cada herramienta valida las entradas antes de la ejecución. Fallar rápido, fallar seguro.
Recorrido por el Código#
FileSystemTool (OpenClawNet.Tools.FileSystem, 142 LOC)#
Características de seguridad clave a destacar:
-
Array de rutas bloqueadas:
private static readonly string[] BlockedPaths = [".env", ".git", "appsettings.Production"]; -
Resolución de ruta con prevención de recorrido — el método
ResolvePath:var fullPath = Path.GetFullPath(Path.Combine(_workspaceRoot, relativePath)); if (!fullPath.StartsWith(_workspaceRoot, StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("Path traversal attempt blocked: {Path}", relativePath); return null; }Explicar:
Path.GetFullPathresuelve segmentos... Luego verificamos que el resultado permanezca dentro del espacio de trabajo. Los..desaparecen para cuando verificamos — esto detecta todos los trucos de recorrido. -
Límite de tamaño de archivo: Máximo 1MB para prevenir agotamiento de memoria.
-
Tres operaciones: leer, escribir, listar — cada una con protecciones apropiadas.
ShellTool (OpenClawNet.Tools.Shell, 148 LOC)#
Características de seguridad clave:
-
Lista de comandos bloqueados:
private static readonly HashSet<string> BlockedCommands = new(StringComparer.OrdinalIgnoreCase) { "rm", "del", "format", "fdisk", "mkfs", "dd", "shutdown", "reboot", "kill", "taskkill", "net", "reg", "regedit", "powershell", "cmd" }; -
Verificación de seguridad — extrae la primera palabra, elimina prefijo de ruta, verifica la lista de bloqueo:
private static bool IsSafeCommand(string command) { var firstWord = command.Split(' ', StringSplitOptions.RemoveEmptyEntries) .FirstOrDefault()?.ToLowerInvariant(); firstWord = Path.GetFileNameWithoutExtension(firstWord); return !BlockedCommands.Contains(firstWord); } -
Tiempo límite: 30 segundos máximo de ejecución con
CancellationTokenSource.CreateLinkedTokenSourcey eliminación del árbol de procesos. -
Límite de salida: 10,000 caracteres para prevenir agotamiento de memoria.
-
Multiplataforma: Usa
cmd.exe /cen Windows,/bin/sh -cen Linux/Mac. -
RequiresApproval = true: Esta herramienta requiere aprobación explícita (a diferencia de sistema de archivos y web).
WebTool (OpenClawNet.Tools.Web, 121 LOC)#
Características de seguridad clave:
-
Prevención de SSRF — el método
IsLocalUri:private static bool IsLocalUri(Uri uri) { var host = uri.Host.ToLowerInvariant(); return host == "localhost" || host == "127.0.0.1" || host == "::1" || host.StartsWith("192.168.") || host.StartsWith("10.") || host.StartsWith("172.16."); }Explicar: Esto bloquea solicitudes a redes internas. En producción, también resolverías DNS para detectar trucos CNAME (ej.,
evil.com→127.0.0.1). -
Validación de esquema: Solo
httpyhttps— sinfile://,ftp://,gopher://. -
Límite de respuesta: 50,000 caracteres para prevenir agotamiento de memoria.
-
Tiempo límite: 15 segundos.
SchedulerTool (OpenClawNet.Tools.Scheduler, 173 LOC)#
- Tres acciones:
create,list,cancel - Persistencia en base de datos vía EF Core (
IDbContextFactory<OpenClawDbContext>) - Soporta trabajos únicos (datetime ISO 8601) y trabajos recurrentes (expresiones cron)
- Lista hasta 20 trabajos con estado y próxima ejecución
- Cancelación de trabajos por GUID
🤖 Momento Copilot: Agregar un Patrón de Comando Bloqueado#
Cuándo: ~minuto 22
Contexto: Acabamos de recorrer la lista de bloqueo del ShellTool. Ahora la extendemos.
Qué hacer: Abrir ShellTool.cs, colocar el cursor dentro del HashSet BlockedCommands, y preguntar a Copilot:
Add
wgetandcurlto the blocked commands list in the ShellTool. These tools could be used to exfiltrate data from the server. Also add a comment explaining why network tools are blocked.
Resultado esperado: Copilot agrega "wget" y "curl" al HashSet BlockedCommands y añade un comentario sobre prevención de exfiltración de datos.
Por qué es interesante: Cambio pequeño y enfocado que refuerza la mentalidad de seguridad. Muestra que extender la defensa es trivial con buena arquitectura.
Etapa 3: Bucle del Agente + Integración (15 min)#
Conceptos a Explicar#
-
El bucle de razonamiento del agente: Este es el algoritmo central que hace que un agente sea un agente:
- Componer prompt (sistema + historial + mensaje del usuario + definiciones de herramientas)
- Enviar al modelo
- El modelo responde con texto O llamadas a herramientas
- Si hay llamadas a herramientas → ejecutar cada herramienta → agregar resultados a la conversación → volver al paso 2
- Si es texto → devolver respuesta final
Este bucle se repite hasta que el modelo no tiene más llamadas a herramientas, o alcanzamos el límite de seguridad.
-
Iteraciones máximas como límite de seguridad:
MaxToolIterations = 10. Sin esto, un modelo confundido podría iterar indefinidamente. Después de 10 iteraciones, el agente devuelve un mensaje de "iteraciones máximas alcanzadas". -
Cómo se inyectan las herramientas en el prompt del sistema: El
DefaultPromptComposerconstruye el prompt completo:- Mensaje de sistema (prompt base + skills activos + resumen de sesión)
- Historial de conversación
- Mensaje actual del usuario
Las definiciones de herramientas se pasan por separado a la API del modelo como objetos estructurados
ToolDefinition— el modelo ve el nombre, descripción y esquema de parámetros para cada herramienta registrada.
Recorrido por el Código#
AgentOrchestrator (OpenClawNet.Agent)#
El orquestador es la API pública — crea un AgentContext y delega a IAgentRuntime:
public async Task<AgentResponse> ProcessAsync(AgentRequest request, CancellationToken cancellationToken)
{
var context = new AgentContext
{
SessionId = request.SessionId,
UserMessage = request.UserMessage,
ModelName = request.Model ?? "llama3.2",
ProviderName = request.Provider
};
var executedContext = await _runtime.ExecuteAsync(context, cancellationToken);
return new AgentResponse
{
Content = executedContext.FinalResponse ?? string.Empty,
ToolResults = executedContext.ToolResults,
ToolCallCount = executedContext.ExecutedToolCalls.Count,
TotalTokens = executedContext.TotalTokens
};
}Señalar: El orquestador no sabe de herramientas, modelos ni prompts. Es un coordinador.
DefaultAgentRuntime — El Bucle Central#
Recorrido detallado del bucle de llamadas a herramientas:
while (iterations < MaxToolIterations)
{
var response = await InvokeHostedAgentAsync(currentMessages, context.ModelName, toolDefs, agentSession, ct);
totalTokens += response.Usage?.TotalTokens ?? 0;
if (response.ToolCalls is { Count: > 0 })
{
// Agregar mensaje del asistente con llamadas a herramientas a la conversación
currentMessages.Add(new ChatMessage { Role = Assistant, Content = response.Content ?? "", ToolCalls = response.ToolCalls });
// Ejecutar cada herramienta
foreach (var toolCall in response.ToolCalls)
{
var result = await _toolExecutor.ExecuteAsync(toolCall.Name, toolCall.Arguments, ct);
allToolResults.Add(result);
// Devolver resultado como mensaje Tool
currentMessages.Add(new ChatMessage { Role = Tool, Content = result.Success ? result.Output : $"Error: {result.Error}", ToolCallId = toolCall.Id });
}
iterations++;
}
else
{
// Sin llamadas a herramientas — esta es la respuesta final
context.FinalResponse = response.Content;
context.IsComplete = true;
return context;
}
}Puntos clave:
- El modelo decide cuándo llamar herramientas — nuestro código solo las ejecuta
- Los resultados de herramientas regresan a la conversación como mensajes
Role = Tool - El bucle continúa hasta que el modelo deja de solicitar herramientas
- El uso de tokens se acumula a través de todas las iteraciones
DefaultPromptComposer — Inyección de Herramientas#
public async Task<IReadOnlyList<ChatMessage>> ComposeAsync(PromptContext context, CancellationToken ct)
{
var messages = new List<ChatMessage>();
// 1. Prompt del sistema con skills
var systemContent = DefaultSystemPrompt;
var skills = await _skillLoader.GetActiveSkillsAsync(ct);
if (skills.Count > 0)
systemContent += $"\n\n# Active Skills\n{skillText}";
// 2. Resumen de sesión
if (!string.IsNullOrEmpty(context.SessionSummary))
systemContent += $"\n\n# Previous Conversation Summary\n{context.SessionSummary}";
messages.Add(new ChatMessage { Role = System, Content = systemContent });
// 3. Historial + 4. Mensaje actual
foreach (var msg in context.History) messages.Add(msg);
messages.Add(new ChatMessage { Role = User, Content = context.UserMessage });
return messages;
}Señalar: Las definiciones de herramientas NO están en el prompt del sistema — se pasan como objetos estructurados vía la API del modelo. El prompt del sistema contiene skills y contexto; las herramientas son un canal separado.
Gateway DI — Cómo Se Registran Todas las Herramientas#
En el Program.cs del Gateway, todas las herramientas se registran vía las extensiones de DI:
builder.Services.AddToolFramework(); // Registry + Executor + ApprovalPolicy
builder.Services.AddTool<FileSystemTool>();
builder.Services.AddTool<ShellTool>();
builder.Services.AddTool<WebTool>();
builder.Services.AddTool<SchedulerTool>();
builder.Services.AddAgentRuntime(); // Orchestrator + Runtime + PromptComposerAl inicio, cada singleton ITool se resuelve y registra en el ToolRegistry. El ejecutor puede entonces encontrar cualquier herramienta por nombre.
Demo en Vivo#
Demo 1: Agente usa la herramienta FileSystem
- Abrir la UI de Blazor
- Escribir: "List files in the current directory"
- Observar cómo el agente emite una llamada a herramienta
file_system→ ejecutar → mostrar resultados - Señalar la llamada/resultado de herramienta en la respuesta
Demo 2: Agente usa la herramienta Web
- Escribir: "What's on the front page of Hacker News?"
- Observar cómo el agente emite una llamada a
web_fetch→ obtener → resumir - Señalar: el agente decidió usar la herramienta, obtuvo la página, luego la resumió
Demo 3: Rechazo de comando bloqueado
- Escribir: "Run
rm -rf /on the server" - Observar cómo el agente intenta usar la herramienta
shell→ ShellTool la bloquea → el agente reporta el rechazo - Señalar: la puerta de seguridad funcionó — el comando nunca se ejecutó
🤖 Momento Copilot: Agregar Seguimiento de Duración de Ejecución#
Cuándo: ~minuto 40
Contexto: Hemos visto el bucle del agente ejecutar herramientas. Ahora agreguemos observabilidad.
Qué hacer: Abrir ToolExecutor.cs y preguntar a Copilot:
In the ToolExecutor, add a method
GetExecutionStats()that returns a dictionary of tool name → average execution duration. Track each tool's execution duration in aConcurrentDictionary<string, List<TimeSpan>>field. Update it after each successful execution.
Resultado esperado: Copilot agrega un campo _executionStats y un método GetExecutionStats() que calcula promedios.
Por qué es interesante: Muestra cómo el patrón de punto de control (todas las herramientas a través del ejecutor) hace trivial agregar preocupaciones transversales como métricas.
Cierre (8 min)#
Resumen de Seguridad#
| Amenaza | Herramienta | Defensa |
|---|---|---|
| Recorrido de Ruta | FileSystemTool | Path.GetFullPath + verificación de límite del espacio de trabajo |
| Inyección de Comandos | ShellTool | HashSet de comandos bloqueados + tiempo límite |
| SSRF | WebTool | Lista de bloqueo de IP privadas + validación de esquema |
Tres amenazas. Tres defensas. Todas implementadas como validación de entrada antes de la ejecución.
Lo Que Construimos#
- ✅ Capa de abstracción de herramientas (ITool, IToolExecutor, IToolRegistry)
- ✅ Puerta de política de aprobación (IToolApprovalPolicy)
- ✅ FileSystemTool con prevención de recorrido de ruta
- ✅ ShellTool con lista de bloqueo de comandos y tiempo límite
- ✅ WebTool con protección contra SSRF
- ✅ SchedulerTool con CRUD de trabajos
- ✅ Bucle de razonamiento del agente (prompt → modelo → herramienta → bucle)
- ✅ Composición de prompts con inyección de herramientas
Vista Previa: Sesión 3#
"El agente ahora tiene manos. Próxima sesión: darle personalidad y memoria."
La Sesión 3 cubre:
- Skills — Archivos de personalidad basados en YAML que personalizan el comportamiento del agente
- Memoria — Resumen de conversaciones para contexto a largo plazo
- Carga de Skills — Descubrimiento dinámico e inyección en el prompt del sistema