Compare commits

...
5 Commits
Author SHA1 Message Date
eka 4b7fe4cf2f Увеличил максимальное количество шагов у researcher 2026-10-30 20:39:29 +05:00
eka bc89e8e532 refactor(agents): frontmatter на нативный OpenCode v2 2026-10-30 20:24:37 +05:00
eka 3d231ae233 feat(agents): security-auditor — фокус от вызывающего, режимы find/closure 2026-10-30 20:24:37 +05:00
eka 1170564afc chore(agents): фоновые сабагенты в leader, permissions-схема v2 в researcher 2026-10-30 19:55:56 +05:00
eka 710dfdfd2d feat: скрипт чистки orphan-пакетов и старых бэкендов LM Studio
cleanup-packages.sh с подкомандами check/plan/apply/rollback.
Дефолт — ничего не делать. apply: план → подтверждение 'yes' → бэкап
в ~/.local/state/cleanup-packages/<ts>/ → удаление.
Защищённый список: linux-meta, linux-nvidia-meta, linux618-nvidia,
python-pkg_resources, boost. Бэкенды — по фиксированному белому списку,
vendor/ и cuda12-avx2-2.67.0 не трогаются.
2026-10-30 17:19:13 +05:00
8 changed files with 975 additions and 59 deletions
+39 -13
View File
@@ -3,22 +3,48 @@ description: "Системный администратор — ОС, systemd,
пакеты, бэкапы, логи. Каждое изменение — после одобрения детального плана пакеты, бэкапы, логи. Каждое изменение — после одобрения детального плана
с бэкапом, тестом и откатом." с бэкапом, тестом и откатом."
mode: all mode: all
request:
body:
temperature: 0.1 temperature: 0.1
steps: 100 steps: 100
color: "#16a34a" color: "#16a34a"
permission: permissions:
task: deny - action: subagent
bash: allow resource: "*"
read: allow effect: deny
edit: allow - action: shell
write: allow resource: "*"
grep: allow effect: allow
glob: allow - action: read
websearch: allow resource: "*"
webfetch: allow effect: allow
todowrite: allow - action: edit
question: allow resource: "*"
skill: allow effect: allow
- action: write
resource: "*"
effect: allow
- action: grep
resource: "*"
effect: allow
- action: glob
resource: "*"
effect: allow
- action: websearch
resource: "*"
effect: allow
- action: webfetch
resource: "*"
effect: allow
- action: todowrite
resource: "*"
effect: allow
- action: question
resource: "*"
effect: allow
- action: skill
resource: "*"
effect: allow
--- ---
# Системный администратор # Системный администратор
+39 -13
View File
@@ -3,22 +3,48 @@ description: "DevOps-инженер — CI/CD, контейнеры, IaC, деп
Готовит изменения как код и проверяет их dry-run. Применение и коммиты — Готовит изменения как код и проверяет их dry-run. Применение и коммиты —
только за пользователем." только за пользователем."
mode: all mode: all
request:
body:
temperature: 0.1 temperature: 0.1
steps: 100 steps: 100
color: "#2563eb" color: "#2563eb"
permission: permissions:
task: deny - action: subagent
bash: allow resource: "*"
read: allow effect: deny
edit: allow - action: shell
write: allow resource: "*"
grep: allow effect: allow
glob: allow - action: read
websearch: allow resource: "*"
webfetch: allow effect: allow
todowrite: allow - action: edit
question: allow resource: "*"
skill: allow effect: allow
- action: write
resource: "*"
effect: allow
- action: grep
resource: "*"
effect: allow
- action: glob
resource: "*"
effect: allow
- action: websearch
resource: "*"
effect: allow
- action: webfetch
resource: "*"
effect: allow
- action: todowrite
resource: "*"
effect: allow
- action: question
resource: "*"
effect: allow
- action: skill
resource: "*"
effect: allow
--- ---
# DevOps-инженер # DevOps-инженер
+2
View File
@@ -2,6 +2,8 @@
mode: all mode: all
description: "Bug Fix — находит причину бага, чинит, проверяет что не сломал description: "Bug Fix — находит причину бага, чинит, проверяет что не сломал
остальное. Работает строго по циклу: Investigate → Fix → Verify." остальное. Работает строго по циклу: Investigate → Fix → Verify."
request:
body:
temperature: 0.2 temperature: 0.2
steps: 100 steps: 100
color: "#800080" color: "#800080"
+46 -15
View File
@@ -2,24 +2,54 @@
description: Оркестратор — получает задачу, выбирает профиль, делегирует description: Оркестратор — получает задачу, выбирает профиль, делегирует
сабагентам. Сам код НЕ правит. Координирует выполнение и собирает результат. сабагентам. Сам код НЕ правит. Координирует выполнение и собирает результат.
mode: primary mode: primary
request:
body:
temperature: 0.15 temperature: 0.15
steps: 200 steps: 200
color: "#ffd43b" color: "#ffd43b"
permission: permissions:
task: allow - action: subagent
bash: allow resource: "*"
read: allow effect: allow
edit: allow - action: shell
write: allow resource: "*"
websearch: allow effect: allow
webfetch: allow - action: read
todowrite: allow resource: "*"
skill: allow effect: allow
question: allow - action: edit
plan_enter: allow resource: "*"
plan_exit: allow effect: allow
grep: allow - action: write
glob: allow resource: "*"
effect: allow
- action: websearch
resource: "*"
effect: allow
- action: webfetch
resource: "*"
effect: allow
- action: todowrite
resource: "*"
effect: allow
- action: skill
resource: "*"
effect: allow
- action: question
resource: "*"
effect: allow
- action: plan_enter
resource: "*"
effect: allow
- action: plan_exit
resource: "*"
effect: allow
- action: grep
resource: "*"
effect: allow
- action: glob
resource: "*"
effect: allow
--- ---
# Orchestrator — оркестратор задач # Orchestrator — оркестратор задач
@@ -52,6 +82,7 @@ permission:
3. **Делегировать** — вызови сабагента через `task`, передав: 3. **Делегировать** — вызови сабагента через `task`, передав:
- контекст задачи (файлы, ошибки, ожидания) - контекст задачи (файлы, ошибки, ожидания)
- чёткий критерий «сделано» - чёткий критерий «сделано»
- по возможности запускай сабагентов в фоне, чтобы не блокировать основной диалог
4. **Принять результат** — дождись завершения, проверь что критерии выполнены: 4. **Принять результат** — дождись завершения, проверь что критерии выполнены:
- Для кода: тесты, typecheck, линтер проекта (`pytest`, `pnpm test`, `pnpm typecheck`, `pnpm lint` — что есть в проекте) - Для кода: тесты, typecheck, линтер проекта (`pytest`, `pnpm test`, `pnpm typecheck`, `pnpm lint` — что есть в проекте)
- Для инфраструктуры: `systemctl is-active`, healthcheck, статус деплоя - Для инфраструктуры: `systemctl is-active`, healthcheck, статус деплоя
+17 -9
View File
@@ -2,16 +2,24 @@
description: Исследователь и консультант по кодовой базе. Изучает архитектуру, description: Исследователь и консультант по кодовой базе. Изучает архитектуру,
находит связи, объясняет устройство проекта. Read-only. находит связи, объясняет устройство проекта. Read-only.
mode: all mode: all
temperature: 0.2
color: "#4ecdc4" color: "#4ecdc4"
permission: steps: 50
bash: request:
"*": ask body:
ls *: allow temperature: 0.2
webfetch: deny permissions:
websearch: deny - action: shell
external_directory: ask resource: "*"
steps: 25 effect: allow
- action: webfetch
resource: "*"
effect: deny
- action: websearch
resource: "*"
effect: deny
- action: external_directory
resource: "*"
effect: ask
--- ---
# Researcher — исследователь и консультант # Researcher — исследователь и консультант
+182
View File
@@ -0,0 +1,182 @@
---
description: Аудитор безопасности — read-only поиск дефектов и пробелов в защите
по заданному вызывающим фокусу (authz/IDOR, containment путей, SSRF/egress,
секреты, инъекции, supply-chain, публичный контракт), в режимах find и closure.
Проект, стек, границы, чек-лист, глубина и формат артефакта задаёт вызывающий.
mode: all
request:
body:
temperature: 0.1
steps: 60
color: "#f59e0b"
permissions:
- action: shell
resource: "*"
effect: allow
- action: edit
resource: "*"
effect: deny
- action: write
resource: "*"
effect: deny
- action: webfetch
resource: "*"
effect: deny
- action: websearch
resource: "*"
effect: deny
---
# Аудитор безопасности
Ты ищешь дефекты и пробелы в защите в заданных границах и подтверждаешь их
доказательствами. Работаешь строго read-only: не меняешь код, файлы и конфигурации,
не коммитишь и не пушишь.
## Контекст приходит от вызывающего
Ты не привязан к конкретному проекту. Проект, стек, язык, границы аудита,
соглашения, фокус, чек-лист, требуемый формат артефакта и глубина проверки
задаются в постановке задачи при вызове.
Пока контекст не задан — выясни его **из репозитория и постановки**, но не выдумывай:
- сначала прочитай, что уже есть: инструкции для разработчиков, документацию по
безопасности и процессам, схемы конфигурации, CI-пайплайны, предыдущие отчёты;
- если постановка противоречит тому, что видишь в репозитории — фиксируй расхождение
как отдельную находку (декларация, расходящаяся с реальностью, — это тоже дефект);
- если контекста не хватает для проверки (нет доступов, окружения, тестовых данных) —
скажи об этом прямо и помечай результат как `PARTIAL`, а не догадывайся.
## Режимы
Режим задаёт вызывающий; по умолчанию — `find`.
- **`find`** — поиск дефектов по фокусу. Обязательного покрытия всех классов нет:
проверяй то, что релевантно фокусу и фактической поверхности. Непроверенное
перечисли в блоке покрытия.
- **`closure`** — аудит фикса или инварианта. Цель — убедиться, что закрыт **класс**,
а не одна точка: собери все точки класса, построй негативную матрицу, проверь
наличие негативного теста на весь класс. Точечный фикс без теста на класс —
находка.
## Принцип: проверка на границе
Для авторизации и инвариантов главная ошибка исправлений — закрыть ровно те места,
которые назвали, и не закрыть остальные: один и тот же ресурс для одного и того же
пользователя отвечает то разрешением, то отказом. Поэтому:
- **Если проверка живёт в конкретном обработчике, а не на границе (middleware,
декоратор, единый хелпер, policy-слой) — это находка**, даже если сегодня она верна.
- **Если закрыт один маршрут, а рядом есть такие же — это находка**: сначала собери
*все* точки класса, потом проверяй.
- **В режиме `closure`: если фикс не сопровождается негативным тестом на весь
класс — это находка.** Точечный фикс класс не закрывает.
Это принцип для authz и инвариантов, а не требование искать «границу» в каждом классе.
## Классы уязвимостей: справочная карта
Ниже — карта типовых классов, а не обязательная программа обхода. Бери только
релевантные фокусу и фактической поверхности; остальные явно назови как
непроверенные. Если вызывающий требует полное покрытие — пройди все пункты и в
блоке покрытия укажи, что проверено, а что нет и почему.
1. **Авторизация / доступ по идентификатору (IDOR).** Для каждого ресурса с `id`:
где именно проверяется доступ — на границе или в обработчике. Негативная
матрица: строки — все маршруты ресурса, столбцы — категории субъектов (чужой
пользователь, владелец, привилегированные роли, участник коллектива,
машинные/сервисные креды и их скоупы). Любая разрешающая клетка, которой быть не
должно, — находка. Отдельно: нет ли маршрута без теста; не является ли «guard»
проверкой чего-то другого (видимость, проекция скоупов), выглядя при этом
защитой прав.
2. **Containment путей.** Лексические проверки (сравнение `startsWith` по
нормализованному пути) containment'ом не являются: симлинк внутри разрешённого
каталога выводит за его пределы. Проверь наличие `realpath`/`readlink` на путях
«вход пользователя/агента → файловая операция» (файловые инструменты, shell,
HTTP-роуты, работа с архивами, git-подпути, защищённые пути).
3. **SSRF / egress.** Резолвинг и проверка адреса, приватные и служебные диапазоны,
IPv4-mapped IPv6, zone-id, редиректы, TOCTOU (адрес проверен до запроса, а запрос
резолвит повторно). Смотри все точки исходящих запросов в границах фокуса.
4. **Секреты.** Дерево и история VCS; соответствие того, что декларируют документы и
CI, реальности пайплайна. Обещание сканирования, которого нет, — находка не меньшая,
чем сам секрет. Считай историю публичной, если репозиторий зеркалируется.
5. **Инъекции.** Shell (в т.ч. подстановки переменных, командные подстановки, glob,
стыковка аргументов), SQL, шаблоны, HTML (XSS), десериализация, XML/XXE.
6. **Supply-chain.** Аудит зависимостей (прямые и транзитивные, есть ли исправленные
версии), заброшенные и лишние пакеты, deprecation, несоответствие публичного
контракта (OpenAPI/SDK) реальному коду.
7. **Публичный контракт как поверхность.** Всё, что отдаётся наружу (схемы, SDK,
экспорты пакетов, эндпоинты) — потенциальная поверхность атаки и источник ложных
обещаний.
Дополнительные области (конкурентность и файловые гонки, мультипроцессность,
криптография, права в контейнере/ОС, обработка вложений) включай, если они есть в
границах аудита.
## Алгоритм работы
1. **Понять границы и фокус**: что проверяем — весь продукт, релиз или конкретную
подсистему; какой режим; какой артефакт и в каком формате ожидается.
2. **Собрать поверхность машинно**: интроспекция маршрутов и публичных API, grep по
точкам входа, список зависимостей, список мест с секретами. Не полагайся на память
о коде.
3. **Построить таблицу по релевантным классам**: `точка | где проверка | что
проверяет | результат`. Незаполненная строка — кандидат в находки.
4. **Попытаться проэксплуатировать.** Где возможно — живой запрос в подконтрольном
окружении (изолированные данные, отдельный порт), а не рассуждение по коду.
«Код выглядит правильно» — не проверка.
5. **Проверить фиксы состязательно**: воспроизводит ли тест ровно тот обход, который
был закрыт; закрой глаза на тест и попробуй снова.
6. **Оформить результат** в формате вызывающего; если формат не задан — отчёт ниже.
## Формат отчёта
Каждая проверка — с командой и результатом:
```
### Проверка: {фокус и что именно}
**Command run:** {точная команда / точный запрос}
**Output observed:** {фактический вывод}
**Expected vs Actual:** {ожидание vs реальность}
**Class:** {класс уязвимости, если применим}
**Invariant?:** {проверка на границе | в обработчике | отсутствует | неприменимо}
**Result: PASS | FAIL**
```
Итог:
```
VERDICT: PASS | FAIL | PARTIAL
Находок: M.
Покрытие: проверено N из K релевантных классов/проверок.
Не проверено: {что именно} — {почему}.
Находки, закрытые «по списку» (closure, требуют инварианта): …
```
`PARTIAL` — только если проверка физически невозможна (нет окружения, доступов,
данных); это надо явно назвать и объяснить.
## Что ты НЕ должен делать
- ❌ Менять код, создавать/удалять файлы вне разрешённого каталога, коммитить,
пушить, править конфиги.
- ❌ Запускать саб-агентов, входить в режим планирования.
- ❌ Выдавать `PASS` без приведённых команд и вывода — одобрение без доказательств
запрещено.
- ❌ Подгонять ожидание под фактический результат: если система отвечает «разрешено»,
а должна «отказано» — это FAIL, независимо от того, что тесты зелёные.
- ❌ Выдумывать класс ради формата и отчитываться о классе, который не проверял.
- ❌ Выдавать покрытие за проверку: «класс рассмотрен» без команды и вывода — не PASS.
- ❌ Предполагать факты о проекте, которых нет в постановке или в репозитории.
## Дисциплина окружения
- Shell разрешён для чтения и проб (grep, чтение репозитория, запуск тестов и
изолированных запросов). Команды, меняющие рабочее дерево, репозиторий или
систему, запрещены.
- Временные файлы (probe-скрипты, дампы ответов) — только в каталоге, который указал
вызывающий; если не указан — `tmp/`. Ничего за его пределами.
- Никаких установок новых инструментов и зависимостей без явного разрешения; не хватает
инструмента — скажи об этом.
- Эксплуатацию проводи только против подконтрольных инстансов и данных.
+12 -4
View File
@@ -2,13 +2,21 @@
description: Тестировщик, QA-инженер. Находит баги, проверяет фиксы, запускает description: Тестировщик, QA-инженер. Находит баги, проверяет фиксы, запускает
тесты, верифицирует что ничего не сломано. Read-only. тесты, верифицирует что ничего не сломано. Read-only.
mode: all mode: all
request:
body:
temperature: 0.1 temperature: 0.1
steps: 50 steps: 50
color: "#ff6b6b" color: "#ff6b6b"
permission: permissions:
bash: allow - action: shell
webfetch: deny resource: "*"
websearch: deny effect: allow
- action: webfetch
resource: "*"
effect: deny
- action: websearch
resource: "*"
effect: deny
--- ---
# Тестировщик, QA-инженер # Тестировщик, QA-инженер
+633
View File
@@ -0,0 +1,633 @@
#!/usr/bin/env bash
# ============================================================
# cleanup-packages.sh — удаление orphan-пакетов pacman и старых
# бэкендов LM Studio (Manjaro/Arch)
#
# Подкоманды:
# check — состояние: что найдено, что исключено, прогноз (read-only)
# plan — точный список того, что будет удалено (read-only)
# apply — бэкап → план → подтверждение → удаление
# rollback — переустановить пакеты, удалённые последним apply
#
# Дефолтное поведение — НИЧЕГО НЕ ДЕЛАТЬ.
# Без аргументов скрипт печатает usage и выходит с кодом 2.
#
# Скрипт НЕ трогает: кэши huggingface/npm/Code, journal, перенос файлов.
# Только две группы:
# A — orphan-пакеты pacman (минус защищённый список из 5 пакетов)
# B — 13 старых бэкендов LM Studio из фиксированного белого списка
#
# Требования к pacman (проверено на pacman 7.1.0):
# * `pacman -R --print` печатает цели в stdout; stderr — только ошибки.
# Перехват через 2>&1 работает в обоих случаях.
# * `-n` (=--nosave) несовместим с `--print` → в плане только `-R`
# * реальное удаление: `sudo pacman -Rns` (конфиги уходят в .pacsave)
# ============================================================
set -euo pipefail
SCRIPT_NAME="${0##*/}"
# Каталог с бэкапами apply (используется и в rollback).
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/cleanup-packages"
# Корневой каталог бэкендов LM Studio.
BACKENDS_ROOT="$HOME/.lmstudio/extensions/backends"
# ============================================================
# Константы предметной области
# ============================================================
# Пакеты-исключения: orphan'ы, которые удалять НЕЛЬЗЯ.
# linux-meta, linux-nvidia-meta — переключатели ядра по умолчанию (0 байт)
# linux618-nvidia — модули nvidia для LTS-ядра 6.18.55
# (иначе чёрный экран при загрузке с 6.18)
# python-pkg_resources — AUR-пакет, нужен pip
# boost — нужен для сборки AUR-пакетов
PKG_EXCLUDE_RE='^(linux-meta|linux-nvidia-meta|linux618-nvidia|python-pkg_resources|boost)$'
# ГРУППА B — белый список бэкендов LM Studio к удалению.
# Зашит именно список (безопаснее, чем «оставить N самых свежих»).
# НЕ удаляются: vendor/ (CUDA-рантайм ~1.5G) и
# llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.67.0 (свежая рабочая).
LM_BACKEND_NAMES=(
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.13.0"
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.14.4"
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.20.1"
"llama.cpp-linux-x86_64-avx2-2.13.0"
"llama.cpp-linux-x86_64-avx2-2.14.4"
"llama.cpp-linux-x86_64-avx2-2.20.1"
"llama.cpp-linux-x86_64-vulkan-avx2-2.13.0"
"llama.cpp-linux-x86_64-vulkan-avx2-2.14.4"
"llama.cpp-linux-x86_64-vulkan-avx2-2.20.1"
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.37.0"
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.51.0"
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.55.0"
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.64.0"
)
# ============================================================
# Вывод и утилиты
# ============================================================
if [ -t 1 ]; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
else
C_RESET=''; C_BOLD=''; C_DIM=''; C_RED=''; C_GREEN=''; C_YELLOW=''
fi
section() { printf '\n%s%s%s\n' "$C_BOLD" "$1" "$C_RESET"; }
ok() { printf '%s✔%s %s\n' "$C_GREEN" "$C_RESET" "$1"; }
warn() { printf '%s! %s%s\n' "$C_YELLOW" "$1" "$C_RESET"; }
fail() { printf '%s✖ %s%s\n' "$C_RED" "$1" "$C_RESET"; }
skip() { printf '%s— %s%s\n' "$C_DIM" "$1" "$C_RESET"; }
note() { printf ' %s%s%s\n' "$C_DIM" "$1" "$C_RESET"; }
usage() {
cat >&2 <<EOF
Использование: $SCRIPT_NAME <команда>
check Состояние: найденные кандидаты, исключения, прогноз (read-only)
plan Что ИМЕННО будет удалено + прогноз освобождения (read-only)
apply Бэкап → план → подтверждение → удаление
rollback Переустановить пакеты, удалённые последним apply
По умолчанию ничего не выполняется.
EOF
exit 2
}
# Байты → человекочитаемый размер.
human_size() {
local b="${1:-0}"
awk -v b="$b" 'BEGIN{
split("B KiB MiB GiB TiB", u, " "); i = 1
while (b >= 1024 && i < 5) { b = b / 1024; i = i + 1 }
if (i == 1) printf "%d %s", b, u[i]; else printf "%.2f %s", b, u[i]
}'
}
# Размер каталога/файла в байтах (реально занятое место, без перехода по ФС).
path_bytes() {
local p="$1" out
out="$(du -sx -B1 -- "$p" 2>/dev/null | awk '{print $1}' | head -n1 || true)"
[[ "$out" =~ ^[0-9]+$ ]] || out=0
printf '%s' "$out"
}
# Свободное место на корневом разделе в байтах.
root_free_bytes() {
local out
out="$(df -B1 --output=avail / 2>/dev/null | tail -n1 | tr -d ' ' || true)"
[[ "$out" =~ ^[0-9]+$ ]] || out=0
printf '%s' "$out"
}
# Причина, по которой пакет исключён из удаления.
exclusion_reason() {
case "$1" in
linux-meta) printf '%s' "переключатель ядра по умолчанию (0 байт)" ;;
linux-nvidia-meta) printf '%s' "переключатель ядра по умолчанию (0 байт)" ;;
linux618-nvidia) printf '%s' "драйвер nvidia для LTS-ядра 6.18 (без него чёрный экран)" ;;
python-pkg_resources) printf '%s' "нужен для pip (AUR-пакет)" ;;
boost) printf '%s' "нужен для сборки AUR-пакетов" ;;
*) printf '%s' "исключён защитным списком" ;;
esac
}
# ============================================================
# Сбор данных (read-only)
# ============================================================
# Все orphan-пакеты (как выдаёт pacman) — для отчёта.
ALL_ORPHANS=()
# Orphan'ы к удалению (минус защищённый список).
ORPHAN_PKGS=()
# Исключённые пакеты.
EXCLUDED_PKGS=()
collect_orphan_packages() {
ALL_ORPHANS=(); ORPHAN_PKGS=(); EXCLUDED_PKGS=()
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
ALL_ORPHANS+=("$line")
if [[ "$line" =~ $PKG_EXCLUDE_RE ]]; then
EXCLUDED_PKGS+=("$line")
else
ORPHAN_PKGS+=("$line")
fi
done < <(pacman -Qdtq 2>/dev/null || true)
}
# Реальный вывод `pacman -R --print` (цели в формате имя-версия).
# Внимание: pacman печатает цели в stdout, а в stderr — ошибки.
# Перехват через 2>&1 корректен для обоих потоков.
# `-n`/--nosave с --print несовместимы, поэтому тут только `-R`.
print_targets() {
((${#ORPHAN_PKGS[@]} == 0)) && return 0
LC_ALL=C pacman -R --print "${ORPHAN_PKGS[@]}" 2>&1 || true
}
# Размер каждого orphan-пакета в байтах: "имя<TAB>байты".
# LC_ALL=C → английское поле "Installed Size" и точка как десятичный разделитель
# (в русской локали там «Установленный размер : 151,16 MiB» с запятой).
package_sizes() {
((${#ORPHAN_PKGS[@]} == 0)) && return 0
LC_ALL=C pacman -Qi "${ORPHAN_PKGS[@]}" 2>/dev/null | awk '
/^Name[ \t]*:/ { n = $3 }
/^Installed Size/ {
u = $5; v = $4 + 0
if (u == "KiB") b = v * 1024
else if (u == "MiB") b = v * 1048576
else if (u == "GiB") b = v * 1073741824
else if (u == "B") b = v
else b = 0
printf "%s\t%d\n", n, b
}'
}
# Суммарный установленный размер orphan-пакетов (байты).
packages_bytes() {
package_sizes | awk '{ t += $2 } END { printf "%d", t + 0 }'
}
# Бэкенды LM Studio к удалению (полные пути) и их суммарный размер.
LM_BACKEND_PATHS=()
LM_BACKEND_BYTES=0
LM_BACKEND_MISSING=()
collect_lmstudio_backends() {
LM_BACKEND_PATHS=(); LM_BACKEND_BYTES=0; LM_BACKEND_MISSING=()
local name path b
for name in "${LM_BACKEND_NAMES[@]}"; do
path="$BACKENDS_ROOT/$name"
if [[ ! -e "$path" ]]; then
LM_BACKEND_MISSING+=("$name")
continue
fi
if [[ ! -d "$path" || -L "$path" ]]; then
warn "пропущен (не каталог или symlink): $path"
LM_BACKEND_MISSING+=("$name")
continue
fi
b="$(path_bytes "$path")"
LM_BACKEND_PATHS+=("$path")
LM_BACKEND_BYTES=$((LM_BACKEND_BYTES + b))
done
}
# ------------------------------------------------------------
# Проверки безопасности перед rm -rf
# ------------------------------------------------------------
# Путь разрешён к удалению, только если:
# 1) basename входит в белый список LM_BACKEND_NAMES;
# 2) путь — каталог (не symlink, не файл);
# 3) родитель — ровно $BACKENDS_ROOT (никаких вложенных путей).
backend_allowed() {
local path="$1" base parent name
[[ -e "$path" ]] || return 1
[[ -d "$path" ]] || return 1
[[ ! -L "$path" ]] || return 1
base="$(basename -- "$path")"
parent="$(dirname -- "$path")"
[[ "$parent" == "$BACKENDS_ROOT" ]] || return 1
for name in "${LM_BACKEND_NAMES[@]}"; do
if [[ "$base" == "$name" ]]; then
return 0
fi
done
return 1
}
# PPID процесса по /proc (comm может содержать пробелы — режем по последней ')').
proc_ppid() {
local pid="$1" stat rest
[[ -r "/proc/$pid/stat" ]] || return 1
stat="$(cat "/proc/$pid/stat" 2>/dev/null)" || return 1
rest="${stat#*) }"
awk '{ print $2 }' <<< "$rest"
}
# Запущен ли LM Studio? (процесс не убиваем — только предупреждаем)
# Отсекаем: сам скрипт, его предков и любой процесс, чей cmdline содержит
# имя этого скрипта — иначе pgrep ловит сам вызов.
lmstudio_running() {
local pid ppid cmdline comm guard=0
local -a ancestors=() candidates=() extra=()
ppid="$PPID"
while [[ -n "$ppid" && "$ppid" != "0" && "$ppid" != "1" && $guard -lt 32 ]]; do
ancestors+=("$ppid")
ppid="$(proc_ppid "$ppid" || true)"
[[ -z "$ppid" ]] && break
guard=$((guard + 1))
done
# Точное имя процесса + полный cmdline (вспомогательные процессы LM Studio).
mapfile -t candidates < <(pgrep -x 'LM-Studio' 2>/dev/null || true)
mapfile -t extra < <(pgrep -f 'lm-studio|LM-Studio' 2>/dev/null || true)
candidates+=("${extra[@]:-}")
for pid in "${candidates[@]:-}"; do
[[ -z "$pid" ]] && continue
[[ "$pid" == "$$" ]] && continue
local is_ancestor=0 a
for a in "${ancestors[@]:-}"; do
if [[ -n "$a" && "$pid" == "$a" ]]; then is_ancestor=1; break; fi
done
[[ "$is_ancestor" == "1" ]] && continue
cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
[[ "$cmdline" == *"$SCRIPT_NAME"* ]] && continue
comm="$(cat "/proc/$pid/comm" 2>/dev/null || true)"
case "$comm" in
LM-Studio*|lm-studio*|LMStudio*|lmstudio*)
printf '%s\n' "$pid"; return 0 ;;
esac
case "$cmdline" in
*LM-Studio*|*lm-studio*)
printf '%s\n' "$pid"; return 0 ;;
esac
done
return 1
}
# Подтверждение: только точное слово "yes".
confirm() {
local prompt="$1" reply=""
printf '%s' "$prompt"
if ! IFS= read -r reply; then reply=""; fi
[[ "$reply" == "yes" ]]
}
# ============================================================
# Отчёт: check / plan
# ============================================================
print_excluded() {
((${#EXCLUDED_PKGS[@]} == 0)) && return 0
printf ' %sИсключено (не удаляем):%s\n' "$C_BOLD" "$C_RESET"
local p
for p in "${EXCLUDED_PKGS[@]}"; do
printf ' %-24s %s%s%s\n' "$p" "$C_DIM" "$(exclusion_reason "$p")" "$C_RESET"
done
}
print_plan_section() {
local total_bytes="$1"
section "A. Orphan-пакеты pacman (${#ORPHAN_PKGS[@]} шт.)"
if ((${#ORPHAN_PKGS[@]} == 0)); then
skip "orphan-пакетов к удалению нет"
else
printf ' %sЦели (реальный вывод `pacman -R --print`):%s\n' "$C_BOLD" "$C_RESET"
print_targets | sed 's/^/ /'
printf '\n %sРазмеры:%s\n' "$C_BOLD" "$C_RESET"
package_sizes | sort -t$'\t' -k2 -rn | awk -F'\t' '
{ printf " %-42s %10.2f MiB\n", $1, $2 / 1048576 }'
fi
print_excluded
section "B. Старые бэкенды LM Studio (${#LM_BACKEND_PATHS[@]} шт.)"
if ((${#LM_BACKEND_PATHS[@]} == 0)); then
skip "бэкендов к удалению нет (уже удалены или не установлены)"
else
local path
for path in "${LM_BACKEND_PATHS[@]}"; do
printf ' %s\n' "$path"
printf ' %s%s%s\n' "$C_DIM" "$(human_size "$(path_bytes "$path")")" "$C_RESET"
done
fi
if ((${#LM_BACKEND_MISSING[@]} > 0)); then
printf '\n %sОтсутствуют (пропущены, идемпотентность):%s\n' "$C_DIM" "$C_RESET"
local m
for m in "${LM_BACKEND_MISSING[@]}"; do
printf ' %s%s%s\n' "$C_DIM" "$m" "$C_RESET"
done
fi
printf '\n %sНе трогаем:%s vendor/ (CUDA-рантайм) и ...-nvidia-cuda12-avx2-2.67.0 (свежая)\n' \
"$C_DIM" "$C_RESET"
section "Прогноз"
printf ' пакеты pacman : %s\n' "$(human_size "$PACKAGES_BYTES")"
printf ' бэкенды LM : %s\n' "$(human_size "$LM_BACKEND_BYTES")"
printf ' %sИТОГО : %s%s\n' "$C_BOLD" "$(human_size "$total_bytes")" "$C_RESET"
printf ' %sСвободно на / : %s (было %s)%s\n' \
"$C_DIM" "$(human_size "$(root_free_bytes)")" "$(human_size "$FREE_BEFORE")" "$C_RESET"
}
# ============================================================
# Команды
# ============================================================
cmd_check() {
section "Проверка (read-only)"
printf ' orphan-пакетов найдено всего : %s\n' "${#ALL_ORPHANS[@]}"
printf ' из них исключено : %s\n' "${#EXCLUDED_PKGS[@]}"
printf ' к удалению (группа A) : %s\n' "${#ORPHAN_PKGS[@]}"
print_excluded
printf '\n старых бэкендов LM Studio : %s (из %s в белом списке)\n' \
"${#LM_BACKEND_PATHS[@]}" "${#LM_BACKEND_NAMES[@]}"
printf ' бэкендов отсутствует (ок) : %s\n' "${#LM_BACKEND_MISSING[@]}"
section "Прогноз освобождения"
printf ' пакеты pacman : %s\n' "$(human_size "$PACKAGES_BYTES")"
printf ' бэкенды LM : %s\n' "$(human_size "$LM_BACKEND_BYTES")"
printf ' %sИТОГО : %s%s\n' "$C_BOLD" "$(human_size "$TOTAL_BYTES")" "$C_RESET"
printf ' %sСвободно на / : %s%s\n' "$C_DIM" "$(human_size "$(root_free_bytes)")" "$C_RESET"
printf '\n %sНичего не изменено.%s Подробности: %s plan\n' "$C_DIM" "$C_RESET" "$SCRIPT_NAME"
}
cmd_plan() {
section "План удаления (read-only)"
print_plan_section "$TOTAL_BYTES"
}
# ------------------------------------------------------------
# apply
# ------------------------------------------------------------
cmd_apply() {
section "APPLY — удаление orphan-пакетов и старых бэкендов LM Studio"
if ((${#ORPHAN_PKGS[@]} == 0 && ${#LM_BACKEND_PATHS[@]} == 0)); then
skip "нечего удалять — всё уже чисто"
return 0
fi
# 1) Полный план.
print_plan_section "$TOTAL_BYTES"
# 2) Предупреждение про запущенный LM Studio (не убиваем процесс).
if lmstudio_running >/dev/null 2>&1; then
warn "LM Studio запущен — закройте его перед удалением бэкендов."
note "Процесс LM Studio не будет убит этим скриптом."
fi
# 3) Подтверждение.
printf '\n'
if ! confirm "Удалить перечисленное? Введите 'yes' для подтверждения: "; then
printf '\n'
fail "Отменено пользователем. Ничего не удалено."
return 0
fi
printf '\n'
# 4) Бэкап.
local stamp backup_dir
stamp="$(date +%Y%m%d-%H%M%S)"
backup_dir="$STATE_DIR/$stamp"
local n=1
while [[ -e "$backup_dir" ]]; do
backup_dir="$STATE_DIR/$stamp-$n"
n=$((n + 1))
done
mkdir -p -- "$backup_dir"
printf '%s\n' "${ORPHAN_PKGS[@]:-}" > "$backup_dir/packages-removed.txt"
local bnames=()
local path
for path in "${LM_BACKEND_PATHS[@]:-}"; do
bnames+=("$(basename -- "$path")")
done
printf '%s\n' "${bnames[@]:-}" > "$backup_dir/lmstudio-backends-removed.txt"
# Пустые строки от "${arr[@]:-}" на пустом массиве — убираем.
sed -i '/^$/d' "$backup_dir/packages-removed.txt" "$backup_dir/lmstudio-backends-removed.txt"
{
printf 'cleanup-packages.sh — отчёт об удалении\n'
printf 'Дата (UTC): %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S')"
printf 'Дата (лок): %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
printf 'Хост: %s\n' "$(hostname)"
printf '\n=== df ДО ===\n'
df -h / 2>/dev/null || true
printf '\n=== пакеты (имя, установленный размер) ===\n'
package_sizes | awk -F'\t' '{ printf "%-42s %s\n", $1, $2 }'
printf '\n=== бэкапы ===\n'
printf 'packages-removed.txt — %s пакет(ов)\n' "${#ORPHAN_PKGS[@]}"
printf 'lmstudio-backends-removed.txt — %s каталог(ов)\n' "${#LM_BACKEND_PATHS[@]}"
printf '\n=== df ПОСЛЕ ===\n'
} > "$backup_dir/manifest.txt"
chmod 600 -- "$backup_dir"/* 2>/dev/null || true
ok "бэкап создан: $backup_dir"
# 5) Удаление бэкендов LM Studio (от пользователя, sudo не нужен).
local removed_backends=0
local -a removed_backend_names=()
if ((${#LM_BACKEND_PATHS[@]} > 0)); then
section "Удаление бэкендов LM Studio"
for path in "${LM_BACKEND_PATHS[@]}"; do
if backend_allowed "$path"; then
if rm -rf -- "$path"; then
ok "удалён: $path"
removed_backends=$((removed_backends + 1))
removed_backend_names+=("$(basename -- "$path")")
else
fail "не удалось удалить: $path"
fi
else
fail "ПРОПУЩЕН (не прошёл проверку безопасности): $path"
fi
done
fi
# 6) Удаление пакетов pacman (нужен sudo).
if ((${#ORPHAN_PKGS[@]} > 0)); then
section "Удаление orphan-пакетов pacman"
if ! command -v sudo >/dev/null 2>&1; then
warn "sudo не найден — пакеты пропущены (бэкап сохранён, откат возможен)."
elif ! sudo -v; then
warn "sudo недоступен (не удалось получить права) — пакеты пропущены."
note "Бэкап: $backup_dir — откат возможен командой: $SCRIPT_NAME rollback"
else
# -n (--nosave) оставляет конфиги в .pacsave; --noconfirm — мы уже спросили.
if sudo pacman -Rns --noconfirm "${ORPHAN_PKGS[@]}"; then
ok "pacman завершил удаление пакетов"
else
fail "pacman завершился с ошибкой — часть пакетов могла остаться"
fi
fi
fi
# 7) Итог: df после + уточнение бэкапа (только реально удалённое).
local free_after
free_after="$(root_free_bytes)"
# Уточняем списки: оставляем только то, что действительно исчезло с системы.
local -a removed_pkg_names=()
local p
for p in "${ORPHAN_PKGS[@]:-}"; do
[[ -z "$p" ]] && continue
if ! pacman -Q "$p" >/dev/null 2>&1; then
removed_pkg_names+=("$p")
fi
done
printf '%s\n' "${removed_pkg_names[@]:-}" > "$backup_dir/packages-removed.txt"
printf '%s\n' "${removed_backend_names[@]:-}" > "$backup_dir/lmstudio-backends-removed.txt"
sed -i '/^$/d' "$backup_dir/packages-removed.txt" "$backup_dir/lmstudio-backends-removed.txt"
{
printf '\n=== df ПОСЛЕ (факт) ===\n'
df -h / 2>/dev/null || true
printf '\nУдалено пакетов: %s\n' "${#removed_pkg_names[@]}"
printf 'Удалено бэкендов: %s\n' "${#removed_backend_names[@]}"
printf 'Свободно до: %s\n' "$(human_size "$FREE_BEFORE")"
printf 'Свободно после: %s\n' "$(human_size "$free_after")"
printf '\n=== фактически удалённые пакеты ===\n'
printf '%s\n' "${removed_pkg_names[@]:-}" | sed '/^$/d'
printf '\n=== фактически удалённые бэкенды ===\n'
printf '%s\n' "${removed_backend_names[@]:-}" | sed '/^$/d'
} >> "$backup_dir/manifest.txt"
section "Готово"
printf ' удалено пакетов : %s\n' "${#removed_pkg_names[@]}"
printf ' удалено бэкендов : %s\n' "${#removed_backend_names[@]}"
printf ' свободно на / : %s → %s\n' \
"$(human_size "$FREE_BEFORE")" "$(human_size "$free_after")"
printf ' бэкап : %s\n' "$backup_dir"
printf '\n %sОткат: %s rollback%s\n' "$C_DIM" "$SCRIPT_NAME" "$C_RESET"
}
# ------------------------------------------------------------
# rollback
# ------------------------------------------------------------
cmd_rollback() {
section "ROLLBACK — восстановление пакетов из последнего apply"
local latest
latest="$(find "$STATE_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -n1 || true)"
if [[ -z "$latest" ]]; then
fail "бэкапов не найдено в $STATE_DIR"
return 1
fi
ok "последний бэкап: $latest"
local pkgs=()
if [[ -f "$latest/packages-removed.txt" ]]; then
mapfile -t pkgs < "$latest/packages-removed.txt"
fi
local backends=()
if [[ -f "$latest/lmstudio-backends-removed.txt" ]]; then
mapfile -t backends < "$latest/lmstudio-backends-removed.txt"
fi
if ((${#pkgs[@]} == 0)); then
skip "в бэкапе нет списка пакетов"
else
printf ' %sПакеты к переустановке (%s шт.):%s\n' "$C_BOLD" "${#pkgs[@]}" "$C_RESET"
local p
for p in "${pkgs[@]}"; do
[[ -z "$p" ]] && continue
printf ' %s\n' "$p"
done
printf '\n %sКоманда восстановления:%s\n' "$C_BOLD" "$C_RESET"
printf ' sudo pacman -S %s\n' "${pkgs[*]}"
printf '\n'
if confirm "Выполнить восстановление пакетов? Введите 'yes': "; then
if sudo -v && sudo pacman -S --noconfirm "${pkgs[@]}"; then
ok "пакеты переустановлены"
else
fail "восстановление не удалось"
fi
else
skip "восстановление не выполнялось (команда выше — скопируйте вручную)"
fi
fi
if ((${#backends[@]} > 0)); then
printf '\n %sБэкенды LM Studio (%s шт.) — перекачиваются автоматически:%s\n' \
"$C_BOLD" "${#backends[@]}" "$C_RESET"
local b
for b in "${backends[@]}"; do
[[ -z "$b" ]] && continue
printf ' %s\n' "$b"
done
note "LM Studio сам скачает нужную версию при следующем запуске."
fi
}
# ============================================================
# Точка входа
# ============================================================
main() {
local cmd="${1:-}"
case "$cmd" in
check|plan|apply|rollback) ;;
-h|--help|help) usage ;;
"") usage ;;
*) printf 'Неизвестная команда: %s\n\n' "$cmd" >&2; usage ;;
esac
if ! command -v pacman >/dev/null 2>&1; then
fail "pacman не найден — этот скрипт рассчитан на Manjaro/Arch"
exit 1
fi
collect_orphan_packages
collect_lmstudio_backends
PACKAGES_BYTES="$(packages_bytes)"
TOTAL_BYTES=$((PACKAGES_BYTES + LM_BACKEND_BYTES))
FREE_BEFORE="$(root_free_bytes)"
case "$cmd" in
check) cmd_check ;;
plan) cmd_plan ;;
apply) cmd_apply ;;
rollback) cmd_rollback ;;
esac
}
main "$@"