Policy Activation Deep Dive
Registry vs activation
Section titled “Registry vs activation”den.policies is a registry — a named collection of policy values.
Registering a policy does not activate it:
# This only registers. The policy will never fire.den.policies.my-policy = { host, ... }: [ (policy.resolve { flag = true; })];Activation happens when a policy value appears in an includes list:
# Now it fires for all hostsden.schema.host.includes = [ den.policies.my-policy ];This separation is the core design principle: policies are reusable values that can be activated, deactivated, and filtered independently of where they’re defined.
How policies become values
Section titled “How policies become values”The module system wraps bare functions into tagged records:
den.policies.foo = { host, ... }: [ ... ] ↓ (module system merge){ __isPolicy = true; name = "foo"; fn = { host, ... }: [ ... ]; }The __isPolicy tag lets the pipeline distinguish policies from aspects
in includes lists. Both are valid include entries — the pipeline
checks the tag and routes accordingly.
Activation paths
Section titled “Activation paths”Policies can be activated at any level of the include hierarchy:
| Level | Where | Scope |
|---|---|---|
| Global | den.default.includes | All entities |
| Per entity kind | den.schema.host.includes | All hosts |
| Per aspect | den.aspects.igloo.includes | The igloo subtree |
| Inline | { includes = [ den.policies.foo ]; } | The enclosing aspect |
Cascading
Section titled “Cascading”When a policy is activated at a scope, it cascades to all descendant
scopes. A policy in den.schema.host.includes fires at the host scope
and is also available at user scopes resolved from that host.
Aspect-level policies
Section titled “Aspect-level policies”Aspects can define policies inline using the policies attribute:
den.aspects.igloo = { policies.to-users = { host, user, ... }: lib.optional someCondition (policy.include { nixos.some.option = true; });
includes = [ den.aspects.igloo.policies.to-users ];};The policy is registered on the aspect and activated via includes in
the same definition.
Deactivation with excludes
Section titled “Deactivation with excludes”excludes is a first-class top-level key on aspects, symmetric with
includes. It prevents policies from firing in the aspect’s subtree:
den.aspects.igloo = { includes = [ den.policies.add-marker ]; excludes = [ den.policies.add-marker ];};Authoritative semantics
Section titled “Authoritative semantics”Parent excludes are authoritative — child scopes cannot re-enable an excluded policy:
parentAspect = { excludes = [ den.policies.blocked ]; includes = [ childAspect ];};
childAspect = { # This does NOT re-enable the policy. The parent's exclude wins. includes = [ den.policies.blocked ];};This prevents downstream aspects from bypassing security or correctness constraints established by parent scopes.
Identity through wrappers
Section titled “Identity through wrappers”Excludes match on policy identity. When a policy is wrapped with
policy.for or policy.when, the inner policy’s identity is preserved:
wrapped = den.lib.policy.for entity den.policies.my-policy;
# This exclude still works — it matches the inner policy identityexcludes = [ den.policies.my-policy ];Conditional firing
Section titled “Conditional firing”den.lib.policy.for entity policy
Section titled “den.lib.policy.for entity policy”Wraps a policy to fire only when a specific entity is in context.
Matching uses id_hash for robust identity comparison:
den.schema.host.includes = [ (den.lib.policy.for den.hosts.x86_64-linux.igloo den.policies.igloo-specific)];The wrapper reads the current scope’s entity (the entity under the
context’s __entityKind key) and compares its id_hash against the
target entities’ hashes. If it doesn’t match, the policy returns [].
Accepts a single policy value or a list:
den.lib.policy.for entity [ den.policies.policy-a den.policies.policy-b]# Returns a list of two wrapped policiesden.lib.policy.when predicate policy
Section titled “den.lib.policy.when predicate policy”Wraps a policy to fire only when a predicate returns true:
den.schema.host.includes = [ (den.lib.policy.when ({ host, ... }: host.wsl.enable) den.policies.wsl-support)];The predicate receives the full context attrset. If it returns false,
the policy returns [].
Composing wrappers
Section titled “Composing wrappers”Wrappers compose — when wrapping for wrapping a raw policy:
den.lib.policy.when (ctx: ctx.flag or false) (den.lib.policy.for entity den.policies.my-policy)The outer wrapper runs first. If the predicate fails, the inner wrapper never executes.
Dispatch cycle
Section titled “Dispatch cycle”When the pipeline reaches an entity scope, policy dispatch follows a fixed-point iteration:
- Collect — gather all active policies from the scope’s include chain (direct includes, schema includes, default includes).
- Check satisfaction — for each policy, introspect its function args. A policy fires only when all required args (non-default) are present in the current context.
- Fire — call satisfied policies, collect their effects.
- Enrich — if any effects add new context bindings
(
policy.resolvewith enrichment), merge them viascope.provideand drain deferred includes. - Iterate — if enrichment introduced new context keys, repeat from step 2. The new bindings may now satisfy previously-unsatisfied policies. Convergence is key-monotonic: only newly-added keys drive another round; changing an existing key’s value does not.
- Converge — when a round adds no new enrichment keys, emit all collected effects.
This fixed-point iteration means policies can depend on context that
other policies provide. The loop is capped at 10 iterations; if new keys
are still appearing past that, the pipeline throws a den: enrichment cycle error rather than looping forever.
Self-exclusion invariant
Section titled “Self-exclusion invariant”A policy does not apply to its own outputs. The source policy name is
threaded through the resolve chain, seeding firedPolicies at child
scopes. This prevents infinite recursion when a policy’s effects would
satisfy its own firing condition.
Policy include dedup
Section titled “Policy include dedup”Aspects injected by a policy’s include effect are tagged with the
source policy’s name — <policy:name> — so the gate can dedup them. When
one policy emits several includes, an [idx] suffix
(<policy:name>[0], <policy:name>[1], …) keeps them distinct instead
of collapsing into a single entry. This prevents the same policy-supplied
content from being emitted twice when the policy fires under multiple
scopes.
See also
Section titled “See also”- Policies — conceptual overview
- den.policies reference — effect types and built-in policies
- CI tests —
policy-excludes.nix,policy-for-include.nix,policy-context-enrichment.nix