Update configuration and documentation for LLM support and local watchdog

- Added `ollama-models.yaml` to .gitignore and implemented logic to copy it in gpu-rent.ps1 and gpu-rent.sh.
- Enhanced env.example to include new variables for LLM runtime options and local watchdog configuration.
- Updated CLI commands to support LLM options during setup and execution, including new flags for Ollama and llama.cpp.
- Improved documentation in cli.md and README.md to reflect changes in LLM integration and local watchdog functionality.
- Adjusted architecture and decisions documentation to clarify the role of LLMs and local watchdog in the system.
This commit is contained in:
Leonid Pershin
2026-08-21 05:29:23 +03:00
parent a9cf2e0f90
commit 2005b00175
43 changed files with 2258 additions and 197 deletions
+1
View File
@@ -19,6 +19,7 @@ CLI-оркестратор: поднимает прерываемый GPU-сер
| [autocomplete.md](autocomplete.md) | Word-list промптов, Settings.fds, проверка версии |
| [local-folders.md](local-folders.md) | `Models/` / `Wildcards/` / `CustomWorkflows/` push, optional `Output/` pull |
| [swarmui.md](swarmui.md) | Нативный install, порт 7801, API, MCP |
| [llm.md](llm.md) | Opt-in Ollama / llama.cpp, `ollama-models.yaml`, порты 17811/17812 |
| [cli.md](cli.md) | Команды, конфиг, локальный state |
| [roadmap.md](roadmap.md) | Порядок реализации |
| [open-questions.md](open-questions.md) | Ещё не закрыто |
+28 -26
View File
@@ -34,23 +34,22 @@
| Модуль | Ответственность |
| --- | --- |
| `cli` | Typer: `up`, `tunnel`, `open`, `status`, `stop`, `destroy`, `logs`, `ssh`, `doctor`, `hold`, `seed-*`, `push-models`, `pull-output`, `resize-data`, `dry-run` |
| `config` | `.env`, валидация, пути к ключам |
| `cli` | Typer: `up`, `setup`, `tunnel`, `open`, `status`, `stop`, `destroy`, `logs`, `ssh`, `doctor`, `hold`, `seed-*`, `push` / `pull-output`, `resize-data`, `watchdog`, `dry-run` |
| `config` / `varsfile` | `.env` + `gpu-rent.vars`, пути |
| `state` | JSON сессии: ids, фаза, timestamps |
| `os_client` | `openstacksdk`, refresh IAM-токена |
| `inventory` | Flavors/images в сегменте, квоты, **фоллбек flavor** |
| `bootstrap` | Идемпотентный first-boot; после успеха — snapshot boot volume |
| `os_client` / `cloud` / `pools` | openstacksdk, ресурсы, скан пулов |
| `inventory` | Flavors/images, квоты, фоллбек flavor |
| `session` | `cmd_up` / `cmd_stop` / adopt |
| `bootstrap` + `remote/bootstrap.sh` | Идемпотентный first-boot; light без apt |
| `provision` | extensions, autocomplete, civitai seed, push, idle-killer arm, start SwarmUI |
| `doctor` | Preflight без mutating compute |
| `civitai_seed` | Манифест + Civitai API на `.red` |
| `models_push` | SFTP `./Models`, `./Wildcards`, `./CustomWorkflows` |
| `output_pull` | Опциональный SFTP с VM `Output/` |
| `git_seed` | Clone `extensions.yaml` |
| `autocomplete_seed` | Word-list + Settings.fds |
| `sync_files` | SFTP `Models` / Wildcards / workflows / Output |
| `notify` | Toast/звук при backend Idle |
| `tunnel` | paramiko / sshtunnel, порт **17801** |
| `watchdog` | EXPIRED → unshelve, пока туннель жив |
| `idle_killer` | systemd на VM + hold-файл + «качалка занята» |
| `reconcile` | Сироты по state и тегу `gpu-rent` |
| `tunnel` | sshtunnel + Nova EXPIRED watchdog |
| `local_watchdog` | Опциональный локальный тик → stop при unclean exit |
| `llm_runtime` / `setup_wizard` | Opt-in Ollama/llama.cpp + `ollama-models.yaml` |
| `idle_killer` / `hold` | systemd на VM + hold-файл |
| `ready` / `snapshot` | Idle backend + boot snapshot |
Каталог сервисов — из Keystone, не из выдуманного `api.selectel.ru/v3/`.
@@ -82,29 +81,32 @@
## Стейт-машина
Фазы в `state.json` (код пишет только эти):
```
idle
│ gpu-rent up
provisioning bootstrapping → seeding_extensions → seeding_autocomplete → seeding_models → waiting_ui
provisioning ← create volumes / server
ready_cloud ← compute жив, idle-killer вооружён
bootstrapping ← SSH + bootstrap + seed (пока не ready)
│ gpu-rent tunnel (опционально, пока ноут онлайн)
ready_tunneled ← localhost:17801
ready_cloud ← compute жив, idle-killer вооружён
хостер: EXPIRED
gpu-rent tunnel (или up с туннелем)
restoring (unshelve) → ready_cloud / ready_tunneled
ready_tunneled ← localhost:17801
│ stop | idle-killer | destroy
│ stop | idle-killer | local-watchdog | destroy
stopping → idle
idle
```
`ready_cloud` ≠ Nova `ACTIVE`. ACTIVE бывает раньше SSH и UI.
Подшаги seed (extensions / autocomplete / models) идут внутри `bootstrapping`, отдельными фазами в state не пишутся. `ready_cloud` ≠ Nova `ACTIVE` (ACTIVE бывает раньше SSH и UI).
На EXPIRED туннель сам делает unshelve → снова `bootstrapping`/`ready_*`.
## Idle-killer (на VM)
@@ -118,7 +120,7 @@ Killer молчит:
- пока существует hold: файл `/mnt/swarm_data/.gpu-rent-hold-until` с unix ts (пишет `gpu-rent hold`);
- пока SwarmUI качает модель в UI (Model Downloader / активный download — точный JSON на spike). Нет сигнала — пользователь жмёт `hold`.
`gpu-rent hold` без аргументов = +`IDLE_MINUTES` от сейчас. `--minutes 90`абсолютное продление. `--until` ISO опционально. `hold --clear` снимает.
`gpu-rent hold` без аргументов = +`IDLE_MINUTES` от сейчас. `--minutes 90`hold до now+90 мин (заменяет предыдущий, не складывает). `--until` ISO опционально. `hold --clear` снимает.
Потом счётчик 30 минут пустой очереди.
@@ -167,7 +169,7 @@ Reconcile: сервер с тегом `gpu-rent` есть, локального
| Слой | Выбор |
| --- | --- |
| Python 3.11+ | openstacksdk, paramiko, Typer, Rich, questionary, dotenv |
| Python 3.11+ | openstacksdk, paramiko, sshtunnel, Typer, Rich, dotenv, httpx, pyyaml |
| SSH | paramiko + sshtunnel (Windows без системного `ssh -L`) |
| Конфиг | `<repo>/.env` + runtime в `<repo>/.gpu-rent/` |
| Конфиг | `<repo>/.env` + `gpu-rent.vars` + runtime в `<repo>/.gpu-rent/` |
| Лицензия | MIT |
+20 -2
View File
@@ -8,7 +8,9 @@
| --- | --- |
| `gpu-rent flavors` | Скан `SCAN_POOLS` (ru-6 multizone…) × `FLAVOR_PREFERENCE`, затем список в текущем `OS_REGION_NAME` |
| `gpu-rent doctor` | Preflight **без** create: Keystone, квота GPU, flavor в AZ, диски, Civitai token+`.red`, манифесты, SSH-ключ. Код выхода ≠ 0, если сессию нельзя начать |
| `gpu-rent up` / `up --yes` | Preflight → create/unshelve → bootstrap → git update SwarmUI/extensions → **туннель** `localhost:17801`; Ctrl+C закрывает туннель |
| `gpu-rent setup` | Wizard: `.env`/манифесты, `LLM_RUNTIME`, пресет ollama-models, опционально local-watchdog |
| `gpu-rent up` / `up --yes` | Preflight → create/unshelve → bootstrap → git update → optional LLM → **туннель** `localhost:17801`; Ctrl+C закрывает туннель |
| `gpu-rent up --ollama` / `--llamacpp` / `--llm …` | Поднять LLM рядом со SwarmUI (см. [llm.md](llm.md)) |
| `gpu-rent up --no-update` | Без `git pull` SwarmUI и extensions (только недостающие clone) |
| `gpu-rent up --no-tunnel` | Только облако + bootstrap, без локального проброса |
| `gpu-rent up --no-spot` | Обычный (не preemptible) сервер |
@@ -32,13 +34,29 @@
| `gpu-rent pull-output` | Забрать новые файлы с VM `Output/` в `./Output` (сервер не чистим) |
| `gpu-rent seed-extensions` | Доклонировать/обновить git-репы; если VM жива — `systemctl restart swarmui` |
| `gpu-rent resize-data --gb 400` | Увеличить data volume вверх (Selectel online resize). Вниз нельзя |
| `gpu-rent watchdog install` | Локальный тик (Task Scheduler / systemd user / launchd): аварийное закрытие туннеля → `stop` после grace |
| `gpu-rent watchdog uninstall` | Снять локальный сервис |
| `gpu-rent watchdog status` | Установлен ли сервис + local lease |
| `gpu-rent watchdog tick` | Один тик (для планировщика; `--dry-run` без delete) |
| `gpu-rent dry-run` | План без mutating-вызовов |
Второй `up` при живой VM: не создавать второй GPU; сделать autocomplete-check и **push** локальных папок; если `PULL_OUTPUT` — подтянуть Output; напомнить про `tunnel` / `stop`. `--adopt` если нашли тег без state.
Нет команды `generate`. Нет зеркала каталога локального SwarmUI — только папки приложения, см. [local-folders.md](local-folders.md).
Сейчас в коде: после seed/`start swarmui` — idle-killer, Idle backend, boot snapshot, toast/MCP-сниппет, `resize-data`. `tunnel` с watchdog: EXPIRED → unshelve + reconnect FIP/SSH. UX-полировка flavors/цен ещё впереди.
Сейчас в коде: после seed/`start swarmui` — idle-killer, Idle backend, boot snapshot, toast/MCP-сниппет, `resize-data`. `tunnel` с Nova-watchdog: EXPIRED → unshelve + reconnect. Опционально `watchdog install` — локальный safety net. UX-полировка flavors/цен ещё впереди.
## Local watchdog
Опционально. **Idle-killer на VM** остаётся основным: ноут можно закрыть, GPU живёт до простоя. Local watchdog — если хочешь гасить GPU при «убили окно / ребут» без `stop`.
1. `gpu-rent watchdog install` (раз на машине, в корне репо).
2. Пока крутится `up`/`tunnel`, пишется heartbeat в `.gpu-rent/local-lease.json`.
3. **Ctrl+C** → detach, GPU **не** трогаем (как раньше).
4. Процесс умер / ребут → через `LOCAL_WATCHDOG_GRACE_MINUTES` (дефолт 10) тик вызывает `stop` (диски остаются).
5. `gpu-rent stop` чистит lease сам.
Без `install` поведение прежнее. Без открытого туннеля (`up --no-tunnel`) lease не вооружается — работает только VM idle-killer.
## `doctor`
+1 -1
View File
@@ -9,7 +9,7 @@ GPU в облаке дорогой. Веса для генерации карт
1. Арендовать **прерываемый** облачный сервер Selectel с GPU (~70% дешевле обычного, без SLA).
2. Хранить ОС, бинарники SwarmUI, модели, ComfyUI и результаты на **сетевых дисках**. Они переживают удаление и остановку VM.
3. Локальный CLI создаёт (или восстанавливает) сервер и отдельно, когда нужно, пробрасывает SwarmUI на `localhost:17801`.
4. GPU гасится командой `stop` **или** idle-killer на самой VM (пустая очередь). Диски остаются. Ноут можно закрыть — compute от этого не умирает.
4. GPU гасится командой `stop`, idle-killer на VM, или (опционально) local-watchdog при unclean exit. Диски остаются. Без watchdog ноут можно закрыть — compute не умирает.
Пользователь открывает браузер на `http://127.0.0.1:17801`, переключает Cursor MCP на этот URL или бьёт в HTTP API SwarmUI. Локальный SwarmUI на `7801` не трогаем.
+3 -2
View File
@@ -5,7 +5,7 @@
| Тема | Решение |
| --- | --- |
| Аудитория | Личный инструмент, репозиторий можно показать другим: MIT, без аккаунтных id и без имён локальных чекпоинтов |
| Сессия | GPU живёт до `stop` **или** простоя (idle-killer **на VM**). Ноут можно закрыть — compute не обязан умереть |
| Сессия | GPU живёт до `stop` **или** простоя (idle-killer **на VM**). Ноут можно закрыть — compute не обязан умереть. Опционально: `gpu-rent watchdog install` — локальный тик на Windows/macOS/Linux; если туннель умер без Ctrl+C/`stop`, после `LOCAL_WATCHDOG_GRACE_MINUTES` (дефолт 10) → `stop` |
| Модели | Пустой `./Models` → не грузим ничего. Появились веса + метадата → на `up` выгрузить эту пару. Civitai-seed отдельно. С сервера не удаляем |
| Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена |
| Пул GPU | Перед `up`/`flavors` сканируем `SCAN_POOLS` (дефолт `ru-6,ru-7`). `ru-6` — мультизональный: ходим на `https://ru-6.cloud.api.selcloud.ru/compute/` тем же токеном (SDK-каталог часто знает только RC-пул). Собираем типы GPU из extra_specs и совпадения с `FLAVOR_PREFERENCE`. Автоматом `.env` не пишем — печатаем рекомендацию `OS_REGION_NAME` / `GPU_RENT_AZ` |
@@ -21,6 +21,7 @@
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
| `up` / `tunnel` | `up` по умолчанию после ready открывает туннель `:17801`, печатает URL и ждёт. `--no-tunnel` — только облако. Ctrl+C на туннеле GPU не гасит (`stop` отдельно). Команда `tunnel` остаётся для повторного входа |
| Git update | На каждом `up` по умолчанию: `git pull` SwarmUI + репы из `extensions.yaml` + уже установленные на data (`Extensions`/`DLNodes`). `--no-update` или `UPDATE_GIT=false` — не тянуть |
| LLM (opt-in) | `none` по умолчанию. `ollama` / `llamacpp` — флаг `--ollama`/`--llamacpp`/`--llm`, `LLM_RUNTIME` в vars, или вопрос в interactive `up`/`setup`. Ollama-модели из `ollama-models.yaml`. Порты: Ollama 17811, llama.cpp 17812. Назначение: помощь с промптами, не замена SwarmUI |
| Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) |
| SSH | CLI генерирует `<repo>/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair |
| Локальные файлы | Всё в корне репозитория: `.env`, `models.yaml`, `extensions.yaml`; runtime (`state.json`, lock, SSH) в `<repo>/.gpu-rent/`. Не `%USERPROFILE%\.gpu-rent` |
@@ -41,6 +42,6 @@
1. **Туннель ≠ жизнь GPU.** `stop` и idle-killer не зависят от того, открыт ли SSH с ноутбука.
2. **Локальный SwarmUI на 7801 не трогаем.** Туннель по умолчанию на **17801** (на VM по-прежнему 7801 на loopback).
3. **`Ctrl+C` на туннеле не удаляет VM.** Иначе «закрыл ноут» невозможно. Чтобы убить GPU — `gpu-rent stop` или простой.
3. **`Ctrl+C` на туннеле не удаляет VM.** Иначе «закрыл ноут» невозможно. Чтобы убить GPU — `gpu-rent stop` или простой. При установленном **local-watchdog** Ctrl+C по-прежнему detach; убийство процесса/ребут без detach → stop после grace.
4. Idle-killer на VM **не может** быть `shutdown -h`: у Selectel остановленная изнутри VM часто продолжает тарифицировать ресурсы. Нужен вызов OpenStack: удалить **этот** compute, диски оставить.
5. Для этого на VM — OpenStack **application credential** с правом удалить/shelve сервер в проекте (не пароль владельца аккаунта). Компрометация SwarmUI в худшем случае сносит GPU-сессию, а не создаёт новые дорогие машины, если роль без `compute:create`.
+59
View File
@@ -0,0 +1,59 @@
# LLM рядом со SwarmUI (opt-in)
По умолчанию поднимается только SwarmUI. Ollama или llama.cpp — по флагу, `LLM_RUNTIME` в `gpu-rent.vars` / `.env`, или через wizard.
## Включение
```text
gpu-rent setup # спросит none/ollama/llamacpp + пресет моделей
gpu-rent up --ollama # разово
gpu-rent up --llm llamacpp
# или в gpu-rent.vars:
LLM_RUNTIME=ollama
```
Без параметров `gpu-rent` / `gpu-rent up` (без `--yes`) спросит про LLM, если в vars ещё `none`.
## Порты (только loopback + туннель)
| Сервис | VM | localhost |
| --- | --- | --- |
| SwarmUI | 7801 | 17801 |
| Ollama | 11434 | 17811 |
| llama.cpp | 8080 | 17812 |
```text
gpu-rent tunnel
gpu-rent open --llm # http://127.0.0.1:17811 (Ollama)
# клиент:
set OLLAMA_HOST=http://127.0.0.1:17811
```
## Ollama models
Как Civitai `models.yaml`:
- `ollama-models.example.yaml` — в git
- `ollama-models.yaml` — локальный (gitignore)
На `up` при `LLM_RUNTIME=ollama` CLI делает `ollama pull` по списку. Уже скачанные не трогает; лишние на диске не удаляет.
### Пресеты setup
| preset | tag | зачем |
| --- | --- | --- |
| **recommended** | `huihui_ai/qwen2.5-abliterate:7b` | RU/EN, ~5GB, мало отказов — помощь с промптами |
| light | `qwen2.5:3b` | быстрее, слабее |
| stock | `qwen2.5:7b` | официальный, больше цензуры |
| alt | `richardyoung/qwen2.5-7b-instruct-abliterated` | другой abliterate |
| empty | `[]` | только runtime |
Community abliterate-модели без гарантий безопасности — для личного prompt-help / NSFW-тегов рядом со SwarmUI.
## llama.cpp
Ставит `llama-server` и systemd. GGUF клади вручную в `/mnt/swarm_data/llamacpp/models` на data-диске (или через SSH), затем `systemctl restart gpu-rent-llamacpp`.
## Idle-killer
Busy также если идёт `ollama pull`, в Ollama есть loaded model, или llama.cpp занимает слоты.
+1 -1
View File
@@ -7,7 +7,7 @@
- Качалка модели в UI: отдельного poll-API нет (`DoModelDownloadWS` только WS) → v1 считает busy через `waiting_gens` / `live_gens` / `loading_models` / backend≠idle; иначе пользователь жмёт `hold`.
- Реальная цена 100 GB сетевого диска в выбранном сегменте и цена 1×4090 preemptible ₽/час (в OpenStack API нет).
- Имя GPU-образа (без Docker) и flavor id в твоём пуле (в git не класть); какие из списка фоллбека реально есть.
- Хватает ли application credential с правами сервисного `member`, или Selectel отдаёт более узкую роль «только delete».
- Хватает ли application credential с access_rules (delete/GET server) на Selectel — при отказе CLI падает назад на unrestricted cred и пишет в лог.
- Snapshot attached boot volume после Idle: время и можно ли сразу create from snapshot.
Если spike покажет, что 45 минут льготы мало на первую установку ComfyUI — поднять `IDLE_GRACE_MINUTES`, не отключать killer.
+111
View File
@@ -0,0 +1,111 @@
# Project Review — 2026-08-21 (1)
## Prior Reviews Summary
> Based on the last 3 review files analysed in Phase 0.
### Still Open (carried forward)
None.
### Resolved Since Last Review
None.
---
## Phase 1: Code Quality
### SOLID
- `session.cmd_up` / `_bind_access` orchestrate cloud, SSH, bootstrap, provision, snapshot, notify — god-flow (`src/gpu_rent/session.py`).
- Docs name separate modules (`civitai_seed`, `models_push`, `git_seed`, `watchdog`, `reconcile`) that do not exist as packages; logic is folded into `provision.py`, `sync_files.py`, `tunnel.py`, `cli.status`.
- `Config` dataclass is a wide bag of unrelated settings (auth, paths, idle, autocomplete, civitai).
### Performance
- `push_tree` / `pull_tree`: per-file `remote_sha256` + `put_file` each open a new SSH (`sync_files.py`, `ssh_ops.py`).
- Every `up` re-runs full `bootstrap.sh` (apt-get) via `_bind_access` even when already bootstrapped.
- `scan_pools` on every `up`: extra Keystone authorize + per-flavor HTTP extra_specs (ThreadPool 16).
- Tunnel watchdog: `connect()` / Keystone authorize every ~30s.
### Correctness & Bugs
- `wait_ssh` aborts after 3 `AuthenticationException`s (~15s) while cloud-init may still be injecting keys (`ssh_ops.py` ~114120).
- `cmd_stop` leaves `bootstrapped=True` (`session.py` ~397400) → next ACTIVE path mis-labeled / wrong branch when FIP cleared.
- `phase` set to `ready_cloud` before bootstrap finishes (`session.py` ~344346).
- Application credential created without role/access_rules restriction (`idle_killer.py` ~5257) vs architecture “delete/shelve only”.
- `GIT_TOKEN` embedded in git remote URL and never stripped (`clone_ext.py` `with_token` / clone).
- SG rules accumulate old CIDRs; never prune (`cloud.py` `ensure_security_group`).
- Session tests out of sync: no `bootstrapped`+FIP setup for “second GPU”; `run_bootstrap` mock missing `update=` (`tests/test_session.py`).
### Code Quality
- State machine phases in docs (`bootstrapping`, `seeding_*`, `waiting_ui`) never written by code (only `idle` / `provisioning` / `ready_*`).
- `os_client.connect` reports `app_version="0.1.0"` while package is `0.2.0`.
- Duplicated SwarmUI busy/Idle polling logic in `ready.py` and `remote/idle_killer.py`.
---
## Phase 2: Logical Consistency
### Domain & Application Layer
- Architecture table invents module names; actual layout is flatter (`cli``session`/`doctor`/`tunnel``cloud`/`provision`/`ssh_ops`).
### Data Flow
- Config sources: `.env` (dotenv override=False) then `gpu-rent.vars` (override empty only) — OK.
- Volume IDs: `state` preferred over `BOOT_VOLUME_ID` / `DATA_VOLUME_ID` env — OK; AZ not re-validated on reuse.
### State Management
- `bootstrapped` not cleared on `stop`.
- `bootstrapped` True only at end of `_bind_access`; failure mid-bind leaves `ready_cloud` + `bootstrapped=False`.
- Marker files on VM (`/opt/swarmui/.gpu-rent-bootstrapped`) independent of local `state.bootstrapped`.
### Consistency
- Error handling: mostly `GpuRentError` / `CloudError`; some soft-log (`wait_backend_idle`, snapshot, idle-killer arm).
- Civitai bad token is **blocking** in doctor → blocks `up` even if user only needs default SwarmUI models.
---
## Phase 3: UI/UX (CLI)
### Usability
- `up` always prints full doctor table (noisy).
- Invalid/expired `CIVITAI_API_TOKEN` blocks entire session start.
- `destroy` has no `--no-pull` (always respects `PULL_OUTPUT` via `cmd_stop`).
### Visual & Consistency
- N/A (CLI). Mixed RU/EN messages by design.
### Interaction & Feedback
- Confirm on `up` without `--yes` is good; `--yes` skips.
- Tunnel Ctrl+C messaging is clear.
- `logs` only tails cloud-init + `is-active`, not `journalctl -u swarmui` as docs imply.
### Accessibility
- N/A.
---
## Full exploration notes (sections 110)
See chat report for narrative. Tasks below are actionable.
---
## Tasks
- [x] 1. [Bug] Soften `wait_ssh` early auth abort (retry until timeout / distinguish “sshd up, keys not ready”) — `src/gpu_rent/ssh_ops.py` line 114
- [x] 2. [Bug] Clear `bootstrapped=False` (and align phase) in `cmd_stop``src/gpu_rent/session.py` line 397
- [x] 3. [Security] Restrict idle-killer application credential (access_rules / roles delete-only) — `src/gpu_rent/idle_killer.py` line 52
- [x] 4. [Security] Strip `GIT_TOKEN` from git `origin` after clone/fetch — `src/gpu_rent/remote/clone_ext.py` line 30
- [x] 5. [Security] Stop accumulating stale SSH SG CIDRs; replace or prune old /32 — `src/gpu_rent/cloud.py` line 154
- [x] 6. [Security] Revisit default `GPU_RENT_SSH_CIDR=0.0.0.0/0` in `env.example``env.example` line 16
- [x] 7. [Performance] Reuse one SSH/SFTP session in `push_tree` / `pull_tree` / hash checks — `src/gpu_rent/sync_files.py` line 15
- [x] 8. [Performance] Skip full bootstrap (or apt) when VM already marked bootstrapped — `src/gpu_rent/session.py` line 87
- [x] 9. [Logic] Do not set `phase=ready_cloud` before `_bind_access` succeeds — `src/gpu_rent/session.py` line 344
- [x] 10. [Logic] Make doctor Civitai failure non-blocking when seed is optional — `src/gpu_rent/doctor.py` line 271
- [x] 11. [CodeQuality] Fix `tests/test_session.py` mocks (`bootstrapped`+FIP; `run_bootstrap(..., update=)`) — `tests/test_session.py` line 40
- [x] 12. [UX] `logs` should include `journalctl -u swarmui` as docs claim — `src/gpu_rent/cli.py` line 404
- [x] 13. [Logic] Update `docs/architecture.md` module names, phases, questionary, hold `--minutes` wording — `docs/architecture.md` line 37
## Closure notes (2026-08-21)
- `wait_ssh`: auth streak give-up after 180s continuous AuthenticationException (not 3 attempts).
- Idle-killer: access_rules for GET/DELETE server; fallback without rules if Keystone rejects.
- Bootstrap light path: `GPU_RENT_BOOTSTRAP_LIGHT=1` skips apt when marker present.
- Also fixed `os_client` `app_version` → package `__version__` (noted in Phase 1, not a numbered task).