Skip to content

den.hosts.<system>.<name>.includes is accepted and silently dropped #663

Description

@sini

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:

  1. 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.
  2. 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.

  1. 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.

  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions