# Авторизация процессов агента: отзыв доступа у потомков

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

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

## Дерево процессов не задаёт границу авторизации

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

Unix даёт процессам несколько способов выйти за пределы ожидаемой структуры. Дочерний процесс может снова создать потомка. Он может создать новый сеанс с помощью `setsid`. Он может передать работу менеджеру служб. Он может остаться в конвейере оболочки, где сигнал получает только один участник. А может просто продолжить работу, потому что родитель вообще не отправил сигнал.

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

Поэтому граница авторизации должна отвечать на узкий вопрос: какой запущенный экземпляр процесса может запрашивать привилегированные действия, до какого момента и как немедленно ему отказать? Ответ не должен звучать как «всё, что произошло от терминала, запустившего задачу». Это случайность размещения процессов, а не решение в области безопасности.

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

## Определите срок действия до запуска задачи

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

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

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

Три вопроса быстро выявляют размытые границы:

- Какой исполняемый файл и какой экземпляр процесса одобрил пользователь?
- Какое событие прекращает его полномочия, если процесс не отправляет корректный сигнал завершения?
- Может ли потомок получить новое привилегированное действие после этого события?

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

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

## Изучите живое дерево до завершения процессов

В macOS начните с фактов о процессах, которые предоставляет ядро, а не с предположений по названию задачи. Руководство `ps` описывает `pid`, `ppid`, `pgid` и `sid` как отдельные поля. Они показывают происхождение процесса, принадлежность к группе и принадлежность к сеансу. Если агенту разрешено вызывать оболочки и инструменты, нужны все эти сведения.

Запустите эту команду и сохраните результат при расследовании запуска:

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

Полезная картина может выглядеть так:

```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
```

Первые три процесса входят в одну группу и один сеанс. У последнего PPID равен 1, а группа и сеанс другие. Возможно, он отсоединился, либо теперь им управляет другой механизм запуска. В любом случае сигнал, отправленный только PID 48102, его не остановит.

Для прямых потомков известного корневого PID используйте:

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

Эта команда находит только одно поколение. Если нужна быстрая ручная проверка, повторите её для каждого потомка. Для записи об инциденте сохраните вывод `ps` до и после отзыва, а затем зафиксируйте точный корневой PID, время запуска, команду, группу процессов и сеанс. Одно имя процесса - плохое доказательство: имена повторяются, а командные строки меняются.

Если риск связан с внешними вызовами, проверьте открытые сетевые соединения. В macOS `lsof` может показать сетевые файлы процесса:

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

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

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

## Группы процессов помогают, но не удерживают отсоединившихся потомков

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

В системах с обычным синтаксисом сигналов отрицательный идентификатор указывает на группу процессов:

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

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

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

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

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

## Полностью уберите секреты из дерева процессов

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

Избегайте таких схем для команд, запускаемых агентом:

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

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

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

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

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

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

## Подтверждайте запуск, а не имя семейства процессов

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

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

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

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

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

## Сначала отзовите разрешение, потом ищите процесс

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

Надёжная последовательность выглядит так:

1. Отзовите сессию или заблокируйте хранилище на шлюзе действий.
2. Сохраните корневой PID, сведения о процессах, последние записи о действиях и время отмены.
3. Отправьте `TERM` известной группе процессов и проверьте оставшиеся процессы.
4. Используйте `KILL` только для процессов, которые всё ещё относятся к задаче и отказываются завершаться.
5. Проверьте, требуют ли отсоединившиеся локальные процессы или удалённые задачи отдельной отмены.

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

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

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

## Журнал аудита должен объединять сведения о процессах

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

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

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

Цепочка хешей не делает неполный журнал полным. Если средство запуска не записало корневой PID, аудит не восстановит его задним числом. Если удалённая задача получила API-запрос и продолжила работу на другой машине, локальные сведения о процессах её не покажут. Аудитируйте запрос, который инициировал удалённую работу, а затем требуйте от удалённой системы собственные средства отмены и записи событий.

## У знакомого сбоя есть два отдельных решения

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

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

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

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

## Проверяйте отмену на упрямом потомке

Контроль, который никогда не тестировали, откажет в самый неудобный момент. Создайте безопасного тестового агента, который запускает потомка. Пусть тот спит, открывает безвредное локальное соединение и пытается выполнить действие через шлюз после завершения родителя. Затем отмените родителя в разные моменты: до запуска потомка, во время его работы, после отсоединения и во время активного действия.

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

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

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