- Haskell 91.1%
- Rust 8%
- Nix 0.9%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| planner-actor | ||
| proto | ||
| src | ||
| test | ||
| .gitignore | ||
| .stan.toml | ||
| AGENTS.md | ||
| cabal.project | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CODESTYLE.md | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| planner-core.cabal | ||
| README.md | ||
FrogOS Planner
Planner is the stateless Haskell planning core for FrogOS. It turns verified FrogOS intent plus an observed system snapshot into a deterministic transition plan.
IRDocument + SystemState -> Either PlanError Plan
The Rust runtime owns observation, actors, execution, retries, generation storage, and rollback orchestration. Planner only computes what should happen next.
Current Status
This repository contains a working planning scaffold:
- pure Haskell
planentry point; - resource graph and delta pipeline for services, packages, and users;
- power profile planning;
- Protobuf FFI boundary;
- Rust
planner-actorwrapper and smoke tests.
The C ABI exists today as hs_planner_init, hs_plan, and hs_plan_free.
IRDocument still enters the boundary as IR JSON from frogos-ir.
SystemState and PlanResult cross the FFI boundary as Protobuf bytes.
Individual DeltaAction values inside PlanStep are JSON-encoded byte fields
inside the Protobuf message.
Architecture
configuration.frog
|
v
DSL / validation
|
v
frogos-ir IRDocument JSON
|
v
Rust runtime
- observes system
- owns actor lifecycle
- stores generations
|
v
planner-actor
|
v
C ABI: hs_plan
|
v
Haskell Planner core
IRDocument + SystemState
|
v
PlanResult
|
v
Rust Executor
- executes steps
- rolls back completed steps on failure
Planner does not read /proc, call dinit, run Nix, persist generations, or
execute commands. Those are runtime responsibilities.
Planning Pipeline
One plan call performs a full deterministic recalculation:
IRDocument
|
| 1. reject unsupported IR version
v
active profiles
|
| 2. evaluate conditions from SystemState
v
desired resource graph
|
| 3. resolve desired service/package/user states and priorities
v
current resource graph
|
| 4. derive from observed SystemState
v
delta
|
| 5. compare desired vs current
v
ordered plan steps
|
| 6. users -> packages -> services, plus power steps
v
Plan
Important invariants:
- equal inputs must produce equal plans;
- already reconciled resources produce no step;
- each step carries undo metadata;
- equal-priority conflicting desired states return
ConflictingActions; - runtime execution errors are not
PlanError.
Input And Output Example
This example asks Planner to create user alice, install htop, enable an
Nginx module named mysite, and switch to the performance power profile.
Input: IRDocument
IRDocument is supplied by frogos-ir as JSON. This is a simplified example of
the shape relevant to Planner:
{
"version": X,
"packages": ["htop"],
"users": [
{
"name": "alice",
"normal_user": true
}
],
"profiles": [
{
"name": "desktop",
"policies": [],
"actions": [
{
"type": "module_enable",
"module": {
"domain": "nginx",
"name": "mysite"
},
"priority": 100
},
{
"type": "power_profile",
"value": "performance",
"priority": 100
}
]
}
]
}
Input: SystemState
SystemState crosses FFI as Protobuf bytes. Human-readable decoded view:
SystemState {
running_processes: ["waybar", "foot"]
active_services: []
power_profile: "balanced"
cpu_load: 0.0 # 0.0 means not observed
battery_percent: -1 # -1 means not observed
running_apps: []
installed_packages: []
existing_users: []
}
Output: PlanResult
PlanResult also crosses FFI as Protobuf bytes. For readability, the
action_json and undo_json byte fields are shown decoded:
PlanResult {
plan: Plan {
steps: [
PlanStep {
action_json: {"type":"user_add","config":{"name":"alice","normal_user":true}}
undo_json: {"type":"user_remove","name":"alice"}
},
PlanStep {
action_json: {"type":"package_install","name":"htop"}
undo_json: {"type":"package_remove","name":"htop"}
},
PlanStep {
action_json: {"type":"module_enable","module":{"domain":"nginx","name":"mysite"},"priority":100}
undo_json: {"type":"module_disable","module":{"domain":"nginx","name":"mysite"},"priority":100}
},
PlanStep {
action_json: {"type":"power_profile","value":"performance","priority":100}
undo_json: {"type":"power_profile","value":"balanced","priority":100}
}
]
}
}
If the current state already contains user alice, package htop, active
module mysite, and power profile performance, the returned plan is:
PlanResult {
plan: Plan {
steps: []
}
}
Errors
Planner returns deterministic planning failures as PlanError:
PlanError {
error_type: "version_mismatch"
message: "expected 9, got 1"
}
PlanError {
error_type: "conflicting_actions"
message: "conflicting desired states at equal priority"
}
Malformed input, invalid Protobuf, invalid IR JSON, allocation failures, and unexpected Haskell exceptions are FFI boundary failures. Execution failures belong to Rust Executor, not Planner.
FFI Boundary
Rust-owned IR JSON bytes
Rust-owned SystemState protobuf bytes
|
v
hs_plan(ir_ptr, ir_len, state_ptr, state_len, out_ptr, out_len)
|
v
decode IR JSON
decode SystemState protobuf
plan ir state
encode PlanResult protobuf
|
v
Haskell-allocated output buffer
|
v
Rust copies result
|
v
hs_plan_free(output_ptr)
Rules:
- all byte buffers are passed as pointer plus explicit length;
- Rust never receives pointers to Haskell data structures;
hs_plan_freereleases buffers allocated byhs_plan;- Haskell catches synchronous exceptions before returning through C;
hs_planner_initis the runtime lifecycle hook exported for callers.
Modules
src/Planner/
Core.hs -- pure orchestration: IRDocument + SystemState -> Either PlanError Plan
Graph.hs -- current and desired resource graphs
Delta.hs -- graph diff and step ordering
Types.hs -- planning domain types
Proto.hs -- Protobuf encode/decode helpers
FFI.hs -- C ABI boundary
planner-actor/
src/ffi.rs -- Rust FFI wrapper
src/actor.rs -- Tokio actor around Planner
tests/ -- Rust smoke tests
Development
nix develop
cabal test
nix fmt
stan --no-default
nix flake check --keep-going --print-build-logs
Behavior changes should be test-driven. The current test suite includes example-based planning tests, table-driven state transition tests, property tests, shrink checks, Protobuf round-trip tests, and Rust FFI/actor smoke tests.
Project rules live in AGENTS.md and CODESTYLE.md.