Changelog
All notable changes to knixl are recorded here. The format follows
Keep a Changelog, and knixl uses
semantic versioning. Release notes on
GitHub are generated from the matching section here (cargo-dist reads this file);
see docs/release-changelog.md for how each entry is written.
1.5.2 - 2026-09-30
Section titled “1.5.2 - 2026-09-30”Release artefacts now carry GitHub build provenance.
- Each prebuilt archive on the GitHub release has a build provenance
attestation, so
gh attestation verify <archive> -R 1stvamp/knixlchecks it was built by this repository’s release workflow, andmiseverifies it when installing from GitHub releases. Earlier releases have none.
1.5.1 - 2026-09-29
Section titled “1.5.1 - 2026-09-29”The oracle now type-checks two kinds of option it was letting through
unchecked, found by a real build that check had passed.
- A union with a singular enum, e.g.
virtualisation.diskSize("auto"or a positive integer), accepted any value, so a string"16384"passedcheckand only failed atnix build. Thevalue "auto" (singular enum)side is now parsed, and a union’s error lists what each alternative expected. - Paths under
virtualisation.vmVariant(andvmVariantWithBootLoader,vmVariantWithDisko) andspecialisation.<name>.configurationare now checked against the host’s option set. They were all left unchecked since 1.5.0. Options only a variant imports, such asvirtualisation.memorySizein the VM variant, are still unchecked there.knixl checkmay now refuse a project it passed before, where such a path carries a value of the wrong type.
1.5.0 - 2026-09-29
Section titled “1.5.0 - 2026-09-29”knixl can now generate the whole system flake for a system that uses other flakes’ NixOS modules (ADR 0014), and hosts and image targets can import hand-written modules.
Nothing here changes the generated output of an existing project: a system {}
without input nodes emits the same flake byte for byte. Anyone using the crates
directly should read the last Changed entry.
- Flake inputs in
system {}:input "disko" url="github:nix-community/disko" [rev="<commit>"] [flake=#false] { follows nixpkgs="nixpkgs" }(#96, ADR 0014). With any input declared,generated/flake.nixtakes those inputs with each rev written into its url, and builds hosts and images withnixpkgs.lib.nixosSystem. Aninput "nixpkgs"is required and replacesnixpkgs-url; its rev is the hosts’ shared baseline, so hosts on different baselines are refused in this mode.knixl upgradepins every other input as aflake-inputline inknixl.lock.kdl, recording a declaredrev=as written and resolving the rest. knixl checkreads the nix-ownedgenerated/flake.lockin input mode and exits 5 when it is missing or pins an input at a different rev thanknixl.lock.kdl(#96). Runnix flake lockingenerated/and commit it.nixpkgs release="unstable" rev="<commit>"on a host pins its baseline to an exact commit, e.g. the one a running system was built from, instead of the release branch tip (#96, ADR 0014).formatter "<attr>"insystem {}adds aformatter.<system>output for each host system, sonix fmtworks (#96).module "disko" input="disko" attr="disko"inoracle-modulestakes a module from a declared input: the oracle validates against it and each host’snixosSystemimportsinputs."disko".nixosModules."disko"(#97, ADR 0014). Hosts only, never installer or guest-image targets.secrets backend="sops-nix" input="sops-nix" { default-file ".."; ssh-key-paths ".." }wires sops-nix into every host of an input-mode flake: the sops module,sops.defaultSopsFile,sops.age.sshKeyPaths, and asops.secrets."<name>" = { }for each(secret)the host references (#98).import "../modules/foo.nix"on a host imports a hand-written NixOS module, merged into the sameimportslist as the host’s side-files, which a raw-niximports = [ ... ]would clash with (#95). The path is relative to the host’s KDL file and has to stay inside the project.importalso works ininstallerandguest-imagetargets, relative toknixl.kdl, so an image module that raw-nix can’t express (aletreadingbuiltins.getEnv, say) carries over unchanged (#102).
Changed
Section titled “Changed”- Library API, for anyone using the crates directly:
LowerOutputgains animportsfield,Lockaflake_inputsfield,OracleModuleaninputfield,SystemConfiginputsandformatter,ProjectConfigsops,GeneratedFilesecrets,FlakeHostsystem,input_modulesandinline_modules, andgather::Projectflake_lock_problems, so constructing any of them by literal no longer compiles.
- The oracle rejected real options inside an option that takes arbitrary keys,
e.g.
nix.settings.experimental-features,nixpkgs.config.allowUnfree, and anything underhome-manager.users.<name>once home-manager is inoracle-modules(#100). Those paths are now let through unchecked, like a submodule’s interior; a declared child such asnix.settings.coresis still type-checked.
1.4.0 - 2026-08-04
Section titled “1.4.0 - 2026-08-04”A breaking change, released as a minor deliberately: KDL that knixl cannot fully interpret is now refused instead of being silently dropped.
Read the first entry below before upgrading. A project carrying a stray or
misspelt node has been generating without it and will start failing, which is the
point of the change. Anyone resolving these crates as ^1 picks this up
automatically, both the CLI behaviour and the library API changes noted at the
end.
Changed
Section titled “Changed”- KDL that knixl cannot fully interpret now refuses to generate, with exit 5
(#85). A node no module claims, or an unknown child of a claimed one, was a
warning at any depth below the top level, so a typo (
timezonfortimezone) was dropped from the emitted Nix whilegeneratewrote the file andcheckexited 0. The lock records the emitted Nix rather than the intent of the KDL, so nothing downstream could see the loss.docs/05-cli.mdalready documented exit 5 as covering a KDL schema error; only the top-level path honoured it. There is no flag to downgrade this: an escape hatch would restore exactly the silently-wrong output. A project carrying a stray or misspelt node has been generating without it and will now fail until the node is fixed or removed. --jsoncarries diagnostics:{"files":[...],"warnings":[...]}normally, and{"validation":[...]}when validation refused (#85). Both only existed on stderr, so CI could not branch on them.- A top-level unclaimed node reports exit 5 rather than exit 1 (#85). It was raised as an internal error, when a typo in the input is validation.
- Library API, for anyone using the crates directly rather than the CLI:
knixl_modules::Diagnosticgains aseverityfield (so constructing one by literal no longer compiles; a newSeverityenum accompanies it), andknixl_pipeline::GenerateError::UnknownNodeis removed in favour ofValidation(#85).
1.3.0 - 2026-08-04
Section titled “1.3.0 - 2026-08-04”Four knobs that previously needed raw-nix, and an oracle that was rejecting
values NixOS accepts.
osgainssession-variable "<NAME>"="<value>", a prop map emittingenvironment.sessionVariables(#86). NixOS writes those into/etc/pam/environment, whichpam_envloads for every session including a non-interactivessh host cmd;environment.variablesreaches interactive shells only, so it is not offered. A name that is a valid bare Nix attribute renders unquoted.osgains repeatedkernel-module "<name>"(boot.kernelModules), the other half of the existingsysctlchild: a sysctl often only exists once its module is loaded, sonet.bridge.bridge-nf-call-iptablesneedsbr_netfilteralongside it (#87).osgains repeatedtmpfiles-rule "<path>" type="d" [mode=] [user=] [group=] [age=] [argument=](systemd.tmpfiles.rules), with the fields named because the bare tmpfiles line is not readable (#88).typeis required; the rest default to-, tmpfiles’ own leave-this-to-the-default marker. Rules keep KDL source order, since tmpfiles applies them in order.nix-ldmodule: repeatedlibrary "<name>"emittingprograms.nix-ld.enableandprograms.nix-ld.libraries(#89), for hosts that run binaries they did not build (a project-pinned rustup toolchain, a prebuilt release binary), where/lib64/ld-linux-x86-64.so.2is NixOS’sstub-ld. The node’s presence is the opt-in, so there is noenablechild. Dotted names work, solibrary "stdenv.cc.cc.lib"emitspkgs.stdenv.cc.cc.lib.
- The oracle no longer rejects a legitimate value for an option whose type is a
top-level union, which is how
nixosOptionsDocrenders aneither(#86, #87).environment.sessionVariableswas typed as an integer andboot.kernelModulesas an attribute set that refuses the list form, so both failed validation withWrongType. Any project setting an option of that shape, through knixl’s own modules or a fetched one, was blocked. - The oracle no longer panics on an option whose type description carries a
multibyte character, such as nixpkgs’
3×3 matrix of floating point numbers. Every command that loads the option set was affected.
1.2.1 - 2026-07-31
Section titled “1.2.1 - 2026-07-31”Image targets generate. Both kinds emitted invalid Nix in 1.2.0, so neither had ever worked.
installerandguest-imagetargets now generate (#81). The base module reached the generatedimportslist as a baremodulesPath + "/...", which cannot be a list element, so every run aborted at the formatter and wrote nothing. Fixed in the emitter, so a raw seam holding a binary expression is bracketed wherever a single value is required.- A formatter failure now reports what the formatter said (#81).
knixl plangave onlyformatter exited non-zero: 1and swallowed the stderr naming the line and column, which for the above meant the cause was invisible. A formatter that rejects its input can also exit before reading all of it, and the resulting broken pipe was reported in place of the real complaint.
1.2.0 - 2026-07-27
Section titled “1.2.0 - 2026-07-27”Incus lxc guest images, and one image-target code path behind them.
guest-image "<name>" [system=]targets (#75): a NixOS system built as an lxc image for Incus, the sibling of the nspawnguestmodule. Alongside asystem {}block the assembly flake gainsnixosConfigurations.<name>and two package outputs,packages.<system>."<name>-lxc"(the rootfs) and"<name>-lxc-metadata", sonix build .#<name>-lxcproduces whatincus image importtakes. knixl builds the image; importing and launching it stays with the operator (ADR 0013).
Changed
Section titled “Changed”- The installer and guest-image paths are now one image-target abstraction
(
ImageKind), so a future image format is a new variant rather than a new code path (#75).
- Two image targets on the same system no longer emit a duplicate
packages.<system>attribute in the assembly flake, which Nix rejects (#75).
1.1.0 - 2026-07-26
Section titled “1.1.0 - 2026-07-26”The homelab-migration feature set: the modules and knobs a real host needed that
were previously worked around with raw-nix (#57-#65).
osmodule: core host config in one place,system.stateVersion, boot loader, kernel package,time.timeZone,i18n.defaultLocale,boot.kernel.sysctl,nix.settings,environment.systemPackages, andusers.mutableUsers(#59, #60).guestmodule: NixOS system containers, a nested module tree re-rooted undercontainers.<name>.config(#64, ADR 0011).installer "<name>": bootable installer media, a generatedinstallation-cdmodule plus a.#<name>-isoflake output (#65, ADR 0012).- disko: pool
options/root-fs-options, filesystemmount-option, and a configurable data-partition label on theboot-root-zfspreset (#57). - A
(scalar)template value form that emits a bound argument as its native Nix bool/int/string instead of a string, and zfsforce-import-rooton top of it (#58). - tailscale
open-firewall(#63) and userhashed-password(#61).
Changed
Section titled “Changed”- incus is now a built-in module: the daemon preseed gained optional bridge ipv6,
a
core.https_addressAPI listener (static or bound to an interface at runtime via a oneshot), and host-firewall integration (#62). - Every host now sets
networking.hostNamefrom its label, overridable withhost { hostname "<name>" }. Existing projects will see a one-line regeneration per host on the nextknixl upgrade.
- The embedded stdlib is self-contained in the published crate, so a fresh
cargo install knixlbuilds (#56).
1.0.0 - 2026-07-24
Section titled “1.0.0 - 2026-07-24”First stable release. knixl compiles opinionated KDL into maintainable, committed, nixfmt-formatted NixOS module source, with a lockfile-backed reproducibility and drift-detection model.
- The generate/check/plan/upgrade/doc/install workflow, with stable exit codes,
and the
Clean/Stale/Drifted/Missing/Orphanedstate model over a lockfile (whole-file taint, ADR 0004). - Built-in and declarative modules: host, postgres, backups, package, raw-nix, web-service, security-headers, zfs, user, openssh, disko, tailscale, incus, home-manager.
- Oracle validation of emitted option paths against a pinned NixOS option set,
including out-of-tree modules declared in
knixl.kdl(ADR 0003, 0008). - Package version pinning with automatic strategy selection, and a per-host nixpkgs baseline (ADR 0005, 0006, 0007).
- Reference-by-name secrets (sops-nix or agenix),
let-hoisting of repeated values, an opt-in system-assembly flake (ADR 0009), and an embedded stdlib plus flake-based fetched modules (ADR 0010). - A TUI for installing packages, browsing modules, and authoring declarative modules.
- Published to crates.io with prebuilt binaries for Linux (gnu and musl) and macOS on x86_64 and aarch64.
