Compare commits

1 Commits
Author SHA1 Message Date
oqyude 3120ceaf40 n8n added 2026-06-14 18:31:49 +03:00
180 changed files with 4615 additions and 10660 deletions
-36
View File
@@ -1,36 +0,0 @@
{
"metaagent_version": "3.0.0",
"session_id": "metaagent-init-2026-10-09",
"target_repo": "S:/Git/nixos",
"goal": "Установить metaagent, перенести накопленные данные (AGENTS.md, docs/arch/*) в структуру .agent/.",
"project_type": "existing",
"phases": {
"init": "completed",
"analyse": "completed",
"roadmap": "completed",
"design": "skipped",
"decomposition": "completed",
"execution": "in_progress",
"metastate": "pending",
"handoff": "pending"
},
"tasks": [
{ "id": "T1", "title": "A1: mobile.nix импортирует несуществующий lib/xlib.nix", "status": "pending", "origin": "user:direct" },
{ "id": "T2", "title": "A2: убедиться, что nix flake check вообще запускается", "status": "pending", "origin": "user:direct" },
{ "id": "T3", "title": "A3: явная финальная политика nftables на VDS", "status": "pending", "origin": "user:direct" },
{ "id": "T4", "title": "B1: guard на несмонтированный носитель /mnt/services", "status": "pending", "origin": "user:direct" },
{ "id": "T5", "title": "B2: зафиксировать, что бэкапов в конфигурации нет", "status": "pending", "origin": "user:direct" },
{ "id": "T6", "title": "C1: вернуть расследование 3x-ui, потерянное при откате", "status": "pending", "origin": "user:direct" },
{ "id": "T7", "title": "C2: зафиксировать фактические версии панели и ядра 3x-ui", "status": "pending", "origin": "user:direct" },
{ "id": "T8", "title": "C3: убрать сервис автообновления 3x-ui", "status": "pending", "origin": "user:direct" },
{ "id": "T9", "title": "C4: записать, что ядро Xray — состояние панели, а не Nix", "status": "pending", "origin": "user:direct" },
{ "id": "T10", "title": "C5: решить судьбу reality443Forwarding", "status": "pending", "origin": "user:direct" },
{ "id": "T11", "title": "D1: пробросы роутера — главный недостающий инвариант", "status": "pending", "origin": "user:direct" },
{ "id": "T12", "title": "D2: зафиксировать 100.64.0.0 как Tailscale-адрес sapphira", "status": "pending", "origin": "user:direct" },
{ "id": "T13", "title": "D3: убрать мёртвое правило firewall на sapphira", "status": "pending", "origin": "user:direct" },
{ "id": "T14", "title": "E1: написать AGENTS.md в корне (с metaagent-шапкой)", "status": "completed", "origin": "user:direct" },
{ "id": "T15", "title": "E2: выбрать проверки, которые заменят половину инвариантов", "status": "pending", "origin": "user:direct" },
{ "id": "T16", "title": "E3: судьба 15 закомментированных модулей", "status": "pending", "origin": "user:direct" }
],
"last_updated": "2026-10-09T20:30"
}
-119
View File
@@ -1,119 +0,0 @@
# Analysis Report
## Session
- **Session ID:** `metaagent-init-2026-10-09`
- **Target repo:** `S:/Git/nixos`
- **Date:** 2026-10-09
- **Project type:** `existing` (NixOS-конфиг, 120 `.nix`, ~8.5k строк)
## 1. Общая информация
- **README:** одна строка без смысла (`"I'm a super newbie who just posted my stuff here. Now maybe about intermediate"`); функциональную роль README играет `AGENTS.md` (корень).
- **Лицензия:** не указана.
- **CI/CD:** отсутствует. `nix flake check` — единственная автоматическая защита, прогоняется вручную. `deploy/default.nix:27-29` — `checks = builtins.mapAttrs (... deployChecks)`, но они покрывают только deploy-сценарий.
- **Точка входа:** `flake.nix` → `nixosConfigurations.<attr>` (хосты) + `nixOnDroidConfigurations.<attr>` (Android). Реестр хостов: `configurations/default.nix:13-39`.
- **Система сборки:** Nix + NixOS flakes. Home-manager, sops-nix, disko, deploy-rs, grub2-themes, justray.
## 2. Стек технологий (existing)
| Компонент | Значение |
|---|---|
| Язык | Nix (`.nix`) |
| Фреймворк | NixOS modules + home-manager |
| База данных | PostgreSQL (в `modules/server/postgresql.nix`), sqlite (3x-ui) |
| Тестовый раннер | отсутствует (см. §5) |
| Пакетный менеджер | Nix (`nix flake`, `nix-env`, `nix profile`) |
| Линтер/форматтер | отсутствует (комментарий-density 38/120 файлов без комментариев — открытый вопрос 0.2) |
## 3. Архитектура (existing)
```
flake.nix
├── configurations/ ← реестр хостов (1 запись = 1 машина)
│ ├── default.nix ← hosts + xlib + mkSystem
│ ├── <host>.nix ← модульное тело хоста
│ └── hardware/<host>.nix
├── home/ ← home-manager (per device-type)
├── modules/
│ ├── options.nix ← кросс-модульные опции
│ ├── default.nix ← defaultModule + strictModule (для nix-on-droid)
│ ├── essentials/ ← packages, services, settings, ssh, shell, systemd-routines
│ └── <type>/ ← per-type: desktop/, server/, vds/, wsl/, containers/, termux/, other/
├── lib/
│ ├── mkSystem.nix ← nixosSystem + specialArgs(xlib, inputs)
│ └── xlib/ ← чистые данные: devices, dirs, helpers
├── overlays/, pkgs/, deploy/, secrets/ (sops)
└── .sops.yaml ← один age-ключ на secrets/<name>.(yaml|json|env|ini)
```
**Паттерн:** модульный монолит с xlib-инъекцией (аналог dependency injection через `specialArgs`).
**Ключевые модули:**
| Модуль | Описание |
|---|---|
| `configurations/default.nix:13-39` | Реестр хостов (7 entries); `mkXlib` в строке 50 |
| `configurations/<host>.nix` | Per-host: имя, тип, импорт модулей; 7 файлов |
| `lib/xlib/default.nix:38-77` | `mkXlib` — единственная точка сборки xlib |
| `lib/xlib/device.nix:12-41` | Закрытое множество device.types: { minimal, primary, secondary, server, vds, wsl, termux } |
| `lib/xlib/helpers.nix` | `mkBindMount`, `mkSystemdBind`, `mkServiceStorage`, `mkNtfsMount`, `mkExfatMount`, `mkTmpDirs`, `mkSymlinks` |
| `modules/options.nix` | Кросс-модульные опции: `host.builder.*`, `host."3x-ui".*` |
| `modules/default.nix:9-39` | `nixosModules.default` — импортируется на каждый NixOS-хост; `nixosModules.strict:40-53` — для nix-on-droid |
| `modules/essentials/` | `packages`, `services`, `settings`, `ssh`, `shell`, `systemd-routines` |
| `modules/users.nix` | Пользователь `oqyude` (uid 1000, sapphira=1001), sops-секреты, hostKey bootstrap |
| `modules/server/` | 20+ системных сервисов sapphira (см. `server/default.nix:imports`) |
| `modules/server/systemd.nix` | rsync oneshots с `requiresMountsFor` guard (единственный пример guard'а) |
| `modules/server/{nginx,coredns}.nix` | Reverse proxy + DNS для зон `zeroq.su` и `home.arpa` |
| `modules/containers/` | podman-контейнеры: 3x-ui (заморожен), tape-rotation, remnanode, kokoro-tts, openhands, remnawave-examples |
| `home/<type>.nix` | Home-manager per device-type; для `root` — без профиля |
| `home/modules/opencode.nix` | OpenCode CLI + systemd user services (см. R2 — `Service` vs `serviceConfig`) |
| `deploy/default.nix` | deploy-rs ноды: sapphira, otreca, rydiwo (НЕ atoridu/wsl/epral) |
## 4. Конвенции (existing)
- **Стиль:** Nix-форматирование, разные отступы в разных файлах (нет единого стандарта).
- **Импорты:** `imports = [ ./foo.nix ./bar.nix ];` или list-spread. `lib.optional` для условных импортов.
- **Типизация:** types из `lib.types` (например, `lib.types.str`, `lib.types.bool`, `lib.types.attrsOf`).
- **Обработка ошибок:** `throw` + literal-сообщения (например, `lib/xlib/device.nix:12-41` — throw со списком валидных типов).
- **Логирование:** не формализовано; rsync oneshots в `modules/server/systemd.nix` — единственный пример с `-v` + structured output.
## 5. Тесты (existing)
- **Команда запуска:** отсутствует. Единственная полуавтоматическая проверка — `nix flake check`.
- **Всего тестов:** 0
- **Пройдено:** N/A
- **Упало:** N/A
- **Пропущено:** N/A
- **Упавшие тесты:** N/A
Кандидаты на CI-проверки (открытый вопрос 11.2):
1. ни одного `:latest` в образах (grep по `image =`)
2. `nix flake check` зелёный — уже ловит A1
3. домены в `coredns.nix` ↔ vhost'ы в `nginx.nix` совпадают в обе стороны
4. для каждого потребителя `mkServiceStorage` каталог существует на `External`
5. последнее правило самописной nftables-цепочки явное
6. `listen.addr` — адрес интерфейса, а не сеть
7. все файлы в `secrets/` матчат `path_regex` из `.sops.yaml`
## 6. Базовая проверка (existing)
- **Сборка:** не проверена в этой сессии (нет `nix` в PATH, AGENTS.md ссылается на `nix flake check`).
- **Запуск:** N/A (NixOS-конфиг, не приложение).
- **Git status:** рабочее дерево было чистым после коммита `7c9aa24` (yaml frontmatter в AGENTS.md). 1 modified файл (AGENTS.md) — fixed.
## 7. Требования (N/A для existing)
## 8. Примечания
- **Проход по репозиторию 2026-10-05**: 120 `.nix`, ~8.5k строк. Подтверждённые
инварианты (1–6) зафиксированы в `.agent/rules/project-rules.md` (R1).
- **Слои 9–11** (home-manager, deploy, формат) перенесены в `.agent/roadmap/sources.md`
как открытые вопросы; см. §9.2, §10.1-10.4, §11.1-11.2 исходного `invariants.md`.
- **Шаблон инварианта** (4 оси: Утверждение, Где, Почему, Действие) — для новых
записей. Обратное: до правки `authelia.nix:51,108,109` шёл через хелпер
`sopsPath = name: "/run/secrets/${name}"`; `open-webui.nix:95`, `tape-rotation.nix:61`,
`remnawave.nix:61, 129, 136` — литеральный хардкод. См. ADR-0001.
- **Bootstrap-цикл ключа** (R3) — должен быть задокументирован, иначе при
переустановке хоста агент не выведет. Открытый вопрос 4.2.
-107
View File
@@ -1,107 +0,0 @@
# Project State
# Auto-generated — updated by ANALYSE (initial) and METASTATE (on updates)
**Last updated:** 2026-10-09T20:30
**Session:** metaagent-init-2026-10-09
## Project Type
`existing` — NixOS-конфиг домашнего флота. 7 host-outputs (6 NixOS + 1 nix-on-droid).
## Tech Stack
| Category | Technology |
|----------|-----------|
| Language | Nix |
| Framework | NixOS modules + home-manager |
| Database | PostgreSQL (sapphira), sqlite (3x-ui) |
| Test runner | none — `nix flake check` is the only guard |
| Package manager | Nix (flakes) |
| Linter/formatter | none |
## Current Architecture
Модульный монолит с xlib-инъекцией. `flake.nix` → `nixosConfigurations.<attr>` через `configurations/default.nix`. Каждый хост получает `xlib` (identity + dirs + helpers) через `specialArgs` в `lib/mkSystem.nix`. `modules/default.nix` импортирует `nixosModules.default` (все NixOS) или `nixosModules.strict` (только nix-on-droid).
```
configurations/ → 7 хостов (default, atoridu, rydiwo, otreca, sapphira, wsl, epral)
modules/ → essentials + per-type (desktop/server/vds/wsl/containers/termux)
home/ → home-manager per device-type
lib/xlib/ → чистые данные (devices, dirs, helpers)
deploy/ → deploy-rs ноды: sapphira, otreca, rydiwo
secrets/ → sops, один age-ключ
```
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| `configurations/default.nix` | existing | Реестр хостов + `mkXlib` |
| `lib/xlib/default.nix` | existing | `mkXlib` — единственная точка сборки xlib |
| `modules/default.nix` | existing | `defaultModule` (NixOS) + `strictModule` (nix-on-droid) |
| `modules/options.nix` | existing | Кросс-модульные опции |
| `modules/users.nix` | existing | Пользователь + sops + hostKey bootstrap |
| `modules/essentials/ssh.nix` | existing | openssh + hostKey |
| `modules/server/{nginx,coredns}.nix` | existing | Reverse proxy + DNS (`zeroq.su`, `home.arpa`) |
| `modules/server/postgresql.nix` | existing | PostgreSQL + `mkServiceStorage` (нужен B1 guard) |
| `modules/server/systemd.nix` | existing | rsync oneshots с `requiresMountsFor` |
| `modules/containers/3x-ui.nix` | frozen | Панель :latest, ядро Xray 26.7.x |
| `home/<type>.nix` | existing | Per-type home-manager |
| `home/modules/opencode.nix` | existing | OpenCode CLI + systemd user (см. R2) |
| `deploy/default.nix` | existing | deploy-rs: sapphira, otreca, rydiwo |
## Hosts
| Attr | hostname | device.type | deploy | stateVersion | Примечание |
|---|---|---|---|---|---|
| `default` | nixos | minimal | — | — | Шаблон |
| `atoridu` | atoridu | primary | — (manual) | 26.05 | xanmod, mini-PC, без nixos-hardware |
| `rydiwo` | rydiwo | secondary | deploy-rs | 26.05 | Chuwi MiniBook, NTFS `lamet-drive` (mask=0000) |
| `otrecа` | otreca | vds | deploy-rs | 25.05 | VPS, Tailscale-only SSH, nftables (нужен A3) |
| `sapphira` | sapphira | server | deploy-rs | 25.05 | Домашний сервер, `firewall.enable = false` намеренно |
| `wsl` | wsl | wsl | — (manual) | 24.11 | WSL на vetymae (Windows 192.168.1.100) |
| `epral` | epral | termux | — | 24.05 | Android nix-on-droid, через `mobile.nix` |
## Services (sapphira, активные)
20+ системных сервисов + 6 контейнеров. Полный инвентарь в `tasks/manifest.json` (задачи A1-F). Все, использующие `mkServiceStorage`, нуждаются в B1 guard: postgresql, samba, homebox, gitea, navidrome, syncthing, uptime-kuma, immich, nextcloud, calibre-web, 3x-ui, tape-rotation.
## Network
```
LAN 192.168.1.0/24:
.20 sapphira (зашит в ~30 мест; см. вопрос 6.6)
.1 роутер (gateway)
.100 vetymae (Windows-хост с WSL)
Tailscale CGNAT 100.64.0.0/10:
100.64.0.0 = sapphira (назначен вручную, в 4 файлах)
100.86.62.4 = opencode на vetymae
100.106.21.39 = miniflux
Internet:
sapphira: пробросы роутера = 22, 80, 443, 8443 (xray), 22000 (syncthing)
```
## Decisions in Effect
| ADR | Decision | Status |
|-----|----------|--------|
| ADR-0001 | sops-пути: `config.sops.secrets.<name>.path` (не литерал) | active |
Подробнее: `.agent/decisions/0001-sops-secrets-paths.md`.
## Testing Status
Тестов нет. Единственная защита — `nix flake check`. Кандидаты на CI-проверки (7 штук) — в `analysis-report.md §5`.
## Open Concerns
- **B1**: нет guard'а на несмонтированный `/mnt/services` — сервисы стартуют на пустой БД.
- **A3**: nftables на VDS без явной финальной политики — неявный accept.
- **C1–C5**: 3x-ui заморожен, но без формального ADR; `podman.autoPrune` + `:latest` = деградация без коммита.
- **2.2/5.5**: `vetymae` / `lamet` / `therima` / `soptur` в `dirs.nix` — природа неясна.
- **4.1/4.2**: root-SSH и bootstrap-цикл ключа — не задокументированы.
- **6.6**: `192.168.1.20` зашит в 30 мест — рефакторинг отложен.
Полный список — в `roadmap/sources.md` (открытые вопросы слоёв 0-11).
@@ -1,83 +0,0 @@
# ADR-0001: sops-пути — только через `config.sops.secrets.<name>.path`
**Статус:** accepted
**Дата:** 2026-10-09
**Контекст:**
sops-nix материализует секреты на `/run/secrets/<attr>` по умолчанию.
Атрибут sops-блока (`config.sops.secrets.<attr>`) — единственный источник
истины для on-disk пути. Любой `path =` override на sops-блоке плюс хардкод
`"/run/secrets/<attr>"` в потребителе делает потребителя **молча**
сломанным: `nixos-rebuild` проходит, сервис стартует, файл читается — но
контент от прошлой версии или пустой. Симптом приходит из рантайма, не из CI.
До этой правки (см. «Обратное») в кодовой базе были хардкоды путей в 5
местах: `authelia.nix`, `open-webui.nix`, `tape-rotation.nix`, `remnawave.nix`.
В `remnawave.nix` тот же риск был двойной: путь хардкожен и в генераторе,
и в контейнере → расхождение двух копий = silent breakage.
**Рассматриваемые альтернативы:**
1. **A. `${config.sops.secrets.<attr>.path}`** — единая точка истины. Любой
`path =` override автоматически подхватывается потребителем.
2. **B. Хелпер `sopsPath = name: "/run/secrets/${name}"`** — был в `authelia.nix:51`
до правки. Компактнее в написании, но тащит хардкод `/run/secrets/` в API
и делает невозможным `path =` override без правки потребителя.
3. **C. `let envFile = "/run/secrets/${name}"; in { … }` для композитных
случаев** — для `remnawave.nix`, где один и тот же env-файл читается и
генератором, и контейнером. Используется, но с явным комментарием.
**Решение:** выбран вариант **A** для прямого доступа к одному секрету и
вариант **C** для композитных env-файлов в `remnawave.nix` (с комментарием).
**Обоснование:**
- **Единая точка истины.** Атрибут sops-блока — единственное место, где
определяется on-disk путь. Потребитель ссылается на `${config.sops.secrets.<attr>.path}`.
- **Симметрия с `mkUserSecret`.** `users.nix:33-41` уже использует этот
паттерн (`config.sops.secrets.<name>.path`) — единый стиль по репо.
- **Без хелпера.** `sopsPath = name: "/run/secrets/${name}"` выглядит
компактнее, но скрывает хардкод `/run/secrets/`. Когда кто-то добавит
`path = "/var/lib/..."` в sops-блок, потребитель через хелпер молча
сломается.
- **Двухкопийный env-файл в remnawave.nix** — композитный случай, где
`let`-биндинг в одном scope с комментарием «не дублировать литерал»
делает связь явной.
**Последствия:**
- Позитивные:
- `nix flake check` (после T1, T2) ловит несоответствие путей в compile-time.
- `path =` override в sops-блоке не ломает потребителя молча.
- Единый стиль по репо: 5 мест исправлены, новые пишутся по образцу.
- Негативные:
- Длиннее в написании, чем `"/run/secrets/${name}"`.
- Риски:
- Если кто-то добавит нового потребителя sops и напишет литерал
`/run/secrets/<name>` — молчаливое расхождение. Защита: код-ревью +
кандидат в CI-проверки (`secrets/missing-paths.nix` или grep).
**Invariant:**
> Любой потребитель sops-секрета в `modules/` ссылается на путь через
> `${config.sops.secrets.<attr>.path}`, а не через литерал
> `"/run/secrets/<attr>"`. Атрибут sops-блока — единственный источник
> истины для on-disk пути.
Зафиксировано в `.agent/rules/project-rules.md` (R1.7) + `analysis-report.md §8`.
**Обратное (где было сломано до этой правки):**
- `modules/server/authelia.nix:51,108,109` — через хелпер `sopsPath = name: "/run/secrets/${name}"`.
- `modules/containers/open-webui.nix:95` — литеральный хардкод.
- `modules/containers/tape-rotation.nix:61` — литеральный хардкод.
- `modules/containers/remnawave.nix:61, 129, 136` — литеральный хардкод, в двух местах (генератор + контейнер).
**Затронутые файлы (после правки):**
- `modules/server/authelia.nix:107-108`
- `modules/containers/open-webui.nix:101`
- `modules/containers/tape-rotation.nix:63`
- `modules/containers/remnawave.nix:16, 70, 138, 145` — `let`-биндинг для композитного env-файла (вариант C).
-14
View File
@@ -1,14 +0,0 @@
{
"version": "3.0.0",
"updated_at": "2026-10-09T20:30",
"decisions": [
{
"id": "0001",
"title": "sops-пути — только через config.sops.secrets.<name>.path",
"status": "accepted",
"date": "2026-10-09",
"file": ".agent/decisions/0001-sops-secrets-paths.md",
"tags": ["sops", "secrets", "security", "invariant"]
}
]
}
-161
View File
@@ -1,161 +0,0 @@
# Roadmap Sources
Источники задач для фазы DECOMPOSITION. Собрано при проходе по репозиторию
2026-10-05, ответы владельца учтены. Полный Q&A-источник (с разделами «Вопрос»,
«Факт», «Риск», «Кандидат») восстановим из git-истории:
`git log -p docs/arch/invariants.md | less` (последний коммит, где Q&A был полным).
Пометки: `[!]` — найденный дефект, не вопрос. `[?]` — не смог определить по коду.
`[✓]` — отвечено владельцем 2026-10-05.
## Статус ответов (2026-10-05)
Отвечено: **2.1, 5.2, 6.1, 6.3, 6.5, 7.1, 7.2** (7 пунктов). Остальные ждут
ответа (таблица ниже). Ключевое из ответов:
- **6.5** — `100.64.0.0` не «сетевой адрес вместо интерфейса»: Tailscale-адрес
sapphira, назначен вручную. См. ADR R1.4.
- **6.3** — `firewall.enable = false` на sapphira не недосмотр: граница на
роутере (5 портов). См. ADR R1.3 + задачу D1.
- **7.2** — 3x-ui рабочий, откат осознанный. Состояние «заморожено», не сломано.
См. ADR R1.5 + задачи C1–C5.
- **5.2** — подтверждённая дыра в защите данных → задача B1.
## Сводка подтверждённых инвариантов
См. `.agent/rules/project-rules.md` (R1.1–R1.7) — закреплено в `AGENTS.md` §«Подтверждённые инварианты» до мерджа.
| # | Пункт | Краткая формулировка | Задача |
|---|---|---|---|
| 1 | Все `outputs` флейка вычисляются | A1: правка `lib/xlib.nix` → `lib/xlib`; закрепить через `nix flake check` | A1 |
| 2 | External-диск монтируется до сервисов | mkServiceStorage + bind без guard'а → сервис стартует на пустой БД | B1 |
| 3 | Сетевая граница sapphira = роутер | 5 портов: 22, 80, 443, 8443, 22000; `firewall.enable = false` намеренно | D1 |
| 4 | `100.64.0.0` = Tailscale sapphira | Назначен вручную; в 4 файлах | D2 |
| 5 | 3x-ui заморожен | Панель на latest; ядро Xray на 26.7.x; миграция 26.9 провалена | C1–C5 |
| 6 | nftables на VDS — явная финальная политика | Сейчас ruleset без финального правила + конфликт с `firewall.*` | A3 |
| 7 | sops-пути — через `config.sops.secrets.<name>.path` | Любой `path =` override на sops-блоке делает хардкод-потребителя молча сломанным | ADR-0001 |
## Открытые вопросы (слои 0–8)
Самые важные — выделены. Источник для новых задач в `.agent/tasks/manifest.json`.
| ID | Вопрос | Что блокирует |
|---|---|---|
| 0.1 | Восстанавливать ли migration notes, удалённые в `22a19be`? | C1: реконструкция заметки 3x-ui |
| 0.2 | Комментарий-density 38/120 файлов без комментариев — нормально? | Стиль модулей |
| 0.3 | 15 закомментированных модулей: удалить или хранить как референс? | E3: чистота кода |
| 0.4 | README пустой, todo.md нет — норма? | E1: AGENTS.md/README |
| 1.3 | Лишние inputs в flake (`justray`, `nix-minecraft`, `proxy-suite`)? | Чистота flake |
| 1.5 | `nix-systems` через `follows` — оптимизация размера lock | Документация |
| **2.2** | **`vetymae` / `lamet` / `therima` / `soptur` — те же машины или хосты вне реестра?** | **DNS/nginx/identity** |
| 2.3 | sapphira uid=1001: блокер ли использование `/mnt/archive`/`/mnt/mobile`? | Миграция ФС |
| **2.5** | **stateVersion 24.05 / 24.11 / 25.05 / 26.05 — намеренный дрейф?** | **Миграции** |
| 2.6 | Есть ли escape hatch для per-host отличий в xlib? | Архитектура |
| 2.7 | `devices.termux` без NixOS-хоста — закрытый список | Документация |
| 3.2 | `any.nix` (minimal) нужны home-manager + sops + disko? | Минималка |
| **4.1** | **Как root получает доступ по SSH — authorizedKeys в коде нет** | **deploy, безопасность** |
| **4.2** | **Как разрешается цикл «ключ в секрете, а нужен для расшифровки»?** | **bootstrap, recovery** |
| 4.3 | Все файлы в `secrets/` покрыты `path_regex`? | sops |
| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе? | sops, scale |
| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя»? | Безопасность |
| 5.1 | `/mnt/services` mode 0777 — осознанно? | Безопасность |
| 5.3 | NFS выключен, Samba работает — миграция? | Сетевые сервисы |
| 5.4 | NTFS-том `lamet-drive` `mask=0000` — что на нём? | Семантика |
| 5.5 | `therima` / `vetymae` / `soptur` в dirs.nix — реально смонтированы? | Семантика |
| 5.6 | Где бэкапы БД и 3x-ui? | B2 |
| 6.6 | `192.168.1.20` зашит в 30 мест — константа? | Рефакторинг |
| 6.7 | DNS ↔ сервисы — как ловим рассинхрон? | Документация, CI |
| **6.8** | **Публичные IP + SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна?** | **Архитектура** |
| 6.9 | Какой путь REALITY правильный сейчас? | C5 |
| 7.4 | Почему не публиковать весь диапазон 14380-15379? | 3x-ui |
| 8.2 | `lamet.opencodes` → `:6061` (порт miniflux) — ошибка? | nginx |
| 8.4 | `onlyoffice` после трёх регрессов — работает? | Статус сервиса |
| 8.5 | Что слушает `:3002` (`/whiteboard` nextcloud)? | Карта сервисов |
| 8.6 | Бэкапы вне Nix — записать | Документация |
## Слой 9. home-manager
**9.1** `home/home.nix:52-57` — для пользователя импортируется
`home/${xlib.device.type}.nix`; для `root` — без профиля (строка 51).
**Вопрос:** почему у `root` нет home-профиля — сознательно?
**Кандидат:** `home/<type>.nix` = единственный источник «что есть на этом хосте»
для пользователя; добавление пакета в новый тип = правильный файл, а не
`home/default.nix`.
**9.2 [!]** `home/home.nix:28-43` для headless-хостов: `xdg.userDirs.* = null` и
`createDirectories = false`, при этом `lib/xlib/dirs.nix:26` обещает
`music-library = "${user-home}/Music"`.
**Вопрос:** кто создаёт `~/Music` и `~/Storage`? `createDirectories = false`
означает, что home-manager их не создаст, а `dirs.nix` на них ссылается.
**Риск:** на headless-хосте путь в конфиге есть, а каталога нет → тихий сбой
сервиса, который туда пишет.
**9.3 [!]** `home/modules/opencode.nix:339-350` (`c73a698`): в home-manager нельзя
писать `serviceConfig = { ... }` — рендерится литеральная секция `[serviceConfig]`,
которую systemd молча игнорирует. Закреплено в R2.
**9.4** `linger = true` добавлен ради `opencode-web` (`users.nix:71-75`) и включён
**для всех** хостов.
**Кандидат:** «user-сервисы переживают logout на всех хостах» — закрепить, потому
что это неочевидное поведение, влияющее на ресурсы и на безопасность.
**9.5** `home/modules/opencode.nix:286-303` — `opencode.web` слушает `0.0.0.0:4096`
(комментарий: nginx проксирует `127.0.0.1:4096`), и nginx на sapphira ходит туда
же по Tailscale у двух других хостов (см. 8.3).
**Кандидат:** `0.0.0.0` в `opencode.web` — обязательное условие для внешнего
доступа через `opencodes.*`; пароль приходит из sops-секрета `opencode_server`.
**9.6** Секреты opencode приходят в `~/.config/opencode/server.env` (dotenv),
`~/.local/share/opencode/auth.json` и `account.json` (json, `key = ""`).
**Кандидат:** эти три файла перезаписываются sops при каждой активации — ручные
правки в них теряются.
## Слой 10. deploy и проверка
**10.1** `deploy/default.nix:20-24` — цели: `sapphira` (server), `otrecа` (vds),
`rydiwo` (ноутбук). **Нет** `atoridu` (основной десктоп), `wsl`, `epral`.
**Вопрос:** почему не деплоится десктоп? И безопасно ли пересобирать ноутбук
`rydiwo` по SSH (он может быть выключен/на другом Wi-Fi)?
**Кандидат:** `deploy-rs` = только серверы + ноутбук; десктоп и WSL обновляются
вручную.
**10.2** `deploy/default.nix:18-19` — `sshUser = "oqyude"`, `user = "root"`.
См. 4.1: root-доход по SSH не описан в конфигурации.
**10.3** `deploy/default.nix:27-29` — `checks = builtins.mapAttrs (... deployChecks)`.
**Вопрос:** `nix flake check` реально проходит сейчас?
**10.4** CI нет, `flake check` не запускается автоматически.
**Кандидат:** минимальный локальный набор перед коммитом:
`nix flake check && nix build .#nixosConfigurations.<хост>.config.system.build.toplevel --dry-run`.
## Слой 11. Формат
**11.1** Где будет жить итог: `AGENTS.md` в корне (читается агентом всегда),
`docs/arch/map.md` (карта хостов/сервисов), `docs/arch/invariants.md` (этот файл).
**Кандидат:** этот файл после ответов превращается в `.agent/roadmap/sources.md`
с колонкой «ответ» и становится источником для `.agent/context/project-state.md`;
`.agent/context/project-state.md` — сжатая выжимка, без подробностей. (Сделано.)
**11.2** Какие инварианты можно превратить в автоматическую проверку (тогда они
перестанут «забываться»): 7 кандидатов в `analysis-report.md §5`.
**Вопрос:** какие из этих проверок ты хочешь, а какие — лишний CI?
## Шаблон инварианта
Этот шаблон — для добавления новых инвариантов в `.agent/rules/project-rules.md`
(и для зеркалирования в `AGENTS.md`). Та же 4-осевая структура используется,
чтобы вытащить «невидимое знание владельца» из существующего кода в явное
утверждение.
1. **Утверждение** — что именно верно и нельзя менять без осознанного
решения. Один-два абзаца, никаких «может быть».
2. **Где** — конкретные файлы и строки. Агент не должен угадывать.
3. **Почему** — что происходит при нарушении. Лучше всего — сценарий
(rebuild / рантайм), а не абстрактный риск.
4. **Действие** — task id в `manifest.json`, ссылка на коммит, или явное
«закреплено автоматической проверкой (см. analysis-report.md §5)».
Дополнительные поля по необходимости: «ловушка» (выглядит сломанным,
намеренно), «обратное» (где это уже было сломано раньше), «как проверить»
(grep / CI).
-160
View File
@@ -1,160 +0,0 @@
# Project Rules
Правила, которым агент обязан следовать во всех фазах. Источник: старый
`AGENTS.md` (накоплен при проходе по репозиторию 2026-10-05, ответы владельца
учтены 2026-10-05). Дополнения и уточнения — через `/adr`.
## Обязательные правила
### R1. Не ломать подтверждённые инварианты
1. **Все `outputs` флейка должны вычисляться.** `configurations/mobile.nix:12`
импортировал несуществующий `lib/xlib.nix` — был сломан, `epral` не
собирался. Закреплено через `nix flake check`.
2. **Носитель данных (`/home/oqyude/External`) обязан быть смонтирован** до
старта `postgresql`, `n8n`, `samba`, `homebox`, `minecraft`, `3x-ui`,
`tape-rotation`. `mkServiceStorage` даёт `bind,x-systemd.automount,nofail`
— без guard'а сервис стартует на пустой БД. Задача `B1` в `manifest.json`.
3. **Сетевая граница sapphira — роутер.** `firewall.enable = false` намеренно.
Роутер пробрасывает ровно 5 портов: **443, 80, 22000 (syncthing),
8443 (xray), 22 (ssh)**. `nginx.nix:225` (`allowedTCPPorts = [80 443]`) мёртв.
`openFirewall`/`allowedTCPPorts` на sapphira не имеют эффекта.
4. **`100.64.0.0` = Tailscale-адрес sapphira**, назначен вручную. Не сеть, не
ошибка. Используется в `nginx.nix`, `nextcloud.nix` (`trusted_proxies`),
`vds/systemd.nix`, `vds/nginx.nix`. При смене — править 4 файла.
5. **3x-ui заморожен.** Панель на последней версии (образ `:latest`),
ядро Xray на 26.7.x. Миграция на 26.9.x провалена. Обходные скрипты
(timer, migrateScript) отключены осознанно. **Не** обновлять ядро через
панель без записи в `decisions/` или `notes/`.
6. **nftables на VDS требует явной финальной политики.** Текущий ruleset
(`vds.nix:73-91`) — без явного последнего правила и без `policy` → неявный
accept. На otreca одновременно `nftables.enable = true` и `firewall.*` —
проверить, кто реально владеет ruleset'ом, перед правкой.
7. **sops-пути — через `config.sops.secrets.<name>.path`.** Любой
`path =` override на sops-блоке делает хардкод-потребителя молча
сломанным: rebuild зелёный, сервис стартует, контент пустой. См. ADR-0001.
### R2. home-manager `Service` ≠ `serviceConfig`
`home/modules/opencode.nix:339-350` (`c73a698`): в home-manager нельзя писать
`serviceConfig = { ... }` — рендерится литеральная секция `[serviceConfig]`,
которую systemd молча игнорирует («Unknown section 'serviceConfig'. Ignoring.»).
Правильно: `systemd.user.services.opencode-web.Service = { ... }`. В home-manager
cgroup-опции (`MemoryHigh`, `OOMScoreAdjust`, …) пишутся в
`systemd.user.services.<name>.Service`, **не** в `serviceConfig`. Ошибка
не диагностируется — она просто не применяется.
### R3. Sops-цикл ключа задокументировать
`/etc/ssh/id_ed25519` одновременно: `hostKeys` для sshd, `sops.age.sshKeyPaths`
для расшифровки, цель `ssh_key_private_known`, цель `ssh_key_public_host`.
Как разворачивается на чистой машине — **одноразовый bootstrap**. Должен быть
задокументирован, иначе при переустановке хоста агент не выведет.
### R4. Перед деплоем External-диска — `findmnt`
Перед рестартом сервисов, использующих `mkServiceStorage` (postgresql, samba,
homebox, gitea, navidrome, syncthing, uptime-kuma, immich, nextcloud,
calibre-web, 3x-ui, tape-rotation):
```bash
findmnt /home/oqyude/External
findmnt /mnt/services
```
До реализации guard'а (задача B1) — это единственная защита от старта на
пустой БД.
### R5. Проверка целостности sops-секретов
Любая правка `users.nix` или потребителя sops-секрета требует:
```bash
sops --version
nix build .#nixosConfigurations.<хост>.config.system.build.toplevel --dry-run
```
Расшифровка sops-секретов зависит от `/etc/ssh/id_ed25519` (см. R3).
Циклическая зависимость — см. «Где НЕ лезть без ответа».
## Ловушки (выглядит сломанным, намеренно)
Прежде чем чинить — проверить этот список. Здесь лежат решения, которые
иначе «поправляются» обратно и ломают рабочую систему.
| Где | Что выглядит ошибкой | На самом деле |
|---|---|---|
| `server.nix:130` | `firewall.enable = false` при 20 сервисах на `0.0.0.0` | Роутер фильтрует, см. R1.3 |
| `mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 vs 26.05 | Каждый хост зафиксирован на своей версии |
| `users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` | Костыль под 1000 = удалённый `yuyus`; удалять только после миграции ФС |
| `3x-ui.nix:54` | `image = …:latest` | Панель намеренно latest; ядро Xray — на 26.7.x |
| `3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS | Следствие отката `c8d4a12`; смысл утрачен, см. задачу C5 |
| `server/default.nix:33-47` | 15 закомментированных модулей | Отключены осознанно, см. задачу E3 |
| `opencode.nix:339` | `systemd.user.services.opencode-web.Service` | `serviceConfig` рендерится в секцию `[serviceConfig]`, systemd молча игнорирует (`c73a698`); см. R2 |
| `vds.nix:73-91` | nftables без финального правила | Известный пробел, см. задачу A3 |
| `100.64.0.0` | Первый адрес CGNAT `/10` | Tailscale-адрес sapphira, см. R1.4 |
| `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. задачу B1 |
## Куда лезть по задаче
| Задача | Файл |
|---|---|
| Добавить хост | `configurations/default.nix` + `configurations/<host>.nix` + `configurations/{hardware,disko}/<host>.nix` |
| Добавить системный сервис | `modules/server/<name>.nix`, добавить в `modules/server/default.nix:imports` |
| Добавить home-пакет для пользователя | `home/<device_type>.nix` (через `lib.mkIf` или просто список) |
| Добавить опцию, читаемую несколькими модулями | `modules/options.nix` |
| Изменить mount/имя пользователя | `lib/xlib/dirs.nix`, `lib/xlib/device.nix` |
| Изменить домен / сертификат | `modules/server/coredns.nix` + `modules/server/nginx.nix` (или `vds/`) |
| Sops-секрет | положить в `secrets/<name>.<yaml\|json\|env\|ini>`; `users.nix:99` уже подключает `secrets/default.yaml`; dotenv/json-секреты — через `mkUserSecret` |
## Где НЕ лезть без ответа владельца
- `secrets/` (sops-encrypted, расшифровываются `/etc/ssh/id_ed25519` → циклический bootstrap).
- `let deploy` без проверки deploy-rs нод: `rydiwo` (ноутбук, может быть выключен).
- Любая правка, противоречащая «Подтверждённым инвариантам» выше (R1).
- `vetymae` / `lamet` / `therima` / `soptur` в `dirs.nix` — природа неясна (открытый вопрос 2.2/5.5).
- `192.168.1.20` в 30 местах — менять только при готовности править все места (открытый вопрос 6.6).
- Ядро Xray 26.7.x → 26.9.x — миграция провалена, не повторять без отдельной задачи.
## Проверки
```bash
# все outputs вычисляются
nix flake check
# правки применились на целевой хост
nix build .#nixosConfigurations.<host>.config.system.build.toplevel
# nixOnDroid
nix build .#nixOnDroidConfigurations.epral.config.system.build.toplevel
# внешний диск смонтирован (до рестарта сервисов на нём)
findmnt /home/oqyude/External
findmnt /mnt/services
# state of guard-зависимостей (когда будет todo B1)
systemctl show postgresql -p Requires -p After | tr ' ' '\n' | grep -E 'mnt-|home-oqyude'
# sops
sops --version
```
## Конвенции проекта
- `xlib` (в `lib/xlib/`) — чистые данные: identity (`device`), capability flags,
директории, helper'ы. Передаётся в каждый модуль через `specialArgs`.
Конфиг не может переопределить `xlib` — единственная точка изменения это
`configurations/default.nix`.
- `device.type` ∈ { minimal, primary, secondary, server, vds, wsl, termux }.
`modules/defaultModule` импортирует `modules/<type>/` через
`lib.optional (!isDesktop && type != "minimal") (./. + "/${type}")`.
- `mkXlib` (`lib/xlib/default.nix:38-77`) — единственная точка сборки xlib.
- Опция живёт в `modules/options.nix`, если её **устанавливает** один модуль,
а **читает** другой. `host.reader.X.enable` живёт в `essentials/ssh.nix`,
потому что его объявляет и использует один модуль.
- `home/<type>.nix` = единственный источник «что есть на этом хосте» для
пользователя; добавление пакета в новый тип = правильный файл, а не
`home/default.nix`.
- `.sops.yaml`: один age-ключ (`*default`), `path_regex: secrets/[^/]+\.(yaml|json|env|ini)$`.
Покрывает только плоские файлы в `secrets/` (без подкаталогов). Дополнительные
секреты dotenv/json — через `mkUserSecret` (`users.nix:33-41`).
-45
View File
@@ -1,45 +0,0 @@
# BOUNDARIES — Рамки и границы
Что агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**.
## Разрешено
| Действие | Примечание |
|---|---|
| Читать любые файлы в целевом репозитории | Включая `.git`, конфиги, историю |
| Создавать/изменять файлы в `.agent/` | Директория метаданных проекта (rules, decisions, tasks, context, requests, roadmap, archive) |
| Создавать `.temp/` в корне проекта | Для временных файлов агента. Всегда в `.gitignore` |
| Писать production-код | В фазе EXECUTION, по задачам из `manifest.json` |
| Рефакторить существующий код | Только если это часть задачи в `manifest.json` |
| Делать коммиты | По завершении задачи, перед созданием request |
| Создавать/дополнять `.gitignore` | Только для добавления `.temp/` |
| Устанавливать/обновлять зависимости | Через штатный пакетный менеджер проекта |
| Изменять конфигурационные файлы | Только если необходимо для сборки/тестов |
| Запускать сборку и тесты | Для верификации окружения и проверки request-ов |
| Читать документацию, issue, PRs | Для понимания контекста |
| Запрашивать уточнения у пользователя | Если не хватает информации для декомпозиции |
| Копировать исходники MetaAgent в `.agent/src/` целевого проекта | На фазе INIT, без перезаписи существующих файлов (если не указан `--update`) |
| Создавать/обновлять `AGENTS.md` в корне целевого проекта | Только если файла не существует |
| **Обязательно:** читать `.agent/rules/project-rules.md` перед каждой фазой | Правила пользователя имеют приоритет выше стандартных протоколов |
| Перемещать завершённые артефакты в `.agent/archive/` | На фазах METASTATE и HANDOFF |
| **Обязательно:** после выполнения задачи создавать request в `.agent/requests/active/` | Request — единица результата |
| Вызывать команды из `COMMANDS/` | По явной просьбе пользователя (`/adr`, `/red-team`, `/risk-register`, `/alt-arch`, `/invariant-tests`) |
## Запрещено
| Действие | Почему |
|---|---|
| Удалять файлы | Если файл мешает — сообщить пользователю |
| Менять удалённые настройки CI/CD | Если CI сломан — сообщить пользователю |
| Модифицировать код, не связанный с задачей | Только то, что нужно в рамках задачи из `manifest.json` |
| Выполнять команды (`/adr`, `/red-team`, и т.д.) без явной просьбы | Команды — on-demand, не авто-фаза |
| Задавать пользователю вопросы про depth / scale / фичи | В v3.0 нет шкалы глубины. Просто работай |
## Когда остановиться
1. **Репозиторий не собирается** — сообщить пользователю с логом ошибки, не продолжать.
2. **Неясна цель** — запросить уточнение, не гадать.
3. **Обнаружены секреты/токены** — не копировать, сообщить пользователю.
4. **Цель выходит за рамки одной сессии** — разбить, запросить приоритет.
5. **Проект не использует известные технологии** — запросить инструкцию по сборке.
6. **Непонятно, какую команду вызвать** — спросить пользователя, не угадывать.
-87
View File
@@ -1,87 +0,0 @@
# Changelog
## 3.0.0 — Упрощение модели
**Дата:** 2026-10-08
### Что изменилось
Принята модель «жизненный цикл + команды на вызов» вместо «жизненный цикл с уровнями глубины».
**Удалено:**
- Шкала глубины (depth 1-10) и все её варианты (Scaffold/Light/Standard/Deep/Maximum).
- Условные фичи в фазах: `adr`, `alternative_arch`, `red_team`, `risk_register`, `invariant_tests`.
- Интервью с пользователем на старте (5 вопросов про depth и фичи).
- `.agent/metaagent-request.md` — конфиг-файл, который сейчас не нужен.
- `TEMPLATES/metaagent-request.md`.
**Добавлено:**
- Директория `COMMANDS/` с пятью on-demand инструкциями: `adr.md`, `red-team.md`, `risk-register.md`, `alt-arch.md`, `invariant-tests.md`.
- `GUIDE.md` — заменяет `META_AGENT_GUIDE.md`, описание цикла + список команд.
- `CHANGELOG.md` — этот файл.
**Переименовано / перенумеровано:**
- `META_AGENT_GUIDE.md` → `GUIDE.md`.
- `PROTOCOLS/01_ANALYSIS.md` → `01_ANALYSE.md`.
- `PROTOCOLS/02_DESIGN.md` → `03_DESIGN.md`.
- `PROTOCOLS/03_DECOMPOSITION.md` → `04_DECOMPOSITION.md`.
- `PROTOCOLS/04_EXECUTION.md` → `05_EXECUTION.md`.
- `PROTOCOLS/05_HANDOFF.md` → `07_HANDOFF.md`.
- `PROTOCOLS/06_METASTATE.md` остался под тем же именем (теперь фаза 6).
**Удалены протоколы:**
- `PROTOCOLS/00_CONFIG.md` — конфигурация больше не нужна.
- `PROTOCOLS/00_MIGRATE.md` — миграция теперь документируется в этом CHANGELOG.
- `PROTOCOLS/04_ENVIRONMENT_SETUP.md` — поглощён фазой `00_INIT.md`.
- `PROTOCOLS/02b_REDTEAM.md` — теперь команда `COMMANDS/red-team.md`.
**Структура `.agent/checkpoints.json`** упрощена: убраны `config.depth`, `config.design.adr`, `config.red_team`, `config.risk_register`, `config.decomposition.invariant_tests`.
### Миграция с v2.1 → v3.0
Для проектов, созданных с MetaAgent v2.1:
1. **Удалить** из `.agent/checkpoints.json` секцию `config` целиком (она больше не читается).
2. **Удалить** `.agent/metaagent-request.md` (не используется).
3. **Удалить** `.agent/decisions/config.json`, если есть (аналог config для решений).
4. **Запустить** `install.sh --update` (или `install.ps1 -Update` / `install.bat --update`) — перезапишет исходники MetaAgent.
5. **Переименовать** пути в существующих артефактах: `layer-1/adr/` → `decisions/` (если остались с v1.x), `layer-2/analysis-report.md` → `context/analysis-report.md` и т.п. — это касается только проектов, оставшихся на v1.x.
6. **Записать** в `.agent/checkpoints.json` новое значение `metaagent_version: "3.0.0"`.
`request.json`, `manifest.json`, `decisions/index.json` остаются в том же формате, что в v2.1.
### Экономия
| | v2.1 | v3.0 |
|---|---|---|
| Markdown строк всего | ~3 820 | ~1 800 (целевой) |
| Протоколов | 10 | 8 |
| Уровней конфигурации | 5 (depth) | 0 |
---
## 2.1.0 — Project Loop + Work Loop + Requests
**Дата:** 2025-08 (предыдущая версия)
- Введён двухконтурный жизненный цикл: Project Loop (однократно) + Work Loop (циклически).
- Добавлены фазы: ROADMAP, METASTATE, RED_TEAM.
- Введены `requests/` как единица результата выполненной задачи.
- Введён `metaagent-request.md` с конфигом сессии (depth scale, фичи).
- Введена структура `.agent/` с семантическими директориями: `decisions/`, `tasks/`, `context/`, `rules/`, `requests/`, `roadmap/`, `archive/`.
- Шкала глубины 1-10 с условными фичами (adr, alternative_arch, red_team, risk_register, invariant_tests).
## 2.0.0 — Реструктуризация `.agent/`
- Переход от слоистой структуры `layer-0..3` к семантическим директориям.
- Полный MIGRATE-протокол для апгрейда с v1.x.
## 1.1.0 — Добавлены rules, archive
- `PROTOCOLS/01_ANALYSIS.md` обзавёлся правилами из `.agent/rules/`.
- Добавлена директория `archive/`.
## 1.0.0 — Первый релиз
- Односессионный pipeline: INIT → ANALYSE → DECOMP → SETUP → HANDOFF.
- Структура `layer-0..3`.
-95
View File
@@ -1,95 +0,0 @@
# ADR — Architecture Decision Record
## Назначение
Зафиксировать архитектурное решение в `.agent/decisions/NNN-slug.md` так, чтобы будущий агент (или человек) мог понять: что решили, почему, какие альтернативы рассматривали, какие последствия.
ADR создаются по явной команде пользователя: «запиши это как решение», «/adr», «сделай ADR для текущего подхода».
## Когда вызывать
- Принято неочевидное архитектурное решение (выбор БД, паттерна, библиотеки, структуры модулей).
- Решение может измениться в будущем — стоит зафиксировать контекст.
- Есть trade-off, который нужно объяснить следующему агенту.
Не вызывать для очевидных вещей: «используем pytest», «классы называем в PascalCase».
## Вход
- Контекст решения: что обсуждалось, какие варианты сравнивались, что выбрали.
- `.agent/decisions/index.json` — текущий список ADR (для нумерации).
- `.agent/context/project-state.md` — текущее состояние проекта.
## Шаги
### 1. Определить номер
Прочитать `.agent/decisions/index.json`. Следующий номер = max существующих + 1. Если файла нет — создать, начать с 001.
### 2. Slug
Короткое имя в kebab-case, отражающее суть: `use-sqlite-for-mvp`, `auth-via-jwt-cookies`, `modular-monolith`.
### 3. Записать ADR
Создать `.agent/decisions/{NNN}-{slug}.md` по шаблону `TEMPLATES/adr-NNNN.md`:
```markdown
# {NNN}. {Заголовок}
**Дата:** {YYYY-MM-DD}
**Статус:** Accepted | Superseded by {NNN} | Deprecated
## Контекст
{Что за проблема. Какие ограничения. Что нужно было решить.}
## Решение
{Что выбрали. Коротко и конкретно.}
## Альтернативы, которые рассмотрели
### {Альтернатива 1}
{Описание. Почему не выбрали.}
### {Альтернатива 2}
{Описание. Почему не выбрали.}
## Последствия
### Положительные
- {что становится лучше}
### Отрицательные
- {что становится хуже или сложнее}
### Инварианты
- {что не должно сломаться, чтобы решение оставалось валидным}
```
### 4. Обновить index.json
```json
{
"version": "3.0.0",
"decisions": [
{ "id": "001", "title": "Использовать SQLite для MVP", "file": "001-use-sqlite-for-mvp.md", "status": "Accepted" }
],
"last_updated": "{timestamp}"
}
```
### 5. Если есть supersession
Если новый ADR отменяет старый — в старом ADR поставить `Статус: Superseded by {NNN}` и добавить ссылку. В новом — в контексте упомянуть, что отменяет.
## Выход
- `.agent/decisions/{NNN}-{slug}.md`
- Обновлённый `.agent/decisions/index.json`
## Связанные команды
- **/invariant-tests** — после ADR можно зафиксировать инварианты как задачи в manifest.
- **/alt-arch** — если хочется явно зафиксировать альтернативу до решения.
-85
View File
@@ -1,85 +0,0 @@
# Alternative Architecture
## Назначение
Описать альтернативный вариант архитектуры / подхода, чтобы сравнить с текущим и принять осознанное решение. Не «сделать вместо», а «сравнить и выбрать».
## Когда вызывать
- Текущий дизайн кажется спорным, нужна трезвая оценка альтернативы.
- Хочется зафиксировать «почему не сделали иначе» — потом пригодится при росте.
- Перед крупным решением (выбор БД, монолит-vs-микросервисы, sync-vs-async).
## Вход
- Текущий дизайн / план (`.agent/context/design-report.md` или текущее состояние).
- Ограничения проекта (сроки, стек, бюджет).
## Шаги
### 1. Определить, что сравниваем
Один конкретный вопрос: «SQLite vs PostgreSQL», «монолит vs микросервисы», «REST vs GraphQL», «sync-обработка vs очередь».
### 2. Сформулировать альтернативу
Краткое описание: что предлагается вместо текущего подхода. Без длинного дизайна — на уровне «как это работает и чем отличается».
### 3. Сравнить
| Аспект | Текущий | Альтернатива |
|---|---|---|
| Сложность реализации | | |
| Время до MVP | | |
| Производительность | | |
| Масштабирование | | |
| Поддерживаемость | | |
| Стоимость изменений | | |
| Риски | | |
### 4. Записать
Создать `.agent/context/alt-architecture.md` (если файла нет) или дополнить. Структура:
```markdown
# Alternative Architecture — {что сравниваем}
**Дата:** {YYYY-MM-DD}
## Контекст
{Почему рассматриваем альтернативу. Что не устраивает в текущем.}
## Альтернатива
{Краткое описание. Архитектура, ключевые компоненты, поток данных.}
## Сравнение
{Таблица из шага 3.}
## Когда альтернатива выигрывает
{В каких условиях стоит переключиться. Триггеры для миграции.}
## Когда остаёмся на текущем
{Что в текущем работает достаточно хорошо, чтобы не менять.}
## Рекомендация
{Остаёмся или мигрируем. Почему.}
```
### 5. Связать с ADR
Если после сравнения принимается решение — использовать **/adr** для фиксации. Альтернативный файл остаётся как исторический артефакт.
## Выход
- `.agent/context/alt-architecture.md`
## Связанные команды
- **/adr** — зафиксировать итоговое решение.
- **/risk-register** — если альтернатива снимает/добавляет риски.
-68
View File
@@ -1,68 +0,0 @@
# Invariant Tests
## Назначение
Превратить инварианты из ADR в задачи-тесты в `.agent/tasks/manifest.json`. Инвариант — это «что не должно сломаться, чтобы ADR оставался валидным». Без явного теста это просто слова.
## Когда вызывать
- После создания ADR, в секции «Инварианты» которого перечислены условия валидности решения.
- Когда хочется, чтобы архитектурные решения были защищены регрессионными тестами.
## Вход
- `.agent/decisions/*.md` — ADR с секцией «Инварианты».
- `.agent/tasks/manifest.json` — текущий манифест (для нумерации задач).
## Шаги
### 1. Найти ADR с инвариантами
Прочитать все `.agent/decisions/*.md`, найти секции «Инварианты».
### 2. Для каждого инварианта — задача
Каждый инвариант = одна задача-тест. Формат:
```json
{
"id": "T-INV-001",
"title": "Invariant: auth-сессия не переживает рестарт сервиса",
"type": "test",
"origin": "invariant:001",
"depends_on": [],
"acceptance_criteria": [
"Тест рестартит auth-сервис и проверяет, что все сессии инвалидированы",
"Тест проверяет, что refresh-токен не работает после рестарта"
],
"files": [
"tests/auth/test_invariants.py"
]
}
```
### 3. Добавить в manifest
Записать задачи в `.agent/tasks/manifest.json` с `status: "pending"`. Связать `depends_on` с задачами, которые реализуют компонент (если ещё не выполнены).
### 4. Связать с ADR
В самом ADR добавить (опционально) ссылку на задачу-инвариант:
```markdown
## Инварианты
- {{ ... }}
### Покрытие тестами
- T-INV-001: ...
```
## Выход
- Новые задачи в `.agent/tasks/manifest.json` с `origin: "invariant:{adr_id}"`
- (опционально) обновлённый ADR со ссылкой на задачи
## Связанные команды
- **/adr** — источник инвариантов.
- **/risk-register** — некоторые инварианты рождаются из рисков.
-104
View File
@@ -1,104 +0,0 @@
# Red Team Review
## Назначение
Попытаться сломать текущий дизайн / архитектуру / план. Зафиксировать найденные уязвимости в `.agent/context/red-team-report.md`, чтобы разработчик мог их закрыть до реализации.
Red Team — это adversarial-проход по дизайну. Не «улучшить», а «найти, что не так».
## Когда вызывать
- После фазы DESIGN, до декомпозиции задач.
- Когда дизайн кажется слишком гладким.
- Перед крупным рефакторингом.
- Когда непонятно, какие риски у текущего подхода.
## Вход
- `.agent/context/design-report.md` (если есть).
- `.agent/decisions/*.md` — связанные ADR.
- `.agent/context/project-state.md` — текущее состояние.
## Шаги
### 1. Прочитать целевой дизайн
Понять, что именно ревьюится: вся архитектура, конкретный модуль, конкретное решение.
### 2. Провести атаки по категориям
#### 2.1. Нагрузка и масштабирование
- Что будет при 10x / 100x объёма?
- Где узкое место?
- Что сломается первым?
#### 2.2. Отказы и доступность
- Что если упадёт БД / кэш / внешний сервис?
- Есть ли SPOF (single point of failure)?
- Как восстанавливаемся?
#### 2.3. Безопасность
- Где хранятся секреты?
- Какие поверхности атаки?
- Что с аутентификацией / авторизацией?
- Injection, SSRF, XSS — что релевантно?
#### 2.4. Корректность
- Где гонки (race conditions)?
- Что с консистентностью данных?
- Какие edge cases не покрыты?
#### 2.5. Поддерживаемость
- Что будет сложно менять через год?
- Где связность, которую придётся разрывать?
- Какие зависимости могут устареть?
#### 2.6. Миграция и совместимость
- Если меняем API — как старые клиенты переживут?
- Если меняем схему БД — что со старыми данными?
- Если выкатываем поэтапно — какой план?
### 3. Записать отчёт
Создать `.agent/context/red-team-report.md` (если файла нет) или дополнить:
```markdown
# Red Team Review — {что ревьюим}
**Дата:** {YYYY-MM-DD}
**Цель:** {что именно атакуем}
## Критические находки
### R1. {Краткое название}
- **Категория:** безопасность / нагрузка / корректность / ...
- **Сценарий:** {как воспроизвести}
- **Воздействие:** {что произойдёт}
- **Рекомендация:** {что сделать}
## Существенные находки
### R2. ...
## Минорные находки
### R3. ...
## Что выдержало атаку
- {Что оказалось надёжным — это тоже полезно знать.}
```
### 4. Связать с задачами
Если находка превращается в задачу — добавить в `.agent/tasks/manifest.json` (фаза DECOMPOSITION) с `origin: "red-team:{номер_находки}"`.
## Выход
- `.agent/context/red-team-report.md`
- (опционально) новые задачи в manifest
## Связанные команды
- **/adr** — если Red Team выявил, что нужно зафиксировать решение иначе.
- **/risk-register** — для систематизации рисков.
-80
View File
@@ -1,80 +0,0 @@
# Risk Register
## Назначение
Явный реестр допущений и рисков проекта в `.agent/context/risk-register.md`. Чтобы не держать в голове «ну мы же понимаем, что X может сломаться» — а записать, оценить и (если надо) превратить в задачи.
## Когда вызывать
- В начале проекта — зафиксировать стартовые допущения.
- При появлении нового риска (новый внешний сервис, новая зависимость, новое требование).
- При обзоре дизайна (после DESIGN или Red Team).
## Вход
- `.agent/context/design-report.md` (если есть).
- `.agent/context/analysis-report.md` — что уже знаем о проекте.
- `.agent/decisions/*.md` — принятые решения (могут быть источниками рисков).
## Шаги
### 1. Собрать риски
Источники:
- Допущения, на которых держится дизайн («считаем, что PostgreSQL выдержит 1k qps»).
- Внешние зависимости без SLA.
- Технологии, которые команда не знает.
- Сроки, которые давят.
- Решения, которые сложно откатить.
### 2. Оценить каждый риск
По двум осям:
- **Вероятность** (1-низкая, 2-средняя, 3-высокая).
- **Воздействие** (1-небольшое, 2-серьёзное, 3-критическое).
`score = вероятность × воздействие` (1-9).
### 3. Записать
Создать или дополнить `.agent/context/risk-register.md` по шаблону `TEMPLATES/risk-register.md`:
```markdown
# Risk Register
**Дата:** {YYYY-MM-DD}
## Высокий риск (score 6-9)
### R-001. {Краткое название}
- **Категория:** технический / продуктовый / организационный
- **Описание:** {что может пойти не так}
- **Воздействие:** {что будет если случится}
- **Вероятность:** 3 / 2 / 1
- **Счёт:** 9 / 6 / 4
- **Митигация:** {что делаем чтобы уменьшить}
- **Владелец:** {кто отвечает}
- **Статус:** open / mitigated / accepted / closed
## Средний риск (score 3-4)
...
## Низкий риск (score 1-2)
...
## Закрытые риски
...
```
### 4. Связать с задачами
Если риск требует действия — добавить задачу в `.agent/tasks/manifest.json` с `origin: "risk:R-001"`.
## Выход
- `.agent/context/risk-register.md`
## Связанные команды
- **/red-team** — источник технических рисков.
- **/adr** — некоторые риски закрываются через принятое решение.
-205
View File
@@ -1,205 +0,0 @@
# MetaAgent GUIDE v3.0
MetaAgent — набор инструкций для AI-агента. Задача: превратить хаотичное общение с агентом в структурированный процесс, в котором состояние проекта переживает любую сессию.
## Два слоя
- **Цикл** (всегда, по необходимости) — последовательность фаз, которую агент проходит при работе с проектом.
- **Команды** (по запросу пользователя) — on-demand инструкции, которые не привязаны к фазе.
Состояние проекта живёт в `.agent/` целевого репозитория. Следующий агент читает `.agent/` и не лезет в исходники.
---
## Цикл
```
.agent/checkpoints.json
│
▼
┌─────────────────────────────────────┐
│ PROJECT LOOP (разово) │
│ │
│ INIT → ANALYSE → ROADMAP → │
│ → [DESIGN] → DECOMPOSITION │
│ │
│ Выход: .agent/tasks/manifest.json │
└──────────────────┬──────────────────┘
│
▼
┌─────────────────────────────────────┐
│ WORK LOOP (циклически) │
│ │
│ EXECUTION → (request) → │
│ → METASTATE (по команде) │
│ │
│ Беру задачу → делаю → request → │
│ накопилось → METASTATE │
└──────────────────┬──────────────────┘
│
▼
HANDOFF (завершение)
```
Фазы выполняются **строго последовательно** внутри PROJECT LOOP. WORK LOOP повторяется многократно.
### Ветвление
| Тип проекта | Цикл |
|---|---|
| **existing** | INIT → ANALYSE → ROADMAP → DECOMPOSITION → EXECUTION → METASTATE → HANDOFF |
| **greenfield / scaffold** | + фаза DESIGN между ROADMAP и DECOMPOSITION |
Тип проекта определяется автоматически в фазе ANALYSE. Никакого интервью с пользователем, никакой шкалы глубины.
---
## Фазы
| # | Фаза | Протокол | Что делает |
|---|---|---|---|
| 0 | INIT | `PROTOCOLS/00_INIT.md` | Создаёт `.agent/`, ставит исходники, инициализирует checkpoints |
| 1 | ANALYSE | `PROTOCOLS/01_ANALYSE.md` | Сканирует проект, создаёт `analysis-report.md` + начальный `project-state.md` |
| 2 | ROADMAP | `PROTOCOLS/02_ROADMAP.md` | Собирает источники задач (FUTURE, ADR, user-запросы) → `roadmap/sources.md` |
| 3 | DESIGN | `PROTOCOLS/03_DESIGN.md` | Только greenfield. Архитектура, модули, API, модели |
| 4 | DECOMPOSITION | `PROTOCOLS/04_DECOMPOSITION.md` | Разбивает цель на атомарные задачи → `tasks/manifest.json` |
| 5 | EXECUTION | `PROTOCOLS/05_EXECUTION.md` | Цикл: берёт задачу → код → тесты → коммит → request |
| 6 | METASTATE | `PROTOCOLS/06_METASTATE.md` | По команде. Ревью requests, обновление project-state, handoff-summary |
| 7 | HANDOFF | `PROTOCOLS/07_HANDOFF.md` | Валидация `.agent/`, финализация checkpoints, session-summary |
---
## Команды
Эти инструкции выполняются **по явной просьбе пользователя** в любой момент сессии. Они не привязаны к фазе.
| Команда | Файл | Что делает |
|---|---|---|
| «запиши ADR» / «/adr» | `COMMANDS/adr.md` | Создаёт `.agent/decisions/NNN-slug.md` |
| «red team» / «/red-team» | `COMMANDS/red-team.md` | Создаёт `.agent/context/red-team-report.md` — попытка сломать дизайн |
| «risk register» / «/risk-register» | `COMMANDS/risk-register.md` | Создаёт `.agent/context/risk-register.md` |
| «альтернативная архитектура» / «/alt-arch» | `COMMANDS/alt-arch.md` | Описывает альтернативу текущему дизайну |
| «invariant-тесты» / «/invariant-tests» | `COMMANDS/invariant-tests.md` | Создаёт задачи-инварианты для ADR |
### Когда вызывать
- **ADR** — после архитектурного решения, которое нужно зафиксировать. Типично во время DESIGN или при появлении неочевидного выбора в EXECUTION.
- **Red Team** — после готового дизайна, чтобы найти слабые места до реализации.
- **Risk Register** — в начале проекта или при появлении новых допущений.
- **Alt Arch** — если сомневаетесь в выбранном подходе, хотите сравнить варианты.
- **Invariant Tests** — после ADR, чтобы зафиксировать «что не должно сломаться».
Команды **не обязательны**. Если не вызваны — не выполняются. Состояние проекта от них не зависит.
---
## Структура `.agent/`
```
.agent/
checkpoints.json # состояние сессии (ядро)
session-summary.md # краткая сводка сессии
handoff-summary.md # сводка для следующего агента (создаётся METASTATE)
src/ # исходники MetaAgent (всегда)
GUIDE.md
BOUNDARIES.md
CHANGELOG.md
PROTOCOLS/
COMMANDS/
TEMPLATES/
VERSION
install.sh / install.ps1
rules/
project-rules.md # ваши правила — читать перед каждой фазой
roadmap/ # источники задач
sources.md
archive/
decisions/ # ADR
index.json
001-*.md
tasks/ # задачи
manifest.json + manifest.md
backlog/
requests/ # результаты выполненных задач
active/ # ready_for_review
archive/ # approved / rejected
context/
analysis-report.md
project-state.md # обновляется в METASTATE
design-report.md # только greenfield
red-team-report.md # если вызывали /red-team
risk-register.md # если вызывали /risk-register
baseline-test-report.log
archive/
index.json
tasks/
decisions/
requests/
checkpoints/
```
`.temp/` в корне проекта — для временных файлов агента. Всегда в `.gitignore`.
---
## Checkpoints
`checkpoints.json` обновляется после каждой фазы:
```json
{
"metaagent_version": "3.0.0",
"session_id": "<uuid>",
"target_repo": "<path>",
"goal": "<цель>",
"project_type": "existing | greenfield | scaffold",
"phases": {
"init": "completed",
"analyse": "completed",
"roadmap": "completed",
"design": "skipped",
"decomposition": "completed",
"execution": "in_progress",
"metastate": "pending",
"handoff": "pending"
},
"tasks": [
{ "id": "T1", "title": "...", "status": "in_progress", "origin": "user:direct" }
],
"last_updated": "<timestamp>"
}
```
Секции `config` больше нет. Параметры, которые раньше были в `config` (depth, adr, red_team и т.п.), теперь либо не существуют, либо живут в отдельных командах.
---
## Принципы
### Цикл vs команды
Цикл — это «что агент делает по умолчанию». Команды — «что агент делает по явной просьбе». Не путать: ADR не запускается автоматически в DESIGN, а только когда пользователь скажет «запиши это как решение».
### `.agent/` как слепок проекта
После METASTATE `.agent/` содержит всю картину. Следующий агент читает только `.agent/`, не исходники.
### Request — единица результата
Каждая выполненная задача в EXECUTION завершается созданием `request` (`.agent/requests/active/req-{id}.json`). Request содержит суть изменений, коммиты, верификацию, закрытые acceptance criteria. Ревью request-ов происходит в METASTATE.
### Правила выше протоколов
Перед каждой фазой читать `.agent/rules/project-rules.md`. Если правило пользователя противоречит протоколу — следовать правилу.
### Контекст бесконечно не растёт
Завершённые задачи архивируются в `.agent/archive/tasks/`, request-ы — в `.agent/requests/archive/`. Текущий manifest остаётся lean.
-128
View File
@@ -1,128 +0,0 @@
# Протокол 00: Инициализация (INIT)
## Цель
Подготовить `.agent/` в целевом репозитории: установить исходники MetaAgent, создать структуру директорий, инициализировать `checkpoints.json`, создать/обновить `AGENTS.md`.
INIT выполняется **один раз** в начале работы с проектом. Если `.agent/` уже существует и инициализирован — пропускается.
## Вход
- Целевой репозиторий (путь или текущая директория)
- `VERSION` — текущая версия MetaAgent
- Опционально: существующий `.agent/` (если обновление)
## Шаги
### 0.1. Определить целевой репозиторий
Если не указан явно — текущая рабочая директория. Если указан как URL — клонировать во временную директорию, дальше работать с копией.
### 0.2. Проверить существующий `.agent/`
Если `.agent/` существует:
- Прочитать `.agent/checkpoints.json` → `metaagent_version`
- Если `metaagent_version == VERSION` → INIT уже выполнен, выйти
- Если версия старше → запустить `install.sh --update` (Unix) или `install.ps1 -Update` (Windows) для переустановки исходников, затем выйти
- Если `.agent/` есть, но `checkpoints.json` отсутствует → продолжить INIT (создать checkpoints)
Если `.agent/` не существует → продолжить INIT.
### 0.3. Создать структуру `.agent/`
Создать директории:
```
.agent/
src/ # исходники MetaAgent (копируются из METAAGENT_SRC)
rules/
decisions/
tasks/
backlog/
context/
requests/
active/
archive/
roadmap/
archive/
archive/
tasks/
decisions/
requests/
checkpoints/
```
### 0.4. Создать `.temp/` в корне проекта
Если не существует — создать `.temp/` в корне целевого репозитория. Добавить в `.gitignore` (если его нет — создать с одной строкой `.temp/`).
### 0.5. Скопировать исходники MetaAgent
Скопировать в `.agent/src/`:
- `GUIDE.md`
- `BOUNDARIES.md`
- `CHANGELOG.md`
- `VERSION`
- `PROTOCOLS/`
- `COMMANDS/`
- `TEMPLATES/`
- `install.sh`, `install.ps1`
Существующие файлы в `.agent/src/` не перезаписывать (только с явным `--update`).
### 0.6. Создать `.agent/rules/project-rules.md`
Если файла нет — создать по шаблону `TEMPLATES/project-rules.md`.
### 0.7. Создать/обновить `AGENTS.md` в корне
Если `AGENTS.md` в корне проекта отсутствует — создать по `AGENTS.template.md` с подставленной версией.
Если существует и не относится к MetaAgent — не трогать (попросить пользователя переименовать или подтвердить перезапись).
### 0.8. Инициализировать `checkpoints.json`
Создать `.agent/checkpoints.json`:
```json
{
"metaagent_version": "3.0.0",
"session_id": "<uuid>",
"target_repo": "<путь>",
"goal": null,
"project_type": null,
"phases": {
"init": "completed",
"analyse": "pending",
"roadmap": "pending",
"design": "pending",
"decomposition": "pending",
"execution": "pending",
"metastate": "pending",
"handoff": "pending"
},
"tasks": [],
"last_updated": "<timestamp>"
}
```
Поля `goal` и `project_type` остаются `null` до фазы ANALYSE (goal может быть задан пользователем заранее — тогда заполнить сразу).
## Выход
- `.agent/` с полной структурой
- `.agent/src/` с актуальными исходниками MetaAgent
- `.agent/rules/project-rules.md`
- `.agent/checkpoints.json` со `session_id` и `phases.init = "completed"`
- `AGENTS.md` в корне проекта
- `.temp/` в корне + `.gitignore` обновлён
## Критерии завершения
- [ ] `.agent/` содержит все обязательные директории
- [ ] `.agent/src/` содержит GUIDE.md, PROTOCOLS/, COMMANDS/, TEMPLATES/, VERSION
- [ ] `.agent/checkpoints.json` валиден (JSON parse)
- [ ] `AGENTS.md` присутствует в корне
- [ ] `.temp/` существует и в `.gitignore`
-95
View File
@@ -1,95 +0,0 @@
# Протокол 01: Анализ репозитория (ANALYSE)
## Цель
Составить полную картину целевого репозитория: тип проекта, стек, архитектура, конвенции, состояние тестов. Создать начальный слепок проекта.
## Вход
- Целевой репозиторий
- `.agent/checkpoints.json` (фаза analyse: pending)
- `.agent/rules/project-rules.md` — прочитать первым
## Шаги
### 1.1. Прочитать правила проекта
Прежде чем что-либо делать — прочитать `.agent/rules/project-rules.md`. Если есть правила, применить их к фазе.
### 1.2. Определить тип проекта
Просканировать корень репозитория:
- **`existing`** — есть исходный код, тесты, система сборки (`.py`, `.js`, `.ts`, `.rs`, `.go` и т.д. помимо конфигов и README).
- **`greenfield`** — пусто или только README/LICENSE/.gitignore.
- **`scaffold`** — есть базовая структура (`pyproject.toml`/`package.json`), но нет значимого кода.
Записать тип в `checkpoints.json → project_type`.
### 1.3. Сканировать проект
Для `existing` / `scaffold` собрать:
- **README** — описание, инструкции по сборке/тестам.
- **Лицензия** — какой LICENSE.
- **CI/CD** — `.github/workflows/`, `.gitlab-ci.yml`, `Jenkinsfile`, `Makefile`.
- **Стек** — язык, фреймворк, БД, тестовый раннер, пакетный менеджер, линтер.
- **Структура** — `tree -L 3` (не более 3 уровней).
- **Архитектурный паттерн** — MVC, модульный монолит, микросервисы, слоистая.
- **Ключевые модули/пакеты** — список с краткой ответственностью.
- **Конвенции** — стиль, именование, обработка ошибок, логирование.
- **Тесты** — где лежат, как запускаются, текущее состояние (запустить).
- **Сборка** — выполняется ли проект.
Для `greenfield` — извлечь требования из README:
- Функциональные требования (user stories, сценарии).
- Нефункциональные (стек, производительность, безопасность).
- Бизнес-контекст (зачем, для кого).
- Сомнительные / неясные требования (вопросы пользователю).
### 1.4. Создать analysis-report
Записать `.agent/context/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`. Заполнить соответствующие секции.
### 1.5. Создать начальный project-state
Создать `.agent/context/project-state.md` по шаблону `TEMPLATES/project-state.md`. Это **начальный** слепок. В дальнейшем обновляется в фазе METASTATE.
Заполнить:
- Тип проекта
- Краткая архитектура (из шага 1.3)
- Ключевые модули и их статус
- Tech stack
- Статус тестов
### 1.6. Обновить checkpoints
```json
{
"phases": { "analyse": "completed" },
"project_type": "existing | greenfield | scaffold",
"last_updated": "<timestamp>"
}
```
## Ветвление
| project_type | Следующая фаза |
|---|---|
| `existing` | ROADMAP → DECOMPOSITION (DESIGN пропускается) |
| `greenfield` | ROADMAP → DESIGN → DECOMPOSITION |
| `scaffold` | ROADMAP → DESIGN → DECOMPOSITION |
## Выход
- `.agent/context/analysis-report.md`
- `.agent/context/project-state.md` (начальный)
- Обновлённый `checkpoints.json`
## Критерии завершения
- [ ] Тип проекта определён
- [ ] `analysis-report.md` содержит все соответствующие секции
- [ ] `project-state.md` создан с начальным слепком
- [ ] `checkpoints.json` обновлён
-106
View File
@@ -1,106 +0,0 @@
# Протокол 02: Дорожная карта (ROADMAP)
## Цель
Собрать все источники задач для проекта, приоритизировать их и записать в `.agent/roadmap/sources.md`. ROADMAP — мост между видением проекта и конкретными задачами в манифесте.
## Вход
- `.agent/context/analysis-report.md`
- Цель сессии (goal из `checkpoints.json` или запрос пользователя)
- `FUTURE/` — директория долгосрочных планов (если существует)
- `.agent/decisions/index.json` — принятые ADR (опционально)
- `.agent/checkpoints.json` (фаза roadmap: pending)
- `.agent/rules/project-rules.md` — прочитать первым
## Шаги
### 2.1. Прочитать правила проекта
Прочитать `.agent/rules/project-rules.md`, применить к фазе.
### 2.2. Сканировать FUTURE/
Если в корне проекта существует `FUTURE/`:
- Прочитать все `.md` файлы.
- Зафиксировать: название, статус (active/archived), приоритет, зависимости.
- Какие планы реализованы, какие ожидают.
### 2.3. Сканировать ADR
Если существует `.agent/decisions/index.json`:
- Прочитать индекс ADR.
- Определить, какие решения требуют реализации (не все ADR технические).
- Для каждого — сформулировать задачу-кандидат.
### 2.4. Собрать внешние источники
- Запрос пользователя (goal).
- issues / feedback (если доступны).
- Tech debt, выявленный в ANALYSE.
### 2.5. Приоритизировать
Присвоить каждой задаче приоритет:
| Приоритет | Описание |
|---|---|
| **P0** | Критично, делать следующим |
| **P1** | Важно, сделать скоро |
| **P2** | Желательно |
| **P3** | Долгосрочно / отложено |
Правила:
- Блокирующие зависимости поднимают приоритет.
- User-запросы получают P0-P1 по умолчанию.
- ADR-задачи получают приоритет по срочности решения.
### 2.6. Создать sources.md
Создать `.agent/roadmap/sources.md` по шаблону `TEMPLATES/roadmap-sources.md`:
```markdown
# Roadmap Sources
## FUTURE Plans
| План | Приоритет | Статус |
|------|-----------|--------|
## ADR-Derived Tasks
| ADR | Задача | Приоритет |
|-----|--------|-----------|
## User Requests
| Запрос | Приоритет | Источник |
|--------|-----------|----------|
## Agent-Identified Improvements
| Наблюдение | Задача | Приоритет |
|------------|--------|-----------|
## Consolidated Priority Queue
1. task (origin) — P0
```
### 2.7. Архивация
Если в `.agent/roadmap/archive/` есть предыдущие версии — оставить справочно.
Если планы из `FUTURE/*` больше не актуальны — переместить в `FUTURE/archive/`.
## Выход
- `.agent/roadmap/sources.md`
- Возможно обновлённый `FUTURE/`
- `checkpoints.json: phases.roadmap = "completed"`
## Критерии завершения
- [ ] Все источники просканированы (FUTURE, ADR, user, agent)
- [ ] `sources.md` создан с приоритетами P0-P3
- [ ] Каждая задача имеет origin-ссылку на источник
- [ ] Устаревшие планы перемещены в archive
- [ ] `checkpoints.json` обновлён
-142
View File
@@ -1,142 +0,0 @@
# Протокол 03: Архитектурное проектирование (DESIGN)
## Цель
Спроектировать архитектуру, модули, данные и интерфейсы для greenfield/scaffold-проекта.
DESIGN выполняется **только** для `project_type = greenfield` или `scaffold`. Для existing-проектов пропускается.
## Вход
- `.agent/context/analysis-report.md` (project_type: greenfield или scaffold)
- `.agent/roadmap/sources.md` (опционально)
- `.agent/checkpoints.json` (фаза design: pending)
- `.agent/rules/project-rules.md` — прочитать первым
## Правила
1. **Реалистичность** — архитектура реализуема за 1 сессию (до 10 задач).
2. **Документируемость** — каждый модуль, модель и интерфейс описывается в `design-report.md`.
3. **Тестируемость** — каждый компонент проектируется с учётом тестирования.
4. **Итеративность** — первая версия минимально рабочая (MVP), расширения — отдельными задачами.
## Шаги
### 3.1. Прочитать правила проекта
Прочитать `.agent/rules/project-rules.md`, применить.
### 3.2. Технологический стек
Если стек не указан в README — предложить обоснованный выбор. Если указан — зафиксировать.
Для каждого компонента:
- Язык и версия
- Фреймворк / библиотека
- База данных (движок, схема)
- Инфраструктура (Docker, CI/CD, хостинг)
### 3.3. High-level архитектура
- **Паттерн** — монолит, модульный монолит, микросервисы, слоистая, луковая.
- **Компоненты** — что делает каждый модуль/сервис.
- **Схема взаимодействия** — текстовое описание потоков данных.
```
[Client] → HTTP → [API Gateway] → [Auth Service]
↓
[Core Service] → [Database]
↓
[External API] → [3rd Party]
```
### 3.4. Модули
| Поле | Описание |
|---|---|
| Имя модуля | `app/services/cashflow.py` |
| Ответственность | Что делает |
| Ключевые классы/функции | Сигнатуры без реализации |
| Зависимости | Какие модули нужны |
| Контракт | Что экспортирует |
### 3.5. Модели данных
Описать сущности, поля, связи:
```json
{
"entity": "Transaction",
"fields": [
{"name": "id", "type": "UUID", "pk": true},
{"name": "amount", "type": "Decimal"},
{"name": "date", "type": "datetime"},
{"name": "category_id", "type": "UUID", "fk": "Category"}
]
}
```
### 3.6. API интерфейсы
| Метод | Путь | Описание | Request | Response | Статусы |
|---|---|---|---|---|---|
| GET | /transactions | Список | ?page, ?limit | [Transaction] | 200 |
| POST | /transactions | Создать | CreateTransactionDTO | Transaction | 201, 400 |
Если GUI — ключевые страницы. Если CLI — команды.
### 3.7. Обработка ошибок
- Стратегия: исключения / Result / коды.
- Формат API-ошибок: `{ "error": "...", "code": "...", "details": {} }`.
- Логирование: уровни для разных событий.
### 3.8. Стратегия тестирования
- Какие тесты нужны (unit, integration, e2e).
- Как изолировать зависимости.
- Команда запуска тестов.
### 3.9. Группировка в задачи
Предварительно наметить задачи по модулям — вход для DECOMPOSITION:
```
T1: Инициализация проекта + зависимости
T2: Модель данных (сущности, миграции)
T3: Service (core logic)
T4: API endpoints
T5: Tests
```
### 3.10. Создать design-report
Записать `.agent/context/design-report.md` по шаблону `TEMPLATES/design-report.md`.
### 3.11. Дополнительно (по команде пользователя)
Эти шаги **не выполняются автоматически** — только если пользователь явно попросил:
- **ADR** — вызвать `COMMANDS/adr.md` для ключевых решений.
- **Alternative Architecture** — вызвать `COMMANDS/alt-arch.md` для сравнения.
- **Risk Register** — вызвать `COMMANDS/risk-register.md` для допущений.
- **Red Team** — вызвать `COMMANDS/red-team.md` для атаки на дизайн.
## Выход
- `.agent/context/design-report.md`
- Предварительная группировка задач (для DECOMPOSITION)
- Возможно: ADR, risk-register, alt-architecture, red-team-report (если вызывали команды)
- `checkpoints.json: phases.design = "completed"`
## Критерии завершения
- [ ] Стек определён
- [ ] High-level архитектура описана
- [ ] Модули и их ответственность описаны
- [ ] Модели данных спроектированы
- [ ] API/интерфейсы описаны (если применимо)
- [ ] Стратегия тестирования определена
- [ ] Задачи предварительно сгруппированы
- [ ] `design-report.md` создан
- [ ] `checkpoints.json` обновлён
-117
View File
@@ -1,117 +0,0 @@
# Протокол 04: Декомпозиция задач (DECOMPOSITION)
## Цель
Разбить цель пользователя (и архитектурный план, если есть) на атомарные, независимо выполнимые задачи. Записать в `manifest.json` + `manifest.md`.
## Вход
- `.agent/context/analysis-report.md`
- `.agent/context/design-report.md` (опционально — для greenfield)
- `.agent/roadmap/sources.md` (опционально)
- `.agent/decisions/*.md` (опционально)
- Цель пользователя (goal из `checkpoints.json`)
- `.agent/rules/project-rules.md` — прочитать первым
- `.agent/checkpoints.json` (фаза decomposition: pending)
## Принципы
1. **Атомарность** — одна задача = одна логическая единица, выполнимая и проверяемая за один подход.
2. **Независимость (макс.)** — минимизировать зависимости между задачами.
3. **Тестируемость** — каждая задача имеет измеримые acceptance criteria.
4. **Границы** — задача не выходит за пределы `BOUNDARIES.md`.
5. **Порядок** — задачи с зависимостями выполняются строго последовательно.
## Шаги
### 4.1. Прочитать правила проекта
Прочитать `.agent/rules/project-rules.md`, применить.
### 4.2. Размер задачи
Задача должна укладываться в **1-2 часа работы агента**. Если крупнее — разбить.
Признак слишком крупной задачи:
- Нельзя сформулировать acceptance criteria одной строкой.
- Затрагивает 5+ файлов.
- Содержит союзы «и», «а также», «после чего».
### 4.3. Сверить с roadmap
Если существует `.agent/roadmap/sources.md`:
- Задачи из roadmap получают приоритет P0-P3 в соответствии с `sources.md`.
- Задачи без явного источника получают `origin: "decomposition"`.
### 4.4. Структура задачи
| Поле | Описание | Пример |
|---|---|---|
| `id` | Уникальный идентификатор | `T1`, `T2` |
| `title` | Что сделать | "Добавить модель User" |
| `description` | Как и зачем | "Создать SQLAlchemy модель..." |
| `type` | Тип | `feature`, `refactor`, `test`, `fix`, `config`, `design`, `docs`, `invariant` |
| `status` | Статус | `pending`, `in_progress`, `completed`, `failed`, `archived` |
| `origin` | Источник | `roadmap:file`, `adr:NNN`, `user:direct`, `agent:analysis`, `decomposition` |
| `files` | Файлы | `["app/models/user.py"]` |
| `depends_on` | Зависимости | `[]` или `["T0"]` |
| `acceptance_criteria` | 3-5 измеримых пунктов | `["Модель проходит миграцию"]` |
| `context` | Доп. информация | `"Смотри app/models/base.py"` |
**Типы origin:**
- `roadmap:{filename}` — из FUTURE/ или roadmap
- `adr:{NNN}` — из Architecture Decision Record
- `user:direct` — от пользователя
- `agent:analysis` — выявлено агентом
- `decomposition` — создано при декомпозиции
- `invariant:{adr_id}` — инвариант для ADR (создаётся командой `/invariant-tests`)
- `risk:{R-NNN}` — из Risk Register
### 4.5. Зелёная декомпозиция (greenfield/scaffold)
Если есть `design-report.md` — задачи на основе группировки из дизайна:
1. **T1: init** — инициализация, зависимости, scaffold.
2. **T2..Tn: features** — модули по одному.
3. **Tn+1: tests** — тесты (можно в составе feature).
4. **Tn+2: polish** — документация, форматирование.
### 4.6. Сортировка
Задачи в манифесте в порядке выполнения:
1. Без зависимостей.
2. Чьи зависимости уже выполнены.
3. С наибольшим числом зависимостей.
### 4.7. Записать manifest
Создать `.agent/tasks/manifest.json` по шаблону `TEMPLATES/task-manifest.json`.
Создать `.agent/tasks/manifest.md` по шаблону `TEMPLATES/task-manifest.md`.
### 4.8. Обновить checkpoints
```json
{
"phases": { "decomposition": "completed" },
"tasks": [...],
"last_updated": "<timestamp>"
}
```
## Выход
- `.agent/tasks/manifest.json`
- `.agent/tasks/manifest.md`
- Обновлённый `checkpoints.json`
## Критерии завершения
- [ ] Цель разбита на атомарные задачи
- [ ] У каждой задачи — acceptance criteria, origin, files
- [ ] Зависимости корректны (нет циклов)
- [ ] Задачи сверены с roadmap (если `sources.md` существует)
- [ ] `manifest.json` и `manifest.md` созданы
- [ ] `checkpoints.json` обновлён
-136
View File
@@ -1,136 +0,0 @@
# Протокол 05: Исполнение задач (EXECUTION)
## Цель
Выполнить задачи из `manifest.json`: реализовать код, написать тесты, закоммитить, создать request — артефакт результата.
EXECUTION — **циклическая** фаза. Работает, пока есть задачи со статусом `pending` и выполненными `depends_on`.
## Вход
- `.agent/tasks/manifest.json`
- `.agent/context/analysis-report.md`
- `.agent/context/design-report.md` (опционально)
- `.agent/decisions/*.md` (опционально)
- `.agent/rules/project-rules.md` — прочитать первым
- `.agent/checkpoints.json` (фаза execution: pending)
## Шаги (цикл)
### 5.1. Прочитать правила проекта
Прочитать `.agent/rules/project-rules.md`, применить.
### 5.2. Setup окружения (первый запуск)
Если это первый запуск EXECUTION в сессии:
- Установить зависимости через штатный пакетный менеджер.
- Запустить сборку / базовые тесты.
- Записать baseline в `.agent/context/baseline-test-report.log`.
### 5.3. Выбрать задачу
Найти в `manifest.json` задачу, удовлетворяющую:
- `status: "pending"`
- Все `depends_on` имеют `status: "completed"` или `"archived"`.
Если таких нет — EXECUTION завершён, перейти к ожиданию команды пользователя.
### 5.4. Заблокировать задачу
В `manifest.json`:
```json
{ "id": "T1", "status": "in_progress" }
```
### 5.5. Исполнить
- Следовать конвенциям проекта (из ANALYSE).
- Соблюдать `BOUNDARIES.md`.
- Если задача ссылается на ADR — следовать архитектурному решению.
- Писать код + тесты.
### 5.6. Верифицировать
- Запустить тесты (все или релевантные).
- Проверить LSP diagnostics на изменённых файлах.
- Убедиться, что acceptance criteria выполнены.
### 5.7. Закоммитить
Сделать git-коммит. Сообщение — суть задачи.
### 5.8. Создать request
Создать `.agent/requests/active/req-{task_id}.json` по шаблону `TEMPLATES/request.json`:
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "GET /health endpoint",
"status": "ready_for_review",
"goal": "Добавить ручку GET /health с тестами",
"changes": {
"summary": "Создан health router, подключён в main.py, написаны тесты",
"commits": ["abc1234"],
"files_changed": ["app/routers/health.py", "app/main.py", "tests/test_health.py"]
},
"verification": {
"tests_passed": "24/24",
"lsp_clean": true
},
"fulfills_ac": ["Ручка возвращает 200 + {\"status\":\"ok\"}"]
}
```
Request фиксирует:
- **summary** — суть изменений (не diff).
- **commits** — ссылки на коммиты.
- **files_changed** — какие файлы.
- **verification** — тесты + LSP.
- **fulfills_ac** — какие acceptance criteria закрыты.
### 5.9. Завершить задачу
```json
{ "id": "T1", "status": "completed" }
```
### 5.10. Цикл
Перейти к шагу 5.3. Если задач больше нет — сообщить пользователю и ожидать команду (METASTATE, новая задача, или завершение).
## Request как единица результата
Не просто «задача сделана», а документированный результат. Request проходит ревью в фазе METASTATE:
- `ready_for_review` → после проверки → `approved` или `rejected`.
## Команды во время EXECUTION
В любой момент цикла пользователь может вызвать:
- **/adr** — зафиксировать архитектурное решение, появившееся в процессе.
- **/red-team** — попытаться сломать текущий подход.
- **/risk-register** — зафиксировать новый риск.
Команды не прерывают EXECUTION, но могут добавить задачи в manifest.
## Выход
- Выполненные задачи в `manifest.json` (status: completed)
- `.agent/requests/active/req-{task_id}.json` для каждой выполненной задачи
- Обновлённый `checkpoints.json`
## Критерии завершения (одна итерация)
- [ ] Acceptance criteria выполнены
- [ ] Тесты проходят
- [ ] LSP diagnostics чист
- [ ] Коммит создан
- [ ] Request создан в `.agent/requests/active/`
- [ ] Задача в `manifest.json` отмечена completed
-145
View File
@@ -1,145 +0,0 @@
# Протокол 06: Обновление метасостояния (METASTATE)
## Цель
По команде пользователя провести ревью накопленных requests, синхронизировать манифест, обновить слепок проекта и подготовить `.agent/` как полную картину для следующей сессии.
## Когда запускать
По команде пользователя:
- «обнови метасостояние»
- «update metastate»
- «подведи итог»
- «заверши сессию»
Может запускаться многократно — после каждой группы выполненных задач.
## Вход
- `.agent/requests/active/` — все request-ы со статусом `ready_for_review`
- `.agent/tasks/manifest.json`
- `.agent/context/project-state.md` (создан в ANALYSE, обновляется здесь)
- `.agent/roadmap/sources.md`
- `.agent/decisions/index.json`
- `.agent/checkpoints.json`
## Шаги
### 6.1. Собрать requests
Прочитать все файлы из `.agent/requests/active/` со статусом `ready_for_review`.
### 6.2. Ревью каждого request
Для каждого:
1. **Верифицировать** — тесты проходят, LSP чист, AC выполнены, коммиты на месте.
2. **Принять или отклонить:**
- ✅ **approved**:
- Переместить в `.agent/requests/archive/`.
- В `manifest.json` убедиться: `status: "completed"`.
- ❌ **rejected**:
- Оставить в `active/` с комментарием.
- В `manifest.json`: `status: "reopened"`, добавить `rejection_reason`.
- В request добавить `rejection_reason`.
### 6.3. Архивация завершённых задач
Для каждой `completed` задачи:
1. Создать `.agent/archive/tasks/{id}.json` — полное описание.
2. В `manifest.json` заменить на one-liner:
```json
{ "id": "T1", "title": "GET /health endpoint", "status": "archived", "origin": "user:direct" }
```
### 6.4. Обновить project-state
Переписать `.agent/context/project-state.md` с учётом выполненных задач:
- Обновить список модулей (добавлены / изменены).
- Обновить архитектурную схему (кратко).
- Обновить статус тестов.
- Добавить новые ADR.
- Убрать закрытые concerns.
**Цель:** следующий агент читает `project-state.md` и понимает проект, не открывая исходники.
### 6.5. Обновить roadmap
В `.agent/roadmap/sources.md`:
- Отметить выполненные пункты.
- Пересчитать приоритеты.
- Добавить новые источники (если появились).
### 6.6. Индекс архива
Создать/обновить `.agent/archive/index.json`:
```json
{
"version": "3.0.0",
"archived_at": "<timestamp>",
"tasks": [{ "id": "T1", "title": "...", "archived_at": "<timestamp>" }],
"requests": [{ "id": "req-T1", "task_id": "T1", "archived_at": "<timestamp>" }],
"checkpoints": [{ "file": "checkpoints/<ts>.json", "archived_at": "<timestamp>" }]
}
```
### 6.7. Создать handoff-summary
Создать `.agent/handoff-summary.md` — полная сводка для следующего агента:
```markdown
## Session Summary
**Session:** <id>
**Goal:** <goal>
**Completed:** N tasks
**Pending:** M tasks
**Approved requests:** req-T1, req-T2
## Project State
(краткая выжимка из project-state.md)
## Next Steps
(с чего начать следующую сессию)
## Key Artifacts
- Project state: `.agent/context/project-state.md`
- Tasks: `.agent/tasks/manifest.json`
- Roadmap: `.agent/roadmap/sources.md`
- Pending reviews: `.agent/requests/active/`
- Archive: `.agent/archive/index.json`
```
### 6.8. Обновить checkpoints
```json
{ "phases": { "metastate": "completed" }, "last_updated": "<timestamp>" }
```
## Выход
- `.agent/requests/archive/` — подтверждённые request-ы
- `.agent/archive/tasks/{id}.json` — архив задач
- Обновлённый `.agent/context/project-state.md`
- Обновлённый `.agent/roadmap/sources.md`
- `.agent/handoff-summary.md`
- `.agent/archive/index.json`
- Финальный `checkpoints.json`
## Критерии завершения
- [ ] Все `ready_for_review` requests проверены (approved / rejected)
- [ ] Approved перемещены в archive
- [ ] Completed задачи архивированы (one-liner в manifest)
- [ ] `project-state.md` отражает актуальное состояние
- [ ] `roadmap/sources.md` обновлён
- [ ] `archive/index.json` создан
- [ ] `handoff-summary.md` готов
- [ ] `checkpoints.json` финализирован
-113
View File
@@ -1,113 +0,0 @@
# Протокол 07: Завершение сессии (HANDOFF)
## Цель
Финализация сессии: валидация структуры `.agent/`, финальный `session-summary.md`, отметка `phases.handoff = "completed"`.
> Если перед HANDOFF был METASTATE — архивация, project-state, handoff-summary уже готовы. HANDOFF только валидирует и финализирует.
## Вход
- `.agent/checkpoints.json` (все фазы кроме handoff: completed или skipped)
- Все артефакты `.agent/`
## Шаги
### 7.1. Проверить: был ли METASTATE?
Если существуют `.agent/handoff-summary.md` и `.agent/context/project-state.md` (обновлён) — METASTATE выполнен. Перейти к шагу 7.3.
Если нет — выполнить лёгкую архивацию (шаг 7.2).
### 7.2. Лёгкая архивация (если METASTATE не было)
Если есть `completed` задачи в `manifest.json`:
- Архивировать в `.agent/archive/tasks/{id}.json`.
- Заменить в `manifest.json` на one-liner.
- Создать `.agent/archive/index.json`.
### 7.3. Валидация
Проверить:
- [ ] Все фазы в `checkpoints.json` отмечены `completed` или `skipped`.
- [ ] `.agent/` содержит обязательные файлы:
- `checkpoints.json`
- `context/analysis-report.md`
- `context/project-state.md`
- `tasks/manifest.json` + `manifest.md`
- `rules/project-rules.md`
- `src/GUIDE.md`
- `src/BOUNDARIES.md`
- `src/VERSION`
- `src/PROTOCOLS/`
- `src/COMMANDS/`
- `src/TEMPLATES/`
- [ ] В `manifest.json` нет циклических зависимостей.
- [ ] У каждой задачи — measurable acceptance criteria и origin.
- [ ] `AGENTS.md` присутствует в корне репозитория.
### 7.4. Создать session-summary
Создать `.agent/session-summary.md`:
```markdown
# Session Summary
**Session:** <id>
**MetaAgent version:** 3.0.0
**Date:** <timestamp>
**Goal:** <goal>
## Phases Executed
- [x] INIT
- [x] ANALYSE
- [x] ROADMAP
- [x] DESIGN (или skipped)
- [x] DECOMPOSITION
- [x] EXECUTION (N tasks)
- [x] METASTATE (или skipped)
- [x] HANDOFF
## Results
- Tasks completed: N
- Requests approved: N
- Files changed: [list]
## Next
Следующий агент: читай `.agent/handoff-summary.md`.
```
### 7.5. Финализировать checkpoints
```json
{ "phases": { "handoff": "completed" }, "last_updated": "<timestamp>" }
```
### 7.6. Сигнал
```
HANDOFF COMPLETE
Session: <session_id>
Target: <target_repo>
Type: <existing | greenfield | scaffold>
Tasks: <N> total, <M> completed, <K> pending
Следующий агент начинает с .agent/handoff-summary.md
```
## Выход
- `.agent/session-summary.md`
- Финальный `.agent/checkpoints.json`
- (если METASTATE не было) `.agent/archive/index.json`
## Критерии завершения
- [ ] Все артефакты на месте
- [ ] (если METASTATE не было) `completed` задачи архивированы
- [ ] `session-summary.md` создан
- [ ] `checkpoints.json` финализирован
- [ ] Сигнал отправлен пользователю
-23
View File
@@ -1,23 +0,0 @@
# ADR-NNNN: <Заголовок решения>
**Статус:** proposed | accepted | deprecated | superseded
**Дата:** {{ date }}
**Контекст:** почему возникла необходимость в решении, какая проблема решается.
**Рассматриваемые альтернативы:**
1. Вариант A — описание
2. Вариант B — описание
3. Вариант C — описание
**Решение:** выбран вариант <A/B/C>.
**Обоснование:** почему выбран именно этот вариант (критерии: сложность, поддерживаемость, производительность, совместимость).
**Последствия:**
- Позитивные: ...
- Негативные: ...
- Риски: ...
**Invariant (если применимо):** ключевое правило, которое не должен нарушать исполнительный агент. Если можно — ссылка на тест, проверяющий invariant.
-86
View File
@@ -1,86 +0,0 @@
# Analysis Report
## Session
- **Session ID:** `{{ session_id }}`
- **Target repo:** `{{ target_repo }}`
- **Date:** {{ date }}
- **Project type:** `{{ project_type }}` (existing / greenfield / scaffold)
## 1. Общая информация
- **README:** {{ readme_summary }}
- **Лицензия:** {{ license }}
- **CI/CD:** {{ ci_cd }}
- **Точка входа:** {{ entry_point }}
- **Система сборки:** {{ build_system }}
## 2. Стек технологий (existing / scaffold)
| Компонент | Значение |
|---|---|
| Язык | {{ language }} |
| Фреймворк | {{ framework }} |
| База данных | {{ database }} |
| Тестовый раннер | {{ test_runner }} |
| Пакетный менеджер | {{ package_manager }} |
| Линтер/форматтер | {{ linter }} |
## 3. Архитектура (existing / scaffold)
```
{{ directory_tree }}
```
**Паттерн:** {{ architecture_pattern }}
**Ключевые модули:**
| Модуль | Описание |
|---|---|
| {{ module }} | {{ description }} |
## 4. Конвенции (existing / scaffold)
- **Стиль:** {{ code_style }}
- **Импорты:** {{ import_style }}
- **Типизация:** {{ typing_usage }}
- **Обработка ошибок:** {{ error_handling }}
- **Логирование:** {{ logging }}
## 5. Тесты (existing / scaffold)
- **Команда запуска:** `{{ test_command }}`
- **Всего тестов:** {{ total_tests }}
- **Пройдено:** {{ passed }}
- **Упало:** {{ failed }}
- **Пропущено:** {{ skipped }}
- **Упавшие тесты:** {{ failed_tests_list }}
## 6. Базовая проверка (existing / scaffold)
- **Сборка:** {{ build_status }}
- **Запуск:** {{ run_status }}
- **Git status:** {{ git_status }}
## 7. Требования (greenfield / scaffold)
### Функциональные требования
{{ functional_requirements_list }}
### Нефункциональные требования
{{ non_functional_requirements_list }}
### Бизнес-контекст
{{ business_context_list }}
### Неясные моменты / Вопросы
{{ open_questions_list }}
## 8. Примечания
{{ notes }}
-80
View File
@@ -1,80 +0,0 @@
# Design Report
## Session
- **Session ID:** `{{ session_id }}`
- **Target repo:** `{{ target_repo }}`
- **Date:** {{ date }}
## 1. Технологический стек
| Компонент | Выбор | Обоснование |
|---|---|---|
| Язык | {{ language }} | {{ language_rationale }} |
| Фреймворк | {{ framework }} | {{ framework_rationale }} |
| База данных | {{ database }} | {{ database_rationale }} |
| Инфраструктура | {{ infrastructure }} | {{ infrastructure_rationale }} |
## 2. High-Level архитектура
**Паттерн:** {{ architecture_pattern }}
```
{{ architecture_diagram }}
```
**Поток данных:**
1. {{ data_flow_step_1 }}
2. {{ data_flow_step_2 }}
3. {{ data_flow_step_3 }}
## 3. Модули
| Модуль | Ответственность | Ключевые компоненты | Зависит от |
|---|---|---|---|
| `{{ module_path }}` | {{ responsibility }} | {{ components }} | {{ dependencies }} |
## 4. Модели данных
### Сущности
{{ entity_descriptions }}
## 5. API / Интерфейсы
{{ api_endpoints_table }}
## 6. Обработка ошибок
- **Стратегия:** {{ error_strategy }}
- **Формат ошибок:** {{ error_format }}
- **Логирование:** {{ logging_strategy }}
## 7. Тестирование
- **Unit-тесты:** {{ unit_test_strategy }}
- **Integration-тесты:** {{ integration_test_strategy }}
- **Mock-стратегия:** {{ mock_strategy }}
- **Команда запуска:** `{{ test_command }}`
## 8. Дополнительные артефакты (по команде пользователя)
Если пользователь вызвал соответствующие команды, добавить ссылки:
- ADR: `.agent/decisions/` (команда `/adr`)
- Alternative Architecture: `.agent/context/alt-architecture.md` (команда `/alt-arch`)
- Risk Register: `.agent/context/risk-register.md` (команда `/risk-register`)
- Red Team Review: `.agent/context/red-team-report.md` (команда `/red-team`)
## 9. Предварительная группировка задач
| Задача | Описание | Тип |
|---|---|---|
| T1 | {{ task_1 }} | config |
| T2 | {{ task_2 }} | feature |
| T3 | {{ task_3 }} | feature |
| T4 | {{ task_4 }} | test |
## 10. Примечания
{{ notes }}
-57
View File
@@ -1,57 +0,0 @@
# Handoff Summary
## Session Info
- **Session ID:** `{{ session_id }}`
- **Target Repo:** {{ target_repo }}
- **Goal:** {{ goal }}
- **Date:** {{ date }}
- **Project type:** {{ project_type }}
## Repo Summary
{{ repo_summary }}
## Artifacts Created
- **Analysis report:** `.agent/context/analysis-report.md`
- **Project state:** `.agent/context/project-state.md`
- **Design report:** {{ design_report_path_or_dash }}
- **Roadmap:** `.agent/roadmap/sources.md`
- **ADR:** {{ adr_summary_or_dash }}
- **Risk Register:** {{ risk_register_path_or_dash }}
- **Red Team Report:** {{ red_team_report_path_or_dash }}
## Environment Status
- **Build:** {{ build_status }}
- **Tests:** {{ tests_passed }}/{{ tests_total }} passed
- **Baseline log:** `.agent/context/baseline-test-report.log`
- **Dependencies:** {{ deps_status }}
## Task Overview
| Status | Count |
|---|---|
| Total | {{ total }} |
| Pending | {{ pending }} |
| In Progress | {{ in_progress }} |
| Completed | {{ completed }} |
| Archived | {{ archived }} |
| Failed/Skipped | {{ failed }} |
## Tasks (ordered)
{{ task_list_markdown }}
## Next Steps
Следующий агент: прочитай `.agent/context/project-state.md`, затем `.agent/tasks/manifest.json` и приступай к первой `pending` задаче.
## Caveats
{{ caveats_list }}
## Checkpoints
Файл: `.agent/checkpoints.json` — состояние фаз и список задач.
-17
View File
@@ -1,17 +0,0 @@
# Project Rules
Правила, которым агент обязан следовать во всех фазах.
Добавляйте сюда условия, которые должны соблюдаться всегда — они будут прочитаны
перед началом каждой фазы и учтены при декомпозиции и реализации.
## Обязательные правила
- (укажите правила, например: «Всегда использовать tabs для отступов»)
## Запреты
- (укажите запреты, например: «Не трогать CI/CD конфигурацию»)
## Конвенции проекта
- (укажите конвенции, например: «Имена классов в PascalCase, функции в snake_case»)
-43
View File
@@ -1,43 +0,0 @@
# Project State
# Auto-generated — updated by ANALYSE (initial) and METASTATE (on updates)
**Last updated:** {{ timestamp }}
**Session:** {{ session_id }}
## Project Type
{{ project_type }}
## Tech Stack
| Category | Technology |
|----------|-----------|
| Language | {{ language }} |
| Framework | {{ framework }} |
| Database | {{ database }} |
| Test runner | {{ test_runner }} |
| Package manager | {{ package_manager }} |
## Current Architecture
{{ architecture_description }}
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| {{ module_name }} | {{ existing / stub / new }} | {{ module_description }} |
## Decisions in Effect
| ADR | Decision | Status |
|-----|----------|--------|
| {{ adr_id }} | {{ decision_summary }} | {{ active / superseded }} |
## Testing Status
{{ testing_summary }}
## Open Concerns
- {{ concern_1 }}
-29
View File
@@ -1,29 +0,0 @@
{
"$schema": ".agent/src/TEMPLATES/schemas/request-schema.json",
"template_version": "1.0",
"request_id": "req-{{ task_id }}",
"task_id": "{{ task_id }}",
"title": "{{ task_title }}",
"status": "ready_for_review",
"created_at": "{{ timestamp }}",
"goal": "{{ task_goal }}",
"changes": {
"summary": "{{ changes_summary }}",
"commits": [
"{{ commit_hash }}"
],
"files_changed": [
"path/to/file.py"
]
},
"verification": {
"tests_passed": "{{ test_results }}",
"lsp_clean": true
},
"fulfills_ac": [
"{{ acceptance_criterion }}"
]
}
-7
View File
@@ -1,7 +0,0 @@
# Risk Register
| # | Assumption | Impact if wrong | Mitigation | Review trigger |
|---|---|---|---|---|
| R1 | Пользователи имеют Python 3.11+ | Проект не запускается на старых версиях | Указать требование в README, CI-проверка | При жалобе на установку |
| R2 | JSON-файлы не превышают 10MB | Деградация производительности | Добавить лимит в model.py | При первом замедлении |
| R3 | ... | ... | ... | ... |
-42
View File
@@ -1,42 +0,0 @@
# Roadmap Sources
# Auto-generated — created by ROADMAP phase
**Created:** {{ timestamp }}
**Session:** {{ session_id }}
## Priority Legend
- **P0** — Critical, do next
- **P1** — Important, do soon
- **P2** — Nice to have
- **P3** — Future / deferred
## Sources
### FUTURE Plans
| Plan | Priority | Status | Origin File |
|------|----------|--------|-------------|
| {{ plan_title }} | {{ P0-P3 }} | {{ active / archived }} | FUTURE/{{ filename }}.md |
### ADR-Derived Tasks
| Source ADR | Task | Priority |
|------------|------|----------|
| {{ adr_id }} | {{ task_description }} | {{ P0-P3 }} |
### User Requests
| Request | Priority | Source |
|---------|----------|--------|
| {{ request }} | {{ P0-P3 }} | {{ direct / issue / feedback }} |
### Agent-Identified Improvements
| Observation | Suggested Task | Priority |
|-------------|----------------|----------|
| {{ observation }} | {{ task }} | {{ P0-P3 }} |
## Consolidated Priority Queue
1. **{{ task_title }}** ({{ origin }}) — {{ priority }}
@@ -1,66 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/checkpoints/3.0.0",
"title": "MetaAgent Checkpoints",
"description": "Schema for .agent/checkpoints.json — session state",
"type": "object",
"properties": {
"metaagent_version": {
"type": "string",
"description": "MetaAgent version that created this checkpoint",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"session_id": {
"type": "string",
"description": "Unique session identifier"
},
"target_repo": {
"type": "string",
"description": "Path to the target repository"
},
"goal": {
"type": ["string", "null"],
"description": "Session goal (set by user, may be null until first task)"
},
"project_type": {
"type": ["string", "null"],
"enum": [null, "existing", "greenfield", "scaffold"],
"description": "Type of the target project (set in ANALYSE phase)"
},
"phases": {
"type": "object",
"properties": {
"init": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"analyse": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"roadmap": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"design": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"decomposition": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"execution": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"metastate": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"handoff": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] }
},
"additionalProperties": false
},
"tasks": {
"type": "array",
"description": "List of tasks (one-liners after archiving)",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"status": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "archived"] }
},
"required": ["id", "title", "status"],
"additionalProperties": false
}
},
"last_updated": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
}
},
"required": ["metaagent_version", "session_id", "phases", "last_updated"],
"additionalProperties": false
}
@@ -1,60 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/decisions-index/3.0.0",
"title": "MetaAgent Decisions Index",
"description": "Schema for .agent/decisions/index.json — machine-readable index of ADRs",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Schema version (3.0 in v3.0+, 2.0 still valid from v2.1)",
"enum": ["2.0", "3.0"]
},
"decisions": {
"type": "array",
"description": "List of architecture decision records",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9]{3,}$",
"description": "ADR number (001, 002, ...)"
},
"title": {
"type": "string",
"description": "Short title of the decision"
},
"status": {
"type": "string",
"enum": ["proposed", "accepted", "deprecated", "superseded"],
"description": "ADR status"
},
"file": {
"type": "string",
"description": "Filename in .agent/decisions/ (e.g. 001-stack.md)"
},
"date": {
"type": "string",
"format": "date",
"description": "Decision date (ISO 8601)"
}
},
"required": ["id", "title", "file"],
"additionalProperties": false
}
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of index creation"
},
"updated_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
}
},
"required": ["version", "decisions"],
"additionalProperties": false
}
@@ -1,95 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/task-manifest/3.0.0",
"title": "MetaAgent Task Manifest",
"description": "Schema for .agent/tasks/manifest.json — the global task manifest",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Schema version",
"enum": ["3.0"]
},
"session_id": {
"type": "string",
"description": "Session ID that created this manifest"
},
"goal": {
"type": "string",
"description": "Overall goal of the session"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of creation"
},
"tasks": {
"type": "array",
"description": "List of tasks",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^(T[0-9]+|T-INV-[0-9]+)$",
"description": "Unique task identifier (T1, T2, ... or T-INV-N for invariants)"
},
"title": {
"type": "string",
"description": "Task title"
},
"description": {
"type": "string",
"description": "Detailed description"
},
"type": {
"type": "string",
"enum": ["feature", "refactor", "test", "fix", "config", "design", "docs", "invariant"],
"description": "Task type"
},
"origin": {
"type": "string",
"description": "Task source (roadmap:file, adr:NNN, user:direct, agent:analysis, decomposition, invariant:NNN, risk:R-NNN)"
},
"files": {
"type": "array",
"items": { "type": "string" },
"description": "Files affected by this task"
},
"depends_on": {
"type": "array",
"items": { "type": "string" },
"description": "Task IDs this task depends on"
},
"acceptance_criteria": {
"type": "array",
"items": { "type": "string" },
"description": "Measurable criteria for completion"
},
"context": {
"type": "string",
"description": "Additional context or references"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed", "failed", "archived"],
"description": "Current task status"
},
"claimed_by": {
"type": "string",
"description": "Worker ID if claimed"
},
"claimed_at": {
"type": "string",
"format": "date-time",
"description": "When the task was claimed"
}
},
"required": ["id", "title", "type", "status"],
"additionalProperties": false
}
}
},
"required": ["version", "tasks"],
"additionalProperties": false
}
-50
View File
@@ -1,50 +0,0 @@
# Session Summary
**Session:** {{ session_id }}
**Target:** {{ target_repo }}
**MetaAgent version:** {{ version }}
**Date:** {{ date }}
## Phase Status
| Phase | Status |
|---|---|
| INIT | {{ init_status }} |
| ANALYSE | {{ analyse_status }} |
| ROADMAP | {{ roadmap_status }} |
| DESIGN | {{ design_status }} |
| DECOMPOSITION | {{ decomposition_status }} |
| EXECUTION | {{ execution_status }} |
| METASTATE | {{ metastate_status }} |
| HANDOFF | {{ handoff_status }} |
## Tasks
| Status | Count |
|---|---|
| Total | {{ total }} |
| Pending | {{ pending }} |
| In Progress | {{ in_progress }} |
| Completed | {{ completed }} |
| Archived | {{ archived }} |
| Failed/Skipped | {{ failed }} |
**By origin:**
- user:direct: {{ user_direct_count }}
- roadmap: {{ roadmap_count }}
- adr: {{ adr_count }}
- decomposition: {{ decomposition_count }}
- (другое): {{ other_count }}
## Commands Invoked (если были)
{{ commands_invoked_list }}
## Quick Links
- Task Manifest: `.agent/tasks/manifest.json`
- Handoff Summary: `.agent/handoff-summary.md`
- Project State: `.agent/context/project-state.md`
- ADR: `.agent/decisions/`
- Risk Register: `.agent/context/risk-register.md`
- Red Team Report: `.agent/context/red-team-report.md`
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": ".agent/src/TEMPLATES/schemas/task-manifest-schema.json",
"version": "3.0",
"session_id": "{{ session_id }}",
"goal": "{{ goal }}",
"created_at": "{{ timestamp }}",
"tasks": [
{
"id": "T1",
"title": "{{ task_title }}",
"description": "{{ task_description }}",
"type": "feature|refactor|test|fix|config|design|docs|invariant",
"origin": "user:direct",
"files": ["path/to/file1.py", "path/to/file2.py"],
"depends_on": [],
"acceptance_criteria": [
"Критерий 1: ...",
"Критерий 2: ..."
],
"context": "Дополнительная информация",
"status": "pending"
}
]
}
-42
View File
@@ -1,42 +0,0 @@
# Task Manifest
**Session:** {{ session_id }}
**Goal:** {{ goal }}
**Date:** {{ timestamp }}
---
## Task Overview
| ID | Title | Type | Depends On | Status |
|---|---|---|---|---|
| T1 | {{ title }} | {{ type }} | — | pending |
| T2 | {{ title }} | {{ type }} | T1 | pending |
**Total tasks:** {{ count }}
---
## Task Details
### T1: {{ title }}
**Type:** {{ type }}
**Description:** {{ description }}
**Files:**
- `{{ file_path }}`
**Depends on:** —
**Acceptance Criteria:**
- [ ] {{ criterion }}
- [ ] {{ criterion }}
**Context:** {{ context }}
---
### T2: {{ title }}
...
-1
View File
@@ -1 +0,0 @@
3.0.0
-240
View File
@@ -1,240 +0,0 @@
# WORKFLOW — Сквозной пример сессии v3.0
---
## Сценарий: рефакторинг auth-модуля
**Цель:** Вынести логику из `auth/login.py` (450 строк, монолит) в отдельные модули `auth/router.py`, `auth/schemas.py`, `auth/deps.py`.
**Целевой репозиторий:** `github.com/example/fastapi-app`
**Пользователь:** «Вынеси авторизацию в отдельные модули».
**MetaAgent:** v3.0.0
---
### PROJECT LOOP
#### INIT
Агент читает `AGENTS.md`, переходит в `.agent/src/GUIDE.md`. Понимает цикл. Создаёт `.agent/`, копирует исходники, инициализирует `checkpoints.json`:
```json
{
"metaagent_version": "3.0.0",
"session_id": "ses_v30_001",
"target_repo": "/tmp/fastapi-app",
"goal": "Вынести авторизацию в auth/{router,schemas,deps}.py",
"project_type": null,
"phases": {
"init": "completed",
"analyse": "pending",
"roadmap": "pending",
"design": "pending",
"decomposition": "pending",
"execution": "pending",
"metastate": "pending",
"handoff": "pending"
},
"tasks": [],
"last_updated": "2026-10-08T15:00:00Z"
}
```
#### ANALYSE
Агент сканирует проект:
- Стек: Python 3.12, FastAPI, SQLAlchemy, pytest.
- `auth/login.py` — 450 строк, монолит (цель рефакторинга).
- Тесты: 48 passed (baseline).
Создаёт:
- `.agent/context/analysis-report.md`
- `.agent/context/project-state.md` (начальный)
`checkpoints.json`: `project_type = "existing"`, `phases.analyse = "completed"`.
#### ROADMAP
- `FUTURE/` — пусто.
- `.agent/decisions/` — пусто.
- Единственный источник — пользовательский запрос.
Создаёт `.agent/roadmap/sources.md`:
```markdown
## User Requests
| Вынести авторизацию | P0 | user:direct |
## Consolidated Priority Queue
1. Вынести auth/ (user:direct) — P0
```
`phases.roadmap = "completed"`.
#### DESIGN
**Пропускается** (existing-проект). `phases.design = "skipped"`.
#### DECOMPOSITION
Задачи:
```json
{
"tasks": [
{
"id": "T1",
"title": "Создать auth/router.py",
"origin": "user:direct",
"files": ["app/auth/router.py"],
"depends_on": [],
"acceptance_criteria": [
"Роуты авторизации вынесены из auth/login.py",
"auth/router.py экспортирует router",
"Существующие тесты проходят"
],
"status": "pending"
},
{
"id": "T2",
"title": "Создать auth/schemas.py",
"origin": "user:direct",
"files": ["app/auth/schemas.py"],
"depends_on": ["T1"],
"acceptance_criteria": [
"Pydantic схемы вынесены в auth/schemas.py",
"Существующие тесты проходят"
],
"status": "pending"
},
{
"id": "T3",
"title": "Создать auth/deps.py",
"origin": "user:direct",
"files": ["app/auth/deps.py"],
"depends_on": ["T1"],
"acceptance_criteria": [
"Dependency injection функции вынесены в auth/deps.py",
"Существующие тесты проходят"
],
"status": "pending"
}
]
}
```
`phases.decomposition = "completed"`.
---
### WORK LOOP (первая итерация)
#### EXECUTION — задача T1
1. Берёт T1 (`pending`, нет зависимостей).
2. `status = "in_progress"`.
3. Создаёт `app/auth/router.py` — переносит роуты.
4. Тесты: 48/48.
5. Коммит: `abc1234 — refactor: extract auth router`.
6. Создаёт request:
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "Создать auth/router.py",
"status": "ready_for_review",
"goal": "Вынести роуты авторизации",
"changes": {
"summary": "Роуты авторизации вынесены из auth/login.py в auth/router.py",
"commits": ["abc1234"],
"files_changed": ["app/auth/router.py", "app/auth/__init__.py", "tests/test_auth_router.py"]
},
"verification": { "tests_passed": "48/48", "lsp_clean": true },
"fulfills_ac": ["Роуты вынесены", "Тесты проходят"]
}
```
7. `T1 → completed`.
#### EXECUTION — задача T2
Создаёт `auth/schemas.py`, request `req-T2`. T2 → completed.
#### EXECUTION — задача T3
Создаёт `auth/deps.py`, request `req-T3`. T3 → completed.
Задачи закончились. Агент ждёт команду.
---
### METASTATE (по команде пользователя)
**Пользователь:** «обнови метасостояние».
1. **Ревью requests:** три request-а, все approved.
- `req-T1`, `req-T2`, `req-T3` → `.agent/requests/archive/`.
2. **Архивация задач:**
- T1, T2, T3 → `.agent/archive/tasks/`.
- В `manifest.json` — one-liner: `status: "archived"`.
3. **Обновление project-state.md:**
```markdown
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| app/auth/router.py | new | Вынесенные роуты |
| app/auth/schemas.py | new | Pydantic схемы |
| app/auth/deps.py | new | Dependency injection |
```
4. **Создание handoff-summary.md:**
```markdown
## Session Summary
**Goal:** Рефакторинг авторизации
**Completed:** 3/3 tasks
**Approved requests:** req-T1, req-T2, req-T3
## Project State
auth разбит на router + schemas + deps.
Исходный auth/login.py: 450 → 120 строк.
## Next Steps
- Проверить, не осталось ли прямых импортов из старого login.py
- Обновить main.py если нужно
```
---
### HANDOFF
```
HANDOFF COMPLETE
Session: ses_v30_001
Target: /tmp/fastapi-app
Type: existing
Tasks: 3/3 completed
Следующий агент начинает с .agent/handoff-summary.md
```
---
## Использование команд в процессе
В любой момент сессии пользователь мог вызвать:
- **«запиши это как ADR»** → `COMMANDS/adr.md` создал бы `.agent/decisions/001-modular-auth.md`.
- **«red team»** → `COMMANDS/red-team.md` создал бы `.agent/context/red-team-report.md` с попыткой сломать новую структуру.
- **«risk register»** → `COMMANDS/risk-register.md` зафиксировал бы допущения (например, «считаем, что порядок middleware не важен»).
Команды **не обязательны**. Если не вызваны — `.agent/decisions/`, `risk-register.md`, `red-team-report.md` не создаются.
-365
View File
@@ -1,365 +0,0 @@
#!/usr/bin/env pwsh
# MetaAgent — установка исходников в целевой проект
# Usage: .\install.ps1 [[-Path] target_path] [-Check] [-Update]
param(
[string]$Path = "",
[switch]$Check,
[switch]$Update,
[switch]$Help
)
$MetaAgentSrc = Split-Path -Parent $MyInvocation.MyCommand.Path
# --- helpers ---
function Write-Info { Write-Host " →" -NoNewline -ForegroundColor Blue; Write-Host " $args" }
function Write-Ok { Write-Host " ✓" -NoNewline -ForegroundColor Green; Write-Host " $args" }
function Write-Skip { Write-Host " −" -NoNewline -ForegroundColor Yellow; Write-Host " $args" }
function Write-Warn { Write-Host " ⚠" -NoNewline -ForegroundColor Yellow; Write-Host " $args" }
function Write-Fail { Write-Host " ✗" -NoNewline -ForegroundColor Red; Write-Host " $args" }
function Write-Header { param([string]$Label)
Write-Host ""
Write-Host ("─" * 40)
Write-Host " $Label"
Write-Host ("─" * 40)
}
function Show-Usage {
@"
Usage: install.ps1 [[-Path] target_path] [-Check] [-Update] [-Help]
Install MetaAgent sources into <target>/.agent/src/
Options:
-Path Path to target project (default: interactive prompt)
-Check Dry-run: only check target readiness, no install
-Update Overwrite existing files in .agent/src/
-Help Show this help
Examples:
.\install.ps1
.\install.ps1 -Path C:\Projects\MyApp
.\install.ps1 -Path C:\Projects\MyApp -Check
.\install.ps1 -Path C:\Projects\MyApp -Update
"@
exit 0
}
if ($Help) { Show-Usage }
# --- resolve target ---
$TargetPath = $Path
if (-not $TargetPath) {
$TargetPath = Read-Host "Enter path to target project"
}
$TargetPath = $TargetPath.Trim()
# --- pre-flight -----------------------------------------------------------
Write-Header "Pre-flight"
# 1. target exists?
if (-not (Test-Path $TargetPath -PathType Container)) {
Write-Fail "Target directory '$TargetPath' does not exist."
exit 1
}
$TargetPath = (Resolve-Path $TargetPath).Path
Write-Ok "Target: $TargetPath"
# 2. write permission? (try to create a temp file as probe)
$probe = [System.IO.Path]::Combine($TargetPath, ".metaagent_probe.tmp")
try {
[System.IO.File]::WriteAllBytes($probe, [byte[]]@())
Remove-Item $probe -Force
Write-Ok "Write permission: yes"
} catch {
Write-Fail "No write permission on '$TargetPath'."
exit 1
}
# 3. already installed? compare versions
$AgentDir = Join-Path $TargetPath ".agent"
$SrcDir = Join-Path $AgentDir "src"
$VersionFile = Join-Path $MetaAgentSrc "VERSION"
$Version = if (Test-Path $VersionFile -PathType Leaf) {
(Get-Content $VersionFile -Raw -Encoding UTF8).Trim()
} else { "?" }
$oldVerPath = Join-Path $SrcDir "VERSION"
if (Test-Path $oldVerPath -PathType Leaf) {
$oldVer = (Get-Content $oldVerPath -Raw -Encoding UTF8).Trim()
if ($oldVer -ne $Version) {
Write-Info "Existing MetaAgent v$oldVer found → upgrading to v$Version"
} else {
Write-Skip "MetaAgent v${Version} already installed (use -Update to reinstall)"
if (-not $Check) {
Write-Warn "No changes applied. Run with -Update to overwrite existing files."
}
}
} else {
Write-Info "Fresh install: MetaAgent v$Version"
}
# 4. summary
$AgentsMd = Join-Path $TargetPath "AGENTS.md"
$RulesDir = Join-Path $AgentDir "rules"
$DecisionsDir = Join-Path $AgentDir "decisions"
$TasksDir = Join-Path $AgentDir "tasks"
$ContextDir = Join-Path $AgentDir "context"
$ArchiveDir = Join-Path $AgentDir "archive"
$RequestsDir = Join-Path $AgentDir "requests"
$RoadmapDir = Join-Path $AgentDir "roadmap"
$TempDir = Join-Path $TargetPath ".temp"
$DirList = @(
$SrcDir, $RulesDir, $DecisionsDir, $TasksDir,
(Join-Path $TasksDir "backlog"), $ContextDir,
$ArchiveDir,
(Join-Path $ArchiveDir "tasks"),
(Join-Path $ArchiveDir "decisions"),
(Join-Path $ArchiveDir "checkpoints"),
(Join-Path $RequestsDir "active"),
(Join-Path $RequestsDir "archive"),
$RoadmapDir,
(Join-Path $RoadmapDir "archive"),
$TempDir
)
if ($Check) {
Write-Host ""
Write-Info "--check mode: all checks passed, no changes applied."
exit 0
}
# --- phase 1: directories ------------------------------------------------
Write-Header "Directories"
foreach ($d in $DirList) {
$null = New-Item -ItemType Directory -Path $d -Force
$short = $d.Replace("$TargetPath\", "")
if (Test-Path $d -PathType Container) {
Write-Ok $short
} else {
Write-Fail "$short (creation failed)"
}
}
# --- phase 2: files ------------------------------------------------------
Write-Header "Files"
$copyCount = 0
$skipCount = 0
$failCount = 0
function Copy-File {
param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf
$dst = Join-Path $DstDir $name
if (-not (Test-Path $Src -PathType Leaf)) {
Write-Skip "$name (source not found)"
$script:skipCount++
return
}
if ($Update -or -not (Test-Path $dst)) {
try {
Copy-Item $Src $dst -Force -ErrorAction Stop
Write-Ok $name
$script:copyCount++
} catch {
Write-Fail $name
$script:failCount++
}
} else {
Write-Skip "$name (exists, use -Update to overwrite)"
$script:skipCount++
}
}
function Copy-Dir {
param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf
$dst = Join-Path $DstDir $name
if (-not (Test-Path $Src -PathType Container)) {
Write-Skip "$name/ (source not found)"
$script:skipCount++
return
}
$null = New-Item -ItemType Directory -Path $dst -Force
try {
if ($Update) {
Get-ChildItem $Src | ForEach-Object {
Copy-Item $_.FullName $dst -Recurse -Force -ErrorAction Stop
}
} else {
Get-ChildItem $Src | ForEach-Object {
$targetPath = Join-Path $dst $_.Name
if (-not (Test-Path $targetPath)) {
Copy-Item $_.FullName $dst -Recurse -ErrorAction Stop
}
}
}
Write-Ok "$name/"
$script:copyCount++
} catch {
Write-Fail "$name/ (partial copy)"
$script:failCount++
}
}
Copy-File (Join-Path $MetaAgentSrc "GUIDE.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "BOUNDARIES.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "CHANGELOG.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "WORKFLOW.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "VERSION") $SrcDir
Copy-Dir (Join-Path $MetaAgentSrc "PROTOCOLS") $SrcDir
Copy-Dir (Join-Path $MetaAgentSrc "COMMANDS") $SrcDir
Copy-Dir (Join-Path $MetaAgentSrc "TEMPLATES") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.sh") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.ps1") $SrcDir
# --- phase 3: AGENTS.md --------------------------------------------------
Write-Header "AGENTS.md"
if (-not (Test-Path $AgentsMd -PathType Leaf)) {
$content = @"
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/GUIDE.md) v$Version —
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Команды (on-demand) | `.agent/src/COMMANDS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| История версий | `.agent/src/CHANGELOG.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Пример работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Слепок проекта | `.agent/context/project-state.md` |
| Анализ репозитория | `.agent/context/analysis-report.md` |
| Дорожная карта | `.agent/roadmap/sources.md` |
| Манифест задач | `.agent/tasks/manifest.json` |
| Сводка для следующего агента | `.agent/handoff-summary.md` |
| Сводка сессии | `.agent/session-summary.md` |
## Для агента
Жизненный цикл MetaAgent v$Version:
```
INIT → ANALYSE → ROADMAP → [DESIGN] → DECOMPOSITION → EXECUTION → METASTATE → HANDOFF
```
1. **Прочитай** `.agent/src/GUIDE.md` — пойми цикл и доступные команды.
2. **Прочитай** `.agent/src/BOUNDARIES.md` — соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` — выполни правила пользователя.
4. **Проверь** `.agent/checkpoints.json` — если существует, используй как состояние сессии.
5. **Проверь** `.agent/context/project-state.md` — получи актуальную картину.
6. **Проверь** `.agent/tasks/manifest.json` — если существует, выполняй задачи по порядку.
7. Если `.agent/` не инициализирован или устарел — запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии.
## Команды (on-demand)
В любой момент пользователь может вызвать:
- `/adr` — записать архитектурное решение
- `/red-team` — попытаться сломать дизайн
- `/risk-register` — зафиксировать допущения
- `/alt-arch` — описать альтернативу
- `/invariant-tests` — тесты-инварианты для ADR
"@
$utf8 = [System.Text.Encoding]::UTF8
[System.IO.File]::WriteAllBytes($AgentsMd, $utf8.GetBytes($content))
Write-Ok "AGENTS.md created"
} elseif ($Update) {
$content = @"
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/GUIDE.md) v$Version —
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Команды (on-demand) | `.agent/src/COMMANDS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| История версий | `.agent/src/CHANGELOG.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Пример работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Слепок проекта | `.agent/context/project-state.md` |
| Анализ репозитория | `.agent/context/analysis-report.md` |
| Дорожная карта | `.agent/roadmap/sources.md` |
| Манифест задач | `.agent/tasks/manifest.json` |
| Сводка для следующего агента | `.agent/handoff-summary.md` |
| Сводка сессии | `.agent/session-summary.md` |
## Для агента
Жизненный цикл MetaAgent v$Version:
```
INIT → ANALYSE → ROADMAP → [DESIGN] → DECOMPOSITION → EXECUTION → METASTATE → HANDOFF
```
1. **Прочитай** `.agent/src/GUIDE.md` — пойми цикл и доступные команды.
2. **Прочитай** `.agent/src/BOUNDARIES.md` — соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` — выполни правила пользователя.
4. **Проверь** `.agent/checkpoints.json` — если существует, используй как состояние сессии.
5. **Проверь** `.agent/context/project-state.md` — получи актуальную картину.
6. **Проверь** `.agent/tasks/manifest.json` — если существует, выполняй задачи по порядку.
7. Если `.agent/` не инициализирован или устарел — запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии.
## Команды (on-demand)
В любой момент пользователь может вызвать:
- `/adr` — записать архитектурное решение
- `/red-team` — попытаться сломать дизайн
- `/risk-register` — зафиксировать допущения
- `/alt-arch` — описать альтернативу
- `/invariant-tests` — тесты-инварианты для ADR
"@
$utf8 = [System.Text.Encoding]::UTF8
[System.IO.File]::WriteAllBytes($AgentsMd, $utf8.GetBytes($content))
Write-Ok "AGENTS.md updated"
} else {
Write-Skip "AGENTS.md (exists, use -Update to overwrite)"
$script:skipCount++
}
# --- summary -------------------------------------------------------------
Write-Header "Summary"
Write-Host " MetaAgent v$Version → $SrcDir"
Write-Host ""
if ($copyCount -gt 0) { Write-Ok "$copyCount file(s) copied" }
if ($skipCount -gt 0) { Write-Skip "$skipCount file(s) skipped" }
if ($failCount -gt 0) { Write-Fail "$failCount file(s) failed" }
Write-Host ""
if ($failCount -eq 0) {
Write-Ok "Installation completed successfully."
} else {
Write-Fail "Installation completed with $failCount error(s)."
exit 1
}
-312
View File
@@ -1,312 +0,0 @@
#!/usr/bin/env bash
# MetaAgent — установка исходников в целевой проект
# Usage: ./install.sh [--check|--update] [target_path]
set -euo pipefail
METAAGENT_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- helpers ---
red=; grn=; ylw=; blu=; rst=
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
red=$(tput setaf 1); grn=$(tput setaf 2)
ylw=$(tput setaf 3); blu=$(tput setaf 4)
rst=$(tput sgr0)
fi
info() { echo " ${blu}→${rst} $*"; }
ok() { echo " ${grn}✓${rst} $*"; }
skip() { echo " ${ylw}−${rst} $*"; }
warn() { echo " ${ylw}⚠${rst} $*"; }
fail() { echo " ${red}✗${rst} $*"; }
header(){ echo; echo "────────────────────────────────────────"; echo " $*"; echo "────────────────────────────────────────"; }
usage() {
cat <<EOF
Usage: $0 [--check|--update] [target_path]
Install MetaAgent sources into <target>/.agent/src/
Options:
--check, -c Dry-run: only check target readiness, no install
--update, -u Overwrite existing files in .agent/src/
--help, -h Show this help
Examples:
$0
$0 /path/to/project
$0 --check /path/to/project
$0 --update /path/to/project
EOF
exit 0
}
# --- arg parsing ---
CHECK=false
UPDATE=false
TARGET_PATH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--check|-c) CHECK=true; shift ;;
--update|-u) UPDATE=true; shift ;;
--help|-h) usage ;;
--*) echo "${red}Unknown option:${rst} $1"; usage ;;
*) TARGET_PATH="$1"; shift ;;
esac
done
# --- resolve target ---
if [[ -z "$TARGET_PATH" ]]; then
read -r -p "Enter path to target project: " TARGET_PATH
fi
TARGET_PATH="${TARGET_PATH/#\~/$HOME}"
# --- pre-flight -----------------------------------------------------------
header "Pre-flight"
# 1. target exists?
if [[ ! -d "$TARGET_PATH" ]]; then
fail "Target directory '$TARGET_PATH' does not exist."
exit 1
fi
# resolve to absolute path
TARGET_PATH="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || {
fail "Cannot access '$TARGET_PATH'."
exit 1
}
ok "Target: $TARGET_PATH"
# 2. write permission?
if [[ ! -w "$TARGET_PATH" ]]; then
fail "No write permission on '$TARGET_PATH'."
exit 1
fi
ok "Write permission: yes"
# 3. already installed? compare versions
AGENT_DIR="$TARGET_PATH/.agent"
SRC_DIR="$AGENT_DIR/src"
VERSION="$(cat "$METAAGENT_SRC/VERSION" 2>/dev/null || echo '?')"
if [[ -f "$SRC_DIR/VERSION" ]]; then
OLD_VER="$(cat "$SRC_DIR/VERSION" 2>/dev/null || echo '?')"
if [[ "$OLD_VER" != "$VERSION" ]]; then
info "Existing MetaAgent v${OLD_VER} found → upgrading to v${VERSION}"
else
skip "MetaAgent v${VERSION} already installed (use --update to reinstall)"
if [[ "$CHECK" == false ]]; then
warn "No changes applied. Run with --update to overwrite existing files."
fi
fi
else
info "Fresh install: MetaAgent v$VERSION"
fi
# 4. summary
AGENTS_MD="$TARGET_PATH/AGENTS.md"
RULES_DIR="$AGENT_DIR/rules"
DECISIONS_DIR="$AGENT_DIR/decisions"
TASKS_DIR="$AGENT_DIR/tasks"
CONTEXT_DIR="$AGENT_DIR/context"
ARCHIVE_DIR="$AGENT_DIR/archive"
ARCHIVE_TASKS_DIR="$ARCHIVE_DIR/tasks"
ARCHIVE_DECISIONS_DIR="$ARCHIVE_DIR/decisions"
ARCHIVE_CHECKPOINTS_DIR="$ARCHIVE_DIR/checkpoints"
REQUESTS_DIR="$AGENT_DIR/requests"
REQUESTS_ACTIVE_DIR="$REQUESTS_DIR/active"
REQUESTS_ARCHIVE_DIR="$REQUESTS_DIR/archive"
ROADMAP_DIR="$AGENT_DIR/roadmap"
ROADMAP_ARCHIVE_DIR="$ROADMAP_DIR/archive"
TEMP_DIR="$TARGET_PATH/.temp"
if [[ "$CHECK" == true ]]; then
echo ""
info "${ylw}--check mode:${rst} all checks passed, no changes applied."
exit 0
fi
# --- phase 1: directories ------------------------------------------------
header "Directories"
mkdir -p "$SRC_DIR" "$RULES_DIR" "$DECISIONS_DIR" "$TASKS_DIR" "$TASKS_DIR/backlog" \
"$CONTEXT_DIR" "$ARCHIVE_DIR" "$ARCHIVE_TASKS_DIR" "$ARCHIVE_DECISIONS_DIR" \
"$ARCHIVE_CHECKPOINTS_DIR" \
"$REQUESTS_ACTIVE_DIR" "$REQUESTS_ARCHIVE_DIR" \
"$ROADMAP_DIR" "$ROADMAP_ARCHIVE_DIR" \
"$TEMP_DIR"
for d in "$SRC_DIR" "$RULES_DIR" "$DECISIONS_DIR" "$TASKS_DIR" "$TASKS_DIR/backlog" \
"$CONTEXT_DIR" "$ARCHIVE_DIR" "$ARCHIVE_TASKS_DIR" "$ARCHIVE_DECISIONS_DIR" \
"$ARCHIVE_CHECKPOINTS_DIR" \
"$REQUESTS_ACTIVE_DIR" "$REQUESTS_ARCHIVE_DIR" \
"$ROADMAP_DIR" "$ROADMAP_ARCHIVE_DIR" \
"$TEMP_DIR"; do
short="${d#$TARGET_PATH/}"
if [[ -d "$d" ]]; then
ok "$short"
else
fail "$short (creation failed)"
fi
done
# --- phase 2: files ------------------------------------------------------
header "Files"
COPY_COUNT=0
SKIP_COUNT=0
FAIL_COUNT=0
copy_file() {
local src="$1" dst_dir="$2"
local name; name="$(basename "$src")"
local dst="$dst_dir/$name"
if [[ ! -f "$src" ]]; then
skip "$name (source not found)"
SKIP_COUNT=$((SKIP_COUNT + 1))
return
fi
if [[ "$UPDATE" == true ]] || [[ ! -f "$dst" ]]; then
if cp "$src" "$dst"; then
ok "$name"
COPY_COUNT=$((COPY_COUNT + 1))
else
fail "$name"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
else
skip "$name (exists, use --update to overwrite)"
SKIP_COUNT=$((SKIP_COUNT + 1))
fi
}
copy_dir() {
local src="$1" dst_dir="$2"
local name; name="$(basename "$src")"
local dst="$dst_dir/$name"
if [[ ! -d "$src" ]]; then
skip "$name/ (source not found)"
SKIP_COUNT=$((SKIP_COUNT + 1))
return
fi
mkdir -p "$dst"
if [[ "$UPDATE" == true ]]; then
if cp -rf "$src"/* "$dst/" 2>/dev/null; then
ok "$name/"
COPY_COUNT=$((COPY_COUNT + 1))
else
fail "$name/ (partial copy)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
else
cp -rn "$src"/* "$dst/" 2>/dev/null || true
ok "$name/"
COPY_COUNT=$((COPY_COUNT + 1))
fi
}
copy_file "$METAAGENT_SRC/GUIDE.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/BOUNDARIES.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/CHANGELOG.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/WORKFLOW.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/VERSION" "$SRC_DIR"
copy_dir "$METAAGENT_SRC/PROTOCOLS" "$SRC_DIR"
copy_dir "$METAAGENT_SRC/COMMANDS" "$SRC_DIR"
copy_dir "$METAAGENT_SRC/TEMPLATES" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.sh" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.ps1" "$SRC_DIR"
# --- phase 3: AGENTS.md --------------------------------------------------
header "AGENTS.md"
create_agents_md() {
cat > "$1" << AGENTS_EOF
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/GUIDE.md) v$VERSION —
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | \`.agent/src/GUIDE.md\` |
| Протоколы фаз | \`.agent/src/PROTOCOLS/\` |
| Команды (on-demand) | \`.agent/src/COMMANDS/\` |
| Шаблоны артефактов | \`.agent/src/TEMPLATES/\` |
| Границы (что разрешено/запрещено) | \`.agent/src/BOUNDARIES.md\` |
| История версий | \`.agent/src/CHANGELOG.md\` |
| Правила проекта | \`.agent/rules/project-rules.md\` |
| Пример работы | \`.agent/src/WORKFLOW.md\` |
| Версия | \`.agent/src/VERSION\` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | \`.agent/checkpoints.json\` |
| Слепок проекта | \`.agent/context/project-state.md\` |
| Анализ репозитория | \`.agent/context/analysis-report.md\` |
| Дорожная карта | \`.agent/roadmap/sources.md\` |
| Манифест задач | \`.agent/tasks/manifest.json\` |
| Сводка для следующего агента | \`.agent/handoff-summary.md\` |
| Сводка сессии | \`.agent/session-summary.md\` |
## Для агента
Жизненный цикл MetaAgent v$VERSION:
\`\`\`
INIT → ANALYSE → ROADMAP → [DESIGN] → DECOMPOSITION → EXECUTION → METASTATE → HANDOFF
\`\`\`
1. **Прочитай** \`.agent/src/GUIDE.md\` — пойми цикл и доступные команды.
2. **Прочитай** \`.agent/src/BOUNDARIES.md\` — соблюдай границы.
3. **Прочитай** \`.agent/rules/project-rules.md\` — выполни правила пользователя.
4. **Проверь** \`.agent/checkpoints.json\` — если существует, используй как состояние сессии.
5. **Проверь** \`.agent/context/project-state.md\` — получи актуальную картину.
6. **Проверь** \`.agent/tasks/manifest.json\` — если существует, выполняй задачи по порядку.
7. Если \`.agent/\` не инициализирован или устарел — запусти \`install.sh --update\` для
обновления исходников MetaAgent до актуальной версии.
## Команды (on-demand)
В любой момент пользователь может вызвать:
- \`/adr\` — записать архитектурное решение
- \`/red-team\` — попытаться сломать дизайн
- \`/risk-register\` — зафиксировать допущения
- \`/alt-arch\` — описать альтернативу
- \`/invariant-tests\` — тесты-инварианты для ADR
AGENTS_EOF
}
if [[ ! -f "$AGENTS_MD" ]]; then
create_agents_md "$AGENTS_MD"
ok "AGENTS.md created"
elif [[ "$UPDATE" == true ]]; then
create_agents_md "$AGENTS_MD"
ok "AGENTS.md updated"
else
skip "AGENTS.md (exists, use --update to overwrite)"
SKIP_COUNT=$((SKIP_COUNT + 1))
fi
# --- summary -------------------------------------------------------------
header "Summary"
echo " MetaAgent v$VERSION → $SRC_DIR"
echo ""
if (( COPY_COUNT > 0 )); then
ok "${COPY_COUNT} file(s) copied"
fi
if (( SKIP_COUNT > 0 )); then
skip "${SKIP_COUNT} file(s) skipped"
fi
if (( FAIL_COUNT > 0 )); then
fail "${FAIL_COUNT} file(s) failed"
fi
echo ""
if (( FAIL_COUNT == 0 )); then
ok "Installation completed successfully."
else
fail "Installation completed with ${FAIL_COUNT} error(s)."
exit 1
fi
-272
View File
@@ -1,272 +0,0 @@
{
"metaagent_version": "3.0.0",
"session_id": "metaagent-init-2026-10-09",
"target_repo": "S:/Git/nixos",
"goal": "Установить metaagent, перенести накопленные данные (AGENTS.md, docs/arch/*) в структуру .agent/.",
"project_type": "existing",
"date": "2026-10-09T20:30",
"phases": {
"init": "completed",
"analyse": "pending",
"roadmap": "pending",
"design": "skipped",
"decomposition": "pending",
"execution": "pending",
"metastate": "pending",
"handoff": "pending"
},
"tasks": [
{
"id": "T1",
"title": "A1: mobile.nix импортирует несуществующий lib/xlib.nix",
"type": "fix",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"nix flake check проходит на .#nixOnDroidConfigurations.epral",
"configurations/mobile.nix:12 использует import ../lib/xlib"
],
"files": ["configurations/mobile.nix"],
"blocks": ["T15", "T16"],
"notes": "Правка как в configurations/default.nix:5. До правки epral мертв."
},
{
"id": "T2",
"title": "A2: убедиться, что nix flake check вообще запускается",
"type": "verify",
"status": "pending",
"origin": "user:direct",
"depends_on": ["T1"],
"acceptance_criteria": [
"nix flake check зелёный (после T1)",
"checks в deploy покрывают всё дерево outputs"
],
"files": ["deploy/default.nix"],
"blocks": ["T15"]
},
{
"id": "T3",
"title": "A3: явная финальная политика nftables на VDS",
"type": "fix",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"nft list ruleset на otreca показывает явное последнее правило или policy",
"firewall.* либо выключен, либо синхронизирован с nftables (не оба сразу)",
"ssh с внешнего адреса работает"
],
"files": ["configurations/vds.nix"],
"blocks": ["T11", "T12"],
"notes": "Сначала диагностика: nft list ruleset, systemctl status nftables firewall-nftables. Политика — белый список (предпочтительно) или мягкий вариант с явным финальным правилом."
},
{
"id": "T4",
"title": "B1: guard на несмонтированный носитель /mnt/services",
"type": "security",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"В xlib/helpers.nix есть mkStorageGuard",
"postgresql, samba, homebox, gitea, navidrome, syncthing, uptime-kuma, immich, nextcloud, calibre-web, 3x-ui, tape-rotation используют mkStorageGuard",
"Имитация отказа: umount /mnt/services + systemctl start postgresql → FAIL, а не пустая база",
"В AGENTS.md добавлена строка про findmnt перед рестартом (уже в R4)"
],
"files": [
"lib/xlib/helpers.nix",
"modules/server/postgresql.nix",
"modules/server/samba.nix",
"modules/server/homebox.nix",
"modules/server/gitea.nix",
"modules/server/navidrome.nix",
"modules/server/syncthing.nix",
"modules/server/uptime-kuma.nix",
"modules/server/immich.nix",
"modules/server/nextcloud.nix",
"modules/server/calibre-web.nix",
"modules/containers/3x-ui.nix",
"modules/containers/tape-rotation.nix"
],
"blocks": [],
"notes": "ConditionPathIsMountPoint=/mnt/services НЕ использовать (bind-mount внутри одной ФС не меняет st_dev). Надёжны requiresMountsFor или ConditionPathIsMountPoint на xlib.dirs.server-home."
},
{
"id": "T5",
"title": "B2: зафиксировать, что бэкапов в конфигурации нет",
"type": "documentation",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"В project-rules.md (R1.x) явно сказано, что бэкапы вне Nix",
"В project-state.md Open Concerns указано, что бэкапы — внешние"
],
"files": [".agent/rules/project-rules.md", ".agent/context/project-state.md"],
"blocks": [],
"notes": "Ждёт ответа 5.6 — где бэкапы и как проверять. Не код, а запись."
},
{
"id": "T6",
"title": "C1: вернуть расследование 3x-ui, потерянное при откате",
"type": "documentation",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"git show 9974784:modules/containers/3x-ui-migration-notes.md > .agent/decisions/notes/3x-ui-xray-26.9.md (или аналогичный путь)",
"Файл дополнен вердиктом: миграция 26.7 → 26.9 провалена, откат осознанный, причина — X25519MLKEM768"
],
"files": [".agent/decisions/notes/3x-ui-xray-26.9.md"],
"blocks": [],
"notes": "200 строк удалены в 22a19be. git show 9974784:... — восстановить."
},
{
"id": "T7",
"title": "C2: зафиксировать фактические версии панели и ядра 3x-ui",
"type": "investigation",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"podman images ... | grep 3x-ui — записано",
"podman inspect ghcr.io/mhsanaei/3x-ui — RepoDigests записано",
"podman exec 3xui_app /app/bin/xray-linux-amd64 version — записано",
"В modules/containers/3x-ui.nix:54 :latest заменён на конкретный тег/digest"
],
"files": ["modules/containers/3x-ui.nix"],
"blocks": ["T8"]
},
{
"id": "T8",
"title": "C3: убрать сервис автообновления 3x-ui",
"type": "fix",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"podman-update-3xui_app удалён из modules/containers/3x-ui.nix",
"Закомментированный таймер (строки 97-103) удалён",
"Оставлен комментарий-предупреждение"
],
"files": ["modules/containers/3x-ui.nix"],
"blocks": [],
"notes": "Обновление панели через pull = путь, по которому в 2026-10-04 декларация разошлась с рантаймом. Автоматизировать нельзя."
},
{
"id": "T9",
"title": "C4: записать в project-rules, что ядро Xray — состояние панели, а не Nix",
"type": "documentation",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"В project-rules.md (R1.x) явно сказано: версия ядра Xray выбирается в UI панели и лежит в её sqlite-БД, то есть вне Nix",
"Перед деплоем/рестартом 3x-ui проверять версию ядра в панели"
],
"files": [".agent/rules/project-rules.md"],
"blocks": []
},
{
"id": "T10",
"title": "C5: решить судьбу reality443Forwarding",
"type": "decision",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"Решение: (а) оставить + описать в инвариантах, или (б) погасить опцию в vds/default.nix + убрать из options.nix, или (в) довести до рабочего состояния",
"Решение зафиксировано в .agent/decisions/"
],
"files": ["modules/vds/default.nix", "modules/options.nix", "modules/containers/3x-ui.nix"],
"blocks": [],
"notes": "Связано с 6.9."
},
{
"id": "T11",
"title": "D1: пробросы роутера — главный недостающий инвариант",
"type": "documentation",
"status": "pending",
"origin": "user:direct",
"depends_on": ["T3"],
"acceptance_criteria": [
"В project-rules.md (R1.3) формулировка: «Экспозиция наружу определяется пробросами на роутере, не openFirewall. На sapphira networking.firewall.enable = false намеренно. Список пробросов: 22, 80, 443, 8443 (3x-ui/Xray REALITY), 22000 (syncthing). Новый сервис не становится доступен из интернета, пока не добавлен проброс.» (уже сделано)",
"В project-state.md Network секция содержит список пробросов"
],
"files": [".agent/rules/project-rules.md", ".agent/context/project-state.md"],
"blocks": []
},
{
"id": "T12",
"title": "D2: зафиксировать 100.64.0.0 как Tailscale-адрес sapphira",
"type": "documentation",
"status": "pending",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"В project-rules.md (R1.4) инвариант сформулирован (уже сделано)",
"Список из 4 мест, которые придётся править при смене: modules/server/nginx.nix, modules/server/nextcloud.nix, modules/vds/systemd.nix, modules/vds/nginx.nix"
],
"files": [".agent/rules/project-rules.md"],
"blocks": []
},
{
"id": "T13",
"title": "D3: убрать мёртвое правило firewall на sapphira",
"type": "fix",
"status": "pending",
"origin": "user:direct",
"depends_on": ["T11"],
"acceptance_criteria": [
"modules/server/nginx.nix:225-228 (allowedTCPPorts = [80 443]) удалено или помечено комментарием «депенит от D1»"
],
"files": ["modules/server/nginx.nix"],
"blocks": []
},
{
"id": "T14",
"title": "E1: написать AGENTS.md в корне (с metaagent-шапкой)",
"type": "documentation",
"status": "completed",
"origin": "user:direct",
"depends_on": [],
"acceptance_criteria": [
"AGENTS.md содержит: yaml frontmatter (для Obsidian dataview), metaagent-указатель, краткую выжимку nixos (хосты, инварианты, ловушки, куда лезть, проверки, где не лезть)"
],
"files": ["AGENTS.md"],
"blocks": [],
"notes": "Сделано в этой инициализации (см. объединённый AGENTS.md)."
},
{
"id": "T15",
"title": "E2: выбрать проверки, которые заменят половину инвариантов",
"type": "decision",
"status": "pending",
"origin": "user:direct",
"depends_on": ["T2"],
"acceptance_criteria": [
"Список из 7 кандидатов (см. analysis-report.md §5) отфильтрован владельцем",
"Выбранные проверки превращены в CI или git pre-commit hook"
],
"files": [],
"blocks": []
},
{
"id": "T16",
"title": "E3: судьба 15 закомментированных модулей в modules/server/default.nix:33-47",
"type": "refactor",
"status": "pending",
"origin": "user:direct",
"depends_on": ["T1"],
"acceptance_criteria": [
"Решение: удалить или оставить как референс",
"Если удалять — то 15 модулей (remnawave, coturn, mealie, memos, minecraft, n8n, netdata, nfs, open-webui, rsync, step-ca, stirling-pdf, transmission, trilium, zerotier) перенесены в archive или удалены"
],
"files": ["modules/server/default.nix"],
"blocks": []
}
],
"backlog_count": 23,
"backlog_note": "Открытые вопросы из roadmap/sources.md (F: 2.2, 2.5, 2.6, 3.2, 4.1, 4.2, 4.3, 4.4, 4.5, 5.1, 5.3, 5.4, 5.5, 5.6, 6.6, 6.7, 6.8, 6.9, 7.4, 8.2, 8.4, 8.5, 8.6) ждут ответа владельца и станут задачами после ответа."
}
-86
View File
@@ -1,86 +0,0 @@
# Task Manifest
**Session:** `metaagent-init-2026-10-09`
**Goal:** Установить metaagent, перенести накопленные данные (AGENTS.md, docs/arch/*) в структуру `.agent/`.
**Date:** 2026-10-09T20:30
**Project type:** `existing`
---
## Task Overview
| ID | Title | Type | Depends On | Status | Origin |
|---|---|---|---|---|---|
| T1 | A1: mobile.nix импортирует несуществующий lib/xlib.nix | fix | — | pending | user:direct |
| T2 | A2: убедиться, что nix flake check вообще запускается | verify | T1 | pending | user:direct |
| T3 | A3: явная финальная политика nftables на VDS | fix | — | pending | user:direct |
| T4 | B1: guard на несмонтированный носитель /mnt/services | security | — | pending | user:direct |
| T5 | B2: зафиксировать, что бэкапов в конфигурации нет | docs | — | pending | user:direct |
| T6 | C1: вернуть расследование 3x-ui, потерянное при откате | docs | — | pending | user:direct |
| T7 | C2: зафиксировать фактические версии панели и ядра 3x-ui | investigate | — | pending | user:direct |
| T8 | C3: убрать сервис автообновления 3x-ui | fix | — | pending | user:direct |
| T9 | C4: записать, что ядро Xray — состояние панели, а не Nix | docs | — | pending | user:direct |
| T10 | C5: решить судьбу reality443Forwarding | decision | — | pending | user:direct |
| T11 | D1: пробросы роутера — главный недостающий инвариант | docs | T3 | pending | user:direct |
| T12 | D2: зафиксировать 100.64.0.0 как Tailscale-адрес sapphira | docs | — | pending | user:direct |
| T13 | D3: убрать мёртвое правило firewall на sapphira | fix | T11 | pending | user:direct |
| T14 | E1: написать AGENTS.md в корне (с metaagent-шапкой) | docs | — | **completed** | user:direct |
| T15 | E2: выбрать проверки, которые заменят половину инвариантов | decision | T2 | pending | user:direct |
| T16 | E3: судьба 15 закомментированных модулей | refactor | T1 | pending | user:direct |
**Total tasks:** 16
**Pending:** 15
**In progress:** 0
**Completed:** 1
**Backlog (ждут ответа):** 23
---
## Задачи по группам
### A. Блокеры (T1–T3)
- **T1 (A1)** — критично: до правки `epral` мёртв. Цена правки: одна строка в `mobile.nix:12`.
- **T2 (A2)** — диагностика: ловит ли `flake check` проблему из T1.
- **T3 (A3)** — опасная зона: править `nftables` без `nft list ruleset` на otreca = риск отрезать SSH.
### B. Защита данных (T4–T5)
- **T4 (B1)** — 12+ сервисов на пустой БД после рестарта без диска. **Самый крупный фикс** (правка `mkStorageGuard` + 12 потребителей).
- **T5 (B2)** — запись в project-rules, не код. Ждёт ответа 5.6.
### C. 3x-ui: заморозить рабочее состояние (T6–T10)
- **T6 (C1)** — восстановить 200 строк из коммита `9974784` + дописать вердикт.
- **T7 (C2)** — сначала диагностика на sapphira и otreca, потом запинить тег.
- **T8 (C3)** — удалить `podman-update-3xui_app` + закомментированный таймер.
- **T9 (C4)** — запись в project-rules (R1.x).
- **T10 (C5)** — связано с вопросом 6.9.
### D. Сетевая граница (T11–T13)
- **T11 (D1)** — запись в project-rules (R1.3) + project-state.md.
- **T12 (D2)** — запись в project-rules (R1.4).
- **T13 (D3)** — мелкая чистка мёртвого правила.
### E. Документация (T14–T16)
- **T14 (E1)** — **выполнено** в этой инициализации.
- **T15 (E2)** — выбор из 7 кандидатов на CI-проверки (см. `analysis-report.md §5`).
- **T16 (E3)** — рефакторинг 15 модулей.
### F. Backlog (ждут ответа)
23 вопроса из `roadmap/sources.md` (F: 2.2, 2.5, 2.6, 3.2, 4.1, 4.2, 4.3, 4.4, 4.5, 5.1, 5.3, 5.4, 5.5, 5.6, 6.6, 6.7, 6.8, 6.9, 7.4, 8.2, 8.4, 8.5, 8.6) — становятся задачами после ответа владельца.
---
## Зависимости
```
T1 → T2 → T15
T1 → T16
T3 → T11 → T13
```
Остальные задачи можно делать параллельно.
-4
View File
@@ -1,5 +1 @@
.vscode .vscode
.omo
__pycache__
scripts
.temp
-145
View File
@@ -1,145 +0,0 @@
---
aliases: []
cssclasses:
date-created: 2026-10-09T19:01
date-modified: 2026-10-09T20:30
tags: [metaagent, nixos, agents]
---
# AGENTS.md
Этот проект использует [MetaAgent](.agent/src/GUIDE.md) v3.0.0 — набор
инструкций для AI-агента.
> NixOS-конфиг домашнего флота. 6 NixOS-хостов + Android (`nix-on-droid`).
> Этот файл — то, что агент должен прочитать **до** первого изменения. Если
> задача выглядит так, что требует сломать что-то из «Подтверждённых
> инвариантов» или «Ловушек» ниже — остановиться и спросить.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Команды (on-demand) | `.agent/src/COMMANDS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| История версий | `.agent/src/CHANGELOG.md` |
| Правила проекта (полные) | `.agent/rules/project-rules.md` |
| Слепок проекта | `.agent/context/project-state.md` |
| Анализ репозитория | `.agent/context/analysis-report.md` |
| Дорожная карта | `.agent/roadmap/sources.md` |
| Манифест задач | `.agent/tasks/manifest.json` |
| ADR (архитектурные решения) | `.agent/decisions/` |
| Пример работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Архитектура (30 секунд)
```
flake.nix
├── configurations/ ← реестр хостов (1 запись = 1 машина)
├── modules/ ← essentials + per-type (desktop/server/vds/wsl/containers/termux)
├── home/ ← home-manager (per device-type)
├── lib/xlib/ ← чистые данные: devices, dirs, helpers
├── deploy/, secrets/ (sops), overlays/, pkgs/
└── .agent/ ← MetaAgent state (rules, decisions, tasks, context, requests, roadmap)
```
Подробная карта: `.agent/context/project-state.md` и `.agent/context/analysis-report.md`.
## Хосты
| Attr / имя | device.type | Роль | Деплой | stateVersion | Примечание |
|---|---|---|---|---|---|
| `default` (nixos) | minimal | Шаблон / минималка | — | — | hostname `"nixos"` |
| `atoridu` | primary | Основной десктоп | — (manual) | 26.05 | xanmod, mini-PC |
| `rydiwo` | secondary | Chuwi MiniBook | deploy-rs | 26.05 | xanmod, NTFS `lamet-drive` |
| `otrecа` | vds | VPS | deploy-rs | 25.05 | Tailscale-only SSH, nftables (см. T3) |
| `sapphira` | server | Домашний сервер | deploy-rs | 25.05 | `firewall.enable = false` намеренно |
| `wsl` | wsl | WSL NixOS | — (manual) | 24.11 | на vetymae (Windows 192.168.1.100) |
| `epral` | termux | Android | — | 24.05 | nix-on-droid, через `mobile.nix` |
## Подтверждённые инварианты
> Полные формулировки (с «Где» и «Почему») — в `.agent/rules/project-rules.md` (R1).
1. **Все `outputs` флейка должны вычисляться.** `configurations/mobile.nix:12` импортировал несуществующий `lib/xlib.nix` — был сломан, `epral` не собирался. → задача T1.
2. **External-диск обязан быть смонтирован** до старта `postgresql`, `n8n`, `samba`, `homebox`, `minecraft`, `3x-ui`, `tape-rotation`. → задача T4.
3. **Сетевая граница sapphira — роутер.** 5 портов: **443, 80, 22000 (syncthing), 8443 (xray), 22 (ssh)**. `firewall.enable = false` намеренно. → задача T11.
4. **`100.64.0.0` = Tailscale-адрес sapphira**, назначен вручную. В 4 файлах. → задача T12.
5. **3x-ui заморожен.** Панель на `:latest`, ядро Xray на 26.7.x. Миграция на 26.9.x провалена. → задачи T6–T10.
6. **nftables на VDS требует явной финальной политики.** Текущий ruleset — без финального правила → неявный accept. → задача T3.
7. **sops-пути — через `config.sops.secrets.<name>.path`.** Любой `path =` override на sops-блоке делает хардкод-потребителя молча сломанным. → ADR-0001.
## Ловушки (выглядит сломанным, намеренно)
| Где | Что выглядит ошибкой | На самом деле |
|---|---|---|
| `server.nix:130` | `firewall.enable = false` при 20 сервисах на `0.0.0.0` | Роутер фильтрует, см. инв. 3 |
| `mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 vs 26.05 | Каждый хост зафиксирован на своей версии |
| `users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` | Костыль под 1000 = удалённый `yuyus`; удалять только после миграции ФС |
| `3x-ui.nix:54` | `image = …:latest` | Панель намеренно latest; ядро Xray — на 26.7.x |
| `3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS | Следствие отката `c8d4a12`; см. задачу T10 |
| `server/default.nix:33-47` | 15 закомментированных модулей | Отключены осознанно, см. задачу T16 |
| `opencode.nix:339` | `systemd.user.services.opencode-web.Service` | `serviceConfig` рендерится в секцию `[serviceConfig]`, systemd молча игнорирует; см. R2 |
| `vds.nix:73-91` | nftables без финального правила | Известный пробел, см. задачу T3 |
| `100.64.0.0` | Первый адрес CGNAT `/10` | Tailscale-адрес sapphira, см. инв. 4 |
| `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. задачу T4 |
## Куда лезть по задаче
| Задача | Файл |
|---|---|
| Добавить хост | `configurations/default.nix` + `configurations/<host>.nix` + `configurations/{hardware,disko}/<host>.nix` |
| Добавить системный сервис | `modules/server/<name>.nix`, добавить в `modules/server/default.nix:imports` |
| Добавить home-пакет | `home/<device_type>.nix` |
| Добавить кросс-модульную опцию | `modules/options.nix` |
| Изменить mount/имя пользователя | `lib/xlib/dirs.nix`, `lib/xlib/device.nix` |
| Изменить домен / сертификат | `modules/server/coredns.nix` + `modules/server/nginx.nix` (или `vds/`) |
| Sops-секрет | `secrets/<name>.<yaml\|json\|env\|ini>`; `users.nix:99` подключает `secrets/default.yaml`; dotenv/json — через `mkUserSecret` |
## Проверки
```bash
# все outputs вычисляются
nix flake check
# правки применились на целевой хост
nix build .#nixosConfigurations.<host>.config.system.build.toplevel
# nixOnDroid
nix build .#nixOnDroidConfigurations.epral.config.system.build.toplevel
# внешний диск смонтирован (до рестарта сервисов на нём)
findmnt /home/oqyude/External
findmnt /mnt/services
# sops
sops --version
```
## Где НЕ лезть без ответа владельца
- `secrets/` (sops-encrypted, расшифровываются `/etc/ssh/id_ed25519` → циклический bootstrap).
- `let deploy` без проверки deploy-rs нод: `rydiwo` (ноутбук, может быть выключен).
- Любая правка, противоречащая «Подтверждённым инвариантам» выше.
- Ядро Xray 26.7.x → 26.9.x — миграция провалена, не повторять без отдельной задачи.
## Команды MetaAgent (on-demand)
- `/adr` — записать архитектурное решение
- `/red-team` — попытаться сломать дизайн
- `/risk-register` — зафиксировать допущения
- `/alt-arch` — описать альтернативу
- `/invariant-tests` — тесты-инварианты для ADR
## Жизненный цикл MetaAgent v3.0.0
```
INIT → ANALYSE → ROADMAP → [DESIGN] → DECOMPOSITION → EXECUTION → METASTATE → HANDOFF
```
Текущее состояние: см. `.agent/checkpoints.json` (`phases.init = completed`,
`phases.analyse = completed`, `phases.roadmap = completed`,
`phases.decomposition = completed`, `phases.execution = in_progress`).
+24 -9
View File
@@ -1,15 +1,30 @@
# Host: "default" (device: minimal) { inputs, ... }@flakeContext:
# let
# The host record lives in configurations/default.nix; this file is only the nixosModule =
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. {
{ config,
inputs, lib,
modulesPath,
pkgs,
xlib,
... ...
}: }:
{ {
imports = [ imports = [
inputs.self.nixosModules.default inputs.self.nixosModules.default
]; ];
system.stateVersion = "26.05"; system = {
stateVersion = "26.05";
};
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "minimal";
};
} }
+8 -71
View File
@@ -1,75 +1,12 @@
{ inputs, ... }@flakeContext: { inputs, ... }@flakeContext:
let
lib = inputs.nixpkgs.lib;
mkSystem = import ../lib/mkSystem.nix flakeContext;
xlibLib = import ../lib/xlib { inherit lib; };
# One record per host. The attribute name IS the hostname, so it is written
# exactly once; `hostname` is only needed where the attribute name is not
# the real hostname (the `default` entry).
#
# device device type, must be a key of `devices` in lib/xlib/device.nix
# modules module body for this host
hosts = {
default = {
hostname = "nixos";
device = "minimal";
modules = [ ./any.nix ];
};
atoridu = {
device = "primary";
modules = [ ./mini-pc.nix ];
};
rydiwo = {
device = "secondary";
modules = [ ./mini-laptop.nix ];
};
otreca = {
device = "vds";
modules = [ ./vds.nix ];
};
sapphira = {
device = "server";
modules = [ ./server.nix ];
};
wsl = {
device = "wsl";
modules = [ ./wsl.nix ];
};
};
mkHost =
name:
{
device,
modules,
hostname ? name,
...
}:
let
xlib = xlibLib.mkXlib {
inherit hostname;
type = device;
};
in
{
inherit xlib;
system = mkSystem { inherit xlib modules; };
};
in
{ {
nixosConfigurations = lib.mapAttrs' ( nixosConfigurations = {
name: spec: lib.nameValuePair name (mkHost name spec).system default = import ./any.nix flakeContext; # default
) hosts; atoridu = import ./mini-pc.nix flakeContext; # atoridu
rydiwo = import ./mini-laptop.nix flakeContext; # rydiwo
# Per-host xlib values, for code that lives outside the module system otreca = import ./vds.nix flakeContext; # vds
# (deploy, overlays, pkgs). otreca-new = import ./vds-new.nix flakeContext; # vds-new
xlib = lib.mapAttrs' (name: spec: lib.nameValuePair name (mkHost name spec).xlib) hosts; sapphira = import ./server.nix flakeContext; # sapphira
wsl = import ./wsl.nix flakeContext; # wsl
nixOnDroidConfigurations = {
epral = import ./mobile.nix flakeContext; # epral (Android device via nix-on-droid)
# Alias so a plain `nix-on-droid switch` from a local clone
# (~/.config/nix-on-droid) picks up the device config without `#epral`.
default = import ./mobile.nix flakeContext;
}; };
} }
-1
View File
@@ -28,7 +28,6 @@
kernel = { kernel = {
sysctl = { sysctl = {
"fs.inotify.max_user_watches" = "204800"; "fs.inotify.max_user_watches" = "204800";
"net.ipv4.ip_forward" = 1;
}; };
}; };
kernelModules = [ kernelModules = [
+23
View File
@@ -0,0 +1,23 @@
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
fileSystems = {
"/" = {
device = lib.mkForce "/dev/disk/by-partlabel/disk-main-root"; # "/dev/disk/by-partlabel/disk-main-root";
fsType = "ext4";
};
};
# swapDevices = [
# { device = "/dev/disk/by-partlabel/disk-main-swap"; }
# ];
networking.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
+118 -12
View File
@@ -1,15 +1,22 @@
# Host: "rydiwo" (device: secondary)
#
# The host record lives in configurations/default.nix; this file is only the
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
{ {
inputs,
...
}@flakeContext:
let
nixosModule =
{
config,
lib, lib,
pkgs, pkgs,
xlib, xlib,
inputs,
... ...
}: }:
{ {
xlib.device = {
type = "secondary";
hostname = "rydiwo";
};
imports = with inputs; [ imports = with inputs; [
nixos-hardware.nixosModules.chuwi-minibook-x nixos-hardware.nixosModules.chuwi-minibook-x
./hardware/mini-laptop.nix ./hardware/mini-laptop.nix
@@ -24,14 +31,113 @@
}; };
}; };
fileSystems = xlib.helpers.mkNtfsMount { fileSystems."${xlib.dirs.lamet-drive}" = {
path = xlib.dirs.lamet-drive; device = "/dev/disk/by-uuid/DC76BD3576BD116E";
uuid = "DC76BD3576BD116E"; fsType = "ntfs3";
mask = "0000"; options = [
"defaults"
"uid=1000"
"gid=1000"
"fmask=0000"
"dmask=0000"
"nofail"
];
};
hardware = {
bluetooth.enable = true;
};
networking = {
hostName = "${xlib.device.hostname}";
networkmanager.enable = true;
firewall.enable = false;
};
i18n = {
extraLocaleSettings = {
LC_ADDRESS = "ru_RU.UTF-8";
LC_IDENTIFICATION = "ru_RU.UTF-8";
LC_MEASUREMENT = "ru_RU.UTF-8";
LC_MONETARY = "ru_RU.UTF-8";
LC_NAME = "ru_RU.UTF-8";
LC_NUMERIC = "ru_RU.UTF-8";
LC_PAPER = "ru_RU.UTF-8";
LC_TELEPHONE = "ru_RU.UTF-8";
LC_TIME = "ru_RU.UTF-8";
};
};
services = {
# xserver = {
# videoDrivers = [
# "nomodeset"
# ];
# };
syncthing = {
enable = true;
systemService = true;
configDir = "${xlib.dirs.user-storage}/Syncthing/${config.system.name}";
dataDir = "${xlib.dirs.user-home}";
group = "users";
user = "${xlib.device.username}";
};
# pipewire = {
# enable = lib.mkDefault true;
# systemWide = true;
# alsa.enable = false;
# alsa.support32Bit = true;
# pulse.enable = true;
# jack.enable = true;
# extraConfig.pipewire = {
# "99-default.conf" = {
# "context.properties" = {
# "default.clock.rate" = 96000;
# "default.clock.allowed-rates" = [
# 44100
# 48000
# 96000
# ];
# "default.clock.quantum" = 1024;
# "default.clock.min-quantum" = 256;
# "default.clock.max-quantum" = 2048;
# };
# };
# };
# };
thermald.enable = true;
earlyoom.enable = true;
openssh = {
enable = true;
allowSFTP = true;
hostKeys = [
{
path = "/etc/ssh/id_ed25519";
type = "ed25519";
}
];
settings = {
PasswordAuthentication = false;
PermitRootLogin = "yes";
UsePAM = true;
};
};
};
security = {
rtkit.enable = true;
}; };
host.ssh.enable = true;
hardware.intel-gpu-tools.enable = true; hardware.intel-gpu-tools.enable = true;
system.stateVersion = "26.05"; system.stateVersion = "26.05";
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = with inputs; [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "secondary";
};
} }
+112 -33
View File
@@ -1,15 +1,22 @@
# Host: "atoridu" (device: primary)
#
# The host record lives in configurations/default.nix; this file is only the
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
{ {
inputs,
...
}@flakeContext:
let
nixosModule =
{
config,
lib, lib,
pkgs, pkgs,
xlib, xlib,
inputs,
... ...
}: }:
{ {
xlib.device = {
type = "primary";
hostname = "atoridu";
};
imports = with inputs; [ imports = with inputs; [
./hardware/mini-pc.nix ./hardware/mini-pc.nix
./disko/mini-pc.nix ./disko/mini-pc.nix
@@ -17,45 +24,100 @@
self.nixosModules.default self.nixosModules.default
]; ];
# mkNtfsMount returns a `{ "<path>" = { ... }; }` attrset (the shape fileSystems = {
# fileSystems itself wants), so several mounts are combined with "${xlib.dirs.therima-drive}" = {
# mergeAttrsList — not listToAttrs, which would demand `name`/`value`. enable = false;
# device = "/dev/disk/by-uuid/C0A2DDEFA2DDEA44";
# These three ntfs3 drives are intentionally left unmounted. The entries fsType = "ntfs3";
# are kept commented out rather than deleted, so restoring a drive is a options = [
# matter of uncommenting its block. `enable = false` would declare a drive "defaults"
# without mounting it; dropping the field mounts it. "uid=1000"
fileSystems = lib.mergeAttrsList ( "gid=1000"
map (xlib.helpers.mkNtfsMount) [ "fmask=0007"
# { "dmask=0007"
# path = xlib.dirs.therima-drive; "nofail"
# uuid = "C0A2DDEFA2DDEA44"; ];
# } };
# { "${xlib.dirs.vetymae-drive}" = {
# path = xlib.dirs.vetymae-drive; enable = false;
# uuid = "6408433908430A0E"; device = "/dev/disk/by-uuid/6408433908430A0E";
# } fsType = "ntfs3";
# { options = [
# path = xlib.dirs.soptur-drive; "defaults"
# uuid = "C00C56E40C56D54E"; "uid=1000"
# } "gid=1000"
] "fmask=0007"
); "dmask=0007"
"nofail"
];
};
"${xlib.dirs.soptur-drive}" = {
enable = false;
device = "/dev/disk/by-uuid/C00C56E40C56D54E";
fsType = "ntfs3";
options = [
"defaults"
"uid=1000"
"gid=1000"
"fmask=0007"
"dmask=0007"
"nofail"
];
};
};
boot = { boot = {
kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable; kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable;
#kernelParams = [ "usbcore.autosuspend=-1" ];
loader = { loader = {
systemd-boot.enable = lib.mkDefault true; systemd-boot.enable = lib.mkDefault true;
efi.canTouchEfiVariables = lib.mkDefault true; efi.canTouchEfiVariables = lib.mkDefault true;
}; };
}; };
services.xserver = { hardware = {
bluetooth.enable = true;
};
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
networking = {
hostName = "${xlib.device.hostname}";
networkmanager.enable = true;
firewall.enable = false;
};
i18n = {
extraLocaleSettings = {
LC_ADDRESS = "ru_RU.UTF-8";
LC_IDENTIFICATION = "ru_RU.UTF-8";
LC_MEASUREMENT = "ru_RU.UTF-8";
LC_MONETARY = "ru_RU.UTF-8";
LC_NAME = "ru_RU.UTF-8";
LC_NUMERIC = "ru_RU.UTF-8";
LC_PAPER = "ru_RU.UTF-8";
LC_TELEPHONE = "ru_RU.UTF-8";
LC_TIME = "ru_RU.UTF-8";
};
};
services = {
#logrotate.checkConfig = false;
#power-profiles-daemon.enable = false;
xserver = {
videoDrivers = [ videoDrivers = [
"amdgpu" "amdgpu"
]; ];
}; };
services.pipewire = { syncthing = {
enable = true;
systemService = true;
configDir = "${xlib.dirs.user-storage}/Syncthing/${config.system.name}";
dataDir = "${xlib.dirs.user-home}";
group = "users";
user = "${xlib.device.username}";
};
pipewire = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
systemWide = true; systemWide = true;
alsa.enable = false; alsa.enable = false;
@@ -78,7 +140,24 @@
}; };
}; };
}; };
thermald.enable = true;
earlyoom.enable = true;
};
nixpkgs.config.pulseaudio = true; nixpkgs.config.pulseaudio = true;
security = {
rtkit.enable = true;
};
system.stateVersion = "26.05"; system.stateVersion = "26.05";
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "primary";
};
} }
-128
View File
@@ -1,128 +0,0 @@
{
inputs,
...
}@flakeContext:
let
# Host: epral (Android device via nix-on-droid, aarch64-linux)
# Integrates with the base defaultModule (imports.self.nixosModules.default),
# which is trimmed for the "termux" device type: NixOS-only modules
# (essentials, users.nix, home-manager, sops-nix, disko, grub2-themes)
# and nixpkgs.overlays are skipped so it evaluates under nix-on-droid's
# module system (class = "nixOnDroid").
xlib = import ../lib/xlib.nix { lib = inputs.nixpkgs.lib; };
nixOnDroidModule =
{
lib,
pkgs,
xlib,
...
}:
{
imports = [
inputs.self.nixosModules.strict
];
# Login shell. nix-on-droid writes /etc/passwd from user.shell on every
# activation, so `chsh` is useless here — set it in nix instead.
# (default is bashInteractive)
user.shell = "${pkgs.zsh}/bin/zsh";
# SSH user (matches the `User` entries in the client's ~/.ssh/config).
# Default is "nix-on-droid"; home stays at the read-only
# /data/data/com.termux.nix/files/home either way.
user.userName = xlib.device.username;
# Minimal termux settings (nix-on-droid options only:
# environment.*, nix.*, time.*, user.*, system.*, android-integration.*)
# user.userName defaults to "nix-on-droid"; set it to override.
# user.home is read-only: /data/data/com.termux.nix/files/home
# Simply install just the packages
environment.packages = with pkgs; [
# User-facing stuff that you really really want to have
vim # or some other editor, e.g. nano or neovim
nano
# Some common stuff that people expect to have
bzip2
diffutils
findutils
git
gnugrep
gnupg
gnused
gnutar
gzip
hostname
man
ncurses
openssh
procps
psmisc # provides killall (attr `killall` was removed from nixpkgs)
treefmt
tzdata
unzip
util-linux # renamed from utillinux
zip
];
# Backup etc files instead of failing to activate generation if a file already exists in /etc
environment.etcBackupExtension = ".bak";
# Shared userspace home-manager config (same cozy shell as on NixOS hosts).
# nix-on-droid forces home.username / home.homeDirectory from user.*,
# so the strict module must not set them.
# xlib is injected via home-manager.extraSpecialArgs (the HM submodule
# does not inherit the nix-on-droid module args).
home-manager = {
useGlobalPkgs = true;
backupFileExtension = "hm-bak";
extraSpecialArgs = {
inherit xlib;
};
config =
{ ... }:
{
imports = [
../home/termux.nix
];
home.stateVersion = "24.05";
};
};
# Read the changelog before changing this value
system.stateVersion = "24.05";
# Set up nix for flakes
nix.extraOptions = ''
experimental-features = nix-command flakes
'';
# Set your time zone
time.timeZone = "Europe/Moscow";
android-integration.termux-setup-storage.enable = true;
# Provides `am` (termux-am) — required by termux-api's broadcast backend.
android-integration.am.enable = true;
};
in
inputs.nix-on-droid.lib.nixOnDroidConfiguration {
pkgs = import inputs.nixpkgs {
system = "aarch64-linux";
};
modules = [
nixOnDroidModule
];
extraSpecialArgs = {
# `xlib` is the same value shape NixOS hosts get (lib/mkSystem.nix);
# the hostname lives here because nixOnDroidConfigurations is keyed by
# both "epral" and the "default" alias, so it cannot come from the
# attribute name.
xlib = xlib.mkXlib {
hostname = "epral";
type = "termux";
};
};
}
+79 -87
View File
@@ -1,15 +1,19 @@
# Host: "sapphira" (device: server) { inputs, ... }@flakeContext:
# let
# The host record lives in configurations/default.nix; this file is only the nixosModule =
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. {
{ config,
lib, lib,
pkgs, pkgs,
xlib, xlib,
inputs,
... ...
}: }:
{ {
xlib.device = {
type = "server";
hostname = "sapphira";
};
imports = [ imports = [
./hardware/server.nix ./hardware/server.nix
inputs.self.nixosModules.default inputs.self.nixosModules.default
@@ -37,103 +41,91 @@
intel-gpu-tools.enable = true; intel-gpu-tools.enable = true;
}; };
fileSystems = fileSystems = {
(xlib.helpers.mkExfatMount {
path = xlib.dirs.archive-drive;
label = "archive";
})
// (xlib.helpers.mkExfatMount {
path = xlib.dirs.mobile-drive;
uuid = "7EB1-DC99";
})
// (xlib.helpers.mkBindMount {
what = xlib.dirs.services-folder;
where = xlib.dirs.services-mnt-folder;
})
// {
# External drive # External drive
"${xlib.dirs.server-home}" = { "${xlib.dirs.server-home}" = {
device = "/dev/disk/by-uuid/37e53ebc-5343-a94d-9fe2-0ca39e13a8de"; device = "/dev/disk/by-uuid/37e53ebc-5343-a94d-9fe2-0ca39e13a8de";
fsType = "ext4"; fsType = "ext4";
}; };
# Archive drive
"${xlib.dirs.archive-drive}" = {
device = "/dev/disk/by-label/archive";
fsType = "exfat";
options = [
"nofail"
"uid=1000"
"gid=1000"
];
};
# Mobile SD-Card
"${xlib.dirs.mobile-drive}" = {
device = "/dev/disk/by-uuid/7EB1-DC99";
fsType = "exfat";
options = [
"nofail"
"uid=1000"
"gid=1000"
];
};
# Services in /mnt folder
"${xlib.dirs.services-mnt-folder}" = {
device = "${xlib.dirs.services-folder}";
fsType = "none";
options = [
"bind"
"nofail"
];
};
}; };
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"z ${xlib.dirs.services-mnt-folder} 0777 root root -" "z ${xlib.dirs.services-mnt-folder} 0777 root root -"
]; ];
host.ssh.enable = true; services = {
power-profiles-daemon.enable = lib.mkForce false;
# Offload Nix builds to the WSL2 NixOS instance running on vetymae earlyoom.enable = true;
# (Windows 11 host). Sapphira only has 2 logical cores; the WSL exposes auto-cpufreq.enable = false;
# 24 cores + 14 GiB. The matchBlock with ProxyCommand is generated by throttled.enable = true;
# modules/server/builder.nix, the other side of the same option lives in journald = {
# modules/wsl/builder.nix. extraConfig = ''
# SystemMaxUse=512M
# `proxyCommand` is what marks this builder as needing the SSH matchBlock '';
# (see modules/server/builder.nix). A builder reachable directly would };
# omit it. openssh = {
# enable = true;
# ---- DISABLED 2026-10-04 ---- allowSFTP = true;
# Remote building temporarily turned off coordinator-side. `host.builder.clients` hostKeys = [
# falls back to its default `[]` (declared in modules/options.nix), so {
# modules/server/builder.nix's `lib.mkIf (clients != [])` never fires and no path = "/etc/ssh/id_ed25519";
# buildMachines / SSH blocks / distributedBuilds override get generated. type = "ed25519";
# All builds run locally on sapphira's 2 cores. Re-enable by removing the }
# Nix comments on the block below (and on `host.builder.enable = true;` ];
# in configurations/wsl.nix). settings = {
# PasswordAuthentication = false;
# host.builder.clients = [ PermitRootLogin = "yes";
# { UsePAM = true;
# hostName = "vetymae-nix"; };
# sshUser = "oqyude"; };
# sshKey = "/root/.ssh/id_ed25519"; };
# # NixOS calls this `systems` (plural), not `systemTypes`. The
# # default is empty — every derivation is rejected. The WSL NixOS
# # runs on x86_64-linux, matching sapphira.
# systems = [ "x86_64-linux" ];
# # vetymae-nix drops kvm + nixos-test from its advertised
# # system-features (see modules/wsl/builder.nix). Listing them here
# # would not break anything (Nix intersects), but listing the
# # features the WSL actually has is the documented contract.
# supportedFeatures = [
# "benchmark"
# "big-parallel"
# ];
# mandatoryFeatures = [ ];
# maxJobs = 24;
# speedFactor = 0.5;
# # Keep the SSH session alive across many small builds in one daemon
# # session — compile-heavy workloads spam the daemon with hundreds of
# # derivations and ControlMaster collapses those into one Windows hop.
# # NB: `nix.buildMachines` has no `sshOptions` attribute, so the
# # ControlMaster directive lives in the SSH matchBlock instead (see
# # modules/server/builder.nix).
# #
# # The OpenSSH alias for this host (matches the user's
# # ~/.ssh/config so known_hosts entries do not collide with the
# # Windows OpenSSH entry on 127.0.0.1/vetymae) is consumed only by
# # the SSH matchBlock below — not by `nix.buildMachines`, which has
# # no such attribute.
# hostKeyAlias = "wsl-nixos-on-vetymae";
# # Use the Windows host's IP directly so the nix-daemon (running as
# # root, without the user's ~/.ssh/config) does not need a separate
# # `vetymae` host alias. With StrictHostKeyChecking=accept-new the
# # first connection adds the Windows host key to /root/.ssh/known_hosts.
# proxyCommand = "ssh oqyude@192.168.1.100 'wsl -d NixOS -- nc 127.0.0.1 22'";
# }
# ];
networking = { networking = {
hostName = "${xlib.device.hostname}";
networkmanager.enable = true; networkmanager.enable = true;
firewall.enable = false; firewall.enable = false;
# nameservers = [
# "192.168.1.1"
# "127.0.0.1"
# ];
}; };
system = { system = {
stateVersion = "25.05"; stateVersion = "25.05";
}; };
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "server";
};
} }
+130
View File
@@ -0,0 +1,130 @@
{ inputs, ... }@flakeContext:
let
nixosModule =
{
config,
lib,
modulesPath,
pkgs,
xlib,
...
}:
{
xlib.device = {
type = "vds-new";
hostname = "otreca-new";
};
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/profiles/qemu-guest.nix")
./disko/vds.nix
./hardware/vds.nix
inputs.self.nixosModules.default
];
boot = {
kernelPackages = pkgs.linuxPackages_xanmod_stable;
hardwareScan = true;
loader = {
grub = {
enable = true;
device = "nodev";
useOSProber = false;
efiSupport = false;
};
systemd-boot.enable = lib.mkDefault false;
};
};
services = {
earlyoom.enable = true;
journald = {
extraConfig = ''
SystemMaxUse=512M
'';
};
openssh = {
enable = true;
allowSFTP = true;
openFirewall = true;
hostKeys = [
{
path = "/etc/ssh/id_ed25519";
type = "ed25519";
}
];
settings = {
PasswordAuthentication = false;
PermitRootLogin = "yes";
UsePAM = true;
};
};
tailscale = {
enable = true;
openFirewall = true;
};
};
networking = {
nameservers = [
"1.1.1.1"
"8.8.8.8"
"2001:4860:4860::8844"
"2001:4860:4860::8888"
"2606:4700:4700::1111"
"2606:4700:4700::1001"
];
hostName = "${xlib.device.hostname}";
networkmanager.enable = true;
tempAddresses = "disabled";
dhcpcd = {
enable = true;
IPv6rs = true;
};
firewall = {
enable = true;
allowPing = true;
};
enableIPv6 = true;
interfaces.ens3 = {
useDHCP = true;
# ipv4.addresses = [
# {
# address = "31.57.158.109";
# prefixLength = 24;
# }
# ];
# ipv6.addresses = [
# {
# address = "2a13:7c00:6:102:f816:3eff:fe91:6b9e";
# prefixLength = 64;
# }
# ];
};
# defaultGateway = {
# address = "31.57.158.1";
# interface = "ens3";
# };
# defaultGateway6 = {
# address = "2a13:7c00:6:102::1";
# interface = "ens3";
# };
};
system = {
stateVersion = "25.05";
};
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "vds-new";
};
}
+52 -17
View File
@@ -1,16 +1,20 @@
# Host: "otreca" (device: vds) { inputs, ... }@flakeContext:
# let
# The host record lives in configurations/default.nix; this file is only the nixosModule =
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. {
{ config,
lib, lib,
modulesPath, modulesPath,
pkgs, pkgs,
xlib, xlib,
inputs,
... ...
}: }:
{ {
xlib.device = {
type = "vds";
hostname = "otreca";
};
imports = [ imports = [
(modulesPath + "/installer/scan/not-detected.nix") (modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/profiles/qemu-guest.nix") (modulesPath + "/profiles/qemu-guest.nix")
@@ -41,23 +45,44 @@
}; };
}; };
host.ssh.enable = true; services = {
# SSH is reachable only over Tailscale (not on the public internet). earlyoom.enable = true;
# This otreca VDS is reached by deploy-rs and by oqyude over the journald = {
# tailnet, so exposing 22 to ens3 is pure attack surface. extraConfig = ''
services.openssh.openFirewall = false; SystemMaxUse=512M
'';
services.tailscale = { };
openssh = {
enable = true;
allowSFTP = true;
openFirewall = true;
hostKeys = [
{
path = "/etc/ssh/id_ed25519";
type = "ed25519";
}
];
settings = {
PasswordAuthentication = false;
PermitRootLogin = "yes";
UsePAM = true;
};
};
tailscale = {
enable = true; enable = true;
openFirewall = true; openFirewall = true;
}; };
# Open port 22 only on the tailscale interface. };
networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 22 ];
networking = { networking = {
nameservers = [ nameservers = [
"1.1.1.1" "1.1.1.1"
"8.8.8.8" "8.8.8.8"
# "2001:4860:4860::8844"
# "2001:4860:4860::8888"
# "2606:4700:4700::1111"
# "2606:4700:4700::1001"
]; ];
hostName = "${xlib.device.hostname}";
networkmanager.enable = true; networkmanager.enable = true;
tempAddresses = "disabled"; tempAddresses = "disabled";
dhcpcd = { dhcpcd = {
@@ -119,4 +144,14 @@
system = { system = {
stateVersion = "25.05"; stateVersion = "25.05";
}; };
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "vds";
};
} }
+74 -31
View File
@@ -1,60 +1,103 @@
# Host: "wsl" (device: wsl) { inputs, ... }@flakeContext:
# let
# The host record lives in configurations/default.nix; this file is only the nixosModule =
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. {
{ config,
lib, lib,
modulesPath,
pkgs, pkgs,
modulesPath,
xlib, xlib,
inputs,
... ...
}: }:
{ {
xlib.device = {
type = "wsl";
hostname = "wsl";
};
imports = [ imports = [
inputs.nixos-wsl.nixosModules.default inputs.nixos-wsl.nixosModules.default
inputs.self.nixosModules.default inputs.self.nixosModules.default
]; ];
#zramSwap.enable = true;
services = {
journald = {
extraConfig = ''
SystemMaxUse=512M
'';
};
earlyoom.enable = true;
};
hardware = { hardware = {
graphics.enable = true; graphics.enable = true;
# amdgpu.opencl.enable = true;
# amdgpu.amdvlk.enable = true;
}; };
networking = { networking = {
# nameservers = [
# "1.1.1.1"
# "8.8.8.8"
# "2001:4860:4860::8844"
# "2001:4860:4860::8888"
# "2606:4700:4700::1111"
# "2606:4700:4700::1001"
# ];
hostName = "${xlib.device.hostname}";
# networkmanager.enable = true;
# tempAddresses = "disabled";
# dhcpcd = {
# enable = true;
# IPv6rs = true;
# };
firewall = { firewall = {
enable = false; enable = false;
allowPing = true; allowPing = true;
}; };
enableIPv6 = true; enableIPv6 = true;
# interfaces.ens3 = {
# useDHCP = true;
# # ipv4.addresses = [
# # {
# # address = "31.57.158.109";
# # prefixLength = 24;
# # }
# # ];
# ipv6.addresses = [
# {
# address = "2a13:7c00:10:6:f816:3eff:fe36:fe1b";
# prefixLength = 64;
# }
# ];
# };
# # defaultGateway = {
# # address = "31.57.158.1";
# # interface = "ens3";
# # };
# defaultGateway6 = {
# address = "2a13:7c00:10:6::1";
# interface = "ens3";
# };
}; };
wsl = { wsl = {
enable = true; enable = true;
startMenuLaunchers = true; startMenuLaunchers = true;
useWindowsDriver = true; useWindowsDriver = true;
defaultUser = xlib.device.username; defaultUser = config.xlib.device.username;
}; };
# Enable SSH server on WSL NixOS so sapphira can drive it directly via a
# ProxyCommand chain through the Windows OpenSSH layer. The shared
# essentials/ssh.nix module wires host keys, sops-managed user keys, and
# passwordless key auth — nothing to repeat here.
host.ssh.enable = true;
# Advertise this WSL instance as a remote Nix builder for sapphira (2
# cores, the bottleneck host). All builder wiring — fixing the
# `system-features` to drop the unsupported `kvm`, and adding the SSH
# user `oqyude` to trusted-users — lives in modules/wsl/builder.nix.
#
# ---- DISABLED 2026-10-04 ----
# Remote building temporarily turned off builder-side. The default of
# `host.builder.enable` is `false` (modules/options.nix), so
# modules/wsl/builder.nix's `lib.mkIf enable` block is skipped: WSL
# keeps its default system-features and trusted-users, and no SSH-side
# state changes. Re-enable by uncommenting the assignment below and
# removing the DISABLED banner in configurations/server.nix.
#
# host.builder.enable = true;
system.stateVersion = "24.11"; system.stateVersion = "24.11";
};
in
inputs.nixpkgs.lib.nixosSystem {
modules = [
nixosModule
];
system = "x86_64-linux";
specialArgs = {
deviceType = "wsl";
};
} }
+3 -3
View File
@@ -6,11 +6,10 @@ let
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos inputs.self.nixosConfigurations.${hostname}; path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos inputs.self.nixosConfigurations.${hostname};
}; };
}; };
# Login user for every deploy target. Read from the hoisted xlib instead of user = "${inputs.self.nixosConfigurations.default.config.xlib.device.username}";
# digging through a built NixOS configuration.
user = "${inputs.self.xlib.default.device.username}";
server = "sapphira"; server = "sapphira";
vds = "otreca"; vds = "otreca";
vds-new = "otreca-new";
mini-laptop = "rydiwo"; mini-laptop = "rydiwo";
in in
{ {
@@ -20,6 +19,7 @@ in
nodes = { nodes = {
"${server}" = mkDeploy "${server}"; "${server}" = mkDeploy "${server}";
"${vds}" = mkDeploy "${vds}"; "${vds}" = mkDeploy "${vds}";
"${vds-new}" = mkDeploy "${vds-new}";
"${mini-laptop}" = mkDeploy "${mini-laptop}"; "${mini-laptop}" = mkDeploy "${mini-laptop}";
}; };
}; };
Generated
+146 -359
View File
@@ -13,11 +13,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789404474, "lastModified": 1781023725,
"narHash": "sha256-UXFQ7tFiwn8sPz0EV4CBB2PCf/ZiGIHWn/6MXk81Lxs=", "narHash": "sha256-Gt+qFANcrDRjl3xzidLYrAUQCd3808iuAsLwZbYYAEU=",
"owner": "serokell", "owner": "serokell",
"repo": "deploy-rs", "repo": "deploy-rs",
"rev": "e760371d631165e7d8de5b0dcf148e21ec4c16f0", "rev": "2ce9051767ee4d1a3c43b52ba327431783bfd463",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -33,11 +33,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789770686, "lastModified": 1781152676,
"narHash": "sha256-uZkBR7yHdIKUFB5SZdfgh1qkGfI3XmYmI/lTiquxbck=", "narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "725ea35e410ad83be4931d1bff7e090eacaf3563", "rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -61,24 +61,6 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1788450739,
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"grub2-themes": { "grub2-themes": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -86,11 +68,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1788271742, "lastModified": 1757136219,
"narHash": "sha256-H4IIqM+fmq9t3ZPHGs9kiuoQzau9AhCGQBSfClqQ/44=", "narHash": "sha256-tKU+vq34KHu/A2wD7WdgP5A4/RCmSD8hB0TyQAUlixA=",
"owner": "vinceliuice", "owner": "vinceliuice",
"repo": "grub2-themes", "repo": "grub2-themes",
"rev": "4c5a77125b93f833edc9bf7b14a899faa8ac79c6", "rev": "80dd04ddf3ba7b284a7b1a5df2b1e95ee2aad606",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -106,11 +88,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1790128814, "lastModified": 1781189114,
"narHash": "sha256-6Gm9q+wW3E4Ey4F6wEbJAwaMsEK6hvCYfTW7yY64ZfA=", "narHash": "sha256-5inaamLgUMWy+MOBE9ChF9QAF1o/74LFuHkI0W/9rqc=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "0b2f1129177f70c5f0f5d88bb53c49ca47d0bfc0", "rev": "486595d2cf49cfcd649b58a284fa11ac0e34da22",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -119,152 +101,16 @@
"type": "github" "type": "github"
} }
}, },
"justray": {
"inputs": {
"flake-parts": [
"flake-parts"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1790165840,
"narHash": "sha256-nQ3uCXiUGTLD/UtuChc2XlStYg9a33PYrkDitmmqxW8=",
"owner": "luynrs",
"repo": "justray",
"rev": "4073f3fb223613e4d780dafad6f93b5c266061a2",
"type": "github"
},
"original": {
"owner": "luynrs",
"repo": "justray",
"type": "github"
}
},
"nfqws2-keenetic": {
"flake": false,
"locked": {
"lastModified": 1789144514,
"narHash": "sha256-G+LvvXDqzYm8SOXNc3N+HIzvD9DR3J+WT+HHDdmjB0Y=",
"owner": "nfqws",
"repo": "nfqws2-keenetic",
"rev": "fa22c177b340e73d8b8a94b295af20e0228c4c22",
"type": "github"
},
"original": {
"owner": "nfqws",
"repo": "nfqws2-keenetic",
"type": "github"
}
},
"nix-formatter-pack": {
"inputs": {
"nixpkgs": [
"nix-on-droid",
"nixpkgs"
],
"nmd": [
"nix-on-droid",
"nmd"
],
"nmt": "nmt"
},
"locked": {
"lastModified": 1705252799,
"narHash": "sha256-HgSTREh7VoXjGgNDwKQUYcYo13rPkltW7IitHrTPA5c=",
"owner": "Gerschtli",
"repo": "nix-formatter-pack",
"rev": "2de39dedd79aab14c01b9e2934842051a160ffa5",
"type": "github"
},
"original": {
"owner": "Gerschtli",
"repo": "nix-formatter-pack",
"type": "github"
}
},
"nix-minecraft": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"nixpkgs": [
"nixpkgs"
],
"systems": [
"nix-systems"
]
},
"locked": {
"lastModified": 1790137578,
"narHash": "sha256-luzO2Bo/RxUHqTPxBttSB1QVzM9Y5s+Iwpip6SjWdWo=",
"owner": "Infinidoge",
"repo": "nix-minecraft",
"rev": "2e6a1d1ceb4da6b6ffb3980bc7993b3d057cd4db",
"type": "github"
},
"original": {
"owner": "Infinidoge",
"repo": "nix-minecraft",
"type": "github"
}
},
"nix-on-droid": {
"inputs": {
"home-manager": [
"home-manager"
],
"nix-formatter-pack": "nix-formatter-pack",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-docs": "nixpkgs-docs",
"nixpkgs-for-bootstrap": "nixpkgs-for-bootstrap",
"nmd": "nmd"
},
"locked": {
"lastModified": 1772387862,
"narHash": "sha256-o7q9flWMCsFW2mkz8TQhvPUcwFczO7VX9I6U2u2vN4o=",
"owner": "nix-community",
"repo": "nix-on-droid",
"rev": "67b105336cb06b764366bfe241afa2352de6a926",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "testing",
"repo": "nix-on-droid",
"type": "github"
}
},
"nix-systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"nixos-hardware": { "nixos-hardware": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": "nixpkgs"
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1789978172, "lastModified": 1781168557,
"narHash": "sha256-FIRXajv1pPZ+l6On6ekkmcQ6le0Zi0ncRUoR3nB0vKA=", "narHash": "sha256-LOnLQ2tpYF9gqIDDr3+j3DbpJJr/QCH6zPRT2GzEUOE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "9ebcb7766700d7e006d9505247bd7ce0426f4232", "rev": "6358ff76821101c178e3ab4919a62799bfe3652e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -284,11 +130,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789164534, "lastModified": 1781182279,
"narHash": "sha256-DoYGPM6QpnYBLWj9gGw6ZwAzIX+HrAVov1BoT+8Jixo=", "narHash": "sha256-V5EQQbDnmdiXGQXrEF1PEL7QYsFqfH8N1E89Z5ONwFk=",
"owner": "nix-community", "owner": "nix-community",
"repo": "NixOS-WSL", "repo": "NixOS-WSL",
"rev": "72c92b11bb8289e6651c7fef29cc0a885fd6a255", "rev": "5675822ba756e6e56f8f6a5a76e90e0da2ece94d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -300,11 +146,88 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1790046670, "lastModified": 1767892417,
"narHash": "sha256-MYiI+CzL0tuWgRPjGsKCDHqYs2T3OzMlMQWOYWG0qso=", "narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"nixpkgs-beets": {
"locked": {
"lastModified": 1774610258,
"narHash": "sha256-HaThtroVD9wRdx7KQk0B75JmFcXlMUoEdDFNOMOlsOs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc", "rev": "832efc09b4caf6b4569fbf9dc01bec3082a00611",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "832efc09b4caf6b4569fbf9dc01bec3082a00611",
"type": "github"
}
},
"nixpkgs-calibre": {
"locked": {
"lastModified": 1776255774,
"narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "566acc07c54dc807f91625bb286cb9b321b5f42a",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "566acc07c54dc807f91625bb286cb9b321b5f42a",
"type": "github"
}
},
"nixpkgs-master": {
"locked": {
"lastModified": 1781298072,
"narHash": "sha256-p7sszdPeM3Gm7LA+NrWlxn5Rp6Qp+TGbt2qC/XBCxgI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "633f0c001a27731ee16cc504e831a4a9ccf071d6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1780952837,
"narHash": "sha256-Fwd1+spDtQ0hDyBwme6ufG3n4mY0UrjjFdYHv+G/Hds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e820eb4a444b46a19b2e03e8dfd2359439ff30fe",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1781074563,
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -314,91 +237,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-docs": {
"locked": {
"lastModified": 1705957679,
"narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-for-bootstrap": {
"locked": {
"lastModified": 1772047000,
"narHash": "sha256-7DaQVv4R97cii/Qdfy4tmDZMB2xxtyIvNGSwXBBhSmo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1788057806,
"narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nmd": {
"inputs": {
"nixpkgs": [
"nix-on-droid",
"nixpkgs-docs"
],
"scss-reset": "scss-reset"
},
"locked": {
"lastModified": 1705050560,
"narHash": "sha256-x3zzcdvhJpodsmdjqB4t5mkVW22V3wqHLOun0KRBzUI=",
"owner": "~rycee",
"repo": "nmd",
"rev": "66d9334933119c36f91a78d565c152a4fdc8d3d3",
"type": "sourcehut"
},
"original": {
"owner": "~rycee",
"repo": "nmd",
"type": "sourcehut"
}
},
"nmt": {
"flake": false,
"locked": {
"lastModified": 1648075362,
"narHash": "sha256-u36WgzoA84dMVsGXzml4wZ5ckGgfnvS0ryzo/3zn/Pc=",
"owner": "rycee",
"repo": "nmt",
"rev": "d83601002c99b78c89ea80e5e6ba21addcfe12ae",
"type": "gitlab"
},
"original": {
"owner": "rycee",
"repo": "nmt",
"type": "gitlab"
}
},
"plasma-manager": { "plasma-manager": {
"inputs": { "inputs": {
"home-manager": [ "home-manager": [
@@ -409,11 +247,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1785762349, "lastModified": 1775856943,
"narHash": "sha256-jZhZkzAwc7f3exzcTDJWP2WCAchCv0iNC3UF/QsahdQ=", "narHash": "sha256-b7Mp7P+q2Md5AGt4rjHfMcBykzMumFTen10ST++AuTU=",
"owner": "nix-community", "owner": "nix-community",
"repo": "plasma-manager", "repo": "plasma-manager",
"rev": "a19a2a029fa180911bd89c554dca1616e10f4c1d", "rev": "a524a6160e6df89f7673ba293cf7d78b559eb1a5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -422,64 +260,25 @@
"type": "github" "type": "github"
} }
}, },
"proxy-suite": {
"inputs": {
"nfqws2-keenetic": "nfqws2-keenetic",
"nixpkgs": [
"nixpkgs"
],
"z2k": "z2k",
"zapret": "zapret"
},
"locked": {
"lastModified": 1790130850,
"narHash": "sha256-k7c+KGZmNLP0ZB9jnKKJUXUTvEJC8A6kHQmZlcN8kNQ=",
"owner": "FUFSoB",
"repo": "proxy-suite-flake",
"rev": "8f65fa9c255e9350fb09f3d3cc9054034918bf49",
"type": "github"
},
"original": {
"owner": "FUFSoB",
"repo": "proxy-suite-flake",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"deploy-rs": "deploy-rs", "deploy-rs": "deploy-rs",
"disko": "disko", "disko": "disko",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"grub2-themes": "grub2-themes", "grub2-themes": "grub2-themes",
"home-manager": "home-manager", "home-manager": "home-manager",
"justray": "justray",
"nix-minecraft": "nix-minecraft",
"nix-on-droid": "nix-on-droid",
"nix-systems": "nix-systems",
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixos-wsl": "nixos-wsl", "nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs_2",
"nixpkgs-beets": "nixpkgs-beets",
"nixpkgs-calibre": "nixpkgs-calibre",
"nixpkgs-master": "nixpkgs-master",
"nixpkgs-stable": "nixpkgs-stable",
"plasma-manager": "plasma-manager", "plasma-manager": "plasma-manager",
"proxy-suite": "proxy-suite",
"sops-nix": "sops-nix", "sops-nix": "sops-nix",
"utils": "utils" "utils": "utils",
} "zapret": "zapret",
}, "zeroq-credentials": "zeroq-credentials"
"scss-reset": {
"flake": false,
"locked": {
"lastModified": 1631450058,
"narHash": "sha256-muDlZJPtXDIGevSEWkicPP0HQ6VtucbkMNygpGlBEUM=",
"owner": "andreymatin",
"repo": "scss-reset",
"rev": "0cf50e27a4e95e9bb5b1715eedf9c54dee1a5a91",
"type": "github"
},
"original": {
"owner": "andreymatin",
"repo": "scss-reset",
"type": "github"
} }
}, },
"sops-nix": { "sops-nix": {
@@ -489,11 +288,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789890976, "lastModified": 1780547341,
"narHash": "sha256-GKwH3zpy7tartuJMG0Rv/xUsdetG1QLmTVv8UKgJLmA=", "narHash": "sha256-Gq8KNx5A7hBB3uGJaj6eQfLDIz5YdLu92gqBcvHvoUo=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "7214124c20c1542c90deb54af50e2f53ae02711f", "rev": "9ed65852b6257fbeae4355bc24ecfea307ca759a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -502,11 +301,24 @@
"type": "github" "type": "github"
} }
}, },
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": { "utils": {
"inputs": { "inputs": {
"systems": [ "systems": "systems"
"nix-systems"
]
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,
@@ -522,59 +334,34 @@
"type": "github" "type": "github"
} }
}, },
"z2k": {
"flake": false,
"locked": {
"lastModified": 1789186266,
"narHash": "sha256-d9gg7s66P3pkN7d4l72ryaGC9Ayoqg4tbHaoDNbDTT4=",
"owner": "necronicle",
"repo": "z2k",
"rev": "7beb9754d65a2cafd9c1d26d382bcb61d453d5b3",
"type": "github"
},
"original": {
"owner": "necronicle",
"ref": "z2k-enhanced",
"repo": "z2k",
"type": "github"
}
},
"zapret": { "zapret": {
"inputs": {
"nixpkgs": [
"proxy-suite",
"nixpkgs"
],
"zapret-flowseal": "zapret-flowseal"
},
"locked": { "locked": {
"lastModified": 1788726932, "lastModified": 1767430655,
"narHash": "sha256-MehJgJpN7BGMaDES9I0aj/YG6JkRlSj5RiZDZfclNpc=", "narHash": "sha256-f9PricXeNm3lG1tk2TepPPY+wxM5y0ezo1HSzNn4BQ8=",
"owner": "kartavkun", "owner": "oqyude",
"repo": "zapret-discord-youtube", "repo": "zapret-easyflake",
"rev": "64a8ee76f4f2e4a8d2751cb732f13448ee1e4fcf", "rev": "302e77aae5fc6030a9c3bcc781d6514d87b19d11",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "kartavkun", "owner": "oqyude",
"repo": "zapret-discord-youtube", "repo": "zapret-easyflake",
"type": "github" "type": "github"
} }
}, },
"zapret-flowseal": { "zeroq-credentials": {
"flake": false,
"locked": { "locked": {
"lastModified": 1788121958, "lastModified": 1772104025,
"narHash": "sha256-WMpxbtA2OH340e4uuXR0tcUW0D6V9Kzs0KI1iKqkXBM=", "narHash": "sha256-tX5I2lkwbB1leoib6Ao/Et0B1GYrn3vxw4DkFYX8uyM=",
"owner": "Flowseal", "ref": "refs/heads/master",
"repo": "zapret-discord-youtube", "rev": "511fc5446b502ff111020bda6d57261648d62333",
"rev": "6cec828910d0809863205702182a3557d9d0e8c3", "revCount": 75,
"type": "github" "type": "git",
"url": "ssh://git@github.com/oqyude/zeroq-credentials.git"
}, },
"original": { "original": {
"owner": "Flowseal", "type": "git",
"repo": "zapret-discord-youtube", "url": "ssh://git@github.com/oqyude/zeroq-credentials.git"
"type": "github"
} }
} }
}, },
+13 -41
View File
@@ -1,11 +1,18 @@
{ {
description = "oqyude flake"; description = "oqyude flake";
inputs = { inputs = {
# My
zeroq-credentials.url = "git+ssh://git@github.com/oqyude/zeroq-credentials.git"; # flake of creds
zapret.url = "github:oqyude/zapret-easyflake"; # stupid flake of zapret
# nixpkgs # nixpkgs
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# nixpkgs-master.url = "github:NixOS/nixpkgs/master"; # nixpkgs-last-unstable.url = "github:NixOS/nixpkgs/6b4955211758ba47fac850c040a27f23b9b4008f";
# nixpkgs-last-unstable.url = "github:NixOS/nixpkgs/3497aa5c9457a9d88d71fa93a4a8368816fbeeba"; nixpkgs-calibre.url = "github:NixOS/nixpkgs/566acc07c54dc807f91625bb286cb9b321b5f42a";
# nixpkgs-fingerprint.url = "github:NixOS/nixpkgs/nixos-24.11"; nixpkgs-master.url = "github:NixOS/nixpkgs/master";
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-beets.url = "github:NixOS/nixpkgs/832efc09b4caf6b4569fbf9dc01bec3082a00611"; # 2343bbb58f99267223bc2aac4fc9ea301a155a16
#nixpkgs-fingerprint.url = "github:NixOS/nixpkgs/nixos-24.11";
# nix-community # nix-community
nixos-wsl = { nixos-wsl = {
@@ -15,13 +22,6 @@
nixpkgs.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
}; };
}; };
nix-on-droid = {
url = "github:nix-community/nix-on-droid/testing"; # testing branch, used on the device
inputs = {
nixpkgs.follows = "nixpkgs";
home-manager.follows = "home-manager";
};
};
deploy-rs = { deploy-rs = {
url = "github:serokell/deploy-rs"; url = "github:serokell/deploy-rs";
inputs = { inputs = {
@@ -30,29 +30,13 @@
utils.follows = "utils"; utils.follows = "utils";
}; };
}; };
justray = {
url = "github:luynrs/justray";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-parts.follows = "flake-parts";
};
};
utils = { utils.url = "github:numtide/flake-utils";
url = "github:numtide/flake-utils";
# flake-utils тянет systems (nix-systems/default) сам -> наследуем корневой, чтобы не плодить дубль-узел в flake.lock
inputs.systems.follows = "nix-systems";
};
flake-compat.url = "github:edolstra/flake-compat"; flake-compat.url = "github:edolstra/flake-compat";
flake-parts.url = "github:hercules-ci/flake-parts"; nixos-hardware.url = "github:NixOS/nixos-hardware/master";
nixos-hardware = {
url = "github:NixOS/nixos-hardware/master";
# без follows nixos-hardware лочит свой собственный nixpkgs (две копии в lock/store)
inputs.nixpkgs.follows = "nixpkgs";
};
nix-systems.url = "github:nix-systems/default";
# nixos-facter-modules.url = "github:numtide/nixos-facter-modules"; # nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
# flake-utils.url = "github:numtide/flake-utils"; # flake-utils.url = "github:numtide/flake-utils";
# flake-parts.url = "github:hercules-ci/flake-parts";
# noctalia = { # noctalia = {
# url = "github:noctalia-dev/noctalia-shell"; # url = "github:noctalia-dev/noctalia-shell";
# inputs.nixpkgs.follows = "nixpkgs"; # inputs.nixpkgs.follows = "nixpkgs";
@@ -73,10 +57,6 @@
home-manager.follows = "home-manager"; home-manager.follows = "home-manager";
}; };
}; };
proxy-suite = {
url = "github:FUFSoB/proxy-suite-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = { sops-nix = {
url = "github:Mic92/sops-nix"; url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@@ -85,14 +65,6 @@
url = "github:vinceliuice/grub2-themes"; url = "github:vinceliuice/grub2-themes";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
nix-minecraft = {
url = "github:Infinidoge/nix-minecraft";
inputs = {
flake-compat.follows = "flake-compat";
nixpkgs.follows = "nixpkgs";
systems.follows = "nix-systems";
};
};
# nix-index-database = { # nix-index-database = {
# url = "github:nix-community/nix-index-database"; # url = "github:nix-community/nix-index-database";
# inputs.nixpkgs.follows = "nixpkgs"; # inputs.nixpkgs.follows = "nixpkgs";
+12
View File
@@ -1,9 +1,21 @@
{ {
config, config,
pkgs, pkgs,
xlib,
... ...
}: }:
let
streamripPath = "${xlib.dirs.wsl-storage}/streamrip";
in
{ {
# xdg = {
# configFile = {
# "streamrip" = {
# source = config.lib.file.mkOutOfStoreSymlink streamripPath;
# target = "streamrip";
# };
# };
# };
home.packages = [ home.packages = [
pkgs.yt-dlp-light pkgs.yt-dlp-light
]; ];
+35 -30
View File
@@ -9,14 +9,10 @@ let
... ...
}: }:
let let
mkUser = mkHomeModule = username: {
username: imports = [
{ (./. + "/${xlib.device.type}.nix")
imports ? [ ], ];
headless ? false,
}:
{
inherit imports;
home = { home = {
username = username; username = username;
stateVersion = lib.mkDefault "26.05"; stateVersion = lib.mkDefault "26.05";
@@ -24,22 +20,26 @@ let
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}"; if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
enableNixpkgsReleaseCheck = false; enableNixpkgsReleaseCheck = false;
}; };
# Headless hosts: no GUI user dirs
xdg = lib.mkIf headless {
enable = true;
autostart.enable = true;
userDirs = {
enable = true;
createDirectories = false;
desktop = null;
documents = null;
download = null;
music = null;
pictures = null;
publicShare = null;
templates = null;
videos = null;
}; };
mkRootModule = username: {
home = {
username = username;
stateVersion = lib.mkDefault "26.05";
homeDirectory =
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
enableNixpkgsReleaseCheck = false;
};
};
mkOthersModule = username: {
imports = [
(./. + "/others/${xlib.device.type}.nix")
];
home = {
username = username;
stateVersion = lib.mkDefault "26.05";
homeDirectory =
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
enableNixpkgsReleaseCheck = false;
}; };
}; };
in in
@@ -48,13 +48,18 @@ let
useGlobalPkgs = true; useGlobalPkgs = true;
useUserPackages = true; useUserPackages = true;
users = { users = {
root = mkUser "root" { }; root = mkRootModule "root";
"${xlib.device.username}" = mkUser xlib.device.username { "${xlib.device.username}" = mkHomeModule xlib.device.username;
imports = [ }
(./. + "/${xlib.device.type}.nix") //
]; lib.optionalAttrs
headless = xlib.isHeadless; (builtins.elem xlib.device.type [
}; "test"
#"secondary"
#"primary"
])
{
snity = mkOthersModule "snity";
}; };
sharedModules = [ sharedModules = [
inputs.plasma-manager.homeModules.plasma-manager inputs.plasma-manager.homeModules.plasma-manager
-417
View File
@@ -1,417 +0,0 @@
# Declarative OpenCode + oh-my-openagent (oh-my-opencode) plugin setup.
#
# Mirrors ~/.config/opencode/ on the current workstation.
# Imported by home/server.nix (sapphira). Auto-enables programs.opencode.
#
# Files this module owns on disk:
# ~/.config/opencode/opencode.json <- programs.opencode.settings
# ~/.config/opencode/tui.json <- programs.opencode.tui
# ~/.omo/omo.jsonc <- oh-my-openagent plugin's PRIMARY
# runtime config (>= v5.x reads
# only this path; the legacy
# ~/.config/opencode/oh-my-openagent.json
# is read only by the migration shim).
# ~/.config/opencode/oh-my-openagent.json <- legacy mirror, kept so omo doctor
# and any downgrade that re-reads
# the old path see the same content.
#
# Override any field in the importing module if needed.
{
config,
lib,
pkgs,
xlib,
...
}:
let
# Body of ~/.omo/omo.jsonc (and the legacy mirror).
# Loaded by the oh-my-openagent opencode plugin on startup.
#
# Plugins >= 5.x resolve their config from ~/.omo/omo.jsonc, NOT from
# ~/.config/opencode/oh-my-openagent.json. Pinning _migrations here prevents
# the 2026-07-opencode-config-unification migration from running on every
# startup and re-backing-up the file (which would otherwise leave us with
# an empty omo.jsonc that drops every agent override — see the journal entry
# below).
ohMyOpenagentConfig = {
"$schema" =
"https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/omo.schema.json";
_migrations = [
"2026-07-opencode-config-unification"
"2026-08-reasoning-unification"
];
agents = {
sisyphus = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
models = [
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
}
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
{ model = "opencode/big-pickle"; }
];
};
hephaestus = {
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
};
oracle = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "xhigh";
models = [
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
}
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
}
];
};
librarian = {
model = "minimax-coding-plan/MiniMax-M3";
};
explore = {
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
models = [
{ model = "minimax-coding-plan/MiniMax-M3"; }
];
};
"multimodal-looker" = {
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "low";
models = [
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
];
};
prometheus = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
models = [
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "high";
}
];
};
metis = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
models = [
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "low";
}
];
};
momus = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "xhigh";
models = [
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
}
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
}
];
};
atlas = {
model = "minimax-coding-plan/MiniMax-M3";
models = [
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
}
{ model = "minimax-coding-plan/MiniMax-M3"; }
];
};
"sisyphus-junior" = {
model = "minimax-coding-plan/MiniMax-M3";
models = [
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
}
{ model = "minimax-coding-plan/MiniMax-M3"; }
{ model = "opencode/big-pickle"; }
];
};
};
categories = {
"visual-engineering" = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
fallback_models = [
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
}
];
};
ultrabrain = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "xhigh";
fallback_models = [
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
}
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
}
];
};
deep = {
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
fallback_models = [
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
}
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
}
];
};
artistry = {
model = "minimax-coding-plan/MiniMax-M3";
variant = "high";
fallback_models = [
{
model = "minimax-coding-plan/MiniMax-M3";
variant = "max";
}
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "high";
}
];
};
quick = {
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
fallback_models = [
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
{ model = "minimax-coding-plan/MiniMax-M3"; }
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
];
};
"unspecified-low" = {
model = "minimax-coding-plan/MiniMax-M3";
fallback_models = [
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
}
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
{ model = "minimax-coding-plan/MiniMax-M3"; }
];
};
"unspecified-high" = {
model = "minimax-coding-plan/MiniMax-M3";
fallback_models = [
{
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
variant = "medium";
}
{ model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview"; }
{ model = "minimax-coding-plan/MiniMax-M3"; }
];
};
writing = {
model = "minimax-coding-plan/MiniMax-M3.1-Flash-Preview";
fallback_models = [
{ model = "minimax-coding-plan/MiniMax-M3"; }
{ model = "minimax-coding-plan/MiniMax-M3"; }
];
};
};
};
in
let
# nixpkgs ast-grep only ships binary `ast-grep`; omo's ast-grep skill probes
# for `sg` (or ast-grep). Provide both via a symlink wrapper.
astGrepWithSg = pkgs.runCommandLocal "ast-grep-with-sg" { } ''
mkdir -p $out/bin
ln -s ${pkgs.ast-grep}/bin/ast-grep $out/bin/ast-grep
ln -s ${pkgs.ast-grep}/bin/ast-grep $out/bin/sg
'';
in
{
programs.opencode = {
enable = true;
# Extras available to opencode-wrapped (via --suffix PATH on the wrapper):
# pkgs.nodejs_22 — npx/npm for MCP servers (webpage-mcp) and omo's plugin loader
# pkgs.ast-grep — `sg` CLI; omo's ast-grep skill requires it (omo doctor)
# pkgs.bun — omo prefers bun; with bun on PATH, `omo doctor` skips node fallback
# pkgs.gh — GitHub CLI; omo's GitHub automation features require it
extraPackages = [
pkgs.nodejs_22
astGrepWithSg
pkgs.bun
pkgs.gh
];
# ~/.config/opencode/opencode.json
settings = {
plugin = [ "oh-my-openagent@latest" ];
mcp = {
webpage = {
type = "local";
command = [
"npx"
"-y"
"-p"
"webpage-mcp@latest"
"webpage-mcp-stdio"
];
};
};
};
# ~/.config/opencode/tui.json
# Mirrors workstation: oh-my-openagent also registered for the TUI.
tui = {
plugin = [ "oh-my-openagent@latest" ];
};
};
# ~/.omo/omo.jsonc — primary file the oh-my-openagent plugin reads at runtime
# (>= v5.x). This path lives outside XDG_CONFIG_HOME (~/.config), so use
# home.file rather than xdg.configFile.
#
# ~/.config/opencode/oh-my-openagent.json is kept as a legacy mirror so
# `omo doctor`, the migration shim, and any future downgrade that re-reads the
# old path see the same content.
#
# MIGRATION TRAP (do not just point back at the legacy path):
# The plugin runs a `2026-07-opencode-config-unification` migration on every
# startup that backs up ~/.omo/omo.jsonc and tries to rewrite it from
# ~/.config/opencode/oh-my-openagent.json. The backup directory name embeds
# the source's content-hashed store path; because HM does not delete the
# previous generation's store path until garbage collection, the same path
# is reused on every retry and omo logs "Migration backup path already
# exists" forever — meanwhile the user's agent overrides disappear and the
# plugin's built-in fallback chain (which references providers like
# kimi-for-coding that opencode's provider registry no longer ships) gets
# picked instead, surfacing as `ProviderModelNotFoundError:
# kimi-for-coding/kimi-for-coding-highspeed` on every subagent spawn.
# Pinning _migrations above makes the migration a no-op; the omo.jsonc
# below is the actual config the plugin sees.
home.file."${config.home.homeDirectory}/.omo/omo.jsonc".text = builtins.toJSON ohMyOpenagentConfig;
xdg.configFile."opencode/oh-my-openagent.json".text = builtins.toJSON ohMyOpenagentConfig;
# Same extras on the user's PATH too, so `omo doctor` and standalone invocations
# of `sg`, `gh`, `bun`, `npm`, `npx` work in the user's shell — not only inside
# the opencode-wrapped binary.
home.packages = [
pkgs.nodejs_22
astGrepWithSg
pkgs.bun
pkgs.gh
];
# Expose `opencode web` as a systemd user service. nginx on sapphira
# proxies https://opencode.zeroq.su -> 127.0.0.1:4096.
#
# --hostname 0.0.0.0 binds the listener to every interface (matches the
# "0.0.0.0" intent; nginx then reverse-proxies 127.0.0.1:4096 internally).
# --cors https://opencode.zeroq.su lets the browser session reach the
# server from that origin without CORS rejection.
#
# SECURITY: with no password, anyone reaching the upstream socket gets full
# opencode. Bind 0.0.0.0 + listener == bridge == shell. The password is
# supplied via sops-managed EnvironmentFile, declared in modules/users.nix
# and decrypted to a path hardcoded here (home-manager modules cannot read
# `config.sops.*` — sops-nix options are NixOS-only).
programs.opencode.web = {
enable = true;
environmentFile = xlib.dirs.opencode-server-env;
extraArgs = [
"--hostname"
"0.0.0.0"
"--cors"
"https://opencode.zeroq.su"
];
};
# RAM constraints for the opencode-web user service.
#
# Sapphira has 5.6 GiB RAM with a ~1 GiB baseline (syncthing + immich + gitea
# + x-ui + nextcloud php-fpm). When something else spikes (immich-ml jobs,
# syncthing indexer, etc.) the system OOM killer activates and picks the
# largest cgroup — opencode at ~260 MiB – 1.4 GiB peak was being chosen and
# systemd then restarted it every few seconds (`RestartSec=5`), masking the
# real cause as a "service crash". The 2026-10-04 incident was exactly this.
#
# Three knobs together make opencode stop being an OOM victim AND stop being
# the source of an OOM:
#
# MemoryHigh soft pressure threshold: kernel reclaims aggressively
# once the cgroup hits this. Process keeps running.
# MemoryMax hard cap: cgroup-local OOM kills Node if exceeded. The
# HOST survives — only this process dies, no restart storm.
# OOMScoreAdjust negative bias for the system-wide OOM killer: opencode
# is killed last, after syncthing/immich/etc.
# OOMPolicy "continue" — systemd does NOT auto-restart on cgroup
# OOM-kill. Without this, a spike triggers the same
# restart-loop the host saw today.
#
# Sizes are derived from observed peak (1.4 GiB at 16:36, 1.1 GiB at 16:59).
# MemoryHigh = 1G gives headroom for normal runs; MemoryMax = 2G caps
# pathological growth. Tweak both together if a workload legitimately
# needs more.
#
# Refs:
# https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#OOMScoreAdjust=
# cgroup/OOM knobs added on top of the [Service] section emitted by
# `programs.opencode.web`. home-manager unions multiple definitions of the
# same systemd unit attrset, so ExecStart/Restart/EnvironmentFile from
# upstream and MemoryHigh/MemoryMax/OOMScoreAdjust/OOMPolicy from here
# land in the same [Service] block systemd actually reads.
#
# NOTE: do NOT use `serviceConfig = { ... }` — home-manager renders that
# as a literal `[serviceConfig]` section, which systemd silently ignores
# (`Unknown section 'serviceConfig'. Ignoring.`). The cgroup protection
# above would never take effect (verified on sapphira, c73a698).
systemd.user.services.opencode-web.Service = {
MemoryHigh = "1G";
MemoryMax = "2G";
OOMScoreAdjust = -900;
OOMPolicy = "continue";
};
# Workaround: home-manager activation updates the GC root `current-home`
# only at the very end (line 358 of the generated activate script), AFTER all
# `home.activation.*` dag entries have run. So we cannot read current-home
# from a dag entry — it still points to the OLD generation at the time our
# script executes. Instead, read `new-home`, which the activator writes
# BEFORE any dag entry runs and which already points at the new generation.
#
# The versioned symlink (`home-manager-NN-link`) is found by following
# `home-manager` one hop rather than hardcoding `home-manager-24-link`,
# so this keeps working across HM major-version bumps.
home.activation.relinkHomeManager = lib.hm.dag.entryAfter [ ] ''
hmVersioned="$(readlink "$HOME/.local/state/nix/profiles/home-manager" 2>/dev/null || true)"
target="$HOME/.local/state/nix/profiles/$hmVersioned"
newGen="$(readlink -e "''${XDG_STATE_HOME:-$HOME/.local/state}/home-manager/gcroots/new-home" 2>/dev/null || true)"
if [[ -n "$hmVersioned" && -n "$newGen" && "$(readlink -f "$target")" != "$newGen" ]]; then
echo "home-manager: relinking $target -> $newGen"
ln -sfn "$newGen" "$target"
fi
'';
}
+27
View File
@@ -8,6 +8,25 @@
programs = { programs = {
mangohud.enable = true; mangohud.enable = true;
keepassxc.enable = true; keepassxc.enable = true;
zed-editor = {
enable = false;
extensions = [
"nix"
];
userSettings = {
"telemetry" = {
"diagnostics" = false;
"metrics" = false;
};
"ui_font_size" = 20;
"buffer_font_size" = 26;
"theme" = {
"mode" = "system";
"light" = "Ayu Light";
"dark" = "Ayu Dark";
};
};
};
}; };
services = { services = {
kdeconnect.enable = true; kdeconnect.enable = true;
@@ -15,6 +34,12 @@
}; };
home = { home = {
packages = with pkgs; [ packages = with pkgs; [
# Surfing
# (brave.override {
# commandLineArgs = [
# "--password-store=basic" # on purpose to make it break "--password-store=gnome-libsecret"
# ];
# })
brave brave
v2rayn v2rayn
@@ -23,6 +48,8 @@
# amdgpu_top # amdgpu_top
vscodium vscodium
ayugram-desktop ayugram-desktop
# vesktop
# discord
gramps gramps
kdePackages.filelight kdePackages.filelight
localsend localsend
+52
View File
@@ -0,0 +1,52 @@
{
config,
lib,
pkgs,
xlib,
...
}:
let
symlinksPaths = {
"/home/oqyude/Games/PrismLaunchers" = "${config.home.homeDirectory}/Games/PrismLaunchers";
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
".local/share/PrismLauncher";
};
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
name = targetPath;
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
}) symlinksPaths;
in
{
imports = [
../minimal.nix
../modules/packages.nix
../modules/plasma-manager.nix
];
xdg = {
enable = true;
autostart.enable = true;
userDirs = {
enable = true;
createDirectories = true;
desktop = "${config.xdg.dataHome}/desktop";
documents = null;
download = "${config.home.homeDirectory}/Downloads";
music = "${config.home.homeDirectory}/Music";
pictures = "${config.home.homeDirectory}/Pictures";
publicShare = "${config.home.homeDirectory}/Misc/Public";
templates = null;
videos = "${config.home.homeDirectory}/Pictures/Videos";
};
};
home = {
file = mkLinks;
pointerCursor = {
enable = true;
x11.enable = true;
gtk.enable = true;
size = 24;
name = "Qogir";
package = pkgs.qogir-icon-theme;
};
};
}
+52
View File
@@ -0,0 +1,52 @@
{
config,
lib,
pkgs,
xlib,
...
}:
let
symlinksPaths = {
"/home/oqyude/Games/PrismLaunchers" = "${config.home.homeDirectory}/Games/PrismLaunchers";
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
".local/share/PrismLauncher";
};
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
name = targetPath;
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
}) symlinksPaths;
in
{
imports = [
../minimal.nix
../modules/packages.nix
../modules/plasma-manager.nix
];
xdg = {
enable = true;
autostart.enable = true;
userDirs = {
enable = true;
createDirectories = true;
desktop = "${config.xdg.dataHome}/desktop";
documents = null;
download = "${config.home.homeDirectory}/Downloads";
music = "${config.home.homeDirectory}/Music";
pictures = "${config.home.homeDirectory}/Pictures";
publicShare = "${config.home.homeDirectory}/Misc/Public";
templates = null;
videos = "${config.home.homeDirectory}/Pictures/Videos";
};
};
home = {
file = mkLinks;
pointerCursor = {
enable = true;
x11.enable = true;
gtk.enable = true;
size = 24;
name = "Qogir";
package = pkgs.qogir-icon-theme;
};
};
}
+1 -1
View File
@@ -32,7 +32,7 @@ in
./modules/dconf.nix ./modules/dconf.nix
./modules/packages.nix ./modules/packages.nix
./modules/plasma-manager.nix ./modules/plasma-manager.nix
# ./modules/noctalia.nix ./modules/noctalia.nix
]; ];
xdg = { xdg = {
enable = true; enable = true;
+2 -2
View File
@@ -14,7 +14,7 @@ let
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" = "${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
".local/share/PrismLauncher"; ".local/share/PrismLauncher";
"${xlib.dirs.lamet-drive}/Users/${xlib.device.username}/Music" = "Music"; "${xlib.dirs.lamet-drive}/Users/oqyude/Music" = "Music";
}; };
mkLinks = lib.mapAttrs' (sourcePath: targetPath: { mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
name = targetPath; name = targetPath;
@@ -27,7 +27,7 @@ in
./modules/dconf.nix ./modules/dconf.nix
./modules/packages.nix ./modules/packages.nix
./modules/plasma-manager.nix ./modules/plasma-manager.nix
# ./modules/noctalia.nix ./modules/noctalia.nix
]; ];
xdg = { xdg = {
enable = true; enable = true;
+25 -3
View File
@@ -5,13 +5,35 @@
xlib, xlib,
... ...
}: }:
let
symlinksPaths = {
"${config.home.homeDirectory}/External/Music" = "Music";
};
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
name = targetPath;
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
}) symlinksPaths;
in
{ {
imports = [ imports = [
./minimal.nix ./minimal.nix
./modules/opencode.nix
]; ];
home.file = xlib.helpers.mkSymlinks config { home.file = mkLinks;
"${config.home.homeDirectory}/External/Music" = "Music"; xdg = {
enable = true;
autostart.enable = true;
userDirs = {
enable = true;
createDirectories = false;
desktop = null;
documents = null;
download = null;
music = null;
pictures = null;
publicShare = null;
templates = null;
videos = null;
};
}; };
home.activation = { home.activation = {
yaziSync = '' yaziSync = ''
-283
View File
@@ -1,283 +0,0 @@
{
config,
lib,
pkgs,
xlib,
...
}:
# Shared "strict" home-manager module.
# Works in BOTH contexts:
# - NixOS hosts (via home-manager.sharedModules or homeConfigurations)
# - nix-on-droid (via home-manager.config in droid/epral.nix)
# Only home-manager options are used here — no systemd.*, no services.*,
# no users.*, no environment.systemPackages. All paths are parameterized
# through config.home.homeDirectory so /home/oqyude (NixOS) and
# /data/data/com.termux.nix/files/home (termux) both work.
#
# NOTE: intentionally duplicates parts of modules/essentials/{shell,packages}.nix
# (which stay NixOS-only for now). When this module is wired into NixOS hosts
# via sharedModules, deduplicate those files.
{
home = {
packages = with pkgs; [
# Lazy (alias lc)
lazycli
# IDE
fresh-editor # EDITOR
# Base utils
curl
wget
fd
tree
dust
gdu
mc
rsync
jq
unzip
zip
zstd
# Net diagnostic
mtr
dnsutils
# Monitoring
htop
];
sessionVariables = {
TUCKR_HOME = "$HOME/Storage/dotfiles";
EDITOR = "fresh";
};
file = {
".nanorc".text = ''
set nowrap
set tabstospaces
set tabsize 2
'';
# Authorized keys for sshd (see modules/termux/default.nix).
# Declarative for now — the Store/.ssh symlink scheme is postponed.
".ssh/authorized_keys".text = ''
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKduJia+unaQQdN6X5syaHvnpIutO+yZwvfiCP4qKQ/P
'';
};
};
programs = {
# ---- Shell: zsh ----
zsh = {
enable = true;
enableCompletion = true;
syntaxHighlighting.enable = true;
history.size = 10000;
oh-my-zsh = {
enable = true;
theme = "robbyrussell";
};
loginExtra = "clear && fastfetch && cd ~/.config/nix-on-droid";
# .zshenv — sourced by zsh in ALL sessions incl. non-login ssh commands.
# runit from nixpkgs defaults to /var/service as SVDIR, but our tree
# lives at ~/service (symlinked as /etc/service). Export it so that
# `sv status sshd` works without qualifying the path.
envExtra = "export SVDIR=/etc/service";
initContent = ''
beet-p() {
local base="${config.home.homeDirectory}/.config/beets/My"
local rel
rel=$(realpath --relative-to="$base" "$PWD")
beet mod "path:$rel" playlist="$*"
}
beet-ims() {
beet im ./ -S $*
}
beet-path() {
realpath --relative-to="${config.home.homeDirectory}/.config/beets/My" "$1"
}
'';
shellAliases = {
# shell
ff = "clear && fastfetch";
l = "ls -l";
lg = "lazygit";
lc = "lazycli";
gp = "git pull";
ns = "nix-on-droid switch --flake ~/.config/nix-on-droid#${xlib.device.hostname}";
gp-ns = "gp && ns";
gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'";
y = "yazi";
nix-shellp = "nix-shell --run $SHELL -p";
beet-path-library = "realpath --relative-to='${config.home.homeDirectory}/.config/beets/My' .";
z-proxy = "export ALL_PROXY=socks5://localhost:10808";
zh-proxy = "export HTTPS_PROXY=http://localhost:10808 && export HTTP_PROXY=http://localhost:10808";
nix-dir = "cd ~/.config/nix-on-droid";
q-ssh = "sv-start"; # start all supervised services (sshd, ...); manage with `sv status sshd` etc
# beets
beet-ima = "beet im ./ -A";
# ssh (hosts live in programs.ssh.settings below)
# NOTE: lamet / pubray-1 have no Host entry yet — kept as aliases.
z-l = "ssh lamet";
z-lt = "ssh lamet-tailscale";
z-p-1 = "ssh pubray-1";
z-map-local-proxy = "ssh -R 10808:localhost:10808";
# Extras
plasma-manager = "nix run github:nix-community/plasma-manager";
pip2nix = "nix run github:nix-community/pip2nix --"; # https://github.com/nix-community/pip2nix
pip2nix-g = "nix run github:nix-community/pip2nix -- generate -r";
json2nix = "nix run github:sempruijs/json2nix";
};
};
# ---- Editor ----
# NOTE: programs.nano is a NixOS-only module (does not exist in
# home-manager); write ~/.nanorc directly instead.
# ---- TUI tools ----
bat.enable = true;
lazygit.enable = true;
fzf.enable = true;
btop.enable = true;
broot.enable = true;
bottom.enable = true;
fastfetch.enable = true;
yazi = {
enable = true;
# explicit: shared module must behave identically on NixOS (26.05, "y")
# and nix-on-droid (24.05, legacy "yy")
shellWrapperName = "y";
plugins = {
inherit (pkgs.yaziPlugins)
gitui
git
sudo
ouch
rsync
diff
mount
chmod
dupes
lazygit
toggle-pane
rich-preview
smart-filter
full-border
recycle-bin
;
};
flavors = {
nord = pkgs.yaziPlugins.nord;
};
theme = {
flavor = {
light = "nord";
dark = "nord";
};
};
keymap = {
mgr.prepend_keymap = [
{
on = [
"M"
];
run = "plugin mount";
desc = "Mount manager";
}
{
on = [
"g"
"i"
];
run = "plugin lazygit";
desc = "run lazygit";
}
{
run = "plugin ouch --args=zip";
on = [
"g"
"C"
];
desc = "Compress with ouch";
}
];
};
settings = {
mgr.ratio = [
1
1
4
];
};
};
# ---- VCS ----
git = {
enable = true;
settings = {
user = {
name = xlib.device.username;
email = "oqyude@gmail.com";
};
pull = {
rebase = true;
};
};
};
# ---- SSH ----
# Declarative ~/.ssh/config, same as the one previously copied by hand.
# matchBlocks is deprecated in current home-manager; use `settings`
# (bare attr names become `Host` headers, keys are upstream directives).
ssh = {
enable = true;
# Reproduce the old enableDefaultConfig values explicitly.
enableDefaultConfig = false;
settings = {
"*" = {
ForwardAgent = false;
AddKeysToAgent = "no";
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
HashKnownHosts = false;
UserKnownHostsFile = "~/.ssh/known_hosts";
ControlMaster = "no";
ControlPath = "~/.ssh/master-%r@%n:%p";
ControlPersist = "no";
};
sapphira = {
HostName = "192.168.1.20";
User = xlib.device.username;
};
sapphira-tailscale = {
HostName = "100.64.0.0";
User = xlib.device.username;
};
otreca-old = {
HostName = "217.60.3.12";
User = xlib.device.username;
};
otreca = {
HostName = "109.248.161.5";
User = xlib.device.username;
};
otreca-tailscale = {
HostName = "100.64.1.0";
User = xlib.device.username;
};
rydiwo = {
HostName = "192.168.1.102";
User = xlib.device.username;
};
epral = {
HostName = "192.168.1.101";
User = xlib.device.username;
Port = 8022;
};
};
};
};
}
+27
View File
@@ -0,0 +1,27 @@
{
config,
pkgs,
xlib,
...
}:
{
imports = [
./minimal.nix
];
xdg = {
enable = true;
autostart.enable = true;
userDirs = {
enable = true;
createDirectories = false;
desktop = null;
documents = null;
download = null;
music = null;
pictures = null;
publicShare = null;
templates = null;
videos = null;
};
};
}
+19
View File
@@ -1,8 +1,27 @@
{ {
config,
pkgs,
xlib,
... ...
}: }:
{ {
imports = [ imports = [
./minimal.nix ./minimal.nix
]; ];
xdg = {
enable = true;
autostart.enable = true;
userDirs = {
enable = true;
createDirectories = false;
desktop = null;
documents = null;
download = null;
music = null;
pictures = null;
publicShare = null;
templates = null;
videos = null;
};
};
} }
+27 -4
View File
@@ -5,15 +5,38 @@
xlib, xlib,
... ...
}: }:
let
symlinksPaths = {
"${config.home.homeDirectory}/External/Music" = "Music";
"${xlib.dirs.wsl-home}" = "External";
"${xlib.dirs.wsl-storage}" = "Storage";
};
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
name = targetPath;
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
}) symlinksPaths;
in
{ {
imports = [ imports = [
./apps ./apps
./minimal.nix ./minimal.nix
]; ];
home.file = xlib.helpers.mkSymlinks config { home.file = mkLinks;
"${config.home.homeDirectory}/External/Music" = "Music"; xdg = {
"${xlib.dirs.wsl-home}" = "External"; enable = true;
"${xlib.dirs.wsl-storage}" = "Storage"; autostart.enable = true;
userDirs = {
enable = true;
createDirectories = false;
desktop = null;
documents = null;
download = null;
music = null;
pictures = null;
publicShare = null;
templates = null;
videos = null;
};
}; };
home.activation = { home.activation = {
yaziSync = '' yaziSync = ''
-29
View File
@@ -1,29 +0,0 @@
{
inputs,
...
}:
# Builds a NixOS system from a host record.
#
# `xlib` is the pure host value (lib/xlib.nix `mkXlib`) built in
# configurations/default.nix. It is handed to every module as the `xlib`
# argument, so modules read plain `xlib.*` data instead of `config.xlib.*`
# and the host record stays the single source of truth.
{
xlib,
modules ? [ ],
system ? "x86_64-linux",
...
}:
let
lib = inputs.nixpkgs.lib;
in
lib.nixosSystem {
inherit
system
modules
;
specialArgs = {
inherit inputs;
inherit xlib;
};
}
-78
View File
@@ -1,78 +0,0 @@
# Pure host library: no module system involved.
#
# Aggregates the four concerns a host record is built from:
# device.nix identity + capability flags from the device type
# dirs.nix well-known paths, derived from username
# helpers.nix pure helper functions shared by modules
#
# `mkXlib` is called in flake-level code (configurations/default.nix) and
# handed to every module as the `xlib` argument via lib/mkSystem.nix, so
# modules read plain `xlib.*` values instead of `config.xlib.*` and nothing in
# xlib can be overridden per host — the host record is the only place to
# change it.
{
lib,
...
}:
let
inherit (import ./device.nix { inherit lib; })
devices
mkDevice
;
# dirs.nix is itself a function of `username`, not an attrset.
mkDirs = import ./dirs.nix;
helpers = (import ./helpers.nix { inherit lib; });
in
{
inherit
devices
helpers
mkDevice
mkDirs
;
# Full host record: identity + capability flags + well-known paths +
# shared helpers.
mkXlib =
{
hostname,
type,
username ? "oqyude",
uid ? 1000,
gid ? 1000,
}:
let
device = mkDevice {
inherit
hostname
type
username
uid
gid
;
};
in
{
device = {
inherit
hostname
type
username
uid
gid
;
};
isDesktop = device.isDesktop;
isHeadless = device.isHeadless;
dirs = mkDirs username;
# Bind the host's ids into the mount helpers, so ntfs3/exfat options
# carry the same uid/gid the primary user actually has.
helpers = import ./helpers.nix {
inherit lib;
uid = device.uid;
gid = device.gid;
};
};
}
-75
View File
@@ -1,75 +0,0 @@
{
lib,
...
}:
# Supported device types and the identity record built from one.
#
# Single source of truth for host identity: hostname, type, username and the
# capability flags derived from the type. Replaces the old `lib.types.enum`
# in modules/options.nix and the hand-written type lists in modules/default.nix
# and home/home.nix.
let
devices = {
minimal = {
desktop = false;
headless = false;
};
primary = {
desktop = true;
headless = false;
};
secondary = {
desktop = true;
headless = false;
};
server = {
desktop = false;
headless = true;
};
vds = {
desktop = false;
headless = true;
};
wsl = {
desktop = false;
headless = true;
};
termux = {
desktop = false;
headless = true;
};
};
in
{
inherit devices;
# Unknown device type fails here, at flake level, with the valid list.
mkDevice =
{
hostname,
type,
username ? "oqyude",
# The primary user is pinned to 1000 rather than left to NixOS'
# nextfree logic: the mount helpers below write uid=/gid= into
# ntfs3/exfat options, and an NTFS/exFAT volume mounted with a
# different id shows every file as owned by `nobody`.
uid ? 1000,
gid ? 1000,
}:
let
capabilities =
devices.${type}
or (throw "xlib: unknown device type '${type}', expected one of ${lib.concatStringsSep ", " (builtins.attrNames devices)}");
in
{
inherit
hostname
type
username
uid
gid
;
isDesktop = capabilities.desktop;
isHeadless = capabilities.headless;
};
}
-36
View File
@@ -1,36 +0,0 @@
# Well-known paths. Everything derives from `username`, which is why the
# whole set can be computed outside the module system.
username:
let
user-home = "/home/${username}";
wsl-home = "/mnt/c/Users/${username}";
server-home = "${user-home}/External";
services-mnt-folder = "/mnt/services";
in
{
inherit
user-home
wsl-home
server-home
services-mnt-folder
;
opencode-server-env = "${user-home}/.config/opencode/server.env";
user-storage = "${user-home}/Storage";
wsl-storage = "${wsl-home}/Storage";
server-credentials = "${server-home}/Credentials/server";
storage = "${server-home}/Storage";
calibre-library = "${server-home}/Books-Library";
services-folder = "${server-home}/Services";
services-nodes-folder = "${services-mnt-folder}/nodes";
postgresql-folder = "${services-mnt-folder}/postgresql";
authelia-folder = "${services-mnt-folder}/authelia";
music-library = "${user-home}/Music";
archive-drive = "/mnt/archive";
lamet-drive = "/mnt/lamet";
mobile-drive = "/mnt/mobile";
therima-drive = "/mnt/therima";
vetymae-drive = "/mnt/vetymae";
soptur-drive = "/mnt/soptur";
}
-161
View File
@@ -1,161 +0,0 @@
{
lib,
# The primary user's ids, bound from xlib.device by mkXlib. ntfs3/exfat
# volumes carry POSIX ids, so a mount using anything other than the real
# uid/gid shows every file as owned by `nobody`.
uid,
gid,
...
}:
# Pure helper functions for module definitions.
# Injected into every module via `xlib.helpers` (see default.nix).
#
# Defined in a `let` because they reference each other (mkTmpDirs uses
# mkTmpfile, mkServiceStorage uses mkTmpDirs + mkSystemdBind).
let
# tmpfiles rule: "type dir mode user group -"
mkTmpfile =
type: dir: mode: user: group:
"${type} ${dir} ${mode} ${user} ${group} -";
# several tmpfiles types for the same dir, e.g. ["d" "z"] or ["d" "Z"]
mkTmpDirs =
{
dir,
mode,
user,
group,
types ? [
"d"
"z"
],
}:
map (type: mkTmpfile type dir mode user group) types;
# fileSystems bind mount
mkBindMount =
{
what,
where,
}:
{
"${where}" = {
device = what;
fsType = "none";
options = [
"bind"
"nofail"
];
};
};
# systemd.mounts bind mount (automount variant)
mkSystemdBind =
{
what,
where,
}:
{
enable = true;
options = "bind,x-systemd.automount,nofail";
requires = [ "local-fs.target" ];
type = "none";
wantedBy = [ "multi-user.target" ];
inherit what where;
};
# Full "service storage" block: services-mnt source dir + /var/lib target,
# tmpfiles d/z + automount bind. Used as:
# storage = xlib.helpers.mkServiceStorage { name = "x"; user = "x"; group = "x"; };
# systemd = storage.systemd;
mkServiceStorage =
{
name,
user,
group,
mode ? "0755",
target ? "/var/lib/${name}",
base ? "/mnt/services",
}:
let
sourceDir = "${base}/${name}";
in
{
inherit sourceDir target;
systemd = {
tmpfiles.rules = mkTmpDirs {
dir = sourceDir;
inherit mode user group;
};
mounts = [
(mkSystemdBind {
what = sourceDir;
where = target;
})
];
};
};
# ntfs3 mount, e.g. fileSystems = mkNtfsMount { path = ...; uuid = ...; }
mkNtfsMount =
{
path,
uuid,
mask ? "0007",
enable ? null,
}:
{
"${path}" = {
device = "/dev/disk/by-uuid/${uuid}";
fsType = "ntfs3";
options = [
"defaults"
"uid=${toString uid}"
"gid=${toString gid}"
"fmask=${mask}"
"dmask=${mask}"
"nofail"
];
}
// lib.optionalAttrs (enable != null) { inherit enable; };
};
# exfat mount, e.g. fileSystems = mkExfatMount { path = ...; uuid = ...; }
mkExfatMount =
{
path,
uuid ? null,
label ? null,
}:
{
"${path}" = {
device = if uuid != null then "/dev/disk/by-uuid/${uuid}" else "/dev/disk/by-label/${label}";
fsType = "exfat";
options = [
"nofail"
"uid=${toString uid}"
"gid=${toString gid}"
];
};
};
# home-manager out-of-store symlinks: path = source (target name = attr name)
mkSymlinks =
config: paths:
lib.mapAttrs' (sourcePath: targetPath: {
name = targetPath;
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
}) paths;
in
{
inherit
mkTmpfile
mkTmpDirs
mkBindMount
mkSystemdBind
mkServiceStorage
mkNtfsMount
mkExfatMount
mkSymlinks
;
}
+66 -65
View File
@@ -5,40 +5,7 @@
xlib, xlib,
... ...
}: }:
let
panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/3x-ui";
# Domain whose Let's Encrypt cert (at /var/lib/acme/<domain>/) gets mounted
# read-only into the 3x-ui container so the panel can terminate TLS itself.
# Null when 3x-ui serves plain HTTP and TLS is terminated by an upstream
# nginx.
certDomain = config.host."3x-ui".certDomain;
certMounts =
if certDomain == null then
[ ]
else
# LE cert mounted read-only so 3x-ui can terminate TLS itself.
# The 3x-ui settings table must point webCertFile / webKeyFile at
# /root/cert/fullchain.pem and /root/cert/key.pem.
map (f: "/var/lib/acme/${certDomain}/${f}:/root/cert/${f}:ro") [
"fullchain.pem"
"key.pem"
];
basePorts = [
# Local-only upstreams for the 3x-ui panel and subscription endpoint.
# The direct Xray inbound remains publicly reachable on 8443.
"127.0.0.1:2049:2049/tcp"
"127.0.0.1:2096:2096/tcp"
"0.0.0.0:8443:8443/tcp"
];
# VDS-only: nginx stream forwards host:443 → 127.0.0.1:15380 →
# container:443, so Xray sees its REALITY inbound on port 443.
realityPorts = lib.optional config.host."3x-ui".reality443Forwarding "127.0.0.1:15380:443/tcp";
in
{ {
# `host."3x-ui"` options are declared in modules/options.nix: they are set
# by modules/server and modules/vds, so this module cannot be the only place
# that knows they exist.
config = {
virtualisation = { virtualisation = {
podman = { podman = {
enable = true; enable = true;
@@ -58,14 +25,13 @@ in
"TZ" = "Europe/Moscow"; "TZ" = "Europe/Moscow";
}; };
volumes = [ volumes = [
"${panel}/cert/:/root/cert:rw" "${xlib.dirs.services-mnt-folder}/containers/3x-ui/cert/:/root/cert:rw"
"${panel}/db/:/etc/x-ui:rw" "${xlib.dirs.services-mnt-folder}/containers/3x-ui/db/:/etc/x-ui:rw"
] ];
++ certMounts;
log-driver = "journald"; log-driver = "journald";
# Adding a new inbound through the 3x-ui panel on a port outside extraOptions = [
# the 14380-15379 range requires extending basePorts and rebuilding. "--network=host"
ports = basePorts ++ realityPorts; ];
}; };
}; };
}; };
@@ -73,12 +39,21 @@ in
systemd = { systemd = {
services = { services = {
"podman-3xui_app" = { "podman-3xui_app" = {
serviceConfig.Restart = lib.mkOverride 90 "always"; serviceConfig = {
partOf = [ "podman-compose-3x-ui-root.target" ]; Restart = lib.mkOverride 90 "always";
wantedBy = [ "podman-compose-3x-ui-root.target" ];
}; };
partOf = [
"podman-compose-3x-ui-root.target"
];
wantedBy = [
"podman-compose-3x-ui-root.target"
];
};
# Update
"podman-update-3xui_app" = { "podman-update-3xui_app" = {
path = [ pkgs.podman ]; path = [
pkgs.podman
];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
TimeoutSec = 300; TimeoutSec = 300;
@@ -88,36 +63,63 @@ in
systemctl restart podman-3xui_app.service systemctl restart podman-3xui_app.service
''; '';
}; };
# Builds
# "podman-build-3xui_app" = {
# path = [
# pkgs.podman
# pkgs.git
# ];
# serviceConfig = {
# Type = "oneshot";
# TimeoutSec = 300;
# };
# script = ''
# cd /mnt/containers/3x-ui
# podman build -t compose2nix/3xui_app -f ./Dockerfile .
# '';
# };
}; };
# Starts/stops together with all 3x-ui compose resources. # Root service
# When started, this will automatically create all resources and start
# the containers. When stopped, this will teardown all resources.
targets."podman-compose-3x-ui-root" = { targets."podman-compose-3x-ui-root" = {
unitConfig.Description = "Root target generated by compose2nix."; unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
}; };
# timers."podman-update-3xui_app" = { timers."podman-update-3xui_app" = {
# wantedBy = [ "timers.target" ]; wantedBy = [ "timers.target" ];
# timerConfig = { timerConfig = {
# OnCalendar = "weekly"; OnCalendar = "weekly";
# Persistent = true; Persistent = true;
# }; };
# }; };
# Folders
tmpfiles.rules = [ tmpfiles.rules = [
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") "d /mnt 0755 root root -"
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root") "d /mnt/containers 0755 root root -"
(xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755" "d /mnt/services/containers 0755 root root -"
"root" "d /mnt/services/containers/3x-ui 0755 root root -"
"root" "d /mnt/services/containers/3x-ui/cert 0755 root root -"
) "d /mnt/services/containers/3x-ui/db 0755 root root -"
(xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root")
# Relabel panel dir for SELinux so containers can access it.
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
]; ];
}; };
# Enable container name DNS for all Podman networks. # Enable container name DNS for all Podman networks.
networking.firewall = { networking.firewall = {
allowedUDPPortRanges = [
{
from = 14380;
to = 15380;
}
];
allowedTCPPortRanges = [
{
from = 14380;
to = 15380;
}
];
interfaces = interfaces =
let let
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
@@ -126,5 +128,4 @@ in
"${matchAll}".allowedUDPPorts = [ 53 ]; "${matchAll}".allowedUDPPorts = [ 53 ];
}; };
}; };
};
} }
-116
View File
@@ -1,116 +0,0 @@
{
lib,
pkgs,
...
}:
let
# The image is built here rather than pulled: zaakirio/kokoro-ru is a
# Hugging Face repo, not a published OCI image, and its Russian G2P has to be
# driven through the repo's own ru_g2p.py.
#
# The build context goes through the store so the image is pinned to the
# config revision: edit a file, `nixos-rebuild`, and the unit below rebuilds
# and restarts. Reading the context off a checkout at runtime would leave the
# running container untraceable back to any config.
#
# runCommand rather than linkFarm: linkFarm entries are symlinks into other
# store paths, and `podman build` only mounts the context root, so every COPY
# fails with "copier: get: lstat ...: no such file or directory". Copying the
# bytes in leaves the context with no symlinks that escape its root.
source = pkgs.runCommand "kokoro-tts-source" { } ''
mkdir -p "$out"
cp -L ${./kokoro-tts/Dockerfile} "$out/Dockerfile"
cp -L ${./kokoro-tts/app.py} "$out/app.py"
cp -L ${./kokoro-tts/fetch_assets.py} "$out/fetch_assets.py"
cp -L ${./kokoro-tts/requirements.txt} "$out/requirements.txt"
'';
image = "localhost/kokoro-tts:latest";
# Unchanged from the silero module, so whatever already points at
# http://127.0.0.1:9898/v1 keeps working without edits.
hostPort = 9898;
containerPort = 8000;
in
{
config = {
virtualisation = {
podman = {
enable = true;
autoPrune = {
enable = true;
flags = [ "--all" ];
};
dockerCompat = true;
};
oci-containers = {
backend = "podman";
containers.kokoro-tts = {
image = image;
ports = [
"127.0.0.1:${toString hostPort}:${toString containerPort}"
];
environment = {
# Inference is CPU-bound and already threaded inside torch. Measured
# on a 24-logical-core host: median end-to-end latency for a 5.6 s
# utterance was 1.203 s at 4 threads, 0.979 s at 12, 0.980 s at 16
# and 1.87 s at 24, so the useful ceiling is the physical core count
# and oversubscribing it roughly doubles the wait. These three must
# stay equal to the Dockerfile ENV and the app.py default: whichever
# of the three is set wins over the others.
KOKORO_THREADS = "12";
OMP_NUM_THREADS = "12";
MKL_NUM_THREADS = "12";
TZ = "Europe/Moscow";
};
# No volumes: the checkpoints, the acute-aware espeak data and
# ruaccent's ONNX models are all baked into the image, so the
# container needs neither a host directory nor the network to start.
log-driver = "journald";
};
};
};
systemd = {
services = {
# Runs before the container. BuildKit caches the expensive layers, so
# on every boot after the first this is a no-op that still verifies the
# image exists.
"podman-build-kokoro-tts" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# First build pulls torch wheels plus ~700 MB of weights.
TimeoutSec = 3600;
};
script = ''
podman build -t ${image} ${source}
'';
wantedBy = [ "multi-user.target" ];
};
"podman-kokoro-tts" = {
# The image does not exist until the build above ran, and a `latest`
# tag must be re-pulled on rebuild, so ordering has to be explicit.
after = [ "podman-build-kokoro-tts.service" ];
requires = [ "podman-build-kokoro-tts.service" ];
serviceConfig.Restart = lib.mkOverride 90 "always";
# Auto-start disabled: start manually with `systemctl start podman-kokoro-tts`.
wantedBy = [ ];
};
};
};
};
}
-68
View File
@@ -1,68 +0,0 @@
# Fully qualified on purpose: NixOS ships a podman registries.conf without
# unqualified-search-registries, so a bare "python:3.12-slim-bookworm" fails to
# resolve before the build even starts.
FROM docker.io/library/python:3.12-slim-bookworm
# Pinned, not "main": a rebuild that only touched the Nix module must not
# silently pick up different weights. Bump these deliberately.
ARG KOKORO_RU_REPO=zaakirio/kokoro-ru
ARG KOKORO_RU_REVISION=d649c57b239b18c4c384378127cbf01dba039bc1
# Trim to "sveta" to halve the image: masha shares her checkpoint and dima is
# a second 327 MB one.
ARG KOKORO_RU_VOICES=sveta,masha,dima
# Thread counts, not a guess: see app.py THREADS. 12 was the measured plateau on
# a 24-logical-core host, and 24 was ~2x worse. Must stay equal to the Nix
# module's environment.environment, which wins over this ENV.
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
HF_HUB_DISABLE_TELEMETRY=1 \
HF_HUB_DISABLE_SYMLINKS_WARNING=1 \
KOKORO_RU_REPO=${KOKORO_RU_REPO} \
KOKORO_RU_REVISION=${KOKORO_RU_REVISION} \
KOKORO_RU_VOICES=${KOKORO_RU_VOICES} \
KOKORO_MODEL_DIR=/app/kokoro-ru \
KOKORO_THREADS=12 \
OMP_NUM_THREADS=12 \
MKL_NUM_THREADS=12 \
TZ=Europe/Moscow
WORKDIR /app
# libgomp1 is torch's OpenMP runtime. espeak-ng comes from the espeakng-loader
# wheel rather than the distro package because the model needs its own
# recompiled ru_dict, and libsndfile is absent because WAV/PCM are written with
# stdlib `wave` while every other format goes through imageio-ffmpeg.
RUN apt-get update \
&& apt-get install -y --no-install-recommends libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# CPU-only torch from its own index: the default PyPI wheel drags in ~2.5 GB of
# CUDA libraries for a machine that has no GPU.
RUN pip install --index-url https://download.pytorch.org/whl/cpu torch
COPY requirements.txt ./
RUN pip install -r requirements.txt
# fetch_assets.py is copied on its own and app.py only after the snapshot, never
# as one COPY. A single COPY would tie the 639 MB download to the application
# source: any edit to app.py would invalidate this layer and refetch every
# checkpoint as hundreds of anonymous, rate-limited requests.
COPY fetch_assets.py ./
# Bakes the checkpoints, the acute-aware espeak data and ruaccent's ONNX models
# into the layer, which is what lets the container start with no network and no
# writable volume.
RUN python fetch_assets.py
COPY app.py ./
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=4)"]
# No workers: the model is a shared in-process singleton, so a second worker
# would only mean a second copy of ~2 GB of weights.
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
-585
View File
@@ -1,585 +0,0 @@
"""OpenAI-compatible TTS API backed by zaakirio/kokoro-ru.
The model itself is language-blind: phoneme ids in, 24 kHz audio out. All the
Russian lives in the G2P front-end, and the one that matters is kokoro-ru's own
`ru_g2p.py` — RUAccent resolves lexical stress, ё and homographs, then an
acute-aware espeak-ng phonemizer turns that into IPA. Stock misaki Russian is
espeak-only and gets stress wrong often enough that the model reads as
non-native (measured 27% vs 22% round-trip WER, per the model card).
So: text -> RuG2P.phonemize -> KModel(ipa, voicepack[len(ipa) - 1]) -> waveform.
Endpoints
POST /v1/audio/speech OpenAI text-to-speech
POST /v1/audio/speech/stream same, but mp3/opus emitted while synthesising
GET /v1/models OpenAI model list
GET /v1/voices voice inventory (extension, not part of OpenAI)
GET /healthz readiness, 503 until the model is loaded
"""
from __future__ import annotations
import io
import logging
import os
import queue
import re
import subprocess
import sys
import threading
import wave
from contextlib import asynccontextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, Literal
import numpy as np
from fastapi import FastAPI
from fastapi.responses import JSONResponse, Response, StreamingResponse
from pydantic import BaseModel, ConfigDict, Field
if TYPE_CHECKING: # torch is imported lazily so /healthz answers during boot
import torch
MODEL_ID = "kokoro-ru"
SAMPLE_RATE = 24000
MODEL_DIR = Path(os.environ.get("KOKORO_MODEL_DIR", "/app/kokoro-ru"))
DEFAULT_VOICE = os.environ.get("KOKORO_DEFAULT_VOICE", "sveta")
# Measured on the host this was tuned for (Ryzen AI 9 HX 370, 24 logical cores):
# median end-to-end latency for a 5.6 s utterance was 1.203 s @ 4 threads,
# 1.066 s @ 8, 0.979 s @ 12, 0.980 s @ 16, then 1.87 s @ 24. The gain stops at
# the physical core count and SMT oversubscription costs ~2x, so cap instead of
# trusting os.cpu_count(), which reports logical CPUs. Override on other hosts.
THREADS = int(os.environ.get("KOKORO_THREADS", min(12, os.cpu_count() or 4)))
# 2026-07-29, when the kokoro-ru revision we pin was published. Clients that
# cache on this treat any change as a new model, so it must stay stable.
MODEL_CREATED = 1785353253
# voice -> (checkpoint stem, gender). The checkpoint carries the timbre and the
# voicepack the identity, which is why sveta and masha share one file.
VOICE_SPECS: dict[str, tuple[str, str]] = {
"sveta": ("kokoro-ru-v2-base", "female"),
"masha": ("kokoro-ru-v2-base", "female"),
"dima": ("kokoro-ru-v2-dima", "male"),
}
# Clients that ship the OpenAI voice list (alloy, nova, echo, ...) send those
# names unless the user overrides them, so map them onto the three we have.
VOICE_ALIASES: dict[str, str] = {
"alloy": "sveta",
"ash": "sveta",
"ballad": "sveta",
"verse": "sveta",
"marin": "sveta",
"coral": "masha",
"sage": "masha",
"shimmer": "masha",
"cedar": "masha",
"echo": "dima",
"fable": "dima",
"onyx": "dima",
}
CONTENT_TYPES = {
"wav": "audio/wav",
"mp3": "audio/mpeg",
"opus": "audio/ogg",
"aac": "audio/aac",
"flac": "audio/flac",
"pcm": "audio/pcm",
}
# Everything except wav and pcm goes through ffmpeg; those two are byte-exact
# from the stdlib and need no encoder at all.
FFMPEG_ARGS = {
"mp3": ["-c:a", "libmp3lame", "-q:a", "2"],
"opus": ["-c:a", "libopus", "-b:a", "64k"],
"aac": ["-c:a", "aac", "-b:a", "128k"],
"flac": ["-c:a", "flac"],
}
FFMPEG_CONTAINERS = {"mp3": "mp3", "opus": "ogg", "aac": "adts", "flac": "flac"}
# Kokoro's Albert context is 510 tokens and KModel.forward asserts
# len(ids) + 2 <= 510, so 508 phonemes is the hard ceiling per forward pass.
MAX_PHONEMES = 508
# Roughly 300 characters of Russian lands near 400 phonemes, comfortably under
# the ceiling, and keeps a chunk short enough that a bad sentence is a short
# chunk.
CHUNK_CHARS = 300
# Silence inserted between chunks. Without it the concatenation clicks at every
# boundary because each forward pass starts and ends on a zero crossing.
CHUNK_GAP_S = 0.08
_SENTENCE_SPLIT = re.compile(r"(?<=[.!?…])\s+")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger("kokoro-ru")
def split_text(text: str, budget: int = CHUNK_CHARS) -> list[str]:
"""Split into sentence-bounded chunks, hard-cutting only as a last resort.
Phonemizing per sentence rather than per paragraph keeps RUAccent's stress
decisions local and gives the model a reset point at every full stop.
"""
chunks: list[str] = []
current = ""
for sentence in _SENTENCE_SPLIT.split(text.strip()):
sentence = sentence.strip()
while len(sentence) > budget:
if current:
chunks.append(current)
current = ""
chunks.append(sentence[:budget])
sentence = sentence[budget:].strip()
if not sentence:
continue
if len(current) + len(sentence) + 1 > budget:
# Guarded: when the first sentence fills the budget exactly, or the
# previous one was hard-cut down to nothing, `current` is empty and
# a bare append would queue a zero-length chunk.
if current:
chunks.append(current)
current = sentence
else:
current = f"{current} {sentence}".strip()
if current:
chunks.append(current)
return chunks
def split_phonemes(ps: str, limit: int = MAX_PHONEMES) -> list[str]:
"""Cut an over-long phoneme string on word boundaries."""
if len(ps) <= limit:
return [ps]
parts: list[str] = []
rest = ps
while len(rest) > limit:
cut = rest.rfind(" ", 0, limit)
if cut <= 0:
cut = limit
parts.append(rest[:cut].strip())
rest = rest[cut:].strip()
if rest:
parts.append(rest)
return [part for part in parts if part]
class KokoroRu:
"""Loaded model plus the G2P front-end, behind a single inference lock."""
def __init__(self) -> None:
self._torch: torch | None = None
self._g2p = None
self._models: dict[str, torch.nn.Module] = {}
self._packs: dict[str, torch.Tensor] = {}
# The Albert encoder and the iSTFTNet decoder keep per-call scratch
# buffers; concurrent forwards on one model interleave into them. The
# model is fast enough on CPU that serialising is not the bottleneck.
self._lock = threading.Lock()
def load(self) -> None:
import torch
from kokoro import KModel
torch.set_num_threads(THREADS)
self._torch = torch
# RuG2P is imported from the baked snapshot, not installed, and it
# resolves espeak-data/ plus kokoro-config.json next to itself.
sys.path.insert(0, str(MODEL_DIR))
from ru_g2p import RuG2P
self._g2p = RuG2P(
espeak_data=MODEL_DIR / "espeak-data",
vocab_path=MODEL_DIR / "kokoro-config.json",
)
for stem in sorted({stem for stem, _ in VOICE_SPECS.values()}):
checkpoint = MODEL_DIR / f"{stem}.pth"
if not checkpoint.exists():
log.warning("checkpoint %s missing, voices using it stay unavailable", checkpoint)
continue
# repo_id is only used to build the default model filename; passing
# both config and model keeps it from touching the HF cache at all.
self._models[stem] = KModel(
repo_id=str(MODEL_DIR),
config=str(MODEL_DIR / "config.json"),
model=str(checkpoint),
).eval()
log.info("loaded checkpoint %s", checkpoint.name)
for name in VOICE_SPECS:
pack = MODEL_DIR / "voices" / f"{name}.pt"
if pack.exists():
self._packs[name] = torch.load(str(pack), map_location="cpu", weights_only=True)
if not self.available_voices():
raise RuntimeError(f"no usable voices under {MODEL_DIR}")
def available_voices(self) -> list[str]:
return [
name
for name in VOICE_SPECS
if name in self._packs and VOICE_SPECS[name][0] in self._models
]
def phonemes(self, text: str):
for chunk in split_text(text):
ps, _oov = self._g2p.phonemize(chunk)
ps = ps.strip()
if ps:
yield from split_phonemes(ps)
def iter_audio_chunks(self, text: str, voice: str, speed: float):
"""Yields float32 audio per phoneme chunk, silence gaps interleaved.
The engine lock is held for the whole iteration, so a caller that stops
consuming early releases synthesis for everyone else.
"""
torch = self._torch
assert torch is not None, "synthesize() before load()"
stem, _gender = VOICE_SPECS[voice]
model = self._models[stem]
pack = self._packs[voice]
gap = np.zeros(int(CHUNK_GAP_S * SAMPLE_RATE), dtype=np.float32)
with self._lock:
for index, ps in enumerate(self.phonemes(text)):
# The style vector is picked by phoneme-string length, which is
# why the model sounds deterministic for identical text.
style = pack[len(ps) - 1]
# The packs ship as [510, 256]; KModel wants a batch of one.
if style.dim() == 1:
style = style.unsqueeze(0)
if index:
yield gap
yield np.asarray(
model(ps, style, speed, return_output=True).audio,
dtype=np.float32,
).reshape(-1)
def synthesize(self, text: str, voice: str, speed: float) -> np.ndarray:
chunks = list(self.iter_audio_chunks(text, voice, speed))
if not chunks:
return np.zeros(0, dtype=np.float32)
return np.concatenate(chunks)
def encode(audio: np.ndarray, fmt: str) -> bytes:
clipped = np.clip(audio, -1.0, 1.0)
if fmt == "pcm":
# OpenAI's pcm is raw signed 16-bit little-endian mono at 24 kHz.
return (clipped * 32767.0).astype("<i2").tobytes()
buffer = io.BytesIO()
with wave.open(buffer, "wb") as out:
out.setnchannels(1)
out.setsampwidth(2)
out.setframerate(SAMPLE_RATE)
out.writeframes((clipped * 32767.0).astype("<i2").tobytes())
wav = buffer.getvalue()
if fmt == "wav":
return wav
import imageio_ffmpeg
command = [
imageio_ffmpeg.get_ffmpeg_exe(),
"-hide_banner",
"-loglevel",
"error",
"-i",
"pipe:0",
"-ar",
str(SAMPLE_RATE),
"-ac",
"1",
*FFMPEG_ARGS[fmt],
"-f",
FFMPEG_CONTAINERS[fmt],
"pipe:1",
]
done = subprocess.run(command, input=wav, capture_output=True, check=False)
if done.returncode != 0:
raise RuntimeError(done.stderr.decode("utf-8", "replace").strip()[-400:])
return done.stdout
class StreamEncoder:
"""One long-lived ffmpeg per request: raw PCM in, encoded bytes out.
A single process is what keeps the container valid. Handing it the audio in
pieces as they are synthesised avoids any byte-level concatenation, whereas
encoding the pieces separately and joining the results would emit chained
Ogg for opus, which plenty of players reject.
"""
def __init__(self, fmt: str) -> None:
import imageio_ffmpeg
self._proc = subprocess.Popen(
[
imageio_ffmpeg.get_ffmpeg_exe(),
"-hide_banner",
"-loglevel",
"error",
"-f",
"s16le",
"-ar",
str(SAMPLE_RATE),
"-ac",
"1",
"-i",
"pipe:0",
*FFMPEG_ARGS[fmt],
"-f",
FFMPEG_CONTAINERS[fmt],
"pipe:1",
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self._blocks: queue.Queue[bytes | None] = queue.Queue()
self._reader = threading.Thread(target=self._pump, daemon=True)
self._reader.start()
def _pump(self) -> None:
assert self._proc.stdout is not None
while True:
block = self._proc.stdout.read(8192)
if not block:
break
self._blocks.put(block)
self._blocks.put(None)
def push(self, audio: np.ndarray) -> None:
assert self._proc.stdin is not None
clipped = np.clip(audio, -1.0, 1.0)
self._proc.stdin.write((clipped * 32767.0).astype("<i2").tobytes())
self._proc.stdin.flush()
def drain(self) -> Iterator[bytes]:
"""Yields whatever ffmpeg has already emitted, without waiting for more."""
while True:
try:
block = self._blocks.get_nowait()
except queue.Empty:
return
if block is None:
return
yield block
def finish(self) -> Iterator[bytes]:
assert self._proc.stdin is not None
self._proc.stdin.close()
self._reader.join(timeout=120)
code = self._proc.wait(timeout=30)
error = self._proc.stderr.read().decode("utf-8", "replace").strip()[-400:]
if code != 0:
raise RuntimeError(error or f"ffmpeg exited with {code}")
yield from self.drain()
def abort(self) -> None:
if self._proc.poll() is None:
self._proc.kill()
engine = KokoroRu()
state: dict[str, str | None] = {"status": "loading", "error": None}
def boot() -> None:
try:
engine.load()
state["status"] = "ready"
log.info("ready: voices=%s", ", ".join(engine.available_voices()))
except Exception as exc:
state["status"] = "error"
state["error"] = f"{type(exc).__name__}: {exc}"
log.exception("model failed to load")
@asynccontextmanager
async def lifespan(_app: FastAPI):
# Off the event loop: loading pulls ~700 MB of weights and runs three ONNX
# sessions, and /healthz has to stay answerable while it happens.
threading.Thread(target=boot, name="kokoro-load", daemon=True).start()
yield
app = FastAPI(title="kokoro-ru OpenAI TTS", version="1.0.0", lifespan=lifespan)
Format = Literal["mp3", "opus", "aac", "flac", "wav", "pcm"]
class SpeechRequest(BaseModel):
# `protected_namespaces` silences pydantic's warning about the `model_`
# prefix; `extra="ignore"` absorbs the fields newer OpenAI clients add
# (instructions, the legacy `format` alias) without failing the request.
model_config = ConfigDict(extra="ignore", protected_namespaces=())
input: str = Field(min_length=1)
model: str = MODEL_ID
voice: str | None = None
response_format: Format = "wav"
speed: float | None = Field(default=None, ge=0.25, le=4.0)
class StreamSpeechRequest(SpeechRequest):
# Streaming needs a container that tolerates unknown length up front, so wav
# (whose header declares the final sizes) and the raw formats are out. mp3
# and opus emit bytes as they go, which is the whole point of the endpoint.
response_format: Literal["mp3", "opus"] = "mp3"
def fail(status: int, message: str, param: str | None = None, code: str | None = None) -> JSONResponse:
return JSONResponse(
status_code=status,
content={
"error": {
"message": message,
"type": "invalid_request_error" if status < 500 else "server_error",
"param": param,
"code": code,
}
},
)
def resolve_voice(requested: str | None) -> str | None:
name = (requested or DEFAULT_VOICE).strip().lower()
name = VOICE_ALIASES.get(name, name)
return name if name in engine.available_voices() else None
# response_model=None: the handler returns a Response subclass directly, and
# FastAPI would otherwise try to build a Pydantic model out of the union.
@app.post("/v1/audio/speech", response_model=None)
def create_speech(request: SpeechRequest) -> Response | JSONResponse:
if state["status"] != "ready":
return fail(503, f"model is not ready: {state['status']}", code="model_not_ready")
voice = resolve_voice(request.voice)
if voice is None:
available = ", ".join(engine.available_voices())
return fail(
400,
f"unknown voice {request.voice!r}; available: {available}",
param="voice",
code="unknown_voice",
)
try:
audio = engine.synthesize(request.input, voice, request.speed or 1.0)
except Exception as exc:
log.exception("synthesis failed")
return fail(500, f"synthesis failed: {exc}", code="synthesis_failed")
if audio.size == 0:
return fail(
400,
"input contains no speakable text for the Russian G2P",
param="input",
code="no_phonemes",
)
try:
payload = encode(audio, request.response_format)
except Exception as exc:
log.exception("encoding to %s failed", request.response_format)
return fail(500, f"encoding to {request.response_format} failed: {exc}", code="encoding_failed")
return Response(
content=payload,
media_type=CONTENT_TYPES[request.response_format],
headers={"model-id": MODEL_ID, "voice-id": voice},
)
# response_model=None for the same reason as create_speech above.
@app.post("/v1/audio/speech/stream", response_model=None)
def stream_speech(request: StreamSpeechRequest) -> Response | JSONResponse:
if state["status"] != "ready":
return fail(503, f"model is not ready: {state['status']}", code="model_not_ready")
voice = resolve_voice(request.voice)
if voice is None:
available = ", ".join(engine.available_voices())
return fail(
400,
f"unknown voice {request.voice!r}; available: {available}",
param="voice",
code="unknown_voice",
)
chunks = engine.iter_audio_chunks(request.input, voice, request.speed or 1.0)
try:
# Pulled before responding: once the status line is sent it cannot become
# a 400, and input with no speakable text has to keep failing that way.
first = next(chunks)
except StopIteration:
return fail(
400,
"input contains no speakable text for the Russian G2P",
param="input",
code="no_phonemes",
)
def body() -> Iterator[bytes]:
encoder = StreamEncoder(request.response_format)
try:
encoder.push(first)
yield from encoder.drain()
for chunk in chunks:
encoder.push(chunk)
yield from encoder.drain()
yield from encoder.finish()
except Exception:
log.exception("streaming synthesis failed")
raise
finally:
chunks.close()
encoder.abort()
return StreamingResponse(
body(),
media_type=CONTENT_TYPES[request.response_format],
headers={"model-id": MODEL_ID, "voice-id": voice},
)
@app.get("/v1/models")
def list_models() -> dict:
return {
"object": "list",
"data": [{"id": MODEL_ID, "object": "model", "created": MODEL_CREATED, "owned_by": "zaakirio"}],
}
@app.get("/v1/voices")
def list_voices() -> dict:
return {
"object": "list",
"ready": state["status"] == "ready",
"data": [
{"id": name, "object": "voice", "checkpoint": VOICE_SPECS[name][0], "gender": VOICE_SPECS[name][1]}
for name in engine.available_voices()
],
}
@app.get("/healthz")
def healthz() -> JSONResponse:
ready = state["status"] == "ready"
return JSONResponse(
status_code=200 if ready else 503,
content={
"status": state["status"],
"model": MODEL_ID,
"voices": engine.available_voices(),
"sample_rate": SAMPLE_RATE,
"error": state["error"],
},
)
@@ -1,82 +0,0 @@
"""Bake every kokoro-ru asset the server needs into the image.
Two things make a plain `FROM python` image useless for this model at runtime,
and both are fixed here at build time:
* kokoro-ru's checkpoints and its recompiled espeak-ng data live in the HF
cache by default, and the HF cache is part of the disposable container
layer, so every `podman run` would re-download ~700 MB.
* ruaccent writes its ONNX models, dictionaries and Koziev data into its own
`site-packages/ruaccent` directory. It only downloads when those files are
missing, so a single `load()` here means the runtime never touches the
network.
RuG2P resolves espeak-data/ and kokoro-config.json relative to ru_g2p.py, so
the snapshot layout has to stay flat inside KOKORO_MODEL_DIR.
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
from huggingface_hub import snapshot_download
REPO = os.environ.get("KOKORO_RU_REPO", "zaakirio/kokoro-ru")
# A commit, not a branch: "main" would silently change the weights under a
# rebuild that only touched an unrelated line of the Nix module.
REVISION = os.environ.get("KOKORO_RU_REVISION", "main")
DEST = Path(os.environ.get("KOKORO_MODEL_DIR", "/app/kokoro-ru"))
VOICES = [v.strip() for v in os.environ.get("KOKORO_RU_VOICES", "sveta,masha,dima").split(",") if v.strip()]
# sveta and masha share one checkpoint and differ only by voicepack, so the two
# female voices cost one 327 MB download, not two.
CHECKPOINTS = {
"sveta": "kokoro-ru-v2-base.pth",
"masha": "kokoro-ru-v2-base.pth",
"dima": "kokoro-ru-v2-dima.pth",
}
PATTERNS = [
# KModel reads config.json; RuG2P reads kokoro-config.json for the phoneme
# vocab. They are not the same file and both are required.
"config.json",
"kokoro-config.json",
"ru_g2p.py",
# Stock espeak-ng ru_dict ignores combining-acute stress marks, which is the
# one thing this whole front-end exists to fix. The model repo ships a
# recompiled dictsource; there is no substitute to fall back to.
"espeak-data/**",
*(CHECKPOINTS[v] for v in VOICES if v in CHECKPOINTS),
*(f"voices/{v}.pt" for v in VOICES),
]
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
DEST.mkdir(parents=True, exist_ok=True)
snapshot_download(
repo_id=REPO,
revision=REVISION,
allow_patterns=PATTERNS,
local_dir=str(DEST),
)
logging.info("kokoro-ru assets in %s at %s", DEST, REVISION)
missing = [name for name in VOICES if not (DEST / "voices" / f"{name}.pt").exists()]
if missing:
raise SystemExit(f"voice packs missing after download: {missing}")
# Warm ruaccent into site-packages so `load()` short-circuits at runtime.
from ruaccent import RUAccent
accent = RUAccent()
accent.load(omograph_model_size="turbo3.1", use_dictionary=True, tiny_mode=False)
logging.info("ruaccent warm: %s", accent.process_all("Здравствуйте, как ваши дела?"))
if __name__ == "__main__":
main()
@@ -1,21 +0,0 @@
# torch is installed separately in the Dockerfile from the CPU-only index;
# do not add it here or pip would pull the ~2.5 GB CUDA build over it.
#
# ru_g2p.py (from zaakirio/kokoro-ru) imports three things stock kokoro does not
# pull on its own: the espeak-ng backend of misaki, ruaccent for stress, and the
# phonemizer fork whose EspeakWrapper misaki drives.
kokoro==0.9.4
misaki[en]>=0.9.4
phonemizer-fork
espeakng-loader
ruaccent
# kokoro's Albert encoder and ruaccent's ONNX exports both go through
# transformers. ru_g2p.py shims token_type_ids for v5, so the floor is what
# matters, not the ceiling.
transformers>=4.46
fastapi
uvicorn
imageio-ffmpeg
numpy>=1.26,<3
-190
View File
@@ -1,190 +0,0 @@
{
config,
lib,
pkgs,
xlib,
...
}:
# Open WebUI — self-hosted AI chat UI, deployed here as a UI-client for
# external LLM APIs (OpenAI-compatible: OpenAI, OpenRouter, vLLM, LM Studio,
# GroqCloud, Mistral, etc.). Runs locally without bundled Ollama.
#
# Architecture mirrors modules/containers/{3x-ui,tape-rotation}.nix:
# - one container, one systemd unit + a root.target
# - data on /mnt/services/nodes/<host>/open-webui/data → /app/backend/data
# (see AGENTS.md §Подтверждённые инварианты #2 — guard chain is satisfied
# because mkServiceStorage already bind-mounts /mnt/services on boot)
# - host port bound to 127.0.0.1 only — the only ingress is the nginx
# vhost open.zeroq.su (no firewall exception, no public exposure).
# Same pattern as 3x-ui.nix:30-31 binding the panel to 127.0.0.1:2049.
#
# Secrets come from a single sops-encrypted dotenv file
# (format = "dotenv", key = "" → whole file). The owner creates the
# encrypted file with `sops modules/containers/secrets/open-webui.env`
# after filling the .example template next to it.
#
# Hard requirement (env.py:762 — SystemExit at startup):
# WEBUI_SECRET_KEY must be set when WEBUI_AUTH=true.
# Generate with: head -c 24 /dev/urandom | base64
#
# Reverse-proxy requirements (docs.openwebui.com/reference/https):
# - WEBUI_URL = public HTTPS URL (OAuth callbacks, internal links)
# - CORS_ALLOW_ORIGIN = same public URL (else WebSocket fails silently)
# - proxy_buffering off (else SSE streaming breaks markdown)
# - proxy_read_timeout ≥ 300s (LLM responses can run minutes)
# - WebSocket pass-through (Upgrade / Connection headers)
# All of the above are wired into modules/server/nginx.nix:open.zeroq.su.
let
panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/open-webui";
in
{
virtualisation = {
podman = {
enable = true;
autoPrune = {
enable = true;
flags = [ "--all" ];
};
dockerCompat = true;
};
oci-containers = {
backend = "podman";
containers."open-webui" = {
image = "ghcr.io/open-webui/open-webui:main";
environment = {
TZ = "Europe/Moscow";
# Container-internal port (also the upstream default).
PORT = "8080";
# Required when behind a public HTTPS URL — OAuth callbacks,
# share links and internal redirects resolve against this.
WEBUI_URL = "https://open.zeroq.su";
# Must exactly match WEBUI_URL or WebSocket connections fail
# silently (per upstream HTTPS docs). nginx (127.0.0.1) is the
# only allowed origin, so a single explicit URL is enough.
CORS_ALLOW_ORIGIN = "https://open.zeroq.su";
# Honour X-Forwarded-* headers from the reverse proxy.
FORWARDED_ALLOW_IPS = "127.0.0.1";
# Closed self-hosted: admin creates accounts manually after the
# first boot via WEBUI_ADMIN_* from the sops env file.
WEBUI_AUTH = "True";
ENABLE_SIGNUP = "False";
ENABLE_LOGIN_FORM = "True";
ENABLE_VERSION_UPDATE_CHECK = "False";
# Out of the box Open WebUI phones home to Scarf. The opt-outs
# below preserve the previous behaviour from the stub at
# modules/server/open-webui.nix (still in tree, commented out in
# modules/server/default.nix:41) until that file is removed.
ANONYMIZED_TELEMETRY = "False";
DO_NOT_TRACK = "True";
SCARF_NO_ANALYTICS = "True";
# No bundled providers. Owners wire OPENAI_API_KEY /
# OPENAI_API_BASE_URL / etc. either via the sops env file
# (see sops.secrets."open-webui-env" below) or interactively in
# Admin → Settings → Connections once WEBUI_AUTH=true. Empty
# base URL is intentional: an empty OPENAI_API_BASE_URL
# disables the default /ollama proxy and prevents the container
# from probing localhost:11434 on boot.
OLLAMA_BASE_URL = "";
OPENAI_API_BASE_URL = "";
};
# Mount the decrypted dotenv only when the sops file exists. Until
# the owner creates ./secrets/open-webui.env, the inline environment
# is the only source — and the container will refuse to start with
# WEBUI_SECRET_KEY="" (env.py:762 — SystemExit). The error message is
# the clear signal that the secret needs to be created.
# Path comes from the sops block below (`config.sops.secrets.<attr>.path`)
# rather than a hardcoded "/run/secrets/<attr>" — see invariant S1
# in docs/arch/invariants.md. Guards the sopsFile existence so the
# block is optional; the same predicate is what `sops.secrets` uses
# to decide whether to declare the attr at all, so `.path` is only
# read when the secret actually exists.
environmentFiles =
lib.optional (builtins.pathExists ./secrets/open-webui.env)
config.sops.secrets."open-webui-env".path;
volumes = [
"${panel}/data:/app/backend/data:rw"
];
log-driver = "journald";
# 127.0.0.1 only — the container is not exposed externally.
ports = [ "127.0.0.1:8080:8080/tcp" ];
};
};
};
# Enable container name DNS for all Podman networks (mirrors 3x-ui.nix:120-128).
networking.firewall.interfaces =
let
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
in
{
"${matchAll}".allowedUDPPorts = [ 53 ];
};
systemd = {
services = {
"podman-open-webui" = {
serviceConfig.Restart = lib.mkOverride 90 "always";
partOf = [ "podman-compose-open-webui-root.target" ];
wantedBy = [ "podman-compose-open-webui-root.target" ];
};
"podman-update-open-webui" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
TimeoutSec = 300;
};
script = ''
podman pull ghcr.io/open-webui/open-webui:main
systemctl restart podman-open-webui.service
'';
};
};
# Starts/stops together with the open-webui container.
targets."podman-compose-open-webui-root" = {
unitConfig.Description = "Root target for open-webui.";
wantedBy = [ "multi-user.target" ];
};
# Enable automatic image updates:
# systemd.timers."podman-update-open-webui" = {
# wantedBy = [ "timers.target" ];
# timerConfig = {
# OnCalendar = "weekly";
# Persistent = true;
# };
# };
tmpfiles.rules = [
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755"
"root"
"root"
)
(xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${panel}/data" "0755" "root" "root")
# Relabel panel dir for SELinux so containers can access it.
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
];
};
# sops secret is declared only when the encrypted file actually exists,
# so the flake still evaluates (and rebuilds apply) on a host that hasn't
# created the secret yet. Once ./secrets/open-webui.env is created and
# encrypted with `sops modules/containers/secrets/open-webui.env`, this
# condition becomes true and the secret is wired in.
#
# Hard requirement (env.py:762 — SystemExit at startup):
# WEBUI_SECRET_KEY must be present in the env file when WEBUI_AUTH=true.
sops.secrets = lib.optionalAttrs (builtins.pathExists ./secrets/open-webui.env) {
"open-webui-env" = {
# key = "" → decrypt the whole file, not a single key.
# format = "dotenv" → the file IS one .env ready for environmentFiles:
# every non-comment KEY=VALUE line lands in the container environment.
# After this module is wired the file is mounted at
# /run/secrets/open-webui-env (sops-nix default for this attr name).
key = "";
format = "dotenv";
sopsFile = ./secrets/open-webui.env;
mode = "0400";
};
};
}
+3 -12
View File
@@ -6,15 +6,6 @@
xlib, xlib,
... ...
}: }:
let
# Composite env file path shared by the generator
# (remnawave-env.service below) and the container's `environmentFiles`.
# Lifting to a single binding prevents the two copies from drifting
# apart in future edits — see invariant S1 in docs/arch/invariants.md.
# Note: this is NOT a sops materialization (it's written by a oneshot),
# so `config.sops.secrets.<...>.path` is not the right primitive here.
envFile = "/run/secrets/remnawave-env";
in
{ {
# Runtime # Runtime
virtualisation.podman = { virtualisation.podman = {
@@ -67,7 +58,7 @@ in
# "WEBHOOK_URL" = "https://your-webhook-url.com/endpoint"; # "WEBHOOK_URL" = "https://your-webhook-url.com/endpoint";
}; };
environmentFiles = [ environmentFiles = [
envFile "/run/secrets/remnawave-env"
]; ];
ports = [ ports = [
"3003:3003/tcp" "3003:3003/tcp"
@@ -135,14 +126,14 @@ in
User = "root"; User = "root";
}; };
script = '' script = ''
cat > ${envFile} <<EOF cat > /run/secrets/remnawave-env <<EOF
DATABASE_URL=$(cat ${config.sops.secrets.DATABASE_URL.path}) DATABASE_URL=$(cat ${config.sops.secrets.DATABASE_URL.path})
DATABASE_PASSWORD=$(cat ${config.sops.secrets.DATABASE_PASSWORD.path}) DATABASE_PASSWORD=$(cat ${config.sops.secrets.DATABASE_PASSWORD.path})
JWT_AUTH_SECRET=$(cat ${config.sops.secrets.JWT_AUTH_SECRET.path}) JWT_AUTH_SECRET=$(cat ${config.sops.secrets.JWT_AUTH_SECRET.path})
JWT_API_TOKENS_SECRET=$(cat ${config.sops.secrets.JWT_API_TOKENS_SECRET.path}) JWT_API_TOKENS_SECRET=$(cat ${config.sops.secrets.JWT_API_TOKENS_SECRET.path})
WEBHOOK_SECRET_HEADER=$(cat ${config.sops.secrets.WEBHOOK_SECRET_HEADER.path}) WEBHOOK_SECRET_HEADER=$(cat ${config.sops.secrets.WEBHOOK_SECRET_HEADER.path})
EOF EOF
chmod 600 ${envFile} chmod 600 /run/secrets/remnawave-env
''; '';
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
}; };
-10
View File
@@ -1,10 +0,0 @@
WEBUI_SECRET_KEY=ENC[AES256_GCM,data:l6USiQmMkMz/zniIebT35HfXxZI8qrhe6Cdl8hpT98c=,iv:Po9bova4dfiykl+ckH4v6DqzSJOgULx7ro3kXMFRvFI=,tag:7jurD14N7QDRHX5ruFDEeQ==,type:str]
WEBUI_ADMIN_EMAIL=ENC[AES256_GCM,data:EZgNXSpbpROz3TZRLaSQTQ==,iv:i98kChemam9nB3iCMwCTRYB69b2eUBy6QCoeZ3AjAP0=,tag:INnB1Zp9VA6M//yyAhRh3A==,type:str]
WEBUI_ADMIN_NAME=ENC[AES256_GCM,data:8l8dJ85p,iv:LltveatNlX4FEGmxhtYLYmviIvLK0xSMuVsk9DRRglw=,tag:OrTlnOLlQe03hoYTkaPotg==,type:str]
WEBUI_ADMIN_PASSWORD=ENC[AES256_GCM,data:PKfZQHiAa96vcGUCGigjXQ==,iv:WLb1mgCV3IJHnHcBvf4yAPiautgXqCC2L2bzt6i0t7U=,tag:nw78tvyUOYmGMunBwvIr+A==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4d3pNVlZEQS85d2R3WUZX\nKy9iOFZ4MjU2UkQwVHdobTlBY3l0MldONWlvCnU5dllobmtLQXlMM28xN0FSTmxD\nNnhmZVRwdnpaZ3NkZDVCWERBckZiQjgKLS0tIFBPeXZMNXRjZ3pBQUlndXB5MTBB\nMVdhSGJvZkE2VzZiZ2VxL0RKTDJ2aDQKsxlibeAoO74411VemXT+8UBG0JdemgHD\nVONIEp/VsbEJDWgDfSGhLaH4KN2hTsCtyhdkCU0FohgWB+xWyJz6MA==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age13l2gtk0nzr484zprp7e0pkrt0ne0j4asyn2pjmlaw73nte7t7d8q4sqtxm
sops_lastmodified=2026-10-07T09:32:44Z
sops_mac=ENC[AES256_GCM,data:SSHgEEc3u2Zf13q5W4LD7bkrVlQTzLIYiZWXhBiDS6CjC4fUZcJq99OTSTNixzqpxSdnjeRtmzA6d6vGNfxvEOmsE1f4hBNm9ps0RHU4yLfr5vQG9Ff973uDwDU+JMqP3aMU+xpUuPkhW0zRpeST+w0thuPtjzhR2z/A2yPvYzU=,iv:5/cfD51eK0R9cGsr4wZu6CnwEdMjP0CYj3CM7+X4XQg=,tag:1FRcAULHG+XzmRiTMK8aWQ==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.13.3
@@ -1,17 +0,0 @@
JWT_SECRET=ENC[AES256_GCM,data:+ut+3v4KrckbDT5m85JhQd8x2ayF55Uy5FiEw8qTJoBgz7Zz+HprhxPB09RDd6CX11SrcsDcf6vW3wOE7IdeQA==,iv:c3GqspoQH2+2NQvsqip3bs4XW1PWSZtK+l7HzE83Qj8=,tag:hDqFgPa8gYLqPeA0svGWfw==,type:str]
ADMIN_PASSWORD=ENC[AES256_GCM,data:UxcSEO7opTme9DR4XM3/FQ==,iv:nSpFt7rVp0K+hAc3aAoorw5XDMNK0V+zeBw4GHwTsOs=,tag:fQ1u/WtlVfEhc7fZnLnboQ==,type:str]
APP_URL=ENC[AES256_GCM,data:OJAv0C1DHYbFltgXmcSG/s97Lf3qJovkcEsPA9Xr,iv:Fw9Mh/+dYgah+/OWPBtRRXkO42KXWvKIuylyXfcaRK8=,tag:a6A8xS9aRyjvEfZvSxgMuQ==,type:str]
CORS_ORIGINS=ENC[AES256_GCM,data:z4MvIbQcvk+aUaME/llV7rWaDtCFYIT0nVGtlD8j,iv:oAL5NcWOfez+vVbKoibUIOagePROKW+4QV81sK+Cets=,tag:+QftIwvmTADEFMEz+ZCh4A==,type:str]
SMTP_HOST=
SMTP_PORT=ENC[AES256_GCM,data:QnI=,iv:PQwsVoOnLmTrnpUTaQAEOX3VG2hTLm/qnZjwIOL9kac=,tag:SZxCnlr0qTxH65bZ53rvQQ==,type:str]
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=ENC[AES256_GCM,data:TaHhXO7WVUzywpUxTr5l+IG7QfXY,iv:gLqOSZBBzC9v/BT+r+ENLjApTmLsra2jDbenJLHn4AY=,tag:HCmGtfnVIjDktQpTtUbc9A==,type:str]
NOTIFY_EMAIL=
TAPE_LABEL_REGEX=
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnb283UjRSRU1SYjBxZWlz\nOWw2cXM2TTU2QmdWUG5nUU9vWVZhUDZoelJRCi9McmV0Q05hNVpXSllsbUYwdkEw\nTnU3OWRCcFhrQzg4blhuRFJjdDVkUFkKLS0tIEt4Nzc2ZWtOL1VQQzVObzZWYURu\nWFUwVWp5OUhDME0xVlBRS3psdVBSd0EKsy77QR7CveXQdKlo+JeNSaNpnUh//AoP\nV+hbUSIj05Ws20rr9uk8uTDnjnc91r2vxGWxznXf6M9putZfARBdfQ==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age13l2gtk0nzr484zprp7e0pkrt0ne0j4asyn2pjmlaw73nte7t7d8q4sqtxm
sops_lastmodified=2026-09-24T13:11:03Z
sops_mac=ENC[AES256_GCM,data:xJO2u9jQM8XiVYekVvwN+iv3megGpf80F1ANib9Kro/kgvTQUZU14jmku8OjfRtjrFsM9b/cBr+ml0Z+MSknmwtR4D3mfRIa0yFfqmS/VZJs8SrH2+c/a8kYGhDNcWgAbx+u/tZB8q0QrQsbDYmN8yvsNwWi2ixMLKlv2thPIbA=,iv:CEgU5499Gr0gD+M5iSYJ315r7RU9RWKKapXywVCQivo=,tag:9YTO7K/zsINSOXfR6PaG8A==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.13.3
-190
View File
@@ -1,190 +0,0 @@
{
config,
lib,
pkgs,
xlib,
...
}:
# TapeRotation — web app for tracking and rotation of backup tape
# cartridges. https://github.com/ElizarovEugene/TapeRotation
#
# Podman adaptation of the upstream docker-compose deployment. Two
# containers on a shared "taperotation_default" network (mirrors the
# compose project network):
# - taperotation-backend: FastAPI/uvicorn on :8001, SQLite at /data,
# file attachments at /app/uploads
# - taperotation-frontend: nginx serving the built React app on :80,
# proxying /api to http://backend:8001
# The backend container gets a static IP on the shared network and the
# frontend maps "backend" → that IP via --add-host, because this host's
# CoreDNS service owns port 53 on every interface: the podman network DNS
# plugin (aardvark-dns) cannot bind on the network gateway, so a network
# with dns_enabled would refuse to attach containers.
#
# Published host port 5174 → container:80 for the web UI. Keep it out
# of networking.firewall like the other panel ports and front it with an
# nginx vhost, e.g. in server/nginx.nix:
# { domain = "tape-rotation.zeroq.su"; port = 5174; }
# and set APP_URL / CORS_ORIGINS in the sops-encrypted env file.
#
# Instance config lives in one sops-encrypted .env file (mirrors the
# upstream .env.example, sops-nix format = "dotenv", key = "" → whole
# file): sops modules/containers/secrets/tape-rotation.env
# On first boot the admin account is created from ADMIN_USERNAME /
# ADMIN_PASSWORD from that file.
let
panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/tape-rotation";
in
{
virtualisation = {
podman = {
enable = true;
autoPrune = {
enable = true;
flags = [ "--all" ];
};
dockerCompat = true;
};
oci-containers = {
backend = "podman";
containers = {
"taperotation-backend" = {
image = "docker.io/elizaroveugene/taperotation-backend:latest";
environment = {
"DATABASE_URL" = "sqlite:////data/taperotation.db";
"JWT_EXPIRE_MINUTES" = "480";
"ADMIN_USERNAME" = "admin";
"ADMIN_LANGUAGE" = "en";
"NOTIFY_DAYS_BEFORE" = "7";
"TZ" = "Europe/Moscow";
};
# Path resolved from the sops block at the bottom of this file —
# see invariant S1 in docs/arch/invariants.md.
environmentFiles = [ config.sops.secrets."tape-rotation-env".path ];
volumes = [
"${panel}/db:/data:rw"
"${panel}/uploads:/app/uploads:rw"
];
log-driver = "journald";
extraOptions = [
"--network=taperotation_default"
# Static IP the frontend reaches "backend" at (see --add-host
# in the frontend container; network DNS is disabled).
"--ip=10.89.0.10"
];
};
"taperotation-frontend" = {
image = "docker.io/elizaroveugene/taperotation-frontend:latest";
ports = [
"0.0.0.0:5174:80/tcp"
];
log-driver = "journald";
extraOptions = [
"--network=taperotation_default"
# Baked-in nginx upstream is http://backend:8001; resolve it via
# /etc/hosts since the network has no DNS plugin.
"--add-host=backend:10.89.0.10"
];
};
};
};
};
# Enable container name DNS for all Podman networks.
networking.firewall.interfaces =
let
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
in
{
"${matchAll}".allowedUDPPorts = [ 53 ];
};
systemd = {
services = {
"podman-taperotation-backend" = {
serviceConfig.Restart = lib.mkOverride 90 "always";
after = [ "podman-network-taperotation_default.service" ];
requires = [ "podman-network-taperotation_default.service" ];
partOf = [ "podman-compose-tape-rotation-root.target" ];
wantedBy = [ "podman-compose-tape-rotation-root.target" ];
};
"podman-taperotation-frontend" = {
serviceConfig.Restart = lib.mkOverride 90 "always";
after = [
"podman-network-taperotation_default.service"
"podman-taperotation-backend.service"
];
requires = [ "podman-network-taperotation_default.service" ];
partOf = [ "podman-compose-tape-rotation-root.target" ];
wantedBy = [ "podman-compose-tape-rotation-root.target" ];
};
"podman-network-taperotation_default" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "podman network rm -f taperotation_default";
};
script = ''
# Always (re)create the stack network: the host's CoreDNS owns :53
# on every interface, so the network DNS plugin (aardvark-dns)
# can't bind on the gateway → --disable-dns. --subnet backs the
# backend's static IP. Recreate-on-start also self-heals after a
# `podman system prune` removed the (temporarily unused) network.
podman network rm -f taperotation_default >/dev/null 2>&1 || true
podman network create --disable-dns --subnet=10.89.0.0/24 taperotation_default
'';
partOf = [ "podman-compose-tape-rotation-root.target" ];
wantedBy = [ "podman-compose-tape-rotation-root.target" ];
};
"podman-update-taperotation" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
TimeoutSec = 300;
};
script = ''
podman pull docker.io/elizaroveugene/taperotation-backend:latest
podman pull docker.io/elizaroveugene/taperotation-frontend:latest
systemctl restart podman-taperotation-backend.service podman-taperotation-frontend.service
'';
};
};
# Starts/stops together with all TapeRotation containers.
targets."podman-compose-tape-rotation-root" = {
unitConfig.Description = "Root target generated by compose2nix.";
wantedBy = [ "multi-user.target" ];
};
# Enable automatic image updates:
# systemd.timers."podman-update-taperotation" = {
# wantedBy = [ "timers.target" ];
# timerConfig = {
# OnCalendar = "weekly";
# Persistent = true;
# };
# };
tmpfiles.rules = [
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755"
"root"
"root"
)
(xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" "${panel}/uploads" "0755" "root" "root")
# Relabel panel dir for SELinux so containers can access it.
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
];
};
sops.secrets."tape-rotation-env" = {
# key = "" → decrypt the whole file, not a single key.
# format = "dotenv" → the file IS one .env ready for environmentFiles:
# every non-comment KEY=VALUE line lands in the container environment.
key = "";
format = "dotenv";
sopsFile = ./secrets/tape-rotation.env;
mode = "0400";
};
}
+25 -24
View File
@@ -1,60 +1,61 @@
{ inputs, ... }@flakeContext: { inputs, ... }@flakeContext:
let let
# NixOS-only modules. termux runs nix-on-droid (its own module system,
# class = "nixOnDroid"): options like services.*, users.*, sops.*, disko.*
# and nixpkgs.overlays (flake assertion) do not exist there.
#
# `xlib` arrives as a module argument (see lib/mkSystem.nix) and is plain
# data, not a module option, so nothing here has to declare or set it.
defaultModule = defaultModule =
{ {
config,
deviceType,
lib, lib,
xlib, xlib,
... ...
}: }:
{ {
imports = imports = with inputs; [
with inputs;
[
./essentials ./essentials
./options.nix
./users.nix ./users.nix
./options.nix
(./. + "/${deviceType}") # specific modules
home-manager.nixosModules.home-manager # home-manager module home-manager.nixosModules.home-manager # home-manager module
# nix-index-database.nixosModules.nix-index # nix-index module # nix-index-database.nixosModules.nix-index # nix-index module
grub2-themes.nixosModules.default # grub2 themes module grub2-themes.nixosModules.default # grub2 themes module
sops-nix.nixosModules.sops # sops module sops-nix.nixosModules.sops # sops module
justray.nixosModules.default
self.homeConfigurations.default.nixosModule # default homeConfigurations self.homeConfigurations.default.nixosModule # default homeConfigurations
disko.nixosModules.disko # disko module disko.nixosModules.disko # disko module
] ];
# desktop class: primary/secondary
++ lib.optional xlib.isDesktop ./desktop
# device-type module dir; "minimal" has no extra modules
++ lib.optional (!xlib.isDesktop && xlib.device.type != "minimal") (./. + "/${xlib.device.type}");
nixpkgs.overlays = with inputs; [ nixpkgs.overlays = with inputs; [
self.nixosOverlays.default self.nixosOverlays.default
]; ];
networking.hostName = lib.mkDefault xlib.device.hostname; _module.args = {
inputs = inputs;
xlib = config.xlib;
}; };
strictModule = };
publicModule =
{ {
config,
lib,
xlib, xlib,
... ...
}: }:
{ {
imports = [ imports = with inputs; [
# ./essentials ./essentials
# ./users.nix ./users.nix
./options.nix ./options.nix
(./. + "/${xlib.device.type}")
# sops-nix.nixosModules.sops disko.nixosModules.disko # disko module
sops-nix.nixosModules.sops # sops module
]; ];
_module.args = {
inputs = inputs;
xlib = config.xlib;
};
}; };
in in
{ {
nixosModules = { nixosModules = {
default = defaultModule; default = defaultModule;
strict = strictModule; public = publicModule;
}; };
} }
+16 -49
View File
@@ -3,7 +3,6 @@
inputs, inputs,
lib, lib,
pkgs, pkgs,
xlib,
... ...
}: }:
{ {
@@ -12,54 +11,6 @@
./theming.nix ./theming.nix
]; ];
# Things every desktop host has in common
hardware.bluetooth.enable = true;
i18n.extraLocaleSettings = {
LC_ADDRESS = "ru_RU.UTF-8";
LC_IDENTIFICATION = "ru_RU.UTF-8";
LC_MEASUREMENT = "ru_RU.UTF-8";
LC_MONETARY = "ru_RU.UTF-8";
LC_NAME = "ru_RU.UTF-8";
LC_NUMERIC = "ru_RU.UTF-8";
LC_PAPER = "ru_RU.UTF-8";
LC_TELEPHONE = "ru_RU.UTF-8";
LC_TIME = "ru_RU.UTF-8";
};
networking = {
networkmanager.enable = true;
firewall.enable = false;
};
security.rtkit.enable = true;
services = {
syncthing = {
enable = true;
systemService = true;
configDir = "${xlib.dirs.user-storage}/persist/Syncthing/${config.system.name}";
dataDir = "${xlib.dirs.user-home}";
group = "users";
user = "${xlib.device.username}";
};
thermald.enable = true;
xserver = {
enable = true;
xkb = {
layout = "us,ru";
variant = "";
# options = "grp:alt_shift_toggle";
};
};
libinput.enable = true;
colord.enable = true;
printing = {
enable = true;
cups-pdf.enable = true;
};
};
boot = { boot = {
plymouth = { plymouth = {
enable = true; enable = true;
@@ -102,6 +53,22 @@
steam.enable = true; steam.enable = true;
xwayland.enable = true; xwayland.enable = true;
}; };
services = {
xserver = {
enable = true;
xkb = {
layout = "us,ru";
variant = "";
# options = "grp:alt_shift_toggle";
};
};
libinput.enable = true;
colord.enable = true;
printing = {
enable = true;
cups-pdf.enable = true;
};
};
# environment = { # environment = {
# systemPackages = [ # systemPackages = [
# pkgs.pcbu-desktop # pkgs.pcbu-desktop
+1
View File
@@ -6,5 +6,6 @@
./kde.nix ./kde.nix
# ./gnome.nix # ./gnome.nix
# ./noctalia.nix # ./noctalia.nix
# ./xfce.nix
]; ];
} }
+36
View File
@@ -0,0 +1,36 @@
{
config,
lib,
pkgs,
...
}:
{
services.xserver.displayManager.lightdm.enable = true;
#services.displayManager.defaultSession = "lomiri";
# services.xserver.desktopManager.budgie.enable = true;
#services.xserver.displayManager.lightdm.greeters.lomiri.enable= true;
#services.desktopManager.lomiri.enable = true;
#-services.xserver.desktopManager.mate.enable = true;
#-services.xserver.desktopManager.lxqt.enable = true;
# services.xserver.desktopManager.lumina.enable = true;
# services.xserver.desktopManager.cde.enable = true;
# services.xserver.desktopManager.cinnamon.enable = true;
# services.xserver.desktopManager.enlightenment.enable = true;
# services.desktopManager.cosmic.xwayland.enable = true;
# services.desktopManager.cosmic.enable = true;
services.xserver = {
enable = true;
desktopManager = {
#xterm.enable = false;
xfce.enable = true;
xfce.enableWaylandSession = true;
};
};
#- services.xserver.desktopManager.pantheon.enable = true;
#- services.pantheon.apps.enable = true;
}
-1
View File
@@ -7,7 +7,6 @@
./packages.nix ./packages.nix
./services.nix ./services.nix
./settings.nix ./settings.nix
./ssh.nix
./systemd-routines.nix ./systemd-routines.nix
./shell.nix ./shell.nix
]; ];
+6 -8
View File
@@ -2,9 +2,13 @@
config, config,
pkgs, pkgs,
inputs, inputs,
xlib,
... ...
}: }:
let
master = import inputs.nixpkgs-master {
system = "x86_64-linux";
};
in
{ {
environment = { environment = {
systemPackages = with pkgs; [ systemPackages = with pkgs; [
@@ -101,9 +105,6 @@
# Test # Test
rgx rgx
net-tools net-tools
usbtree
iperf3
glow
# lazydocker # lazydocker
# dtop # dtop
# framework-tool-tui # framework-tool-tui
@@ -112,9 +113,6 @@
environment.variables.EDITOR = "fresh"; environment.variables.EDITOR = "fresh";
programs = { programs = {
# nix-ld.enable = true; # nix-ld.enable = true;
justray = {
enable = true;
};
nano = { nano = {
enable = true; enable = true;
nanorc = '' nanorc = ''
@@ -187,7 +185,7 @@
enable = true; enable = true;
config = { config = {
user = { user = {
name = xlib.device.username; name = "oqyude";
email = "oqyude@gmail.com"; email = "oqyude@gmail.com";
}; };
pull = { pull = {
-17
View File
@@ -1,17 +0,0 @@
root-ca: ENC[AES256_GCM,data:5WxHCyp8sWl+XMpmMFQGfs+IhZx0r61JVFp7TJsn4pnCwV2KY/eyh6pEF+GF+P7eXALPEWGzdFYTzMJd6KJ+D+Ew0bLomIZ+KVhlxhgVzKG0xThrLpxSuyrrRFFfXzMz7C28v1HlBht6xtwKouPnxQcXDYTLyV52w7FrhWTXPuJ+TvaYDlHJcs3wHY5U+UiCoQZMcM8ddNkgbMqCu+/QC4G213EoJvjhQz+woXzIuqN8SYsOVmQ5su9Xy1sDMc8vIF+vl1Ax1E+T7ZqnE4E/7Y7HNzBQFpBGZjTb3JLUoyO4ALo0S36Yls12MWj/AZBmoBG8yL4wwA48FBHzGyH1aYtbWA/twJLDYzmu9saORW/TKIMrzeruxdufWU9sRs1f2BxpefAa2kZ8QTxhx2+3kUiOC1lipIjdMu9RSeq7q4HmKRTmuuXW087ere+IMhogh11RoshttB1W0/BU/dz6sYwAnCioSoOCd3n+WBGfuMNyeDjkpNJ4veg2HuU/K84rLFjNsxWnRfQML0BjUDHzFypA4dNIDo/FI0Z1cOrPojiqjwKt5Fb0XljTwfDQvkRtcZOQLVln7HfJlsU67YUCBMgJUJUTC3XKo82smZbz8JV1z8I9hVfnSfoEMiqNvDu3wKPGDpjWq0hAKD2R82keKe3Ji33Z2O4HK0e0Q7vSPP/Z5gIAay2S2aS2YLpqlvgzipqiLo2owzM2Mr4NrKRL9Vg36na7bLBPEoz+l9EK8EEiZaq+c8H2+dpmXpdmnUMQDF5yBxs4EU2M6Ga+X+2SYd1q9aZBT0yxykFkQrMUcA==,iv:Ee4tvSVLdk0Clh8vohbajEaKXJlQJjqOIu2lxfWueAI=,tag:oy99+HrMV1uGCdcAy7epug==,type:str]
intermediate-ca: ENC[AES256_GCM,data:NUMdfNy+Opi/UypY1N2N8pZZK99m3VouzLxA7EKwC2sd2Q24uEflRfucjWupAASp7duncfSjmC1jQxCQpSzwV7Qbq3+PkKD+MnrceVzz40G97C5jEKEJG6ln5RdwGX5FeIF9tZpb/gBvkWcyrPQ6wDEpzd9rCDGJgvPzg/PaFuluO6UXKpppyC9Zzl2x/dms+2pV2aQcR0rEPEuXKK7NKxc+Zk3HFgcfhe26mbayZXZxdxs44vwPqPKr6xmWRiERBlCoMEfjLSWAiVjau/RXsW+/5Mtu80rbiRUHUesdZYIX4hqfoj914Mp1gsQ+bihnfBQ3xYyxtskBfHO8+oZNvOcDhx7zl8Vo+giD4435EYEFWNxjzRtbXoR0CHu50obmDDjpPcUpkRNzjpJK3VEJM45SEA2EZyupmf+Y8o4uHmOg/xTciVHLFqVOnchb6pm9WPbkyMT9K/+O9XvN3yVJzab6se6DdnEB/8PeA4s51M0ONk6PrFXSGdfwos84BmD8uaAqGhp64sXpTuunhfcmo02b1pZ2OqlGnjam5ZJOuyJ6vPeVYB/o60Dl/w5QtdeWc93EgsxNRAOyxOsrtSKdQPuZgp7+/I4S6kYaRJS8S7mvqSNdtCdrov1vYSEoHDXtu2XaDaJrStHJsP1FmTAAmXhzeyfGosB9JX7sdOGRkskesREqkzJ5V7+O0k3cpHFJvIZIFyj12rwpPSnrB66BELAQVxkqeol2dfPWn+G1aG9dmtq3T7+C3M5wW7VE41mmVgUrx9o4/8Z3wkHkBxZXiUkN4LfFHcZXmvuHFqRYYBOUpTzxRCkW6dl/gvP0qedMR3/zplhe51Gux7IFW7/s0wvTWHqWrpxirpW+c4l/IGf53Ox7kt3D,iv:atbEM7iZALuT1UipYmxlH0k0FOPJ7VKrfPrmCs3X9Mo=,tag:w6l49PKgjlzTZa1mIVpI4A==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSamN6dkxYalBZc25EYThR
NnV0YW5oQWgrQWNhSE9Gemt3VGljaE0rTGcwCnYyV2JpdW1GbEZTRElDNFk2Y213
bk1FY1grM2tuc0UzV21ROG5BanpjbFEKLS0tIG83dnpNQzEvYVZ1ODArRjlYSFRY
WHp0NktOQVF0UW1KajhYM3U5WkZCNmsKrN8T73fg7JoT+7WheveOC3Jlxa79EFjs
ePfVY07TKEHsycFhNyjcsFCWMF2ddE7q28A+Sjg+C3SA+/cHO8U9lw==
-----END AGE ENCRYPTED FILE-----
recipient: age13l2gtk0nzr484zprp7e0pkrt0ne0j4asyn2pjmlaw73nte7t7d8q4sqtxm
lastmodified: "2026-06-23T20:42:27Z"
mac: ENC[AES256_GCM,data:XftvodNnD0YXmd1GsNt2w820CWYY3v2pXOtlFYE0k25kuKNeKqqGDgN4geTA/xh6eYMrDut6mryNHOOoWXTuU9r/NvpXotwT5/vW2s4Nq+Cd9XySceJn/Ja6aN8R5ICbq3BhmpmC3SCVXDTce4+MwIHeBmp+Lrff+mXvZ5cT1A4=,iv:FqI/KdPJFHOMHykeZj0oEcuIZsCBWF1WCK4saz9Es7g=,tag:IZ1xHkB4eqkp04L2iAbkOw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
+4 -8
View File
@@ -1,17 +1,13 @@
{ {
config,
inputs,
lib, lib,
pkgs,
xlib, xlib,
... ...
}: }:
{ {
services.tailscale.enable = xlib.device.type != "wsl"; # true, if not wsl
# All real hosts (not the bare "minimal" test config) get OOM protection
# and a bounded journal.
services = { services = {
earlyoom.enable = lib.mkIf (xlib.device.type != "minimal") true; tailscale.enable = xlib.device.type != "wsl"; # true, if not wsl
journald.settings.Journal = lib.mkIf (xlib.device.type != "minimal") {
SystemMaxUse = "512M";
};
}; };
} }
+2 -49
View File
@@ -15,7 +15,7 @@
settings = { settings = {
require-sigs = false; require-sigs = false;
substituters = [ substituters = [
"https://nix-cache.zeroq.su" "http://100.64.0.0:5000"
"https://cache.nixos.org" "https://cache.nixos.org"
"https://nix-community.cachix.org" "https://nix-community.cachix.org"
"https://mirror.yandex.ru/nixos" "https://mirror.yandex.ru/nixos"
@@ -27,7 +27,7 @@
# "https://nixos.snix.store" # https://nixos.snix.store/ # "https://nixos.snix.store" # https://nixos.snix.store/
]; ];
trusted-public-keys = [ trusted-public-keys = [
"nix-cache.zeroq.su:be5jFLkiwNyOep/McxSafB3jguBmztxx+oJ46ySyc/s=" "cache.local:be5jFLkiwNyOep/McxSafB3jguBmztxx+oJ46ySyc/s="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
]; ];
stalled-download-timeout = 8; stalled-download-timeout = 8;
@@ -64,34 +64,6 @@
}); });
''; '';
}; };
pki.certificates = [
''
-----BEGIN CERTIFICATE-----
MIIBlDCCATmgAwIBAgIQUR+DM/LKIbokKxYPGZbCCjAKBggqhkjOPQQDAjAoMQ4w
DAYDVQQKEwVaZXJvUTEWMBQGA1UEAxMNWmVyb1EgUm9vdCBDQTAeFw0yNjA2MTMy
MjU2MDZaFw0zNjA2MTAyMjU2MDZaMCgxDjAMBgNVBAoTBVplcm9RMRYwFAYDVQQD
Ew1aZXJvUSBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgbhGtnlm
qd2Uv1B1VBSeg6NlXFMj4BG/k5gVu9bVFBK4cw9HVx21aHw9HhFW94P2KaySR6bu
K8tLDtzvs0xkyqNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
AQEwHQYDVR0OBBYEFH07/1blaqp0MVuSZIUHS9W3SjIrMAoGCCqGSM49BAMCA0kA
MEYCIQD1coTa7hqU1PAdnamAIgq1ApadDWpWfNaXPGiLCrkxTwIhAJhj/YSzqTJR
HvurdJ9m2glxV3rQHIUiVqKbQRcibObd
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBvjCCAWOgAwIBAgIRAMiJigRk8xbvHhCWN6a7D68wCgYIKoZIzj0EAwIwKDEO
MAwGA1UEChMFWmVyb1ExFjAUBgNVBAMTDVplcm9RIFJvb3QgQ0EwHhcNMjYwNjEz
MjI1NjA3WhcNMzYwNjEwMjI1NjA3WjAwMQ4wDAYDVQQKEwVaZXJvUTEeMBwGA1UE
AxMVWmVyb1EgSW50ZXJtZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
QgAE2gUlKZ/z9kt5RrdYZHnGE1TVVegn+aDmGpZk5uvF04O9k/sfjD6QE7VtjwNH
ervZKu3iBXGRg92ba0k369VJpKNmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB
/wQIMAYBAf8CAQAwHQYDVR0OBBYEFCtYg4LEAHPIMrDPO7lrxKuFvw4PMB8GA1Ud
IwQYMBaAFH07/1blaqp0MVuSZIUHS9W3SjIrMAoGCCqGSM49BAMCA0kAMEYCIQD2
nNNHqs9/mIstOxetObgg8eqbrPWHXEVQ9CDucNFmQAIhANXAz2z1Rc7hxc6er23W
I8TU6UQc8dledPvalDJLyGym
-----END CERTIFICATE-----
''
];
}; };
systemd.network.wait-online.enable = false; systemd.network.wait-online.enable = false;
@@ -103,23 +75,4 @@
"ru_RU.UTF-8/UTF-8" "ru_RU.UTF-8/UTF-8"
]; ];
}; };
# sops.secrets = {
# intermediate-ca = {
# format = "yaml";
# key = "intermediate-ca";
# sopsFile = ./secrets/settings.yaml;
# # owner = "nobody";
# # group = "nogroup";
# mode = "0700";
# };
# root-ca = {
# format = "yaml";
# key = "root-ca";
# sopsFile = ./secrets/settings.yaml;
# # owner = "nobody";
# # group = "nogroup";
# mode = "0700";
# };
# };
} }
+4 -33
View File
@@ -1,7 +1,6 @@
{ {
config, config,
pkgs, pkgs,
xlib,
... ...
}: }:
{ {
@@ -20,18 +19,15 @@
theme = "robbyrussell"; theme = "robbyrussell";
}; };
shellInit = '' shellInit = ''
beet-n() {
echo "$*" | aichat -cer beets
}
beet-p() { beet-p() {
local base="${xlib.dirs.user-home}/.config/beets/My" beet mod path:. playlist="$*"
local rel
rel=$(realpath --relative-to="$base" "$PWD")
beet mod "path:$rel" playlist="$*"
} }
beet-ims() { beet-ims() {
beet im ./ -S $* beet im ./ -S $*
} }
beet-path() {
realpath --relative-to="${xlib.dirs.user-home}/.config/beets/My" "$1"
}
''; '';
shellAliases = { shellAliases = {
# shell # shell
@@ -46,9 +42,7 @@
gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'"; gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'";
y = "yazi"; y = "yazi";
nix-shellp = "nix-shell --run $SHELL -p"; nix-shellp = "nix-shell --run $SHELL -p";
beet-path-library = "realpath --relative-to='${xlib.dirs.user-home}/.config/beets/My' .";
z-proxy = "export ALL_PROXY=socks5://localhost:10808"; z-proxy = "export ALL_PROXY=socks5://localhost:10808";
zh-proxy = "export HTTPS_PROXY=http://localhost:10808 && export HTTP_PROXY=http://localhost:10808";
# beets # beets
beet-ima = "beet im ./ -A"; beet-ima = "beet im ./ -A";
@@ -63,29 +57,6 @@
z-p-1 = "ssh pubray-1"; z-p-1 = "ssh pubray-1";
z-map-local-proxy = "ssh -R 10808:localhost:10808"; z-map-local-proxy = "ssh -R 10808:localhost:10808";
# authelia — Argon2id hash with the exact params configured for the
# authelia daemon (memory=65536, iterations=3, parallelism=4,
# salt-length=16). Defaults already match, but explicit so an upstream
# default change can't silently produce a hash the daemon rejects.
#
# Note: the old `authelia hash-password --argon2id` form is from
# Authelia ≤4.35. Current CLI is `crypto hash generate argon2
# -v argon2id`. The subcommand takes no positional password — it
# prompts with confirmation (omit confirmation with `--no-confirm`)
# or accepts `--password <value>`.
#
# Example usage:
# authelia-hash
# # interactive prompt (twice — confirmation), then hash on stdout
# authelia-hash --no-confirm
# # single prompt, hash on stdout
# authelia-hash --no-confirm --password 'mypassword'
# # non-interactive (for scripts)
#
# Output goes to stdout — append it to users_database.yml by hand:
# authelia-hash --no-confirm >> /mnt/services/authelia/users_database.yml
authelia-hash = "authelia crypto hash generate argon2 -v argon2id -i 3 -m 65536 -p 4 -s 16";
# Somethings # Somethings
reboot-bios = "sudo systemctl reboot --firmware-setup"; reboot-bios = "sudo systemctl reboot --firmware-setup";
-33
View File
@@ -1,33 +0,0 @@
{
config,
lib,
...
}:
{
options.host.ssh = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable the SSH server with the shared config below.";
};
};
config = lib.mkIf config.host.ssh.enable {
services.openssh = {
enable = true;
allowSFTP = true;
openFirewall = lib.mkDefault false;
hostKeys = [
{
path = "/etc/ssh/id_ed25519";
type = "ed25519";
}
];
settings = {
PasswordAuthentication = false;
PermitRootLogin = "yes";
UsePAM = true;
};
};
};
}

Some files were not shown because too many files have changed in this diff Show More