Перейти к содержанию

Конфигурация

agate-server (точка входа в Docker) настраивается одним файлом agate.toml со слоем переопределений из окружения. Смонтируйте файл в контейнер и укажите на него AGATE_CONFIG. Готовый шаблон — agate.example.toml.

Источники и приоритет

От низшего к высшему — каждый следующий слой переопределяет предыдущий:

  1. Встроенные значения по умолчанию.
  2. agate.toml — путь из AGATE_CONFIG (по умолчанию /etc/agate/agate.toml). Отсутствие файла допустимо — применяются значения по умолчанию.
  3. ОкружениеAGATE__SECTION__KEY (в верхнем регистре, __ между уровнями). Секреты предпочтительно задавать через окружение.

То есть [audit].database_url переопределяется через AGATE__AUDIT__DATABASE_URL.

docker run --rm \
  -v "$PWD/agate.toml:/etc/agate/agate.toml:ro" \
  -e AGATE_CONFIG=/etc/agate/agate.toml \
  -e AGATE__AUDIT__DATABASE_URL='postgres://agate:secret@db:5432/agate' \
  ghcr.io/c3equalzz/agate

Отсутствие обязательного значения (proxy.agent_endpoint, audit.database_url) прерывает запуск — отказ при неверной конфигурации вместо работы в деградированном режиме.

[proxy]

Ключ Обязателен По умолчанию Значение
agent_endpoint да URL вышестоящего AG-UI-агента, которому Agate пересылает проинспектированный трафик.
bind нет 0.0.0.0:8080 Адрес/порт, который Agate слушает для входящего AG-UI-трафика.
connect_timeout_secs нет 5 Быстрый таймаут подключения к вышестоящему агенту.
read_timeout_secs нет 60 Таймаут простоя между чанками SSE-ответа. Не общий дедлайн — здоровый поток работает дальше.
max_body_bytes нет 1048576 Максимальный размер тела запроса (1 МиБ). Слишком большие запросы получают 413.
max_concurrent_requests нет 256 Максимум одновременно обрабатываемых прогонов. Каждый держит соединение с агентом на всё время потока; запросы сверх лимита отбрасываются с 503 (не ставятся в очередь), чтобы наплыв не исчерпал память/соединения.
max_response_events нет 100000 Лимит на число событий ответа, отдаваемых клиенту за прогон. Агент, превысивший его, отрезается с RUN_ERROR. 0 = без лимита — это потоковый счётчик, а не буфер, поэтому 0 отключает guard против флуда клиента (availability), а не ограничение памяти (память на run ограничена max_frame_bytes).
max_frame_bytes нет 1048576 Максимум байт, буферизуемых для одного ещё-не-завершённого SSE-события. Учитывается, пока кадр ещё принимается (лимиты на run выше считают только завершённые события), поэтому upstream, отправляющий кадр без терминатора, не может неограниченно растить память. Превышение завершает run через RUN_ERROR. Должно быть > 0 — 0 вернул бы неограниченное поведение. Корректное AG-UI событие — несколько КиБ.
max_response_bytes нет 67108864 Лимит на число байт ответа, отдаваемых клиенту за прогон (64 МиБ). 0 = без лимита. Как и max_response_events, это потоковый счётчик, а не буфер — 0 это availability-trade-off, а не ограничение памяти.
rate_limit_per_second нет 0 Постоянная частота запросов на один IP клиента. Превышение отбрасывается с 429 Too Many Requests и подсказкой Retry-After. 0 = выключено (по умолчанию). IP берётся из пира соединения, поэтому включайте только там, где Agate видит реального клиента. За обратным прокси (nginx) или балансировщиком все запросы идут с одного IP прокси — тогда лимит зарежет всех клиентов как одного и начнёт отклонять легитимный трафик, поэтому оставьте 0 и ограничивайте частоту на самом прокси.
rate_limit_burst нет 0 Глубина всплеска для лимита на IP — наибольший мгновенный всплеск до выхода на постоянную частоту. 0 означает откат к rate_limit_per_second.
api_key нет Одиночный API-ключ в заголовке X-API-Key (сокращение, объединяется с api_keys). Секрет лучше через AGATE__PROXY__API_KEY.
api_keys нет [] Принятые API-ключи; запрос аутентифицирован, если совпал с любым (иначе 401). Несколько ключей одновременно дают ротацию без даунтайма: добавить новый, перевести клиентов, убрать старый. Если api_key и api_keys пусты — прокси открыт.

Пробы liveness и readiness

/healthz (liveness) возвращает 200, пока процесс жив. /readyz (readiness) возвращает 200 только когда хранилище журнала прозрачности доступно, иначе 503 — направьте на него readiness-пробу оркестратора, чтобы трафик удерживался, пока Agate не сможет записывать. Проверка доступности скрыта за портом HealthCheck, поэтому остаётся корректной при смене бэкенда хранилища. Обе пробы обходят проверки API-ключа и размера тела.

[audit]

Ключ Обязателен По умолчанию Значение
backend нет postgres Какой бэкенд хранилища собирается при старте. Сейчас postgres; другие бэкенды подключаются через Cargo-фичи.
database_url да Строка подключения PostgreSQL для журнала прозрачности (для backend = "postgres"). Миграции выполняются при старте. Пароль лучше задавать через AGATE__AUDIT__DATABASE_URL.
max_connections нет 10 Максимум подключений в пуле к базе.
acquire_timeout_secs нет 30 Сколько ждать свободного подключения из пула до ошибки.
connect_max_retries нет 10 Сколько раз повторить начальное подключение перед отказом старта (0 = одна попытка). Позволяет переждать БД, которая ещё поднимается рядом с Agate (compose/Kubernetes), вместо падения на первой неудаче.
connect_backoff_secs нет 1 Базовый бэкофф между попытками подключения (удваивается каждый повтор, с потолком).
checkpoint_interval_secs нет 0 Как часто фоновая задача выпускает подписанный чекпоинт (STH) для журнала, в секундах (0 = выключено). Собственная каденция доказуемости журнала (как частота STH в CT-логе); неизменное между тиками дерево подписывается, но повторно не якорится. Требует ключ подписи (ниже).
checkpoint_key_id нет checkpoint-ed25519 Идентификатор ключа подписи, который запрашивает периодический издатель; должен совпадать с загруженным в хранилище (AUDIT_CHECKPOINT_KEY_ID).
outbox_capacity нет 1024 Сколько инспектированных записей может стоять в очереди в журнал прозрачности до заполнения outbox. Ограничено, чтобы медленная БД не растила память без предела.
outbox_on_full нет block Что делает прокси при полном outbox: block (backpressure — медленная БД замедляет прокси, но запись не теряется; по умолчанию) или shed (отбросить запись, с громким логом и счётчиком, чтобы прокси продолжал работать ценой пробела в журнале).

Ключ подписи чекпоинтов

Ed25519-ключ задаётся только через окружение, никогда не в файле конфигурации:

  • AUDIT_CHECKPOINT_SEED — 32-байтовый seed как 64 hex-символа. Без него подпись чекпоинтов выключена (периодическому интервалу нечего подписывать).
  • AUDIT_CHECKPOINT_KEY_ID — необязательный id ключа (по умолчанию checkpoint-ed25519); держите его равным [audit].checkpoint_key_id.

Журнал, в который добавляются записи, закрепляется переменной окружения AUDIT_LOG_ID (UUID). Если она не задана, при старте создаётся новый журнал и его id печатается, чтобы закрепить его при следующем запуске.

[policy.tools] и [policy]

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

Ключ Формат Значение
[policy.tools].mode allow-all | allowlist | denylist Как авторизуются вызовы инструментов. По умолчанию allow-all.
[policy.tools].names массив сопоставителей инструментов Инструменты, управляемые mode (игнорируются при allow-all). Каждая запись сопоставляется с полным именем инструмента, с учётом регистра, по виду: голое имя — точное (search); glob: — shell-стиль */? (glob:fs.*); regex: — регулярное выражение, заякоренное на всё имя (regex:db_.*). Поэтому search никогда не совпадёт с research, а glob:fs.* покрывает все инструменты fs.. Неверный glob/regex прерывает запуск.
[[policy.tools.deny_arguments]] таблицы { tool?, path?, contains \| matches } Правила запрета по аргументам: разрешённый вызов инструмента блокируется, если его аргументы совпадают. Каждое правило задаёт ровно одно из contains (литерал без учёта регистра) или matches (regex). tool ограничивает правило одним инструментом; опустите его — правило применится к любому. path (точечный путь вроде url или config.endpoint) ограничивает совпадение одним полем разобранных аргументов, чтобы оно не срабатывало на другом поле с тем же текстом; опустите — совпадение по всей сырой строке аргументов. Правило с path не срабатывает, если аргументы не валидный JSON или путь отсутствует.
[[policy.tools.deny_results]] таблицы { tool?, path?, contains \| matches } Правила запрета по результатам: результат инструмента блокируется (отбрасывается до клиента), если его содержимое совпадает — зеркало deny_arguments для возвращаемого. Та же форма tool / path / contains / matches. tool срабатывает только когда имя инструмента результата известно (сопоставлено по началу вызова) и совпадает.
[policy].redact массив литеральных маркеров Подстроки, маскируемые (без учёта регистра) в исходящем тексте и результатах инструментов до их доставки клиенту.
[policy].redact_regex массив regex-паттернов Regex-маркеры, маскируемые в исходящем тексте и результатах инструментов (полный синтаксис regex; префикс (?i) для регистронезависимости). Невалидное выражение прерывает запуск.
[policy].fail_mode open | closed Что делать при таймауте решения политики: переслать (open) или заблокировать (closed). По умолчанию closed (безопасность важнее доступности).
[policy].decision_timeout_ms целое (мс) Дедлайн одного решения политики. По умолчанию 5000; должен быть > 0.
[policy].on_malformed_event forward | drop | terminate Что делать с распознанным, но битым событием ответа (известный тип без обязательного поля), которое нельзя инспектировать. forward пересылает сырой фрейм, drop отбрасывает его, terminate завершает прогон. По умолчанию terminate (оно не должно обходить политику).
[policy.session_memory].enabled bool Память реплея между прогонами: если инструмент запрещён в одном прогоне, отказывать ему (по имени) до конца сессии, чтобы агент не повторил его с другими аргументами в следующем прогоне. Эшелонированная защита поверх stateless-политики. По умолчанию false.
[policy.session_memory].ttl_secs целое (с) Сколько карантин сессии живёт без активности. Сессия, простаивающая дольше, забывается. По умолчанию 3600; должно быть > 0 при включении.
[policy.session_memory].backend memory | redis Где хранится ledger. memory — в пределах процесса (теряется при рестарте, не шарится между репликами); redis — общий для реплик и переживает рестарт. По умолчанию memory. Redis-backend fail-open: если Redis недоступен, деградирует до «нет памяти», никогда не wrong-allow.
[policy.session_memory].redis_url строка URL подключения к Redis (например, redis://127.0.0.1:6379). Обязателен при backend = "redis", иначе игнорируется.
[policy].backend ruleset | cel | rego Какой движок принимает вердикты. ruleset (по умолчанию) — встроенная статическая политика, описанная выше. cel и rego передают каждое решение политике оператора (из [policy.cel] / [policy.rego]) — статические правила выше тогда не учитываются. Выбор cel / rego требует сборки с Cargo-feature policy-cel / policy-rego.
[policy.cel].policy_path строка Путь к файлу CEL-политики (TOML-список записей [[rule]]; см. ниже). Обязателен при backend = "cel". Каждое правило компилируется при старте, поэтому ошибка разбора прерывает процесс.
[policy.cel].watch bool Авто-перезагрузка файла политики при его изменении на диске, поверх всегда-включённого SIGHUP. По умолчанию false (opt-in — слежение опирается на inotify/FSEvents платформы и может не срабатывать на некоторых сетевых ФС). Перезагрузка по watch — та же fail-safe перезагрузка, что и по SIGHUP.
[policy.cel].max_rules целое (> 0) Максимум записей [[rule]] в CEL-политике. По умолчанию 1000. Вычисление синхронно и линейно по числу правил, поэтому это ограничивает стоимость одного решения; политика с бо́льшим числом правил отклоняется при загрузке и при перезагрузке (работающая политика сохраняется). По умолчанию щедро — у легитимных политик десятки–сотни правил.
[policy.rego].policy_path строка Путь к файлу Rego (OPA) политики (см. ниже). Обязателен при backend = "rego". Компилируется при старте, поэтому ошибка разбора прерывает процесс.
[policy.rego].watch bool Авто-перезагрузка Rego-политики при изменении файла, поверх SIGHUP. По умолчанию false; та же fail-safe семантика, что и у CEL watch.

Неверная политика прерывает запуск

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

Движок политик CEL (backend = "cel")

Статический набор правил выше покрывает частые случаи декларативно. Для политик, которым нужны выражения — сравнения, булева логика, адресация вложенных полей, — Agate поставляет альтернативный движок, вычисляющий правила на CEL (Common Expression Language). CEL не полон по Тьюрингу (нет циклов и рекурсии), поэтому любое выражение завершается. Вычисление синхронно и выполняется inline на каждое событие, поэтому ограничитель decision_timeout_ms не прерывает CEL-решение в процессе (он ограничивает лишь backend'ы, ждущие чего-то внешнего); стоимость одного решения ограничивается вместо этого через max_rules. Это отдельный backend PolicyPort, выбираемый через [policy].backend = "cel", доступный только в сборке с Cargo-feature policy-cel (cargo build -p agate-server --features policy-cel).

Файл политики — TOML-список таблиц [[rule]], вычисляемых по порядку; побеждает первое правило, чьё when истинно. Если ни одно правило не совпало, событие разрешается — правила перечисляют, что блокируется или редактируется, поэтому для режима default-deny добавьте замыкающее правило-запрет when = "true".

Поле Обязательно Значение
when да CEL-булево выражение над action и context (ниже).
effect да deny (блок с reason), redact (заменить текст события) или allow (пропустить и прекратить вычисление).
reason нет Сообщение запрета (для effect = "deny"). По умолчанию — общая причина.
replacement нет CEL-строковое выражение, дающее текст замены (для effect = "redact"). Применяется только к сообщениям и результатам инструментов; redact-правило, совпавшее с любым другим видом события, падает в закрытую сторону (событие запрещается, а не пропускается). Выражение видит весь action, поэтому не возвращайте найденный секрет назад (replacement = 'action.text' замаскировал бы его сам в себя). Если оно падает или даёт не-строку, используется "[REDACTED]" (логируется). По умолчанию "[REDACTED]".

Каждое правило видит две переменные. action — плоская карта, описывающая событие; каждый ключ всегда присутствует (null, если неприменим), поэтому правило может называть любое поле, не падая на отсутствующем ключе:

Поле action Присутствует для Значение
kind каждого события "tool_call", "message", "tool_result", "state" или "other".
name вызовов и результатов инструментов Имя инструмента.
arguments вызовов инструментов Сырая строка аргументов.
arguments_json вызовов инструментов Аргументы, разобранные как JSON (адресация полей: action.arguments_json.url), или null, если это не валидный JSON.
text сообщений Текст исходящего сообщения.
content результатов инструментов Сырое содержимое результата.
content_json результатов инструментов Результат, разобранный как JSON, или null.
state_json мутаций состояния Полезная нагрузка состояния, разобранная как JSON, или null.

context несёт идентичность прогона: context.session_id и context.run_id (обе строки).

# cel-policy.toml — на него ссылается [policy.cel].policy_path

# Блокировать инструмент по имени.
[[rule]]
when = 'action.kind == "tool_call" && action.name == "delete_file"'
effect = "deny"
reason = "destructive tool is not permitted"

# Блокировать SSRF-подобный аргумент, адресуя разобранное поле. Сначала охраните
# nullable-поле: без `!= null` не-JSON аргумент заставит правило упасть и быть
# пропущенным (см. примечание про null-guard ниже), а не заблокировать.
[[rule]]
when = 'action.arguments_json != null && action.arguments_json.url.startsWith("http://169.254.169.254")'
effect = "deny"
reason = "link-local metadata endpoint"

# Заредактировать форму API-ключа в исходящем тексте сообщения.
[[rule]]
when = 'action.kind == "message" && action.text.contains("sk-")'
effect = "redact"
replacement = '"[REDACTED]"'

# Default-deny: всё, что явно не разрешено выше, блокируется.
[[rule]]
when = "true"
effect = "deny"
reason = "not permitted by policy"

Охраняйте nullable-поля — упавшее правило не блокирует

Правило, которое падает при вычислении (например, лезет в action.arguments_json.url, когда аргументы не JSON и arguments_json равно null), считается не совпавшим — логируется и пропускается — но никогда не приводит к жёсткому отказу. Поэтому само по себе оно не блокирует: если ни одно последующее правило не совпало, событие разрешается. Охраняйте nullable-поля (action.arguments_json != null && action.arguments_json.url...) и, где нужен default-deny, завершайте файл правилом-запретом when = "true".

Горячая перезагрузка (SIGHUP)

Пошлите процессу SIGHUP, чтобы перечитать и перекомпилировать файл политики без рестарта (kill -HUP <pid> или docker kill --signal=HUP <container>). Новый набор правил подменяется атомарно — решение, уже выполняющееся, сохраняет правила, с которыми началось, и на пути запроса нет блокировок.

Перезагрузка fail-safe: если файл отсутствует, не парсится или какое-то правило не компилируется, работающая политика сохраняется (ошибка перезагрузки логируется, остаются прежние известно-хорошие правила) — плохая правка никогда не оставит шлюз без политики. Перезагрузка, дающая ноль правил (пустой или обрезанный файл — например, неатомарная запись, пойманная на лету), тоже отклоняется, ведь отсутствие правил означает allow-all; при правке вживую предпочитайте атомарную запись (во временный файл, затем rename). Перезагрузка по SIGHUP только под Unix; на других платформах политика фиксируется при старте.

Задайте [policy.cel].watch = true, чтобы также перезагружать автоматически при любом изменении файла на диске (кросс-платформенно, opt-in). Слежение идёт за директорией файла — поэтому переживает атомарную запись temp-затем-rename, которой пользуются редакторы, — и схлопывает пачку событий одного сохранения в одну перезагрузку. Сама перезагрузка — тот же fail-safe путь, поэтому временный пустой/недописанный файл отклоняется, а работающая политика сохраняется.

Движок политик Rego (backend = "rego")

Командам, уже вложившимся в Open Policy Agent, Agate может принимать решения на Rego — языке политик OPA, — вычисляемом pure-Rust интерпретатором regorus (без sidecar, без WASM-рантайма). Как и CEL, он не полон по Тьюрингу, поэтому вычисление всегда завершается; это отдельный backend PolicyPort, выбираемый через [policy].backend = "rego", доступный только в сборке с Cargo-feature policy-rego (cargo build -p agate-server --features policy-rego).

Файл политики — Rego-исходник в пакете agate.policy, определяющий правило decision. Для каждого события Agate задаёт input = { "action": …, "context": … } (та же проекция, что видит CEL-движок — input.action.kind, input.action.name, input.action.arguments_json.…, input.context.run_id и т.д.) и вычисляет data.agate.policy.decision, ожидая объект с effect из allow / deny / redact (плюс опциональные reason / replacement):

# rego-policy.rego — на него ссылается [policy.rego].policy_path
package agate.policy

import rego.v1

# Блокировать инструмент по имени.
decision := {"effect": "deny", "reason": "destructive tool"} if {
    input.action.kind == "tool_call"
    input.action.name == "delete_file"
}

# Блокировать SSRF-подобный аргумент (сначала охраните nullable разобранное поле).
decision := {"effect": "deny", "reason": "metadata endpoint"} if {
    input.action.arguments_json.url
    startswith(input.action.arguments_json.url, "http://169.254.169.254")
}

# Заредактировать форму API-ключа в исходящем тексте сообщения.
decision := {"effect": "redact", "replacement": "[REDACTED]"} if {
    input.action.kind == "message"
    contains(input.action.text, "sk-")
}

Если decision не определён (ни одно правило не совпало), событие разрешается — правила оператора перечисляют, что блокируется; для default-deny добавьте замыкающий decision. Ошибка вычисления или некорректное решение (не объект, либо effect отсутствует/неизвестен) падает в закрытую сторону (deny) и логируется — сломанная политика никогда не разрешает молча. Редакция следует тому же правилу, что и в CEL: применяется к сообщениям и результатам инструментов, а redact-решение на любом другом виде события падает в закрытую сторону. Горячая перезагрузка (SIGHUP) и [policy.rego].watch работают точно как у CEL выше.

[observability.logging]

Ключ По умолчанию Значение
enabled true Устанавливать ли подписчик логов вообще; false отключает логи.
format pretty pretty (консоль) или json (по объекту на строку, для сборщиков логов).
level info Директива фильтра (например, agate_proxy=debug,info). RUST_LOG переопределяет её, если задана.

На info видны события жизненного цикла: старт, каждый проксированный прогон, запреты и редакции политики, создание журнала прозрачности. Поднимите до debug (например, level = "agate_proxy=debug,info") для детализации по каждому событию (каждое переданное/буферизованное событие, каждая добавленная запись аудита).

[observability.metrics]

Эндпоинт для Prometheus на отдельном порту, не на публичном data-plane порту (скрейпится из внутренней сети).

Ключ По умолчанию Значение
enabled false Устанавливать ли recorder + экспортёр метрик. Если выключено — метрики no-op.
exporter prometheus prometheus (эндпоинт /metrics) или none.
bind 0.0.0.0:9090 Адрес, который слушает эндпоинт /metrics.

Экспортируемые метрики:

  • agate_runs_total — проксированных прогонов.
  • agate_events_inspected_total{outcome="forward|buffer|transform|deny|terminate"} — проинспектированные события по исходу (разбивка по вердиктам).
  • agate_upstream_errors_total{kind="connect|timeout|status|stream"} — ошибки запроса/потока к вышестоящему агенту по видам.
  • agate_audit_records_appended_total / agate_audit_records_dropped_total — записи в журнал прозрачности против дропов (ненулевой drop-rate = аудит не успевает, ставьте алерт).
  • agate_audit_outbox_depth / agate_audit_outbox_capacity — gauge'и заполненности outbox; depth у capacity = растёт backpressure (прокси вот-вот замедлится при block или начнёт шедить при shed). Алерт до насыщения.

Готовый стек Prometheus + Grafana с преднастроенным дашбордом — в deploy/observability/.

[observability.tracing]

Экспорт трейсов по OTLP — третий столп наблюдаемости наряду с логами и метриками. Когда трейсинг выключен, спаны всё равно создаются, но не экспортируются; в логах они видны, только если [observability.logging].enabled = true (подписчик, который и рендерит спаны в логи, и экспортирует их, поднимается лишь тогда).

Ключ По умолчанию Значение
enabled false Экспортировать спаны в OTLP-коллектор по gRPC.
endpoint http://localhost:4317 OTLP gRPC-эндпоинт коллектора.
service_name agate-server service.name в экспортируемых спанах.

Спаны покрывают путь запроса от края до края:

  • proxy_run — по одному на каждый проксированный прогон на data-plane.
  • audit.request — по одному на каждую отправленную команду/запрос аудита. TracingBehavior оборачивает весь конвейер медиатора (самым внешним звеном, поверх behaviour'ов метрик и транзакции), поэтому каждый use case трассируется единообразно — новые use case'ы получают спан автоматически.
  • db.log.load / db.log.save / db.proof.inclusion / db.proof.consistency — по одному на каждый SQL-оператор, вложенному в спан audit.request, который его инициировал.

Когда трейсинг включён (и [observability.logging].enabled = true, чтобы подписчик был установлен), спаны сбрасываются при graceful shutdown. Укажите endpoint на OpenTelemetry Collector (или любой OTLP/gRPC-бэкенд), чтобы собирать трейсы по каждому прогону.

[tls]

Терминация TLS на собственном слушателе Agate. По умолчанию выключено — тогда Agate отдаёт обычный HTTP, что разумно только за TLS-терминирующим шлюзом (ingress, балансировщик, service mesh). Включите, чтобы отдавать HTTPS напрямую (например, в zero-trust-развёртывании без отдельного терминатора).

Ключ По умолчанию Значение
enabled false Отдавать HTTPS вместо обычного HTTP. При false cert/key игнорируются.
cert Путь к PEM-цепочке сертификатов (листовой сертификат первым). Обязателен при включении.
key Путь к PEM-приватному ключу для cert. Обязателен при включении.

При включении отсутствие или некорректность cert/key прерывает запуск (отказ при неверно настроенном слушателе). Тогда и пробы, и data-plane отдаются по одному TLS-слушателю на proxy.bind.

Полный пример

[proxy]
agent_endpoint = "http://agent:8000/run"
bind = "0.0.0.0:8080"
connect_timeout_secs = 5
read_timeout_secs = 60
max_body_bytes = 1048576
max_concurrent_requests = 256
max_response_events = 100000
max_response_bytes = 67108864
max_frame_bytes = 1048576        # макс. байт на одно незавершённое SSE-событие
rate_limit_per_second = 0        # лимит запросов на IP клиента; 0 = выключено
rate_limit_burst = 0             # глубина всплеска; 0 = откат к частоте
# api_key = "change-me"          # один ключ; секрет лучше через AGATE__PROXY__API_KEY
# api_keys = ["current", "next"] # несколько ключей для ротации без даунтайма

[audit]
backend = "postgres"
# Пароль лучше задавать через AGATE__AUDIT__DATABASE_URL.
database_url = "postgres://agate@postgres:5432/agate"
max_connections = 10
acquire_timeout_secs = 30
connect_max_retries = 10
connect_backoff_secs = 1
checkpoint_interval_secs = 0   # 0 = выключено; включить вместе с AUDIT_CHECKPOINT_SEED
checkpoint_key_id = "checkpoint-ed25519"
outbox_capacity = 1024
outbox_on_full = "block"   # block (backpressure) | shed (drop + alert)

[policy.tools]
mode = "allowlist"
names = ["search", "fetch", "glob:fs.*", "regex:db_.*"]

[[policy.tools.deny_arguments]]
tool = "search"
contains = "rm -rf"
[[policy.tools.deny_arguments]]
tool = "fetch"
path = "url"
matches = "^https?://169\\.254\\.169\\.254"
[[policy.tools.deny_results]]
contains = "BEGIN RSA PRIVATE KEY"

[policy]
backend = "ruleset"              # или "cel" / "rego" (нужна соответствующая сборка); см. ниже
redact = ["sk-", "AKIA"]
redact_regex = ["sk-[A-Za-z0-9]{20,}", "AKIA[0-9A-Z]{16}"]
fail_mode = "closed"
decision_timeout_ms = 5000
on_malformed_event = "terminate"

# [policy.cel]
# policy_path = "/etc/agate/cel-policy.toml"   # обязателен при backend = "cel"
# watch = false                                # авто-reload при изменении файла

# [policy.rego]
# policy_path = "/etc/agate/rego-policy.rego"  # обязателен при backend = "rego"
# watch = false                                # авто-reload при изменении файла

[policy.session_memory]
enabled = false
ttl_secs = 3600
backend = "memory"
# redis_url = "redis://127.0.0.1:6379"  # обязателен при backend = "redis"

[observability.logging]
enabled = true
format = "pretty"
level = "info"

[observability.metrics]
enabled = true
exporter = "prometheus"
bind = "0.0.0.0:9090"

[observability.tracing]
enabled = false
endpoint = "http://localhost:4317"
service_name = "agate-server"

[tls]
enabled = false
cert = "/etc/agate/tls/cert.pem"
key = "/etc/agate/tls/key.pem"