No description
Find a file
2026-06-15 15:24:28 +03:00
LICENSE Initial commit 2026-05-27 12:12:59 +03:00
README.md refactor: rename Watcher actor to Toad 2026-06-15 15:24:28 +03:00

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, которая проходит двухуровневую валидацию:

  1. Структурная — типы Haskell. Структурно невозможные конструкции не компилируются.
  2. Семантическая — чистый валидатор после построения конфигурации. Проверяет конфликты, дубликаты, несовместимые намерения. Накапливает все ошибки сразу: 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 метаданные поколения на диске