Facts are on by default; disabling is opt-out, env- and flag-controllable, and skips resolution
Status: proposed. This records the decided model; the --disable/FACTS_DISABLE surface, the resolver gating, and the reserved control key are implemented by a dedicated OpenSpec change.
Facts today has only an opt-out blocklist that filters already-resolved facts out of output, configured by the Facter-compatible blocklist key. This change makes disabling a first-class facts-native control and gives it teeth. Every fact is on by default — the voluminous packages fact (ADR-0014) included — and a fact is removed only by disabling it. The disabled set is the union of three inputs: the CLI --disable a,b,c (comma-separated), the FACTS_DISABLE=a,b,c environment variable, and the facts-native facts.conf disable key. disable is the native term across all three surfaces; the Facter blocklist key keeps working as its compatibility alias — facter.conf is part of the binding input contract, so blocklist is superseded in name by disable, never removed, with native disable winning on collision. A disable target is a fact name or a fact group, reusing the existing group expansion (BlocklistedFactsWithGroups), so --disable networking drops the whole group. There is no opt-in, allowlist, or default-off tier; the model is purely all-on with subtractive disabling.
Disabling aims to skip work, not just output — but the gating is per-resolver and per-probe, not a blanket per-name skip, because the resolver-to-top-level-fact map is many-to-one. Three classes follow:
- A top-level fact produced by its own standalone resolver is resolution-gated: disabling it skips that resolver.
packagesis this case —packagesCoreFactsproduces onlypackages, so--disable packagesskips package collection entirely. (networking,processors,memory,timezone,ssh,fips,augeasare likewise single-output.selinuxis not in this set: it emits asos.selinux.*descendants, so a name-level--disable selinuxis a no-op — it stays eager and is disabled viaos.selinux.) - A top-level fact that shares a multi-output category resolver —
osCoreFacts→os/kernel/filesystems,disksCoreFacts→disks/partitions/mountpoints/zfs/zpool,uptimeCoreFacts→load_averages/system_uptime— is gated only when every top-level fact that resolver produces is disabled; otherwise the resolver runs and the disabled outputs are pruned. - A fact built inline in
buildCoreFactswith no resolver seam (facterversion,is_virtual/virtual,path) is always resolve-then-prune.
Gating is therefore defined per probe, not per category: a shared memoized probe — cachedIdentity, which feeds both the identity fact and the SSH privilege gate; or DMI, which feeds gce and virtualization — is skipped only when every one of its consumers is disabled. Where a kept fact still needs a disabled fact’s probe, that probe runs and the disabled fact is prune-only (its work is still paid). packages shares no probe, so disabling it fully skips its work — the case the design optimizes for. This gating is new machinery: today every core fact builds eagerly (buildCoreFacts takes no disabled-set parameter) and the blocklist filters afterward. Implementing it requires a fact/group-to-resolver mapping and plumbing the disabled set into buildCoreFacts; what is reused from the existing blocklist is the disabled-set expansion (names and groups), not a resolver-skip mechanism, which does not exist today. A disabled sub-fact (os.release) likewise cannot skip its resolver without dropping its siblings, so it is resolve-then-prune through the existing descendant-pruning filter; sub-facts are cheap, so no meaningful work is paid.
Three semantics fix the surface. The inputs union — disabling is additive and order-independent. --no-block is the master override: it clears the entire disabled set for that run, including --disable and FACTS_DISABLE, and resolves everything. Disable beats query — a disabled fact named in a query returns nothing, matching Facter’s blocklist-beats-query and the engine’s existing order (the disabled set is applied before query projection). Because the disabled set can come from the ambient environment (FACTS_DISABLE exported in a shell rc) or from config, a query for a fact disabled by a source other than the same command line emits a one-line stderr diagnostic (e.g. fact "packages" is disabled by FACTS_DISABLE) while keeping stdout empty, so a silently-empty result is diagnosable. A cache-ordering invariant must hold: the disabled set is subtracted before the cache is consulted and before any cache write, and a pruned sub-fact is never persisted into a cached group — so a disabled fact is never served stale from cache (this preserves today’s order, where FilterBlockedFacts precedes cache resolution).
FACTS_DISABLE is a reserved control key, reserved by its resolved fact name, not its literal spelling. The FACTS_*/FACTER_* prefixes are the external-fact environment namespace, and environmentFactName lowercases the variable and strips any of facts_, facts, facter_, or facter — so FACTS_DISABLE, FACTSDISABLE, FACTER_DISABLE, and FACTERDISABLE all resolve to an external fact named disable. The environment-fact loader therefore drops any variable whose resolved fact name is disable (and the other reserved control names) rather than reserving one literal string, covering every prefix spelling uniformly. The documented cost is that an external environment fact literally named disable cannot be defined.
Adding --disable/FACTS_DISABLE and any facts-native fact groups (such as the packages group) diverges the Facter-mirrored --list-block-groups/--list-cache-groups output from Ruby Facter. This is accepted: facts-native ergonomics over the same disabled set, consistent with the facts-native-names-with-facter-compat-inputs pattern.
Considered Options
- Opt-in / default-off optional tier (enable to turn things on) — rejected: it requires per-fact “optional” metadata and a query-auto-enable path, and it makes
packagesinvisible until asked. The all-on model is simpler and predictable, and resolution-gating still skipspackages’ work for anyone who disables it. - Display-only disable (resolve, then hide) — rejected: it would still pay
packages’ full collection cost on every run; resolution-gating skips the work at the resolver seam. - Blocklist-only, no CLI or env (Facter parity as-is) — rejected: it forces a config file for a one-off and offers no environment control;
--disableandFACTS_DISABLEare the facts-native ergonomics operators expect. - All-on, disable-only, resolution-gated (chosen) — predictable, minimal new user-facing surface, reuses the existing blocklist/group set-expansion, and the disable path skips real work for standalone-resolver facts like
packages. The new machinery it requires — a fact-to-resolver map and a disabled-set parameter intobuildCoreFacts— is contained and does not change how existing categories resolve.