Skidbladnir RON orchestration + steering modules (2026-07-26, Rickard)
Make Skidbladnir usable two ways: (1) declarative RON that describes a provisioning flow (an interpreter runs it), and (2) direct/custom library calls (imperative escape hatch). The RON steps dispatch to pluggable steering modules packaged/verified in znippy.
Two layers (the difference)
- RON-triggered (declarative): a
.rondescribes a sequence of ops; an interpreter executes it. Same vocabulary, different files:
// A: [ BootOS(backend: Redfish, os: Ubuntu), InstallApp(spec: "korp") ]
// B: [ BootOS(backend: Redfish, os: Talos), Configure(target: Talos, ...) ]
Data-driven, reusable, versionable, shareable, no-code. A new scenario = a new RON, not new Rust.
- Custom (imperative): call Skidbladnir's Rust API / a steering module directly for flows the RON vocabulary doesn't cover. Full power, but code. One is the vocabulary; the other is the raw language. Mirrors
.runa.ron→jera and rel2graph emit-vs-execute.
Vocabulary + interpreter
Typed Step enum over what Skidbladnir/draupnir already own: BootOS{backend, os} (Redfish/KVM/OCI), Seed{config} (NoCloud), InstallApp{spec}, Configure{target, params}, Federate{registry}, … A RON interpreter (like runa_exec) maps each Step → a steering module call. The library stays the imperative core; the RON layer is a thin declarative interpreter on top.
Steering modules (the execution/plugin model)
Each Step dispatches to a steering module = a pluggable capability. The seam is a SteeringModule trait; modules are registered by name and packaged in znippy (blake3-verified, self-contained); Skidbladnir "plucks" the needed module from the znippy at runtime (reuses the two-znippy root-of-trust → tamper-evident, offline, appliance-friendly).
Module kinds — START NATIVE, add WASM later (behind the same trait)
- Native Rust modules (start here):
RedfishBoot,KvmBoot,OciDeploy,TalosConfig,InstallApp,TerraformApply. Full/direct I/O, pure-Rust, no ABI/WASM friction. In-tree registry. - WASM modules (future): the real win is a third-party / language-agnostic / hot-load module ecosystem — NOT sandboxing (Skidbladnir is trusted operator infra, low value) and NOT I/O (WASM needs host-import capabilities
http/exec/file/seed— real complexity for these I/O-heavy modules). So don't lead with WASM; slot awasmmodule-kind behind the SAMESteeringModuletrait when an open ecosystem is wanted. Same "one seam, swappable backends" pattern as ContainerBackend / Graph trait / rel2graph emitters / runa steps.
Terraform = the "support-all-clouds" bridge
Skidbladnir natively drives Redfish/KVM/OCI (draupnir). A TerraformApply steering module bridges cloud providers it doesn't natively speak — pragmatic (Terraform isn't pure-Rust, but it's the meet-every-provider escape, like ContainerBackend supporting non-Rust runtimes). znippy carries the tf binary + configs so the appliance stays self-contained. (Alternative to stay pure-Rust: native cloud SDKs.)
Reuse / relation
Skidbladnir already has the imperative core (ServiceSpec + 11 init renderers + Redfish/KVM/OCI via draupnir + airgap + NoCloud seed + federation). This adds the RON vocabulary + interpreter + the SteeringModule seam + znippy packaging on top. Same RON-as-description language as .runa.ron (jera) and rel2graph's .runa.ron emitter — the constellation converges on RON for declarative flows.
Generalization → mikronova/rover (WASM = Python control, even Mars) 🚀
The engine (declarative flow + SteeringModule seam + znippy-packaged modules) is brandable. Same core, different product:
- Skidbladnir (nordisk): infra provisioning · RON · native modules (trusted operator; I/O-heavy; sandbox low-value) → native-first.
- mikronova/rover (own repo/branding): robotics/edge control · description-language-agnostic (RON OR Python OR …) · WASM modules → WASM-first.
Why WASM flips from optional→essential for the rover: the very thing that's friction for Skidbladnir — WASM modules can only call host-granted capabilities, not raw I/O — becomes the SAFETY MODEL:
- The rover host exposes ONLY safe caps (
move_motor,read_sensor,take_photo,drive). A rogue/buggy control module physically cannot exceed them — can't brick the rover. Capability-gated = safety-critical. - Python (or any language) as the control language — science team writes Python → compile to WASM → runs sandboxed on the rover. No Rust needed for control authors.
- znippy-packaged, blake3-verified, self-contained, offline → deploy a verified control module to gear with no network + no toolchain — extreme airgap, up to Mars.
So: same SteeringModule trait, module-kind DEFAULT flips per branding (native ↔ wasm). Build the trait right once (native for Skidbladnir now), and the WASM/Python-control rover is a branding + a wasm module-kind
- a capability-interface later — not a rewrite. mikronova/rover = its own repo, sharing this engine.
Companion docs (the as-built reference)
This file is the original design capture. The verified, as-built reference for what actually ships lives in two siblings — read these for ground truth:
ron-schema.md— the full RON object model: every top-level object (Flow,Step,BootBackend, …), its fields, the deserialize path, and an annotated end-to-end example.custom-objects.md— the custom-object / custom-code seam: theSteeringModuletrait, therun_flowdispatch point, the two-phase INFRA→APP lifecycle, the 11 service backends beyond systemd, and a worked custom-object example.
Status
Phase 1 SHIPPED (nornir 78deafc): the SteeringModule trait + name→module Registry (native_default) + the run_flow RON interpreter + native modules — KvmBoot and InstallApp REAL, the rest honest mocks. Verified in ron-schema.md / custom-objects.md. NOT YET IMPLEMENTED (still design-only, flagged in the companions): znippy-packaged modules + runtime "pluck from znippy"; the WASM module-kind + capability-import interface; a CLI runner (skidbladnir run flow.ron). The design listed a TalosConfig module; the code ships a generic Configure step/module instead.