# Herramientas de agentes sin secretos: contratos de acciones que resisten

Los agentes deben solicitar acciones, no recibir los medios para suplantar a una persona o una cuenta de servicio. Parece obvio hasta que se examina una herramienta típica: una función `http_request` acepta una URL, encabezados, método y cuerpo, mientras el agente obtiene un token bearer de una variable de entorno. La llamada parece ordenada. La autoridad, sin embargo, queda repartida entre el texto del prompt, la memoria del proceso, los registros, el historial del shell y cualquier subproceso que el agente inicie después.

Un diseño sin secretos establece un límite claro entre la intención y la ejecución. El agente dice: «crea un despliegue para este servicio en este entorno». Una capa propietaria de las credenciales decide si la acción puede ejecutarse, selecciona la identidad adecuada, realiza la llamada autenticada y devuelve el resultado. Así cambia lo que puede auditarse, aprobarse y revocarse. También obliga a diseñar interfaces que realmente merezcan autonomía.

## El contrato debe describir la intención, no el transporte

Un contrato de acción nombra la operación que una persona reconoce y limita sus entradas a los datos necesarios para realizarla. Los detalles del transporte deben quedar detrás del límite. La diferencia pasa desapercibida porque HTTP hace que cada acción parezca un método, una URL, unos encabezados y un cuerpo JSON.

Considera dos interfaces para abrir una solicitud de cambio. La primera es común, pero insegura para un proceso autónomo:

```json
{
  "name": "http_request",
  "input": {
    "method": "POST",
    "url": "https://code.example/api/projects/alpha/changes",
    "headers": {
      "Authorization": "Bearer ${TOKEN}",
      "Content-Type": "application/json"
    },
    "body": {
      "title": "Fix timeout",
      "branch": "agent/fix-timeout"
    }
  }
}
```

Esta interfaz concede al agente autoridad sobre el destino, el método de autenticación y la forma de la solicitud. Quitar el token literal no resuelve el problema si el agente puede elegir un alias de encabezado, un identificador de credencial, una URL de proxy o un comando de shell que lea el token en otro lugar. Has movido el secreto, pero no has reducido la autoridad.

Una interfaz orientada a contratos se parece más a esto:

```json
{
  "name": "create_change_request",
  "input": {
    "project": "alpha",
    "source_branch": "agent/fix-timeout",
    "title": "Fix timeout in retry path",
    "description": "Adds a bounded retry and a regression test."
  }
}
```

La capa de ejecución asocia `project` a un punto final conocido y a una cuenta aprobada. Añade por sí misma el encabezado de autenticación. Puede rechazar el nombre de la rama, verificar el repositorio de destino, pedir aprobación o devolver un error del servicio remoto. El agente no tiene ningún parámetro equivalente a «usa la credencial que conceda más acceso».

Hay una diferencia que se confunde con frecuencia: **sin secretos no significa lo mismo que con el token oculto**. La ocultación intenta controlar lo que el agente ve después de recibir la autoridad. Un contrato de acción impide que el agente tenga esa autoridad desde el principio. Si se filtra un prompt, se copia una transcripción de la herramienta o un subproceso lee el entorno, el primer diseño ya ha perdido una credencial. El segundo puede exponer datos operativos, que necesitan sus propios controles, pero no entrega el material de firma.

El contrato tampoco debe fingir que cada punto final merece una herramienta propia. Las acciones personalizadas tienen sentido cuando una persona puede expresar el resultado esperado en una frase. «Reinicia esta carga de staging» describe un resultado. «Envía un PATCH a cualquier URL» es una primitiva de transporte. Si necesitas esa primitiva para una tarea de mantenimiento, entrégasela a una integración separada y muy limitada, no a un agente de programación de propósito general.

## El propietario de la credencial debe ejecutar la solicitud

Un contrato no protege nada si el agente sigue haciendo la llamada de red final con un secreto montado en su proceso. El componente que almacena la credencial debe realizar por sí mismo la solicitud HTTP o la conexión SSH.

El límite de ejecución tiene cinco responsabilidades:

- Resolver el nombre de la acción en un destino fijo y un comportamiento de protocolo conocido.
- Elegir una identidad almacenada de un conjunto pequeño y aprobado.
- Inyectar la credencial únicamente en la solicitud saliente o en el intercambio de autenticación SSH.
- Registrar la solicitud, la decisión y el resultado sin escribir material secreto en el registro.
- Devolver una respuesta adaptada a la acción, no un volcado de su estado interno.

El proceso del modelo no debe recibir ni un token ni una clave privada, tampoco una temporal. Evita convenciones de shell como `TOKEN=$(vault read ...)`, archivos de credenciales en el directorio de trabajo, valores de `Authorization` en comandos curl generados y agentes SSH compartidos con un shell controlado por el agente. Cada opción parece cómoda porque conserva los scripts existentes. Todas convierten el proceso del agente en propietario de una credencial.

La buena práctica de seguridad de OAuth 2.0 de IETF plantea el mismo punto en otro contexto: los tokens bearer deben protegerse en almacenamiento y tránsito porque cualquiera que los posea puede usarlos. Un token bearer no se vuelve seguro por pedirle a un modelo que no lo imprima. La posesión es la comprobación de autorización. Para una herramienta de agentes, el diseño preferible consiste en no entregar la posesión al proceso.

En SSH, el límite debe controlar más que la clave privada. Una interfaz de tipo `ssh host command` ofrece un alcance amplio aunque la clave nunca abandone un asistente. El asistente debe seleccionar una definición de host y una identidad almacenadas, y después imponer una forma de comando adecuada para ese host. Un host de despliegue podría permitir `status`, `restart-service` y `tail-release-log` con un nombre de servicio. No debería aceptar silenciosamente `bash -c` porque alguien quería un atajo.

No confundas esto con un proxy de intermediario. Un proxy retransmite tráfico arbitrario del cliente y suele ver las credenciales en tránsito. Una capa de acciones propietaria de las credenciales recibe una solicitud para una operación con nombre, construye la llamada saliente y conserva la credencial en su propia bóveda. Esa diferencia determina si el agente puede convertir una acción aprobada en otra distinta.

## El diseño de los parámetros decide cuánta autoridad se filtra

Cada campo de un contrato crea un grado de libertad. Los buenos campos identifican el objeto de trabajo o proporcionan contenido que la acción necesita realmente. Los malos cambian el destino de la autoridad, la identidad usada o la operación de nivel inferior.

Aplica esta prueba a cada entrada propuesta: si el agente cambia este valor, ¿puede redirigir una solicitud privilegiada a otro sistema, ampliar el conjunto de recursos afectados o alterar la autenticación? Si la respuesta es sí, elimina el campo, conviértelo en un enum asociado por el ejecutor o divide la operación en contratos separados.

Una interfaz de despliegue muestra la idea:

```json
{
  "name": "deploy_release",
  "input_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["service", "environment", "version", "reason"],
    "properties": {
      "service": {"type": "string", "enum": ["api", "worker"]},
      "environment": {"type": "string", "enum": ["test", "production"]},
      "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
      "reason": {"type": "string", "maxLength": 500}
    }
  }
}
```

El esquema impide campos inesperados como `url`, `headers`, `credential_name` o `command`. El ejecutor puede asociar `service` y `environment` a un destino de despliegue conocido. `additionalProperties: false` importa más de lo que parece. Sin él, un validador permisivo puede conservar un campo desconocido y alguien podría conectarlo más adelante a un cliente HTTP «por flexibilidad». Así es como un punto de extensión aparentemente inocente se convierte en una salida para las credenciales.

Los enums no siempre bastan. Un nombre de repositorio, rama, incidencia o ruta de archivo puede tener que variar. Valida esos valores según su dominio y aplica una comprobación de límites después de resolverlos. Por ejemplo, resuelve el identificador del repositorio mediante una lista local de permitidos y usa después la ubicación remota asociada. No aceptes una URL de repositorio para decidir si parece fiable.

El texto libre exige una decisión aparte. Un agente puede necesitar escribir la descripción de una incidencia, el resumen de una solicitud de cambios o una respuesta de soporte. Ese texto es contenido, no autoridad, pero puede causar daños mediante menciones, marcado, plantillas o comandos incrustados que consuma el servicio posterior. Limita su longitud, define claramente cómo se mostrará y no lo interpoles en un comando de shell. Cuando una acción deba ejecutar un comando, construye directamente el vector de argumentos y mantén el texto no confiable en un argumento de datos, nunca en una cadena de comandos.

## Las herramientas de solicitudes genéricas crean motores de políticas ocultos

Una herramienta HTTP genérica resulta popular porque permite conectar un agente a cualquier servicio en una tarde. Para la mayoría de los trabajos privilegiados de un agente, es una mala elección: cada prompt, descripción de herramienta y rama de código se convierte en una política de autorización no oficial.

Un equipo suele empezar con un envoltorio como:

```text
request(method, url, headers, body)
```

Después añade barreras: bloquea algunos dominios, elimina `Authorization`, permite ciertos métodos, analiza un prefijo de URL, rechaza `localhost` o exige un diálogo de aprobación para llamadas arriesgadas. Meses más tarde alguien necesita un nuevo punto final con un encabezado personalizado, añade una excepción y el envoltorio ya contiene un lenguaje de políticas sin pruebas ni responsable claro.

El problema no es que las herramientas genéricas sean siempre malas. Encajan en una consola de depuración operada por una persona, donde el operador ya tiene la autoridad y puede inspeccionar cada byte. También sirven para un servicio de integración que recibe llamadas de código controlado y tiene una identidad de red limitada. Un agente autónomo es distinto: puede hacer muchas llamadas, descubrir rutas inesperadas y actuar sobre texto no confiable. Necesita menos grados de libertad.

Escribe acciones con nombre alrededor de unidades de trabajo estables. Para un servicio de control de código, prefiere `read_merge_request`, `comment_on_merge_request` y `create_branch` a un cliente REST universal. Para operaciones, prefiere `get_service_status`, `fetch_release_logs` y `request_deployment`. Puede que necesites más contratos, pero cada uno tendrá un responsable, un conjunto de pruebas, una etiqueta de aprobación clara y un alcance revisable.

Tampoco escondas una solicitud genérica dentro de una acción con nombre. Una herramienta llamada `update_ticket` que acepte `path`, `method` y `body` arbitrarios solo ha cambiado la etiqueta. El contrato debe fijar esos detalles. Puede exponer un objeto de cambios controlado cuando la API posterior lo necesite, pero el ejecutor debe decidir el punto final, el método HTTP, el tipo de contenido y la cuenta.

La especificación de Model Context Protocol ayuda a descubrir herramientas porque permite que un servidor publique nombres, descripciones y esquemas JSON de entrada para un cliente. Ese esquema es útil, pero no puede hacer segura una acción demasiado amplia. JSON Schema puede decir que una URL es una cadena. No puede decir que esa URL sea el único punto final de facturación al que una credencial de producción debe llegar. La autorización sigue siendo responsabilidad de la capa de ejecución.

## La aprobación debe nombrar una acción que una persona pueda juzgar

La aprobación humana funciona cuando la persona ve una solicitud reconocible y puede rechazarla rápidamente. Falla cuando se le pide aprobar un paquete opaco de detalles de transporte después de que el agente ya haya tomado las decisiones importantes.

Compara estas tarjetas de aprobación:

```text
Allow POST https://api.example/v1/resources/882?
Headers: Authorization, X-Region, X-Client
```

```text
Deploy version 2.14.3 of api to production
Reason: Fixes failed payment retries
Requested by: signed agent process build-worker
```

La segunda tarjeta permite juzgar la intención y ofrece al registro de auditoría una frase útil. La primera obliga a reconstruir el significado a partir de una URL y una lista de encabezados, lo que favorece la fatiga de aprobación. Las personas aceptan mensajes ilegibles sin leerlos, sobre todo cuando una ejecución normal del agente genera varios.

Usa la aprobación en el punto donde una decisión cambia la autoridad. Una capa de ejecución puede autorizar una vez un proceso nuevo durante una sesión y exigir una decisión nueva para determinadas credenciales sensibles o acciones destructivas. Así el trabajo rutinario sigue siendo práctico sin tratar todas las credenciales como equivalentes. Un token de proyecto de solo lectura y una identidad de despliegue en producción no deberían compartir una regla de aprobación solo porque ambos viajan en encabezados HTTP.

El texto de aprobación debe indicar quién solicitó la operación. La identidad del proceso es útil porque un agente de terminal, un asistente en segundo plano y un ejecutable desconocido no merecen el mismo nivel de confianza. En macOS, la autoridad de firma de código puede ofrecer a quien aprueba una señal concreta del origen. No demuestra que todas las instrucciones del prompt sean seguras, pero responde a la primera pregunta: ¿qué proceso pide actuar con esta cuenta?

La aprobación nunca debe ser el único control. Una persona puede leer mal un prompt, aprobar con prisa o dejar abierta una sesión. El contrato todavía necesita entradas limitadas y una ruta fija para la credencial. A la inversa, no añadas un lenguaje de políticas cuando un contrato claro y una decisión de aprobación resuelvan la necesidad. Las reglas que comparan campos arbitrarios, ventanas temporales, expresiones regulares y afirmaciones del usuario pronto se convierten en otro programa que nadie puede revisar con confianza durante un incidente.

## Un despliegue fallido muestra dónde se rompen los contratos laxos

Un fallo habitual comienza con un agente que puede desplegar en test mediante una herramienta de shell. El equipo guarda un token de nube en el entorno del agente porque la CLI de despliegue lo necesita. El esquema acepta `environment` y `extra_args`, que parecían inofensivos cuando solo existía test.

Una incidencia pide al agente «verificar el arreglo urgente en test y compartir el resultado». El agente ejecuta el comando previsto. Después encuentra un mensaje antiguo de despliegue en el repositorio e intenta un argumento adicional copiado de un script viejo. Ese argumento selecciona producción, cambia la cuenta de destino o inyecta una expansión del shell. El token tenía alcance de producción porque mantener credenciales separadas parecía demasiado trabajo. En ese momento, la redacción del prompt ya no puede protegerte. El proceso tiene una autoridad amplia y la interfaz le permite elegir el destino.

Un límite contractual cambia la secuencia:

1. El agente llama a `deploy_release` con un servicio, entorno, versión y motivo enumerados.
2. El ejecutor resuelve el entorno en un destino fijo y selecciona la identidad asignada a ese destino.
3. El ejecutor pide una decisión si esa identidad la requiere y registra el resultado en la ejecución del agente que la solicitó.
4. El ejecutor devuelve un identificador y el estado del despliegue, o una denegación estructurada que explica al agente por qué no puede continuar.

El agente no puede añadir `--account`, establecer un punto final de nube ni leer un token. Un valor equivocado de `production` sigue siendo posible, porque las personas y los modelos pueden pedir algo incorrecto. Pero el texto de aprobación ahora dice «producción» en lenguaje claro, la identidad seleccionada puede tener únicamente la autoridad prevista para despliegues de producción y el registro vincula la decisión con el proceso y la solicitud.

Esta diferencia importa durante el diagnóstico. En el diseño laxo, los investigadores suelen encontrar fragmentos: un transcript de shell, eventos de auditoría de la nube, un registro de CI y quizá un token que ahora necesita rotarse. En el diseño contractual pueden consultar la acción solicitada, el destino resuelto, la etiqueta de identidad, el resultado de la aprobación, el estado de la respuesta y la sesión que la inició. Un registro de auditoría no borra un error, pero reduce el tiempo dedicado a adivinar qué ruta se ejecutó.

## Las respuestas de error deben guiar la recuperación sin exponer información interna

Una capa de acciones segura debe devolver errores que el agente pueda usar sin recibir el secreto, los datos de firma de la solicitud ni la estructura interna de la bóveda. Los fallos vagos empujan a los agentes hacia reintentos y soluciones alternativas. Los fallos demasiado detallados convierten los registros de errores en un canal de información.

Usa códigos de error estables y una estructura pública pequeña:

```json
{
  "ok": false,
  "error": {
    "code": "APPROVAL_REQUIRED",
    "message": "Deployment to production needs user approval.",
    "retryable": true,
    "request_id": "act_01H..."
  }
}
```

Una bóveda bloqueada debe informar `VAULT_LOCKED`; una decisión de usuario denegada, `APPROVAL_DENIED`; una infracción del contrato, `INVALID_ARGUMENT`; y un 429 del servicio remoto puede informar `REMOTE_RATE_LIMITED`. El agente puede comunicar el estado, esperar, reintentar cuando cambie la condición indicada o elegir una alternativa no destructiva. No debe recibir un error con un encabezado de autorización, un volcado del sujeto del token, la configuración de un host privado ni una solicitud firmada completa.

Separa el fallo de autorización del fallo remoto. «Permiso denegado» puede significar que el ejecutor local rechazó la acción, que la cuenta remota seleccionada carece de permisos o que el servicio posterior rechazó una autenticación mal formada. Cada caso necesita una reparación distinta. El mensaje público puede ser breve mientras el registro protegido del ejecutor conserva el motivo preciso y el estado remoto.

Los reintentos necesitan semántica contractual. Las operaciones de lectura suelen tolerarlos. Crear una incidencia, enviar un mensaje o iniciar un despliegue puede no tolerarlos. Incluye un identificador de idempotencia cuando la API remota lo admita, generado por el ejecutor o proporcionado como un identificador de solicitud limitado. Registra la asociación antes de enviar la solicitud y reutilízala al reintentar. No permitas que el agente invente un identificador nuevo cada vez que ve un tiempo de espera, porque podría crear trabajo duplicado intentando ayudar.

Para las acciones sin idempotencia remota, usa una disposición de preparación y confirmación. La acción de preparación devuelve un plan de vida corta que describe el destino y la diferencia. La acción de confirmación hace referencia a ese plan y exige una aprobación vigente. Cuesta un viaje adicional, pero es más barato que repetir un pago, una eliminación o un cambio en producción después de un fallo de red ambiguo.

## Los registros de auditoría necesitan dos vistas y una fuente de verdad

Un sistema de auditoría útil responde a dos preguntas: ¿qué intentó ejecutar esta sesión del agente? y ¿qué hizo cada llamada privilegiada? Si las mezclas en un único flujo indiferenciado, resulta difícil reconstruir una sesión o localizar una solicitud concreta.

Conserva un diario de sesión para la ejecución. Debe mostrar la identidad del proceso, el inicio y el final, la decisión de autorización, el estado de revocación y las acciones solicitadas durante la ejecución. Conserva también un diario de actividad para las llamadas. Debe mostrar el nombre de la acción, los parámetros normalizados, la etiqueta de la credencial sin su secreto, el resultado de la aprobación, los tiempos, la clase de destino y el resultado.

Ambas vistas deben derivarse del mismo registro de solo adición. De lo contrario, la pantalla de sesión y el registro de llamadas pueden discrepar si un escritor falla o filtra eventos de otra manera. Un registro cifrado de escritura ciega tiene además una ventaja práctica: el componente que añade un evento no necesita descifrar los registros anteriores para escribir uno nuevo.

La evidencia contra manipulaciones necesita una comprobación sin conexión. Una cadena de hashes permite detectar eliminaciones, sustituciones o cambios de orden cuando el verificador dispone de la secuencia del registro. La comprobación debe ejecutarse sobre el texto cifrado para que un auditor pueda verificar la continuidad sin recibir la clave de la bóveda. Esto no demuestra que una máquina comprometida nunca haya dejado de registrar un evento. Sí demuestra que una cadena conservada no se ha editado silenciosamente después. Son afirmaciones distintas.

Un verificador de línea de comandos debe hacer que los fallos sean claros. Su salida puede ser tan sencilla como:

```text
$ sp audit verify audit.log
verified: 184 records
first sequence: 9012
last sequence: 9195
chain: valid
```

Si se cambia el registro 9137, el comando debe identificar la primera secuencia rota y terminar con un código distinto de cero. No informes únicamente de que «la verificación ha fallado». Los responsables de responder a un incidente necesitan saber dónde deja de ser fiable la evidencia.

Sallyport usa esta separación para las sesiones de agentes y la actividad individual, proyectando ambas vistas desde un único registro de auditoría cifrado y encadenado mediante hashes que `sp audit verify` puede comprobar sin la clave de la bóveda. Es la forma adecuada para una pasarela local de agentes, porque revocar una sesión activa e investigar una llamada concreta son tareas distintas.

## Los contratos necesitan pruebas que intenten escapar de ellos

Las pruebas del camino correcto demuestran que una acción funciona. Las pruebas de seguridad demuestran que sus entradas declaradas son los únicos controles que tiene el solicitante. Escribe esas pruebas antes de añadir un parámetro cómodo, porque ahí es donde la autoridad suele volver a filtrarse.

Para cada acción, prueba al menos estos casos:

- Rechazar un campo inesperado, incluidos `headers`, `url`, `command` y las referencias a credenciales.
- Rechazar valores que resuelvan fuera del conjunto de recursos permitido para la acción.
- Confirmar que las solicitudes HTTP salientes reciben credenciales solo después de que el ejecutor construya el destino.
- Confirmar que las entradas de auditoría omiten tokens, material de claves privadas y campos de autorización firmados.
- Confirmar que una sesión denegada o revocada no puede reutilizar una aprobación anterior.

Usa un servidor saliente falso en las pruebas e inspecciona la solicitud recibida. La prueba debe comprobar la URL real, el método, los encabezados que construye el ejecutor y la ausencia de autenticación controlada por el solicitante. Simular únicamente el cliente interno del ejecutor deja fuera la pregunta importante: ¿qué saldría de la máquina si el agente proporcionara parámetros hostiles?

Prueba también las entradas incómodas que un modelo acabará produciendo: un identificador de repositorio con prefijo de esquema, un nombre de rama con puntuación de shell, un carácter Unicode parecido en una etiqueta de entorno, campos JSON repetidos, una descripción enorme y un tiempo de espera después de que el servicio remoto haya aceptado la acción. La validación debe fallar de forma segura. Si el ejecutor no puede resolver un destino solicitado con confianza, debe rechazar la llamada y devolver un error útil.

Revisa los contratos como código con autoridad. Pregunta si un campo nuevo ofrece al solicitante una ruta hacia otro host, una cuenta más amplia, un comando diferente o una clase de objeto distinta. Si es así, haz explícita esa autoridad en el nombre de la acción y en su comportamiento de aprobación. Una acción sencilla `delete_file` con una ruta absoluta libre es mucho más difícil de razonar que `remove_preview_asset`, cuyo identificador se resuelve dentro de un proyecto conocido.

El primer contrato que suele merecer una corrección es el que acepta una URL arbitraria o una cadena de shell. Sustitúyelo por la acción con nombre más pequeña que cubra el trabajo que las personas realmente necesitan. La interfaz será menos ingeniosa y el agente tendrá menos poder de formas que después no tendrás que explicar. Eso es progreso.
