Ir al contenido

Barandillas — una política para cada acción mutadora

Los agentes de Control Center pueden eliminar archivos, enviar ramas, publicar pull requests y llamar a API externas. Cada una de esas cosas es un tipo distinto de riesgo y, durante mucho tiempo, solo una de ellas —los comandos de shell— tenía política. Las barandillas existen para responder de forma uniforme a una única pregunta, para cada acción mutadora que pueda emprender un agente, sea cual sea el entorno de ejecución o la herramienta que la proponga: ¿se permite, requiere una aprobación o se deniega?

Hay exactamente un motor de políticas. Se consulta en todos los puntos por los que una acción puede salir del sistema: el registro de herramientas del arnés integrado, la puerta del despachador MCP del lado del servidor y las mutaciones de operaciones de repositorio. Una regla escrita una vez se aplica, por tanto, tanto al entorno de ejecución integrado como a los CLIs de agentes externos: no hay una segunda vía, más silenciosa, que la eluda. La antigua política de comandos, limitada a bash, sigue funcionando: una regla puede coincidir con un prefijo de comando en lugar de con una clase de acción, y ambas formas conviven en el mismo almacén.

El motor no sustituye, sin embargo, al entorno aislado del sistema operativo, ni el entorno aislado lo sustituye a él. Responden a preguntas distintas: las barandillas deciden si una llamada que Control Center puede ver llega a producirse, mientras que el entorno aislado acota hasta dónde puede llegar el proceso resultante. cc_server aplica el entorno aislado allí donde el host ofrece un backend, pero Windows no ofrece ninguno y Linux necesita bwrap y socat. En esos hosts, las barandillas son la capa de aplicación más externa en lugar de una política sentada sobre un suelo, algo que conviene saber cuando decides cuán permisivo ser.

ActionClass: una taxonomía deliberadamente cerrada

Sección titulada «ActionClass: una taxonomía deliberadamente cerrada»

Cada herramienta mutadora declara a qué ActionClasses pertenece, dentro de un conjunto cerrado de trece clases de efecto. Cinco de ellas preguntan de serie; el resto permiten, y es una regla lo que cambia eso.

Clase Valor predeterminado Qué cubre
fileDelete prompt Eliminar un archivo (una escritura con forma de borrado, rm)
gitPush prompt Enviar a un remoto de git
prCreate prompt Crear una pull request
prPublish prompt Publicar una revisión, fusionar una PR
vendorSyncWrite prompt Escribir en un proveedor externo de tickets (Linear, Jira, sincronización con GitHub)
fileWriteOutsideWorktree allow Escribir un archivo fuera del worktree aislado
gitCommit allow Crear un commit de git
networkEgress allow Salida a la red (obtención web, HTTP arbitrario)
secretAccess allow Leer un secreto o una credencial
packageInstall allow Instalar un paquete (npm, pip, pub, brew)
processSpawn allow Lanzar un proceso (shell y cualquier cosa que invoque un shell)
workspaceMutation allow Modificar la estructura del espacio de trabajo (repos, espacios, agentes)
enclosureControl allow Controlar un recinto (rig): arrancar una VM, enviarle entrada

Las ocho clases permitidas de forma predeterminada se eligieron partiendo de la premisa de que bajo ellas actúa el suelo del entorno aislado. Así ocurre allí donde el host ofrece un backend, y no ocurre en Windows ni en una máquina Linux a la que le falten bwrap y socat; por eso, si quieres que fileWriteOutsideWorktree o packageInstall queden restringidas ahí, esa es una regla que tienes que escribir.

El conjunto es cerrado a propósito. Una nueva herramienta mutadora que no declare ninguna clase no supera el test de trinquete, de modo que la taxonomía solo puede crecer mediante una decisión explícita y argumentada: «la dispersión de la taxonomía es la muerte de esta función». Trece clases bastan para expresar las políticas que la gente escribe de verdad (no enviar nunca sin preguntar, no eliminar nunca fuera de un worktree, permitir siempre las lecturas) y son lo bastante pocas para que la interfaz de ajustes siga siendo comprensible.

Una regla asigna (scope, ActionClass | command prefix) → allow | prompt | deny. Los ámbitos se anidan de más específico a menos:

espacio > agente > espacio de trabajo > preajuste del modo > valor predeterminado del sistema

La resolución va primero por especificidad: decide el primer ámbito con una regla coincidente y la resolución se detiene ahí. Lo más restrictivo solo sirve de desempate —entre dos reglas igual de específicas dentro de un mismo ámbito y al combinar las diversas clases que declara una misma acción—. Dentro de un ámbito, una regla de comando coincide según el prefijo de comando más largo. Permitir no gana a denegar en ningún sitio.

Dos decisiones conllevan garantías firmes:

  • Los avisos deniegan por defecto. Un prompt sin ningún aprobador conectado se resuelve como denegado. Una pregunta sin respuesta nunca es un sí.
  • Combinación más restrictiva. Una acción que abarca varias clases las combina del modo más restrictivo; cuando varias clases preguntan, una sola confirmación las enumera todas.

Deliberadamente no hay ningún enclavamiento por turno. Cada llamada a una herramienta vuelve a resolver la política desde cero, de modo que la repetición de avisos para una misma acción está acotada por el propio «recuerda esta decisión» del operador, y no por nada ligado al turno.

Una regla puede restringir los ARGUMENTOS de una acción, no solo su verbo. «Puede enviar» y «puede enviar a feature/*» son afirmaciones distintas, y solo la segunda es un control: una puerta de capacidad que autoriza únicamente el verbo es la forma que hay detrás de la mayoría de los incidentes publicados con agentes.

Una restricción puede nombrar:

  • rutas — patrones glob (repos/**, !**/.env)
  • refs — referencias de git, con negación ! (['**', '!main'] se lee como «cualquier rama excepto main»)
  • hosts — destinos de red, con comodines de subdominio (*.internal)
  • comandos — prefijos de comando, que coinciden en un límite de palabra
  • topes — maxCount / maxCents, de modo que «eliminar hasta 50 archivos» es expresable

Tres propiedades se derivan de mantener la gramática cerrada y sin bucles, y las tres son el objetivo: toda regla se representa como una frase que puedes leer tal cual, toda denegación puede nombrar la restricción que coincidió y el conjunto de la política se puede enumerar: puedes ver qué puede hacer un agente antes de que se ejecute.

Tres reglas que importan:

  • Una regla restringida es más específica que una sin restricciones en el mismo ámbito, así que «denegar el envío a main» gana a «permitir el envío» sin que nadie tenga que pensar en el orden.
  • Una regla restrictiva (denegar / preguntar) se aplica cuando la solicitud toca cualquier valor que nombre; una permisiva (permitir) solo se aplica cuando la solicitud está enteramente dentro de ella. Leer ambas del mismo modo permitiría a un agente blanquear una ruta prohibida colocando otra inocente a su lado.
  • Una regla que nombra una faceta de la que la solicitud no dice nada ni se aplica ni se omite: escala a una aprobación. Omitirla en silencio dejaría inerte cualquier regla de rama protegida frente a una herramienta que no informa de su ref; denegarla en silencio rechazaría todos los envíos que el extractor no sabe describir. Sin ningún aprobador conectado, la regla existente de denegación por defecto convierte esa escalada en una denegación.

Qué puede detectar una regla de comando y qué no

Sección titulada «Qué puede detectar una regla de comando y qué no»

Una regla commandPrefix coincide en un límite de palabra: git push cubre git push origin main y no git pushx. No normaliza el shell. git push (dos espacios), cd sub && git push y GIT_DIR=… git push son cadenas distintas y ninguna coincide con una regla sobre git push.

Ese es un límite real y se expone en lugar de disimularse: las reglas de comando son una comodidad para el operador a la hora de modelar el comportamiento habitual de un agente, no una frontera frente a un agente adversario. Las fronteras son las clases de efecto (que una herramienta declara y no puede reescribir), la red de comandos del modo que hay bajo el almacén de políticas y el entorno aislado del sistema operativo bajo ambos. Una regla de comando que se resuelve como allow nunca relaja ninguna de esas dos capas: la red del modo se ejecuta después y su deny es definitivo.

Responder a un aviso con «recuerda esto» escribe una regla de política real: con ámbito (espacio / agente / espacio de trabajo), restringida por argumentos (aprobar un envío a feature/login concede feature/**, no main) y autorrevocable según un TTL explícito. Decir sí durante las próximas horas no es lo mismo que reescribir en silencio tu política permanente, y las reglas que escribe acaban en el mismo almacén con procedencia remembered y una caducidad, de modo que se pueden revocar antes de tiempo.

Cada regla lleva uno:

  • advisory — se permite, pero se registra que la regla coincidió. Esta es la vía de adopción: despliega una regla estricta en modo advisory, consulta el registro de auditoría durante una semana para ver exactamente qué habría bloqueado y luego asciéndela. Sin este nivel, las políticas estrictas nunca llegan a activarse.
  • soft — se deniega, con posibilidad de anulación por quien tenga el permiso de anulación y una justificación registrada. La anulación es en sí misma un evento auditado.
  • hard (el valor predeterminado) — se deniega. Nada lo anula.

El operador de una instalación puede fijar políticas que ningún administrador de espacio de trabajo pueda relajar. Las reglas gestionadas viven en el servidor y se combinan del modo más restrictivo con la cadena propia de cada espacio de trabajo; no son un ámbito a la cabeza de esa cadena, porque un allow gestionado a la cabeza de la cadena permitiría que la instalación anulara una denegación de un espacio de trabajo, lo contrario de para lo que sirve un tope. Una regla gestionada solo puede, por tanto, apretar.

Definir CC_SERVER_MANAGED_POLICY con un archivo JSON prevalece por completo sobre las reglas almacenadas, y eso es lo que permite a un operador fijar una postura que ninguna interfaz de administración puede cambiar: la respuesta a «¿puedo impedir que mis desarrolladores desactiven los controles de seguridad?».

El dial de autonomía es un perfil, no un sistema paralelo

Sección titulada «El dial de autonomía es un perfil, no un sistema paralelo»

El dial de autonomía por espacio es un perfil con nombre sobre este mismo almacén de políticas, no un segundo mecanismo. Sus tres ajustes se almacenan y se envían como proposeOnly, actWithApproval y actFreely (las formas con guion son solo texto de la interfaz y autonomy.setForSpace rechaza cualquier otra cosa). Dejarlo sin definir se comporta como actWithApproval.

Qué hace cada uno a una decisión ya resuelta:

  • proposeOnly deniega sin más toda herramienta sometida al control. El agente aún puede razonar y responder; simplemente no puede actuar, y su mensaje dice por qué.
  • actWithApproval (y sin definir) es la puerta de aprobación que deniega por defecto descrita más arriba.
  • actFreely preaprueba todo lo que no se haya resuelto como un deny firme.

Esa última merece decirse con claridad en lugar de suavizarse: bajo actFreely, una decisión prompt no escala a un aprobador: se permite y no se pregunta a nadie. Solo una regla deny explícita sobrevive al dial. Así que actFreely es una concesión deliberada de autonomía, no un ajuste de comodidad, y la regla de denegación por defecto protege exactamente el caso en el que importa (actWithApproval, el valor predeterminado).

La delegación entre agentes está protegida en el punto de control de delegate_task por los cuatro controles: un tope de profundidad (3 de forma predeterminada), la detección de ciclos, un techo de autonomía (un delegado nunca puede actuar con más autonomía que su delegador: el privilegio no se puede blanquear pasando trabajo a un agente más libre) y una envolvente de presupuesto (la delegación se carga al presupuesto restante del delegador y no puede acuñar más). Cada rechazo es explícito, con el motivo del control devuelto al agente tal cual.

No todos los entornos de ejecución permiten que Control Center intercepte todas las acciones. Los CLIs de agentes externos ejecutan algunas herramientas de forma nativa, dentro de su propio proceso, donde no cabe ninguna puerta. En lugar de fingir lo contrario, cada adaptador lleva una matriz de honestidad: por adaptador, si Control Center filtra la superficie de herramientas, intercepta las llamadas a herramientas, observa el contrato de finalización, puede ver las herramientas nativas del ejecutor y si las herramientas en proceso están cubiertas por un perfil de entorno aislado.

Merece la pena leer dos entradas antes de fiarte de una configuración. El arnés integrado declara que sus herramientas de archivo en proceso no están aisladas: la superficie de herramientas y este motor de políticas son su único límite del sistema de archivos. Claude Code declara que sus propias llamadas a herramientas no se pueden interceptar, porque se lanza con --dangerously-skip-permissions; Control Center ve sus llamadas mcp__* y, mediante un hook, sus líneas de comandos de Bash, mientras que sus propias herramientas de lectura, escritura y edición se ejecutan sin ser vistas.

La pantalla de ajustes muestra esta matriz, porque el único fallo imperdonable de un sistema de barandillas es reclamar una cobertura que no existe.

Las barandillas están en Ajustes → Espacio de trabajo → Permisos de agentes (/workspaces/<id>/settings/workspace/permissions). La pantalla contiene la matriz de políticas donde se escriben las reglas, plantillas de políticas, una sonda de tipo «¿Qué pasaría si?», el registro de auditoría de autorización y la matriz de honestidad de los adaptadores.

La sonda recibe una clase de acción (o un comando) y un ámbito, y muestra qué regla decidió y por qué. Como la resolución es pura y determinista —la misma acción, el mismo ámbito, la misma decisión—, la respuesta de la sonda es la respuesta que recibirá el agente.