The M6 hardening milestone is on main. The headline is unglamorous on purpose:
take the self-healing loop from “works in the demo” to “you could run this in a
regulated, air-gapped shop and trust the audit trail.” Fifteen issues, one epic
(#158).
The interesting part isn’t the feature list. It’s that four of the bugs we found don’t exist in any single branch — they only appeared when the branches were combined. That’s the honest argument for integrating deliberately instead of merging fifteen green PRs and hoping.
What shipped
- Durable audit trail. The remediation record (proposal → decision → command → result) now lives in append-only Postgres, not memory. “Auditable” stops being a slide when the record survives a control-plane restart. (#143)
- An in-cluster Kubernetes executor. The pod-restart templates finally have
something that runs them: a component that pulls signed commands, re-verifies
the Ed25519 signature in-cluster, and executes typed capabilities
(
delete_pod,restart_deployment,pod_state) under least-privilege, namespaced RBAC. (#146) - Signing-key rotation with versioned keys and an overlap window — rotate the command-signing key with no fleet re-enrollment. (#150)
- Self-observability. Prometheus
/metricsand a Ravn event when the policy circuit breaker trips. The monitoring tool now monitors itself. (#149) - Air-gapped install, a non-NixOS host quickstart, a Helm chart, a real
ravn-evalbenchmark harness, and an MCP server — the install story is now first-class on host, Kubernetes, and fully offline.
Through all of it, the founding rule is unchanged: the LLM is never in the detection or action path. It writes the explanation; deterministic code and signed templates decide and act.
The bugs that hid
These are the useful part — each is invisible until two branches meet.
-
The guard that rejected its own executor. The in-cluster executor runs commands through the shared
handle_command. One branch added a guard there that rejects Kubernetes capabilities — so the executor would have rejected every command it exists to run. Its own unit tests passed, because they test the guard and the executor separately, never composed. We moved the guard to the host actuator’s socket boundary, where it belongs. -
Two designs for one enum. Two branches independently invented the K8s capability type — different variant names, different fields. Both compiled. Reconciling them to one canonical design was a deliberate call, not a textual merge.
-
A typed-path check that caught a real template typo. New startup validation of template condition paths immediately rejected a shipped K8s template that used
reasoninstead ofpayload.reason— a template that, before the check, would have silently never matched. -
A
SIGKILLthat produced no proposal. This one only surfaced in the NixOS VM end-to-end test. The test kills a unit withSIGKILL; systemd reports the result assignal. But the generic restart template had been narrowed to match onlyresult = "exit-code". So the unit failed, and Ravn proposed nothing — the worst kind of failure, the silent one. The fix was to match any failed unit regardless of result, and to lock it in with a test that fails the moment the catch-all gets narrowed again.
Every one of those passed cargo test on its own branch. The VM test — boot a
real NixOS machine, kill a unit, assert a signed heal record appears — is the only
thing that catches the last one. Slow tests earn their keep.
A smaller, sharper one: “is it broken, or just deploying?”
A question we kept getting: how does Ravn avoid “healing” something that’s just
being restarted or redeployed? Most of the answer is state semantics — systemd’s
failed state and Kubernetes’ failure reasons don’t fire for clean restarts or
rollouts. But a unit can briefly enter failed before systemd’s own Restart=
recovers it. So the failed-unit tap now has a grace period: a unit must stay
failed for a configurable window (default 15s) before it emits anything. Brief
blip, recovered in time → no proposal, no noise. (#164)
What’s next
The code is on main and green; the cross-target end-to-end proofs (k3d heal,
air-gapped VM, live-Postgres restart) are running their way through CI now, and
that’s the bar for calling the epic done — not “it compiles.” There’s also a
copy-paste remediation template library
now, so adding a heal for your service is a TOML file, not a code change.
As always, built in the open. The bugs above are in the git history, not hidden in a changelog — that’s the whole point.
Comments are powered by Giscus (GitHub Discussions). They go live once Discussions is enabled, the Giscus app is installed, and
repo_id/category_idare set in_config.yml.