Skip to content

contracts: init - #506343

Draft
KiaraGrouwstra wants to merge 19 commits into
NixOS:masterfrom
KiaraGrouwstra:contracts-automated
Draft

contracts: init#506343
KiaraGrouwstra wants to merge 19 commits into
NixOS:masterfrom
KiaraGrouwstra:contracts-automated

Conversation

@KiaraGrouwstra

@KiaraGrouwstra KiaraGrouwstra commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Implements contracts (NixOS/rfcs#189) by building on the work by @ibizaman (and earlier work by @fricklerhandwerk).

Closes NixOS/rfcs#189.

For more info, see the manual entry or options reference.

Supersedes: #432529, #485453, #495303, #500287.

Disclaimer: I have made use of a coding agent to get things to this state.

Use-cases

Differences from the RFC

  1. Topology: point-to-point vs aggregated: The RFC has each consumer directly link to one provider (consumer.provider = config.services.restic...), with bidirectional references (provider.consumer = config.services.nextcloud...). We use centralized aggregation - consumers register into contracts.<type>.want, providers register into contracts.<type>.providers, and a defaultProvider/defaultProviderName selects which provider fulfills all requests. This avoids the 'dual-link' problem the RFC mentioned.
  2. Modular service support: The RFC doesn't address modular services. We implement a bridge pattern to give modular services the same API for using contracts as NixOS modules.
  3. Cross-system support: The RFC is NixOS-only. Our lib/contracts/module.nix and lib/services/ are system-agnostic, designed to pave the way for contracts support from other systems managing nix (modular) services, e.g. home-manager, nimi, nix-darwin, finix, NixBSD, etc.
  4. Contract type registration: The RFC defines contracts inline as contracts.<name> with deferredModule types. We separate type definitions (contractTypes) from instances (contracts), with types shipped in lib.contracts (usable from the sandbox for docs builds) and extensible from outside nixpkgs via config.contractTypes (without such docs).
  5. Naming: input/output vs request/result: The RFC uses input/output. We use request/result. This is more specific about the directionality (a consumer requests, a provider produces results), and more consistent with SelfHostBlocks.

Tests to run on changes

  • bash lib/tests/modules.sh
  • nix-instantiate --eval lib/services/test.nix
  • nix-build -A nixosTests.contracts
  • nix-build -A stash.passthru.tests.stash
  • nix-build nixos/release.nix -A manual.x86_64-linux

Next steps

None of these are breaking changes necessarily, so may not need to be in-scope for the initial PR:

Things done

  • Built on platform:
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • Tested, as applicable:
  • Ran nixpkgs-review on this PR. See nixpkgs-review usage.
  • Tested basic functionality of all binary files, usually in ./result/bin/.
  • Nixpkgs Release Notes
    • Package update: when the change is major or breaking.
  • NixOS Release Notes
    • Module addition: when adding a new NixOS module.
    • Module update: when the change is significant.
  • Fits CONTRIBUTING.md, pkgs/README.md, maintainers/README.md and other READMEs.

@KiaraGrouwstra
KiaraGrouwstra marked this pull request as ready for review April 3, 2026 16:53
@nixpkgs-ci nixpkgs-ci Bot added 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux. 10.rebuild-darwin: 1-10 This PR causes between 1 and 10 packages to rebuild on Darwin. 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` 6.topic: module system About "NixOS" module system internals 6.topic: lib The Nixpkgs function library labels Apr 3, 2026
@nixpkgs-ci nixpkgs-ci Bot added the 8.has: documentation This PR adds or changes documentation label Apr 3, 2026
@KiaraGrouwstra
KiaraGrouwstra force-pushed the contracts-automated branch 2 times, most recently from 47917e6 to 2d43014 Compare April 3, 2026 17:23
@nixos-discourse

Copy link
Copy Markdown

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/self-host-blocks-building-blocks-for-self-hosting-with-best-practices/26963/43

@KiaraGrouwstra

Copy link
Copy Markdown
Contributor Author

nixpkgs-review result

Generated using nixpkgs-review.

Command: nixpkgs-review pr 506343
Commit: 1c0ed60bc297e2b47d301aa64a401dcf55ea4c37


x86_64-linux

⏩ 2 packages blacklisted:
  • nixos-install-tools
  • tests.nixos-functions.nixos-test
✅ 4 packages built:
  • extra-container
  • nixos-container
  • nixpkgs-manual
  • tests.lib-tests

@KiaraGrouwstra
KiaraGrouwstra force-pushed the contracts-automated branch 3 times, most recently from 1f5d306 to 1fa0650 Compare April 28, 2026 13:34
@KiaraGrouwstra
KiaraGrouwstra force-pushed the contracts-automated branch 4 times, most recently from 60fee56 to bd15605 Compare May 17, 2026 09:04
@KiaraGrouwstra

Copy link
Copy Markdown
Contributor Author

marking as draft again, as having aligned with @ibizaman, having gained confidence on direction here we feel that for reviewing purposes we should go for more manageable chunks such as #495303

@ibizaman

ibizaman commented May 19, 2026

Copy link
Copy Markdown
Contributor

On the latest change related to hardcoded secrets and systemd, although technically correct to rely on systemd, the usefulness is limited because the harcoded secret module is only intended to be used in tests, which can probably all be written acceptably using the system activation script.

@ibizaman ibizaman mentioned this pull request May 20, 2026
20 tasks
ibizaman and others added 19 commits July 1, 2026 18:41
This initial change demonstrates the concept of a contract:
an expectation as verified by a test (here: `fileSecrets` test),
that may be fulfilled by a provider (in our example here: `hardcoded-secret`).

This simple example demonstrates that at their core, contracts do not
technically require any machinery: without the test,
our `hardcoded-secret` 'contract provider' module would still just work:

This just uses existing NixOS machinery of options, types
(with those of the abstract expectation in this change still left implicit:
the interface used by the test to verify expected behavior), and tests.

What this achieves is also just things the module system does:
given the options, one may put info in, which may generate
configuration, as well as lead to calculated options yielding
information one may take back out again.
Add `lib.types.nestedAttrsOf`, as a nested attribute set of a given type.

Further add functions in `lib.attrsets` to process values of this type:

- `isNestedAttrsLeaf`
- `mapNestedAttrsWith`
- `mapNestedAttrs'`
- `concatMapNestedAttrsWith`
- `concatMapNestedAttrs'`

Note that recursion over this type may either require all or any of the
leaf types to be present.

While this type and its helpers are generic in function,
these will later be used for abstracting overc
variable-depth nested attrsets used in name-spacing for contract instances.
In that particular use-case, we may only presume for
part of the leaf type's keys to be present.

Signed-off-by: cinereal <cinereal@riseup.net>
Adds module system helper functions intended to facilitate contracts:

- `lib.evalOption`: evaluate config in the context of an option,
  which allows using the type checks (and calculated options) of the
  module system as a function, e.g. for use in `lib`.

Further adds two functions intended to facilitate extending submodule options/types,
so as to resolve contracts' challenge of deduplicating
types that may differ largely in overriding `default` values.

- `lib.extendOption`: extend a submodule option with overrides
- `lib.extendSubmodule`: extend a submodule type with overrides

Signed-off-by: cinereal <cinereal@riseup.net>
Adds a central place for storing contracts,
alongside a way to aggregate requests to be handled by
preferred (default) providers for the contract.

The contracts are presented as typed request/result
interfaces between consumers and providers.

Storing the contracts themselves in `lib` is technically optional,
but any stored in there we can generate documentation for.
One may nevertheless extend this nixpkgs-provided set of contracts
with their own in `config.contracts`.

Note that our contracts module is intended to be environment-agnostic,
that is, should work not just in NixOS, but also in modular services
and e.g. home-manager.

That said, providers are expected to be specific to an environment:
each environment exposes a different set of configuration options
(while if we had no need to set configuration options,
one might as well use plain functions over contracts).

`lib/contracts` uses the following structure:

- `default.nix`: entrypoint for `lib.contract`
- `helpers.nix`: helper functions for use with contracts: so far `isInstance`
- `module.nix`: contracts module with `contractDefinitions` and `contracts`
- `templates/default.nix`: exposes contract templates in this directory as `lib.contracts`
- `templates/file-secrets.nix`: example contract template `fileSecrets`

Signed-off-by: cinereal <cinereal@riseup.net>
Rename the raw `mkProviderType` on the contract-definition type to
`_mkProviderType` and introduce `config.contracts.<name>.mkProviderType`
as a bridge that pre-binds `_requests`. The bridge re-applies the
consumer's want-derived request data at `mkDefault` priority at each
nestedAttrsOf leaf, so deployer writes at provider-specific options
(`providerOptions`) no longer mask consumer wants via leaf-level
priority filtering.

Adds a regression test covering exactly that path
(`contracts-partial-overrides`).

Test modules using `mkProviderType` switch to the bridge form
(`config.contracts.<X>.mkProviderType`).

Assisted-by: Claude:claude-opus-4-7[1m]
`lib.contract.forModule config` rebinds every contract's
`mkProviderType` against the module's `config`, returning an attrset
shaped like `lib.contracts` and extended with any inline
contracts on `config.contractDefinitions`. For each contract, it
prefers the bridge (`config.contracts.<name>.mkProviderType`) when
available and falls back to the raw lib function -- so modules built
inside the NixOS-manual sandbox (where `contracts` is absent) still
get a valid `mkProviderType`.

Switches the submodule-request test to consume `mkProviderType`
through `forModule`.

Assisted-by: Claude:claude-opus-4-7[1m]
Add `interface.extraImports.{request,result}` so contracts can ship
backward-compatible aliases for renamed request/result options, and
expose the pattern for renaming a whole contract via a deprecated
`contractType` plus a warning. Includes lib tests covering both cases.

Assisted-by: Claude:claude-sonnet-4-6
Adds the NixOS contracts wrapper module that imports the generic
`lib.contract.module` and seeds `config.contractDefinitions` with
`lib.contracts`, making all nixpkgs contract types available in NixOS
alongside user-defined types.

Assisted-by: Claude:claude-sonnet-4-6
…r services

- `lib/contracts/module.nix`: remove `readOnly` from `requests`/`results` so
  `mkForce` seeding works; extend `results` description with modular consumer
  and bridge path examples
- `lib/services/lib.nix`: add `evalServices` for peer-to-peer contract
  resolution across a service set (no parent NixOS context required); update
  `configure` to accept `contracts`/`upstreamContractDefinitions` and seed
  each service's `contracts.*` via an upstream seed module
- `lib/services/service.nix`: import `lib.contract.module`; propagate
  `contractDefinitions` and scoped `contracts.*` (results, defaultProvider)
  to sub-services via direct injection
The `behaviorTest` framework used to write directly to the provider
option at a caller-supplied `providerRoot`. That bypassed the
canonical consumer `want` flow and made the test machinery hostile
to the leaf-priority handling that `nestedAttrsOf` performs on
provider-side instances.

Switch the framework to declare its request at
`contracts.fileSecrets.want.<wantPath>.request` (a normal consumer
write) and read the resolved result back from
`contracts.fileSecrets.results.<wantPath>`. Callers now pass a
`wantPath` rooted under `want`, not the full provider-option path.

Assisted-by: Claude:claude-opus-4-7[1m]
Extract the contract-name prefix, want-path wiring, and result
extraction from `behaviorTest` into a reusable `lib.contract.mkBehaviorTest`
helper parameterized by `contractName`, `testName`, and `wantPath`.

Refactor the file-secrets `behaviorTest` to call the helper, leaving
only per-contract pieces (test options, request shape, nodeModule,
testScript body) inline.

Assisted-by: Claude:claude-opus-4-7[1m]
Rewrites `testing.hardcoded-secret` onto `mkProviderType` so it is a
proper `fileSecrets` provider rather than a bespoke implementation.
Adds `nixos/tests/contracts/arithmetic-contract.nix` — a reusable
module that registers the `arithmetic` contract type for NixOS tests.

Uses a per-secret systemd oneshot service rather than an activation
script: activation scripts cannot reliably order against
`systemd-sysusers.service` (both land in `sysinit.target` without an
explicit dependency edge), so `chown` in an activation script silently
fails when the target user is created by sysusers.
- `after = ["systemd-sysusers.service" "nixos-activation.service"]`
  ensures both user creation paths have completed before the file is
  written.
- `before = ["multi-user.target"]` ensures the secret is in place
  before any normal service starts.
- `requires/after` injected into the consumer service provide an
  explicit dependency edge for services that start before
  `multi-user.target` is fully reached.

Assisted-by: Claude:claude-sonnet-4-6
Introduce `secretOptionType` (a `oneOf [ path contractSecretsType ]`) for the
three secret options and update the startup script to use
`lib.contract.isInstance` to branch between a plain path and a contract result.

Rename `jwtSecretKeyFile`/`sessionStoreKeyFile` to `jwtSecretKey`/`sessionStoreKey`,
keeping the old names working via `mkRenamedOptionModule` so existing
configurations continue to evaluate with a deprecation warning.

Register `passwordFile`, `jwtSecretKey`, and `sessionStoreKey` with the
fileSecrets contract system via `contracts.fileSecrets.want.stash` so providers
can fulfill them, and default `jwtSecretKey`/`sessionStoreKey` to the contract
result so users relying on a fileSecrets provider need not set them explicitly.

Assisted-by: Claude:claude-opus-4-8
Add a `Development` chapter covering motivation, consumer/provider patterns,
provider selection, adding a new contract type, chaining, cross-node
contracts, and renames. Wire it into `development.md`'s chapter list and
register the section anchors in `redirects.json`.
Also collapses the `byRef`/`byName` test groups in
`contracts-provider-selection.nix` into a single shared `by`
contract type and switches the file to consume `mkProviderType`
via `lib.contract.forModule`.

Assisted-by: Claude:claude-opus-4-7[1m]
Adds two NixOS VM tests demonstrating advanced contract patterns:
- `chaining`: a provider that is also a consumer of another contract
- `cross-node`: using a shared `evalModules` to wire a contract across
  two separate NixOS nodes
- `cross-node-modular-services`: similar but using modular services

Assisted-by: Claude:claude-sonnet-4-6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2.status: merge conflict This PR has merge conflicts with the target branch 6.topic: lib The Nixpkgs function library 6.topic: module system About "NixOS" module system internals 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 6.topic: package configuration modules Modular services / package configuration modules https://matrix.to/#/#modular-services:nixos.org 8.has: changelog This PR adds or changes release notes 8.has: documentation This PR adds or changes documentation 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 1-10 This PR causes between 1 and 10 packages to rebuild on Darwin. 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux. llm-assisted Contributions that include code generation with LLMs

Projects

Status: No status
Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants