# Árvores de processos MCP: encontre processos filhos inesperados

Servidores MCP locais facilitam confundir um limite de protocolo limpo com um limite de execução igualmente limpo. O agente envia uma chamada de ferramenta, o servidor retorna um resultado e o histórico parece contido. Enquanto isso, o servidor pode iniciar um shell, um executor de pacotes, um runtime de linguagem, um compilador, um cliente SSH ou um auxiliar baixado para um diretório do projeto. A árvore de processos mostra para onde o trabalho realmente foi.

Para fins de análise, não trato um servidor MCP como um único processo. Trato o servidor e todos os descendentes que ele inicia como uma única família de execução até que esses descendentes terminem. Essa regra identifica falhas que os históricos de ferramentas costumam deixar passar: um argumento que fez um shell chamar outra coisa, um auxiliar selecionado pelo `PATH`, um processo filho que reteve um token de API ou um programa de longa duração que abriu um socket depois que a solicitação original parecia concluída.

## Uma chamada de ferramenta pode se transformar em vários programas locais

Um servidor MCP costuma iniciar processos filhos por motivos normais. Uma ferramenta de repositório pode chamar `git`; uma ferramenta de código pode invocar um formatador; uma ferramenta de infraestrutura pode chamar `ssh`; um servidor que entende pacotes pode iniciar um runtime e depois um executável de pacote. A existência de um filho não prova uma invasão.

A distinção útil é entre um **auxiliar esperado** e um **caminho de execução sem explicação**. Um auxiliar esperado tem uma função documentada, um local conhecido para o executável, argumentos compatíveis com a solicitação e uma duração adequada à tarefa. Um caminho sem explicação quebra uma dessas premissas. Ele pode continuar sendo inofensivo, mas precisa de uma justificativa antes de receber uma credencial, acesso ao sistema de arquivos ou conectividade de rede.

Um PID pai, sozinho, não basta. A criação de processos Unix produz uma linhagem, não um contrato de segurança. Um descendente costuma herdar o ID de usuário do pai, o diretório de trabalho atual, as variáveis de ambiente, os limites de recursos e, às vezes, descritores de arquivo abertos. A herança exata depende de como o pai o inicia, mas o filho normalmente recebe contexto suficiente para agir como o pai nos lugares importantes.

Por isso, «o servidor MCP só chama um comando» não é uma garantia útil. `tool-server` pode iniciar `/bin/sh -c`, que inicia `node`, que inicia um script de pacote, que inicia `curl`. Um responsável que registra apenas `tool-server` documentou a parte menos interessante da cadeia.

O modelo `exec` do POSIX explica o ponto crítico. Um processo pode substituir sua imagem por outro programa sem mudar de PID. Assim, uma lista simples de processos feita antes e depois de uma solicitação pode não mostrar um intermediário de curta duração. Quando a tarefa é sensível o bastante, você precisa tanto de uma visão em árvore quanto de evidências dos eventos.

## Estabeleça uma linha de base antes de deixar um agente trabalhar

Uma linha de base registra os filhos que o servidor inicia durante uma solicitação aprovada e rotineira. Ela oferece um ponto de comparação para observações posteriores. Sem isso, todo interpretador parece suspeito e cada alerta vira uma discussão sobre o que poderia ser normal.

Inicie o servidor manualmente em uma sessão dedicada do Terminal. Registre o PID imediatamente e capture a tabela de processos com as linhas de comando completas:

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

As colunas são importantes. `PID` identifica o processo, `PPID` identifica o pai direto, `USER` mostra qual conta é proprietária, `ETIME` informa o tempo de execução e `STAT` pode revelar um processo parado ou zumbi. `COMMAND` só é tão completo quanto o sistema operacional consegue informar, mas ainda é o primeiro lugar para procurar um wrapper de shell, um caminho temporário ou uma flag inesperada.

No macOS, `ps` não vem com uma visualização de árvore disponível de forma universal. Você pode enumerar os filhos diretos com `pgrep`:

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

Substitua `48192` pelo PID do servidor. Uma saída típica tem este formato:

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

Depois, repita o comando para cada PID filho até que não apareçam novos descendentes. Isso parece trabalhoso, e para uma análise pontual realmente é. Também obriga você a enxergar a cadeia real, em vez de confiar em um diagrama da documentação. Se instalar `pstree` pelo processo normal de gerenciamento de pacotes, `pstree -p 48192` fornecerá um instantâneo mais legível. Não transforme a ferramenta de visualização em parte da sua afirmação de segurança. Ela apenas evita digitação.

Execute um pequeno conjunto de solicitações esperadas: uma consulta somente leitura ao repositório, a ação do formatador e uma ação SSH, se o servidor oferecer uma. Salve um instantâneo de cada uma. Anote os caminhos dos executáveis, os argumentos normais, os diretórios de trabalho usuais e se os auxiliares terminam. Um compilador que aparece durante uma solicitação de compilação pode ser esperado. O mesmo compilador durante uma solicitação para resumir um arquivo de texto é um desvio.

Não estabeleça a linha de base em um checkout de desenvolvimento cheio de wrappers mutáveis e scripts de pacotes para depois chamar o resultado de confiável. Essa configuração mostra o que a sua máquina executa hoje. Ela não mostra o que o servidor deveria ter permissão para executar. Registre separadamente os caminhos esperados dos executáveis.

## Wrappers de shell escondem o processo que você queria inspecionar

A invocação de shell é popular porque facilita montar comandos dinâmicos. Ela também transforma um limite de argumentos em texto, e o texto pode adquirir significado por meio de aspas, expansão, globbing, redirecionamentos, substituição de comandos e funções de shell. O autor do servidor pode pretender executar um formatador, enquanto o processo filho real inicia um shell que decide o significado do comando do formatador.

Essa é uma fonte frequente de análises enganosas. Alguém vê o nome de um binário permitido em uma lista no código-fonte e presume que esse é o binário executado. As evidências de execução mostram `/bin/sh -c ...`, e o shell resolve o restante depois. São afirmações diferentes.

Compare estes dois padrões em uma implementação de servidor:

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

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

O primeiro padrão mantém o executável e os argumentos separados. Ainda é preciso validar `repositoryPath`, mas o padrão evita a análise do shell. O segundo dá a `repositoryPath` a oportunidade de mudar a gramática do comando se o código errar no tratamento das aspas. Ele também adiciona um processo de shell à árvore, que pode iniciar outros processos antes que o comando final comece.

Não aceite «nós higienizamos a entrada» como substituto de um vetor de argumentos. Os filtros se desgastam à medida que as opções aumentam. Desenvolvedores adicionam uma funcionalidade, permitem espaços, acrescentam uma flag condicional e o filtro antigo deixa de descrever a gramática do comando. Um executável explícito com um array de argumentos torna o limite visível no código e na saída da auditoria.

Se um servidor realmente precisa de sintaxe de shell, restrinja-a. Use um script fixo armazenado fora do espaço de trabalho gravável pelo agente, passe os dados por parâmetros posicionais e registre o caminho e o digest do script nas notas de implantação. Nunca monte uma linha de shell com dados da ferramenta e considere o processo filho esperado apenas porque ele veio da sua árvore de código.

O caso mais complicado é o das ferramentas de pacotes. Comandos como executores de pacotes costumam executar scripts de ciclo de vida fornecidos pelo projeto. Portanto, um servidor que executa um comando de pacote dentro de um checkout editado pelo agente pode iniciar comandos que o agente colocou na configuração do projeto. Isso não é um defeito do gerenciador de pacotes. É uma decisão de execução sua, tomada ao tratar um repositório mutável como fonte de instruções confiáveis.

## PATH e diretórios de trabalho mudam o significado de um comando

`git` não é uma identidade de executável. É uma solicitação de busca. O processo o resolve procurando no `PATH`, e um repositório controlado pelo agente pode influenciar essa busca se o servidor incluir diretórios locais do projeto ou a configuração herdada do shell. Um arquivo chamado `git` nesse diretório pode ser executado antes de `/usr/bin/git`.

Inspecione o ambiente recebido pelo processo, não apenas o comando mostrado no histórico da ferramenta. Para um processo sob seu controle, o macOS permite consultar informações do ambiente por meio de `ps` em muitas versões, embora a disponibilidade varie. Uma abordagem prática é fazer o servidor registrar na inicialização um ambiente propositalmente curto e redigido: `PATH`, `HOME`, `TMPDIR`, diretório atual e os caminhos dos executáveis fixos. Não registre tokens de acesso, cookies de sessão ou despejos completos do ambiente em um log de projeto compartilhado.

Resolva comandos sensíveis com caminhos absolutos. Isso não é formalidade quando um agente pode alterar a árvore de trabalho. Por exemplo:

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

Caminhos absolutos eliminam um problema de busca. Eles não tornam o destino seguro. O `git` pode chamar hooks ou programas externos de diff em algumas configurações. O SSH pode carregar configurações e chamar auxiliares. O diretório de trabalho também controla leituras de arquivos relativos, configurações do projeto e saídas temporárias. Registre-o ao inspecionar um filho.

Use `lsof` para examinar o diretório atual e os arquivos abertos de um processo:

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

Procure `cwd` na coluna de descritores de arquivo, o texto executável em `txt` e arquivos no projeto, em diretórios temporários ou em locais de credenciais. `lsof` é um instantâneo. Um processo filho rápido pode aparecer e desaparecer antes da execução do comando, mas a saída ainda é útil para detectar um servidor que mantém discretamente um auxiliar ativo.

Um filho iniciado a partir de `/private/var/folders/...` merece mais atenção do que um iniciado a partir de um diretório de aplicativo gerenciado, principalmente se o nome se parecer com o de um utilitário normal. Locais temporários são legítimos para produtos de compilação. Também são locais convenientes para esconder um processo entre arquivos descartáveis. Pergunte qual componente o criou e por que esse componente precisava de um executável ali.

## A atividade de rede precisa corresponder à ação solicitada

Um processo filho local pode ultrapassar o limite da solicitação MCP mesmo sem gravar um arquivo suspeito. Um formatador normalmente não deveria abrir uma conexão de saída. Um auxiliar SSH deveria se conectar ao host solicitado e depois terminar. Uma instalação de pacote pode contatar registros, mas essa ação precisa de análise explícita porque pode baixar e executar código novo.

Inspecione os sockets do servidor e de cada filho que permanecer ativo por tempo suficiente:

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

A saída normalmente inclui protocolo, endereço local, endereço remoto e estado da conexão. As flags `-nP` impedem buscas de nomes e serviços, mantendo a saída literal e evitando tráfego adicional de resolução durante a investigação. Uma entrada `LISTEN` significa que o processo aceita conexões locais ou de rede. Uma entrada `ESTABLISHED` significa que ele tem um par ativo. Compare cada entrada com a ação que a provocou.

Não reaja exageradamente a todo processo de biblioteca de rede. Algumas ferramentas de desenvolvimento verificam serviços de atualização, o status de certificados ou metadados de dependências. Ainda assim, trato esse comportamento como motivo para restringir o servidor. Uma chamada de ferramenta que supostamente lê código local não deveria adquirir um canal de saída não declarado apenas porque um auxiliar o considera conveniente.

Separe a injeção de credenciais da observação de processos. A inspeção pode mostrar que `curl` foi executado. Ela não pode garantir que o token passado em uma variável de ambiente nunca foi copiado, registrado ou herdado por um neto. Uma recomendação comum é «simplesmente coloque a chave de API no ambiente do processo filho para um comando». Ela é popular porque é fácil e funciona em uma demonstração. Está errada para ações executadas por agentes, porque um filho pode imprimir o ambiente, passá-lo adiante ou continuar ativo depois que o comando visível retorna.

O Sallyport usa um limite diferente para seus canais HTTP e SSH: o agente não recebe a credencial de API ou SSH armazenada, e o aplicativo executa a ação. Isso não elimina a necessidade de inspecionar os processos filhos locais do MCP, mas evita transformar cada auxiliar em um possível portador do segredo.

## Uma falha muitas vezes começa com um wrapper conveniente e razoável

Considere um servidor MCP local de repositório que oferece uma ferramenta `run_test`. O autor quer um comando flexível, então o handler entra no repositório e executa um script de testes definido pelo projeto por meio de um comando de pacote. O agente tem permissão para editar o checkout porque editar código faz parte da tarefa.

O agente altera o script do projeto como parte de uma correção proposta. O servidor executa a ferramenta de teste. O comando de pacote inicia um shell, que inicia o runtime, que executa o script modificado. Esse script inicia um auxiliar em segundo plano com a saída redirecionada para um arquivo temporário. A ferramenta retorna «testes aprovados» porque o comando em primeiro plano termina com sucesso.

Nada nessa sequência exige uma exploração exótica. O problema é que o servidor tratou metadados do projeto graváveis pelo agente como configuração executável aprovada. Um responsável pode ver apenas a solicitação MCP original e o resultado de sucesso. A árvore de processos mostra a história relevante:

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

O auxiliar em segundo plano é o ponto de investigação. Verifique a linha de comando completa, o caminho do executável, a cadeia de pais, o diretório de trabalho, os arquivos abertos, os sockets e o horário de início. Confira se ele continua ativo depois que a sessão do servidor termina. Depois, inspecione a alteração do projeto que forneceu o script. Não trate isso apenas como uma falha do agente. O servidor ofereceu execução sobre instruções mutáveis e chamou o resultado de teste.

A correção depende do comportamento desejado do produto. Um servidor cauteloso pode executar um binário de teste fixo com argumentos fixos. Se scripts definidos pelo projeto forem necessários, trate o arquivo de script e os metadados do pacote como entradas executáveis: mostre-os para aprovação, execute-os em um ambiente restrito e proíba processos em segundo plano sempre que possível. No mínimo, o servidor deve informar todos os descendentes que iniciou, inclusive os que sobrevivem à chamada da ferramenta.

A distinção importante é entre um agente que pede ao servidor para executar um comando de teste conhecido e um agente que altera o significado desse comando antes que o servidor o execute. Ambos podem aparecer como `run_test` em um histórico MCP. O risco apresentado é muito diferente.

## Observe o nascimento dos processos, não apenas instantâneos

Instantâneos respondem a «o que está ativo agora?». Eles não respondem a «o que foi executado por 200 milissegundos e terminou?». Para chamadas de ferramentas suspeitas ou sensíveis, observe os nascimentos dos processos enquanto a solicitação é executada.

O macOS inclui o Endpoint Security para produtos de segurança com a autorização necessária, mas ferramentas locais comuns não podem presumir que tenham esse recurso. Não projete uma solução que dependa da coleta privilegiada de eventos sem realmente oferecer e operar essa capacidade. Para uma investigação de desenvolvimento, use um wrapper controlado ao redor do executável e colete detalhes de início e término, ou execute o servidor sob um monitor de processos disponível no seu ambiente.

Um wrapper simples pode tornar a execução visível quando você controla o caminho de comando do 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 "$@"
```

Esse artefato registra a identidade do wrapper e depois usa `exec` para substituí-lo por `git`. Assim, o wrapper não permanece como um pai irrelevante. Isso não captura todos os descendentes que o `git` possa criar e o wrapper não deve receber segredos nos argumentos. Use-o para testar uma hipótese controlada, não como um sistema de auditoria completo.

Para um processo existente, o `dtruss` do macOS pode revelar chamadas de sistema, mas costuma exigir privilégios elevados e produz uma grande quantidade de saída. É uma ferramenta de investigação, não de monitoramento rotineiro. Comece pela árvore de processos, `lsof` e logs do aplicativo. Recorra ao rastreamento de chamadas de sistema quando precisar responder a uma pergunta específica, como saber se um processo filho executou outro caminho ou se conectou a um socket.

Mantenha os horários em UTC e registre o PID do pai em cada evento. Uma linha de comando sem pai e horário é uma evidência fraca. Os valores de PID são reutilizados depois que os processos terminam, então um instantâneo tardio pode associar por engano um processo novo e sem relação a um incidente antigo. O horário de início do processo em `ps` ou no seu próprio log de eventos ajuda a evitar esse erro.

## A aprovação precisa indicar o limite do executável

Uma confirmação que diz «Permitir run_test?» oferece pouca informação à pessoa. Quem aprova precisa saber qual processo assinado iniciou a ação, qual executável será executado, qual é o diretório de trabalho, se a ação pode alcançar a rede e se pode iniciar outros programas. Um cartão de aprovação vago ensina as pessoas a aprovar ações vagas.

Não tente resolver isso solicitando aprovação para cada chamada de `fork`. Isso cria fadiga de aprovação, e os usuários passam a aprovar mecanicamente ou desativam os avisos. Analise as transições importantes: um novo caminho de executável, a passagem de trabalho local para acesso à rede, um comando originado de um arquivo de projeto mutável ou um processo filho que continua ativo depois da solicitação.

As informações de assinatura de código fornecem um sinal útil de identidade no macOS, mas não as superestime. Inspecione um executável com:

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

O comando informa a autoridade de assinatura quando ela existe e retorna um erro para código sem assinatura. Isso mostra quem assinou o arquivo que o macOS inspecionou. Não mostra se os argumentos são seguros, se a configuração é confiável ou se o processo executará depois um script sem assinatura. Inspecione separadamente o caminho do filho e o contexto de execução.

A autorização de sessão do Sallyport dá prioridade à autoridade de assinatura de código do processo conectado, o que ajuda a decidir se um novo processo de agente pode agir. A configuração de chaves por chamada se aplica ao caso mais restrito em que cada uso de uma determinada credencial precisa de uma decisão humana. Nenhum dos dois controles deve ser usado para fingir que a aprovação de um pai explica automaticamente todos os descendentes que ele pode criar.

Baseie a linguagem de aprovação nas consequências. «Esta solicitação executará `/usr/bin/ssh` na sua conta e se conectará a `host.example`» pode ser analisado. «Esta ferramenta precisa de acesso» não pode. Se a ferramenta puder invocar scripts do projeto, diga isso diretamente. As pessoas conseguem tomar decisões informadas quando a solicitação tem uma forma concreta.

## Contenha o servidor antes de investigar o processo filho

Quando encontrar um filho inesperado, preserve evidências suficientes para explicá-lo e depois interrompa a família de execução. Não comece apagando arquivos temporários ou reiniciando o computador. Essas ações podem remover o único caminho, a linha de comando e as evidências de tempo disponíveis.

Use esta ordem quando o processo filho ainda estiver ativo:

1. Capture a saída de `ps` para o servidor, o pai e os descendentes conhecidos. Registre PIDs, PPIDs, tempo decorrido e comandos completos.
2. Execute `lsof -nP -p <pid>` e `lsof -nP -i -p <pid>` para o processo suspeito. Salve a saída fora do espaço de trabalho do agente.
3. Pare o descendente suspeito com `kill <pid>` se uma terminação normal for segura. Use `kill -KILL <pid>` apenas se ele não terminar e a continuidade da execução criar um risco inaceitável.
4. Pare o servidor MCP e revogue ou encerre a sessão do agente que iniciou a ação. Verifique se existem filhos que foram reatribuídos depois que o servidor terminou.
5. Inspecione o executável, a origem de seu lançamento e as alterações no repositório ou na configuração que produziram o comando.

`kill` não encerra automaticamente um grupo de processos ou todos os filhos. Um descendente colocado em segundo plano pode continuar depois que o pai direto termina. Por isso, você precisa da árvore antes da limpeza. Em um servidor controlado, coloque os auxiliares em um grupo de processos ou escopo de supervisão dedicado para que o servidor possa encerrar o grupo ao fim da sessão. Teste esse comportamento com um auxiliar colocado deliberadamente em segundo plano antes de depender dele.

Verifique também a persistência. No macOS, `launchctl print` pode inspecionar serviços de inicialização carregados quando você tem um rótulo ou domínio específico para examinar. Não descarregue cegamente tarefas sem relação porque os nomes parecem desconhecidos. Primeiro associe o suposto filho ao caminho do executável, à configuração de inicialização e aos horários. Um processo que retorna depois de uma reinicialização limpa ou de uma nova inicialização do servidor exige uma investigação diferente de um processo que existiu apenas em uma sessão de teste.

## Faça com que toda ação possa ser explicada depois

Um registro de auditoria útil conecta um processo de agente aprovado pelo usuário a uma solicitação, a uma ação executada e ao resultado observado. A telemetria de processos deve acrescentar o caminho do executável, a cadeia de pais, o diretório de trabalho, os horários de início e término e o destino da atividade de rede. Se o registro não tiver a árvore de processos, ele não poderá responder se um auxiliar fazia parte da solicitação ou se era um processo independente da máquina.

Não coloque segredos nos registros de auditoria para torná-los completos. Armazene nomes de credenciais ou identificadores opacos, os metadados de que os responsáveis precisam e o resultado da ação depois da redação. Um log que resolve a proveniência copiando tokens de acesso criou um segundo armazenamento de credenciais com controles mais fracos.

A evidência contra adulteração importa depois de um incidente porque logs locais podem ser editados pela mesma conta que executou o comando. O Sallyport projeta seus diários Sessions e Activity a partir de um log de auditoria criptografado e encadeado por hash, e `sp audit verify` verifica essa cadeia offline sem uma chave do cofre. Isso é uma evidência útil para uma ação do agente, mas os registros de processos ainda precisam de contexto suficiente para explicar o que o servidor local iniciou.

Inclua uma pergunta no procedimento operacional do seu servidor: «Qual executável esta solicitação fez ser executado e por que esse executável tinha permissão para existir neste diretório de trabalho?» Se ninguém puder respondê-la a partir do registro da solicitação e da captura de processos, reduza o escopo da ferramenta. Um servidor MCP local que não consegue dar conta de seus descendentes tem mais autoridade do que seus operadores conseguem analisar com segurança.
