- Rust 79.8%
- Nix 17.9%
- Just 2.3%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| CODESTYLE.md | ||
| flake.lock | ||
| flake.nix | ||
| Justfile | ||
| README.md | ||
Supervisor
FrogOS actor lifecycle manager. Starts and supervises Toad (system watcher) and Planner (action planner) under an Erlang-style rest_for_one strategy.
Does not observe, plan, or execute — it wires actors together and restarts them on failure.
Architecture
┌─────────────────────────────────────────────┐
│ Supervisor │
│ │
│ ToadActor ──watch──▶ Bridge ──mpsc──▶ PlannerActor
│ (system watcher) (encoder) (Haskell FFI)
│ │ │
│ SystemState Plan
│ │
└─────────────────────────────────────────────┘
▼
plan_rx (caller)
ToadActor polls the system (processes, services, power, apps) and publishes SystemState on a watch channel.
Bridge subscribes to ToadState changes, encodes them as protobuf, and forwards PlannerMsg::SystemStateUpdate over mpsc.
PlannerActor receives state updates and an initial IR config, calls the Haskell FFI planner, and emits Plan structs.
Restart semantics (rest_for_one)
| Actor exits | Restarted |
|---|---|
| Toad | Toad + Bridge + Planner |
| Bridge | Bridge + Planner |
| Planner | Bridge + Planner |
Bridge is always co-restarted with Planner because they share the paired ends of an mpsc channel — a new channel must be created on each restart.
Restarts are rate-limited: max 10 within a 60-second window (configurable via SupervisorConfig).
Requirements
- Rust (edition 2024)
- Nix with flakes (recommended)
PLANNER_FFI_LIB_DIRpointing to the compiledlibplanner-ffi.so— set automatically insidenix develop
cargo checkworks without the Haskell library.cargo buildandcargo testrequire it.
Development
Enter the Nix dev shell (sets PLANNER_FFI_LIB_DIR and other env vars):
nix develop
Common tasks via just:
just check # cargo check --all-targets --all-features
just lint # cargo clippy --all-targets --all-features -- -D warnings
just test # cargo test --all-targets --all-features
just build # cargo build --all-targets --all-features
just ci # fmt-check + check + lint + test + build
just nix-check # nix flake check --keep-going --print-build-logs
Running
The binary takes one optional argument — path to the intent representation (IR) JSON file:
# Default path: /run/current-system/intent.json
supervisor
# Custom IR path
supervisor /path/to/intent.json
Logs are emitted via tracing. Set RUST_LOG to control verbosity:
RUST_LOG=info supervisor /path/to/intent.json
RUST_LOG=debug supervisor /path/to/intent.json
Each received plan is logged with its steps. Plans are also delivered on the plan_rx receiver returned by Supervisor::new for downstream consumers.
Library usage
use std::time::Duration;
use bytes::Bytes;
use supervisor::{Supervisor, SupervisorConfig};
let config = SupervisorConfig {
poll_interval: Duration::from_secs(1),
ir_bytes: Bytes::from(std::fs::read("intent.json")?),
max_restarts: 10,
restart_window: Duration::from_secs(60),
restart_delay: Duration::from_secs(1),
};
let (supervisor, mut plan_rx) = Supervisor::new(config);
tokio::spawn(async move {
while let Some(plan) = plan_rx.recv().await {
// handle plan
}
});
supervisor.run().await?;
License
LGPL-2.1-only