# Árboles de procesos MCP: encuentra procesos secundarios inesperados

Los servidores MCP locales facilitan confundir un límite de protocolo limpio con un límite de ejecución limpio. El agente envía una llamada a una herramienta, el servidor devuelve un resultado y la transcripción parece contenida. Mientras tanto, el servidor puede iniciar un shell, un ejecutor de paquetes, un runtime, un compilador, un cliente SSH o un auxiliar descargado en el directorio de un proyecto. El árbol de procesos muestra dónde terminó realmente el trabajo.

Para revisar un servidor MCP, no lo trato como un único proceso. Trato el servidor y cada descendiente que inicia como una sola familia de ejecución hasta que esos descendientes terminan. Esta regla detecta fallos que las transcripciones de herramientas suelen pasar por alto: un argumento que hizo que un shell invocara otra cosa, un auxiliar elegido mediante `PATH`, un proceso secundario que conservó un token de API o un programa de larga duración que abrió un socket después de que la solicitud original pareciera terminada.

## Una llamada a una herramienta puede convertirse en muchos programas locales

Un servidor MCP suele iniciar procesos secundarios por motivos normales. Una herramienta para repositorios puede llamar a `git`; una herramienta de código puede invocar un formateador; una herramienta de infraestructura puede llamar a `ssh`; un servidor que entiende de paquetes puede iniciar un runtime y después un ejecutable del paquete. La existencia de un proceso secundario no demuestra que haya una intrusión.

La distinción útil está entre un **auxiliar esperado** y una **ruta de ejecución no contabilizada**. Un auxiliar esperado tiene una función documentada, una ubicación de ejecutable conocida, argumentos que encajan con la solicitud y una duración adecuada para la tarea. Una ruta no contabilizada rompe una de esas suposiciones. Puede seguir siendo inofensiva, pero necesita una explicación antes de recibir una credencial, acceso al sistema de archivos o conectividad de red.

Un PID padre por sí solo no basta. La creación de procesos en Unix establece una ascendencia, no un contrato de seguridad. Un descendiente suele heredar el ID de usuario del padre, el directorio de trabajo actual, las variables de entorno, los límites de recursos y, en ocasiones, descriptores de archivo abiertos. La herencia exacta depende de cómo lo inicie el padre, pero normalmente un proceso secundario recibe suficiente contexto para actuar como el padre en los lugares importantes.

Por eso, «el servidor MCP solo llama a un comando» no es una garantía útil. `tool-server` podría iniciar `/bin/sh -c`, que inicia `node`, que inicia un script de paquete, que inicia `curl`. Un revisor que registra solo `tool-server` ha documentado la parte menos interesante de la cadena.

El modelo `exec` de POSIX explica el punto delicado. Un proceso puede reemplazar su imagen por otro programa sin cambiar de PID. Por eso, una lista de procesos tomada antes y después de una solicitud puede no mostrar un intermediario de corta duración. Cuando la tarea sea lo bastante sensible, necesitas tanto una vista del árbol como pruebas de eventos.

## Establece una referencia antes de dejar trabajar al agente

Una referencia es un registro de los procesos secundarios que inicia el servidor durante una solicitud rutinaria y aprobada. Ofrece un punto de comparación para las observaciones posteriores. Sin ella, cada intérprete parece sospechoso y cada advertencia se convierte en una discusión sobre lo que podría ser normal.

Inicia el servidor manualmente en una sesión de Terminal dedicada. Registra su PID de inmediato y captura la tabla de procesos con las líneas de comando completas:

```sh
ps -axo pid,ppid,user,etime,stat,command > mcp-process-baseline.txt
```

Las columnas importan. `PID` identifica el proceso, `PPID` identifica a su padre directo, `USER` muestra qué cuenta lo posee, `ETIME` indica cuánto tiempo lleva ejecutándose y `STAT` puede revelar un proceso detenido o zombi. `COMMAND` solo es tan completa como permita informar el sistema operativo, pero sigue siendo el primer lugar donde buscar un envoltorio de shell, una ruta temporal o una opción inesperada.

En macOS, `ps` no incluye una vista de árbol disponible de forma universal. Puedes enumerar los procesos secundarios directos con `pgrep`:

```sh
pgrep -P 48192 -alf
```

Sustituye `48192` por el PID del servidor. La salida típica tiene esta forma:

```text
48207 /usr/bin/python3 /Users/me/tools/format_request.py
48211 /usr/bin/ssh -o BatchMode=yes build.example
```

Después repite el comando para cada PID secundario hasta que no aparezcan nuevos descendientes. Parece tedioso, y para una revisión puntual lo es. También te obliga a ver la cadena real en lugar de confiar en un diagrama de la documentación. Si instalas `pstree` mediante tu proceso habitual de gestión de paquetes, `pstree -p 48192` ofrece una instantánea más fácil de leer. No conviertas la herramienta de visualización en parte de la garantía de seguridad. Solo te ahorra escribir.

Ejecuta un pequeño conjunto de solicitudes esperadas: una consulta de solo lectura al repositorio, la acción del formateador y una acción SSH si el servidor admite una. Guarda una instantánea de cada una. Anota las rutas de los ejecutables, los argumentos normales, los directorios de trabajo habituales y si los auxiliares terminan. Que aparezca un compilador durante una solicitud de compilación puede ser normal. El mismo compilador durante una solicitud para resumir un archivo de texto sería una desviación.

No tomes como referencia un checkout de desarrollo lleno de envoltorios modificables y scripts de paquetes para luego declararlo confiable. Esa configuración te dice qué ejecuta casualmente tu máquina actual. No te dice qué debería poder ejecutar el servidor. Escribe por separado las rutas de ejecutables esperadas.

## Los envoltorios de shell ocultan el proceso que querías inspeccionar

Invocar un shell resulta popular porque facilita ensamblar comandos dinámicos. También convierte un límite entre argumentos en texto, y el texto puede adquirir significado mediante comillas, expansión, comodines, redirecciones, sustitución de comandos y funciones del shell. El autor del servidor puede querer ejecutar un formateador, mientras que el proceso secundario real inicia un shell que decide qué significa el comando del formateador.

Esta es una fuente frecuente de revisiones engañosas. Alguien ve en el código fuente el nombre de un binario incluido en una lista permitida y supone que ese es el binario ejecutado. Las pruebas de ejecución muestran `/bin/sh -c ...`, y el shell resuelve el resto después. Son afirmaciones distintas.

Compara estos dos patrones en una implementación de servidor:

```js
spawn("/usr/bin/git", ["status", "--short"], {
  cwd: repositoryPath,
  shell: false
});
```

```js
exec(`git -C ${repositoryPath} status --short`);
```

El primer patrón mantiene separados el ejecutable y los argumentos. Aun así, necesita validar `repositoryPath`, pero evita el análisis del shell. El segundo permite que `repositoryPath` cambie la gramática del comando si el código gestiona mal las comillas. También añade un proceso de shell al árbol, que puede iniciar más procesos antes de que comience el comando final.

No aceptes «sanitizamos la entrada» como sustituto de un vector de argumentos. Los sanitizadores se deterioran a medida que crecen las opciones. Los desarrolladores añaden una función, permiten espacios, incorporan una opción condicional y el filtro anterior deja de describir la gramática del comando. Un ejecutable explícito junto con un array de argumentos hace visible el límite en el código y en la salida de auditoría.

Si un servidor realmente necesita sintaxis de shell, redúcela. Usa un script fijo almacenado fuera del espacio de trabajo modificable por el agente, pasa los datos mediante parámetros posicionales y registra la ruta y el resumen del script en tus notas de despliegue. Nunca compongas una línea de shell a partir de datos de la herramienta y consideres esperado el proceso resultante solo porque se originó en tu árbol de código.

El caso incómodo es el de las herramientas de paquetes. Comandos como los ejecutores de paquetes suelen ejecutar scripts del ciclo de vida proporcionados por el proyecto. Por tanto, un servidor que ejecuta un comando de paquetes dentro de un checkout editado por el agente puede iniciar comandos que el agente colocó en la configuración del proyecto. No es un defecto del gestor de paquetes. Es una decisión de ejecución que tomaste al tratar un repositorio modificable como fuente de instrucciones confiables.

## `PATH` y los directorios de trabajo cambian el significado de un comando

`git` no es una identidad de ejecutable. Es una solicitud de búsqueda. El proceso lo resuelve buscando en `PATH`, y un repositorio controlado por el agente puede influir en esa búsqueda si el servidor incluye directorios locales del proyecto o la configuración heredada del shell. Un archivo llamado `git` en uno de esos directorios puede ejecutarse antes que `/usr/bin/git`.

Inspecciona el entorno que recibe un proceso, no solo el comando mostrado en la transcripción de la herramienta. Para un proceso que controlas, macOS permite consultar información del entorno mediante `ps` en muchas versiones, aunque la disponibilidad varía. Un enfoque práctico consiste en hacer que el servidor registre al iniciar un entorno deliberadamente breve y redactado: `PATH`, `HOME`, `TMPDIR`, el directorio actual y las rutas de los ejecutables fijos. No registres tokens de acceso, cookies de sesión ni volcados completos del entorno en un registro compartido del proyecto.

Resuelve los comandos sensibles mediante rutas absolutas. No es una formalidad cuando un agente puede modificar el árbol de trabajo. Por ejemplo:

```sh
/usr/bin/git -C /Users/me/work/repo status --short
/usr/bin/ssh -o BatchMode=yes -o IdentitiesOnly=yes host.example
```

Las rutas absolutas eliminan un problema de búsqueda. No hacen que el objetivo sea seguro. `git` puede invocar hooks o programas externos de diferencias con determinadas configuraciones. SSH puede cargar configuraciones e invocar auxiliares. El directorio de trabajo también controla las lecturas de archivos relativas, la configuración del proyecto y las salidas temporales. Regístralo cuando inspecciones un proceso secundario.

Usa `lsof` para examinar el directorio actual y los archivos abiertos de un proceso:

```sh
lsof -nP -p 48207 | sed -n '1,35p'
```

Busca `cwd` en la columna de descriptores de archivo, el ejecutable bajo `txt` y archivos dentro del proyecto, directorios temporales o ubicaciones de credenciales. `lsof` es una instantánea. Un proceso secundario rápido puede aparecer y desaparecer antes de que lo ejecutes, pero la salida sigue siendo buena para detectar un servidor que mantiene un auxiliar activo en silencio.

Un proceso iniciado desde `/private/var/folders/...` merece más atención que uno iniciado desde un directorio de aplicaciones administrado, especialmente si su nombre se parece al de una utilidad normal. Las ubicaciones temporales son lugares legítimos para los productos de compilación. También son lugares convenientes para ocultar un proceso entre archivos desechables. Pregunta qué componente lo creó y por qué necesitaba un ejecutable allí.

## La actividad de red debe encajar con la acción solicitada

Un proceso secundario local puede atravesar el límite de la solicitud MCP aunque nunca escriba un archivo sospechoso. Un formateador normalmente no debería abrir una conexión saliente. Un auxiliar SSH debería conectarse al host solicitado y después terminar. Una instalación de paquetes puede contactar con registros, pero esa acción requiere una revisión explícita porque puede descargar y ejecutar código nuevo.

Inspecciona los sockets del servidor y de cada proceso secundario que dure lo suficiente:

```sh
lsof -nP -i -p 48211
```

La salida suele incluir el protocolo, la dirección local, la dirección remota y el estado de la conexión. Las opciones `-nP` evitan las búsquedas de nombres y servicios, mantienen la salida literal y evitan tráfico adicional del resolvedor durante la investigación. Una entrada `LISTEN` significa que el proceso acepta conexiones locales o de red. Una entrada `ESTABLISHED` significa que tiene un interlocutor activo. Compara cada entrada con la acción que la provocó.

No reacciones de forma exagerada ante cada proceso que use una biblioteca de red. Algunas herramientas de desarrollo comprueban servicios de actualización, el estado de certificados o los metadatos de dependencias. Aun así, considero ese comportamiento una razón para endurecer el servidor. Una llamada a una herramienta que supuestamente lee código local no debería adquirir un canal saliente no declarado solo porque un auxiliar lo considera conveniente.

Separa la inyección de credenciales de la observación de procesos. La inspección puede decirte que se ejecutó `curl`. No puede garantizar que el token pasado en una variable de entorno nunca se copiara, registrara o heredara por un nieto. Una recomendación habitual es «pon la clave de API en el entorno del proceso secundario para un solo comando». Es popular porque resulta fácil y funciona en una demostración. Es incorrecta para acciones ejecutadas por agentes, porque un proceso secundario puede imprimir su entorno, pasarlo a otro proceso o seguir activo después de que termine el comando visible.

Sallyport establece un límite distinto para sus canales HTTP y SSH: el agente no recibe la credencial de API o SSH almacenada y la aplicación realiza la acción. Eso no elimina la necesidad de inspeccionar los procesos secundarios locales de MCP, pero evita convertir cada auxiliar en un posible poseedor del secreto.

## Un fallo suele empezar con un envoltorio práctico y razonable

Imagina un servidor MCP local para repositorios que ofrece una herramienta `run_test`. El autor quiere un comando flexible, así que el controlador cambia al repositorio y ejecuta un script de pruebas definido por el proyecto mediante un comando de paquetes. El agente tiene permiso para editar el checkout porque editar código forma parte de su trabajo.

El agente cambia el script del proyecto como parte de una corrección propuesta. El servidor ejecuta la herramienta de pruebas. El comando de paquetes inicia un shell, que inicia el runtime, que ejecuta el script modificado. Ese script inicia un auxiliar en segundo plano y redirige su salida a un archivo temporal. La herramienta devuelve «tests passed» porque el comando en primer plano termina correctamente.

Nada de esa secuencia requiere un exploit exótico. El problema es que el servidor trató los metadatos del proyecto modificables por el agente como configuración ejecutable aprobada. Un revisor puede ver solo la solicitud MCP original y el resultado satisfactorio. El árbol de procesos muestra la historia importante:

```text
mcp-repo-server(48192)
  package-runner(48230)
    sh(48233)
      runtime(48234)
        test-script(48240)
          helper(48247)
```

El auxiliar en segundo plano es el punto de investigación. Comprueba su línea de comandos completa, la ruta del ejecutable, la cadena de padres, el directorio de trabajo, los archivos abiertos, los sockets y la hora de inicio. Comprueba si sobrevive después de cerrar la sesión del servidor. Después inspecciona el cambio del proyecto que proporcionó el script. No presentes esto únicamente como un fallo del agente. El servidor ofreció ejecutar instrucciones modificables y llamó al resultado una prueba.

La reparación depende del comportamiento que deba tener el producto. Un servidor prudente puede ejecutar un ejecutable de pruebas fijo con argumentos fijos. Si son necesarios los scripts definidos por el proyecto, trata el archivo del script y los metadatos del paquete como entradas ejecutables: muéstralos para su aprobación, ejecútalos en un entorno restringido y prohíbe los procesos en segundo plano cuando sea posible. Como mínimo, el servidor debe informar de cada descendiente que inició, incluido aquel que sobreviva a la llamada de la herramienta.

La distinción importante está entre un agente que pide al servidor ejecutar un comando de prueba conocido y un agente que cambia el significado de ese comando antes de que el servidor lo ejecute. Ambos pueden aparecer como `run_test` en una transcripción MCP. Su riesgo es muy diferente.

## Observa los nacimientos de procesos, no solo las instantáneas

Las instantáneas responden a «¿qué está activo ahora?». No responden a «¿qué se ejecutó durante 200 milisegundos y terminó?». Para las llamadas sospechosas o sensibles, observa los nacimientos de procesos mientras se ejecuta la solicitud.

macOS incluye Endpoint Security para productos de seguridad con la autorización necesaria, pero las herramientas locales normales no pueden darla por sentada. No diseñes una solución que dependa de la recopilación privilegiada de eventos a menos que realmente dispongas de esa capacidad y la administres. Para una investigación de desarrollo, usa un envoltorio controlado alrededor del ejecutable y recopila los datos de inicio y salida, o ejecuta el servidor bajo un monitor de procesos disponible en tu entorno.

Un envoltorio sencillo puede hacer visible la ejecución cuando controlas la ruta de comandos del servidor:

```sh
#!/bin/sh
printf '%s pid=%s ppid=%s cwd=%s argv=%s\n' \
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$PPID" "$(pwd)" "$*" \
  >> "$HOME/.local/state/mcp-exec.log"
exec /usr/bin/git "$@"
```

Este artefacto registra la identidad del envoltorio y después usa `exec` para reemplazarlo por `git`. Así evita que el envoltorio permanezca como un padre irrelevante. No captura todos los descendientes que `git` pueda crear y no debe recibir secretos en los argumentos. Úsalo para comprobar una hipótesis controlada, no como un sistema de auditoría completo.

Para un proceso existente, `dtruss` de macOS puede revelar llamadas al sistema, pero suele requerir privilegios elevados y produce una gran cantidad de salida. Es una herramienta de investigación, no de supervisión rutinaria. Empieza con el árbol de procesos, `lsof` y los registros de la aplicación. Recurre al rastreo de llamadas al sistema cuando necesites responder una pregunta concreta, como si un proceso secundario ejecutó otra ruta o se conectó a un socket.

Mantén las marcas de tiempo en UTC y registra el PID del padre con cada evento. Una línea de comandos sin padre ni hora es una prueba débil. Los valores de PID se reutilizan después de que terminan los procesos, así que una instantánea tardía puede asociar por accidente un proceso nuevo y ajeno con un incidente antiguo. La hora de inicio del proceso en `ps` o en tu propio registro de eventos ayuda a evitar ese error.

## La aprobación debe nombrar el límite del ejecutable

Una confirmación que diga «¿Permitir run_test?» ofrece muy poca información a una persona. Quien aprueba necesita saber qué proceso firmado inició la acción, qué ejecutable se ejecutará, cuál es el directorio de trabajo, si la acción puede acceder a la red y si puede iniciar más programas. Una tarjeta de aprobación vaga enseña a aprobar acciones vagas.

No intentes resolverlo solicitando aprobación para cada llamada a `fork`. Eso provoca fatiga de aprobación y hace que los usuarios aprueben mecánicamente o desactiven las solicitudes. Revisa las transiciones importantes: una nueva ruta de ejecutable, el paso del trabajo local al acceso de red, un comando obtenido de un archivo de proyecto modificable o un proceso secundario que persiste después de la solicitud.

La información de firma de código proporciona una señal de identidad útil en macOS, pero no la exageres. Inspecciona un ejecutable con:

```sh
codesign -dv --verbose=4 /path/to/executable 2>&1 | sed -n '1,20p'
```

El comando informa de la autoridad de firma cuando existe y devuelve un error para el código sin firma. Eso te dice quién firmó el archivo que inspeccionó macOS. No te dice si los argumentos del comando son seguros, si su configuración es confiable ni si después ejecutará un script sin firma. Inspecciona por separado la ruta del proceso secundario y su contexto de ejecución.

La autorización de sesión de Sallyport comienza con la autoridad de firma de código del proceso que se conecta, lo que resulta útil para decidir si un nuevo proceso de agente puede actuar. Su configuración de clave por llamada encaja en el caso más limitado en que cada uso de una credencial concreta necesita una decisión humana. Ninguno de estos controles debe utilizarse para fingir que la aprobación de un padre explica automáticamente cada descendiente que pueda crear.

Construye el lenguaje de aprobación alrededor de las consecuencias. «Esta solicitud ejecutará `/usr/bin/ssh` con tu cuenta y se conectará a `host.example`» se puede revisar. «Esta herramienta necesita acceso» no. Si la herramienta puede invocar scripts del proyecto, dilo directamente. Las personas pueden tomar decisiones informadas cuando la solicitud tiene una forma concreta.

## Contén el servidor antes de investigar el proceso secundario

Cuando encuentres un proceso secundario inesperado, conserva suficientes pruebas para explicarlo y después detén la familia de ejecución. No empieces borrando archivos temporales ni reiniciando. Esas acciones pueden eliminar la única evidencia que tengas de la ruta, la línea de comandos y los tiempos.

Usa este orden cuando el proceso siga ejecutándose:

1. Captura la salida de `ps` para el servidor, su padre y los descendientes conocidos. Registra los PID, PPID, tiempos transcurridos y comandos completos.
2. Ejecuta `lsof -nP -p <pid>` y `lsof -nP -i -p <pid>` para el proceso sospechoso. Guarda la salida fuera del espacio de trabajo del agente.
3. Detén el proceso secundario sospechoso con `kill <pid>` si una terminación normal es segura. Recurre a `kill -KILL <pid>` solo si no termina y la continuidad de la ejecución crea un riesgo inaceptable.
4. Detén el servidor MCP y revoca o termina la sesión del agente que inició la acción. Comprueba si quedaron procesos secundarios reasignados después de que saliera el servidor.
5. Inspecciona el ejecutable, el origen de su lanzamiento y los cambios en el repositorio o la configuración que produjeron el comando.

`kill` no termina automáticamente un grupo de procesos ni todos sus procesos secundarios. Un descendiente enviado al segundo plano puede continuar después de que salga su padre directo. Por eso necesitas el árbol antes de limpiar. En un servidor controlado, coloca los auxiliares en un grupo de procesos o ámbito de supervisión dedicado para que el servidor pueda terminar el grupo al cerrar la sesión. Prueba ese comportamiento con un auxiliar enviado deliberadamente al segundo plano antes de confiar en él.

Comprueba también la persistencia. En macOS, `launchctl print` puede inspeccionar los servicios de lanzamiento cargados cuando tienes una etiqueta o un dominio concretos que examinar. No descargues a ciegas trabajos no relacionados porque sus nombres resulten desconocidos. Primero relaciona el proceso sospechoso con su ruta de ejecutable, la configuración de lanzamiento y las marcas de tiempo. Un proceso que reaparece después de un reinicio limpio o del inicio de un servidor nuevo requiere una investigación distinta de la de uno que solo existió durante una sesión de pruebas.

## Haz que cada acción se pueda explicar después

Un registro de auditoría útil conecta un proceso de agente aprobado por el usuario con una solicitud, una acción ejecutada y su resultado observado. La telemetría de procesos debe añadir la ruta del ejecutable, la cadena de padres, el directorio de trabajo, las horas de inicio y salida y el destino de la actividad de red. Si el registro no contiene el árbol de procesos, no puede responder si un auxiliar formaba parte de la solicitud o era un proceso independiente de la máquina.

No pongas secretos en los registros de auditoría para hacerlos completos. Guarda nombres de credenciales o identificadores opacos, los metadatos de la solicitud que necesitan los revisores y el resultado de la acción después de redactarlo. Un registro que resuelve la procedencia copiando tokens portadores ha creado un segundo almacén de credenciales con controles más débiles.

La evidencia contra manipulaciones importa después de un incidente porque los registros locales pueden ser editados por la misma cuenta que ejecutó el comando. Sallyport proyecta sus registros de Sessions y Activity a partir de un registro de auditoría cifrado y encadenado mediante hashes, y `sp audit verify` comprueba esa cadena sin conexión y sin una clave de la bóveda. Es una prueba útil para una acción del agente, pero los registros de procesos aún necesitan suficiente contexto para explicar qué inició el servidor local.

Incluye esta pregunta en el procedimiento operativo de tu servidor: «¿Qué ejecutable hizo que se ejecutara esta solicitud y por qué se permitió que ese ejecutable existiera en este directorio de trabajo?». Si nadie puede responderla a partir del registro de la solicitud y de una captura de procesos, reduce el alcance de la herramienta. Un servidor MCP local que no puede dar cuenta de sus descendientes tiene más autoridad de la que sus operadores pueden revisar de forma segura.
