Skip to content

user-class forwarder ignores config arriving via provides.to-users #473

@max-miller1204

Description

@max-miller1204

user-class forwarder ignores config arriving via provides.to-users

Summary

Config written to the user class via provides.to-users (or provides.<userName>) from a host aspect is silently dropped — it never reaches users.users.<userName>. The same pattern works correctly for the homeManager class.

The root cause is in modules/aspects/provides/os-user.nix: the forwarder reads user.aspect directly, bypassing den.ctx.user and therefore missing everything that mutual-provider contributes (including provides.to-users). The homeManager forwarder, in contrast, routes through userEnvAspect in nix/lib/home-env.nix which explicitly includes den.ctx.user.

Minimal repro

A host aspect declares both homeManager and user class config, both via provides.to-users:

den.aspects.igloo = {
  provides.to-users.homeManager.programs.jq.enable = true;  # works
  provides.to-users.user.extraGroups = [ "input" ];          # dropped
};

Evaluating the host:

  • config.home-manager.users.<u>.programs.jq.enabletrue
  • config.users.users.<u>.extraGroups → does not contain "input"

Direct use on the user aspect (not via to-users) works:

den.aspects.tux.user.extraGroups = [ "input" ];   # works — ends up in users.users.tux.extraGroups

Requires den.schema.user.classes to include "user" (it does in my setup).

Root cause

In modules/aspects/provides/os-user.nix:

fwd = { user, host }: den.provides.forward {
  each = lib.singleton user;
  fromClass = _: "user";
  intoClass = _: host.class;
  intoPath = _: [ "users" "users" user.userName ];
  fromAspect = _: den.lib.parametric.fixedTo { inherit host user; } user.aspect;  # <-- reads user.aspect directly
  adaptArgs = args: args // { osConfig = args.config; };
};

den.ctx.user.includes = [ fwd ];

user.aspect is just the named user aspect (e.g. den.aspects.tux). It does not include contributions from den.ctx.user.includes — specifically, it does not include what mutual-provider forwards from host aspects via provides.to-users, provides.to-hosts, or provides.<userName>.

Compare to nix/lib/home-env.nix (the homeManager forwarder):

userEnvAspect = ctxName: { host, user }: { class, aspect-chain }: {
  includes = [
    (den.ctx."${ctxName}-user" { inherit host user; })
    (den.ctx.user { inherit host user; })    # <-- full user context, including mutual-provider
  ];
};

forwardToHost = { className, ctxName, forwardPathFn }: { host, user }:
  den.provides.forward {
    ...
    fromAspect = _: userEnvAspect ctxName { inherit host user; };  # <-- uses userEnvAspect
  };

homeManager reads the full den.ctx.user, so provides.to-users.homeManager.* lands correctly. user reads the raw aspect and misses everything mutual-provider contributes.

Suggested fix

Change fromAspect in os-user.nix so it reads the resolved user context rather than the raw aspect. The simplest swap that matches the homeManager path:

fromAspect = _: den.ctx.user { inherit host user; };

Or, if a self-referential dependency is a concern there, factor out a helper analogous to userEnvAspect that's usable by both paths.

Why this matters

provides.to-users is the documented way for a host aspect to contribute user-scoped config without hardcoding usernames. Today that works for HM-class config but not for OS-level user config (extraGroups, packages, shell, etc.) that the user class is supposed to forward. Users end up with users.users.<hardcoded-name> in host aspects, which re-introduces exactly the coupling the user class exists to remove.

Environment

  • den rev: 0696fb8b155f1829f8bfe37013c54ab6ee80abbe
  • nixpkgs: nixos-unstable (as of 2026-04-19)
  • flake using flake-parts + import-tree
  • den.schema.user.classes = [ "homeManager" "user" ]

Repro flake

Happy to provide a self-contained minimal flake if the above doesn't reproduce.

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