harness-base: generic bash-env.sh + _bashEnvFragments accumulator (mara on #779)

mara: 'if we replace it with one thing, that should be named more
generic so we dont have to change it for future additions'.

extract the BASH_ENV plumbing into a shared shape:

- new internal option `hyperhive._bashEnvFragments` (types.lines)
  accumulates shell snippets across feature modules.
- file path is now `/etc/hyperhive/bash-env.sh` (was the
  cargo-specific bash-cargo-short.sh).
- the file + BASH_ENV + interactiveShellInit are gated on
  `_bashEnvFragments != """ so a fully feature-disabled agent has
  no overhead.

cargo function moves to a `lib.mkIf cargo.shortMessages` contribution
to `_bashEnvFragments` — same behaviour, no rename when the next
hook (nix-env helper, claude-cmd helpers, whatever) lands.
This commit is contained in:
damocles 2026-05-31 14:40:46 +02:00 committed by mara
commit ea5f70629c

View file

@ -537,6 +537,30 @@ in
''; '';
}; };
# Internal accumulator for shell snippets that should land in
# `/etc/hyperhive/bash-env.sh`. Per-feature hooks set this via
# `lib.mkIf` gated on their own option; the lines type merges
# all contributions across modules into one file. Loaded via
# `$BASH_ENV` for non-interactive shells (claude's `Bash` tool
# runs `bash -c`) and via `programs.bash.interactiveShellInit`
# for interactive shells. Generic by design (mara on #779) so
# future hooks don't need to either rename this file or invent
# a parallel dispatcher.
options.hyperhive._bashEnvFragments = lib.mkOption {
type = lib.types.lines;
default = "";
internal = true;
description = ''
Shell snippets concatenated into `/etc/hyperhive/bash-env.sh`.
Feature hooks contribute via `lib.mkIf` gated on their own
option. When empty, the file isn't created, `BASH_ENV` stays
unset, and the interactive bashrc hook is omitted zero cost
when no feature is on. Internal set indirectly via the
per-feature options that own the gate (e.g.
`hyperhive.cargo.shortMessages`).
'';
};
options.hyperhive.cargo.shortMessages = lib.mkOption { options.hyperhive.cargo.shortMessages = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
@ -550,12 +574,12 @@ in
the response window with per-crate progress lines that the response window with per-crate progress lines that
carry no signal beyond the warning/error summary (#777). carry no signal beyond the warning/error summary (#777).
Implementation: a `cargo` shell function defined in Implementation: contributes a `cargo` shell function to
`/etc/hyperhive/bash-cargo-short.sh`. Loaded via `BASH_ENV` `/etc/hyperhive/bash-env.sh` (see `hyperhive._bashEnvFragments`).
for non-interactive shells (`bash -c` what the claude Loaded via `BASH_ENV` for non-interactive shells (`bash -c`
`Bash` tool runs) and sourced from `programs.bash.interactiveShellInit` what the claude `Bash` tool runs) and sourced from
for interactive shells (operator pokes around inside the `programs.bash.interactiveShellInit` for interactive shells.
container). The function: The function:
- handles the `+toolchain` selector prefix (`cargo +nightly - handles the `+toolchain` selector prefix (`cargo +nightly
build` works); build` works);
@ -746,50 +770,55 @@ in
source = config.hyperhive.icon; source = config.hyperhive.icon;
}; };
# Cargo `--message-format short` injector (#777). Sourced from # Cargo `--message-format short` injector (#777). Contributes a
# BASH_ENV in non-interactive shells AND interactiveShellInit # `cargo` shell function to `hyperhive._bashEnvFragments`; the
# so both claude's `Bash` tool and operator SSH sessions get # bash-env infrastructure below packages that into a single file
# the same compact compile output. `command cargo …` falls back # sourced by both non-interactive and interactive shells.
# to the un-wrapped binary in PATH (the rust toolchain's cargo # `command cargo …` falls back to the un-wrapped binary in PATH
# — either from `environment.systemPackages` or from whatever # (the rust toolchain's cargo — either from `environment.systemPackages`
# `nix develop` shell the agent's working in). # or from whatever `nix develop` shell the agent's working in).
environment.etc."hyperhive/bash-cargo-short.sh" = hyperhive._bashEnvFragments = lib.mkIf config.hyperhive.cargo.shortMessages ''
lib.mkIf config.hyperhive.cargo.shortMessages # Auto-injects --message-format short on cargo compile
{ # subcommands so per-crate progress lines don't flood
text = '' # claude's context (#777). Bypassed when the caller
# Auto-injects --message-format short on cargo compile # already passes --message-format (any form).
# subcommands so per-crate progress lines don't flood cargo() {
# claude's context (#777). Bypassed when the caller # Strip leading +toolchain selectors (cargo +nightly …).
# already passes --message-format (any form). local pre=()
cargo() { while [ "''${1:0:1}" = "+" ] && [ -n "''${1:-}" ]; do
# Strip leading +toolchain selectors (cargo +nightly …). pre+=("$1")
local pre=() shift
while [ "''${1:0:1}" = "+" ] && [ -n "''${1:-}" ]; do done
pre+=("$1") case "''${1:-}" in
shift build|check|clippy|test|run|doc|bench|install|rustc|fix)
done local sub="$1"
case "''${1:-}" in shift
build|check|clippy|test|run|doc|bench|install|rustc|fix) local arg
local sub="$1" for arg in "$@"; do
shift case "$arg" in
local arg --message-format|--message-format=*)
for arg in "$@"; do command cargo "''${pre[@]}" "$sub" "$@"
case "$arg" in return $?
--message-format|--message-format=*)
command cargo "''${pre[@]}" "$sub" "$@"
return $?
;;
esac
done
command cargo "''${pre[@]}" "$sub" --message-format short "$@"
;;
*)
command cargo "''${pre[@]}" "$@"
;; ;;
esac esac
} done
''; command cargo "''${pre[@]}" "$sub" --message-format short "$@"
}; ;;
*)
command cargo "''${pre[@]}" "$@"
;;
esac
}
'';
# Single bash-env file with all configured shell fragments.
# Wiring is gated on at least one fragment being active so a
# fully feature-disabled agent has neither the file nor the
# `BASH_ENV` / interactive sourcing — zero cost in that case.
environment.etc."hyperhive/bash-env.sh" =
lib.mkIf (config.hyperhive._bashEnvFragments != "") {
text = config.hyperhive._bashEnvFragments;
};
environment.etc."hyperhive/bash-allow.json".text = environment.etc."hyperhive/bash-allow.json".text =
builtins.toJSON config.hyperhive.allowedBashPatterns; builtins.toJSON config.hyperhive.allowedBashPatterns;
@ -853,23 +882,25 @@ in
// lib.optionalAttrs (config.hyperhive.forge.skipNotifyReasons != [ ]) { // lib.optionalAttrs (config.hyperhive.forge.skipNotifyReasons != [ ]) {
HIVE_FORGE_NOTIFY_SKIP_REASONS = lib.concatStringsSep "," config.hyperhive.forge.skipNotifyReasons; HIVE_FORGE_NOTIFY_SKIP_REASONS = lib.concatStringsSep "," config.hyperhive.forge.skipNotifyReasons;
} }
// lib.optionalAttrs config.hyperhive.cargo.shortMessages { // lib.optionalAttrs (config.hyperhive._bashEnvFragments != "") {
# Non-interactive bash invocations (claude's `Bash` tool runs # Non-interactive bash invocations (claude's `Bash` tool runs
# `bash -c`) source $BASH_ENV at startup — drops the cargo # `bash -c`) source $BASH_ENV at startup — drops every active
# function defined in the file above into scope without # feature hook's snippet into scope without touching
# touching /etc/profile (login-only). Interactive shells # `/etc/profile` (login-only). Interactive shells source the
# source the same file via the interactiveShellInit hook # same file via the `interactiveShellInit` hook below so
# below so behaviour matches across both modes (#777). # behaviour matches across both modes (#777).
BASH_ENV = "/etc/hyperhive/bash-cargo-short.sh"; BASH_ENV = "/etc/hyperhive/bash-env.sh";
}; };
# Interactive shells don't honour BASH_ENV — wire the same file # Interactive shells don't honour BASH_ENV — wire the same file
# in via the bashrc hook so operator SSH sessions get the same # in via the bashrc hook so operator SSH sessions get the same
# short-format cargo output as claude's non-interactive calls. # hook surface as claude's non-interactive calls. Gated on at
# least one fragment being active so we don't write a no-op
# source line into `/etc/bashrc` on fully-feature-disabled agents.
programs.bash.interactiveShellInit = programs.bash.interactiveShellInit =
lib.mkIf config.hyperhive.cargo.shortMessages '' lib.mkIf (config.hyperhive._bashEnvFragments != "") ''
if [ -r /etc/hyperhive/bash-cargo-short.sh ]; then if [ -r /etc/hyperhive/bash-env.sh ]; then
. /etc/hyperhive/bash-cargo-short.sh . /etc/hyperhive/bash-env.sh
fi fi
''; '';