# Autorización de procesos de agentes: revoca el acceso bifurcado

Los agentes autónomos rara vez se detienen de forma tan limpia como sugieren sus registros de tareas. Un agente puede terminar una solicitud de programación, mostrar un alegre mensaje de finalización y dejar atrás un formateador, un ejecutor de pruebas, un túnel, un shell o un proceso auxiliar que sigue ejecutándose. Si ese proceso residual aún puede llamar a las API de producción o abrir sesiones SSH, la tarea no terminó realmente donde indica la interfaz.

La solución no consiste en obsesionarse con terminar procesos. La limpieza de procesos es necesaria, pero no es un sistema de autorización. Define por separado la duración de la autoridad y la de los procesos Unix, haz que el límite sea observable y revoca el acceso antes de empezar a perseguir procesos descendientes. He visto equipos tratar un PID principal como prueba de contención. No demuestra gran cosa en cuanto un agente puede invocar un shell.

## Un árbol de procesos no define un límite de autorización

La relación entre proceso principal y secundario indica quién creó un proceso. No indica si ese proceso debe conservar autoridad después de que termine el principal. Son preguntas distintas, y mezclarlas produce el fallo habitual: alguien cancela una tarea del agente, ve desaparecer el proceso principal y descubre después que un proceso secundario sigue haciendo solicitudes cinco minutos más tarde.

Unix ofrece varias formas de escapar de la estructura prevista. Un proceso secundario puede bifurcarse otra vez. Puede crear una sesión nueva con `setsid`. Puede pedir a un gestor de servicios que lo supervise. Puede quedar en una canalización del shell donde solo uno de sus miembros recibe la señal. También puede sobrevivir simplemente porque el proceso principal no envió ninguna señal.

Un proceso contiene más que su línea de comandos. Puede heredar variables de entorno, el directorio de trabajo actual, archivos abiertos, tuberías, sockets y descriptores de archivo. Si un proceso principal guarda un token de portador en una variable de entorno, cualquier proceso secundario que herede ese entorno tiene el token. Terminar después ese proceso no devuelve el secreto a la bóveda ni deshace las solicitudes ya realizadas.

Por eso, un límite de autorización debe responder a una pregunta concreta: qué ejecución de proceso activa puede solicitar acciones privilegiadas, hasta cuándo y cómo se le deniega el acceso de inmediato. La respuesta no debería ser «cualquier proceso descendiente de la terminal que inició la tarea». Eso es un accidente de la estructura de procesos, no una decisión de seguridad.

Para un agente que necesita acceso HTTP o SSH, mantén las credenciales fuera del proceso del agente. Deja que un componente que posee las credenciales realice la acción después de reconocer a un solicitante aprobado. Así, la limpieza deja de ser un intento desesperado de borrar autoridad filtrada y se convierte en una rutina operativa normal.

## Define la duración antes de iniciar la tarea

Una ejecución de agente necesita una condición de inicio, una condición de finalización y una condición de revocación explícitas. Escríbelas antes de decidir si el control corresponde a un clic en una interfaz, un grupo de procesos o un envoltorio del shell.

Para el trabajo interactivo, el valor predeterminado más sensato suele ser una autorización para un único proceso raíz del agente. La autorización comienza cuando apruebas esa ejecución concreta y termina cuando el proceso raíz sale o cuando la revocas. Una nueva invocación del agente recibe una decisión nueva, aunque ejecute el mismo binario desde el mismo directorio.

Para el trabajo sin supervisión, no prolongues discretamente la aprobación durante todo el día solo porque los avisos repetidos molestan. Asigna al trabajo un responsable identificado, una duración limitada, un conjunto restringido de destinos y una vía de cancelación que alguien pueda usar mientras el trabajo siga activo. Si el trabajo debe continuar después de que termine su tarea principal, trátalo como un trabajo independiente y haz que solicite una autoridad separada.

Tres preguntas revelan enseguida los diseños imprecisos:

- ¿Qué ejecutable y qué instancia de proceso aprobó el usuario?
- ¿Qué evento termina su autoridad si el proceso nunca envía una señal de finalización limpia?
- ¿Puede un descendiente obtener una nueva acción privilegiada después de ese evento?

Si la última respuesta es sí porque el descendiente heredó un token, has delegado autoridad sin registrar la delegación. Si es sí porque la puerta de enlace sigue considerando de confianza a todos los descendientes, has convertido la ascendencia de procesos en tu lenguaje de políticas. Ambas decisiones son difíciles de explicar durante un incidente.

No confundas la comodidad con un límite. Una pestaña de terminal, un directorio de proyecto, una cuenta de agente y una identidad de firma de código aportan contexto útil. Ninguno identifica por sí solo una ejecución. Una identidad de firma de código indica quién firmó un ejecutable. No puede indicar si el ejecutable inició el auxiliar previsto, una copia antigua del auxiliar o un proceso secundario que se desvinculó tras la cancelación.

## Inspecciona el árbol activo antes de terminar nada

En macOS, empieza por los datos del proceso que expone el kernel, en lugar de hacer suposiciones a partir de una etiqueta de tarea. El manual de `ps` documenta `pid`, `ppid`, `pgid` y `sid` como campos distintos. Muestran la ascendencia, la pertenencia a grupos de procesos y la pertenencia a sesiones. Necesitas los cuatro cuando se ha permitido al agente invocar shells y herramientas.

Ejecuta este comando y guarda el resultado cuando investigues una ejecución:

```sh
ps -axo pid,ppid,pgid,sid,stat,etime,command
```

La estructura útil se parece a esta:

```text
  PID  PPID  PGID   SID STAT ELAPSED COMMAND
48102 47790 48102 48102 S    00:18:04 agent-cli run build
48131 48102 48102 48102 S    00:17:59 /bin/sh -c make test
48144 48131 48102 48102 S    00:17:56 test-runner --watch
48209     1 48209 48209 S    00:16:02 helper --upload-results
```

Los tres primeros procesos comparten grupo y sesión. El último tiene PPID 1 y un grupo y una sesión distintos. Puede haberse desvinculado o puede que ahora lo controle un lanzador. En cualquier caso, enviar una señal solo al PID 48102 no lo detendrá.

Para consultar los procesos secundarios directos de un PID raíz conocido, usa:

```sh
pgrep -P 48102 -alf
```

Ese comando encuentra una sola generación. Repítelo para cada proceso secundario si necesitas recorrer el árbol manualmente. Para un registro de incidente, captura la salida de `ps` antes y después de la revocación y registra el PID raíz exacto, la hora de inicio, el comando, el grupo de procesos y la sesión. Un simple nombre de proceso es una prueba deficiente porque los nombres se repiten y las líneas de comandos cambian.

Comprueba las conexiones de red abiertas cuando el riesgo implique llamadas externas. En macOS, `lsof` puede mostrar los archivos de red de un proceso:

```sh
lsof -nP -p 48209 -i
```

Un socket a la escucha, una conexión saliente establecida o un transporte SSH de larga duración cambian la urgencia. No demuestran un comportamiento malicioso. Demuestran que un proceso que creías desaparecido todavía tiene un canal que merece atención.

No construyas en producción un control de seguridad que dependa de analizar la salida legible para humanos de `ps`. Úsala para investigar y probar. Un lanzador real debe registrar los identificadores en el momento del inicio y conservar un mecanismo directo de revocación en la puerta de enlace de autorización.

## Los grupos de procesos ayudan, pero los procesos desvinculados los esquivan

Un grupo de procesos dedicado ofrece al lanzador una forma práctica de cancelar un árbol de tareas normal. Crea el grupo antes de iniciar el agente, conserva el proceso raíz como líder del grupo y envía las señales al grupo en lugar de enviarlas solo al proceso raíz. Esto cubre el caso habitual de shells, compiladores, ejecutores de pruebas y canalizaciones que permanecen en el mismo grupo.

En los sistemas compatibles con la sintaxis habitual de señales, un ID de grupo negativo apunta a un grupo de procesos:

```sh
kill -TERM -48102
sleep 3
kill -KILL -48102 2>/dev/null || true
```

La señal `TERM` da a las herramientas normales la oportunidad de cerrar archivos e informar de la cancelación. La señal `KILL` posterior gestiona los procesos que se niegan a salir o no consiguen hacerlo. No copies esto en una automatización hasta comprobar que 48102 es el grupo de procesos correcto. Un ID equivocado puede terminar tu propio shell o trabajo no relacionado.

Este método tiene límites. Un proceso secundario puede llamar a `setsid`, que crea una sesión nueva y normalmente un grupo de procesos nuevo. Una tarea puede enviar trabajo a un servicio local, un sistema de compilación remoto o una cola. Un shell puede iniciar un proceso en segundo plano fuera del grupo. Cuando eso ocurre, terminar el grupo sirve para limpiar, no para contener.

En Linux, un gestor de servicios puede colocar una tarea en un cgroup dedicado y terminar el cgroup como una unidad. Suele ser más sólido que limpiar un grupo de procesos porque el kernel sigue la pertenencia más allá de la ascendencia normal. No des a entender que una aplicación de macOS en la barra de menús tiene este control. macOS y Linux tienen modelos de supervisión de procesos distintos, y un diseño de agente portátil no debe fingir lo contrario.

La recomendación de «terminar simplemente el árbol de procesos» sigue siendo popular porque funciona en las demostraciones. Falla justo en las condiciones que hacen arriesgado el acceso de los agentes: trabajos largos, auxiliares en segundo plano, envoltorios y cancelaciones parciales. Usa grupos de procesos porque reducen los residuos. No los uses como único mecanismo de revocación.

## Mantén los secretos completamente fuera del árbol

El proceso secundario más seguro sigue siendo el que no puede leer una credencial. Pasar un token mediante una variable de entorno lo pone a disposición de todos los descendientes que hereden el entorno y también puede exponerlo a diagnósticos, informes de errores o registros descuidados. Pasarlo mediante un archivo temporal es solo un poco menos malo si el proceso secundario puede copiarlo antes de que lo elimines.

Evita estos patrones en los comandos iniciados por agentes:

```sh
export DEPLOY_TOKEN='token-value'
agent-cli run deploy
```

```sh
agent-cli run deploy --token "$(cat ~/.config/deploy-token)"
```

Ambos ponen la autoridad sin procesar en el entorno de ejecución del agente. El segundo también puede dejarla en los argumentos del proceso, el historial del shell o los registros. Rotar el token después de un problema puede ser necesario, pero la rotación es una medida de recuperación, no una vía normal de cancelación.

Usa en su lugar una puerta de enlace de acciones local. El agente debe solicitar una operación, como una solicitud HTTP a un endpoint aprobado o un comando SSH, sin recibir nunca el material de la credencial. La puerta de enlace inyecta la credencial correspondiente, ejecuta la acción y devuelve el resultado. Así tienes un lugar donde denegar la siguiente llamada aunque un proceso secundario perdido siga ejecutándose.

Sallyport aplica este enfoque a las API HTTP y SSH: su bóveda cifrada permanece en la aplicación, mientras el agente se conecta mediante el componente `sp mcp` y recibe resultados de acciones en lugar de secretos. Esto importa más que cualquier script ingenioso para terminar procesos, porque un proceso secundario no puede heredar un token que nunca tuvo.

No exageres este beneficio. Un proceso con una sesión de puerta de enlace aprobada todavía puede solicitar acciones hasta que la sesión termine o la revoques. Mantener los secretos fuera del árbol de procesos limita el robo de credenciales, pero no vuelve inofensivo a un agente aprobado.

## La aprobación debe vincularse a una ejecución, no a un nombre de familia

La autorización basada únicamente en el nombre de un ejecutable es débil. Cualquiera puede copiar un binario a otra ruta, envolverlo en un script de shell o ejecutar otra instancia más adelante. La autorización basada únicamente en un firmante mejora la atribución, pero sigue siendo demasiado amplia si aprueba silenciosamente todas las ejecuciones futuras firmadas por la misma parte.

Una tarjeta de aprobación útil debe identificar el proceso solicitante de una forma que una persona pueda comprobar: su autoridad de firma de código, la ruta del ejecutable, el PID raíz y la hora de inicio. La decisión debe aplicarse a esa única ejecución del proceso. Un proceso secundario no debe obtener un derecho indefinido a actuar solo porque tiene un antecesor aprobado en algún punto de su historial.

Hay dos modelos razonables para los descendientes. El modelo más estricto pide aprobación para cada solicitante distinto. El modelo práctico permite las llamadas que tienen lugar durante una ejecución raíz aprobada y rechaza todas las llamadas nuevas cuando esa ejecución termina. El segundo modelo funciona bien con agentes que legítimamente inician herramientas de corta duración, siempre que la puerta de enlace pueda saber que la ejecución raíz terminó y que la autorización no pueda vincularse de nuevo a un proceso posterior con el mismo nombre.

Sallyport usa por defecto la autorización por sesión: la primera llamada de un proceso de agente nuevo muestra una tarjeta de aprobación que comienza con la autoridad de firma de código del proceso, y la aprobación dura hasta que termina esa ejecución. La puerta de la bóveda deniega toda acción mientras está bloqueada, y una opción por llamada puede exigir aprobación para una credencial concreta cada vez que se use. Son controles deliberadamente pequeños. Un conjunto enorme de reglas de políticas solo ocultaría la pregunta de quién aprobó qué.

La aprobación por llamada encaja con credenciales donde cada acción merece una revisión deliberada, como una cuenta de despliegue de producción o una API administrativa destructiva. No encaja con todas las solicitudes de solo lectura. Cuando cada acción inofensiva exige atención, la gente aprende a aprobarlo todo. Coloca la fricción donde lo justifique la consecuencia y mantén breve y claro el límite de sesión en el resto de los casos.

## Revoca la autorización antes de perseguir el proceso

Cuando una tarea principal termina de forma inesperada, revoca primero su capacidad para realizar nuevas llamadas privilegiadas. Después termina el grupo raíz, inspecciona los procesos supervivientes y limpia lo que haya escapado. Invertir ese orden crea un intervalo de riesgo: el proceso secundario que todavía no has encontrado puede seguir comunicándose mientras inspeccionas las tablas de procesos.

Una secuencia de respuesta sólida sería esta:

1. Revoca la sesión o bloquea la bóveda en la puerta de enlace de acciones.
2. Conserva el PID raíz, la información de los procesos, los registros recientes de acciones y la hora de cancelación.
3. Envía `TERM` al grupo de procesos conocido e inspecciona los procesos restantes.
4. Recurre a `KILL` solo para los procesos que todavía pertenecen a la tarea y se niegan a salir.
5. Comprueba si los procesos locales desvinculados o los trabajos remotos requieren una cancelación independiente.

La primera acción debe funcionar aunque el PID raíz ya haya desaparecido. También debe funcionar si un agente intenta deliberadamente mantener activo un proceso auxiliar. Una puerta de enlace que exige que el solicitante esté presente para revocar la autorización tiene el enfoque invertido.

La revocación inmediata debe denegar las solicitudes futuras, no reescribir el historial. Conserva los registros que muestran la autorización y las llamadas anteriores. Si una puerta de enlace solo registra las acciones correctas, oculta pruebas útiles durante una investigación. Las llamadas denegadas después de la revocación indican que algo siguió intentando actuar.

El bloqueo de la bóveda es el freno de emergencia para todas las sesiones activas, lo que resulta apropiado cuando no puedes identificar rápidamente la ejecución afectada. La revocación de sesión es una respuesta más limitada cuando sí puedes identificarla. Mantén separadas ambas operaciones para que el operador no tenga que elegir entre no hacer nada e interrumpir el trabajo de todos los ingenieros.

## Un registro de auditoría debe combinarse con las pruebas de los procesos

Un registro de actividad sin contexto del proceso responde solo a la mitad de la pregunta. Puede decir que ocurrió una solicitud HTTP, pero no qué ejecución aprobada la inició. Un registro de sesión sin llamadas individuales tiene el problema contrario. Necesitas ambas perspectivas y una forma de verificar que nadie las editó discretamente después de un incidente.

Registra al menos el evento de autorización, la identidad del proceso solicitante, el inicio y el final de la sesión, cada acción privilegiada, la revocación y cualquier denegación posterior a la revocación. Incluye marcas de tiempo e identificadores de correlación estables. No registres secretos sin procesar. Tampoco supongas que es seguro almacenar una línea de comandos, porque las líneas de comandos suelen contener valores que nunca deberían haberse pasado de esa forma.

Sallyport genera sus registros Sessions y Activity a partir de un único registro de auditoría cifrado y encadenado mediante hashes. Su comando `sp audit verify` comprueba la cadena sin conexión sobre el texto cifrado y sin necesitar una clave de la bóveda. Esta verificación resulta útil cuando necesitas entregar un registro exportado a alguien que debe confirmar su integridad, pero no debe leer las credenciales almacenadas.

Una cadena de hashes no convierte un registro incompleto en uno completo. Si tu lanzador nunca registró el PID raíz, el registro de auditoría no puede reconstruirlo después. Si un trabajo remoto recibió una solicitud API y continuó en otra máquina, las pruebas de procesos locales no mostrarán el proceso remoto. Audita la solicitud que inició el trabajo remoto y exige después que el sistema remoto exponga sus propios registros de cancelación y eventos.

## Un fallo conocido requiere dos soluciones independientes

Imagina un agente de programación al que se le pide ejecutar pruebas de integración y publicar un informe. Inicia un shell, que inicia el ejecutor de pruebas, que a su vez inicia un auxiliar para subir los resultados. El usuario ve un fallo en las pruebas y cancela el agente. El agente principal termina. El shell desaparece. El auxiliar ya se ha desvinculado, conserva una conexión saliente y envía un informe después de la cancelación.

Si la credencial de carga estaba en una variable de entorno, el auxiliar puede conservarla incluso después de que revoques la aprobación local. Ahora necesitas rotar la credencial, revisar los registros y posiblemente responder a un incidente. La limpieza de procesos llegó demasiado tarde porque la autoridad ya había pasado al proceso secundario.

Si el auxiliar solicitó la carga mediante una puerta de enlace, revocar la sesión principal detiene una nueva solicitud de carga. Si la carga ya había comenzado, la auditoría de la puerta de enlace lo muestra. Aun así terminas el auxiliar, pero ya no dependes de la terminación como único medio de control.

El caso incómodo es un auxiliar que debe sobrevivir al agente por diseño, como un servidor local de vista previa o un trabajo de publicación en cola. No lo llames proceso secundario para heredar autoridad indefinidamente. Asígnale un responsable explícito, un registro de autorización independiente, una caducidad definida y un control de detención visible. Cuando el trabajo sobrevive a la tarea que lo inició, se ha convertido en un objeto operativo propio.

## Prueba la cancelación con un proceso secundario persistente

Un control que nunca pruebas fallará de la forma menos conveniente. Crea un agente de prueba inofensivo que inicie un proceso secundario que duerma, abra una conexión local inofensiva e intente realizar una acción en la puerta de enlace después de que termine el proceso principal. Después, cancela el proceso principal en varios momentos: antes de que empiece el proceso secundario, mientras se ejecuta, después de que se desvincule y mientras tenga una acción en curso.

El resultado esperado debe ser concreto. El proceso secundario normal termina con el grupo de procesos. Un proceso secundario desvinculado puede seguir visible, lo que demuestra por qué aún debes inspeccionar. Después de la revocación, toda nueva solicitud privilegiada recibe una denegación. El registro de sesión muestra la aprobación y la revocación, mientras que el registro de actividad muestra las llamadas realizadas antes y después del límite.

No permitas que una prueba aprobada signifique únicamente que la interfaz cambió de estado. Verifica el estado del proceso en el sistema operativo, la decisión de la puerta de enlace y el registro de auditoría. Son tres observaciones distintas. Un botón de cancelación que solo oculta una tarjeta de tarea es puro teatro.

El primer cambio práctico es sencillo: registra la identidad del proceso raíz cada vez que autorices un agente y haz que la autorización caduque de forma independiente cuando termine esa ejecución. Después, ejecuta la prueba con el proceso secundario persistente antes de confiar una función de cancelación a credenciales capaces de modificar producción.
