From 309879dba0465fb4cf007aae9fc6fac48ade0967 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 31 May 2026 14:39:18 +0200 Subject: [PATCH 01/43] docs: extract 3 substantive harness-base.nix prose blocks (#718, first pass) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iris's #718 scope: move substantive design context from `#` comment blocks in `nix/` to corresponding `docs/` files, leave short references in code. iris handed it back to me on #10114 since nix/ is my lane + #775 established the pattern. First pass — three highest-density blocks in harness-base.nix: 1. **First-boot agent-user migration** (~70 lines → `~20 lines code + short ref` in the activation script). Substantive prose moves to new `docs/persistence.md::First-boot agent-user migration (post-#658)` section explaining the 4 steps the script performs + the eventual removability of the marker-guarded body. 2. **nix-daemon `sandbox-fallback = true`** (10-line block → 5-line ref). New `docs/gotchas.md::Containerized nix-daemon needs sandbox-fallback = true` section covers the user-namespaces rationale + nixpkgs-default override. 3. **Matrix daemon + token-arrival trigger** (~50 lines across two systemd units → ~10 lines code + short refs). New `docs/persistence.md::Matrix per-agent daemon + token-arrival trigger` covers the socket-path rationale, the runtime-dir ownership story, and the first-boot ordering pattern. Net: harness-base.nix -84 lines, docs +74 lines. Substantive design context moves to durable docs; in-code refs follow iris's pattern from her #712 batches (`see docs/::
`). Follow-ups: hive-c0re.nix, hive-forge.nix, hive-matrix.nix (already trimmed via #775 but a couple of remaining blocks could go), and the smaller files in #718's scope table. Shipping this first to get the pattern reviewed before larger batches. Verified: `nix eval` on agent-base toplevel still resolves. --- docs/gotchas.md | 11 ++++ docs/persistence.md | 63 ++++++++++++++++++++ nix/templates/harness-base.nix | 106 +++++++-------------------------- 3 files changed, 96 insertions(+), 84 deletions(-) diff --git a/docs/gotchas.md b/docs/gotchas.md index 5cf01a20..85fb1b85 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -227,3 +227,14 @@ hive-forge lint assignments # per-assignee open item count Credentials come from `$HYPERHIVE_STATE_DIR/forge-token`; default repo from `$HIVE_FORGE_REPO`, overridden per-invocation by the global `-r/--repo` flag. + +## Containerized nix-daemon needs `sandbox-fallback = true` + +Agent containers bind-mount the host's nix-daemon socket. nspawn +containers don't get user-namespaces by default, so `nix build` +invocations *inside* the container can't set up the build sandbox +and fail outright if the host daemon's +`nix.settings.sandbox-fallback` is `false` (nixpkgs default). +`nix/templates/harness-base.nix` does `lib.mkForce true` so builds +fall back to unsandboxed local builds rather than failing. Security +implications: `docs/security.md`. diff --git a/docs/persistence.md b/docs/persistence.md index dd9b3f52..03bf8982 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -198,3 +198,66 @@ On startup, `Coordinator::register_agent` drops any prior socket task before rebinding — idempotent so a hive-c0re restart followed by `rebuild alice` recreates the agent's socket without a clean reinstall. + +## First-boot agent-user migration (post-#658) + +Pre-#658 the harness ran as root inside the container. #658 dropped +to a per-agent unix user (`hyperhive.user.name`, defaults to the +agent's logical label so each container has a uniquely-named user). +The transition needs a one-time data shuffle so existing operators +who deployed pre-#658 don't lose their claude session. + +`system.activationScripts.hive-agent-user-migrate` (in +`nix/templates/harness-base.nix`) runs on every activation, +marker-guarded so the substantive moves only happen once per +container lifetime: + +1. **`${homeDir}` exists with the right ownership** — covers the + very first boot before `useradd`'s `createHome` has had a + chance to chown. Also re-applies on every rebuild in case the + meta-flake's per-agent name evolves (rare). +2. **Migrate any leftover `/root/.claude` content into + `${homeDir}/.claude`** — pre-#658 `claude` wrote to root's + empty home; the bind mount didn't exist yet. Marker + (`/var/lib/hive-agent-user-migrated`) guards single-shot. + `cp -an` (no-clobber) so any pre-existing files at the new + location win — never blow over data already there. +3. **Chown the bind-mounted state dir** (`/agents/*/state`) + recursively so the new agent user can read/write it. Wildcard + matches the single agent that container sees; `-h` skips + symlinks the agent might have planted. +4. **Chown the `~/.claude/` bind-mount** recursively. Pre-#658 + `claude` wrote `.credentials.json` 0600 root:root; post-#658 + the harness reads `~/.claude/` as the agent user to decide + Online vs NeedsLogin in `login::has_session`. Without the + chown the existing credentials get silently treated as "no + session" and the operator re-prompts every boot. + +The activation script will eventually become unnecessary once no +operators have pre-#658 state dirs left to migrate; drop the body ++ marker check at that point. + +## Matrix per-agent daemon + token-arrival trigger + +`hive-matrix-daemon` is a long-running matrix-sdk Client + sync +process per agent. Holds the unix socket the stdio +`hive-matrix-mcp` bridge talks to, emits hyperhive wake signals +on incoming room events via `/run/hive/mcp.sock`. Conditional on +`hyperhive.matrix.enable` (which both the daemon AND the +auto-injected `extraMcpServers.matrix` entry read). + +Socket path lives inside the systemd-managed runtime dir +(`RuntimeDirectory = "hive-matrix"` → `/run/hive-matrix/`, owned by +the agent user) so the daemon can bind without needing root over +`/run/` itself. Both daemon + bridge agree on the path via the +`HIVE_MATRIX_SOCKET` env var. + +**First-boot ordering**: hive-c0re provisions the matrix token AFTER +agent containers come up. Without the path-trigger sibling +(`systemd.paths.hive-matrix-daemon`, `PathExistsGlob = +/agents/*/state/matrix-token`), the daemon would exit 0 quietly the +first time it ran and the MCP would have no backend until the next +restart. The `.path` unit makes the appearance of the token re-fire +the service so the daemon comes alive in the same boot cycle as +provisioning. `matrix-avatar-sync.path` uses the same pattern for +the icon-upload oneshot (#571). diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 0c433b59..ae65bc3b 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -656,46 +656,20 @@ in } ]; - # First-boot migration from the legacy root-run shape (#658). - # Runs on every activation; marker-guarded so the move only - # happens once. The bind mount that hive-c0re sets up has - # already moved from `/root/.claude` to `${homeDir}/.claude` - # by the time we get here (per `lifecycle::CONTAINER_CLAUDE_MOUNT` - # — the host-side path stays the same, the container-side - # mount target shifts), so the bulk of the data is already at - # the new location. This script just: - # - # - ensures `${homeDir}` exists with correct ownership (covers - # the very first boot before useradd's `createHome` has - # anything to chown); - # - migrates any leftover `/root/.claude` content that an - # operator might have populated before #658 deployed (the - # bind mount didn't exist in that lifecycle, so claude - # would have written into the root user's empty home — - # nothing important typically, but safer to move than to - # strand); - # - chowns the bind-mounted state dir (`/agents/*/state`) so - # the agent user can read/write it. + # Post-#658 first-boot migration to the per-agent unix user — + # creates the home dir, chowns the bind-mounted state + + # `~/.claude/`, and (marker-guarded) moves any leftover + # `/root/.claude` content from the pre-#658 root-run shape. See + # `docs/persistence.md::First-boot agent-user migration` for the + # step-by-step rationale; this script implements it. system.activationScripts.hive-agent-user-migrate = lib.stringAfter [ "users" "specialfs" ] '' homeDir=${lib.escapeShellArg homeDir} userName=${lib.escapeShellArg userName} - # Always ensure the home dir exists with the right ownership — - # useradd's createHome handles the very first creation but - # doesn't re-chown if a rebuild changes the user name (rare - # but possible if the meta-flake's per-agent name evolves). mkdir -p "$homeDir" chown "$userName:$userName" "$homeDir" - # One-time migration of pre-#658 /root/.claude content into the - # new home. Marker-guarded so the move only runs once per - # container lifetime — subsequent activations skip the legacy - # path even if claude were to repopulate /root/.claude for any - # reason. marker=/var/lib/hive-agent-user-migrated if [ ! -e "$marker" ] && [ -d /root/.claude ] && [ "$(ls -A /root/.claude 2>/dev/null)" ]; then mkdir -p "$homeDir/.claude" - # `mv -n` (no-clobber) so any pre-existing files at the - # destination (e.g. from the bind mount) win — we never - # blow over data already at the new location. if cp -an /root/.claude/. "$homeDir/.claude/" 2>/dev/null; then rm -rf /root/.claude echo "hive-agent-user-migrate: moved /root/.claude → $homeDir/.claude" @@ -703,25 +677,10 @@ in fi mkdir -p "$(dirname "$marker")" : > "$marker" - # Chown the bind-mounted state dir so the agent user can - # read/write it. `/agents/*/state` is the canonical mount - # point set by hive-c0re's `set_nspawn_flags`. Wildcard - # because each container only sees its own - # `/agents//state` (one match); -h to avoid following - # any symlinks the agent might have planted in there. for stateDir in /agents/*/state; do [ -d "$stateDir" ] || continue chown -hR "$userName:$userName" "$stateDir" 2>/dev/null || true done - # Same treatment for the bind-mounted `~/.claude/` dir. Pre-#658 - # the harness ran as root and `claude` wrote `.credentials.json` - # there 0600 root:root; post-#658 the harness reads - # `~/.claude/` as the agent user to decide Online vs - # NeedsLogin (`login::has_session`), and the host-side bind - # source is still root-owned 0700 from those legacy writes. - # Chown recursively so the existing credentials are readable - # under the new identity instead of getting silently treated - # as "no session" and re-prompting login every boot. if [ -d "$homeDir/.claude" ]; then chown -hR "$userName:$userName" "$homeDir/.claude" 2>/dev/null || true fi @@ -829,15 +788,11 @@ in "flakes" ]; - # Containers bind-mount the host's nix-daemon socket. The host daemon - # may be configured with remote builders or strict sandbox settings - # (sandbox-fallback = false) that make local `nix build` invocations - # fail inside the container. Enable sandbox-fallback so builds that - # can't set up the sandbox (no user-namespaces in nspawn) fall back - # to unsandboxed local builds rather than failing outright. - # mkForce overrides the nixpkgs nix module which sets this to false - # at normal priority -- without it agents get a conflicting definition - # error on rebuild. Security implications: see docs/security.md. + # `lib.mkForce` overrides nixpkgs's normal-priority `false` so + # in-container `nix build` invocations fall back to unsandboxed + # local builds rather than failing on the missing user-namespace. + # See `docs/gotchas.md::Containerized nix-daemon needs + # sandbox-fallback = true` + `docs/security.md` for the rationale. nix.settings.sandbox-fallback = lib.mkForce true; # `claude-code` is unfree. Each per-agent container's nixosConfiguration @@ -1005,14 +960,12 @@ in ''; }; - # Long-running matrix-sdk Client + sync per agent (#548 phase 3). - # Holds the unix socket the stdio `hive-matrix-mcp` bridge talks - # to, and emits hyperhive wake signals on incoming room events - # via `/run/hive/mcp.sock`. Conditional on `hyperhive.matrix.enable` - # AND token-file presence (the daemon binary itself exits 0 on - # missing token, but the path watcher below restarts it the - # moment the token lands — same first-boot-ordering pattern as - # matrix-avatar-sync.path / #571). + # Long-running matrix-sdk client + sync per agent. Holds the unix + # socket the stdio `hive-matrix-mcp` bridge connects to + emits + # hyperhive wake signals on incoming room events via + # `/run/hive/mcp.sock`. See + # `docs/persistence.md::Matrix per-agent daemon + token-arrival + # trigger` for the socket-path / first-boot-ordering rationale. systemd.services.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable { description = "long-running matrix-sdk Client + MCP daemon socket"; wantedBy = [ "multi-user.target" ]; @@ -1020,12 +973,6 @@ in wants = [ "network-online.target" ]; environment = { HIVE_MATRIX_URL = config.hyperhive.matrix.url; - # Socket path lives inside the systemd-managed runtime dir - # (`RuntimeDirectory = "hive-matrix"` → `/run/hive-matrix/`, - # owned by the agent user) so the daemon can bind it without - # needing root over `/run/` itself (#658). The stdio bridge - # picks up the same path via its own `HIVE_MATRIX_SOCKET` env - # in `extraMcpServers.matrix` below. HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket"; RUST_LOG = "info"; }; @@ -1033,26 +980,17 @@ in ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon"; Restart = "on-failure"; RestartSec = 5; - # Run as the per-agent unix user (#658). The runtime dir - # (`/run/hive-matrix/`) is owned by that user via - # `RuntimeDirectory`; claude (also as that user) can - # connect to the socket inside it when the stdio bridge - # spawns per turn. User = userName; Group = userName; RuntimeDirectory = "hive-matrix"; }; }; - # Path-trigger sibling so hive-matrix-daemon fires the moment - # `/matrix-token` appears (#548 phase 3, mirrors the - # matrix-avatar-sync.path pattern from #571). On clean boot - # hive-c0re provisions the token AFTER agent containers come up; - # without the trigger the daemon would exit 0 quietly and the - # MCP would have no backend until next restart. With the watcher - # the daemon comes alive in the same boot cycle as provisioning. - # The glob matches every agent (manager sees its own state at - # `/agents/hm1nd/state/` via the `/agents` bind). + # Re-fire the daemon when the matrix token appears (hive-c0re + # provisions it after agent containers come up). Without this + # the daemon would exit 0 silently on first boot and the MCP + # would have no backend until next restart. See + # `docs/persistence.md` (same section as above). systemd.paths.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable { description = "trigger hive-matrix-daemon when matrix-token appears"; wantedBy = [ "multi-user.target" ]; From 5f61528133041141180f7348c33d7198776e4cce Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 14:28:05 +0200 Subject: [PATCH 02/43] harness-base: cargo --message-format short by default (#777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #777. saves tokens by collapsing per-crate progress lines into warning/error summaries when claude (or the operator) runs cargo inside an agent container. implementation: /etc/hyperhive/bash-cargo-short.sh defines a 'cargo' bash function that injects '--message-format short' on compile subcommands (build/check/clippy/test/run/doc/bench/install/rustc/fix). loaded via BASH_ENV in non-interactive shells (claude's Bash tool runs 'bash -c') and via programs.bash.interactiveShellInit in interactive shells (operator SSH inside the container). handles the '+toolchain' selector (cargo +nightly build), skips injection when the caller already passes --message-format (any form), leaves third-party cargo-* subcommands alone. new option: hyperhive.cargo.shortMessages (default true) — agents that parse cargo json output should set false. --- nix/templates/harness-base.nix | 98 ++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index ae65bc3b..ca02087f 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -537,6 +537,40 @@ in ''; }; + options.hyperhive.cargo.shortMessages = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = '' + Auto-inject `--message-format short` on cargo compile + subcommands (`build`, `check`, `clippy`, `test`, `run`, + `doc`, `bench`, `install`, `rustc`, `fix`) when claude (or + anything else) invokes `cargo` inside this container. + Saves tokens + context — the verbose default output floods + the response window with per-crate progress lines that + carry no signal beyond the warning/error summary (#777). + + Implementation: a `cargo` shell function defined in + `/etc/hyperhive/bash-cargo-short.sh`. Loaded via `BASH_ENV` + for non-interactive shells (`bash -c` — what the claude + `Bash` tool runs) and sourced from `programs.bash.interactiveShellInit` + for interactive shells (operator pokes around inside the + container). The function: + + - handles the `+toolchain` selector prefix (`cargo +nightly + build` works); + - passes through cleanly when the caller already specified + `--message-format` (any form); + - leaves non-compile subcommands (`new`, `add`, `search`, + third-party `cargo-*` subcommands) untouched so they + don't error on the unknown flag. + + Set to `false` for agents that need full cargo output (e.g. + tooling that parses `--message-format json` programmatically + and doesn't pass the flag explicitly). + ''; + }; + options.hyperhive.autoCompact = lib.mkOption { type = lib.types.bool; default = true; @@ -712,6 +746,51 @@ in source = config.hyperhive.icon; }; + # Cargo `--message-format short` injector (#777). Sourced from + # BASH_ENV in non-interactive shells AND interactiveShellInit + # so both claude's `Bash` tool and operator SSH sessions get + # the same compact compile output. `command cargo …` falls back + # to the un-wrapped binary in PATH (the rust toolchain's cargo + # — either from `environment.systemPackages` or from whatever + # `nix develop` shell the agent's working in). + environment.etc."hyperhive/bash-cargo-short.sh" = + lib.mkIf config.hyperhive.cargo.shortMessages + { + text = '' + # Auto-injects --message-format short on cargo compile + # subcommands so per-crate progress lines don't flood + # claude's context (#777). Bypassed when the caller + # already passes --message-format (any form). + cargo() { + # Strip leading +toolchain selectors (cargo +nightly …). + local pre=() + while [ "''${1:0:1}" = "+" ] && [ -n "''${1:-}" ]; do + pre+=("$1") + shift + done + case "''${1:-}" in + build|check|clippy|test|run|doc|bench|install|rustc|fix) + local sub="$1" + shift + local arg + for arg in "$@"; do + case "$arg" in + --message-format|--message-format=*) + command cargo "''${pre[@]}" "$sub" "$@" + return $? + ;; + esac + done + command cargo "''${pre[@]}" "$sub" --message-format short "$@" + ;; + *) + command cargo "''${pre[@]}" "$@" + ;; + esac + } + ''; + }; + environment.etc."hyperhive/bash-allow.json".text = builtins.toJSON config.hyperhive.allowedBashPatterns; @@ -773,8 +852,27 @@ in } // lib.optionalAttrs (config.hyperhive.forge.skipNotifyReasons != [ ]) { HIVE_FORGE_NOTIFY_SKIP_REASONS = lib.concatStringsSep "," config.hyperhive.forge.skipNotifyReasons; + } + // lib.optionalAttrs config.hyperhive.cargo.shortMessages { + # Non-interactive bash invocations (claude's `Bash` tool runs + # `bash -c`) source $BASH_ENV at startup — drops the cargo + # function defined in the file above into scope without + # touching /etc/profile (login-only). Interactive shells + # source the same file via the interactiveShellInit hook + # below so behaviour matches across both modes (#777). + BASH_ENV = "/etc/hyperhive/bash-cargo-short.sh"; }; + # Interactive shells don't honour BASH_ENV — wire the same file + # in via the bashrc hook so operator SSH sessions get the same + # short-format cargo output as claude's non-interactive calls. + programs.bash.interactiveShellInit = + lib.mkIf config.hyperhive.cargo.shortMessages '' + if [ -r /etc/hyperhive/bash-cargo-short.sh ]; then + . /etc/hyperhive/bash-cargo-short.sh + fi + ''; + boot.isNspawnContainer = true; # Every agent gets flakes + the modern `nix` CLI out of the box. From ea5f70629c1185436c0f8941375361d45b741933 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 14:40:46 +0200 Subject: [PATCH 03/43] harness-base: generic bash-env.sh + _bashEnvFragments accumulator (mara on #779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- nix/templates/harness-base.nix | 149 ++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 59 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index ca02087f..e2ba4a3a 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -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 { type = lib.types.bool; default = true; @@ -550,12 +574,12 @@ in the response window with per-crate progress lines that carry no signal beyond the warning/error summary (#777). - Implementation: a `cargo` shell function defined in - `/etc/hyperhive/bash-cargo-short.sh`. Loaded via `BASH_ENV` - for non-interactive shells (`bash -c` — what the claude - `Bash` tool runs) and sourced from `programs.bash.interactiveShellInit` - for interactive shells (operator pokes around inside the - container). The function: + Implementation: contributes a `cargo` shell function to + `/etc/hyperhive/bash-env.sh` (see `hyperhive._bashEnvFragments`). + Loaded via `BASH_ENV` for non-interactive shells (`bash -c` — + what the claude `Bash` tool runs) and sourced from + `programs.bash.interactiveShellInit` for interactive shells. + The function: - handles the `+toolchain` selector prefix (`cargo +nightly build` works); @@ -746,50 +770,55 @@ in source = config.hyperhive.icon; }; - # Cargo `--message-format short` injector (#777). Sourced from - # BASH_ENV in non-interactive shells AND interactiveShellInit - # so both claude's `Bash` tool and operator SSH sessions get - # the same compact compile output. `command cargo …` falls back - # to the un-wrapped binary in PATH (the rust toolchain's cargo - # — either from `environment.systemPackages` or from whatever - # `nix develop` shell the agent's working in). - environment.etc."hyperhive/bash-cargo-short.sh" = - lib.mkIf config.hyperhive.cargo.shortMessages - { - text = '' - # Auto-injects --message-format short on cargo compile - # subcommands so per-crate progress lines don't flood - # claude's context (#777). Bypassed when the caller - # already passes --message-format (any form). - cargo() { - # Strip leading +toolchain selectors (cargo +nightly …). - local pre=() - while [ "''${1:0:1}" = "+" ] && [ -n "''${1:-}" ]; do - pre+=("$1") - shift - done - case "''${1:-}" in - build|check|clippy|test|run|doc|bench|install|rustc|fix) - local sub="$1" - shift - local arg - for arg in "$@"; do - case "$arg" in - --message-format|--message-format=*) - command cargo "''${pre[@]}" "$sub" "$@" - return $? - ;; - esac - done - command cargo "''${pre[@]}" "$sub" --message-format short "$@" - ;; - *) - command cargo "''${pre[@]}" "$@" + # Cargo `--message-format short` injector (#777). Contributes a + # `cargo` shell function to `hyperhive._bashEnvFragments`; the + # bash-env infrastructure below packages that into a single file + # sourced by both non-interactive and interactive shells. + # `command cargo …` falls back to the un-wrapped binary in PATH + # (the rust toolchain's cargo — either from `environment.systemPackages` + # or from whatever `nix develop` shell the agent's working in). + hyperhive._bashEnvFragments = lib.mkIf config.hyperhive.cargo.shortMessages '' + # Auto-injects --message-format short on cargo compile + # subcommands so per-crate progress lines don't flood + # claude's context (#777). Bypassed when the caller + # already passes --message-format (any form). + cargo() { + # Strip leading +toolchain selectors (cargo +nightly …). + local pre=() + while [ "''${1:0:1}" = "+" ] && [ -n "''${1:-}" ]; do + pre+=("$1") + shift + done + case "''${1:-}" in + build|check|clippy|test|run|doc|bench|install|rustc|fix) + local sub="$1" + shift + local arg + for arg in "$@"; do + case "$arg" in + --message-format|--message-format=*) + command cargo "''${pre[@]}" "$sub" "$@" + return $? ;; 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 = builtins.toJSON config.hyperhive.allowedBashPatterns; @@ -853,23 +882,25 @@ in // lib.optionalAttrs (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 - # `bash -c`) source $BASH_ENV at startup — drops the cargo - # function defined in the file above into scope without - # touching /etc/profile (login-only). Interactive shells - # source the same file via the interactiveShellInit hook - # below so behaviour matches across both modes (#777). - BASH_ENV = "/etc/hyperhive/bash-cargo-short.sh"; + # `bash -c`) source $BASH_ENV at startup — drops every active + # feature hook's snippet into scope without touching + # `/etc/profile` (login-only). Interactive shells source the + # same file via the `interactiveShellInit` hook below so + # behaviour matches across both modes (#777). + BASH_ENV = "/etc/hyperhive/bash-env.sh"; }; # Interactive shells don't honour BASH_ENV — wire the same file # 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 = - lib.mkIf config.hyperhive.cargo.shortMessages '' - if [ -r /etc/hyperhive/bash-cargo-short.sh ]; then - . /etc/hyperhive/bash-cargo-short.sh + lib.mkIf (config.hyperhive._bashEnvFragments != "") '' + if [ -r /etc/hyperhive/bash-env.sh ]; then + . /etc/hyperhive/bash-env.sh fi ''; From b07d74b51b18f056f72f55138c3d3a01cc701bca Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 31 May 2026 14:37:43 +0200 Subject: [PATCH 04/43] agent.css + docs: migrate icon sizing + popover :not([hidden]) prose (#713 batch 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent.css carried 17 #NNN cookies, mostly attribution refs to the #394 vibec0re overhaul (mara's full-screen redesign). Most were not substantive WHY-prose — just "this exists because of #394" breadcrumbs. The two genuine WHY-explanations move to docs. Moved to docs/web-ui.md::Per-agent page: - **Agent icon paragraph**: rewrote with explicit-em-sizing rationale (intrinsic dimensions push parent flex container open via align-items: stretch height feedback) + the 5em ≈ 6em min-height - 0.5em padding × 2 derivation + align-self: flex-start sticks-to-top. - **Overflow button paragraph**: added the :not([hidden]) display scoping rationale (UA stylesheet sets display:none on [hidden], but author display:flex would override — scope to :not([hidden]) so the popover stays hidden until JS unhides). Collapsed in agent.css: 17 cookies scrubbed across the file: - #360 (full-screen vibec0re overhaul — closed) ×2: section header preface + side-panel section header - #394 (vibec0re header redesign — closed) ×7: header height, main column, agent icon, meta-nav, overflow trigger, two orphaned-style tombstones - #411 (popover scoping + icon align-start — closed) ×2: icon sticks-to-top + overflow popover scoping - #568 (OAuth code mask + reveal — closed) ×1: show/hide toggle - #666 (ask→operator inline-answer slot — closed) ×1: slot styling (substance partly in PR #780 docs section) - #559 (mark all read header row — closed) ×1 - #376 (inbox row layout — closed) ×1: long-message wrap - #375 (tail pill z-index — closed) ×1: collision fix agent.css: 17 → 0 refs (100% reduction, fully migrated). --- docs/web-ui.md | 21 +++-- frontend/packages/agent/src/agent.css | 120 +++++++++++++------------- 2 files changed, 72 insertions(+), 69 deletions(-) diff --git a/docs/web-ui.md b/docs/web-ui.md index 6c88f02a..7a38849a 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -807,13 +807,17 @@ Three fixed-position layers frame a full-viewport terminal: **Fixed-overlay header** (`
`): frosted glass — `backdrop-filter: blur` lets scrolled terminal rows show -through. Three flex columns (#394 redesign): +through. Three flex columns: - **Agent icon** (``): fixed-size square - identity anchor (5em, `width: 5em; aspect-ratio: 1; - align-self: flex-start` — capped so a tall state-row doesn't - inflate the icon, #411). Falls back to the dimmed hyperhive mark - on load error. + identity anchor — `width: 5em; height: 5em` with explicit pixel + sizing so the ``'s intrinsic (large) dimensions don't push + the parent flex container open via `align-items: stretch`-driven + height feedback. 5em ≈ header content area (header `min-height: 6em` + minus `2 × 0.5em` padding). `align-self: flex-start` keeps the + icon stuck to the top so a state-row line-wrap doesn't drag it + down with it. Falls back to the dimmed hyperhive mark on load + error. - **Main column** (`.agent-header-main`): two rows. - Row 1 (`.agent-header-title-row`): title (`

`) + meta-nav (`