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

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

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

## Аудит должен быть частью авторизации

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

Здесь команды часто смешивают две разные проблемы:

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

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

Модель Sallyport хорошо показывает это различие: журналы Sessions и Activity строятся на одном зашифрованном аудите с хеш-цепочкой, а не являются отдельными источниками истины. Проекция может отставать или требовать перестроения. Именно зашифрованная цепочка, которую нельзя читать без расшифровки, должна оставаться неповрежденной.

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

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

## Предупреждение и запрет, это разные состояния

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

Используйте четыре состояния:

1. **Нормальное**: свободное место превышает рабочий резерв, а недавняя проверка надежной записи завершилась успешно.
2. **Предупреждение**: места стало меньше порога предупреждения, но полный резерв еще доступен. Новые вызовы продолжаются, оператор получает одно оповещение.
3. **Ограниченное**: места стало меньше резерва или воркер столкнулся с временным сбоем, который еще не достиг границы критической ошибки. Не допускайте действий, способных создать большие записи или несколько событий. Разрешайте только уже допущенную работу и продолжайте проверять воркер.
4. **Запрещено**: шлюз не может надежно добавить событие аудита или оставшегося места недостаточно для минимального бюджета записи. Отклоняйте каждый новый авторизованный вызов.

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

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

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

## Резерв, это бюджет записей, а не оптимизм по поводу свободного места

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

Начните с таких вопросов:

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

Предположим, шлюз разрешает 12 одновременных действий, для каждого может потребоваться запись о намерении и запись о результате, а консервативный бюджет одной зашифрованной записи составляет 128 КиБ. Только записи действий займут 3 МиБ. Это еще не резерв. Добавьте максимальный ожидаемый пакет проекции журнала, накладные расходы на смену файла, маркер ошибки и существенный запас на особенности выделения места. Затем округлите результат до числа, за которым легко следить.

Полезный результат, это записанная политика, а не магическое число:

```text
warning threshold: 2 GiB free on the audit volume
hard reserve:      512 MiB reserved for audit completion and recovery records
admission budget:  256 KiB minimum available per new action
recovery threshold: 3 GiB free plus one durable test append
```

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

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

Для аудит-воркера это означает, что проверка допуска должна спрашивать: «Могу ли я сейчас безопасно выделить этот бюджет записи?» Она не должна спрашивать: «Показывает ли строка меню ненулевой объем свободного места?»

## Неудачная запись должна сразу изменить поведение шлюза

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

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

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

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

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

Используйте небольшой классификатор ошибок:

```text
append or sync succeeds                 -> action may proceed or complete normally
append fails before dispatch             -> deny the action
append fails after dispatch              -> deny new actions, preserve intent, raise incident
sync reports I/O failure                 -> deny new actions, preserve files, investigate storage health
space check below admission budget       -> deny this new action before dispatch
```

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

## Воркеру нужна граница надежной фиксации

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

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

```text
1. Serialize the next encrypted event with sequence number N and previous hash H(N-1).
2. Append the complete event frame to the active segment.
3. Sync the segment file and check the result.
4. Update the manifest with the new high-water sequence and segment hash.
5. Sync the manifest and check the result.
6. Only now mark event N as committed to the dispatcher.
```

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

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

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

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

## Проведите учение на временном томе аудита

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

В macOS создайте и подключите небольшой образ APFS для учения:

```sh
hdiutil create -size 2g -fs APFS -volname AuditDrill /tmp/audit-drill.dmg
hdiutil attach /tmp/audit-drill.dmg
df -h /Volumes/AuditDrill
```

Точный путь подключения может отличаться, если том с таким именем уже существует. Перед изменением тестовой конфигурации проверьте его с помощью `df`. Ожидаемый вывод должен указывать на подключенную файловую систему и показывать примерно 2 ГиБ емкости:

```text
Filesystem        Size   Used  Avail Capacity  Mounted on
/dev/diskXsY      2.0G   ...   ...     ...%    /Volumes/AuditDrill
```

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

Затем расходуйте место только внутри подключенного образа диска:

```sh
dd if=/dev/zero of=/Volumes/AuditDrill/fill.bin bs=1048576
```

`dd` остановится, когда том больше не сможет выделять блоки. Команда намеренно грубая. Не используйте для этого теста команду создания разреженного файла: такой файл может выглядеть большим, не занимая блоки, необходимые для запуска проверяемого условия.

Проводите учение поэтапно, не заполняйте том сразу до нуля:

1. Заполняйте том до срабатывания порога предупреждения. Убедитесь, что небольшие новые действия по-прежнему выполняются, а оператор получает одно оповещение.
2. Заполняйте том до срабатывания критического резерва. Убедитесь, что шлюз отклоняет новые действия в соответствии с бюджетом допуска.
3. Если это предусмотрено сценарием, непосредственно перед переходом в состояние «Ограниченное» допустите одно контролируемое действие и проверьте, успешно ли записывается его финальное событие.
4. Заполняйте том до фактического сбоя добавления или синхронизации. Убедитесь, что шлюз переходит в состояние «Запрещено» и показывает причину.
5. Остановите процесс, при необходимости подключите образ заново и проверьте зашифрованную цепочку до удаления файла-заполнителя.

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

## Проверяйте опасные временные окна, а не только полностью заполненный том

Поверхностное учение заполняет том, наблюдает запрет, освобождает место и объявляет успех. Так вы пропускаете временные окна, которые создают пробелы в аудите.

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

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

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

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

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

## Сохраните шифротекст до попытки восстановления

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

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

В контрольном списке сохранения должны быть ответы на пять вопросов:

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

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

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

## Оповещения должны показывать, какое решение принял шлюз

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

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

```text
state: denied
reason: audit append failed with ENOSPC
audit path: /configured/audit/path
free bytes observed: 41,943,040
hard reserve: 536,870,912
last committed sequence: 8412
unresolved admitted actions: 1
new credentialed calls: denied
existing action handling: completion record could not be committed
first failure time: 2026-07-22T14:37:18Z
```

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

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

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

## Восстановление должно подтвердить исправность воркера

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

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

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

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

Затем примените порог восстановления. Если предупреждение настроено на 2 ГиБ, критический резерв на 512 МиБ, а восстановление на 3 ГиБ, не открывайте вызовы при 600 МиБ только потому, что воркер смог выполнить одну запись. Более высокий порог предотвращает немедленное повторение проблемы и дает оператору время найти источник нагрузки.

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