# Деревья процессов MCP: как находить неожиданные дочерние процессы

Локальные MCP-серверы позволяют легко принять чистую границу протокола за чистую границу выполнения. Агент отправляет вызов инструмента, сервер возвращает результат, и журнал выглядит изолированным. Тем временем сервер может запустить оболочку, менеджер пакетов, среду выполнения языка, компилятор, SSH-клиент или вспомогательную программу, загруженную в каталог проекта. Дерево процессов показывает, где на самом деле выполнялась работа.

При проверке я не считаю MCP-сервер одним процессом. Я рассматриваю сервер и каждого запущенного им потомка как единую семью выполнения, пока потомки не завершатся. Это правило помогает заметить то, что регулярно пропускают журналы вызовов инструментов: аргумент, из-за которого оболочка запустила другую программу, вспомогательный файл, найденный через `PATH`, дочерний процесс, сохранивший API-токен, или долгоживущую программу, открывшую сокет после того, как исходный запрос уже завершился.

## Один вызов инструмента может превратиться во множество локальных программ

MCP-сервер часто запускает дочерние процессы по вполне обычным причинам. Инструмент для работы с репозиторием может вызвать `git`, инструмент для кода может запустить форматтер, инфраструктурный инструмент может вызвать `ssh`, а сервер, работающий с пакетами, может запустить среду выполнения, а затем исполняемый файл пакета. Сам факт появления дочернего процесса ещё не означает взлом.

Полезно различать **ожидаемый вспомогательный процесс** и **неучтённый путь выполнения**. У ожидаемого помощника есть документированная задача, известное расположение исполняемого файла, аргументы, соответствующие запросу, и срок жизни, соответствующий задаче. Неучтённый путь нарушает хотя бы одно из этих предположений. Он может быть безвредным, но прежде чем давать ему учётные данные, доступ к файловой системе или сети, нужно получить объяснение.

Одного родительского PID недостаточно. Создание процессов в Unix формирует родословную, а не контракт безопасности. Потомок обычно получает идентификатор пользователя родителя, текущий рабочий каталог, переменные окружения, ограничения ресурсов и иногда открытые файловые дескрипторы. Точное наследование зависит от способа запуска, но дочерний процесс обычно получает достаточно контекста, чтобы действовать как родитель там, где это важно.

Поэтому утверждение «MCP-сервер вызывает только одну команду» не даёт полезной гарантии. `tool-server` может запустить `/bin/sh -c`, который запустит `node`, тот запустит скрипт пакета, а скрипт запустит `curl`. Проверяющий, который записал только `tool-server`, задокументировал наименее интересную часть цепочки.

Модель `exec` в POSIX показывает, где возникает проблема. Процесс может заменить свой образ другой программой, не меняя PID. Поэтому простой список процессов, снятый до и после запроса, может не заметить короткоживущего посредника. Когда задача достаточно чувствительна, нужны и представление дерева, и свидетельства событий.

## Сначала создайте базовую картину

Базовая картина - это запись дочерних процессов, которые сервер запускает во время обычного разрешённого запроса. Она даёт точку сравнения для последующих наблюдений. Без неё каждый интерпретатор выглядит подозрительно, а каждое предупреждение превращается в спор о том, что считать нормой.

Запустите сервер вручную в отдельном окне Terminal. Сразу запишите его PID, затем снимите таблицу процессов с полными командными строками:

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

Столбцы важны. `PID` определяет процесс, `PPID` - его непосредственного родителя, `USER` показывает владельца учётной записи, `ETIME` - прошедшее время работы, а `STAT` может выявить остановленный процесс или процесс-зомби. Полнота `COMMAND` зависит от того, что операционная система может сообщить, но это всё равно первое место для поиска оболочки-обёртки, временного пути или неожиданного флага.

В macOS у `ps` нет единого доступного во всех системах режима отображения дерева. Непосредственных потомков можно перечислить с помощью `pgrep`:

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

Замените `48192` на PID сервера. Типичный вывод выглядит так:

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

Затем повторяйте команду для каждого дочернего PID, пока новые потомки не перестанут появляться. Для разовой проверки это действительно утомительно. Зато так вы видите реальную цепочку, а не доверяете схеме из документации. Если вы устанавливаете `pstree` обычным для вас способом, команда `pstree -p 48192` даст более понятный снимок. Не делайте инструмент отображения частью утверждения о безопасности. Он лишь экономит набор текста.

Выполните небольшой набор ожидаемых запросов: запрос только для чтения к репозиторию, действие форматтера и действие SSH, если сервер его поддерживает. Сохраните снимок для каждого случая. Отметьте пути к исполняемым файлам, обычные аргументы, рабочие каталоги и то, завершаются ли вспомогательные процессы. Компилятор во время сборки может быть ожидаемым. Тот же компилятор во время запроса на пересказ текстового файла - отклонение.

Не создавайте базовую картину на основе рабочего клона разработки с изменяемыми обёртками и скриптами пакетов, а затем не объявляйте результат доверенным. Такая настройка показывает, что сейчас запускается на вашем компьютере. Она не говорит, что серверу разрешено запускать. Запишите ожидаемые пути к исполняемым файлам отдельно.

## Оболочки скрывают процесс, который вы хотели проверить

Оболочки популярны, потому что упрощают сборку динамических команд. Но они превращают границу аргумента в текст, а текст получает смысл через кавычки, раскрытие переменных, подстановку шаблонов, перенаправления, подстановку команд и функции оболочки. Автор сервера может хотеть запустить форматтер, а фактически дочерний процесс запускает оболочку, которая сама решает, что означает команда форматтера.

Это частый источник ошибочных проверок. Кто-то видит в исходном коде имя разрешённого бинарного файла и предполагает, что запускается именно он. Данные выполнения показывают `/bin/sh -c ...`, а оболочка разрешает остальное позже. Это разные утверждения.

Сравните два шаблона в реализации сервера:

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

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

В первом случае исполняемый файл и аргументы разделены. Проверять `repositoryPath` всё равно нужно, но разбор оболочкой исключён. Во втором случае ошибка в кавычках позволяет `repositoryPath` изменить грамматику команды. Кроме того, в дереве появляется процесс оболочки, который может запустить дополнительные процессы ещё до запуска конечной команды.

Не считайте фразу «мы очищаем ввод» заменой вектора аргументов. По мере роста числа опций фильтры устаревают. Разработчики добавляют функцию, разрешают пробелы, добавляют условный флаг, и старый фильтр уже не описывает грамматику команды. Явный исполняемый файл и массив аргументов делают границу видимой и в коде, и в отчёте аудита.

Если серверу действительно нужен синтаксис оболочки, ограничьте его. Используйте фиксированный скрипт, хранящийся за пределами рабочего пространства, доступного агенту для записи, передавайте данные через позиционные параметры и записывайте путь к скрипту и его дайджест в заметках о развёртывании. Не собирайте строку оболочки из входных данных инструмента и не считайте получившийся дочерний процесс ожидаемым лишь потому, что он появился в вашем дереве исходного кода.

Особенно неудобна работа с пакетами. Команды вроде менеджеров пакетов часто выполняют скрипты жизненного цикла, заданные проектом. Поэтому сервер, запускающий команду пакета в изменяемом агентом клоне, может выполнить команды, которые агент добавил в конфигурацию проекта. Это не дефект менеджера пакетов. Это решение о выполнении, принятое вами, когда изменяемый репозиторий стал источником доверенных инструкций.

## PATH и рабочие каталоги меняют смысл команды

`git` - это не идентичность исполняемого файла. Это запрос на поиск. Процесс ищет его через `PATH`, и репозиторий под контролем агента может повлиять на поиск, если сервер включает локальные каталоги проекта или унаследованную конфигурацию оболочки. Файл с именем `git` в таком каталоге может запуститься раньше `/usr/bin/git`.

Проверяйте окружение, которое получает процесс, а не только команду в журнале инструмента. Для принадлежащего вам процесса macOS во многих версиях позволяет запросить сведения об окружении через `ps`, хотя доступность зависит от версии. Практичный вариант - попросить сервер при запуске записывать короткое, очищенное от секретов окружение: `PATH`, `HOME`, `TMPDIR`, текущий каталог и пути к фиксированным исполняемым файлам. Не записывайте токены доступа, cookies сеансов и полный дамп окружения в общий журнал проекта.

Разрешайте чувствительные команды в абсолютные пути. Это не формальность, если агент может менять рабочее дерево. Например:

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

Абсолютные пути устраняют одну проблему поиска, но не делают цель безопасной. В некоторых конфигурациях `git` может вызвать хуки или внешние программы сравнения. SSH может загрузить конфигурацию и вызвать помощников. Рабочий каталог также определяет относительное чтение файлов, конфигурацию проекта и временные результаты. Записывайте его при проверке дочернего процесса.

Изучить текущий каталог процесса и открытые файлы можно с помощью `lsof`:

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

Ищите `cwd` в столбце файловых дескрипторов, исполняемый текст под `txt`, а также файлы внутри проекта, временных каталогов или мест хранения учётных данных. `lsof` даёт снимок. Быстрый дочерний процесс может появиться и исчезнуть до проверки, но вывод хорошо выявляет сервер, который незаметно оставляет помощника работать.

К дочернему процессу, запущенному из `/private/var/folders/...`, нужно относиться внимательнее, чем к процессу из управляемого каталога приложения, особенно если его имя похоже на обычную утилиту. Временные каталоги подходят для продуктов сборки, но в них удобно прятать процессы среди одноразовых файлов. Выясните, какой компонент его создал и зачем ему понадобился исполняемый файл в этом месте.

## Сетевая активность должна соответствовать запросу

Локальный потомок может выйти за границы запроса MCP, даже если не записывает подозрительный файл. Форматтер обычно не должен открывать исходящее соединение. SSH-помощник должен подключиться к указанному узлу и завершиться. Установка пакета может обращаться к реестрам, но такое действие требует отдельной проверки, потому что оно может загружать и выполнять новый код.

Проверяйте сокеты сервера и каждого достаточно долго работающего потомка:

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

Вывод обычно содержит протокол, локальный и удалённый адреса, а также состояние соединения. Флаги `-nP` отключают поиск имён и сервисов, поэтому вывод остаётся буквальным, а во время расследования не создаётся дополнительный трафик к резолверам. Запись `LISTEN` означает, что процесс принимает локальные или сетевые соединения. `ESTABLISHED` означает активное соединение с узлом. Сопоставьте каждую запись с действием, которое её вызвало.

Не реагируйте чрезмерно на каждый процесс, использующий сетевую библиотеку. Некоторые инструменты разработчика проверяют службы обновлений, статус сертификатов или метаданные зависимостей. Но я всё равно считаю такое поведение поводом ужесточить сервер. Вызов инструмента, который якобы читает локальный код, не должен получать незаявленный исходящий канал лишь потому, что помощнику так удобнее.

Отделяйте передачу учётных данных от наблюдения за процессами. Проверка может показать, что запускался `curl`. Она не гарантирует, что токен из переменной окружения не был скопирован, записан в журнал или унаследован внуком. Часто советуют «просто передать API-ключ в окружение дочернего процесса на одну команду». Это популярный вариант, потому что он прост и работает в демонстрации. Для действий, запущенных агентом, он неверен: дочерний процесс может вывести окружение, передать его дальше или продолжить работу после видимого завершения команды.

Sallyport использует другую границу для HTTP- и SSH-каналов: агент не получает сохранённые API- или SSH-учётные данные, а приложение выполняет действие само. Это не отменяет необходимости проверять локальные дочерние процессы MCP, но не превращает каждого помощника в потенциального владельца секрета.

## Сбой часто начинается с удобной обёртки

Представим локальный MCP-сервер репозитория с инструментом `run_test`. Автор хочет гибкую команду, поэтому обработчик переходит в каталог репозитория и запускает определённый проектом тестовый скрипт через команду пакета. Агент может менять рабочий клон, потому что редактирование кода входит в задачу.

Агент меняет скрипт проекта в рамках предлагаемого исправления. Сервер запускает инструмент тестирования. Команда пакета запускает оболочку, оболочка запускает среду выполнения, а та выполняет изменённый скрипт. Скрипт запускает фоновый помощник с перенаправлением вывода во временный файл. Инструмент возвращает «тесты пройдены», потому что команда переднего плана завершилась успешно.

Для такой последовательности не нужен экзотический эксплойт. Проблема в том, что сервер счёл изменяемые агентом метаданные проекта разрешённой конфигурацией для выполнения. Проверяющий может увидеть только исходный запрос MCP и успешный результат. Дерево процессов показывает настоящую историю:

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

Фоновый помощник - точка для расследования. Проверьте полную командную строку, путь к исполняемому файлу, цепочку родителей, рабочий каталог, открытые файлы, сокеты и время запуска. Выясните, остаётся ли он после завершения сеанса сервера. Затем изучите изменение проекта, из-за которого появился этот скрипт. Не сводите проблему только к сбою агента. Сервер предоставил выполнение изменяемых инструкций и назвал результат тестом.

Исправление зависит от поведения, которое должен поддерживать продукт. Осторожный сервер может запускать фиксированный тестовый исполняемый файл с фиксированными аргументами. Если нужны скрипты проекта, относитесь к файлу скрипта и метаданным пакета как к входным данным для выполнения: показывайте их для подтверждения, запускайте в ограниченном окружении и по возможности запрещайте переход в фон. Как минимум сервер должен сообщать о каждом запущенном потомке, включая тот, который пережил вызов инструмента.

Важно различать запрос агента на выполнение известной тестовой команды и изменение агентом смысла этой команды до запуска сервером. В журнале MCP оба действия могут выглядеть как `run_test`, но уровень риска у них совершенно разный.

## Наблюдайте за рождением процессов, а не только за снимками

Снимок отвечает на вопрос «что сейчас работает?». Он не отвечает на вопрос «что выполнялось 200 миллисекунд и завершилось?». Для подозрительных или чувствительных вызовов наблюдайте за запуском процессов во время выполнения запроса.

В macOS есть Endpoint Security для продуктов безопасности с необходимым разрешением, но обычные локальные инструменты не могут на него рассчитывать. Не проектируйте решение вокруг привилегированного сбора событий, если вы действительно не поставляете и не обслуживаете такую возможность. Для расследования разработчиком используйте контролируемую обёртку вокруг исполняемого файла и собирайте сведения о запуске и завершении либо запускайте сервер под доступным в вашей среде монитором процессов.

Простая обёртка может сделать выполнение видимым, если вы контролируете путь команды сервера:

```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 "$@"
```

Этот артефакт записывает идентичность обёртки, а затем заменяет её на `git` с помощью `exec`. Обёртка не остаётся лишним родительским процессом. Она не записывает каждого потомка, которого может создать `git`, и не должна получать секреты в аргументах. Используйте её для проверки контролируемой гипотезы, а не как полноценную систему аудита.

Для существующего процесса `dtruss` в macOS может показать системные вызовы, но часто требует повышенных прав и создаёт много вывода. Это инструмент расследования, а не средство регулярного мониторинга. Начинайте с дерева процессов, `lsof` и журналов приложения. Переходите к трассировке системных вызовов, когда нужно ответить на конкретный вопрос, например запускал ли дочерний процесс другой путь или подключался ли к сокету.

Храните временные метки в UTC и записывайте родительский PID с каждым событием. Командная строка без родителя и времени - слабое доказательство. После завершения процессов значения PID переиспользуются, поэтому поздний снимок может случайно связать новый процесс со старым инцидентом. Время запуска из `ps` или собственного журнала событий помогает избежать ошибки.

## Подтверждение должно называть границу исполняемого файла

Подтверждение «Разрешить run_test?» даёт человеку слишком мало информации. Нужно знать, какой подписанный процесс инициировал действие, какой исполняемый файл запустится, каков рабочий каталог, возможен ли доступ к сети и может ли программа запускать другие процессы. Расплывчатая карточка подтверждения приучает людей подтверждать расплывчатые действия.

Не пытайтесь решить задачу, запрашивая подтверждение для каждого вызова `fork`. Это создаёт усталость от подтверждений, после чего пользователи начинают нажимать автоматически или отключают запросы. Проверяйте значимые переходы: новый путь к исполняемому файлу, переход от локальной работы к сетевому доступу, команду из изменяемого файла проекта или потомка, который продолжает работать после запроса.

Сведения о подписи кода дают полезный сигнал идентичности в macOS, но не переоценивайте его. Проверить исполняемый файл можно так:

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

Команда сообщает подписавшую сторону, если подпись есть, и выдаёт ошибку для неподписанного кода. Это говорит, кто подписал проверенный macOS файл. Но не показывает, безопасны ли аргументы, надёжна ли конфигурация и не запустит ли программа потом неподписанный скрипт. Путь к потомку и контекст выполнения проверяйте отдельно.

Авторизация сеанса Sallyport сначала показывает статус подписи кода подключившегося процесса. Это помогает решить, может ли новый процесс агента вообще выполнять действия. Настройка ключей для отдельных вызовов подходит для случая, когда каждое использование конкретного учётного ключа должно подтверждаться человеком. Ни один из этих механизмов не должен создавать видимость, будто подтверждение родителя автоматически объясняет каждого его потомка.

Формулируйте подтверждения через последствия. «Этот запрос запустит `/usr/bin/ssh` от имени вашей учётной записи и подключится к `host.example`» можно проверить. «Инструменту нужен доступ» - нельзя. Если инструмент может вызывать скрипты проекта, скажите об этом прямо. Человек может принять обоснованное решение, когда запрос имеет конкретную форму.

## Ограничьте сервер до расследования потомка

Обнаружив неожиданный дочерний процесс, сохраните достаточно доказательств, чтобы объяснить его, а затем остановите всю семью выполнения. Не начинайте с удаления временных файлов или перезагрузки. Эти действия могут уничтожить единственные сведения о пути, командной строке и времени.

Если процесс ещё работает, действуйте в таком порядке:

1. Снимите вывод `ps` для сервера, его родителя и известных потомков. Запишите PID, PPID, время работы и полные команды.
2. Выполните `lsof -nP -p <pid>` и `lsof -nP -i -p <pid>` для подозрительного процесса. Сохраните вывод за пределами рабочего пространства агента.
3. Выполните `kill <pid>`, если обычное завершение безопасно. Используйте `kill -KILL <pid>` только если процесс не завершается, а продолжение работы создаёт неприемлемый риск.
4. Остановите MCP-сервер и отзовите или завершите сеанс агента, инициировавшего действие. Проверьте потомков, которые могли получить нового родителя после завершения сервера.
5. Изучите исполняемый файл, источник его запуска и изменения в репозитории или конфигурации, из-за которых появилась команда.

`kill` автоматически не завершает группу процессов и всех потомков. Процесс, переведённый в фон, может продолжить работу после выхода непосредственного родителя. Поэтому дерево нужно снимать до очистки. На контролируемом сервере помещайте помощников в отдельную группу процессов или область управления, чтобы при завершении сеанса сервер мог остановить всю группу. Перед доверием такой схеме проверьте её специально запущенным фоновым помощником.

Проверьте и механизмы постоянного запуска. В macOS команда `launchctl print` позволяет изучать загруженные службы запуска, если у вас есть конкретная метка или область для проверки. Не выгружайте вслепую несвязанные задания только потому, что их имена кажутся незнакомыми. Сначала сопоставьте подозрительный процесс с путём к исполняемому файлу, конфигурацией запуска и временными метками. Процесс, возвращающийся после перезагрузки или нового запуска сервера, требует другого расследования, чем процесс, существовавший только во время тестового сеанса.

## Сделайте каждое действие объяснимым задним числом

Полезная запись аудита связывает одобренный пользователем процесс агента с запросом, выполненным действием и наблюдаемым результатом. Телеметрия процессов должна добавлять путь к исполняемому файлу, цепочку родителей, рабочий каталог, время запуска и завершения и назначения сетевой активности. Если в записи нет дерева процессов, она не отвечает, был ли помощник частью запроса или отдельным процессом машины.

Не помещайте секреты в записи аудита ради полноты. Храните имена учётных данных или непрозрачные идентификаторы, нужные проверяющим метаданные запроса и очищенный от секретов результат. Журнал, который решает задачу происхождения копированием токенов носителя, создаёт второе хранилище учётных данных с более слабыми ограничениями.

После инцидента важна защита от изменения: локальные журналы может редактировать та же учётная запись, которая запускала команду. Sallyport строит журналы Sessions и Activity на основе зашифрованного журнала аудита с цепочкой хешей, а `sp audit verify` проверяет эту цепочку офлайн без ключа хранилища. Это полезное доказательство для действия агента, но записи процессов всё равно должны содержать достаточно контекста, чтобы объяснить, что запустил локальный сервер.

Включите в рабочую процедуру сервера один вопрос: «Какой исполняемый файл запустил этот запрос и почему ему было разрешено существовать в этом рабочем каталоге?» Если ответ нельзя получить из записи запроса и снимка процессов, сократите область действия инструмента. Локальный MCP-сервер, который не может отчитаться о своих потомках, обладает большими полномочиями, чем его операторы способны безопасно проверить.
