Large merge: 209 commits, +47k/-5k across 428 files. Five clusters of work, plus a deliberate behavior change and a known-deferred backlog spelled out below. 1. IoT walking skeleton — harmony-reconciler-contracts crate, harmony-fleet-operator (Deployment CRD → NATS KV), harmony-fleet- agent reconciling PodmanV0Score into containers, K8sBareTopology, maud+htmx operator frontend, request/reply commands over NATS. 2. NATS auth callout + from-scratch nats/jwt crate (algorithm, claims, builders, xkey). nats/callout maps Zitadel roles to NATS permissions; harmony-fleet-auth holds the shared credential plumbing. 3. Zitadel deepening — setup.rs rewritten (cross-org admin, persisted admin password, device-code OIDC, OKD-compat values). New harmony_zitadel_auth crate (axum login flow, JWKS, sessions). Agent gets a JWT-bearer credential source. 4. Deploy-architecture refactor per ADR-023 — Scores everywhere, new harmony-fleet-deploy crate, harmony-fleet-e2e harness covering Pod target plus VM target (aarch64 production + x86_64 fast iteration). First Score companion lands: AgentObservation (ADR-023 P7). 5. Device enrollment via Zitadel SSO + aarch64 KVM support (AAVMF firmware, per-VM NVRAM, TCG perf overrides), IotDeviceSetupScore /FleetDeviceSetupScore SSH apply, fleet-sso-login example. Behavior change worth flagging in this merge: harmony_secret now panics on an unknown SECRET_STORE value instead of silently defaulting to Infisical. No in-tree caller hits this; a typo in the env var is now loud rather than mysterious. Deferred (not merge blockers — file as issues post-merge): - CI is red on master because hub.nationtech.io/harmony/ harmony_composer:latest is missing libvirt-dev + pkg-config, which `cargo check --all-features` requires once harmony's `kvm` feature is unified. Local `build/check.sh` is green. Fix is a two-line Dockerfile edit + rebuild of the composer image; top priority post-merge so master goes green again. - Smoke-test contract (ADR-023 P4) — the principle is locked but the trait/struct shape is open. Each Score implements its own readiness today; harmony-fleet-e2e has `wait_until_ready` as a per-test stand-in. - Rust equivalents for smoke-a1.sh / smoke-a4.sh (smoke-a3* are superseded by the new VM e2e tests). - Five #[ignore]'d tests in examples/fleet_e2e_demo and nats/ integration-test-callout — waiting on a CI runner with libvirt + k3d + podman. - ADR-024 (capability decomposition) — kept as draft under docs/adr/drafts/024- pending more conviction.
Supporting a new field in OPNSense config.xml
Two steps:
- Supporting the field in
opnsense-config-xml - Enabling Harmony to control the field
We'll use the filename field in the dhcpcd section of the file as an example.
Supporting the field
As type checking if enforced, every field from config.xml must be known by the code. Each subsection of config.xml has its .rs file. For the dhcpcd section, we'll modify opnsense-config-xml/src/data/dhcpd.rs.
When a new field appears in the xml file, an error like this will be thrown and Harmony will panic :
Running `/home/stremblay/nt/dir/harmony/target/debug/example-nanodc`
Found unauthorized element filename
thread 'main' panicked at opnsense-config-xml/src/data/opnsense.rs:54:14:
OPNSense received invalid string, should be full XML: ()
Define the missing field (filename) in the DhcpInterface struct of opnsense-config-xml/src/data/dhcpd.rs:
pub struct DhcpInterface {
...
pub filename: Option<String>,
Harmony should now be fixed, build and run.
Controlling the field
Define the xml field setter in opnsense-config/src/modules/dhcpd.rs.
impl<'a> DhcpConfig<'a> {
...
pub fn set_filename(&mut self, filename: &str) {
self.enable_netboot();
self.get_lan_dhcpd().filename = Some(filename.to_string());
}
...
Define the value setter in the DhcpServer trait in domain/topology/network.rs
#[async_trait]
pub trait DhcpServer: Send + Sync {
...
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
...
Implement the value setter in each DhcpServer implementation.
infra/opnsense/dhcp.rs:
#[async_trait]
impl DhcpServer for OPNSenseFirewall {
...
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filename(filename);
debug!("OPNsense dhcp server set filename {filename}");
}
Ok(())
}
...
domain/topology/ha_cluster.rs
#[async_trait]
impl DhcpServer for DummyInfra {
...
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
...
Add the new field to the DhcpScore in modules/dhcp.rs
pub struct DhcpScore {
...
pub filename: Option<String>,
Define it in its implementation in modules/okd/dhcp.rs
impl OKDDhcpScore {
...
Self {
dhcp_score: DhcpScore {
...
filename: Some("undionly.kpxe".to_string()),
Define it in its implementation in modules/okd/bootstrap_dhcp.rs
impl OKDDhcpScore {
...
Self {
dhcp_score: DhcpScore::new(
...
Some("undionly.kpxe".to_string()),
Update the interpret (function called by the execute fn of the interpret) so it now updates the filename field value in modules/dhcp.rs
impl DhcpInterpret {
...
let filename_outcome = match &self.score.filename {
Some(filename) => {
let dhcp_server = Arc::new(topology.dhcp_server.clone());
dhcp_server.set_filename(&filename).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename to {filename}"),
)
}
None => Outcome::noop(),
};
if next_server_outcome.status == InterpretStatus::NOOP
&& boot_filename_outcome.status == InterpretStatus::NOOP
&& filename_outcome.status == InterpretStatus::NOOP
...
Ok(Outcome::new(
InterpretStatus::SUCCESS,
format!(
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}]",
self.score.boot_filename, self.score.boot_filename, self.score.filename
)
...