| LICENSE | ||
| README.md | ||
FrogOS — Архитектура
Базовая документация к проекту FrogOS
Концепция
FrogOS — реактивная декларативная система управления Linux, в которой пользователь описывает намерение, а не план выполнения. Система самостоятельно строит безопасный путь к желаемому состоянию и поддерживает его актуальность.
Намерение → IR → План → Состояние
Научная новизна: предложена модель управления состоянием ОС где намерение пользователя транслируется в верифицированный план с формальной гарантией safety — применённое состояние всегда консистентно с описанным намерением.
Стек
| Слой | Технология | Обоснование |
|---|---|---|
| DSL + Validator | Haskell | типовая система даёт статическую верификацию, невалидная конфигурация не компилируется |
| Planner (ядро) | Haskell | чистая функция IR + SystemState → Plan; общие типы с DSL/IR, GHC native lib через FFI |
| Акторы / Runtime | Rust | memory safety без GC, нативный async/tokio, идиоматические каналы |
| Init / Service Manager | dinit | dependency-aware граф сервисов, control socket, минималистичный |
| Пакетный менеджер | Nix | поколения пакетов, воспроизводимость, content-addressed store |
| IR (wire format) | JSON | стабильный контракт DSL → Planner; Rust передаёт байты, парсит Haskell ядро |
| Поколения (архив) | JSON + BSON | intent.json для diff, manifest.bson для метаданных |
Слои системы
1. DSL (Haskell)
Файл конфигурации: /etc/frogos/configuration.hs
Пользователь пишет Haskell EDSL. Все примитивы доступны через FrogOS.Prelude
без явных импортов.
profile "gaming" $
when (processRunning "steam" `via` poll 500) $ do
disable "docker"
setPowerProfile Performance
service "docker" $
when (not $ processRunning "steam") $ do
enable "docker"
service "backend" $ do
allowPorts [5432, 5433, 5434]
fallback (port 5432) (fallbackPorts [5433, 5434])
DSL строит UnvalidatedConfiguration, которая проходит двухуровневую валидацию:
- Структурная — типы Haskell. Структурно невозможные конструкции не компилируются.
- Семантическая — чистый валидатор после построения конфигурации. Проверяет
конфликты, дубликаты, несовместимые намерения. Накапливает все ошибки сразу:
Either [ValidationError] IR.
IR сериализуется в JSON только при успешной валидации. Конструктор проверенного
IR не входит в публичный API — создать IR можно только через validate.
2. IR (JSON)
IR — стабильный JSON контракт между слоями. Haskell сериализует проверенную
конфигурацию в JSON. Rust сторона IR не парсит — передаёт intent.json как
непрозрачные байты в ядро Planner, которое десериализует их теми же типами
frogos-ir (aeson).
IR организован в области намерений: profiles, services и другие будущие
разделы. Каждая область содержит узлы трёх типов:
- Condition — boolean-триггер. "Когда это актуально".
- Policy — ограничения и возможные действия. Конкретный выбор делает движок.
- Action — желаемое изменение системы.
{
"version": "1",
"profiles": [
{
"name": "gaming",
"condition": {
"type": "process_running",
"name": "steam",
"observe": { "type": "poll", "interval_ms": 500 }
},
"actions": [
{ "type": "service_disable", "name": "docker" },
{ "type": "power_profile", "value": "performance" }
]
}
],
"services": [
{
"name": "backend",
"policies": [
{ "type": "allow_ports", "values": [5432, 5433, 5434] },
{
"type": "fallback",
"target": { "type": "port", "value": 5432 },
"strategy": { "type": "next_available_port", "values": [5433, 5434] }
}
]
}
]
}
Новый домен добавляется как новый ключ в корень документа — существующие области не меняются.
3. Planner (Haskell core за FFI, stateless)
Planner — единственный кто думает. Получает IR + текущее состояние системы, возвращает Plan.
Ядро Planner написано на Haskell и использует типы frogos-ir напрямую —
без дублирования контракта на Rust стороне. GHC компилирует ядро в нативную
библиотеку, Rust Supervisor линкует её через FFI и оборачивает в тонкого
актора tokio, чтобы Planner жил в акторной модели вместе с остальными
модулями (общение с Executor через каналы).
Planner stateless — не хранит состояние между вызовами. Каждый вызов это полный пересчёт на основе актуального IR и состояния системы.
Plan — список атомарных действий с обратными операциями:
plan :: IRDocument -> SystemState -> Either PlanError Plan
newtype Plan = Plan { steps :: [(Action, Undo)] }
Через границу FFI данные ходят как сериализованные байты: SystemState (Rust → Haskell) и Plan (Haskell → Rust). Оба — версионированные JSON контракты, как IR.
Акторная модель (Rust + tokio)
Все компоненты runtime — акторы с изолированными mailbox. Rust Supervisor держит все акторы и перезапускает упавшие с последним известным состоянием из Store.
Rust Supervisor
├── Toad Actor — наблюдает /proc, udev, события системы
├── Planner Actor — тонкая обёртка над Haskell ядром (FFI), строит Plan из IR + SystemState
├── Executor Actor — исполняет команды, не знает контекста
├── Backend Actor — маршрутизирует к нужному бэкенду
│ ├── DinitBackend — сервисы via dinit control socket
│ └── NixBackend — пакеты via Nix daemon socket
└── Store Actor — управляет поколениями
Каналы
| Канал | Тип tokio | Направление |
|---|---|---|
| Новый IRDocument | watch::Sender |
Supervisor → Toad, Planner |
| События системы | watch::Sender |
Toad → Planner |
| Plan на исполнение | mpsc::Sender |
Planner → Executor |
| Результат шага | oneshot::Sender |
Executor → Planner |
| Commit / Rollback | mpsc::Sender |
Planner → Store |
| Системные операции | mpsc::Sender |
Executor → Backend |
Принцип разделения
Toad — только наблюдает. Не интерпретирует события, просто сообщает об изменениях состояния системы.
Toad владеет polling-циклами: читает ObserveStrategy из Condition каждого
IR-узла и держит внутренние таймеры под каждую стратегию. При получении нового
IRDocument (канал Supervisor → Toad) reconcile таймеры — убивает устаревшие,
создаёт новые с актуальными интервалами. Executor не знает о polling.
Planner — только думает. Знает IR и текущее состояние. Строит Plan. Принимает результаты от Executor и решает: commit или rollback.
Executor — только делает. Получает одну атомарную команду, исполняет,
возвращает ok/err. Не знает контекста, не знает плана, не знает зачем:
{ "op": "service_stop", "name": "docker" }
{ "op": "power_profile", "value": "performance" }
Backend — только говорит с внешними системами. Маршрутизирует: сервисы → DinitBackend, пакеты → NixBackend. Замена любого суб-бэкенда не затрагивает остальные акторы.
Store — только хранит. Управляет поколениями, симлинками, архивом IR.
Reconciliation Loop
Planner пересчитывает план при двух независимых триггерах.
Триггер 1: изменение состояния системы
Toad обнаруживает изменение состояния (/proc, udev, poll тик)
↓
Planner получает новый SystemState через watch канал
↓
Строит Plan из currentIR + newSystemState
↓
Отправляет Plan шаг за шагом в Executor
↓
├── шаг успешен → следующий шаг
└── шаг упал → откат шагов 0..N в обратном порядке
↓
Успех → Planner отправляет commit в Store
Ошибка → Planner отправляет rollback в Store
Триггер 2: смена конфигурации (новое поколение)
Supervisor применяет новое поколение
↓
Supervisor рассылает новый IRDocument через watch канал
├─ Toad получает UpdateIR → reconcile poll таймеры под новый IR
└─ Planner получает NewConfig → строит Plan из newIR + currentSystemState
↓
безусловные шаги → Executor сразу
условные шаги → ждут следующего Toad события
Гарантия Safety
Применённое состояние всегда консистентно с верифицированным намерением. Частичное применение невозможно — либо весь Plan, либо откат.
Хранение поколений
/run/current-system -> /frogos/generations/gen-42/
/etc/frogos/
configuration.hs
/frogos/
generations/
gen-42/
intent.json ← IR сериализованный, human-readable, diffable
manifest.bson ← timestamp, хеш IR, статус применения
artifacts/ ← симлинк на /frogos/store/...
gen-41/
...
store/
abc123/ ← неизменяемо, content-addressed
def456/
intent.json — архивный формат IR для хранения и diff. Не wire format.
IR сериализуется в JSON только для записи в поколение.
Откат — атомарная операция на уровне ФС:
fs::remove_symlink("/run/current-system")?;
fs::symlink("/frogos/generations/gen-41", "/run/current-system")?;
Diff между поколениями бесплатно:
diff /frogos/generations/gen-41/intent.json /frogos/generations/gen-42/intent.json
Форматы данных
| Сущность | Формат | Где живёт |
|---|---|---|
| IR | JSON | DSL → ядро Planner (Rust передаёт байты) |
| SystemState | JSON | граница Rust → Haskell (FFI) |
| Plan | JSON | граница Haskell → Rust (FFI) |
| intent.json | JSON | архив поколения на диске |
| manifest.bson | BSON | метаданные поколения на диске |