Обычная авторизация оценивает каждый запрос изолированно: решение по нему не зависит от того, что происходило раньше. Для агента, который выстраивает цепочку действий, этого мало — сама последовательность становится объектом управления. Разбираем, как Dogwood добавляет к политикам Cedar условия о недавней истории событийзапись о вызове инструмента агентом или о его результате, которая сохраняется в истории и влияет на последующие решения и как именно вычисляется вердикт.
Зачем нужна темпоральная проверка
Cedar в AgentCore Policy даёт point-in-time авторизациюрешение по каждому запросу в изоляции, без зависимости от прошлых действий. Такой подход очерчивает «safety envelope» вокруг одного действия, но не предназначен для правил о последовательностях действий. Когда агенты составляют несколько действий в более длинные workflow, сама последовательность становится объектом управления.
Dogwood позволяет политикам смотреть назад по недавним событиям агента, а не только на текущий запрос, и добавляет temporal conditionsусловия, ссылающиеся на историю предшествующих событий. Это даёт язык для выражения политик над последовательностями: ограничения на предварительные условия, лимиты частоты и порядок. Чтобы применять такие ограничения, слой политик должен уметь заглядывать за пределы текущего запроса.
Разница видна на уровне синтаксиса. Условие Cedar в клаузе when { ... } видит только текущий authorization request. Dogwood добавляет второй вид клаузы — when temporal { ... } — условие которой может смотреть и на то, что было до запроса.
Как Dogwood встраивается в AgentCore Policy
Dogwood встроен в AgentCore Policy и совместим с существующими Cedar-политиками, поэтому миграция не требуется: текущие политики продолжают работать, а временные условия можно добавлять для расширения существующих и создания новых.
Каждое событие соответствует либо запросу вызова инструмента, либо его результату, и записывает данные вызова — например, входные аргументы и запрашивающего субъекта. Набор вызовов, о которых может говорить политика, — это action schemaсхема действий, генерируемая из инструментов, которые агент уже предоставляет через MCP — протокол, через который агент подключает свои инструменты.
Вердикт ALLOW/DENY вычисляется при обработке потока событий. При воспроизведении потока вердикт меняется по мере изменения недавнего прошлого: запрос без предшествующего одобрения получает DENY; ответ на запрос одобрения не является запросом и не имеет вердикта, но записывается в историю событий и влияет на последующие запросы; запрос с одобрением внутри временного окна получает ALLOW; при одобрении вне окна — снова DENY.
Модель событий и времени
Поток событий представляется последовательностью строк. Каждая начинается с метки времени вида @0, @60, @1700, затем идёт имя события (SellShares::request, ApproveSale::response, Transfer::request, Transfer::response), затем полезная нагрузка в фигурных скобках с полями (stock, shares, approved, amount) и, для запросов, результат проверки -> ALLOW или -> DENY.
Метки времени — числовые отметки, увеличивающиеся от события к событию; в примерах они принимают значения 0, 1, 2, 3, 4, 5, 60, 120, 180, 240, 300, 1700, 1800, 7200. События responseСобытие потока в Dogwood, обозначающее результат вызова инструмента, которое не имеет собственного вердикта ALLOW/DENY, но записывается в историю и влияет на последующие решения. не имеют результата ALLOW/DENY, тогда как request имеют.
Ключевое свойство: все временные условия используют время относительно, измеряя разницу между запросом и более ранними событиями, поэтому абсолютное значение метки не важно.
Оператор formerly
formerly смотрит назад и истинен, если описанное условие выполнялось хотя бы один раз в заданном временном окне. Окно задаётся прямо в операторе, например within 1h — просмотр последнего часа назад.
Условие внутри formerly в примере с продажей акций требует, чтобы ранее произошёл ответ ApproveSale::response, у которого флаг output.approved равен true.
Вердикт меняется по мере появления новых событий. Первый запрос SellShares::request получает DENY, потому что одобрения ещё не было. После ответа ApproveSale::response следующий запрос получает ALLOW, так как одобрение попадает в окно. Более поздний запрос снова получает DENY, потому что одобрение уже вне окна.
Смешивание темпоральных и нетемпоральных условий
Обычное point-in-time условие и темпоральное условие комбинируются внутри одного when-блока через логическое И: сначала проверяется факт о текущем запросе, затем проверяется недавняя история событий. В отличие от отдельной клаузы when temporal { ... }, маркер temporal { ... } является выражением и вставляется прямо в обычное when рядом с Cedar-кодом.
when { context.input.shares <= 100 && temporal { formerly within 1h AgentCore::Action::"ApproveSale"::response{Здесь context.input.shares <= 100 — ровно та Cedar-политика, которая была бы без Dogwood: она смотрит только на текущий запрос. temporal { ... } рядом с ней смотрит назад по недавним событиям. Для авторизации должны выполняться оба условия.
Разберём трассу. Запрос @0 SellShares::request { stock: "AMZN", shares: 100 } получает DENY — в истории ещё нет ни одного одобрения. Событие @1700 ApproveSale::response { stock: "AMZN", shares: 100, approved: true } не является запросом, поэтому у него нет вердикта, но оно записывается в историю и влияет на последующие запросы. Запрос @1800 SellShares::request получает ALLOW: ответ с совпадающими input.stock и input.shares и output.approved: true произошёл в пределах часа до запроса. Запрос @7200 снова получает DENY, так как одобрение вышло за пределы окна.
count_within: подсчёт событий в окне
count_within подсчитывает, сколько раз за указанное окно произошло событие, и сравнивает число с порогом. В примере окно 1h — последний час — и подсчитываются все запросы Transfer::request. Подстановочный шаблон _ в аргументе input.amount означает, что конкретная сумма перевода не важна: учитывается только сам факт запроса.
forbid ( principal, action == AgentCore::Action::"Transfer", resource )
when temporal { count_within(1h, AgentCore::Action::"Transfer"::request{ input.amount: _ }) > 5 };Правило срабатывает на шестом событии: первые пять запросов (@0, @60, @120, @180, @240) получают ALLOW, а @300 Transfer::request { amount: 20 } — DENY, потому что счётчик превышает пять. Окно скользящее: при сдвиге времени из него выпадают старые события, и счёт определяется только событиями за последний час.
count_distinct_within: подсчёт уникальных значений
count_distinct_within отличается от count_within тем, что считает не события, а различные значения привязанной переменной. Функция собирает значения получателя по всем подходящим запросам перевода за окно и подсчитывает, сколько среди них различных. Поэтому повторная оплата тому же получателю считается один раз — получатель уже встречался в окне, и его значение не добавляет новый уникальный элемент, — а новый получатель увеличивает счётчик.
count_distinct_within(u, 1h, AgentCore::Action::"Transfer"::request{ input.user: u }) > 3Политика возвращает DENY, когда число различных получателей за час превышает 3. В примере переводы к bob, carol и dave разрешены, перевод @180 Transfer::request { user: "erin" } отклоняется — это был бы четвёртый различный получатель, хотя сам перевод корректен. Последующий повтор @240 Transfer::request { user: "bob" } тоже остаётся DENY, так как окно уже содержит слишком много различных получателей.
sum_within: накопление суммы в окне
sum_within повторяет count_within, но вместо подсчёта событий суммирует привязанные значения. Поле amount каждого перевода привязывается к переменной a, и эти значения складываются. Окно задаётся вторым аргументом 1h.
sum_within(a, 1h, AgentCore::Action::"Transfer"::request{ input.amount: a }) > 5000Запрет срабатывает, когда сумма превышает $5,000 за последний час, независимо от числа переводов. Вариант с Transfer::response суммирует только завершённые переводы, что позволяет обойти лимит, если запросы ещё не разрешились.
Rate limiting: запросы против ответов
Разница принципиальна для rate limiting. Политика, суммирующая Transfer::request, включает все запросы, в том числе тот, чья авторизация рассматривается, — она видит суммы «в полёте» (in flight), то есть запрошенные, но ещё не завершённые переводы. Политика, суммирующая Transfer::response, включает только завершённые переводы (settledуже завершённые и учтённые переводы).
При конкурентных и асинхронных вызовах агент может обойти лимит, если политика основана на Transfer::response: он отправляет много одновременных запросов до того, как хотя бы один из них завершится.
Вердикты расходятся на третьем Transfer::request (шаг @2): вариант с суммированием Transfer::request даёт DENY, вариант с Transfer::response — ALLOW. В этот момент «в полёте» находится $6,000 суммарно запрошенных средств, тогда как расчётная сумма равна нулю, поскольку событий Transfer::response ещё не было.
Сравнение агрегата с переменной запроса через bind
bind связывает имя с агрегатом, после чего про него можно писать обычное условие. В примере сумма всех settled за последний час получает имя prior:
bind(prior, sum_within(a, 1h, AgentCore::Action::"Transfer"::response{ input.amount: a }), context.input.amount > prior)Агрегат вычисляется по событиям Transfer::response за окно 1h. Затем текущий запрос сравнивается с этим агрегатом: context.input.amount — сумма перевода, который решается сейчас, и условие context.input.amount > prior запрещает именно тот перевод, который превышает все уже settled вместе взятые.
На трассе при settled $1,000 запрос на 500 разрешён (500 ≤ 1,000 settled), запрос на 2000 запрещён (2,000 > 1,000 settled), запрос на 800 разрешён (800 ≤ 1,000 settled). Отклоняется только $2,000 — как непропорциональный недавнему settled-поведению агента.
Конфигурируемость и границы применимости
Примеры используют Dogwood как есть, но почти каждая часть конфигурируема. Можно определять собственные макросыименованные шаблоны для повторяющихся конструкций и строить общую библиотеку сверх стандартной. Можно объявлять более богатую модель событийописание дополнительных видов событий помимо request/response для вызовов инструментов, чтобы выражать свойства о других событиях в окружении агента. Поддерживается определение новых поставщиков информациинебольших изолированных функций, вычисляющих факт, который политика читает inline — совпадение с шаблоном, проверку по денайлисту, вердикт классификатора.
Для старта Dogwood включает генерацию схемы действий прямо из манифеста MCP-инструментов агента — по одному действию на инструмент, на готовом шаблоне, который уже моделирует идентичности, под которыми аутентифицируется агент.
В разделе «Where this is going» заявлены три направления. Более богатые операторы, включая абсолютное время: окна, привязанные к границам настенных часов, а не отсчитываемые назад от текущего момента. Выход за пределы безопасности к livenessсвойствам о том, что должно произойти, с операторами, рассуждающими о будущем так же, как о прошлом — эти концепции уже моделирует MFOTLлогика, позволяющая рассуждать о будущем так же, как о прошлом. Оркестрация для многоагентных систем: свойства об ансамбле — кто кому может передавать управление, какой агент держит блокировку, прогрессирует ли группа в целом. Цель этих планов — повысить выразительность политик, отражая растущий масштаб и автономность агентов.
Как начать
Dogwood распространяется как открытый исходный код под лицензией Apache 2.0. Вместе с эталонным кодом есть сопроводительное руководство по языку, которое проходит по всему языку с практическими примерами. Прямые вклады пока не принимаются, но приветствуется обратная связь по дизайну языка и будущим направлениям; Dogwood уже был показан участникам сообщества Cedar, и их ранние замечания были учтены. План — наращивать открытость итеративно: сначала собрать реакции и обратную связь, затем открыть вклады по мере стабилизации языка и строить управление вместе с сообществом.
Поскольку Dogwood поддерживается внутри AgentCore Policy и совместим с существующими политиками Cedar, начать можно без миграции: продолжать использовать текущие политики и применять временные условия Dogwood для их расширения.
Что из этого следует на практике
- Вердикт зависит от момента. Один и тот же запрос получает ALLOW или DENY в зависимости от того, что попадает в окно. При воспроизведении потока событий вердикт меняется по мере изменения недавнего прошлого, поэтому проверять политику нужно на трассе, а не на отдельном запросе.
- Ответы влияют, но не имеют вердикта. Событие
responseне является запросом, поэтому собственного вердикта у него нет, но оно записывается в историю и меняет решения по последующим запросам. Учитывать это нужно при построении трасс. - Абсолютные метки не важны. Все временные условия измеряют разницу между запросом и более ранними событиями, поэтому значение метки само по себе роли не играет.
- Выбор request или response определяет, обходим ли лимит. Суммирование
Transfer::requestвидит суммы «в полёте» и учитывает незавершённые операции; суммированиеTransfer::response— только settled. На конкурентных и асинхронных вызовах вариант с response позволяет агенту выдать много параллельных запросов до разрешения любого из них и обойти лимит. - Агрегаты можно сравнивать с текущим запросом.
bindдаёт имя агрегату, и дальше с ним работает обычное условие — например, запрет перевода, превышающего все settled вместе. - Уникальность и объём считаются по-разному.
count_withinсчитает события,count_distinct_within— различные значения привязанной переменной,sum_within— сумму привязанных значений. Один и тот же поток даёт разные вердикты в зависимости от выбранного агрегата.