# Аудитная запись действия агента: практическая модель контроля

Действия агентов требуют записей, которые описывают полномочия, а не только активность. Строка вроде `POST /deploy returned 200` не скажет расследующему, тот ли процесс отправил запрос, разрешил ли его человек, какая система его получила и остановил ли что-нибудь последующий отзыв.

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

## Аудитная запись должна описывать попытку с полномочиями

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

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

Считайте это разными объектами:

- **Сессия** определяет один запущенный процесс агента и период его работы.
- **Одобрение** фиксирует решение человека с заданной областью действия.
- **Действие** описывает одну запрошенную внешнюю операцию и ее результат.
- **Отзыв** прекращает сессию или авторизацию в точно указанное время.

Во время инцидента это различие имеет практическое значение. Если человек отозвал сессию в 14:03, нужно увидеть все действия до 14:03, все попытки после 14:03 и локальное решение, которое отклонило эти поздние попытки. Одна изменяемая строка `session approved: false` уничтожает эту историю.

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

## Участник, это процесс, а не дружелюбная метка агента

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

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

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

```json
{
  "actor": {
    "session_id": "ses_01J8Q1F9K9Z3",
    "pid": 48217,
    "started_at": "2025-02-18T21:14:06Z",
    "executable": "/usr/local/bin/agent-runner",
    "signing_authority": "Developer ID Application: Example Developer",
    "parent_pid": 48091,
    "parent_executable": "/Applications/Terminal.app"
  }
}
```

Значение `signing_authority` должно поступать из проверки подписи платформы, а не из строки, которую передал агент. Агент может назвать себя `claude-code`, `deploy-helper` или `trusted-agent`. Метка остается лишь оформлением, если операционная система не связывает ее с идентичностью исполняемого файла.

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

RFC 5424, спецификация протокола syslog, отделяет поля заголовка, такие как имя приложения и ID процесса, от структурированных данных. Для записей агентов это разделение по-прежнему полезно. Стабильные идентификаторы и время помещайте в отдельные поля, по которым могут выполнять запросы машины. Переменный контекст, например репозиторий или ссылка на задачу, храните в объекте с пространством имен. Если складывать все в одну строку сообщения, любое последующее расследование превращается в разбор текста.

## Цели нужны и адрес, и смысл

Записывайте, куда направилось действие и на какой ресурс или команду оно хотело повлиять. Это связанные факты, но они не взаимозаменяемы.

Для HTTP сетевым назначением может быть `api.example.internal`, а осмысленной операцией, `POST /v1/releases/{release_id}/promote`. Сохраняйте хост, порт, протокол, HTTP-метод и шаблон маршрута. Добавьте идентификатор целевой системы, которым управляет ваша команда, например `release-service-prod`. Идентификатор переживет миграцию хоста, а имя хоста поможет понять, какой запрос действительно покинул машину.

Для SSH сохраняйте псевдоним или имя хоста, порт, отпечаток ключа хоста или ссылку на запись known-hosts и метку удаленной учетной записи, если ее безопасно хранить. В цели также должен быть указан класс запрошенной команды. `restart-worker` сообщает больше, чем `ssh succeeded`, но раскрывает меньше, чем полная команда оболочки с путями клиентов и переменными окружения.

Удаляйте секреты до сохранения, а не после того, как аналитик откроет журнал. Строки запроса URL, заголовки запросов, аргументы оболочки и тела JSON регулярно содержат токены. Библиотека журналирования, которая сохраняет их «для отладки», однажды обязательно создаст файл инцидента, полный учетных данных.

Такая форма записи сохраняет сведения о цели доступными для запросов, не копируя запрос целиком:

```json
{
  "target": {
    "kind": "http",
    "system_id": "release-service-prod",
    "endpoint": {
      "scheme": "https",
      "host": "api.example.internal",
      "port": 443,
      "method": "POST",
      "route_template": "/v1/releases/{release_id}/promote"
    }
  },
  "operation": {
    "name": "promote_release",
    "request_fingerprint": "sha256:8e8c...",
    "request_bytes": 286,
    "redacted_parameters": {
      "environment": "production",
      "release_id": "rel_7b2"
    }
  }
}
```

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

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

## Запрошенная операция и наблюдаемый результат требуют разных полей

Запрос агента выражает намерение. Результат показывает, что шлюз увидел после попытки выполнить это намерение. Не объединяйте их в расплывчатый статус вроде `completed`.

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

Используйте словарь результатов, который указывает место сбоя. Например:

- `denied_vault_locked` означает, что локальная граница секретов отклонила вызов.
- `denied_approval` означает, что необходимое решение человека не разрешило действие.
- `network_error` означает, что шлюз не смог установить или сохранить соединение.
- `target_rejected` означает, что удаленная служба вернула отказ.
- `target_accepted` означает, что удаленная служба приняла запрос.

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

Полный объект результата может выглядеть так:

```json
{
  "result": {
    "outcome": "target_accepted",
    "started_at": "2025-02-18T21:19:42.184Z",
    "finished_at": "2025-02-18T21:19:43.021Z",
    "duration_ms": 837,
    "http_status": 202,
    "response_fingerprint": "sha256:2a64...",
    "response_summary": "promotion job accepted",
    "evidence_ref": null
  }
}
```

Не называйте `target_accepted` успехом в схеме. Позже это создаст проблемы. Если служба принимает задачу, а сама задача завершается ошибкой, шлюз сделал все правильно, но бизнес-операция не завершилась. На этот отдельный вопрос может ответить более позднее событие о завершении на удаленной стороне, связанное с ID задачи.

## В записях об одобрении нужно указывать, что именно одобрил человек

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

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

```json
{
  "approval": {
    "approval_id": "apr_01J8Q1P4Y5D6",
    "decision": "approved",
    "scope": "session",
    "subject_session_id": "ses_01J8Q1F9K9Z3",
    "approved_at": "2025-02-18T21:14:11Z",
    "expires_at": "2025-02-18T22:02:53Z",
    "approver_presence": "local_user_confirmation"
  }
}
```

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

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

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

## Отзыв, это событие с моментом отсечения

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

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

Рассмотрим такую последовательность сбоя:

1. В 09:00 процесс агента получает одобрение сессии и в 09:17 отправляет изменение в рабочую среду.
2. Оператор замечает неожиданную цель и отзывает сессию в 09:18:04.
3. Цель отвечает на запрос от 09:17 в 09:18:07, поскольку уже поставила работу в очередь.
4. В 09:18:09 агент пытается выполнить еще один вызов, и шлюз его отклоняет.

Хороший журнал сохраняет все четыре события. Действие в 09:17 было разрешено в момент начала. Ответ после отзыва относится к более раннему действию. Отклоненная попытка доказывает, что отзыв вступил в силу для последующей работы. Если пометить каждое прежнее действие как `revoked`, исчезнет последовательность, объясняющая реальный масштаб воздействия.

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

## Защита от незаметного изменения требует режима добавления без перезаписи

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

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

```json
{
  "journal": {
    "sequence": 1842,
    "recorded_at": "2025-02-18T21:19:43.024Z",
    "previous_hash": "sha256:68b1...",
    "record_hash": "sha256:93f4...",
    "hash_format": "canonical-json-v1"
  }
}
```

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

NIST Special Publication 800-92, Guide to Computer Security Log Management, рассматривает создание, хранение, анализ и хранение в течение установленного срока журналов как разные обязанности. Это помогает избежать распространенной ошибки: команды добавляют хеш к записям и считают работу законченной. Вам по-прежнему нужны надежное хранилище, контроль доступа к записывающему процессу, решение о сроках хранения, регулярная проверка и процедура расследования неудачной проверки.

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

## Один ID корреляции делает запись полезной в стрессовой ситуации

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

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

Компактная полная запись может иметь такую форму:

```json
{
  "schema_version": "1.0",
  "action_id": "act_01J8Q2ABR8M7",
  "event_type": "action.completed",
  "actor": {"session_id": "ses_01J8Q1F9K9Z3", "pid": 48217},
  "target": {"kind": "http", "system_id": "release-service-prod"},
  "operation": {"name": "promote_release", "request_fingerprint": "sha256:8e8c..."},
  "authorization": {"vault": "unlocked", "approval_id": "apr_01J8Q1P4Y5D6", "decision": "approved"},
  "result": {"outcome": "target_accepted", "http_status": 202},
  "journal": {"sequence": 1842, "previous_hash": "sha256:68b1..."}
}
```

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

## Хранение должно сохранять доказательства и не создавать второе хранилище секретов

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

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

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

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

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