# Как работает атрибуция общей API-учетной записи в сессиях агентов

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

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

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

## Учетная запись поставщика и исполнитель - разные идентичности

Общая учетная запись поставщика отвечает на вопрос: «Какие учетные данные принял поставщик?» Атрибуция исполнителя отвечает на другой вопрос: «Какой локальный субъект вызвал эту конкретную операцию?» Ответы часто различаются, и объединение их в одно поле создает плохую аудит-трассировку.

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

- **Внешняя учетная запись**: тенант поставщика, сервисный пользователь, OAuth-клиент или идентичность API-ключа, видимая удаленному API.
- **Сессия выполнения**: один запущенный процесс агента с новым непредсказуемым идентификатором сессии.
- **Инициирующий субъект**: человек, задание CI или родительский сервис, запустивший эту сессию.
- **Ссылка на работу**: задача, запрос на изменение, развертывание, репозиторий или явно указанное задание, объясняющее причину вызова.

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

Это различие не сводится к терминологии. Допустим, `vendor-prod` удаляет удаленное развертывание. Поставщик может честно сказать, что удаление выполнил `vendor-prod`. В вашей локальной записи должно быть видно, пришла ли сессия `s_7f3...` от агента релиза, запущенного Maya, было ли у нее подтверждение на этот запуск и было ли удаление прямым вызовом или повтором после тайм-аута. Одно поле `actor=vendor-prod` скрывает все факты, которые могут изменить ход реагирования на инцидент.

RFC 8693 проводит похожее различие в делегировании OAuth. Он отделяет субъекта, от имени которого существует полномочие, от текущего исполнителя, использующего утверждение JWT `act`. В документе также сказано, что серверы ресурсов должны принимать решения о доступе на основе утверждений токена верхнего уровня и текущего исполнителя, а не исторических вложенных исполнителей. Это полезная граница и для локальных систем агентов: сохраняйте цепочку происхождения для расследования, но принимайте решения о доступе по ясной идентичности текущей сессии, а не по длинной неоднозначной истории прошлых вызовов.

Не называйте учетную запись поставщика «агентом». Ее могут использовать агенты, скрипты, дежурные операторы и задания миграции. Дайте ей точное имя, например `external_principal`, а исполнителем в собственном аудите назначьте локальную сессию.

## Идентичность сессии должен создавать запускающий процесс

Процесс, который запускает агента, должен создать идентичность сессии до того, как агент получит возможность запросить внешнее действие. Не позволяйте агенту выбирать ее самостоятельно. Агент, который может указать `session_id=release-approved`, способен сделать последующую проверку вводящей в заблуждение.

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

```json
{
  "session_id": "ses_01JQ6EXAMPLE3K5A",
  "started_at": "2026-07-22T15:04:18Z",
  "initiator": {
    "kind": "human",
    "id": "maya@example.test"
  },
  "agent": {
    "process_id": 48192,
    "binary_authority": "signed-local-agent",
    "launch_path": "/workspace/payments"
  },
  "work": {
    "kind": "change_request",
    "id": "CR-1842"
  },
  "parent_session_id": null
}
```

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

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

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

## Атрибуцию нужно фиксировать до передачи учетных данных

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

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

```json
{
  "call_id": "call_01JQ6F9K4W7D",
  "session_id": "ses_01JQ6EXAMPLE3K5A",
  "external_principal": "vendor-prod",
  "channel": "http",
  "request": {
    "method": "POST",
    "host": "api.vendor.example",
    "path_template": "/v1/deployments/{id}",
    "operation": "create_deployment"
  },
  "authorization": {
    "vault_unlocked": true,
    "session_authorized": true,
    "per_call_approval": false
  },
  "work_id": "CR-1842",
  "attempt": 1,
  "created_at": "2026-07-22T15:08:34Z"
}
```

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

Такой подход также разделяет намерение и результат. Агент может запросить `create_deployment`, но удаленный сервис способен вернуть ошибку проверки. Журнал вызова должен сохранить оба факта. Позже вы сможете ответить, пытался ли агент выполнить действие, не утверждая, что действие завершилось успешно.

Sallyport использует именно такую схему: агент подключается через свой MCP shim, а приложение хранит секрет и выполняет действие по HTTP или SSH. Поэтому записи сессии и активности могут связать локальный запуск с использованием общего ключа, не помещая этот ключ в контекст агента.

## Заголовки, заданные агентом, служат свидетельством, но не доказательством

Команды часто добавляют заголовки вроде `X-Agent-Name`, `X-Task-ID` или `X-Run-ID` и считают проблему решенной. Эти поля могут помочь сопоставить удаленные журналы, но агент, управляющий запросом, также может не отправить их, изменить или воспроизвести. Это метки, а не граница полномочий.

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

Например, внутри шлюза можно зарезервировать небольшой набор заголовков:

```text
X-Execution-Session: ses_01JQ6EXAMPLE3K5A
X-Action-Call: call_01JQ6F9K4W7D
X-Work-Reference: CR-1842
```

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

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

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

## Для повторов нужна цепочка происхождения, а не одна отметка времени

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

Моделируйте оба уровня. Дайте запланированной операции `operation_id`, а каждой сетевой попытке отдельный `call_id`. Связывайте повторы с предыдущей попыткой и записывайте причину повтора.

```json
{
  "operation_id": "op_01JQ6F8P0Z",
  "call_id": "call_01JQ6F9K4W7D",
  "attempt": 2,
  "retries_call_id": "call_01JQ6F79S2M1",
  "retry_reason": "connection_closed_before_response",
  "idempotency_key": "idem_94c2e1",
  "vendor_request_id": "req_8d71"
}
```

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

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

Типичная ошибка выглядит так. Сессия A запрашивает создание развертывания, соединение обрывается, и клиент повторяет запрос. Через несколько мгновений сессия B начинает работу с той же ссылкой на задачу, не видит развертывание и отправляет запрос снова. Теперь у поставщика два развертывания. Полезный журнал показывает две сессии, две запланированные операции, отдельные попытки и общий ключ идемпотентности, если он использовался. Слабый журнал показывает четыре строки `POST /deployments` под одной сервисной учетной записью и заставляет команду восстанавливать остальное по отметкам времени.

## Подтверждение должно быть связано с процессом, а не с понятным именем

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

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

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

Записывайте решение как событие со стабильной ссылкой:

```json
{
  "approval_id": "apr_01JQ6G3C",
  "session_id": "ses_01JQ6EXAMPLE3K5A",
  "scope": "session",
  "decision": "approved",
  "decided_at": "2026-07-22T15:06:11Z",
  "process_authority": "signed-local-agent"
}
```

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

## Журналы аудита поставщика должны подтверждать вашу запись

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

Документация GitHub дает наглядный пример такого различия. Вызовы, сделанные с помощью server-to-server token пользователя GitHub App, могут показывать пользователя как исполнителя аудита, одновременно указывая программный тип доступа как соответствующий тип токена. События аудита GitHub Enterprise также для многих типов событий раскрывают такие поля, как исполнитель, сведения о токене, идентификатор запроса и user agent. Это полезные данные со стороны поставщика, но значение этих полей определяется моделью авторизации GitHub, а не моделью локальных сессий агентов.

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

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

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

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

## Разделяйте учетные данные только ради реальной границы

Обычно советуют: «Дайте каждому агенту собственный API-ключ». Он популярен, потому что его легко объяснить и потому, что экран аудита поставщика выглядит аккуратнее. Но такой контроль подходит не всегда.

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

Перед созданием еще одной идентичности поставщика проверьте следующее:

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

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

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

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

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

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

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

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

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