# Доказывают ли что-нибудь ваши тесты утечки секретов агента?

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

Вам нужен не тест «избежал ли агент повторения секрета?», а тест «можем ли мы выполнить реальные действия через тестовый шлюз, собрать все артефакты, которые способен создать агент, и показать, что ни один из них не содержит уникальные рабочие учетные данные?». Это утверждение достаточно узкое для проверки и достаточно сильное, чтобы выявлять сбои, которые проявляются в продакшене.

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

## Чистый ответ не доказывает чистоту границы

Агент может получить секрет, ни разу не поместив его в естественный язык. Частый источник утечки, реализация инструмента, которая предлагает агенту самостоятельно вызвать API, а затем помещает значение `Authorization` во входные данные инструмента. Другой источник, запуск подпроцесса, наследующего `API_TOKEN`, потому что перед выполнением никто не заменил его окружение. Третий, SSH-помощник, который записывает временный файл с идентификатором туда, где его может прочитать агент.

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

Разделяйте два утверждения:

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

Команды часто проверяют первое утверждение и предполагают второе. Так агент может успешно вызвать тестовый API через шлюз и при этом получить токен в отладочном поле или унаследованной переменной окружения.

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

Стандарт теста должен звучать так:

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

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

## Тестовым учетным данным нужны назначение и отпечаток

Тестовый токен должен выполнять одну полезную функцию и ничего больше. Для HTTP создайте тестовую учетную запись, которая может вызвать один endpoint, например `GET /whoami` или `POST /echo-action`, и возвращает безобидный идентификатор учетной записи. Для SSH создайте ограниченную учетную запись на одноразовом хосте и разрешите небольшой набор команд, записывающих событие на стороне сервера.

Не используйте токен вроде `test-token`, а затем не ищите именно его. Короткие предсказуемые маркеры дают ложные совпадения и делают проверки кодировок бессмысленными. Генерируйте отдельную строку для каждого запуска теста. Добавьте в нее узнаваемый префикс и случайные данные, чтобы человек мог определить сбой и не принять обычный вывод за секрет.

Например, тестовый стенд может создать canary такого вида:

```text
sallyport_probe_7M3jP4Fqk2rV9dN8xC5a
```

Эта строка является значением учетных данных, а не идентификатором, который выводится агенту. Для журналов используйте отдельную общедоступную метку, например `run-2026-07-22-ssh-04`. Метка может появляться в стенограммах. Canary не должен.

Используйте отдельные canary для каждого канала и каждого пути ошибки. Повторное использование одного HTTP-токена во всех тестах превращает одну утечку в набор устаревших совпадений. Кроме того, становится трудно понять, появился ли поздний артефакт в текущем запуске или остался после предыдущей неудачной очистки.

Практический набор фикстур состоит из четырех частей:

1. HTTP-тестовая учетная запись, сервер которой после успешной аутентификации возвращает фиксированный ответ без секретов.
2. SSH-тестовая учетная запись, принудительная команда которой записывает идентификатор действия и возвращает фиксированное сообщение.
3. Запись учетных данных шлюза со свежим canary без копии, доступной агенту.
4. Манифест вне каталога артефактов, сопоставляющий метку запуска с использованными в нем canary.

Манифест содержит чувствительные тестовые данные. Храните его там, где агент не может его прочитать, и удаляйте canary после запуска. После очистки тестовая учетная запись должна отклонять эти учетные данные, даже если сбой оставил копию в локальном архиве.

Sallyport позволяет шлюзу хранить тестовые HTTP- и SSH-учетные данные, а агент получает результаты действий, а не сами учетные данные.

Запись на стороне сервера важна. Она подтверждает, что аутентификация действительно произошла, и не позволяет плохому тесту пройти только потому, что действие не запускалось. Ответ вроде `authenticated action accepted for run-2026-07-22-ssh-04` дает агенту достаточно свидетельств успеха, не возвращая ему входные данные аутентификации.

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

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

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

Для локального тестового стенда достаточно небольшой обертки на Python:

```python
#!/usr/bin/env python3
import json
import os
import pathlib
import sys

out = pathlib.Path(os.environ["PROBE_LAUNCH_RECORD"])
out.parent.mkdir(parents=True, exist_ok=True)
record = {
    "argv": sys.argv[1:],
    "environment": dict(os.environ),
}
out.write_text(json.dumps(record, sort_keys=True), encoding="utf-8")
os.execvp(sys.argv[1], sys.argv[1:])
```

Запускайте ее с намеренно ограниченным окружением. Оставьте только то, что агенту нужно для поиска исполняемого файла, временного каталога, endpoint MCP или stdio-обертки и пути, куда обертка записывает результат. Не наследуйте всю оболочку разработчика по привычке. Полное окружение импортирует посторонние учетные данные, облачную конфигурацию, токены реестра пакетов и старые настройки SSH, из-за чего тест может провалиться по неправильной причине.

Ожидаемая запись запуска имеет такой вид:

```json
{
  "argv": ["agent-command", "run", "tests/agent-task.txt"],
  "environment": {
    "HOME": "/private/tmp/agent-home",
    "PATH": "/usr/bin:/bin",
    "PROBE_LAUNCH_RECORD": "/private/tmp/probe/launch.json"
  }
}
```

Точные пути не важны. Важно, чтобы запись не содержала HTTP- или SSH-cанary, его форму base64, URL-кодированную форму или имя файла с закрытым ключом.

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

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

## Дочерние процессы входят в границу агента

Агенты часто запускают форматтеры, менеджеры пакетов, тестовые средства, команды Git, SSH-клиенты и скрипты. Если агент может запустить дочерний процесс, тот способен записать свое окружение и аргументы на диск, вернуть их в выводе или передать сетевому запросу. Считайте каждый дочерний процесс доступным агенту, если у вас нет веской технической причины считать иначе.

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

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

```python
#!/usr/bin/env python3
import json
import os
import pathlib
import sys

path = pathlib.Path(os.environ["PROBE_CHILD_RECORD"])
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
    json.dumps(
        {"argv": sys.argv, "environment": dict(os.environ)},
        sort_keys=True,
    ),
    encoding="utf-8",
)
print("child probe completed")
```

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

Отдельно проверяйте пути в аргументах. Разработчики знают, что переменные окружения могут раскрыть секрет, но аргументы командной строки часто опаснее: их могут записать инспекция процессов, история оболочки, форматирование ошибок и сборщики диагностики. Apple указывает, что процесс может получить собственные аргументы через `CommandLine.arguments`, а `ProcessInfo` предоставляет и аргументы, и окружение. Значит, секрет, переданный в argv, сразу виден коду внутри процесса.

Не принимайте аргумент вроде `--token-file=/private/tmp/secret` только потому, что байты токена отсутствуют. Проверьте и сам путь к файлу. Если агент может прочитать файл, у него есть секрет. Если файл доступен только отдельному процессу шлюза и никогда не попадает в рабочие каталоги, контролируемые агентом, зафиксируйте это в настройках теста.

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

## Успешным действиям нужны недружественные стенограммы

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

HTTP-тест должен запросить endpoint, подтверждающий тестовую учетную запись, но не возвращающий заголовки запроса. Полезным будет фиксированный ответ вроде этого:

```json
{
  "account": "gateway-test-http",
  "accepted": true,
  "request_label": "run-2026-07-22-http-01"
}
```

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

Для SSH пусть хост принимает фиксированную команду вроде `report-status <run-label>`. Сервер записывает аутентифицированную учетную запись, запрошенную команду и метку. Он возвращает ответ вроде `status recorded`. Агент не должен получать закрытый ключ, экспорт SSH-агента или стенограмму аутентификационного обмена.

Сохраните следующие артефакты на стороне агента:

- Исходный запрос агента и стенограмму модели.
- Исходные сообщения MCP или эквивалентные запросы и ответы действий.
- Стандартный вывод и стандартный поток ошибок команд, контролируемых агентом.
- Отладочные журналы инструментов, журналы повторных попыток и структурированные файлы событий.
- Файлы, записанные в рабочем пространстве агента, временном каталоге и настроенном каталоге кэша.

Соберите их до запуска очистки, которая удалит свидетельства. Затем сканируйте точные байты, а не только текст, декодированный как UTF-8. Секрет может появиться в JSON-экранировании, процентном кодировании, base64, сжатой трассировке или файле, где некорректные байты заставят обычный текстовый поиск пропустить совпадение.

Shim `sp mcp` в Sallyport хорошо подходит для такого теста: он позволяет проверить тот же обычный путь MCP, которым пользуется агент, пока учетные данные остаются в хранилище приложения.

Не принимайте отредактированную стенограмму за доказательство. Строка журнала `Authorization: [REDACTED]` может быть безопасной для оператора, но исходный объект события, создавший ее, все еще может содержать токен. Сначала собирайте данные до форматирования отображения, а затем тестируйте форматтер отдельно. Это два разных требования.

## Именно обработка ошибок чаще всего ломает изоляцию секретов

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

Начните с HTTP-ошибок, возникающих на разных этапах:

1. Пусть тестовый сервер после получения корректного canary вернет несекретный ответ `401`. Агент должен узнать, что аутентификация не удалась, но не значение отправленного заголовка.
2. Пусть сервер вернет `500` с телом, содержащим общедоступную метку запуска. Шлюз может вернуть ограниченное тело ошибки, но не должен добавлять заголовки запроса или эквивалент команды curl.
3. Закройте соединение после подготовки аутентификации шлюзом. Это выявляет низкоуровневые исключения, включающие объекты запроса в описании.
4. Верните некорректный JSON после успешной аутентификации. Парсер может включить ошибочный ответ или окружающий контекст в исключение.
5. Используйте имя DNS, которое не разрешается для одного маршрута теста. Это выявляет вывод повторных попыток и диагностики endpoint.

Затем запустите SSH-сбои до и после установки соединения. Используйте хост с неверной идентичностью, удаленную команду с ненулевым кодом завершения и принудительную команду, возвращающую контролируемую ошибку. Не тестируйте неверный закрытый ключ, отправляя тестовый ключ обратно агенту или заставляя сервер записывать его. Агенту достаточно получить классификацию вроде `connection rejected` или `remote command failed` и безопасную метку запроса.

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

Полезная фикстура для ошибок содержит проверки с обеих сторон:

```text
Сторона агента: canary отсутствует во всех собранных артефактах.
Сторона шлюза: действие имеет ожидаемую безопасную классификацию ошибки.
Сторона сервера: ожидаемый запрос произошел или не произошел в тесте с заблокированным хранилищем.
```

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

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

## Для артефактов сбоя нужен намеренный отказ

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

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

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

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

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

Если позже вы будете запускать похожие тесты в Linux, добавьте в набор артефактов метаданные core dump и вывод journal. В руководстве `systemd-coredump` описаны поля, способные хранить командную строку и окружение сбившегося процесса. Набор, который проверяет только файл core и игнорирует его метаданные, пропускает очевидный путь утечки.

## Проверяйте байты, кодировки и разделенные значения

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

Минимальный набор вариантов для каждого canary:

```text
исходные байты
текст base64
URL-кодированный текст
текст с экранированием JSON
текст в шестнадцатеричном формате
первая и вторая половины, разделенные одним переносом строки
```

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

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

```python
import base64
import json
import pathlib
import urllib.parse

secret = bytes.fromhex("73616c6c79706f72745f70726f62655f5837")
needles = {
    "raw": secret,
    "base64": base64.b64encode(secret),
    "url": urllib.parse.quote_from_bytes(secret).encode(),
    "json": json.dumps(secret.decode()).encode(),
    "hex": secret.hex().encode(),
}

for path in pathlib.Path("artifacts").rglob("*"):
    if not path.is_file():
        continue
    data = path.read_bytes()
    for name, needle in needles.items():
        offset = data.find(needle)
        if offset >= 0:
            raise SystemExit(f"secret match: {path} transform={name} offset={offset}")
```

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

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

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

## В отчете должны быть и отсутствие секрета, и факт действия

Хороший отчет отвечает на четыре вопроса и не заставляет читателя доверять вашей интерпретации.

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

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

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

Для проверок выпуска сделайте условие успеха строгим:

```text
PASS только если сервер подтвердил нужное действие,
все обязательные артефакты собраны,
и в материалах, доступных агенту, нет исходного или преобразованного canary.
```

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

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

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