# Что доказывают тесты с принудительным завершением о шлюзе агента?

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

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

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

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

## Принудительное завершение должно обходить очистку

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

Используйте два режима завершения и правильно обозначайте их:

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

В macOS тестовый стенд может использовать `kill -TERM` для первого случая и `kill -KILL` для второго. `SIGTERM` даёт процессу возможность обработать завершение. `SIGKILL` такой возможности не даёт. Не называйте оба случая «принудительным завершением» в таблице результатов: они отвечают на разные вопросы.

```sh
# Find the gateway process you started for the test.
pgrep -fl Sallyport

# Graceful termination case.
kill -TERM 48192

# Abrupt termination case.
kill -KILL 48192

# Confirm the process is gone.
ps -p 48192
# PID TTY           TIME CMD
# 48192 ttys003    0:00.42 gateway-test
# After SIGKILL, ps should print no process row.
```

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

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

## Для запроса нужны явные точки прерывания

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

Для HTTP-действия используйте примерно такую последовательность:

1. Шлюз принимает описание разрешённого действия и назначает ID действия.
2. Он записывает намерение, если по проекту это нужно сделать до сетевой работы.
3. Он получает учётные данные внутри защищённого процесса.
4. Он формирует исходящий запрос и добавляет учётные данные.
5. Он открывает соединение и записывает запрос.
6. Он получает достаточную часть ответа, чтобы классифицировать результат на удалённой стороне.
7. Он записывает результат завершения или неизвестный результат, затем передаёт агенту ответ.

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

Компактный протокол стенда может выглядеть так:

```json
{
  "action_id": "fq-2026-07-22-017",
  "hold_after": "request_headers_built",
  "release": false
}
```

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

Делайте контрольные точки узкими. «До сетевого обмена» слишком расплывчато, если DNS-разрешение, создание соединения, TLS-согласование и передача тела выполняются в разных задачах. Не нужна точка внутри каждого вызова библиотеки, но структуры должно хватать, чтобы определить, мог ли удалённый сервер получить учётные данные или тело, меняющее состояние.

Для SSH действует тот же принцип, но нужны другие свидетельства. Остановите выполнение до того, как `sp-ssh` получит запрос на соединение с учётными данными, после запуска помощника, но до аутентификации, после аутентификации, но до начала команды, а также после возврата удалённой команды, но до фиксации локального завершения. Записывайте начало и выход удалённой команды на одноразовом тестовом хосте. Не делайте вывод о выполнении команды по открытому TCP-соединению.

## Завершение до внедрения не должно оставлять следов удалённой стороне

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

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

Проверьте четыре прерывания до отправки:

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

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

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

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

## Внедрение это локальное событие, а не доказательство отправки

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

Разделяйте как минимум четыре локальных состояния:

| Состояние | Что можно честно утверждать | Чего утверждать нельзя |
|---|---|---|
| Учётные данные получены | Защищённый процесс прочитал учётные данные для этого действия | Их получил удалённый узел |
| Запрос собран | Процесс создал в памяти запрос с учётными данными | Соединение открыто |
| Запись в транспорт начата | Процесс попытался отправить байты | Удалённое приложение их обработало |
| Результат на удалённой стороне наблюдался | Процесс получил свидетельство от удалённой стороны | Запись завершения сохранена на диске |

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

Затем завершите процесс в первой точке, где транспортная библиотека может начать запись. Это неудобный тест. Локальный процесс мог вызвать функцию записи, но ядро, прокси, TLS-слой или удалённая служба могли не получить полный запрос. Ожидаемый результат должен быть `outcome=unknown`, если только приёмник не располагает положительным свидетельством того, что запрос был или не был получен.

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

Полезная запись стенда выглядит так:

```json
{
  "action_id": "fq-2026-07-22-017",
  "connection_seen": true,
  "headers_complete": true,
  "credential_marker": "sha256:7b8c...",
  "body_complete": false,
  "response_sent": false
}
```

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

## Сетевой обмен создаёт честное неизвестное состояние

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

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

1. Служба не получила запрос.
2. Служба получила запрос, но отклонила его.
3. Служба приняла запрос и создала развёртывание.

Локальный шлюз может не знать, какой вариант произошёл. Запись со статусом `failed` будет ложной в третьем случае. Запись со статусом `succeeded` будет ложной в первом и втором. Используйте `unknown` и сохраните ID действия, ID удалённого запроса, назначение, метод и точку, на которой прекратилось локальное наблюдение.

Здесь слепые повторы особенно опасны. Командам нравятся автоматические повторы, потому что временные сетевые сбои распространены, а удачные демонстрации выглядят гладко. Для GET повтор обычно допустим, если он не вызывает учёт операции на сервере и проблем с ограничением частоты. Для POST, PATCH, удалённой команды или вызова API, способного менять данные, нужен механизм идемпотентности или последующая сверка.

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

Проверьте именно такую последовательность на своём приёмнике:

```text
Gateway writes intent record
Gateway sends POST with action ID fq-2026-07-22-017
Receiver stores action ID and body
Receiver delays its HTTP response
Harness kills gateway with SIGKILL
Gateway restarts
Agent requests retry
Gateway checks receiver for action ID before another POST
```

Результат должен показать одно сохранённое удалённое действие и одну локальную запись с прерванным или неизвестным результатом, которая позднее сверяется с удалённым состоянием. Если второй POST появляется до проверки, тест обнаружил ошибку повторов. Если журнал затирает неопределённость и оставляет только чистую запись об успехе, тест обнаружил ошибку аудита.

Не считайте закрытие TCP доказательством того, что удалённое приложение не выполнило действие. Сокет может закрыться после того, как узел уже передал запрос коду приложения. Важным свидетельством остаётся долговечная запись действия на стороне приёмника.

## Фиксация аудита должна иметь определённую гарантию сохранности

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

Сформулируйте обещание одним предложением. Например: «До начала меняющего состояние внешнего действия шлюз надёжно сохраняет намерение; после наблюдения результата он надёжно сохраняет этот результат и только потом возвращает его агенту». Затем тестируйте слова «начинает» и «надёжно сохраняет», а не принимайте объект в памяти с временной меткой за запись.

POSIX определяет `fsync()` как запрос на передачу данных файла в связанное хранилище и указывает, что функция не возвращается до завершения этой операции или сообщения об ошибке. Стандарт также предупреждает, что гарантии хранилища зависят от реализации и конфигурации. Это не повод пропускать вызов. Это означает, что нужно точно описать обещания программы и проверить управляемое ею восстановление.

Для зашифрованного журнала с цепочкой хешей проверьте как минимум такие точки:

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

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

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

При проверке восстановления разделяйте историю запуска агента и историю вызовов действий. Сессия может завершиться резко, а несколько действий иметь разные результаты. Одна запись уровня запуска «завершён» не заменяет отдельные записи для запроса, достигшего удалённой системы, и другого запроса, который так и не вышел из памяти.

## Состояние подтверждения не должно переживать объект, к которому оно относится

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

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

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

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

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

## Составьте матрицу результатов до запуска набора тестов

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

Используйте такие столбцы:

| Точка прерывания | Может ли удалённая сторона увидеть соединение? | Может ли удалённая сторона увидеть учётные данные? | Допустимый локальный статус действия | Результат агента после восстановления | Нужные свидетельства |
|---|---:|---:|---|---|---|
| До поиска учётных данных | Нет | Нет | Не начато или прервано | Новое подтверждение или путь повтора | Только трассировка шлюза |
| После получения учётных данных | Нет | Нет | Прерванная подготовка | Новое подтверждение или путь повтора | Только трассировка шлюза |
| Во время записи запроса | Да | Возможно | Неизвестно | Сверка до повтора | Запись приёмника и локальная запись |
| После принятия удалённой стороной | Да | Да | Неизвестно до наблюдения или сверки | Сверка до повтора | Долговечная запись удалённой стороны |
| После долговечного завершения | Да | Да | Завершено | Кешированный или сверенный результат | Проверенная запись аудита |

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

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

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

## Считайте расхождения частью работы над проектом, а не шумом теста

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

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

Лучший результат это не панель, заполненная зелёными тестами. Это контракт сбоя, которым оператор может воспользоваться под давлением: это действие точно не покинуло компьютер; это действие могло достичь удалённой системы и требует сверки; это действие завершилось, а его запись пережила проверку. Только такие категории позволяют обоснованно принять решение после сбоя.

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