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

OpenAI

Память Codex унесла чат локального провайдера в OpenAI

7 мин чтения

Текст написан ИИ-моделью по источникам, на которые ведут ссылки, и сверен с ними второй моделью. Как мы работаем

Всё о теме: Агенты для программирования →
Изображение: github.com

Codex отправил на chatgpt.com/backend-api/codex/responses WebSocket-кадр response.create размером 38 095 байт с полем request_kind = "memory". Внутри был прошлый диалог, записанный под другим, не OpenAI провайдером. Такую картину показал захват трафика, который автор отчёта в репозитории Codex на GitHub опубликовал 30 августа. По его версии, фоновая генерация памяти берёт старые сессии без оглядки на то, через какого провайдера они шли, и отправляет их тому провайдеру, который активен в текущей сессии.

Проверял он стандартный бинарник Codex для Windows 0.150.0-alpha.12.2. Автор приводит SHA-256 и сверяет бинарник с публичным тегом rust-v0.150.0-alpha.12.2. В изолированной конфигурации память была включена, аналитика выключена через [analytics] enabled = false, а все экспортёры OpenTelemetry выставлены в none. Исходную сессию он подписал синтетическим провайдером synthetic_source, а память затем запустил из сессии с аккаунтом OpenAI. Автор подчёркивает, что перехваченный запрос относится к инференсу модели, а не к аналитике или телеметрии.

Что оказалось в запросе

Запрос состоял из трёх элементов верхнего уровня. Первый - developer-элемент с дополнительными инструментами. Второй - developer-сообщение с инструкциями для памяти длиной 31 000 символов. Третий - пользовательское сообщение, в которое упакован сериализованный исходный диалог: обёртка на 3 817 байт, из них 3 092 байта приходятся на сам разговор. В клиентском запросе стояли store = false и prompt_cache_retention = "24h". Автор оговаривает, что это значения, которые отправил клиент, и о том, что происходит на сервере, они ничего не говорят.

В исходном роллауте (так в Codex называется сохранённая запись прошлой сессии) было шесть элементов, видимых модели. Пять из них попали в запрос памяти и, по словам автора, совпали со структурой исходника полностью:

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

Исходное developer-сообщение в запрос не попало, как и служебные записи о сессии, ходах, событиях и состоянии. При этом путь к файлу роллаута и рабочая директория ушли отдельно, за пределами того фрагмента, который проходит редактирование перед отправкой.

OpenAI приняла поток и ответила response.created, затем response.completed. В ответе указана модель gpt-5.6-luna и расход в 7 337 входных и 983 выходных токена, всего 8 320. Сгенерированная запись памяти длиной 3 405 символов несколько раз воспроизвела канареечные метки исходника: уникальные синтетические строки из реплик пользователя и ассистента, из вывода инструмента, из «приватного» текста и из адреса почты. Автор считает это доказательством того, что материал на сервере обработали, а не просто приняли соединение.

Три места в коде

Механизм автор разбирает по исходникам с того же тега. Сначала задача памяти выбирает кандидатов: в state/src/runtime/memories.rs селектор роллаутов вызывается с model_providers: None. Остальные условия отбора при этом проверяются: режим памяти, возраст сессии, простой, тип источника и признак того, что роллаут уже кем-то взят в работу. Провайдер среди них не значится.

Дальше рантайм памяти создаёт провайдера из config.model_provider текущей конфигурации, вместе с ModelClient, который и отправляет запрос (memories/write/src/runtime.rs). На третьем шаге, в memories/write/src/phase1.rs, первая фаза загружает сохранённый роллаут, фильтрует и сериализует оставшиеся элементы, кладёт их в новое пользовательское сообщение и отправляет через этот клиент.

Вывод автора: настоящий роллаут, созданный под локальным провайдером, может быть выбран позже сессией памяти с бэкендом OpenAI и уйти в OpenAI, если его не отсечёт какое-то другое условие отбора.

Объём зависит от длины сессии. В запрос попадают обычные сообщения пользователя и ассистента, сообщения агентов, локальные шелл-вызовы, вызовы функций и их результаты, поиск инструментов, кастомные инструменты, веб-поиск и сконвертированная переписка между агентами. Отбрасываются developer-сообщения, рассуждения модели, записи компакции, часть деклараций инструментов и медиа, а также служебные записи. Длинный ввод обрезается посередине на 70% эффективного контекстного окна модели, по умолчанию лимит 150 000 токенов, начало и конец сохраняются. Короткая сессия, по словам автора, может уйти почти целиком: с аргументами и результатами инструментов, ошибками, контекстом окружения и локальными путями.

Фильтр секретов

Перед отправкой сериализатор вызывает redact_secrets. Регулярные выражения в secrets/src/sanitizer.rs ловят ключи с префиксом sk-, идентификаторы AWS вида AKIA, достаточно длинные bearer-токены и значения, присвоенные переменным с именами вроде api_key, token, secret или password.

Автор проверил фильтр отдельно, на локальном loopback-стенде. Фильтр вырезал фейковый ключ OpenAI обычного вида, bearer-токен и именованное присваивание api_key. Пропустил он ключ нестандартного формата с подчёркиваниями, значение, похожее на cookie, адрес почты, номер телефона, заголовок приватного ключа, текст с пометкой «проприетарный», путь Windows и все канареечные метки. Автор называет его полезным узким фильтром шаблонов секретов и отмечает, что для конфиденциальных, персональных или привилегированных данных и для путей файловой системы он не предназначен.

Какой переключатель закрывает канал?

Запрос ушёл с выключенной аналитикой и без экспортёров OpenTelemetry: эти настройки управляют другими подсистемами. Канал целиком закрывает только флаг [features] memories = false. Оставить память и при этом запретить передачу между провайдерами нельзя, такого отдельного переключателя нет. Пользовательская документация, по словам автора, описывает локальное хранение памяти и её настройки. README для разработчиков говорит, что первая фаза фильтрует роллаут и отправляет его модели. О том, что роллаут одного провайдера может уйти через другого, ни там, ни там прямо не сказано.

Для воспроизведения автор предлагает отдельный домашний каталог Codex с теми же настройками, а в нём подходящий по условиям роллаут с меткой не OpenAI провайдера и уникальными синтетическими маркерами. Память запускается из сессии с бэкендом OpenAI, а собственный трафик перехватывается через доверенный локальный TLS-прокси. Дальше нужно найти исходящий кадр с request_kind = "memory", сравнить его содержимое с исходником и проверить, что ответ сервера опирается на маркеры. Настоящие приватные промпты для этого не нужны.

С уже открытым отчётом о передаче ID моделей OpenAI стороннему провайдеру автор этот случай не смешивает. Там данные идут в обратную сторону, и причина другая.

Предупреждение за «cyber abuse»

Разбираться автор начал после того, как его аккаунт OpenAI получил предупреждение за «cyber abuse». Он утверждает, что ни в одном чате, который шёл в OpenAI, не было ничего, что могло бы объяснить предупреждение. Соответствующая активность, по его словам, была только в чатах, которые он намеренно направлял через свой приватный локальный провайдер. Отсюда его «твёрдое, подкреплённое доказательствами убеждение», что OpenAI применила к аккаунту санкции на основании содержимого чатов, которое не имела права получать. Прямой связи между Memories и предупреждением захват не показывает: автор выводит её из этих двух фактов.

Происходящее он называет утечкой данных «в обычном смысле слова»: приватные данные вышли за границу выбранного им провайдера и попали к удалённой стороне без его осознанного согласия. Задумано так или это баг, по его словам, ничего не меняет в том, что именно ушло. Если поведение намеренное, его законность и достаточность раскрытия автор считает «как минимум юридически спорными». Санкции, выросшие из такого доступа, он называет «плодом отравленного дерева». Он просит OpenAI сохранить логи, решения модерации и флаги аккаунта на случай будущего судебного раскрытия. Любые негативные действия против своего аккаунта после отчёта он заранее объявляет ответной мерой.

Требования к коду конкретные. Роллаут, созданный под локальным или сторонним провайдером, не должен уходить другому провайдеру без явного согласия для конкретного адресата. Память должна строиться через провайдера исходной сессии, а если он недоступен, роллаут нужно пропускать. Если обработку между провайдерами оставят, она должна включаться только по запросу и заранее называть адресата и типы содержимого. Интерфейс должен показывать квитанцию: какая задача обработана, какой провайдер её получил и когда. Изоляцию провайдеров при отборе и при отправке автор предлагает закрепить автотестами.

К мейнтейнерам у него десять вопросов. Среди них: это задуманное поведение или баг, какие релизы затронуты, какие правила хранения и вторичной обработки действуют для запросов памяти и используется ли полученное через этот канал содержимое для модерации, предупреждений и ограничений аккаунтов. Он также спрашивает, сколько пользователей получили санкции целиком или частично из-за чатов с локальными и сторонними провайдерами. От OpenAI автор требует публичного разбора инцидента, аудита и отмены таких санкций и уведомления каждого затронутого пользователя.

«Тихо поправить селектор провайдеров - недостаточный ответ пользователям, которые, возможно, уже наказаны», - пишет автор.

Пока никто не написал. Будьте первым.

Присоединяйтесь к разговору

Войдите, чтобы оставить комментарий. Он появится после модерации.