Перейти к содержимому

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

Агенты в Control Center умеют удалять файлы, отправлять ветки, публиковать pull request и вызывать внешние API. Каждое из этих действий — свой вид риска, и долгое время политика была лишь у одного из них — команд оболочки. Ограничения нужны, чтобы единообразно ответить на один вопрос для любого изменяющего действия агента, независимо от того, какой рантайм или инструмент его предлагает: разрешено ли это, требуется ли одобрение или это запрещено?

Движок политик ровно один. К нему обращаются в каждой точке, где действие может покинуть систему: реестр инструментов встроенного раннера, серверный шлюз MCP-диспетчера и изменяющие операции над репозиторием. Поэтому правило, написанное один раз, действует одинаково и для встроенного рантайма, и для внешних CLI-агентов — второго, более тихого обходного пути нет. Старая политика, работавшая только с bash, тоже никуда не делась: правило может сопоставляться с префиксом команды вместо класса действий, и обе формы сосуществуют в одном хранилище.

При этом движок не заменяет песочницу ОС, а песочница не заменяет движок. Они отвечают на разные вопросы: ограничения решают, разрешено ли вообще выполнить вызов, который видит Control Center, а песочница ограничивает, куда может дотянуться порождённый процесс. cc_server применяет песочницу там, где хост предоставляет бэкенд, — но в Windows его нет, а в Linux нужны bwrap и socat. На таких хостах ограничения — самый внешний уровень принуждения, а не политика поверх нижней границы; это стоит учитывать, решая, насколько мягкими делать правила.

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

Класс Встроенное по умолчанию Что охватывает
fileDelete prompt Удаление файла (запись в форме удаления, rm)
gitPush prompt Отправка в удалённый git-репозиторий
prCreate prompt Создание pull request
prPublish prompt Публикация ревью, слияние PR
vendorSyncWrite prompt Запись во внешний трекер тикетов (Linear, Jira, синхронизация GitHub)
fileWriteOutsideWorktree allow Запись файла вне изолированного рабочего дерева
gitCommit allow Создание git-коммита
networkEgress allow Сетевой трафик наружу (загрузка из веба, произвольный HTTP)
secretAccess allow Чтение секрета или учётных данных
packageInstall allow Установка пакета (npm, pip, pub, brew)
processSpawn allow Запуск процесса (оболочка и всё, что вызывает оболочку)
workspaceMutation allow Изменение структуры рабочего пространства (репозитории, пространства, агенты)
enclosureControl allow Управление изолированной средой (rig): загрузка виртуальной машины, отправка ей ввода

Восемь классов, разрешённых по умолчанию, выбирались в расчёте на то, что под ними действует нижняя граница песочницы. Она действует там, где хост предоставляет бэкенд, и не действует в Windows или в Linux без bwrap и socat, — так что если вы хотите, чтобы fileWriteOutsideWorktree или packageInstall там закрывались правилом, это правило придётся написать.

Набор закрыт намеренно. Новый изменяющий инструмент, не объявивший ни одного класса, не проходит тест-храповик, поэтому таксономия может расти только по явному, обоснованному решению — «разрастание таксономии — смерть этой функции». Тринадцати классов хватает, чтобы выразить политики, которые люди действительно пишут (никогда не отправлять без запроса, никогда не удалять вне рабочего дерева, всегда разрешать чтение), и достаточно мало, чтобы интерфейс настроек оставался понятным.

Правило отображает (scope, ActionClass | command prefix) → allow | prompt | deny. Области вложены от самой конкретной к самой общей:

пространство > агент > рабочее пространство > пресет режима > встроенное по умолчанию

Разрешение идёт сначала по конкретности: решает первая область, в которой нашлось подходящее правило, и на этом разрешение останавливается. Наиболее строгий вариант — лишь способ разрешить ничью: между двумя одинаково конкретными правилами внутри одной области и при объединении нескольких классов, объявленных одним действием. Внутри области правило для команды сопоставляется по самому длинному префиксу команды. allow нигде не побеждает deny.

Два решения несут жёсткие гарантии:

  • Запросы с отказом по умолчанию. prompt, когда не подключён ни один одобряющий, разрешается в отказ. Вопрос без ответа — это никогда не «да».
  • Наиболее строгое объединение. Действие, затрагивающее несколько классов, объединяет их по наиболее строгому; если несколько классов дают prompt, одно подтверждение перечисляет их все.

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

Правило может ограничивать АРГУМЕНТЫ действия, а не только сам глагол. «Можно отправлять» и «можно отправлять в feature/*» — разные утверждения, и только второе является контролем: шлюз возможностей, разрешающий один лишь глагол, лежит в основе большинства опубликованных инцидентов с агентами.

Ограничение может называть:

  • пути — glob-шаблоны (repos/**, !**/.env)
  • ссылки — git-ссылки, с отрицанием ! (['**', '!main'] читается как «любая ветка, кроме main»)
  • хосты — сетевые назначения, с подстановочными знаками для поддоменов (*.internal)
  • команды — префиксы команд, сопоставляемые по границе слова
  • потолки — maxCount / maxCents, так что «удалить до 50 файлов» выразимо

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

Три важных правила:

  • Правило с ограничением конкретнее правила без ограничения в той же области, поэтому «запретить отправку в main» побеждает «разрешить отправку» без всяких размышлений о порядке.
  • Строгое правило (deny / prompt) применяется, когда запрос касается любого из названных в нём значений; разрешающее (allow) — только когда запрос целиком укладывается в них. Если читать оба одинаково, агент смог бы «отмыть» запрещённый путь, подмешав к нему безобидный.
  • Правило, называющее аспект, о котором запрос ничего не говорит, не применяется и не пропускается: оно эскалируется к одобрению. Молчаливое пропускание сделало бы любое правило о защищённых ветках бездейственным для инструмента, не сообщающего свою ссылку; молчаливый отказ отвергал бы любую отправку, которую экстрактор не может описать. Если одобряющий не подключён, действующее правило отказа по умолчанию превращает эту эскалацию в отказ.

Что правило для команд может и не может поймать

Заголовок раздела «Что правило для команд может и не может поймать»

Правило commandPrefix сопоставляется по границе слова: git push покрывает git push origin main, но не git pushx. Оно не нормализует оболочку. git push (два пробела), cd sub && git push и GIT_DIR=… git push — все разные строки, и ни одна из них не совпадает с правилом о git push.

Это реальное ограничение, и о нём говорится прямо, а не замалчивается: правила для команд — удобство оператора для формирования обычного поведения агента, а не граница против враждебного. Границы — это классы эффектов (которые инструмент объявляет и не может переписать иначе), командная сетка режима под хранилищем политик и песочница ОС под ними обеими. Правило для команды, разрешающееся в allow, никогда не ослабляет ни то, ни другое: сетка режима применяется после, и её deny окончателен.

Ответ на запрос «запомнить это» записывает настоящее правило политики: с областью (пространство / агент / рабочее пространство), ограниченное по аргументам (одобрение отправки в feature/login даёт feature/**, но не main) и самоотзывающееся по явному TTL. Сказать «да» на ближайшие часы — не то же самое, что тихо переписать свою постоянную политику; записанные правила попадают в то же хранилище с происхождением remembered и сроком действия, поэтому их можно отозвать досрочно.

Каждое правило несёт один из них:

  • advisory — разрешить, но зафиксировать, что правило сработало. Это путь внедрения: включите строгое правило в режиме advisory, неделю читайте журнал аудита, чтобы точно увидеть, что оно заблокировало бы, а затем повысьте его. Без этого уровня строгие политики так и не включают.
  • soft — запретить, с возможностью переопределения тем, у кого есть право на переопределение, и с записанным обоснованием. Само переопределение — тоже событие в журнале аудита.
  • hard (по умолчанию) — запретить. Ничто его не переопределяет.

Оператор установки может закрепить политику, которую не ослабит ни один администратор рабочего пространства. Управляемые правила живут на сервере и объединяются с собственной цепочкой каждого рабочего пространства по наиболее строгому принципу — они не являются областью во главе этой цепочки, потому что управляемый allow во главе цепочки позволил бы установке переопределить запрет рабочего пространства, то есть ровно противоположное тому, для чего нужен ограничитель. Поэтому управляемое правило может только ужесточать.

Если задать CC_SERVER_MANAGED_POLICY с JSON-файлом, он полностью перекрывает сохранённые правила — именно это позволяет оператору закрепить позицию, которую не переключит ни один админский интерфейс. Это ответ на вопрос «могу ли я запретить своим разработчикам отключать средства безопасности?».

Регулятор автономии — это профиль, а не параллельная система

Заголовок раздела «Регулятор автономии — это профиль, а не параллельная система»

Регулятор автономии для каждого пространства — это именованный профиль поверх того же хранилища политик, а не второй механизм. Его три настройки хранятся и передаются как proposeOnly, actWithApproval и actFreely (формы через дефис — только текст интерфейса, и autonomy.setForSpace отвергает всё остальное). Если его не задавать, поведение соответствует actWithApproval.

Что каждый из них делает с уже разрешённым решением:

  • proposeOnly запрещает напрямую каждый инструмент, закрытый шлюзом. Агент по-прежнему может рассуждать и отвечать; он просто не может действовать, и в его сообщении сказано почему.
  • actWithApproval (и незаданное значение) — это шлюз одобрения с отказом по умолчанию, описанный выше.
  • actFreely заранее одобряет всё, что не разрешилось в жёсткий deny.

Последний пункт стоит сказать прямо, без смягчений: при actFreely решение prompt не эскалируется к одобряющему — оно разрешается, и никого не спрашивают. Регулятор переживает только явное правило deny. Так что actFreely — это осознанное предоставление автономии, а не настройка для удобства, и правило отказа по умолчанию защищает ровно тот случай, где оно важно (actWithApproval, значение по умолчанию).

Делегирование между агентами охраняется в точке перехвата delegate_task всеми четырьмя защитами: ограничение глубины (по умолчанию 3), обнаружение циклов, потолок автономии (делегат никогда не может действовать с большей автономией, чем его делегатор, — привилегию нельзя «отмыть», передав работу более свободному агенту) и рамки бюджета (делегирование списывает остаток бюджета делегатора и не может создать новый). Каждый отказ происходит громко, а причина от защиты возвращается агенту дословно.

Не всякий рантайм позволяет Control Center перехватывать каждое действие. Внешние CLI-агенты выполняют часть инструментов нативно, внутри процесса, куда никакой шлюз не поставить. Вместо того чтобы делать вид, что это не так, каждый адаптер несёт матрицу честности: для каждого адаптера — фильтрует ли Control Center поверхность инструментов, перехватывает ли вызовы инструментов, соблюдается ли контракт завершения, видит ли он нативные инструменты раннера и покрыты ли внутрипроцессные инструменты профилем песочницы.

Две записи стоит прочитать, прежде чем доверять какой-либо конфигурации. Встроенный раннер заявляет, что его внутрипроцессные файловые инструменты не изолированы песочницей — поверхность инструментов и этот движок политик остаются единственной границей файловой системы для него. Claude Code заявляет, что его собственные вызовы инструментов не поддаются перехвату, поскольку он запускается с --dangerously-skip-permissions; Control Center видит его вызовы mcp__* и, через хук, его командные строки Bash, а его собственные инструменты чтения, записи и редактирования работают незаметно.

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

Ограничения находятся в разделе Настройки → Рабочее пространство → Разрешения агента (/workspaces/<id>/settings/workspace/permissions). На этом экране есть матрица политики, где пишутся правила, шаблоны политик, проба «Что если?», журнал аудита авторизации и матрица честности адаптеров.

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