Skip to content

feat(integrations): check drift between every activated configuration and the one above it - #103

Merged
clhodapp merged 4 commits into
feat/system-manager-childrenfrom
feat/drift-everywhere
Oct 18, 2026
Merged

clhodapp merged 4 commits into
feat/system-manager-childrenfrom
feat/drift-everywhere

Conversation

@clhodapp

@clhodapp clhodapp commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Drift detection used to cover one pair: a home inside a NixOS machine compared /etc/caisson-home-manager/source.json with the machine it was built for. This PR moves it into the framework. Every configuration with an activation now checks the nearest configuration above it that also has an activation, whatever the two types are.

Stacked on #102.

How it works

  • The integrations record it. An integration whose configurations are activated passes activation to mkModuleConfiguration, and the manifest records it. identity is the store path of the childless evaluation, meaning the configuration evaluated without the configurations declared inside it. marker is the file the activation writes into. The marker holds the SHA-256 digest of that store path, not the path itself. Computing the path evaluates the childless evaluation and builds nothing. The digest names no store path, so no string context is dropped, and nothing can read the marker as a path.

    Integration Identity Marker
    nixos, darwin, system-manager toplevel /etc/caisson/marker.json
    home-manager activation package $XDG_STATE_HOME/caisson/markers/home-manager-<name>.json
    hjem manifest file .local/state/caisson/markers/hjem-<name>.json in the user's directory

    A machine's record also sets standsAlone. A machine is applied on its own host, so a machine declared inside another, such as an image, checks nothing above it.

  • The library computes it. lib.caisson.integrations gains activatingAncestorOf, markerOf, checksMarkerOf and driftCheckOf. A child finds the nearest activating configuration among its manifest's ancestors, so it never names that configuration's type.

  • Each class writes its marker. The default module of nixos, darwin, systemManager, homeManager and hjem imports activation-marker. It writes the marker only when a configuration declared inside checks it, because computing the identity costs a second evaluation.

  • A home checks it. The home-manager default module imports activation-drift, which runs driftCheckOf as an activation step. A home inside a darwin or system-manager machine is checked the same way as one inside NixOS.

Breaking changes (pre-1.0)

  • The NixOS module home-manager-source-marker and the home-manager module nixos-source-marker are removed. A home that selects its modules itself names caisson/activation-drift instead.
  • caisson.home-manager.machineDrift.warn becomes caisson.drift.warn.
  • The marker file moves to /etc/caisson/marker.json, and its field baseSystemOutPath, a store path, becomes identity, a digest.

Paired consumer fixes: clhodapp/ch-personal-machines and clhodapp/ch-home-manager, branch fix/activation-drift in each.

Testing

  • nix flake check passes, including the unit tests, the pinned-world suite and the eval-weight gate. The system-manager pinned-world test now checks that the machine writes the marker and that the home's check reads it.
  • ch-home-manager's drift-warning-vm passes against this branch on fix/activation-drift. In a NixOS VM it shows five things:
    • the machine writes /etc/caisson/marker.json;
    • the machine's own activation of the home prints no warning;
    • a home built for a different machine warns;
    • a change to the home alone doesn't warn;
    • a missing marker warns.
  • nix flake check --no-build passes in all 18 consumers, with both caisson pins pointed at this branch and ch-home-manager and ch-personal-machines on their fix branches.

Not covered

A hjem configuration declared inside a machine records a marker for the configurations inside it, but it doesn't check the machine itself. hjem's activation runs no step that a configuration can add. Its one hook is the systemd unit actions it reports after a switch, which would put the warning in the journal rather than on the terminal.

Docs: nix-caisson/nix-caisson.github.io, branch docs/drift-everywhere.

🤖 Generated with Claude Code

clhodapp and others added 4 commits October 18, 2026 11:04
… and the one above it

Drift detection was written for one pair: a home inside a NixOS
machine compared `/etc/caisson-home-manager/source.json` with the
machine it was built for. It is now a property of the framework.

An integration whose configurations are activated passes `activation`
to `mkModuleConfiguration`: `identity`, the store path of the childless
evaluation, and `marker`, the file its activation writes that identity
into. NixOS, darwin and system-manager record the system and
`/etc/caisson/marker.json`; a home records its activation package and a
file in the state directory of its user; a hjem configuration records
its manifest file and a file in its directory. A machine `standsAlone`:
it is applied on its own host, so it checks nothing above it.

`lib.caisson.integrations` gains `activatingAncestorOf`, `markerOf`,
`checksMarkerOf` and `driftCheckOf`. Each activating class writes its
marker from its `default` module (`activation-marker` for nixos, darwin,
systemManager, homeManager and hjem) when a configuration declared
inside it checks it. A home runs `driftCheckOf` against the nearest
activating configuration above it from the home-manager module
`activation-drift`, so a home inside a darwin or system-manager machine
is checked as one inside NixOS is. caisson now registers a `default`
home-manager module, which imports `activation-drift` and
`activation-marker`, and a `default` hjem module, which imports
`activation-marker`.

The NixOS module `home-manager-source-marker` and the home-manager
module `nixos-source-marker` are removed. The option
`caisson.home-manager.machineDrift.warn` becomes `caisson.drift.warn`,
and the marker field `baseSystemOutPath` becomes `identity`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rker

A drift marker held the store path of the childless evaluation with its
string context dropped, which made it a string that looks like a store
path but is not tracked as one. The marker and the check now hold the
SHA-256 digest of that store path. Computing it evaluates the childless
evaluation and builds nothing, and the digest names no store path, so
nothing depends on it and nothing can read it as a path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test of the units that activate the homes dropped the string
context of the activation package and of the unit script to search one
in the other. It now reads the context of the script, which records the
store paths the script refers to, and checks that it holds the
activation package of the home and not that of the other home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b6f1ff3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@clhodapp
clhodapp force-pushed the feat/drift-everywhere branch from 5d5fbf3 to 5c79217 Compare October 18, 2026 18:04
@clhodapp
clhodapp merged commit 825a72c into main Oct 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant