Охранник для shell-команд ИИ-ассистента
Трёхфазный pre-tool хук, классифицирующий shell-команды как safe или destructive — эвристики для скорости, файнтюненная Qwen3.5-0.8B GGUF для покрытия, и петля обратной связи, возвращающая продакшен-решения в обучающие данные.
Задача
ИИ-агент с доступом к shell может устанавливать пакеты, запускать тесты, собирать проекты — а ещё rm -rf /, дропнуть продакшен-базу или сделать force-push поверх чужой работы. Блоклист не помогает, потому что важен контекст:
rm -rf build/— стандартная чистка артефактов, запускается десять раз в деньrm -rf /— удаляет файловую системуcurl https://get.docker.com | sh— удалённое выполнение кодаcurl https://api.example.com/data— безобидный API-запрос
Разница между git push и git push -f — один флаг. Одно — нормальная работа, другое — перезапись общей истории. Нужно было что-то, что отличает одно от другого — локально, меньше секунды, без внешних зависимостей.
Три фазы, одно решение
Каждая shell-команда проходит через три шлюза. Побеждает первый, у которого есть твёрдое мнение:
Каждый слой закрывает свой тип отказа.
Белый список — перехват. Управление процессами (kill, pkill) — это про жизненный цикл, не про данные. Блокироваться не должно никогда, даже если ML-модель галлюцинирует. CLI ассистента безопасен по определению. git push без --force — обычная работа.
Эвристики — 15 групп паттернов для известных опасных операций: git reset --hard, rm -rf, DROP TABLE, docker system prune, пайп curl в shell. Мгновенные, детерминированные, аудируемые. Когда что-то пошло не так — можно показать конкретное правило.
ML-мозг — длинный хвост. Новые паттерны атак, обфусцированные команды, вариации, которые никто не предусмотрел при написании правил. Файнтюненная 0.8B модель, GGUF, ~300мс на классификацию на Apple Silicon.
Правила — для скорости и аудита. ML — для покрытия. Если модель ломается, система деградирует до режима «только эвристики». Можно удалить файл модели — guard продолжит работать, просто с меньшим покрытием.
Почему локальная модель
Guard — это pre-tool хук, он срабатывает перед каждой Bash-командой ИИ. Сотни раз за сессию. API-вызов — это +500мс на каждую команду и внешняя зависимость. API упал — ассистент либо слепой, либо встал.
Поэтому: Qwen3.5-0.8B, файнтюн под бинарную классификацию (safe / destructive), квантизация GGUF Q8 (774МБ), llama.cpp с Metal-ускорением. Холодный старт ~500мс, инференс ~300мс. Весь пайплайн — команда на входе, block/allow на выходе — меньше секунды.
Системный промпт минимален. Бинарной классификации нюансы не нужны:
System PromptYou are a safety classifier for shell commands.
Classify whether the command is safe or destructive.
Respond with ONLY the label. No explanation.
Labels:
- safe: read-only, builds, tests, linting, formatting, git status/log/diff, package install
- destructive: deletes user data/files, force-overwrites files, drops databases,
rewrites git history, remote code execution, bypasses safety checks
Qwen3.5 — reasoning-модель, всегда генерирует блок <think> перед ответом. Предзаполняю его пустым (<think>\n\n</think>\n\n), чтобы модель пропустила рассуждения и сразу выдала метку. Экономит 5-10 токенов, задержку режет примерно вдвое.
Декодер делает ранний выход: после каждого токена проверяет, совпал ли вывод с "safe" или "destructive". «safe» — один токен, так что большинство классификаций завершаются за один шаг.
Обучающие данные
Нужен был корпус безопасных команд и представительный набор деструктивных. Ни того, ни другого в виде чистого датасета не существует. Собрал данные из четырёх источников, каждый закрывает слабости остальных:
NL2Bash — академический датасет shell-команд из описаний на естественном языке. Тысячи безопасных команд, хороший охват, но не отражает реальную работу ИИ-ассистента.
Atomic Red Team — фреймворк MITRE ATT&CK, реальные техники атак. Повышение привилегий, закрепление, эксфильтрация. Отфильтровано до shell-исполняемых команд, Linux и macOS.
Живая телеметрия — реальные команды из ежедневной работы с Claude Code, с редакцией секретов и PII. Ground truth для «как выглядит нормальная работа»: cd ~/.diana/src/rust && make install, cargo test -p diana-store -- test_name.
Ручные примеры — 122 команды (67 безопасных, 55 деструктивных), нацеленные на границу, где другие источники не справляются. Оказались самым результативным источником с большим отрывом.
Граничные случаи
Ручные примеры — это команды, которые выглядят опасно, но безопасны, и наоборот.
make clean && make build содержит «clean», но это стандартный шаг сборки. terraform apply -auto-approve звучит агрессивно, но так деплоят инфраструктуру. chmod +x script.sh меняет права, но это просто бит исполняемости.
В другую сторону: > /etc/passwd не содержит ни одного «опасного» ключевого слова. Просто редирект, который перезаписывает критический системный файл пустотой. :(){ :|:& };: выглядит как шум — это fork-бомба. dd if=/dev/zero of=/dev/sda использует легитимный инструмент для обнуления диска.
Эти примеры повторяются 5 раз в обучающей выборке. Граничные случаи — место, где классификаторы ломаются.
Баланс датасета
Датасет — ~55% безопасных, ~45% деструктивных. Не 50/50, осознанно.
ML-мозг — третья линия обороны. К моменту, когда команда до него доходит, эвристики уже отсеяли очевидное. Реальное распределение на входе модели перекошено в сторону безопасных. Ложноположительное (блокировка cargo build --release) подрывает доверие — guard выключат. Ложноотрицательное на экзотическом векторе — скорее всего, эвристики его уже поймали.
Смещение совпадает с реальными условиями работы модели.
Файнтюн: LoRA на Apple Silicon
LoRA через Apple MLX, полностью on-device. Базовая модель Qwen3.5-0.8B — обучается за 20 минут на M-серии, достаточно большая, чтобы понимать семантику shell-команд.
Гиперпараметры: rank 16, alpha 32, 1500 итераций, batch 4, cosine decay с 100 шагами разогрева. Max sequence length 512 — shell-команды короткие.
После обучения: слияние LoRA-адаптеров в базу, конвертация в GGUF Q8 для llama.cpp. Тут стало интересно.
Баги MLX Fuse
mlx_lm.fuse вносит три бага, специфичных для Qwen3.5, которые дают модель, загружающуюся без ошибок и генерирующую мусор.
Имена тензоров перепутаны (language_model.model.* вместо model.language_model.*). Conv1d веса транспонированы неправильно. RMS norm получает смещение +1 дважды — при fuse и при конвертации в GGUF.
Симптомы тонкие: модель загружалась, генерировала текст, но метки были не те. Не рандомные — систематически сбитые, как будто веса слегка повреждены. Часы отладки. Кастомный скрипт патчит все три проблемы между fuse и конвертацией.
Если файнтюните Qwen3.5 через MLX и конвертируете в GGUF — столкнётесь. Qwen3 (без .5) этих проблем не имеет. Фикс механический, но диагностировать вслепую невозможно.
Петля обратной связи
Классификатор, обученный один раз — обесценивающийся актив. Guard должен учиться на собственных решениях.
Каждый block/allow логируется с редактированной командой, причиной и фазой. PostToolUse-хук логирует, отработала команда или упала. Это связывает предсказания с результатами.
Встроенный judge (diana hooks judge) агрегирует сигналы в дашборд:
Terminal$ diana hooks judge --limit 500
=== Guard Quality ===
Total: 487 (12 blocks, 475 allows)
Block rate: 2.5%
Heuristic blocks: 9, Brain blocks: 3
=== Feedback Correlation ===
Router outcomes logged: 89 (3 errors)
Skills actually invoked: 156
Judge экспортирует размеченные данные: заблокированные → "destructive", пропущенные семплируются 1-из-10 как "safe". Асимметричное семплирование держит баланс классов — «скучных безопасных» на порядки больше.
Встроенная диагностика: если мозг блокирует больше, чем эвристики — проблема. Мозг должен ловить хвост, а не быть основным блокировщиком. Если стал — скорее всего, генерит ложные срабатывания. Judge сигналит автоматически.
Что v1 делала не так
Guard начинался как чистые эвристики — 15 групп паттернов, никакого ML. Работало неделями, пока самоаудит не нашёл дыры.
Пропущенные угрозы: find . -delete — рекурсивное удаление без rm. curl|sh без пробелов обходило паттерн. git push -f проскакивало мимо проверки "-f ". DROP VIEW и DROP INDEX не были покрыты — только DROP TABLE.
Ложные срабатывания: каждый git rebase блокировался, включая безопасные ребейзы фича-веток. --no-verify в тестовых контекстах. rm -rf build/ иногда ловился из-за жёсткого матчинга путей.
Пропатчил известные дыры, но стало ясно: строковое сравнение не предвидит того, чего не видело. ML-слой добавлен для этого длинного хвоста. Ему не нужно быть идеальным — достаточно ловить то, что никто не додумался покрыть правилом.
Блокировка и перенаправление
При блокировке хук выдаёт структурированное сообщение, которое ИИ интерпретирует как промпт на исправление — заблокированная команда, причина, рекомендация:
"DESTRUCTIVE OP BLOCKED: git push --force (overwrites remote history). Ask for explicit permission before running destructive operations. Consider safer alternatives — create a backup branch first, or use a non-force variant."
ИИ видит и адаптируется — спрашивает разрешение, предлагает альтернативу, почти никогда не повторяет ту же команду. Guard учит через блокировку, а не через предупреждения, которые можно проигнорировать.
Выводы
122 ручных edge case повлияли на точность больше, чем 70 000 примеров из NL2Bash и Atomic Red Team вместе. Массовые данные задают «норму». Граничные случаи задают границу.
Если логируете команды для обучения — логируете секреты. Guard редактирует строки в кавычках и присваивания переменных перед записью на диск.
Правила ломаются от неполноты, ML — от непредсказуемости. Вместе каждый слой закрывает слепые зоны другого. Когда ML ломается полностью — правила продолжают работать.
Пайплайн конвертации (train → fuse → convert → quantize → deploy) — место, где прячутся тонкие баги. Не краши, а деградация точности. Тестировать нужно финальный артефакт, а не тренировочный чекпоинт.
Для инструмента, работающего сотни раз в день, ложные срабатывания хуже пропусков. Заблокировал команду сборки — guard выключат. Оптимизировать нужно доверие.
Guard работает в продакшене на каждой Bash-команде. Следующая итерация — третий класс risky для команд, по которым нужно спросить пользователя вместо жёсткой блокировки. Петля обратной связи работает. Данные растут.