feat(nix): agentPackages attrset + extraPackages + agent tools baseline (ADR-0033) #76

Merged
dominik.polakovics merged 1 commit from afk/74 into main 2026-07-09 21:57:30 +02:00

Closes #74

Replaces the module's single claude-shaped claudePackage knob with a provider-keyed agent-CLI seam, and gives every session a fixed tools baseline. Design per the issue's agent brief (grilling session 2026-07-09), pinned in ADR-0033.

What changed

  • services.lab.agentPackagesattrsOf (nullOr package), keyed by lab provider ID (claude-code, codex — the registry/DB/API strings). Every non-null value's bin/ lands on the unit PATH. Defaults are injected per-key at mkOptionDefault priority, so agentPackages."claude-code" = null drops the (unfree) Claude CLI while the codex default survives. The option description documents the unfree default and the null opt-out.
  • services.lab.extraPackageslistOf package, default [], appended to the unit PATH; purely additive.
  • Fixed tools baseline, unconditional, not an option: gawk gnutar gzip xz zstd unzip curl jq file patch procps ripgrep + the host's config.nix.package (sessions can nix develop a project flake for per-project toolchains). No language toolchains by design.
  • claudePackage is now a deprecated alias: non-null populates agentPackages."claude-code" and warns; set together with an explicit agentPackages."claude-code" it fails eval with an assertion naming both options (explicitness read from per-attribute definition priorities via lib.modules.mergeAttrDefinitionsWithPrio, values never forced).
  • nixos-module flake check grows variants for all of the above. The new variants are eval-only — they assert on package names off config.systemd.services.lab.path, so the check's build closure never pulls in claude-code/codex; the text-level dummy keeps every existing unit-invariant grep and proves PATH serialization with pkgs.hello stand-ins.
  • Docs: ADR-0033 (amends ADR-0002's option surface), ops.md option table/example/unit-invariants — including the operator-facing allowUnfreePredicate one-liner.

Verification

  • nix build .#checks.x86_64-linux.nixos-module green (all six eval asserts + unit-text greps); checks.aarch64-linux.nixos-module drvPath evals clean.
  • nix derivation show -r over the check: zero claude-code/codex references in the build closure.
  • Mutation-tested the conflict assertion: with claudePackage set alone the conflict-variant assert correctly fails the check, proving the assertion keys on the double definition, not on the alias alone.
  • Pinned nixpkgs (d407951) verified to ship claude-code-2.1.201, codex-0.142.3, and lib.modules.mergeAttrDefinitionsWithPrio.

Out of scope (per issue)

Codex AgentProvider implementation (#2, stays ready-for-human); downstream fleet-config migration; one-time deploy check that the nix daemon socket is reachable from a live session.

🤖 Generated with Claude Code

https://claude.ai/code/session_014BKNq5c2thNWaDoMGCepWL

Closes #74 Replaces the module's single claude-shaped `claudePackage` knob with a provider-keyed agent-CLI seam, and gives every session a fixed tools baseline. Design per the issue's agent brief (grilling session 2026-07-09), pinned in **ADR-0033**. ## What changed - **`services.lab.agentPackages`** — `attrsOf (nullOr package)`, keyed by lab provider ID (`claude-code`, `codex` — the registry/DB/API strings). Every non-null value's `bin/` lands on the unit PATH. Defaults are injected per-key at `mkOptionDefault` priority, so `agentPackages."claude-code" = null` drops the (unfree) Claude CLI while the codex default survives. The option description documents the unfree default and the `null` opt-out. - **`services.lab.extraPackages`** — `listOf package`, default `[]`, appended to the unit PATH; purely additive. - **Fixed tools baseline**, unconditional, not an option: `gawk gnutar gzip xz zstd unzip curl jq file patch procps ripgrep` + the host's `config.nix.package` (sessions can `nix develop` a project flake for per-project toolchains). No language toolchains by design. - **`claudePackage`** is now a deprecated alias: non-null populates `agentPackages."claude-code"` and warns; set together with an explicit `agentPackages."claude-code"` it fails eval with an assertion naming both options (explicitness read from per-attribute definition priorities via `lib.modules.mergeAttrDefinitionsWithPrio`, values never forced). - **`nixos-module` flake check** grows variants for all of the above. The new variants are eval-only — they assert on package names off `config.systemd.services.lab.path`, so the check's build closure never pulls in claude-code/codex; the text-level dummy keeps every existing unit-invariant grep and proves PATH serialization with `pkgs.hello` stand-ins. - Docs: ADR-0033 (amends ADR-0002's option surface), ops.md option table/example/unit-invariants — including the operator-facing `allowUnfreePredicate` one-liner. ## Verification - `nix build .#checks.x86_64-linux.nixos-module` green (all six eval asserts + unit-text greps); `checks.aarch64-linux.nixos-module` drvPath evals clean. - `nix derivation show -r` over the check: **zero** `claude-code`/`codex` references in the build closure. - Mutation-tested the conflict assertion: with `claudePackage` set alone the conflict-variant assert correctly fails the check, proving the assertion keys on the double definition, not on the alias alone. - Pinned nixpkgs (`d407951`) verified to ship `claude-code-2.1.201`, `codex-0.142.3`, and `lib.modules.mergeAttrDefinitionsWithPrio`. ## Out of scope (per issue) Codex `AgentProvider` implementation (#2, stays `ready-for-human`); downstream fleet-config migration; one-time deploy check that the nix daemon socket is reachable from a live session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014BKNq5c2thNWaDoMGCepWL
feat(nix): agentPackages attrset + extraPackages + agent tools baseline (ADR-0033)
All checks were successful
ci / native (pull_request) Successful in 5m29s
ci-nix / flake-check (pull_request) Successful in 14m7s
a7a6c4f997
The NixOS module's agent-CLI surface generalizes from the single
claude-shaped claudePackage knob to services.lab.agentPackages, an
attrsOf (nullOr package) keyed by lab provider ID — the same strings
the provider registry, the DB provider column, and the API use
(claude-code, codex). The defaults ({ "claude-code" = pkgs.claude-code;
codex = pkgs.codex; }) are injected per-key at mkOptionDefault
priority, so agentPackages."claude-code" = null drops the (unfree)
Claude CLI while the codex default survives — per-key merge, never
whole-attrset replacement. services.lab.extraPackages (listOf package,
default []) appends host-specific tools, purely additive.

The unit PATH — inherited by every spawned session through the tmux
server — gains a fixed tools baseline, deliberately not an option:
gawk, gnutar, gzip, xz, zstd, unzip, curl, jq, file, patch, procps,
ripgrep, and the host's config.nix.package, so sessions can run a
project flake's devshell (nix develop / nix shell) for per-project
toolchains. Previously ps/rg reached sessions only as an accident of
the nixpkgs claude-code wrapper prefixing its own PATH — a second
provider's sessions would have lacked them. Deliberately no language
toolchains: those come from each project's flake.

claudePackage stays as a deprecated alias: non-null populates
agentPackages."claude-code" and warns; combined with an explicit
agentPackages."claude-code" definition it fails eval with an assertion
naming both options. Explicitness is read off the module system's
per-attribute definition priorities (mergeAttrDefinitionsWithPrio),
values never forced — the unfree default cannot leak into a host that
nulled it.

The nixos-module flake check grows eval-only variants: defaults land
both CLIs plus the baseline on the unit PATH; the per-key null
opt-out; extraPackages additivity; the alias warning; the both-set
assertion message naming both options; the option-description unfree
note. The variants assert on package names off
config.systemd.services.lab.path — never realizing unit text carrying
the real agent CLIs — so the check's build closure stays free of
claude-code/codex (verified: zero matches in the recursive derivation
closure). The text-level dummy keeps every existing unit-invariant
grep and proves PATH serialization end-to-end with cheap stand-ins.

Out of scope, unchanged: the Codex AgentProvider implementation (#2),
the downstream fleet-config migration, and the one-time deploy check
that the nix daemon is reachable from a live session (ADR-0033).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BKNq5c2thNWaDoMGCepWL
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!76
No description provided.