Оформление
Контракт EACP
Протокол управления внешними агентами: версии, авторизация, все вызовы, формат описания агента, формат ошибок и правила идемпотентности.
Что это
EACP это EVA Agent Control Protocol, универсальный контракт между платформой и внешним агентом. Название задано в самом описании протокола.
| Версия | Что покрывает |
|---|---|
| 1.0 | Базовый протокол: описание, состояние, настройки, задания, события, команды, спорные случаи |
| 1.1 | Расширение интерфейса: объявляемые агентом разделы |
Описание протокола лежит в платформе в каталоге contracts/eacp/, хеши файлов зафиксированы отдельным файлом и проверяются тестом. То есть незамеченное изменение контракта невозможно.
Платформа принимает только версию 1
При подключении проверяется версия протокола из описания агента. Если она не начинается с 1., подключение отклоняется с ошибкой о неподдерживаемой версии.
Авторизация
Один механизм, других нет.
Authorization: Bearer <service token>К каждому обращению платформа добавляет ещё два заголовка:
| Заголовок | Значение |
|---|---|
X-Request-ID | Уникальный идентификатор обращения |
Accept | application/json |
Токен задаётся при подключении агента, хранится в базе в зашифрованном виде и наружу не отдаётся.
Ошибки авторизации маскируются
Ответы агента 401 и 403 платформа наружу отдаёт как 502 с отдельными кодами о неудачной аутентификации и неудачной авторизации. Сделано, чтобы не путать отказ агента с отказом самой платформы.
Требования к адресу агента
| Правило | Подробности |
|---|---|
| Схема | Только http или https |
| Данные для входа в адресе | Запрещены |
| Параметры запроса | Запрещены |
| Якорь | Запрещён |
| Косая черта на конце | Срезается автоматически |
Ожидаемый вид адреса оканчивается на /agent-api/v1.
Описание агента
Первое, что платформа запрашивает при подключении.
json
{
"protocol_version": "1.0",
"agent": {
"type": "mail-processor",
"instance_id": "mail-processor-01",
"name": "Обработка почты",
"version": "0.1.0"
},
"capabilities": {
"settings": true,
"settings_schema": true,
"event_history": true,
"event_stream": true,
"jobs": true,
"llm_via_eva": true,
"commands": []
},
"links": {
"status": "/agent-api/v1/status"
}
}Раздел возможностей определяет, какие вкладки появятся в карточке агента и будет ли платформа читать его поток событий.
Формат ошибок
Ошибки приходят по стандарту описания проблем.
json
{
"type": "urn:eva:error:unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid token",
"code": "UNAUTHORIZED",
"request_id": "0f3c..."
}Базовый протокол
Состояние и описание
| Метод | Путь | Назначение |
|---|---|---|
GET | /manifest | Описание агента |
GET | /health/ready | Готовность |
GET | /status | Живое состояние, активные и очередные задачи |
Настройки
| Метод | Путь | Назначение |
|---|---|---|
GET | /settings/schema | Схема настроек, по ней платформа строит форму |
GET | /settings | Текущие значения |
PATCH | /settings | Изменение значений |
GET | /settings/history | История изменений, постранично |
Задания и реестр операций
| Метод | Путь | Назначение |
|---|---|---|
GET | /jobs | Список заданий, фильтр только по состоянию |
GET | /jobs/{jobId} | Одно задание |
GET | /ledger | Реестр операций, постранично |
GET | /ledger/export | Выгрузка реестра файлом |
Команды и операции
| Метод | Путь | Назначение |
|---|---|---|
POST | /commands | Послать команду, обязателен ключ идемпотентности |
GET | /operations/{operationId} | Состояние операции |
Команда принимается кодом 202 и возвращает идентификатор операции, состояние и время принятия. Выполнение асинхронное.
События
| Метод | Путь | Назначение |
|---|---|---|
GET | /events | Постраничное чтение истории |
GET | /events/stream | Поток событий, постоянное соединение |
Оба принимают номер, после которого читать. Так платформа продолжает с той позиции, на которой остановилась.
Требование к событиям
Каждое событие обязано быть помечено агентом как прошедшее маскирование персональных данных. Событие без такой пометки платформа отклоняет и в базу не пишет.
Спорные случаи
| Метод | Путь | Назначение |
|---|---|---|
GET | /attention-cases | Очередь, по умолчанию открытые |
GET | /attention-cases/{caseId} | Один случай |
GET | /attention-cases/{caseId}/source-file | Исходный файл случая |
POST | /attention-cases/{caseId}/corrections | Исправление полей |
POST | /attention-cases/{caseId}/select-... | Выбор кандидата |
POST | /attention-cases/{caseId}/resume | Продолжить обработку |
POST | /attention-cases/{caseId}/resolve | Завершить вручную |
POST | /attention-cases/{caseId}/replacement-document | Загрузить замену файла |
Все изменяющие обращения обязаны содержать четыре вещи:
| Поле | Зачем |
|---|---|
expected_version | Защита от одновременной правки: если версия не совпала, изменение отклоняется |
actor | Кто выполняет действие |
reason | Обязательная причина, попадает в аудит |
Idempotency-Key | Защита от повторного применения |
Пример обращения:
POST https://agent.internal/agent-api/v1/attention-cases/case-1/select-program
Idempotency-Key: action-key-1
{
"expected_version": 3,
"actor": "eva:user/42",
"reason": "Проверено",
"candidate_id": "p-1"
}Расширение интерфейса, версия 1.1
Позволяет агенту объявить свои разделы, которые платформа нарисует сама.
| Метод | Путь | Особенности |
|---|---|---|
GET | /ui/manifest | Условный запрос по метке, поддерживается ответ «не изменилось», предел 512 КБ |
GET | /ui/views/{viewId}/data | Данные раздела, повторяющиеся параметры для нескольких значений фильтра |
GET | /ui/views/{viewId}/records/{recordId} | Одна запись |
POST | /ui/views/{viewId}/actions/{actionId} | Нажатие объявленной кнопки, нужен ключ идемпотентности |
GET | /ui/views/{viewId}/export | Выгрузка раздела файлом |
При запросе описания разделов платформа сообщает поддерживаемую версию расширения отдельным заголовком.
Ключевое решение по безопасности
Агент объявляет идентификаторы разделов, а не адреса. Путь обращения платформа собирает сама из идентификатора. В коде это прокомментировано прямо: маршрут выводится из идентификатора раздела, поэтому агент никогда не передаёт платформе адрес, по которому ей идти.
Там же отмечено, что в этом слое сознательно нет ни одного упоминания конкретного раздела, столбца или фильтра какого-либо агента.
Выдача модели
| Метод | Путь | Назначение |
|---|---|---|
GET | /llm-binding | Текущая привязка |
PUT | /llm-binding | Установить привязку |
DELETE | /llm-binding | Удалить привязку |
Платформа сообщает агенту адрес своего OpenAI-совместимого шлюза, служебный ключ и разрешённую модель.
Идемпотентность
Правило простое: всякое изменяющее обращение снабжается ключом идемпотентности. Это касается команд, действий над спорными случаями и нажатий объявленных кнопок.
Платформа хранит эти ключи в аудите, поэтому повторная отправка того же действия не приводит к двойному выполнению.
Таймауты и пределы
| Ограничение | Значение | Где задаётся |
|---|---|---|
| Обычное обращение | 15 секунд | EACP_TIMEOUT_MS |
| Выгрузка | 120 секунд | EACP_EXPORT_TIMEOUT_MS |
| Размер описания разделов | 512 КБ | Задано в коде, не настраивается |
| Размер выгрузки | 32 МБ | MANAGED_AGENT_UI_EXPORT_MAX_BYTES |
| Страница списков | 100 записей по умолчанию | Параметр обращения |