Summary
Writing includes = [ … ] directly on a host entity does nothing. The key is
accepted by the freeform host submodule, never read by the resolver, and
discarded with no warning and no error. The host builds successfully with an
empty aspect tree.
This is a silent drop on a plausible, discoverable spelling — includes is the
activation key everywhere else in den (den.aspects.<n>.includes,
den.schema.host.includes, den.default.includes), so writing it on the host
entity is the natural guess.
Found while supporting #660 / discussion #662, where a third-party integration
guide used the host-level spelling in two of its three patterns. The user's
hosts evaluated cleanly and contained nothing.
Reproduction
{ den, inputs, ... }:
{
imports = [ inputs.den.flakeModule ];
den.aspects.base = {
nixos = {
networking.hostName = "from-base";
fileSystems."/" = { device = "/dev/null"; fsType = "auto"; };
boot.loader.grub.enable = false;
system.stateVersion = "25.05";
};
};
# negative: per-host includes
den.hosts.x86_64-linux.igloo = {
includes = [ den.aspects.base ];
};
# positive control: aspect-level includes, same aspect, same run
den.aspects.iceberg.includes = [ den.aspects.base ];
den.hosts.x86_64-linux.iceberg = { };
}
$ nix eval --impure --expr 'let f = builtins.getFlake "path:."; in {
perHostIncludes = f.nixosConfigurations.igloo.config.networking.hostName;
aspectIncludes = f.nixosConfigurations.iceberg.config.networking.hostName;
}'
{ aspectIncludes = "from-base"; perHostIncludes = "nixos"; }
"nixos" is nixpkgs' default — nothing from den.aspects.base reached igloo.
Measured against c7ef3f1, nixpkgs nixos-unstable (26.11pre1062397).
Cause
resolveEntity builds the entity root aspect's includes from exactly two
sources — the entity's self-provide (den.aspects.<name> via lookupAspect)
and the schema-level collection:
nix/lib/resolve-entity.nix:73
includes = selfProvide ++ schemaIncludes;
Nothing reads includes off the entity instance. The instance type is
constructed with strict = false:
nix/lib/entities/host.nix:55-57
schemaLib.mkInstanceType den.schema.host {
strict = false;
…
so the freeform type absorbs the key instead of raising "option does not
exist". The two behaviours are individually reasonable and jointly silent.
The only signal emitted is indirect and easy to miss — if the user also has no
den.aspects.<hostName>, lookupAspect warns:
nix/lib/entities/_types.nix:19-24
lib.warn "den.aspects.${config.name} not defined — entity gets empty aspect" { }
That warning describes a different fact (no aspect by that name) and does not
mention the dropped key. If the user does have a same-named aspect, there is no
output at all.
Expected
One of, in preference order:
- Honour it. Fold instance-level
includes/excludes into the entity
root's collections, so the key means what it means everywhere else. This is
the least surprising outcome and needs no user-facing deprecation.
- Reject it. Reserve
includes/excludes on entity instances and throw a
den:-prefixed error naming the correct spelling.
Silently discarding a key that is the activation verb of the whole system is
the one outcome that shouldn't survive.
Whichever way it goes, the same question applies to excludes, and to the
other schema collections (isEntity, isolated) written at instance level.
Suggested test
templates/ci/modules/features/deadbugs/ — a paired cell, since the failure
mode is "produces a plausible value" rather than "throws":
test-host-instance-includes-honoured — host-level includes delivers.
test-control-aspect-includes-honoured — same aspect via
den.aspects.<host>.includes, same run, guarding against a false green from
the aspect never having worked.
If the resolution is (2), the first cell becomes an expectedError on the
den: message instead.
Related findings, not filed here
Two more from the same investigation. Both are arguably working as designed;
filing separately if you want them.
-
os-class content silently skips custom OS classes. The built-in
os-to-host policy is gated on builtins.elem host.class [ "nixos" "darwin" ] (modules/aspects/batteries/os-class.nix:30-44), so
den.aspects.x.os = { … } vanishes on a host with a user-registered class.
Measured: MISSING. Same shape as the above — a convenience class quietly
becoming inert. Arguably it should fall through to host.class for any
registered class.
-
The always-on unfree-predicate / insecure-predicate batteries hard-code
nixpkgs.config. Both import a module into ${host.class} that sets
config.nixpkgs.config.*
(modules/aspects/batteries/unfree/unfree-predicate-builder.nix:25-27,38-40
and the insecure sibling). Since a mkIf false definition still requires the
option to exist, any custom OS class whose module set lacks nixpkgs.config
fails at eval with The option `nixpkgs.config' does not exist — even when
neither battery is used. This makes every non-nixpkgs-shaped builder need a
shim before it can boot at all. Gating the import on the target class
declaring the option, or on the battery actually being used, would remove
that.
Summary
Writing
includes = [ … ]directly on a host entity does nothing. The key isaccepted by the freeform host submodule, never read by the resolver, and
discarded with no warning and no error. The host builds successfully with an
empty aspect tree.
This is a silent drop on a plausible, discoverable spelling —
includesis theactivation key everywhere else in den (
den.aspects.<n>.includes,den.schema.host.includes,den.default.includes), so writing it on the hostentity is the natural guess.
Found while supporting #660 / discussion #662, where a third-party integration
guide used the host-level spelling in two of its three patterns. The user's
hosts evaluated cleanly and contained nothing.
Reproduction
"nixos"is nixpkgs' default — nothing fromden.aspects.basereachedigloo.Measured against
c7ef3f1, nixpkgsnixos-unstable(26.11pre1062397).Cause
resolveEntitybuilds the entity root aspect'sincludesfrom exactly twosources — the entity's self-provide (
den.aspects.<name>vialookupAspect)and the schema-level collection:
nix/lib/resolve-entity.nix:73Nothing reads
includesoff the entity instance. The instance type isconstructed with
strict = false:nix/lib/entities/host.nix:55-57so the freeform type absorbs the key instead of raising "option does not
exist". The two behaviours are individually reasonable and jointly silent.
The only signal emitted is indirect and easy to miss — if the user also has no
den.aspects.<hostName>,lookupAspectwarns:nix/lib/entities/_types.nix:19-24That warning describes a different fact (no aspect by that name) and does not
mention the dropped key. If the user does have a same-named aspect, there is no
output at all.
Expected
One of, in preference order:
includes/excludesinto the entityroot's collections, so the key means what it means everywhere else. This is
the least surprising outcome and needs no user-facing deprecation.
includes/excludeson entity instances and throw aden:-prefixed error naming the correct spelling.Silently discarding a key that is the activation verb of the whole system is
the one outcome that shouldn't survive.
Whichever way it goes, the same question applies to
excludes, and to theother schema collections (
isEntity,isolated) written at instance level.Suggested test
templates/ci/modules/features/deadbugs/— a paired cell, since the failuremode is "produces a plausible value" rather than "throws":
test-host-instance-includes-honoured— host-levelincludesdelivers.test-control-aspect-includes-honoured— same aspect viaden.aspects.<host>.includes, same run, guarding against a false green fromthe aspect never having worked.
If the resolution is (2), the first cell becomes an
expectedErroron theden:message instead.Related findings, not filed here
Two more from the same investigation. Both are arguably working as designed;
filing separately if you want them.
os-class content silently skips custom OS classes. The built-inos-to-hostpolicy is gated onbuiltins.elem host.class [ "nixos" "darwin" ](modules/aspects/batteries/os-class.nix:30-44), soden.aspects.x.os = { … }vanishes on a host with a user-registered class.Measured:
MISSING. Same shape as the above — a convenience class quietlybecoming inert. Arguably it should fall through to
host.classfor anyregistered class.
The always-on
unfree-predicate/insecure-predicatebatteries hard-codenixpkgs.config. Both import a module into${host.class}that setsconfig.nixpkgs.config.*(
modules/aspects/batteries/unfree/unfree-predicate-builder.nix:25-27,38-40and the insecure sibling). Since a
mkIf falsedefinition still requires theoption to exist, any custom OS class whose module set lacks
nixpkgs.configfails at eval with
The option `nixpkgs.config' does not exist— even whenneither battery is used. This makes every non-nixpkgs-shaped builder need a
shim before it can boot at all. Gating the import on the target class
declaring the option, or on the battery actually being used, would remove
that.