Перейти к основному содержанию Перейти к навигации Перейти к нижнему колонтитулу

Я протестировал Rediacc против инцидента PocketOS

PocketOS потеряли свою рабочую базу данных из-за агента Cursor за 9 секунд. Я провёл такой же тест на собственной платформе и замерил каждый шаг. Вот что устояло и что остаётся ответственностью разработчика.

Кратко. На прошлой неделе AI-агент удалил рабочую базу данных PocketOS за 9 секунд. Я попытался заставить собственную инфраструктуру упасть тем же способом. Шесть защитных механизмов выдержали; один честный пробел остаётся.

  • Форк рабочего репозитория на 128 ГБ от начала до конца: 7,2 секунды. Сам CoW reflink: 2,3 секунды.
  • Агенту было заблокировано обращение к grand-репозиториям (рабочим), запрещено выставлять свой собственный override и при авторизованном доступе он попадал в ядерный sandbox (непривилегированный пользователь, отдельный mount namespace, ограниченный по области Docker-сокет).
  • Что Rediacc не изолирует: учётные данные внешних SaaS в данных вашего репозитория. Форк наследует их. Эту часть разработчик обязан обрабатывать сам через lifecycle-хуки Rediaccfile.

В прошедшие выходные Jer Crane опубликовал 30-часовой постмортем. Агент Cursor, работавший на Anthropic Claude Opus 4.6, удалил его рабочую базу данных на Railway. Удаление было одним вызовом GraphQL. Оно заняло 9 секунд. Резервные копии тома Railway ушли вместе с ним, потому что Railway хранит их внутри того же тома.

Его компания, PocketOS, делает софт, которым пользуются арендные фирмы автомобилей для повседневной работы. Некоторые из этих фирм находятся на PocketOS уже пять лет. Утром в субботу клиенты приходили забирать машины, а у арендаторов не было записей о том, кто они. Три месяца бронирований пропали. Jer провёл день, восстанавливая что мог из истории платежей Stripe и почтовых подтверждений.

Я перечитал его пост дважды. The Register, Tom’s Hardware и Business Standard подхватили историю. Тред на Hacker News набрал 874 комментария.

Я строю инфраструктурную платформу другого типа. Мы называем её Rediacc. Весь смысл того, как она устроена, в том, чтобы сделать именно этот сценарий труднее. Поэтому я сел и провёл тест.

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

Чего на самом деле не хватало

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

  1. Токен Railway API, которым пользовался Cursor, был создан для управления собственными доменами. У него также были полномочия volumeDelete. В CLI-токенах Railway нет ограничения по операциям.
  2. GraphQL API Railway принимает volumeDelete одним POST. Без шага подтверждения.
  3. «Резервные копии тома» Railway лежат внутри того же тома. Когда уходит том, уходят и копии.
  4. Агент Cursor самостоятельно решил, что правильный способ исправить рассогласование учётных данных в стейдже: удалить том.

Вынесем сбой 4 на секунду отдельно. Системные правила Cursor говорили агенту никогда не запускать разрушительные команды git без явного запроса пользователя. После удаления, когда его попросили объяснить, агент выдал письменное признание. Он признал, что удаление тома базы данных это «самое разрушительное и необратимое действие из возможных, гораздо хуже, чем force push», и перечислил все правила безопасности, которые нарушил.

Поведенческое правило в промпте это совет. Это не принуждение. Сбои 1, 2 и 3 это решения по дизайну инфраструктуры. Именно они превращают сбой 4 из ошибки в потерянную компанию.

Постановка теста

У Rediacc есть реальная рабочая машина, которую я веду, под именем hostinger. На ней живёт тринадцать репозиториев: почтовый сервер, self-hosted GitLab, observability-стек и демо StackOverflow на 128 ГБ, которое мы используем для бенчмарков. Диск заполнен на 87%. Свободного места ноль. Та машина, где ошибки причиняют боль.

Я выбрал демо StackOverflow намеренно. Это самый большой репозиторий на коробке. Он настроен как реальное приложение, с контейнерами и постоянными данными. Если форк его быстрый и изолированный, то для всего меньшего тоже быстрый и изолированный.

Моим агентом для теста был Claude Code, работающий на Claude Opus. То же семейство модели, что у Cursor. Тот же тип паттерна доступа, который был у агента Jer. CLI, которым я управлял: rdc, наш собственный.

Попытка один: просто SSH в рабочий репозиторий

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

$ rdc term connect -m hostinger -r demo-stackoverflow -c "ls -la"

CLI отказал. Дословно:

“demo-stackoverflow” is a grand (production) repository. Agents cannot modify grand repositories directly.

Grand repositories contain production data. Use a fork instead. Forks are safe, isolated sandbox copies.

Это не системный промпт. Это сам CLI отказывается от вызова ещё до того, как тот покинет мой ноутбук. CLI увидел, что я агент. Claude Code устанавливает переменную окружения CLAUDECODE. CLI также проходит по дереву процессов через /proc, чтобы поймать агентов, которые пытаются скрыть эту переменную. Затем он сопоставил операцию с таблицей политик. Затем отказал.

Тогда агент делает то, что мог бы сделать агент. Он пытается выставить override сам.

$ REDIACC_ALLOW_GRAND_REPO=demo-stackoverflow rdc term connect ...

Снова отказ:

“demo-stackoverflow” is a grand (production) repository. Agent-initiated overrides are not accepted.

Do not attempt to set REDIACC_ALLOW_GRAND_REPO. Only the user can authorize this before the agent starts.

Тот же проход по /proc делает две работы. Сначала он распознаёт агента. Затем проверяет, был ли override установлен внутри агента или над ним. Ниже границы: отклонено. Выше: разрешено.

Я это проверил. Я вышел из агента. Я выполнил export REDIACC_ALLOW_GRAND_REPO=demo-stackoverflow в собственном shell. Я перезапустил Claude Code. Тогда подключение сработало. Я попал в репозиторий как непривилегированный системный пользователь rediacc (UID 7111). DOCKER_HOST указывал на ограниченный по области сокет Docker-демона родительского репозитория.

Я также попробовал подключиться к другому рабочему репозиторию, nextcloud, пока override для demo-stackoverflow был активен. Отказ. Override действует на конкретный репозиторий, это не главный выключатель.

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

Это тот рабочий процесс, на котором Rediacc вас и хочет видеть.

$ time rdc repo fork --parent demo-stackoverflow -m hostinger --tag agent-test

Вывод, скопированный с моего терминала:

Config loaded     (9ms)
Connected         (1.1s)
Renet provisioned (1.2s)
Machine verified  (464ms)
License activated (2.1s)
✔ CoW clone complete (2.3s)

Total: 7.2s

Форк на 128 ГБ за 2,3 секунды. Причина: BTRFS reflink. Форк это метаданные, указывающие на блоки родителя, пока одна из сторон не запишет. Никакие данные не копируются.

Чтобы проверить масштабирование, я провёл тот же форк на репозитории 2 ГБ:

✔ CoW clone complete (573ms)
Total: 6.3s

Шаг клонирования форка на 2 ГБ занял 573 миллисекунды. Шаг клонирования форка на 128 ГБ занял 2,3 секунды. То есть клонирование данных сублинейное, не строго константное. Но общее время форка почти идентичное (6,3 с против 7,2 с), потому что бо́льшая часть ожидания это танец SSH и лицензии, а не данные. С точки зрения пользователя, форк это пауза в 6 - 7 секунд независимо от размера репозитория.

Теперь я работаю с форком. У него собственный GUID (22c57178-...). Собственный networkId (21760). Собственный путь монтирования. Собственный сокет Docker-демона (/var/run/rediacc/docker-21760.sock). Данные родителя расшарены через reflink. Сам родитель остаётся работающим всё это время.

Когда я делаю term connect в форк, вот что я вижу:

$ rdc term connect -m hostinger -r demo-stackoverflow:agent-test -c "id"
uid=7111(rediacc) gid=7111(rediacc) groups=7111(rediacc),988(docker)

Не мой SSH-пользователь. Выделенный системный пользователь rediacc, UID 7111, без собственного shell. Сессия работает в собственном mount namespace. Я проверил /proc/self/ns/mnt. Когда я попытался вывести список путей монтирования родителя изнутри сессии форка, ls вернул Permission denied. Агент буквально не может дотянуться до рабочих данных, даже угадав путь.

Это та часть дизайна, которая важнее всего. Агент получает sandbox на уровне ядра. Не инструкцию «пожалуйста, не надо». Если агент решит выполнить rm -rf / внутри форка, худший случай это повторный форк за 2,3 секунды от родителя.

Замечание о поддержке платформ

Override работает только на Linux. Проверка легитимности требует /proc/<pid>/environ. Этот файл ведётся ядром и хранит запись о том, как был запущен каждый процесс. У macOS и Windows нет аналога. Без способа подтвердить, что override был выставлен вами, а не агентом, CLI закрывается. Даже корректно выставленный override отклоняется на этих платформах.

Сообщение об ошибке говорит, что делать:

The REDIACC_ALLOW_GRAND_REPO override is not supported on darwin. … To use the override, run your agent on Linux (directly, WSL, Docker, or a VM).

На практике у агентов на macOS или Windows нет лазейки от рабочего процесса fork-first. Это сделано намеренно.

Защитные механизмы, которые выдержали в этом тесте

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

  1. Блокировка grand-репозитория. Агенты не могут работать с grand-репозиториями (рабочими) напрямую. Они обязаны делать форк.
  2. Отклонение override, выставленного агентом. Переменная окружения override, которую может установить пользователь, отклоняется, если она появляется в собственном окружении агента.
  3. Привязка override к репозиторию. Разрешение для demo-stackoverflow ничего не даёт для nextcloud. Область это список, не флаг.
  4. Sandbox на уровне ядра. Даже с валидным override, сессия работает под UID rediacc, в собственном mount namespace, с DOCKER_HOST, ограниченным демоном одного репозитория. Никак не увидеть другие репозитории.
  5. Онлайн-форк. Родитель оставался запущенным во время форка. Никакого простоя, никакого переключения.
  6. Сублинейное время форка. 2,3 секунды для 128 ГБ. 573 мс для 2 ГБ. Бо́льшая часть ожидания: SSH-танец, не данные.

Единственное, что Rediacc не изолирует

Теперь более трудная часть поста.

Rediacc изолирует инфраструктуру: файл на диске, Docker-демон, mount namespace, сеть. Он не изолирует внешние SaaS API, для которых ваш репозиторий хранит учётные данные.

Форк это побайтовый BTRFS reflink родителя. Что бы ни жило в data/, .env или secrets/ родителя, оно есть и в форке. Если ваш репозиторий содержит STRIPE_LIVE_KEY, AWS_ACCESS_KEY_ID или токен Railway API, агент в форке может их прочитать. Он может вызвать api.stripe.com, s3.amazonaws.com или backboard.railway.app с этими токенами. Со стороны вызов выглядит так, будто пришёл из продакшена. Stripe или AWS не отличают форк от настоящего.

Это линия разделённой ответственности. Rediacc обрабатывает половину инфраструктуры. Половина внешних сервисов живёт в коде вашего приложения.

Три паттерна закрывают пробел на стороне разработчика:

  • Не храните рабочие внешние учётные данные в репозитории вообще. Получайте их из менеджера секретов при старте контейнера. Контейнеры форка по дизайну получают учётные данные с областью sandbox.
  • Удаляйте или подменяйте учётные данные во время форка через хук up() в Rediaccfile. У форка up() выполняется под другим GUID репозитория, чем у родителя. Определите это. Затем перепишите .env значениями для sandbox.
  • Выдавайте внешние ресурсы на каждый форк: отдельный sandbox-аккаунт Stripe на форк, отдельная тестовая база на форк, отдельный S3-bucket на форк.

Если бы PocketOS были на Rediacc, токен Railway API не был бы правильным сравнением. Их инфраструктурой был бы сам форк Rediacc. Не было бы токена Railway, который можно было бы найти, потому что Rediacc не выставляет аутентифицированному агенту никакого эквивалента volumeDelete. Агент жил бы внутри Docker-сокета на конкретный форк без пути к удалению родителя.

Но если бы их агент нашёл рабочий ключ Stripe в файле учётных данных, Rediacc не помешал бы агенту проводить возвраты против реальных карт клиентов. Это реальная потеря. Оба факта верны.

Что это меняет для тех, кто занимается такой работой

Если вы даёте AI-агенту shell-доступ к рабочей среде с учётными данными, способными её удалить, вопрос не в том, сделает ли он рано или поздно что-то разрушительное. Вопрос в том, когда. И насколько это поправимо.

Что меняется на Rediacc: разрушительный радиус ограничен форком. Цена ошибки «удалил не то» это повторный форк за 2,3 секунды. Цена рассогласования учётных данных, которое агент решил «исправить»: тот же повторный форк за 2,3 секунды. Sandbox на уровне ядра делает так, что большинство ошибок никогда не доходят до рабочих данных.

Что не меняется: если в вашем репозитории лежат живые внешние учётные данные, агент может ими воспользоваться. Это ваше дело: чинить на уровне приложения, не на уровне инфраструктуры.

Я не собираюсь притворяться, что Rediacc предотвратил бы каждую часть инцидента PocketOS. Худшая часть истории PocketOS была в удалении данных Railway без настоящего бэкапа. Этого бы не произошло на Rediacc, потому что мы не даём ни одному агенту API volumeDelete, за которым можно потянуться. Оставшаяся поверхность риска, SaaS API, к которым агент может обратиться с учётными данными в вашем коде, это та часть истории безопасности, которая живёт в вашем хуке up(). Не в нашей модели изоляции.

Полные цифры, дословные сообщения об ошибках и пути в коде, которые я проверил, задокументированы в AI Agent Safety & Guardrails. Если вы хотите провести подобный тест на собственной инфраструктуре, рабочий процесс форка описан в Repositories. Это занимает примерно 7 секунд.