Tags are the only composition primitive in firn. A module joins zero or
more tags by declaring them in its .bnix source; a host enables a list of
tags; the resolver computes the active module set by union-then-subtract.
The legacy myConfig.bundles.* pattern (per-bundle sub-options + mkDefault
proxies) has been removed — both the bundles/ directory and the bundle CLI
node are gone. The audit that drove the schema decisions still lives at the
bottom of this document for historical context.
;; modules/firefox/default.bnix — module joins tags
:tags [browsers gui-only]
:tag-overrides {browsers {:myConfig.modules.firefox.default true}}
;; hosts/whiterabbit/enabled-tags.bnix — host enables tags
{:enabled [terminal
[browsers -gjoa]
[theming -nerd-fonts]]
:disabled [piper]}Resolution: every module that declares a tag in :tags is enabled when the
host enables that tag, unless the host writes -<module> in that tag's edit
list or lists the module in :disabled. Modules with :tags-opt-in only join
when the host writes +<module> in a matching tag.
modules/<name>/default.bnix may carry three tag-related clauses. All three are
optional; a module with no tags is reachable only via direct host wiring or via
another module's imports.
#lang beagle/nix
(ns default)
(nix/module [config lib pkgs ...]
{:options.myConfig.modules.firefox.enable
(lib.mkEnableOption "Enable Firefox browser")
;; ---- TAG MEMBERSHIP ----
:tags [browsers gui-only] ;; default-on memberships
:tags-opt-in [headless-ok] ;; opt-in-only memberships
:tag-overrides ;; per-tag value-overrides
{browsers {:myConfig.modules.firefox.default true}}
:config
(lib.mkIf config.myConfig.modules.firefox.enable
{ ;; package + service wiring …
})})A vector of bare symbols. Each entry is a tag this module joins by default.
When a host enables one of these tags, the module is enabled — unless the host
explicitly writes -firefox in that tag's edit list or lists firefox in
:disabled.
Naming convention: kebab-case, no namespace prefix. Tags are flat; there is no hierarchy.
Same shape as :tags, but the membership only activates when the host writes
+firefox in the tag's edit list. This is for "this module makes sense for tag
T but I don't want every T host to get it by default" — e.g. a beta browser
under browsers, or a heavy LSP variant under dev.
Omit the clause when empty. Do not write :tags-opt-in [].
A map keyed by tag-symbol. Each value is an attrset of option-path => value
pairs that should be applied (as mkDefault) when the module is enabled
because of that tag. The audit found 9 cases that need this:
- 8 in
bundles/browsers/default.bnix— every browser module exports a.defaultbool (mark-as-default-browser) and the bundle proxied a<browser>.defaultslot through. Under the new model, thefirefoxmodule declares:tag-overrides {browsers {:myConfig.modules.firefox.default true}}and the resolver setsfirefox.default = truewheneverbrowsersis the reason firefox is on. - 1 in
bundles/theming/default.bnix—stylix.chosenThemeis a string-enum value-override, modelled the same way.
The remaining 132 mkDefault hits in the legacy bundles are pure enable-wiring
and collapse into plain :tags membership — no override needed.
Overrides apply only when the tag is the default-on reason for the module
being active. A module pulled in via +name does not get the override.
This keeps "I opted in manually" semantically distinct from "the tag claimed
me."
hosts/<host>/enabled-tags.bnix:
#lang beagle/nix
(ns enabled-tags)
{:enabled
[terminal ;; bare = defaults, no edits
[browsers -gjoa +qutebrowser-experimental] ;; vector = edits applied
[theming -nerd-fonts]
dev]
:disabled [piper auto-upgrade]}A vector of entries. Each entry is either:
- A bare tag symbol (
terminal,dev) — enable the tag with no edits. Every module that lists this tag in:tagsis activated. - A vector
[<tag> <flag>…]— enable the tag with per-tag edits. Each flag is a symbol prefixed-or+:-mnameremovesmnamefrom this tag's default-on set. Scoped to this tag — ifmnameis also pulled in by another enabled tag, that one still counts. To kill the module everywhere, put it in:disabled.+mnameaddsmnameto this tag, but only ifmname's:tags-opt-inlist includes this tag. Otherwise the validator errors out.
A vector of module names. Applied after the union. A module here is off no matter how many tags would activate it. Use for "I never want this module on this machine," independent of tag mix.
active := union over each enabled-tag T of (
defaults := { m | T ∈ m.:tags }
minuses := { x | -x ∈ T's edit-flags }
pluses := { x | +x ∈ T's edit-flags AND T ∈ x.:tags-opt-in }
(defaults \ minuses) ∪ pluses
)
enabled-modules := active \ host.:disabled
Notes:
- Per-tag minus.
-firefoxunderbrowsersonly removes firefox from browsers' contribution; ifgui-onlyalso lists firefox, firefox stays on. - Validated plus.
+qutebrowser-experimentalunderbrowsersfails the validator if thequtebrowser-experimentalmodule doesn't listbrowsersin:tags-opt-in. - Disabled is absolute.
:disabledwins over every tag, opt-in or otherwise.
For each active module M, the resolver also walks each tag T where M ∈ T's
defaults (not pluses), and applies any M.:tag-overrides[T] entries as
mkDefault on M's options. The host's own settings beat mkDefault, so any
explicit value the host writes in its configuration.bnix still wins.
Modules:
;; modules/git/default.bnix
:tags [dev terminal]
;; modules/ripgrep/default.bnix
:tags [dev terminal]
;; modules/firefox/default.bnix
:tags [browsers gui-only]Host:
;; hosts/whiterabbit/enabled-tags.bnix
{:enabled [dev terminal browsers]}Resolved active set: {git, ripgrep, firefox}. No edits, no opt-ins, no
disables. The host gets the tag's full default-on roster.
Modules (same as above).
Host:
{:enabled [dev
[browsers -firefox]]}Resolved: {git, ripgrep}. Firefox is on browsers by default, but the host
subtracted it under browsers. Note that if firefox also had :tags [gui-only]
and the host enabled gui-only, firefox would still be active via that tag.
Modules:
;; modules/qutebrowser-experimental/default.bnix
:tags-opt-in [browsers] ;; never on by defaultHost:
{:enabled [[browsers +qutebrowser-experimental]]}Resolved active set includes qutebrowser-experimental. Without the +,
enabling browsers alone would not pull it in (because it lives in
:tags-opt-in, not :tags).
If the host wrote +qutebrowser-experimental under a tag the module does
not list in :tags-opt-in (e.g. [terminal +qutebrowser-experimental]),
the validator rejects the host config with a tag/opt-in mismatch error.
Modules:
;; modules/piper/default.bnix
:tags [dev]Host:
{:enabled [dev]
:disabled [piper]}Resolved: piper is off. Even though dev would activate it by default,
the global :disabled list applies last and outranks every tag-based
contribution.
This is the right tool when you want a module gone everywhere it touches — no matter how many tags would otherwise pull it in.
;; modules/firefox/default.bnix
:tags [browsers gui-only]
:tag-overrides {browsers {:myConfig.modules.firefox.default true}}Host A:
{:enabled [browsers]}Resolved: firefox is on (via browsers defaults), and firefox.default is
set to true via the override. The host can still write
:myConfig.modules.firefox.default false in its configuration.bnix to win
over the mkDefault.
Host B:
{:enabled [gui-only]}Resolved: firefox is on (via gui-only defaults), but firefox.default is
not set by the override — the :tag-overrides map only fires for the
specific tag that listed it, and gui-only has no override entry.
Host C:
{:enabled [[browsers +firefox]]} ;; hypothetical: firefox in :tags-opt-inIf firefox lived in :tags-opt-in [browsers] rather than :tags [browsers],
a +firefox plus would not trigger the override. Overrides apply to
default-on memberships only.
firn tag enable <tag> # add <tag> to :enabled (no flags)
firn tag disable <tag> # remove <tag> from :enabled entirely
firn tag opt-in <tag> <module> # add +<module> under <tag>
firn tag opt-out <tag> <module> # add -<module> under <tag>
firn tag status # show :enabled / :disabled + resolved active set
firn module enable <module> # remove <module> from :disabled (un-blacklist)
firn module disable <module> # append <module> to :disabled (hard off)Tag edits mutate hosts/<current-host>/enabled-tags.bnix in place through the
AST-aware, idempotent tag-edit parser. Module enable/disable edits the host's
:disabled set directly.
firn repo validatechecks each module's:tagsand:tags-opt-inentries are bare symbols, that:tag-overridespaths exist in the schema, and that no module declares the same tag in both:tagsand:tags-opt-in.firn repo validatechecks each host's:enabledflags reference real modules, that every+nameflag matches a module whose:tags-opt-inlists this tag, and that every name in:disabledis a real module.firn tag resolve <host>is the runtime debugger — it prints the per-tag contributions, the unions, the disabled subtraction, and the final active set.
The reference fixture lives at
docs/fixtures/tags-example.bnix and is the
canonical syntax that the parser must accept.