feat(#2693): let the operator pin the claude-code every agent runs
Agents run whatever `claude-code` the meta flake's `nixpkgs` resolves to, and that is normally a release channel. This one package moves fast enough that stable trails unstable by weeks — 26.05 is on 2.1.187 while unstable carries 2.1.220 — and an agent cannot fix it for itself: it only ever sees the single nixpkgs hive-c0re injects, so an `agent.nix` has no other tree to reach for. New host option `services.hyperhive.c0re.claudeCodePackage` takes the package directly and rides the existing `hyperhiveDocs` threading path — serveConfigJson -> HiveEnv -> render_flake — to reach each agent as `hyperhive.claudeCodePath`. Null (the default) is today's behaviour. What travels is the store *path*, as a plain string literal, not a flake input: containers share the host's `/nix/store`, so the build is already reachable inside them with its whole closure and has nothing to travel. An input would be worse than useless — a `path:/nix/store/<pkg>` input is re-copied as a reference-less `-source`, which strips exactly the closure the binary needs. The catch is that a path written into a generated flake is text, so nothing in the container's closure keeps the binary alive. The host does that instead, and gets it for free: the package is interpolated into `/etc/hyperhive/serve.json`, `builtins.toJSON` preserves string context, so the /etc entry references it and the system closure gc-roots it for as long as that generation is the one the agents were rendered from. An assertion pins that property, because losing the context is invisible at eval and at deploy — it would surface only as every agent failing to spawn `claude` whenever the next gc ran. Container side wraps the path in a symlink farm rather than putting it on PATH directly: `systemd.services.<name>.path` and `environment.systemPackages` both coerce a store-path *string* through `lib.toDerivation`, i.e. `builtins.storePath`, which pure evaluation rejects. Interpolating the path into a builder is just text and evaluates anywhere. `claude-code` drops out of systemPackages when a pin is set, so there is exactly one claude in the container. Refs #2693
This commit is contained in:
parent
ca7146e4f0
commit
b08176f089
7 changed files with 281 additions and 1 deletions
|
|
@ -61,6 +61,33 @@
|
|||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudeCodePath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
example = "/nix/store/…-claude-code-2.1.220";
|
||||
description = ''
|
||||
Store path of the `claude-code` this agent runs, or `""` (the
|
||||
default) to use the `claude-code` from the container's own
|
||||
nixpkgs.
|
||||
|
||||
Set by the generated meta flake when the operator sets
|
||||
`services.hyperhive.c0re.claudeCodePackage` host-side, so a hive
|
||||
can run a claude built from a *different* nixpkgs than the one
|
||||
its agents evaluate against — a release channel can trail
|
||||
unstable by weeks on this one package. It arrives as a path and
|
||||
not a package because agents share the host's `/nix/store`, so
|
||||
the build is already reachable here with its whole closure and
|
||||
has nothing to travel.
|
||||
|
||||
When set, `claude` on PATH is a symlink to this path's
|
||||
`bin/claude` and the container's own `claude-code` is dropped, so
|
||||
there is only ever one claude in the container. Note that neither
|
||||
the symlink nor anything else in the container's closure *refers*
|
||||
to the target — keeping it alive is the host's job, see
|
||||
`services.hyperhive.c0re.claudeCodePackage`.
|
||||
'';
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
# Guard the inputs-routed-as-output pattern: the agent flake.nix is
|
||||
|
|
@ -183,8 +210,31 @@
|
|||
environment.systemPackages = [
|
||||
config.hyperhive.packages.hive-metric
|
||||
]
|
||||
++ [
|
||||
(
|
||||
if config.hyperhive.claudeCodePath == "" then
|
||||
pkgs.claude-code
|
||||
else
|
||||
# Host-pinned claude: a symlink farm around a path the
|
||||
# container was handed as text. It has to be a derivation —
|
||||
# `environment.systemPackages` coerces a store-path *string*
|
||||
# with `toDerivation`, i.e. `builtins.storePath`, which pure
|
||||
# evaluation rejects (`systemd.services.*.path` does the same,
|
||||
# which is why the harness gets this via PATH like everything
|
||||
# else rather than a unit-level entry). Interpolating the path
|
||||
# into the builder is just text, so it evaluates anywhere.
|
||||
#
|
||||
# The symlink registers no store reference — the target isn't
|
||||
# among this derivation's inputs, so nothing here keeps the
|
||||
# binary alive. That is deliberate and it is the host's job:
|
||||
# see `services.hyperhive.c0re.claudeCodePackage`.
|
||||
pkgs.runCommandLocal "claude-code-pinned" { } ''
|
||||
mkdir -p "$out/bin"
|
||||
ln -s ${config.hyperhive.claudeCodePath}/bin/claude "$out/bin/claude"
|
||||
''
|
||||
)
|
||||
]
|
||||
++ (with pkgs; [
|
||||
claude-code
|
||||
bashInteractive
|
||||
coreutils-full
|
||||
# procps for pkill — used by the web UI's /api/cancel to SIGINT the
|
||||
|
|
|
|||
Loading…
Reference in a new issue